SMALL

Stream이란 

Stream 구조 및 메서드 소개

  • 생성
  • 중간 연산자
  • 최종 연산자 

Stream 특징


Stream이란 

스트림(Streams)은 자바 8에서 추가되었고, 람다를 활용할 수 있는 기술 중 하나입니다.

Stream은 '데이터의 흐름’입니다.

배열 또는 컬렉션 인스턴스에 함수 여러 개를 조합해서 원하는 결과를 필터링하고 가공된 결과를 얻을 수 있습니다. 

또한 람다를 이용해서 코드의 양을 줄이고 간결하게 표현할 수 있습니다. 

즉, 배열과 컬렉션을 함수형으로 처리할 수 있습니다.


Stream 구조 및 메서드 소개 

Stream 구조는 크게 3가지로 나뉜다. 

스트림 생성->  중개 연산 ->  최종 연산

실제 사용법으로 표기하면 "Collections 같은 객체 집합.스트림생성().중개연산().최종연산();" 이런 식이다.

( 계속해서 . 으로 연계할 수 있게 하는 방법을 파이프라인이라고도 한다 )

 

그럼 다음으로 각각의 구조에 해당하는 연산자들이 어떠한 것들이 있는지 대표적으로 몇 가지를 살펴보자 

 

스트림 생성

보통 배열과 컬렉션을 이용해서 스트림을 만든다.

이 외에도 다양한 방법으로 스트림을 만들 수 있습니다.

String[] arr = new String[]{"a", "b", "c"};
Stream<String> stream = Arrays.stream(arr);

List<String> list = Arrays.asList("a", "b", "c");
Stream<String> stream = list.stream();

 

Stream.builder()
빌더(Builder)를 사용하면 스트림에 직접적으로 원하는 값을 넣을 수 있습니다. 

마지막에 build 메서드로 스트림을 리턴합니다.

 

Stream.generate()
generate 메서드를 이용하면 Supplier<T> 에 해당하는 람다로 값을 넣을 수 있습니다. 

Supplier<T> 는 인자는 없고 리턴 값만 있는 함수형 인터페이스 -> Supplier에서 리턴하는 값이 Stream으로 들어갑니다. 

 * 이때 생성되는 스트림은 크기가 정해져 있지 않고 무한(infinite) 하기 때문에 특정 사이즈로 최대 크기를 제한 * 

 

Stream.iterate()
iterate 메서드를 이용하면 초기값과 해당 값을 다루는 람다를 이용해서 스트림에 들어갈 요소를 만듭니다. 

Stream 생성 시, 요소가 다음 요소의 인풋으로 들어갑니다.

 * 이때 생성되는 스트림은 크기가 정해져 있지 않고 무한(infinite) 하기 때문에 특정 사이즈로 최대 크기를 제한 * 

Stream<String> builderStream = 
  Stream.<String>builder()
    .add("Eric").add("Elena").add("Java")
    .build(); 
//output - [Eric, Elena, Java]

Stream<String> generatedStream = 
  Stream.generate(() -> "el").limit(5);
//output - [el, el, el, el, el]

Stream<Integer> iteratedStream = 
  Stream.iterate(30, n -> n + 2).limit(5); 
//output - [30, 32, 34, 36, 38]

중간 연산 

값을 원하는 형태로 처리하기 위한 연산자이다. 

각각의 중간 연산자 결과로 stream을 반환한다.

그렇기 때문에 중간 연산자는 method chaining 형태로 연결하여 처리할 수 있다.

연산의 결과가 stream으로 반환되기 때문에 stream-producing 연산자라고 부르기도 한다.

 

대표적인 메서드를 예시로 몇 개 알아보도록 하자 

 

filter()
원하는 요소만 추출하기 위한 메서드이다. 인자로는 Predicate를 받는데, boolean값을 반환하는 람다식을 넣으면 된다.

Stream<T> filter(Predicate<? super T> predicate);

List<String> names = Arrays.asList("Hello", "World", "Test", "array");
List<String> filteredNames = names.stream()
        .filter(it -> it.contains("e"))
        .collect(Collectors.toList());
// output - ["Hello", "Test" ]        

 

map()
스트림 내 요소를 가공한다. 

mapper를 간단히 설명하자면, T를 인자로 받아 변환한 값 R을 반환하는 함수이다. 

이는 람다식으로 간단히 표현할 수 있다.

<R> Stream<R> map(Function<? super T, ? extends R> mapper);

Arrays.asList("Hello", "World", "Test", "array");
        .stream()
        .map(String::toUpperCase)
        .forEach(System.out::println);
// output - [ "HELLO", "WORLD", "TEST", "ARRAY" ]        

 

flatMap() 
중첩 구조를 한 단계 제거하고 단일 컬렉션으로 만들어 주는 역할을 한다.

이러한 작업을 flattening이라고 한다.
map과 가장 큰 차이는 함수의 반환 값이 stream 형태라는 것이다. 

이는 map만으로 처리하면 복잡해지는 코드를 간결하게 만들어준다.

<R> Stream<R> flatMap(Function<? super T, ? extends Stream<? extends R>> mapper);

int arr[][] = {{1, 2, 3}, {4, 8}, {9, 10, 20}, {11, 22}};
Stream.of(arr)
        .flatMapToInt(IntStream::of)
        .forEach(System.out::println);

 

최종 연산

가공한 스트림을 가지고 내가 사용할 결괏값으로 만들어내는 단계입니다.

따라서 스트림을 끝내는 최종 작업(terminal operations)입니다.

스트림의 요소를 소모해서 결과를 만들어낸다. 따라서 최종 연산 후에는 스트림이 닫히게 되고 더 이상 사용할 수 없다.

 

대표적인 메서드를 예시로 몇 개 알아보도록 하자 

 

reduce() 
스트림은 reduce라는 메서드를 이용해서 결과를 만들어냅니다. 

다음은 reduce 메서드는 총 세 가지의 파라미터를 받을 수 있습니다.

  • accumulator : 각 요소를 처리하는 계산 로직. 각 요소가 올 때마다 중간 결과를 생성하는 로직.
  • identity : 계산을 위한 초기값으로 스트림이 비어서 계산할 내용이 없더라도 이 값은 리턴.
  • combiner : 병렬(parallel) 스트림에서 나눠 계산한 결과를 하나로 합치는 동작하는 로직.
// 1개 (accumulator)
Optional<T> reduce(BinaryOperator<T> accumulator);

// 2개 (identity)
T reduce(T identity, BinaryOperator<T> accumulator);

// 3개 (combiner)
<U> U reduce(U identity, BiFunction<U, ? super T, U> accumulator, BinaryOperator<U> combiner);

인자가 하나만 있는 경우입니다. 

여기서 BinaryOperator<T> 는 같은 타입의 인자 두 개를 받아 같은 타입의 결과를 반환하는 함수형 인터페이스입니다.

OptionalInt reduced = 
  IntStream.range(1, 4) // [1, 2, 3]
  .reduce((a, b) -> {
    return Integer.sum(a, b);
  }); // output - 6

두 개의 인자를 받는 경우입니다. 

여기서 첫 번째 인자는 초기값이고, 스트림 내 값을 더해서 결과는 16(10 + 1 + 2 + 3)이 됩니다. 

int reducedTwoParams = 
  IntStream.range(1, 4) // [1, 2, 3]
  .reduce(10, Integer::sum); 
// output - 16

마지막으로 세 개의 인자를 받는 경우입니다. 
Combiner는 병렬 처리 시 각자 다른 스레드에서 실행한 결과를 마지막에 합치는 단계입니다. 따라서 병렬 스트림에서만 동작합니다.

 

collect()  

collect 메서드는 또 다른 종료 작업입니다. Collector 타입의 인자를 받아서 처리를 하는데요, 자주 사용하는 작업은 Collectors 객체에서 제공하고 있습니다.

 

해당 메서드에서 인자로 받는 Collector 타입의 예제를 몇 개 보도록 하자

 

Collectors.toList()

스트림에서 작업한 결과를 담은 리스트로 반환합니다.

List<String> collectorCollection =
  productList.stream()
    .map(Product::getName)
    .collect(Collectors.toList());
// [potatoes, orange, lemon, bread, sugar]

Stream 특징

 

Stream은 재사용이 불가능하다.

한 번 사용한 스트림에 대해서 다시 사용하려고 하면 에러가 난다.

Stream<String> a = names.stream().filter(x -> x.contains("o"));
count = a.count();
List<String> lists = a.collect(Collectors.toList()); // error ! 

병렬 스트림은 여러 스레드가 작업한다.

stream()으로 스트림을 생성하지 않고 위처럼 parallelStream()으로 병렬 스트림을 만들 수 있다.

이렇게 하면 여러 스레드가 스트림에서 요소를 필터링하고 나온 요소 수를 계산하고 스레드끼리 다시 한번 각자 계산한 count 값들을 더해서 리턴해준다.

위에서 설명한 최종 연산의 reduce 메서드와 같이, 우리가 의도한 바와 다른 형태로 동작할 수 있으니 잘 생각해서 사용해야 한다.

 

중개 연산은 미리 하지 않는다 -> 지연 연산을 한다.
최종 연산이 적용될 때 중개 연산도 실행된다.
이로써 얻는 장점은 미리 계산하면서 두 번 순회하는 짓을 안 할 수 있게 된다는 점이다.

 

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

필요하다면, 정렬된 결과를 컬렉션이나 배열에 담아서 반환할 수 있다.

 

 

참고 블로그 :)

 

LIST

+ Recent posts