참조 : 이것이 자바다 16강 인강
중간 처리 메서드 Section에서 peek()을 설명하며 비교군으로 많이 활용했던 구문이다.
forEach()의 리턴 타입이 void인 만큼 forEach는 Stream의 데이터를 변경하는 등의 특수한 역할을 수행할 수는 없다.
forEach() 구문은 주로 Stream에 저장된 모든 요소를 출력하는 용도로 활용된다.
peek()을 배우며 많이 활용해봤으니 예시 코드는 생략하겠다.
매칭은 최종 처리 단계에서 Stream에 담긴 값들이 특정 조건을 만족하는지 조사하는 메서드이다.
중간 단계 메서드 중 filter()와 비슷하다고 생각할 수 있지만 조금 다른 성격을 띤다.
filter()는 중간 단계에서 로직을 수행하며 "모든 데이터 중 원하는 데이터만 추출"하는 역할을 수행했다.
원하는 데이터를 추출하기 위해선 Stream에 담겨 있는 모든 데이터를 순회해봐야 하므로 filter는 모든 데이터를 한 번씩 조회한다.
하지만 최종 처리 메서드의 매칭 같은 경우에는 "Stream 데이터가 조건에 맞는가"만 확인하면 된다.
따라서 만약 Stream 데이터가 조건에 맞다고 판단되거나 틀리다고 판단되는 즉시 메서드 동작을 중지한다.
매칭 최종 처리 메서드는 anyMatch, allMatch, noneMatch가 존재하고 각각의 역할은 아래와 같다.
3개의 매칭 최종 처리메서드는 Parameter로써 Predicate를 전달해주면 된다.
이전에 말했듯 Predicate란 boolean 값을 반환하는 익명 구현체를 의미한다.
anyMatch는 Stream에서 조건에 맞는 원소를 찾는 순간 메서드가 끝나고(True 반환), allMatch와 noneMatch는 조건에 맞지 않는 원소를 찾는 순간 메서드가 끝난다.(False 반환)
집계 최종 처리 메서드를 알아보기 전 "Optional"에 대하여 알 필요가 있다.
Optional은 Java 8부터 나온 클래스로써 값의 존재 여부가 확정되지 않았을 경우 사용되는 클래스이다.
Optional 클래스에서 값을 추출하는 방법은 "get()" 메서드를 사용하는 것과 "orElse()" 메서드를 사용하는 방법이 존재하지만, Optional이라는 클래스의 특성을 살리기 위해서는 orElse() 메서드를 사용하는 것이 좋다.
그렇다면 get()과 orElse(), 그리고 사용 이유에 대해 코드를 통해 알아보자.
List<Integer> list = new ArrayList<>();
Integer i = list.stream().max((a, b) -> a - b).get();
System.out.println(i);
값이 존재하지 않는 비어 있는 List를 Stream으로 만들고 최댓값을 "get()" 메서드를 통해 얻어오는 상황이다.
그런데 List가 비어 있기 때문에 최댓값이 존재하지 않을 것이며 get() 메서드를 사용하면 당연히 NoSuchElementException이 발생할 것이다.
Optional Class란 이런 값이 저장되지 않은 상황에도 에러를 발생시키지 않도록 처리하기 위해 도입된 클래스인데 NoSuchElementException 에러가 발생한다는 것은 Optional 클래스를 제대로 활용하고 있지 않다는 의미이다.
그렇다면 orElse() 구문을 쓰면 어떻게 될까?
List<Integer> list = new ArrayList<>();
Integer i = list.stream().max((a, b) -> a - b).orElse(-1);
System.out.println(i);
orElse 안에 넣어준 값인 -1이 출력됨을 볼 수 있다.
orElse(default) 메서드는 Optional 객체 내에 실제 값이 저장되어 있다면 저장되어 있는 값을 반환하고, 만약 값이 저장되어 있지 않다면 orElse의 Parmeter로 설정했던 default 값을 출력하는 메서드이다.
이처럼 값이 존재하지 않는 상황에도 에러를 발생시키지 않고 정상 코드로 처리 가능하기 때문에 Optional 클래스를 사용하는 것이며, get보다는 orElse 사용을 추천하는 것이다.
(이외에도 Optional에는 값이 존재하는지 파악하는 isPresent() 메서드나 값이 존재할 경우 Consumer에서 로직을 처리하는 ifPresent() 메서드도 있긴 하지만 orElse가 이 모든 상황을 대체할 수 있으므로 orElse는 꼭 기억해 두자)
Stream에서 집계 과정을 수행할 때 Stream에 데이터가 무조건 저장되어 있을 거라 확신할 수 없다.
Stream에 데이터가 저장되어 있지 않은 상황에서 NullPointerException이나 NoSuchElementException을 발생시키지 않게 하기 위하여 집계 관련 최종 처리 메서드는 모두 OptionalX 리턴타입을 가지는 것이다.
int[] arr = new int[10];
for(int i =0;i<10;i++){
arr[i] = ThreadLocalRandom.current().nextInt(1, 1000);
}
System.out.println("[arr의 전체 원소 출력]");
for(int i =0;i<10;i++){
System.out.print(arr[i]+" ");
}
System.out.println();
System.out.println("[arr에서 첫 번째로 저장된 원소 출력]");
System.out.println(Arrays.stream(arr).findFirst().getAsInt());
Stream의 원소 중 첫 번째로 저장된 원소를 출력하는 메서드이다.
첫 번째로 저장된 원소를 출력하는 것은 index를 통한 접근이 더욱 빠르다 생각하여 큰 활용성이 있는지 애매한 메서드이다.
int[] arr = {1,2,3,4,5,6,7,8,9,10};
System.out.println("arr 원소 전체 개수 : "+Arrays.stream(arr).count());
Stream에 저장되어 있는 원소 개수를 반환하는 메서드이다.
IntStream, LongStream, DoubleStream에서 max와 min을 활용할 경우 int형, long형, double형 데이터는 대소 비교 수행 방법이 고정되어 있으므로 아무런 Parmeter 없이도 동작 가능하다.
하지만 Stream에서 max와 min을 활용할 경우에는 어떻게 대소 비교를 수행해야 하는지를 명시해줘야 한다.
Parmeter로는 Comparator 익명 구현체를 넣어주면 되고 이전에 사용했던 것처럼 람다식을 통해 Parmeter를 전달해주는 것이 좋을 것이다.
Integer[] arr = new Integer[10];
for(int i = 0; i < arr.length; i++){
arr[i] = ThreadLocalRandom.current().nextInt(1, 1000);
}
System.out.println("[arr 원소 값 출력]");
for(int i = 0; i < arr.length; i++){
System.out.print(arr[i] + " ");
}
System.out.println("\n=====================");
System.out.println("arr 최댓값 : "+Arrays.stream(arr).max((a,b)->a-b).orElse(Integer.MIN_VALUE));
System.out.println("arr 최솟값 : "+Arrays.stream(arr).min((a,b)->a-b).orElse(Integer.MIN_VALUE));
정상적으로 최댓값과 최솟값을 반환함을 확인할 수 있다.
다른 집계 메서드와는 달리 IntStream, DoubleStream, LongStream에서만 활용할 수 있는 메서드이다.(즉, Stream 객체에서는 활용 불가한 메서드)
용어 그대로 average()는 "Stream에 저장된 데이터의 평균"을 구하는 메서드, sum()은 "Stream에 저장된 데이터의 합"을 구하는 메서드이다.
average()는 리턴타입이 OptionalDouble로써 double 타입으로 평균값을 반환함을 확인할 수 있다.
약간은 특이한 성격을 지닌 게 "sum()" 메서드이다.
sum() 메서드는 count() 메서드와 유이하게 집계 최종 메서드 중 Optional 리턴 타입을 갖지 않는 메서드이다.
그런데 만약 Stream에 데이터가 저장되어 있지 않다면 count()는 0을 반환하면 되지만 sum()은 NoSuchElementException을 반환해야 하지 않을까?
한 번 확인해 보자.
List<String> list = new ArrayList<>();
System.out.println(list.stream().mapToInt(s -> Integer.parseInt(s)).sum());
분명 List에는 아무런 데이터가 저장되어 있지 않으므로 Stream 또한 아무 데이터도 저장하고 있지 않아 NoSuchElementException을 반환해야 할 것 같은데 답으로 0.0을 반환했다.
이는 Stream의 sum() 메서드를 자세히 확인해보면 알 수 있다.
@Override
public final int sum() {
return reduce(0, Integer::sum);
}
Stream의 최종 메서드인 sum()의 구현내용이다. 메서드가 reduce(0, Integer::sum)을 반환함을 알 수 있다.
즉, Stream의 sum() 최종 메서드는 Stream에 s[0], s[1], ... s[n] 값이 존재할 때 s[0] + s[1] +... + s[n]을 반환하는 것이 아닌 0 + s[0] + ... + s[n]을 반환하기 때문에 Stream에 아무런 값이 전달되지 않더라도 0이 반환되는 것이다.
reduce()는 "커스텀 집계"를 위한 메서드이다.
우리가 이전까지 사용했던 집계 메서드는 이미 로직이 정해져 있었다.
그런데 만약 "회원들의 이름을 구분자(|)를 통해 모두 합치고 싶다"같은 집계 로직이 필요할 경우 어떻게 해야 할까?
이런 상황에서 원하는 대로 커스텀 집계 함수를 만들기 위해 활용하는 것이 reduce() 메서드이다.
reduce 메서드의 사용 방법은 아래와 같다.
// 초기값을 주고 싶지 않은 경우
reduce(BinaryOperator<T> accumulator)
// 리턴 타입 : Optional<T>
// 초기값을 주고 싶은 경우
reduce(T identity, BinaryOperator<T> accumulator)
// 리턴 타입 : T
BinaryOperator란 동일한 타입의 인수 2개를 받아 인수와 같은 타입의 데이터를 반환하는 함수를 말한다.
대표적으로 Integer.add, Integer.min 같은 함수가 존재한다.
위 코드처럼 초기값을 주고 싶으면 첫 번째 Parmeter로 default 값(T identity)을 입력하면 되고, 초기값을 안 줄 거면 바로 BinaryOperator를 입력해주면 된다.
초기값을 줄 경우 Stream에 데이터가 존재하지 않을 때 identity 값을 반환하므로 리턴 타입이 T이며(예시 : sum()) 초기값을 주지 않을 경우 Stream에 데이터가 존재하지 않는 상황을 대비해야 하므로 Optional<T>
형태로 리턴타입이 지정된다.
그렇다면 위에서 설명했던 회원들의 이름을 구분자(|)를 통해 모두 합치는 집계 메서드를 만들어보자.
class Member {
int age;
String name;
public Member(int age, String name) {
this.age = age;
this.name = name;
}
// BinaryOperator 역할을 할 함수
static Member sum(Member m1, Member m2){
return new Member(m1.age + m2.age, m1.name +"|"+ m2.name);
}
}
List<Member> list = new ArrayList<>();
list.add(new Member(10, "홍길동"));
list.add(new Member(20, "김길동"));
list.add(new Member(30, "박길동"));
list.add(new Member(40, "최길동"));
System.out.println("회원 전체 이름 : " + list.stream().reduce(Member::sum).orElse(new Member(0, "")).name);