스트림
데이터 처리 연산을 지원하도록 소스에서 추출된 연속된 요소
스트림과 컬렉션
컬렉션 : 현재 자료구조가 포함하는 모든 값을 메모리에 저장하는 자료구조.
-> 컬렉션의 모든 요소는 컬렉션에 추가하기 전에 계산되어야 함.
스트림 : 요청할 때만 요소를 계산하는 고정된 자료구조
-> 스트림에 요소를 추가하거나 제거할 수 없음.
-> 즉, 사용자가 데이터를 요청할 때만 값 계산
ex)
List<String> names = menu.stream()
.filter(dish -> dish.getCalories() > 300)
.map(Dish::getName)
.limit(3)
.collect(toList());
https://velog.io/@cham/JAVA-%EC%8A%A4%ED%8A%B8%EB%A6%BCStream
스트림은 그저 또 하나의 API가 아닌, 함수형 프로그래밍에 기초한 패러다임.(이론의 틀이나 개념의 집합체)
핵심 : 일련의 변환(transformation)으로, 재구성하는 부분
이 때 각 변환 단계는 이전 단계의 결과를 받아 처리하는 순수 함수여야 한다.
(순수 함수 : 오직 입력만이 결과에 영향을 주는 함수)
다른 가변 상태를 참조하지 않고, 함수 스스로도 다른 상태를 변경하지 않는다.
Map<String, Long> freq = new HashMap<>();
try (Stream<String> words = new Scanner(file).tokens()) {
words.forEach(word -> {
freq.merge(word.toLowerCase(), 1L, Long::sum);
});
}
스트림 코드 x
왜 ? 스트림 코드를 가장한 반복적 코드.
forEach에서 외부 상태를 수정하는 람다를 실행하면서 문제가 생김. (위에서 변경 조건에 어긋나겠죠?)
올바르게 고치면
Map<String, Long> freq;
try (Stream<String> words = new Scanner(file).tokens()) {
freq = words
.collect(groupingBy(String::toLowerCase, counting()));
}
forEach 연산은 스트림 계산 결과를 보고할 때만 사용하고, 계산하는 데는 쓰지 말자!
이 클래스는 39개의 메서드 보유.
타입 매개변수가 5개나 된다.
(깊게 알 필요는 없고, 그만큼 장점이 많다 정도.)
수집기가 생성하는 객체는 일반적으로 컬렉션이며, 그래서 collect라는 이름을 쓴다.
컬렉션?
맨 위에서도 말했지만,
한 마디로 객체의 모음, 그룹
https://crazykim2.tistory.com/557
이를 사용하면 스트림의 원소를 손쉽게 켈렉션으로 모을 수 있다.
: toList(), toSet(), toCollection(collectionFactory)
리스트, 집합, 프로그래머가 지정한 컬렉션 타입
List<String> topTen = freq.keySet().stream()
.sorted(comparing(freq::get).reversed())
.limit(10)
.collect(toList());
마지막 toList -> Collectors의 메서드.
Collectors의 멤버를 정적 임포트하여 쓰면 스트림 파이프라인 가독성이 좋아져, 흔히들 이렇게 사용한다고 한다.
스트림 파이프라인 가독성??
스트림 : 연속된 요소
파이프라인 : 한 데이터 처리 단계의 출력이 다음 단계의 입력으로 이어지는 형태로, 연결된 구조
가독성 : 읽기 좋은거
스트림의 각 원소는 키 하나와 값 하나에 연관되어 있다.
다수의 스트림 원소가 같은 키에 연관될 수 있다.
가장 간단한 맵 수집기.
스트림 원소를 키에 매핑하는 함수와 값에 매핑하는 함수를 인수로 받는다.
private static final Map<String, Operation> stringToEnum =
Stream.of(values()).collect(
toMap(Object::toString, e -> e));
-> 간단한 toMap 형태는 스트림의 각 원소가 고유한 키에 매핑되어 있을 때 적합.
인수 3개를 받는 toMap은 어떤 키와 그 키에 연관된 원소들 중 하나를 골라 연관 짓는 맵을 만들 때 유용.
각 키와 해당 키의 특정 원소를 연관 짓는 맵을 생성하는 수집기의 예시
(다양한 음악가의 앨범들을 담은 스트림을 가지고, 음악가와 그 음악가의 베스트 앨범을 연관 짓고 싶다고 가정)
Map<Artist, Album> topHits = albums.collect(
toMap(Album::artist, a->a, maxBy(comparing(Album::sales))));
:: (이중콜론 연산자)
정식 명칭 : 메소드 참조 표현식
람다식에서 파라미터를 중복해서 쓰기 싫을 때 사용.
람다 표현식에서만 사용 가능.
- [인스턴스] :: [메소드명 (또는 new)]
http://yoonbumtae.com/?p=2776
비교자 생성 메서드인 comparing이 maxBy에 넘겨줄 비교자를 반환하는데,
자신의 키 추출 함수로는 Album::sales를 받았다.
--> " 앨범 스트림을 맵으로 바꾸는데, 이 맵은 각 음악가와 그 음악가의 베스트 앨범을 짝지은 것"
매핑 함수가 키 하나에 연결해준 값 들이 모두 같을 때, 혹은 값이 다르더라도 모두 허용되는 값일 때 이렇게(마지막에 쓴 값을 취하는) 동작하는 수집기 필요.
toMap(KeyMapper, valueMapper, (oldVal, newVal) -> newVal)
Collectors가 제공하는 또 다른 메서드.
입력으로 분류 함수를 받고 출력으로는 원소들을 카테고리별로 모아 놓은 맵을 담은 수집기 반환.
분류 함수는 입력받은 원소가 속하는 카테고리 반환.
그리고 이 카테고리가 해당 원소의 맵 키로 쓰인다.
반환된 맵에 담긴 각각의 값은 해당 카테고리에 속하는 원소들을 모두 담은 리스트.
아이템45의 아나그램 프로그램에서 사용한 바로 그 수집기로,
알파벳화한 단어를 알파벳화 결과가 같은 단어들의 리스트로 매핑하는 맵을 생성했다.
words.collect(groupingBy(word->alphabetize(word)))
groupingBy가 반환하는 수집기가 리스트 외의 값을 갖는 맵을 생성하게 하려면,
분류 함수와 함께 다운스트림 수집기도 명시해야 한다.
다운스트림 수집기 : 해당 카테고리의 모든 원소를 담은 스트림으로부터 값을 생성하는 일.
-> 가장 간단한 방법 : toSet()
그러면 groupingBy는 원소들의 리스트가 아닌 집합(Set)을 값으로 갖는 맵 생성
toSet()대신 toCollection(collectionFactory)를 건네는 방법도 있음.
리스트나 집합 대신 컬렉션을 값으로 갖는 맵을 생성
(원하는 컬렉션 타입을 선택할 수 있는 유연성은 덤)
다운스트림 수집기로 counting()을 건네는 방법도 있음.
각 카테고리(키)를 (원소를 담은 컬렉션이 아닌)해당 카테고리에 속하는 원소의 개수(값)와 매핑한 맵을 얻는다.
Map<String, Long> freq = words
.collect(groupingBy(String::toLowerCase, counting())));
많이 쓰이진 않지만 groupingBy의 사촌격
분류 함수 자리에 프레디키트를 받고 키가 Boolean인 맵 반환.
프레디키트
항상 '참' 또는 '거짓'을 보고하는 특별한 종류의 리포터 블록.
Collections에는 이런 속성의 메서드가 16개나 더 있다.
그 중 9개는 이름이
summing, averaging, summarizing 으로 시작하며,
각각 int, long, double 스트림용으로 하나씩 존재.
그리고 다중정의된 reducing 메서드들,
filtering, mapping, flatMapping, collectingAndThen 메서드가 있는데,
대부분 프로그래머는 이들의 존재를 모르고 있어도 상관없다.
이 수집기들은 스트림 기능의 일부를 복제하여 다운스트림 수집기를 작은 스트림처럼 동작하게 한 것.
java.util.functin.BinaryOperator 의 메서드
Collectors에 정의되어 있지만 '수집'과는 관련이 없음.
minBy, maxBy : 인수로 받은 비교자를 이용해 스트림에서 가장 값이 작은, 값이 큰 원소를 찾아 반환.
(문자열 등의)CharSequence 인스턴스의 스트림에만 적용 가능.
이 중 매개변수가 없는 joining은 단순히 원소들을 연결하는 수집기를 반환.
구분문자로 쉼표(,)를 입력하면 CSV 형태의 문자열을 만들어준다.
CVS?
comma-separated values
몇 가지 필드를 쉼표(,)로 구분한 텍스트 데이터 및 텍스트 파일
핵심 정리
스트림 파이프라인 프로그래밍의 핵심은 부작용 없는 함수 객체에 있다.
스트림뿐 아니라 스트림 관련 객체에 건네지는 모든 함수 객체가 부작용이 없어야 한다.
종단 연산 중 forEach는 스트림이 수행한 계산 결과를 보고할 때만 이용해야 한다.
계산 자체에는 이용하지 말자.
스트림을 올바로 사용하려면 수집기를 잘 알아둬야 한다.
가장 중요한 수집기 팩터리는 toList, toSet, toMap, joining이다.