스트림 API 핵심
1. 스트림은 데이터 원소의 유한 혹은 무한 시퀀스를 뜻한다.
2. 스트림 파이프 라인은 이 원소들이 수행하는 연산 단계를 표현하는 개념이다.
스트림의 원소
- 어디로부터든 올 수 있다.
- 대표적으로 컬렉션, 배열, 파일, 정규표현식 패턴 매처, 난수 생성기, 혹은 다른 스트림등이 존재
스트림 안의 데이터 원소
- 객체 참조나 기본 타입 값
- 기본 타입 값으로 int, long, double 3가지만 지원
스트림 파이프라인 특징
- 스트림 파이프라인은 소스스트림 -> (중간연산) -> 종단연산 으로 이루어진다.
- 각 중간 연산은 스트림을 어떠한 방식으로 변환(transform)한다.
- 중간연산을 합친 다음에 합쳐진 중간연산을 최종 연산으로 한번에 처리 -> Lazy evaluation
- 평가는 종단 연산이 호출될 때 이뤄지며, 종단 연산에 쓰이지 않는 데이터 원소는 계산에 쓰이지 않는다.
- 종단 연산이 없는 스트림 파이프라인은 아무 일도 하지 않는 명령어인 no-op과 같으므로, 종단 연산은 필수다.
- 스트림 API는 메서드 연쇄를 지원하는 플루언트 API(fluent API)다.
- 파이프라인 하나를 구성하는 모든 호출을 연결하여 단 하나의 표현식으로 완성할 수 있으며, 파이프라인 여러 개를 연결해 표현식 하나로 만들 수도 있다.
- 기본적으로 스트림 파이프라인은 순차적으로 수행된다.
- 파이프라인을 병렬로 실행하려면 파이프라인을 구성하는 스트림 중 하나에서 parallel 메서드를 호출해주기만 하면 된다. 다만, 효과를 볼 수 있는 상황은 많지 않다.
중간연산
스트림을 변환하며 결과 스트림의 원소 타입은 시작 스트림의 원소 타입과 같을 수도 있고, 다를 수도 있다.
1. sorted
2. filterStream<Integer> sorted = operands.stream().sorted();
3. map: 요소들을 변경하여 새로운 컨텐츠를 생성하는 기능Stream<Integer> integerStream = operands.stream().filter((value) -> value > 2);
(ex) 소문자를 대문자로 변경
list.map(s -> s.toUpperCase());
종단연산
마지막 중간 연산의 스트림에 최후의 연산. 1개 이상의 중간연산들은 계속합쳐진 후 종단연산 시 수행된다.
즉, 스트림 파이프라인은 지연평가(lazy evaluation)된다.
종단 연산이 없는 파이프라인은 어떤 연산도 수행되지 않는다.지연평가는 무한 스트림을 다룰 수 있게 해주는 열쇠다.
1. forEach() : 요소의 출력intList.stream().forEach(System.out::println); // 1,2,3 intList.stream().forEach(x -> System.out.printf("%d : %d\n",x,x*x)); // 1,4,9
2. reduce() : 요소의 소모
두개의 인자(a, b)을 가지며 연산결과를 전달하여 다시 다음 요소와 연산을 한다.
int sum = intList.stream().reduce((a,b) -> a+b).get(); System.out.println("sum: "+sum); // 6
3. findFirst(), findAny() : 요소의 검색
스트림에서 지정한 첫번째 요소를 찾는 메서드이다.
strList.stream().filter(s -> s.startsWith("H")) .findFirst() .ifPresent(System.out::println); //Hwang strList.parallelStream().filter(s -> s.startsWith("H")) .findAny() .ifPresent(System.out::println); //Hwang or Hong
4. anyMatch(), allMatch(), noneMatch() : 요소의 검사
스트림의 요소중 특정 조건을 만족하는 요소를 검사하는 메서드. 원소중 일부, 전체 혹은 일치하는 것이 없는 경우를 검사하고 boolean 값을 리턴한다. noneMatch()의 경우 일치하는 것이 하나도 없을때 true.
boolean result1 = strList.stream().anyMatch(s -> s.startsWith("H")); //true boolean result2 = strList.stream().allMatch(s -> s.startsWith("H")); //false boolean result3 = strList.stream().noneMatch(s -> s.startsWith("T")); //true System.out.printf("%b, %b, %b",result1,result2, result3);
5. count(), min(), max() : 요소의 통계
스트림의 원소들로부터 전체 갯수, 최소값, 최대값을 구하기 위한 메서드
intList.stream().count(); // 3 intList.stream().filter(n -> n !=2 ).count(); // 2 intList.stream().min(Integer::compare).ifPresent(System.out::println);; // 1 intList.stream().max(Integer::compareUnsigned).ifPresent(System.out::println);; // 3 strList.stream().count(); // 3 strList.stream().min(String::compareToIgnoreCase).ifPresent(System.out::println);// Hong strList.stream().max(String::compareTo).ifPresent(System.out::println); // Kang
6. sum(), average() : 요소의 연산
스트림의 원소들의 합계를 구하거나 평균을 구하는 메서드
intList.stream().mapToInt(Integer::intValue).sum(); // 6 intList.stream().reduce((a,b) -> a+b).ifPresent(System.out::println); // 6 intList.stream().mapToInt(Integer::intValue).average(); // 2 intList.stream().reduce((a,b) -> a+b) .map(n -> n/intList.size()).ifPresent(System.out::println); // 2
7. collect() : 요소의 수집
스트림의 결과를 모으기 위한 메서드로 Collectors 객체에 구현된 방법에 따라 처리하는 메서드이다. 최종 처리 후 데이터를 변환하는 경우가 많기 때문에 잘 알아 두어야한다. 용도별로 사용할 수 있는 Collectors의 메서드는 기능별로 다음과 같다.
스트림을 배열이나 컬렉션으로 변환 : toArray(), toCollection(), toList(), toSet(), toMap()
요소의 통계와 연산 메소드와 같은 동작을 수행 : counting(), maxBy(), minBy(), summingInt(), averagingInt() 등
요소의 소모와 같은 동작을 수행 : reducing(), joining()
요소의 그룹화와 분할 : groupingBy(), partitioningBy()
strList.stream().map(String::toUpperCase) .collect(Collectors.joining("/")); // Hwang/Hong/Kang strList.stream().collect(Collectors.toMap(k -> k, v -> v.length())); // {Hong=4, Hwang=5, Kang=4} intList.stream().collect(Collectors.counting()); intList.stream().collect(Collectors.maxBy(Integer::compare)); intList.stream().collect(Collectors.reducing((a,b) -> a+b)); // 6 intList.stream().collect(Collectors.summarizingInt(x -> x)); //IntSummaryStatistics{count=3, sum=6, min=1, average=2.000000, max=3} Map<Boolean, List<String>> group = strList.stream() .collect(Collectors.groupingBy(s -> s.startsWith("H"))); group.get(true).forEach(System.out::println); // Hwang, Hong Map<Boolean, List<String>> partition = strList.stream() .collect(Collectors.partitioningBy(s -> s.startsWith("H"))); partition.get(true).stream().forEach(System.out::println); // Hwang, Hong
참고: https://insight-bgh.tistory.com/440
스트림을 과용하면 프로그램이 읽거나 유지보수하기 어려워진다.
스트림은 적절히 사용해야 깔끔하고 명료해진다.
※ 스트림을 사용할 때 주의할 점
- 람다 매개변수의 이름은 주의해서 정해야 한다.
- 람다에서는 타입 이름을 자주 생략하므로 매개변수 이름을 잘 지어야 스트림 파이프라인의 가독성이 유지된다.
- 도우미 메서드를 적절히 활용하는 일의 중요성은 일반 반복 코드에서보다 스트림 파이프라인에서 훨씬 크다. 파이프라인에서는 타입 정보가 명시되지 않거나 임시 변수를 자주 사용하기 때문이다.
- 자바는 기본 타입인 char용 스트림을 지원하지 않는다.
char은 int 값을 갖기 때문이고, 그 덕에 int 스트림을 반환하면 헷갈릴 수 있다. 올바르게 동작하게 하려면 명시적으로 형변환을 해줘야 한다.
따라서 char 값들을 처리할 때는 스트림을 삼가는 편이 낫다.
- 스트림이 언제나 가독성과 유지보수 측면으로 뛰어난 것은 아니다.
스트림과 반복문을 적절히 조합하는 게 최선이다.
따라서 기존 코드는 스트림을 사용하도록 리팩터링하되, 새 코드가 더 나아 보일 때만 반영하도록 한다.
반복 코드 vs 스트림 파이프라인
- 스트림 파이프라인은 되풀이되는 계산을 함수 객체(주로 람다나 메서드 참조)로 표현한다.
- 반복 코드에서는 코드 블록을 사용해 표현한다.
함수 객체로는 할 수 없지만 코드 블록으로는 할 수 있는 일
- 코드 블록에서는 범위 안의 지역변수를 읽고 수정할 수 있다.
- 람다에서는 final이거나 사실상 final인 변수만 읽을 수 있고, 지역변수를 수정하는 건 불가능하다.
- 코드 블록에서는 return 문을 사용해 메서드에서 빠져나가거나, break나 continue 문으로 블록 바깥의 반복문을 종료하거나 반복을 한 번 건너뛸 수 있다.
- 또한, 메서드 선언에 명시된 검사 예외를 던질 수 있다.
- 람다로는 이 중 어떤 것도 할 수 없다.
계산 로직에서 이상의 일들을 수행해야 한다면 스트림과는 맞지 않는 것이다.
스트림을 사용하기 적절한 경우
- 원소들의 시퀀스를 일관되게 변환할 때
- 원소들의 시퀀스를 필터링할 때
- 원소들의 시퀀스를 하나의 연산을 사용해 결합할 때(더하기, 연결하기, 최솟값 구하기 등).
- 원소들의 시퀀스를 컬렉션에 모을때(공통된 속성을 기준으로)
- 원소들의 시퀀스에서 특정 조건을 만족하는 원소를 찾을 때
스트림으로 처리하기 어려운 경우
- 한 데이터가 여러 개의 파이프라인을 거칠 때 이 데이터의 각 단계에서의 값들에 동시에 접근하는 경우(스트림 파이프라인은 일단 한 값을 다른 값에 매핑하고 나면 원래의 값은 잃는 구조이기 때문)
핵심 정리
- 스트림과 반복 방식은 각각에 알맞은 일이 있다.
- 수 많은 작업은 이 둘을 조합했을 때 가장 멋지게 해결된다.
- 만약 스트림과 반복 중 어느 쪽이 나은지 확신하기 어렵다면 둘 다 해보고 더 나은 쪽을 선택하도록 한다.
'Effective Java' 카테고리의 다른 글
[Effective Java] 아이템 47 - 반환 타입으로는 스트림보다 컬렉션이 낫다 (0) | 2022.03.29 |
---|---|
[Effective Java] 아이템 46 - 스트림에서는 부작용 없는 함수를 사용하라 (0) | 2022.03.28 |
[Effective Java] 아이템 44 - 표준 함수형 인터페이스를 사용하라 (0) | 2022.03.25 |
[Effective Java] 아이템 43 - 람다보다는 메서드 참조를 사용하라 (0) | 2022.03.23 |
[Effective Java] 아이템 42 - 익명 클래스보다는 람다를 사용하라 (0) | 2022.03.23 |