본문 바로가기

기타

JAVA Stream 이해하기

코드를 읽을 수 있는 수준으로 JAVA Stream 이해하기

이번에 연구실에서 알바를 하게 되면서 선배님께서 작성하고 간 Spring 코드를 읽게 되었는데

Stream 구문이 상당히 많았다.

나는 Stream을 제대로 이해를 하고 있지 못했어서 애를 많이 먹었고, 

이제는 코드를 수정하는 일까지 해야하기 때문에 제대로된 이해가 필요했다.

포스팅을 바탕으로 이해해보자.

 

 

 

개요

Stream은 Java 8에 도입되었으며, lambda를 활용하는 기술이다.

기존의 배열과 collection을 다룰 때는 for문과 foreach문을 이용하여 

데이터를 하나하나 꺼내보면서 처리를 해줘야했는데,

이는 로직이 복잡해질수록 코드가 많이 복잡해지는 결과를 초래했다.

 

Stream은 '데이터의 흐름'으로서, 배열과 collection에 함수 여러개를

조합해 적용하여 원하는 결과를 얻을 수 있다.

lambda 식을 이용해서 코드 양을 줄이고 간결하게 처리할 수 있다.

즉, 함수형으로 배열과 collection을 처리할 수 있다.

 

Stream에 대한 내용은 크게 3가지로 나눌 수 있다.

1. 생성하기 : Stream 인스턴스 생성.

2. 가공하기 : Filtering, Mapping 등 원하는 결과를 만들어 가는 중간 작업

3. 결과 만들기 : 최종적으로 결과를 만들어내는 작업


전체 -> mapping1 -> Filtering1 -> mapping2 -> Filtering2  -> ... -> 결과 만들기 -> 결과물

과 같은 형태를 띈다.

 

 

 

생성하기

스트림을 이용하기 위해서는 먼저 생성을 해야한다.

보통 배열과 collection을 이용하여 Stream을 만든다.

이외에도 많은 방법이 있지만 그건 나중에 알아보도록 하자.

스트림은 배열이나 Collection 인스턴스를 이용해서 생성할 수 있다.

 

배열은 다음과 같이 Array.stream 메소드를 이용한다.

Sting[] arr = {"no", "na", "ni", "no", "na"};
Stream<String> stream = Arrays.stream(str);

 

컬렉션은 Collection Interface에 추가된 default method인

stream을 이용해서 만들 수 있다.

즉, 위에서의 배열과 같은 방식을 사용한다는 뜻이다.

public interface Collection<E> extends Iterable<E> {
  default Stream<E> stream() {
    return StreamSupport.stream(spliterator(), false);
  } 
  /* ... */
}

 

그래서 다음처럼 생성할 수 있다.

List<String> list = Arrays.asList("no", "na", "ni", "no", "na");
Stream<String> stream = list.stream();

 

 

 

가공하기

전체 요소를 포함하는 Stream을 생성했다면, 이제 여러 함수들을 통해서

내가 원하는 값만 뽑아낼 수 있다.

이 함수들은 또한 Stream을 리턴하므로 chaning이 가능하다.

  • filter
  • map
  • flatmap
  • sorted
  • peek

 

 

 

Filtering

filter는 Stream 내의 요소들을 하나씩 살펴보며 걸러내는 작업이다.

인자로 받는 Predicate는 boolean을 리턴하는 함수형 인터페이스로서,

걸러낼지의 여부를 판단하는 함수가 들어가게 된다.

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

간단한 예제를 보자.

List<String> list = Arrays.asList("no", "na", "ni", "no", "na");
Stream<String> stream = 
  names.stream()
  .filter(name -> name.contains("i"));
// [ni]

 

 

 

Mapping

map

mapping은 Stream 내의 요소들을 하나씩 특정 값으로 변환한다.

이때 값을 변환하기 위한 람다식을 인자로 받는다.

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

 

간단한 예제를 보자.

List<String> list = Arrays.asList("no", "na", "ni", "no", "na");
Stream<String> stream = 
  names.stream()
  .map(String::toUpperCase);
// [NO, NA, NI, NO, NA]

 

flatMap

flatMap은 중첩구조를 한 단계 제거하고 단일 Collection으로 만들어주는 역할을 한다.

정의는 다음과 같다..... 함수형 인터페이스와 generic을 좀 더 이해하고 오자...

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

 

List<List<String>> list = 
  Arrays.asList(Arrays.asList("no"), 
                Arrays.asList("na"), 
                Arrays.asList("ni"),
                );
// [[no], [na], [ni]]
List<String> flatMappedList = 
  list.stream()
  .flatMap(Collection::stream)
  .collect(Collectors.toList());
// [no, na, ni]

collect는 이후 결과만들기에서 뭐하는 친구인지 확인해보자.

 

 

 

Sorting

정렬은 Comparator를 넘겨주면 된다. 인자를 넘겨주지 않은 기본값은 오름차순이다.

Stream<T> sorted();
Stream<T> sorted(Comparator<? super T> comparator);

 

List<String> list = 
  Arrays.asList("no", "na", "ni", "no", "na");

list.stream()
  .sorted()
  .collect(Collectors.toList());
// [na, na, ni, no, no]

list.stream()
  .sorted(Comparator.reverseOrder())
  .collect(Collectors.toList());
// [no, no, ni, na, na]

 

 

 

Iterating

Stream 내의 요소들을 대상으로 특정 연산을 수행하는 peek 메소드가 있다.

리턴이 없는 Consumer라는 함수형 인터페이스를 인자로 받는다.

Stream<T> peek(Consumer<? super T> action);

 

int sum = IntStream.of(1, 2, 3, 4, 5)
  .peek(System.out::println)
  .sum();
  

<console output>
1
2
3
4
5

 

 

 

결과만들기

가공하기 단계를 마친 Stream을 가지고 내가 원하는 결과값을 만들어내는 단계이다.

  • Calculate
  • Reduce
  • Collect
  • Match
  • Iterate

 

 

 

Calculate

다양한 종료 값을 계산할 수 있다

  • min
  • max
  • sum
  • average
long count = IntStream.of(1, 2, 3, 4, 5).count(); //5
long sum = LongStream.of(1, 2, 3, 4, 5).sum(); //15

 

 

 

Reduce

reduce 메소드는 세 가지의 파라미터를 받을 수 있다.

  • accumulator : 각 요소를 처리하며 중간 결과를 생성함.
  • identity : 계산을 위한 초기값. 스트림이 비어있어 계산할 내용이 없더라도 이 값을 리턴함.
  • combiner : 병렬 스트림에서 나눠 계산한 결과를 하나로 합치는 로직.

병렬스트림의 내용까지는 몰라도 될 것 같다.

// 인자가 1개 (accumulator)
Optional<T> reduce(BinaryOperator<T> accumulator);

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

 

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

 

int totalWithTwoPrams = 
  IntStream.range(1, 7) // [1, 2, 3, 4, 5, 6]
  .reduce(10, Integer::sum); // method reference
  //31

 

 

 

Collect

Collectors 객체에서 제공하는 메소드로 인자를 넘겨주면 된다.

  • toList : Stream에서 작업한 결과를 리스트로 반환한다.
  • joining : Stream에서 작업한 결과를 하나의 스트링으로 이어 붙인다.
  • averageingInt : Stream에서 작업한 결과 중 숫자 값의 평균을 낸다.
  • summingInt : Stream에서 작업한 결과의 합을 낸다.
  • summarizingInt : Stream에서 작업한 결과로 평균과 합을 낸다.
  • groupingBy : Stream에서 작업한 결과를 특정 조건으로 묶을 수 있다. Map 타입을 리턴한다.
List<String> list = Arrays.asList("no", "na", "ni", "no", "na");
Map<String, List<String>> map = 
	list.stream()
    .collect(Collectors.groupingBy(s -> s.substring(1,2)));
//{a=[na, na], i=[ni], o=[no, no]}
  • partitioningBy : groupingBy와 유사하지만 true, false로만 구분한다(predicate 이용)
  • collectingAndThen : collect 이후에 추가작업이 필요한 경우에 사용할 수 있다.
List<String> list = Arrays.asList("no", "na", "ni", "no", "na");
Set<String> set =
        list.stream()
                .collect(Collectors.collectingAndThen(Collectors.toSet(),
                        Collections::unmodifiableSet));
//[no, na, ni]

 

 

 

Match

Predicate를 넘겨 매칭되는 개수를 따지는 메소드들이 있다.

boolean anyMatch(Predicate<? super T> predicate);
boolean allMatch(Predicate<? super T> predicate);
boolean noneMatch(Predicate<? super T> predicate);

 

 

 

Iterate

앞서 나왔던 peek와 유사하게 foreach 메소드가 존재한다. 중간 작업이냐 최종작업이냐의 차이.

List<String> list = Arrays.asList("no", "na", "ni", "no", "na");
list.stream().forEach(System.out::println);

<console output>
no
na
ni
no
na

 

 

 

기타 메소드들

ifPresent

Optional의 method, 객체가 값을 갖고 있다면 true, 아니면 false 리턴

 

boxed

intStream과 같이 원시타입에 대한 Stream 지원이 존재하는데,

얘를 Stream<Integer>와 같이 전형적인 클래스 타입으로 변환한다

 

collect를 이용해 List Collection에 담으려면 원시타입인 int가 아니라 Integer를 써야하기 때문에

이런 경우에 이용한다고 할 수 있겠다

 

range

IntStream, LongStream에 존재하며 시작값과 종료값을 인자로 받아서

그 범위의 숫자를 순서대로 가지는 Stream을 반환한다.

range 메소드는 종료값을 포함하지 않고

rangeClosed 메소드는 종료값을 포함한다.

 

of

Stream을 바로 생성할 수 있게 해준다.

 

참고자료