본문 바로가기
Java

Stream 사용하기(3) - 최종 연산의 종류와 방법

by GGShin 2022. 5. 21.

드디어 stream 가공과 사용의 마지막 단계인 최종 연산에 왔습니다. 🥳🥳

stream 연산자 리스트표가 있어서 한 번 가져와 봤습니다.  Intermediate으로 적혀 있는 연산자는 지난번 포스팅에서 다루었고 이번에는 Terminal(최종)로 적혀있는 연산자를 알아보겠습니다. 

최종 연산자 중에 return type이 Optional<T>인 연산자들은 null값이 나올 경우를 대비하여 예외처리를 해주어야 하는데요, 

이 경우는 뒷쪽에서 다루도록 하겠습니다. 

 

1. forEach()

먼저 forEach는 이름에서도 바로 각각의 요소들에 대한 작업이 이루어질 것이란게 느껴집니다. 값을 하나씩 출력하고 싶을 때 사용하면 됩니다. 

List<String> list = new ArrayList<>(){{
            add("A1");
            add("A2");
            add("B1");
        }};

//forEach()
list.stream()
	.forEach(System.out::println); 
    //.forEach(n -> System.out.println(n)); 을 람다식으로 표현한 것입니다.

이렇게 하면 아래와 같이 모든 요소들을 하나씩 출력할 수 있습니다.

굳이 println일 필요는 없고, 우리가 보통 System.out할 때처럼 줄바꿈이 없는 print도 사용해도 되고, 각 값마다 특정한 문자를 붙여서 out해도 됩니다.

 

 list.stream()
 	.forEach(n -> System.out.print(n + "\t"));
    //Prints A1 A2 B1

2. count()

역시나 단어에서 알 수 있듯이 stream안의 요소가 몇개인지 카운트해주는 연산자입니다. return type은 long입니다.

 

List<String> list = new ArrayList<>(){{
            add("A1");
            add("A2");
            add("B1");
}};

long totalCount = list.stream()
	.filter(n -> n.startsWith("A"))
	.count();
    
System.out.println(totalCount); //Prints 2

list에 A로 시작하는 요소들이 몇개 있는지가 궁금해서,

list를 stream을 이용해 A로 시작하는 요소들만 필터하고, 몇개가 stream에 남아있는지를 .count()를 이용해 세주었습니다. 

여기에서는 2가 나오겠죠?

 

3.sum() 

sum()은 숫자의 합을 반환하게 되어서 IntStream, LongStream, DoubleStream에만 사용가능합니다. Stream<String>과 같이 값을 더할 수 없는 요소들인 경우는 물론이고 Stream<Integer>, Stream<Double>처럼 String<T> type에도 사용이 불가능합니다. 그리고 Return type은 어떤 primitive type stream이냐에 따라 달라집니다. ('+' 연산자 사용하듯이 모든 string들이 다 붙어서 나오게되거나 하지 못합니다 😅)

 

double[] doubles = new double[] { 1.2, 30.1, 6.56, 84.0, 2.45};

double mySum = Arrays.stream(doubles)
                  	.filter(n -> n > 10)
              	    .sum();
        
System.out.println(mySum); //Prints 114.1

 

 

4. average()

평균을 구해주는 average()의 return type은 OptionalDouble입니다. Optional 이라는 것은 null이 나올 수도 있다는 것을 의미하는데요, Optional type을 반환한다고 하면 null이 되는 경우에는 어떻게 처리할 지까지 명시해 주기만하면 됩니다. (Optional은 class입니다.)

 

double[] doubles = new double[] {1.2, 30.1, 6.56, 84.0, 2.45};

//average()의 return type은 OptionalDouble이므로 같은 타입의 변수를 만들어서 값 할당
OptionalDouble myAvg = Arrays.stream(doubles)
                			.filter(n -> n > 10)
               			    .average();


if(myAvg.isPresent()) {
     System.out.println(myAvg.getAsDouble()); 
     //myAvg는 OptionalDoubl이므로 .getAsDouble()을 사용하여 double로 변환
}
//Prints 57.05

 

if문에 사용된 .isPresent()는 값이 존재하는지 즉, null이 아닌지 확인하는 방법으로, boolean을 반환합니다. 

myAvg의 값이 null이 아닌 경우에는 .getAsDouble()로 값을 double로 변경하여 out하도록 하였습니다. 

 

.getAsDouble()을 사용하지 않는다면 아래처럼 Optional 타입으로 나오게 됩니다. 

OptionalDouble[57.05]

 

만약에 myAvg의 값이 null인데 .isPresent() 확인 없이 강제로 out하려고 하면 어떻게 될까요?

 OptionalDouble myAvg = Arrays.stream(doubles)
                .filter(n -> n > 100) //100 이상의 수가 없으므로 빈 IntStream이 return
                .average();

System.out.println(myAvg);

이런 경우에는 아래처럼 나오게 됩니다. 

 

5. min() / max()

이 연산자들은 primitive type이 아닌 stream의 경우에도 사용이 가능합니다. 숫자 타입은 가장 작은 수 또는 가장 큰 수를 반환해주고, String이나 Class의 경우에는 Comparator에 명시된 기준에 따라 값을 반환해 줍니다. 역시나 Optional을 return하기 때문에 null인 경우 처리를 해주면 되겠죠?

Stream의 min() & max()

List<String> list = new ArrayList<>(){{
            add("A9");
            add("A1");
            add("B1");
}};

Optional<String> optStr = list.stream()
               		.min(Comparator.naturalOrder());
                    
optStr.ifPresent(System.out::println); //Prints A1

/*
if(optStr.isPresent()) {
 	System.out.println(optStr.get());
}
*/

 

List<String> type으로 stream을 이용해보았는데요, 

.min(Comparator.naturalOrder())을 사용하였더니 알파벳과 숫자 오름차순으로 비교하여, 그 중에 가장 작은 값으로 나온 요소를 반환받을 수 있었습니다. 

그리고 Optional이기 때문에 null check을 해주었고, Optional class에서 제공하는 .ifPresent()를 사용해서 값이 존재할 때는 어떤 행위를 할 지 괄호 안에 적어주었습니다. average() 예시에서처럼 .isPresent를 사용하고 싶다면 아래에 comment 처리된 부분처럼 수행하면 됩니다. 

 

6.reduce()

 

reduce()는 stream내의 요소들을 모두 결합해주는 역할을 합니다. 전체 요소들을 모두 더할 수도 있고, 곱할 수도 있고, 특정한 형태로 이어서 나오게 할 수도 있습니다.

sum()도 전체 요소들 다 더한다는 점에서 reduce()의 예가 될 수 있는데, 특별히 자주 사용되니까 따로 sum()이라는 연산을 만들어 둔 것 같습니다. 원한다면 reduce()을 사용해서 sum()을 구현할 수도 있습니다. 

reduce()의 return type은 크게 초기값(identity)을 정하는 경우와 정하지 않는 경우로 나눌 수 있습니다. 초기값이 없는 경우는 Optional<T> 타입으로 반환하고, 초기값을 정해두면 stream이 비어있는 상황을 대비할 수 있기 때문에 return type이 Optional이 아니게 됩니다. 

 

List<String> list = new ArrayList<>(){{
            add("D1");
            add("A20");
            add("B1");
}};

//초기값을 정해주지 않았기 때문에 Optional<String> type으로 반환됩니다.
Optional<String> chainedStr = list.stream()
                                  .reduce((a,b) -> a + " & " + b);

chainedStr.ifPresent(System.out::println);
//Prints D1 & A20 & B1

 

identity를 정한 경우도 한 번 보겠습니다. identity를 설정해두면 identity부터 작업이 수행됩니다. 

 

String chainedStr = list.stream()
                        .reduce("A",(a,b) -> a + " & " + b);
                        //identity를 "A"로 설정
                        
System.out.println(chainedStr); //Prints A & D1 & A20 & B1

 

보시면 identity로 설정해 둔 A부터 reduce가 되었음을 볼 수 있습니다.

 

DoubleStream으로 작업한 것을 보여드리면

double[] doubles = new double[] {1.2, 30.1, 6.56, 84.0, 2.45};

double reducedDbl = Arrays.stream(doubles)
                .reduce(1, (a, b) -> a*b);

System.out.println(reducedDbl); //Prints 48763.73376

identity를 1로 설정해두고 곱하기 연산을 수행하였습니다. (1(identity) * 1.2 -> 1.2 * 30.1 ->  36.12 * 6.56 -> ... )이런식으로 연산이 됩니다. 

 

7.collect()

collect()를 사용하면 stream을 List나 Set 자료형으로 변형할 수 있습니다. 

 

  
  
List<String> list = new ArrayList<>(){{
       add("A");
       add("B");
}};

//filter까지만 수행한 경우 Stream<T>의 타입입니다.
Stream<String> stringStream = list.stream()
                .filter(n -> n.equals("B"));

//Stream<T> 타입을 .collect(Collectors.toList())를 사용하면 
//List<T> 타입으로 변경할 수 있습니다!

List<String> filteredList = stringStream
                .collect(Collectors.toList());

 

8. toArray()

반대로 stream을 array 형태로 바꿀 수도 있습니다. Stream<T> 형태일때와 primitive type stream일 때 사용상의 차이가 약간 있습니다. Stream<T> 형태를 array로 바꾸기 위해서는 어떤 데이터 타입의 array로 변경할 것인지 toArray() 괄호 안에 명시해주어야 합니다. 만약 생략하면 Object[] 타입으로 반환받게 됩니다. Primitive type stream인 경우에는 생략해도 해당되는 데이터 타입으로 반환됩니다!

 

 //예시1
 
 List<String> list = new ArrayList<>(){{
            add("A");
            add("D");
            add("F");
            add("B");
        }};

String[] listToArr = list.stream()
                .toArray(size->new String[size]); //()안에 object type 명시해주어야 함 (안하면 Object[]으로 반환)
        
        
 //예시2
 
List<Integer> integerList = new ArrayList<>(){{
            add(1);
            add(5);
            add(2);
 }};
 
int[] listToArr2 = integerList.stream()
                .mapToInt(n->n)//mapToInt를 사용해 IntStream으로 변환
                .toArray(); //()안에 따로 object type 명시해주지 않아도 됨

 

 

9.anyMatch() / allMatch() / noneMatch()

이 친구들은 stream안에 특정 조건의 요소들이 있는지 존재 여부를 확인하기 위한 도구들입니다. Return type은 boolean입니다. 

List<String> list = new ArrayList<>(){{
            add("A1");
            add("A2");
            add("B1");
        }};

if(list.stream().anyMatch(n -> n.equals("A1"))){
            System.out.println("A1 is in the list");
} else {
		System.out.println("A1 is not in the lsit");
}

anyMatch는 하나라도 조건에 맞는 것이 있으면, allMatch는 모든 요소들이 조건에 부합할 때, noneMatch는 조건에 부합하는 것이 하나도 없을 때 true를 return하게 됩니다. 상황에 맞춰서 쓰면 되겠죠?

 

10. findFirst() / findAny()

 

findFirst()는 stream 내의 첫번째 요소를 반환해주고 findAny()는 stream 내의 어떠한 요소를 하나 반환합니다. 이 둘의 Return type은 Optional<T>입니다. 

먼저 findFirst()부터 사용 예를 보면,

List<String> list = new ArrayList<>(){{
            add("D1");
            add("A20");
            add("B1");
}};

//Optional type 반환
Optional<String> firstEl = list.stream().findFirst();

firstEl.ifPresent(System.out::println); //Prints D1

 

findFirst()를 사용했기 때문에 Stream<String>에 있던 가장 첫번째 요소인 D1을 반환하였습니다. 

 

이번에는 findAny()를 살펴보겠습니다.

어떠한 하나의 요소를 반납한다고 하였는데, stream안에 요소가 남아있다면 아무거나 나오게 하는 연산자입니다. 

 

//Optional type 반환
Optional<String> anyEl = list.stream().findAny();

anyEl.ifPresent(System.out::println); //Prints D1 (바뀔 수 있음)

 

하지만 막상 돌려보면 가장 앞에 있는 요소가 나옵니다. 저도 확인해보려고 while문으로 반복 수행해보았지만 제가 돌린 시간 안에서는 첫번째 요소가 반환되더라구요. 그래도 반드시 맨 앞의 요소만이 나오는 것은 아니라고 합니다. 

 

이렇게 최종연산자까지 알아보았는습니다. 👏👏

구글링 하면서 느낀점은 활용 방법이 정말 다양하구나! 라는 것인데요 

Stream 사용이 필수는 아니라고 하지만 declarative하게 프로그래밍 스타일이 바뀌는 추세이기 때문에 익숙해지는 것도 나쁘지 않다고 개인적으로 생각합니다. Swift도 그렇게 바뀌어 가는 것을 보았으니까요! 

궁금한 사항이나 수정 사항 있으면 꼭 댓글로 알려주시면 많은 도움이 됩니다 감사합니다 ☺️

 


참고자료

 

Oracle 공식 문서

https://docs.oracle.com/javase/8/docs/api/

 

Java Platform SE 8

 

docs.oracle.com

https://intrepidgeeks.com/tutorial/java-flow-final-calculation-method

 

[Java] 스트림 최종 연산 메서드

최종 연산 메서드 대표적인 유형과 메서드는 다음과 같습니다. 대부분의 최종연산은 결과값만 리턴되므로 별도의 출력문을 연결해 사용하기 어렵습니다. 각 메서드 설명에 사용된 예제에서는

intrepidgeeks.com

 

 

https://codechacha.com/ko/java8-stream-reduction/

 

Java8의 Stream reduction 사용 방법 및 예제

reduce는 Stream의 데이터를 변환하지 않고, 더하거나 빼는 등의 연산을 수행하여 하나의 값으로 만들 수 있습니다. 예를 들어 수열을 계산하는데 사용할 수 있습니다. 또한 병렬처리를 적용하여 연

codechacha.com

https://stackoverflow.com/questions/23079003/how-to-convert-a-java-8-stream-to-an-array

 

How to convert a Java 8 Stream to an Array?

What is the easiest/shortest way to convert a Java 8 Stream into an array?

stackoverflow.com

 

반응형