java 스트림

조항주·2022년 7월 2일

study

목록 보기
18/20
post-thumbnail

스트림이란?

Collection이나 Iterator 같은 인터페이스를 이용해서 컬렉션을 다루는 방식을 표준화 했지만, 각 컬렉션 클래스에는 같은 기능의 메서드들이 중복해서 정의되어 있다. List를 정렬할 때는 Collection.sort()를 사용해야하고, 배열을 정렬할 때는 Arrays.sort()를 사용해야 한다. 이렇게 데이터 소스마다 다른 방식으로 다루어야하는 문제점을 해결해주는 것이 Stream 이다.
스트림은 데이터 소스를 추상화하고, 데이터를 다루는데 자주 사용되는 메서드들을 정의해 놓았다.

// 기존
String[] strArr = {"aaa", "bbb", "ccc"};
List<String> strList = Arrays.asList(strArr);

// 스트림 생성
Stream<String> strStream1 = strList.stream();
Stream<String> strStream2 = Arrays.stream(strArr);

// 스트림 출력
strStream1.sorted().forEach(System.out::println);
strStream2.sorted().forEach(System.out::println);

스트림은 데이터 소스로 부터 데이터를 읽기만 할 뿐, 변경하지 않는다.

필요하다면 결과를 컬렉션이나 배열에 담아서 변환할 수도 있다

List<Integer> collect = intStream.sorted().collect(Collectors.toList());

스트림은 한번 사용하면 닫혀서 다시 사용할 수 없다.

List<Integer> list= Arrays.asList(1,2,3,4,5);
Stream<Integer> intStream=list.stream();
intStream.forEach(System.out::println);
intStream.forEach(System.out::println); //에러

스트림은 작업을 내부 반복으로 처리한다.

내부 반복이란 반복문을 메서드의 내부에 숨길 수 있다는 것을 의미한다. forEach()는 스트림에 정의된 메서드 중의 하나로 매개변수에 대입된 람다식을 데이터 소스의 모든 요소에 적용한다.

 void forEach(Consumer<? super T> action) {
    Objects.requireNonNull(action); // 매개변수의 널 체크
    
    for(T t : src)  {
      action.accept(T);
    }
  }

스트림의 연산

스트림이 제공하는 연산은 중간 연산과 최종 연산이 있다.

중간 연산 - 연산 결과가 스트림인 연산. 스트림에 연속해서 중간 연산할 수 있음
최종 연산 - 연산 결과가 스트림이 아닌 연산. 스트림의 요소를 소모하므로 단 한번만 가능

지연된 연산

스트림 연산에서는 최종 연산이 수행되기 전까지는 중간 연산이 수행되지 않는다. 스트림에 대해 distinct()나 sort()같은 중간 연산을 호출해도 즉각적인 연산이 수행되는 것은 아니다.

중간 연산을 호출하는 것은 단지 어떤 작업이 수행되어야 하는지를 지정하는 것일 뿐, 최종 연산이 수행되어야 비로소 스트림의 요소들이 중간 연산을 거쳐 최종 연산에서 소모된다.

병렬 스트림

병렬 스트림은 내부적으로 fork&join프레임웍을 이용해서 자동적으로 연산을 병렬로 수행한다.

스트림에 parallel()이라는 메서드를 호출해서 병렬로 연산을 수행하거나, sequential()을 호출해 병렬로 처리되지 않게 할 수 있다.

모든 스트림은 기본적으로 병렬 스트림이 아니므로 sequential()을 호출할 필요가 없다. 이 메서드는 parallel()을 호출한 것을 취소할 때만 사용한다.

  • parallel()과 sequential()은 새로운 스트림을 생성하는 것이 아니라, 스트림의 속성을 변경한다.

스트림 만들기

스트림의 소스가 될 수 있는 대상은 배열, 컬렉션, 임의의 수 등이 있다.

컬렉션

컬렉션의 최고 조상인 Collection에 stream()이 정의되어 있다.

그래서 Collection의 자손인 List와 Set을 구현한 컬렉션 클래스들은 모두 이 메서드로 스트림을 생성할 수 있다.

stream()은 해당 컬렉션을 소스(source)로 하는 스트림을 반환한다.

List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);	//가변인자
Stream<Integer> intStream = list.stream();	//list를 소스로 하는 컬렉션 생성

배열

배열을 소스로 하는 스트림을 생성하는 메서드는 Stream과 Arrays에 static메서드로 정의되어 있다.

Stream<T> Stream.of(T... values)	//가변 인자
Stream<T> Stream.of(T[])
Stream<T> Arrays.stream(T[])
Stream<T> Arrays.stream(T[] array, int startInclusive, int endExclusive)

//문자열 스트림 생성
Stream<String> strStream = Stream.of("a", "b", "c");	//가변 인자
Stream<String> strStream = stream.of(new String[]{"a", "b", "c"});
Stream<String> strStream = Arrays.stream(new String[]{"a", "b", "c"});
Stream<String> strStream = Arrays.stream(new String[]{"a", "b", "c"}, 0, 3);

//기본형 배열을 소스로 하는 스트림 생성
IntStream IntStream.of(int... values)	//Stream이 아니라 IntStream
IntStream IntStream.of(int[])
IntStream Arrays.stream(int[])
IntStream Arrays.stream(int[] array, int startInclusive, int endExclusive)

특정 범위의 정수

IntStream과 LongStream은 지정된 범위의 연속된 정수를 스트림으로 생성해서 반환하는 range()와 rangeClosed()를 가지고 있다.
range()의 경우 경계의 끝인 end가 범위에 포함되지 않고, rangeClosed()의 경우는 포함된다.

IntStream IntStream.range(int begin, int end)
IntStream IntStream.rangeClosed(int begin, int end)

//range()의 경우 경계의 끝인 end가 범위에 포함되지 않고,
//rangeClosed()의 경우는 포함된다.
IntStream intStream = IntStream.range(1, 5);	//1, 2, 3, 4
IntStream intStream = IntStream.rangeClosed(1, 5);	//1, 2, 3, 4, 5

임의의 수

난수를 생성하는데 사용하는 Random클래스에는 해당 타입의 난수들로 이루어진 스트림을 반환하는 인스턴스 메서드들이 포함되어 있다.

IntStream ints()
LongStream longs()
DoubleStream doubles()
IntStream intStream = new Random().ints();	//무한 스트림
intStream.limit(5).forEach(System.out::println);	//5개의 요소만 출력한다.

//아래의 메서드들은 매개변수로 스트림의 크기를 지정해서 유한 스트림을 생성해 반환하므로
//limit()을 사용하지 않아도 된다.
IntStream ints(long streamSize)
LongStream longs(long streamSize)
DoubleStream doubles(long streamSize)

IntStream intStream = new Random().ints(5);	//크기가 5인 난수 스트림을 반환

람다식 - iterate(), generate()

Stream클래스의 iterate()와 generate()는 람다식을 매개변수로 받아서, 이 람다식에 의해 계산되는 값들을 요소로 하는 무한 스트림을 생성한다.

iterate()는 씨앗값(seed)으로 지정된 값부터 시작해서,
람다식 f에 의해 계산된 결과를 다시 seed값으로 해서 계산을 반복한다.


//0부터 시작해서 값이 2씩 계속 증가
Stream<Integer> evenStream = Stream.iterate(0, n -> n+2);	//0, 2, 4, 6, ...

generate()도 iterate()처럼, 람다식에 의해 계산되는 값을 요소로 하는 무한 스트림을 생성해서 반환하지만, iterate()와 달리, 이전 결과를 이용해서 다음 요소를 계산하지 않는다.

Stream<Double> randomStream = Stream.generate(Math::random);
Stream<Integer> oneStream = Stream.generate(() -> 1);

빈 스트림

요소가 하나도 없는 비어있는 스트림을 생성할 수 있다.

스트림에 연산을 수행한 결과가 하나도 없을 경우, null이 아닌 빈 스트림을 반환하는 방법을 쓰는 것이 좋다.

Stream emptyStream = Stream.empty();	//empty()는 빈 스트림을 생성해서 반환한다.
//count()는 스트림 요소의 개수를 반환한다.
long count = emptyStream.count();	//count의 값은 0

두 스트림의 연결

Stream의 static메서드인 concat()을 사용하면, 두 스트림을 하나로 연결할 수 있다.

연결하려는 두 스트림의 요소는 같은 타입이어야 한다.

String[] str1 = {"123", "456", "789"};
String[] str2 = {"ABC", "abc", "DEF"};

Stream<String> strs1 = Stream.of(str1);
Stream<String> strs2 = Stream.of(str2);
Stream<String> strs3 = Stream.concat(strs1, strs2);	//두 스트림을 하나로 연결

0개의 댓글