본문 바로가기
Java

Stream 사용하기(2) - 중간 연산의 종류와 방법

by GGShin 2022. 5. 20.

앞선 포스팅에서 배열이나 collection에 담겨있는 데이터들을 가공하기 위한 사전 작업인

stream 생성 방법에 대해 자세히 알아보았습니다. 이제는 드디어 가공을 해 볼 차례입니다 

도표를 보시면 연산 종류에 따라서 어떤 method들이 있는지 나와있는데요,

이번에 알아볼 중간 연산에 해당하는 methods는 filter(), map(), peek(), boxed()가 있네요! 여기에 sorted(), distinct() 까지 더해서 알아보겠습니다.

출처: https://java-8-tips.readthedocs.io/en/stable/streams.html

참고로 중간 연산은 Stream type을 반환합니다. 그래서 IDE에서 추천(?) 코드 중에 반환 타입이 Stream<T>이나 IntStream등 Stream type인 경우는 최종 연산이 아닌 중간 연산 method라고 보시면 됩니다.

 

1. filter()

filter()는 말 그대로 데이터들을 걸러주는 역할을 합니다. 어떤 기준으로 걸러줄 지는 조건을 설정해주면 됩니다!

만약에 IntStream에서 짝수들만 남기고 싶다면 이런식으로 코드를 작성할 수 있습니다.

 

int[] intArr = {1, 2, 3, 4, 5, 6};
IntStream intStream = Arrays.stream(intArr);

//filter() 사용        
IntStream filteredStream = intStream.filter(num -> num % 2 == 0);

 

filter()의 괄호 안에 조건을 작성해주면 되는데, 람다(Lambda) 표현이 사용됩니다. 

생소하지만 enhanced for loop을 사용할 줄 안다면 독해하기가 크게 어렵지 않을 것 같습니다. num은 stream 안의 요소 하나하나를 지칭하기 위한 단어이고(임의로 아무거나 붙여도 됩니다.), "->" 표시 이후에 어떻게 각 요소를 가공할 지 적어주면 되는 것입니다.

 

*코드를 해석해 보면 : 저는 짝수를 찾기로 했으므로, Stream이 생성된 intArr의 요소들인 1, 2, 3, 4, 5, 6을 num으로 지칭하며 하나씩 돌아가면서 % 2 연산 값이 0인지 확인하게 하고, 조건에 부합하는 요소들은 새로운 Stream으로 반환 받았습니다. 그리고 그 반환 받은 Stream은 filteredStream이라는 변수에 할당해주었습니다. 

 

 

이 외에도 .filter(num -> (num > 3)) 과 같이 3보다 큰 수만 필터링 할 수도 있고 여러 가지 조건을 적용해 볼 수 있습니다.

for loop을 작성하지 않아도 간략한 코드로 같은 효과를 내주니 정말 편리합니다 😲

 

2. distinct()

이 method는 중복된 값을 제거해줍니다. distinct는 "구별된" 이라는 의미인데, 다른 요소들과 같은 값을 갖지 않는 요소들만 추려낸다는 의미로 distinct라고 붙인 것 같습니다. 

저는 이 distinct() method를 보고 행복했습니다.. 머리를 써서 코드를 작성하지 않아도 중복된 데이터들을 제거해준다니,,👏,,

유용하게 쓸 수 있을 것 같았습니다 ㅎㅎ

 

ArrayList<String> stringArrayList = new ArrayList<>(){{
            add("H");
            add("H"); //"H"를 한 번 더 add함
            add("F");
            add("P");
        }};
        
Stream<String> arrListStream = stringArrayList.stream();

arrListStream.distinct()
             .forEach(System.out::println);

 

stringArrayList에 "H"가 두번 들어간 상태인데, .distinct()를 쓰면 어떻게 될까요?

 

 

H가 한번만 프린트된 것을 보실 수 있습니다!

 

*위 코드에서 .forEach(System.out::println);은 최종 연산 부분인데요, 각 요소들을 줄바꿈하여 프린트하라는 의미입니다. 

 

3. sorted()

sorted()를 사용하면 natural order(오름차순)로 데이터를 분류하여 줍니다. 

 

Integer[] myArr = {11, 2, 31, 4, 57, 3, 9, 32};
Stream<Integer> myArrStream = Arrays.stream(myArr);

myArrStream.sorted()
      .forEach(System.out::println);

 

콘솔에 숫자들이 오름차순으로 분류되어 나온 것을 확인할 수 있습니다.

 

 

만약에 다른 방식으로 분류하고 싶으면 어떻게 할까요?

.sorted() 괄호 안에 원하는 분류 방법을 넣어주면 됩니다. 예를 들어서 내림차순을 원한다면 .sorted(Comparator.reverseOrder()) 를 사용하면 됩니다.

 

Integer[] myArr = {11, 2, 31, 4, 57, 3, 9, 32};
Stream<Integer> myArrStream = Arrays.stream(myArr);
        
myArrStream.sorted(Comparator.reverseOrder())
           .forEach(System.out::println);

 

이렇게 하면 큰 수부터 작은 수로 프린트되어 나오게 됩니다.

 

primitive type의 wrapper class들은 기본적으로 Comparable를 implements하고 있기 때문에 위에서처럼 오름차순이나 내림차순으로 sorte를 할 수 있는 것입니다. 만약에 Stream에 들어있는 데이터가 Comparable을 implements하지 않는 class type의 instance들이라면 런타임에 java.lang.ClassCastException을 발생시킨다고 합니다.

 

4. map()

map()은 Stream 내의 각 요소마다 특정한 작업을 수행한 후 새로운 Stream을 반환합니다. 아래의 그림에서처럼 map을 거치면 네모 모양에서 동그라미가 되듯이 각 자리에 있는 요소의 값 또는 타입이 바뀌게 됩니다. 

출처:&nbsp;https://www.java67.com/2015/01/java-8-map-function-examples.html

 

어떤 작업을 해줄 지는 .map() 괄호 안에 람다식으로 명시해주면 됩니다.

 


map의 의미:
프로그래밍에서 map이란 요소들의 묶음에 특정한 작업을 수행하거나 한 요소들의 묶음과 다른 묶음을 matching 하는 것을 의미합니다. ASCII code에서 알파벳과 숫자를 연결(matching)한 것도 mapping의 예라고 볼 수 있습니다.

Integer[] myArr = {11, 2, 31, 4, 57, 3, 9, 32};
Stream<Integer> myArrStream = Arrays.stream(myArr);

myArrStream.map(num -> num * 10)
            .forEach(System.out::println);

myArrStream에 있는 각 요소에 곱하기 10을 한 후 반환받고 각 요소를 콘솔에 프린트하도록 코드를 작성하였습니다.

 

 

결과가 의도한 대로 잘 나온 것을 확인할 수 있습니다.

 

앞서 얘기한 것처럼, 중간 연산은 여러번 수행할 수 있습니다. map으로 각 요소에 10을 곱한 것은 좋은데, 오름차순으로 정렬하고 싶다면 sorted()도 같이 사용하면 됩니다.

 

myArrStream.map(num -> num * 10)
           .sorted()
           .forEach(System.out::println);

 

이렇게 하면 조금더 보기 편하게 숫자들이 정렬까지 되어 나왔습니다!

 

 

mapToInt(), mapToDouble(), mapToLong() methods도 있는데요, 각각 Intstream, DoubleStream, LongStream의 형태로 반환한다는 점에서 map과 차이가 있습니다. 하지만 수행하는 기능은 모두 똑같습니다. 

 

5. flatMap()

flatMap을 이해하려면 'flat'이 무슨 의미인지 알면 좋습니다. Flat은 '평평한'이라는 뜻이고, 이 flatMap은 복수의 array나 collections을 '평평하게(flattening)' 만들어줍니다. 즉, flatMap은 2D array나 여러개의 array 또는 collections을 하나로 합쳐주는 역할을 합니다. flatMap() 종류에는 데이터의 타입에 따라서 flatMapToInt(),flatMapToDouble, flatMapToLong도 있습니다.

 

예를 들어서 아래와 같은 String[] 을 담고 있는 List를 생각해봅시다.

String[] strings = {"A", "B", "C", "D"};
String[] strings1 = {"E", "F", "G"};
String[] strings2 = {"H", "I"};

List<String[]> list1 = Arrays.asList(
		 strings,
                strings1,
                strings2
);

 

list1은 이런 형태를 띄고 있을 겁니다.

{ [A, B, C, D], [E, F, G], [H, I] }

 

이때 flatMap()을 사용하면 세개의 array에 담긴 요소들을 하나로 합쳐서 작업할 수 있게 됩니다. 

 

  //flat map
 list1.stream()
 		//list1에 들어있는 각각의 array들을 다시 한 번 stream 해줌
         .flatMap(innerArr -> Arrays.stream(innerArr))
         .forEach(System.out::print);

이렇게 하여 .forEach로 각 요소들을 프린트해 보면 ABCDEFGHI 와 같이 요소들이 하나로 합쳐졌다는 것을 확인할 수 있습니다.

 

이번에는 한 번 2D array에 flatMap()을 사용해보겠습니다.

 

//2D array
int[][] ints = new int[][] {
                {1, 2}, {3,4}, {1,5}
        };

//flatMap 사용
Arrays.stream(ints)
      .flatMapToInt(inner -> Arrays.stream(inner))
      .map(n -> n*2)
      .forEach(System.out::print);

int type의 배열이기 때문에 .flatMapToInt를 사용해주었습니다. 그리고 .map()을 이용해서 각 요소에 2씩 곱하여주고 각 요소를 printline 해주었습니다. 

결과가 잘 나왔네요!

 

6. peek() 

peek()은 현재 Stream의 내용을 그대로 다음 새로운 stream을 return해 주는 중간 연산자입니다. 중간 연산은 여러번 진행이 가능하다고 얘기드렸는데, 그러다 보니 중간에 peek()을 사용해서 원하는대로 값이 가공되고 있는지 확인하는 용도로 사용됩니다. Debugging(디버깅) 이라고 하죠. 

 

int[][] ints = new int[][] {
                {1,2}, {3,4}, {5,6}
        };

Arrays.stream(ints)
       .flatMapToInt(n-> Arrays.stream(n))
       .peek(n -> System.out.println("Check: "+n))
       .map(n -> n*2)
       .forEach(System.out::println);

 

요렇게 중간에 peek()을 사용해서 어떻게 콘솔에 나오는지 확인해보았는데요, 어떻게 결과가 나왔을 거 같으신가요?

이렇게 프린트가 되었습니다. 

 

저는 사실 

 

Check: 1

...

Check: 6   //peek에 명시해둔 println 끝나고

2

4

...

12  //최종 연산자에 있는 println 끝나고

 

이런식으로 프린트되어 나올 것이라고 예상했는데, 결과값을 보고 좀 더 정확히 stream이 돌아가는 방식을 알게된 것 같았습니다. 

중간연산자를 요소들이 이미 다 거친 후에 결과 연산이 시작되면 하나씩 출력되는 줄 알았는데,

정말 for loop처럼 요소 하나씩 중간 연산과 결과 연산이 이루어지는, loop를 도는 구조라는 것을 알게되었습니다. 

 

7. boxed() 📦 

boxed()는 IntStream, DoubleStream, LongStream을 위한 중간 연산입니다. 이 세가지 stream은 일반 Stream에 사용이 가능한 .collect(Collectors.toList()) 같은 몇 가지 연산을 할 수가 없습니다. 그래서 일반 Stream과 같이 사용될 수 있도록 해주는 중간 연산이 바로 boxed()입니다!

 

int[][] ints = new int[][] {
          {1,2}, {3,4}, {5,6}
};

List<Integer> a = Arrays.stream(ints)
                .flatMapToInt(n-> Arrays.stream(n))
                .peek(n -> System.out.println("Check: "+n))
                .map(n -> n*2)
                .collect(Collectors.toList()); //compile error 발생

위 예제에서 .map() 부분까지 거치면 return값이 IntStream형태로 되는데, 이때 바로 .collect() 최종연산이 불가합니다. 

최종 연산 바로 전에 .boxed()를 넣어주면 문제 없이 .collect()를 적용할 수 있게 됩니다.

 

 List<Integer> a = Arrays.stream(ints)
                .flatMapToInt(n-> Arrays.stream(n))
                .peek(n -> System.out.println("Check: "+n))
                .map(n -> n*2)
                .boxed() //boxed 사용
                .collect(Collectors.toList());

 

여기까지 자주 사용되는 중간 연산의 종류와 사용방법에 대해 쭉 알아보았습니다. 

저도 아직은 익숙치 않지만 이 역시 자주 사용하면서 감을 익혀야겠습니다. 확실한 건 직관적이라 금방 익숙해질 수 있을 것 같습니다.

감사합니다 ☺️

 


참고자료

https://howtodoinjava.com/java8/stream-flatmap-example/

 

Java Stream flatMap() with Examples - HowToDoInJava

Learn to use Java Stream flatMap() method which is used to flatten a stream of collections to a stream of elements combined from all collections.

howtodoinjava.com

https://howtodoinjava.com/java8/java8-boxed-intstream/

 

Boxed Streams in Java - Stream of Primitives

In java 8, to convert a stream of primitives to collection, you must first box the elements in their wrapper class and then collect them i.e. boxed stream.

howtodoinjava.com

https://madplay.github.io/post/difference-between-map-and-flatmap-methods-in-java

 

자바 map 메서드와 flatMap 메서드의 차이

자바 8에서 추가된 map 메서드와 flatMap 메서드의 차이는 무엇일까?

madplay.github.io

반응형