Java 스트림(Stream)
1. 스트림
지금까지 많은 수의 데이터를 다룰 때, 컬렉션이나 배열에 데이터를 담고 원하는 결과를 얻기 위해,
for문과 Iterator문을 이용해 코드를 작성해왔다.
그러나 이런 방식으로 작성된 코드는 너무 길고 알아보기 어렵다. 재사용성도 떨어진다.
또 다른 문제는 데이터 소스마다 다른 방식으로 다뤄야한다는 것이다.
인터페이스를 이용해 컬렉션을 다루는 방식을 표준화하기는 했지만,
각 컬렉션을 다루는 방식을 표준화하기는 했지만,
각 컬렉션 클래스에는 같은 기능의 메서드들이 중복해서 정의되어 있다.
예를 들면 List를 정렬할 때는 Collections.sort()를 이용하고,
배열을 정렬할 때는 Arrays.sort()를 이용한다
이 문제를 해결해주는 것이 스트림
스트림은 데이터소스를 추상화하고, 데이터를 다루는데 자주 사용되는 메서드들을 정의해놓았다.
데이터소스를 추상화하였다는 것은,
데이터 소스가 무엇이던 간에 같은 방식으로 다룰 수 있게 되었다는 것과 코드의 재사용성이 높아진다는 것을 의미한다.
String[] strArr = {"aaa", "ddd", "ccc"};
List<String>strList = Arrays.asList(strArr);
이 두 데이터 소스를 기반으로 하는 스트림은 다음과 같이 생성한다.
Stream<String> strStream1 = strList.stream(); // 스트림 생성
Stream<String> strStream2 = Arrays.stream(strArr); // 스트림 생성
두 스트림으로 데이터 소스의 데이터를 읽어서 정렬하고 화면에 출력하는 방법은 다음과 같다.
데이터 소스가 정렬되는 것은 아니라는 것에 유의
스트림은 데이터 소스(원본 데이터)를 변경하지 않는다.
그리고 스트림은 데이터 소스로부터 데이터를 읽기만 할 뿐이다.
필요하다면, 결과를 컬렉션이나 배열에 담아 반환할 수도 있다.
스트림은 일회용이다.
Iterator처럼 일회용이다.
필요하다면 다시 생성해야 한다.
얘넨 내부적으로 받은 값들에 대한 객체를 따로 만듬
스트림은 작업을 내부 반복으로 처리한다.
스트림을 이용한 작업이 간결할 수 있는 비결 중 하나가 바로 '내부 반복'이다.
내부반복이라는 것은 반복문을 메서드 내부에 숨길 수 있다는 의미
forEach()는 스트림에 정의된 메서드 중 하나로 매개변수에 대입된 람다식을 데이터 소스의 모든 요소에 적용
즉, forEach는 메서드 안으로 for문을 넣은 것이다. 수행할 작업은 매개변수로 받는다
스트림의 연산
스트림이 제공하는 다양한 연산을 이용해서 복잡한 작업들을 간단히 처리할 수 있다.
마치 데이터베이스에 sellect문으로 질의하는 것과 같은 느낌이다.
참고 스트림에 정의된 메서드 중에서 데이터 소스를 다루는 작업을 수행하는 것을 연산이라고 한다
스트림이 제공하는 연산은 중간 연산과 최종 연산으로 분류할 수 있는데,
중간 연산은 연산 결과를 스트림으로 반환하기 때문에 중간 연산을 연속해서 연결할 수 있다.
반면, 최종 연산은 스트림의 요소를 소모하면서 연산을 수행하므로 단 한번만 가능하다.
stream.distinct().limit(5).sorted().forEach(System.out::println)
중간연산 중간연산 중간연산 최종 연산
모든 중간 연산의 결과는 스트림이지만, 연산 전의 스트림과 같은 것은 아니다.
2. 지연된 연산
스트림 연산에서 한 가지 중요한 점은 최종 연산이 수행되기 전까지는 중간 연산이 수행되지 않는다는 것이다.
스트림에 대해 distinct()나 sort()같은 중간 연산을 호출해도 즉각적인 연산이 수행되는 것은 아니라는 것
중간 연산을 호출하는 것은 단지 어떤 작업이 수행되어야 하는지를 지정해주는 것일 뿐,
최종 연산이 수행되어야 비로소 스트림의 요소들이 중간 연산을 거쳐 최종 연산에서 소모된다.
Stream<Integer>와 IntStream
요소의 타입이 T인 스트림은 기본적으로 Stream<T>지만,
오토박싱&언박싱으로 인한 비효율을 줄이기 위해 데이터 소스의 요소를 기본형으로 다루는 스트림,
IntStream, LongStream, DoubleStream이 제공된다.
일반적으로 Stream<Integer> 대신 IntStream을 사용하는 게 더 효율적이다.
IntStream 안에 유용한 메서드들이 있음
3. 병렬 스트림
스트림으로 데이터를 다룰 때 장점 중 하나가 병렬 처리가 쉽다.
앞서 fork&join프레임웍으로 병렬처리 작업을 배웠는데,
병렬스트림은 내부적으로 이 프레임웍을 이용해 자동적으로 병렬로 연산한다
우리가 할 일은 그저 스트림에 rarallel()이라는 메서드를 호출해서 병렬 연산을 지시만 하면 된다.
병렬이 아니게 처리하고 싶으면 sequential()을 호출하면 된다.
모든 스트림은 기본적으로 병렬 스트림이 아니므로 호출할 필요가 없다
이 메서드는 parallel을 호출한 것을 취소할 때만 사용한다
참고 parallel과 sequential은 새로운 스트림을 생성하는 것이 아니라, 그저 스트림의 속성을 바꿀 뿐이다.
4. 스트림 만들기
4 - 1컬렉션
컬렉션의 최고 조상인 Collection에 stream()이 정의되어 있다.
stream은 해당 컬렉션을 소스로 하는 스트림을 반환한다.
Stream<T> Collection.stream()
List로부터 스트림을 생성하는 코드
List<Integer> list = Arrays.asList(1,2,3,4,5); // 가변인자
Stream<Integer> intStream = list.stream(); // list를 소스로 하는 컬렉션 생성
4 - 2배열
배열을 소스로 하는 스트림 생성 메서드는 Stream과 Arrays에 static 메서드로 정의되어 있다.
Stream<T> Stream.of(T... values) // 가변인자
Stream<T> Stream.of(T[]) // 가변인자
Stream<T> Arrays.stream(T[])
Stream<T> Arrays.stream(T[] array, int startInclusive, int endExclusive)
예시
Stream<String> strStream = Stream.of("a", "b", "c"); // 가변 인자
Stream<String> strStream = Stream.of(new String[]{"a", "b", "c"});
Stream<String> strStream = Arrays.stream(new String[]{"a", "b", "c"});
Stream<String> strStream = Arrays.stream(new String[]{"a", "b", "c"}, 0, 3);
IntStream, LongStream에는 지정된 범위의 연속된 정수를 스트림으로 생성해서 반환하는 range(), rangeClosed()를 가지고 있다.
IntStream.range(int begin, int end)
IntStream.rangeClosed(int begin, int end)
>>
IntStream.range(1, 5) // 1, 2, 3, 4
IntStream.rangeClosed(1, 5) // 1, 2, 3, 4, 5
4 - 3 임의의 수
난수를 생성하는 Random클래스에는 아래와 같은 인스턴스 메서드들이 포함되어 있다.
해당 타입의 난수들로 이루어진 스트림을 반환한다.
IntStream ints()
LongStream longs()
DoubleStream doubles()
이 메서드들이 반환하는 스트림은 크기가 정해지지 않은 무한스트림(infinite stream)이므로,
limit()도 같이 사용해서 스트림의 크기를 제한해 주어야 한다.
limit는 스트림의 개수를 지정하며, 무한 스트림을 유한 스트림으로 만든다.
IntStream int Stream = new Random().ints(); // 무한 스트림
IntStream.limit(5).forEach(sysout::println); // 5개의 요소만 출력
4 - 4 람다식 - iterate(), generate()
Stream클래스의 iterate()와 generate()는 람다식을 매개변수로 받아서,
이 람다식에 의해 계산되는 값들을 요소로 하는 무한스트림을 생성한다.
static <T> Stream<T> iterate(T seed, UnaryOperator<T> f)
static <T> Stream<T> generate(Supplier<T> s)
iterate()는 seed로 지정된 값부터 시작해서, 람다식 f에 의해 계산된 결과를 다시 seed값으로 해서 계산을 반복한다
Stream<Integer> evenStream = Stream.iterate(0, n -> n+2); // 0, 2, 4, 6, 8, ...
generate()도 람다식에 의해 계산되는 값을 요소로 하는 무한스트림을 생성해서 반환하지만,
이전 결과를 이용해서 다음요소를 계산하지는 않는다.
generate()에 정의된 매개변수 타입은 Supplier<T>이므로 매개변수가 없는 람다식만 허용된다.
Stream<Integer> oneStream = Stream.generate(() -> 1);
Stream<Double> randomStream = Stream.generate(Math::random);
한 가지 주의할 점은 iterate와 generate에 의해 생성된 스트림을 기본형 스트림타입의 참조변수로 다룰 수 없다.
IntStream evenStream = Stream.iterate(0, n -> n+2); // 에러
굳이 필요하다면 mapToInt()같은 메서드로 변환해야 한다.
IntStream evenStream = Stream.iterate(0, n -> n+2).mapToInt(Integer::valueOf);
4 - 5 파일
java.nio.file.Files는 파일을 다루는데 필요한 유용한 메서드들을 제공하는데,
list()는 지정된 디렉토리(dir)에 있는 파일의 목록을 소스로 하는 스트림을 생성해서 제공한다
참고 Path는 하나의 파일 또는 경로를 의미한다.
Stream<Path> Files.list(Path dir)
그리고, 파일의 한 행을 요소로 하는 스트림을 생성하는 메서드도 있다.
아래의 세번째 메서드는 BufferedReader클래스에 속한 것인데,
파일 뿐 아니라 다른 입력대상으로부터도 데이터를 행단위로 읽어올 수 있다.
Stream<String> Files.lines(Path path)
Stream<String> Files.lines(Path path, Charset cs)
Stream<String> lines // BufferedReader클래스의 메서드
4 - 6 빈 스트림
요소가 하나도 없는 비어있는 스트림을 생성할 수도 있다.
스트림에 연산을 수행한 결과가 하나도 없을 때, null보다 빈 스트림을 반환하는 것이 낫다.
Stream emptyStream = Stream.empty() // empty는 빈 스트림을 생성해서 반환
long count = emptyStream.count() // count의 값은 0
4 - 7 두 스트림의 연결
Stream의 static메서드인 concat()을 이용하면, 두 스트림을 하나로 연결할 수 있다.
물론 두 스트림의 요소는 같은 타입이어야 한다.
String[] str1 = {"123", "456", "789"};
String[] str2 = {"ABC", "abc", "DEF"};
Stream<String> strs1 = Stream.of(str1);
Stream<String> strs2 = Stream.of(str2);
Stream<String> strs3 = Stream.concat(strs1, strs2);
5. 스트림의 중간 연산
5 - 1 스트림 자르기 skip(), limit()
Stream<T> skip(long n)
Stream<T> limit(long maxSize)
skip(3)은 처음 3개의 요소를 건너뛰고, limit(5)는 스트림의 요소를 5개로 제한한다.
IntStream intStream = IntStream.rangeClosed(1, 10); // 1~10
intStream.skip(3).limit(5).forEach(sysout::print) // 45678
5 - 2스트림 요소 걸러내기 filter(), distinct()
distinct는 중복 요소 제거, filter는 주어진 조건(Predicate)에 맞지 않는 요소 제거
IntStream intStream = IntStream.of(1, 2, 2, 3, 3, 3, 4, 5, 5, 6);
intStream.distinct().forEach(sysout.print); // 123456
filter는 매개변수로 Predicate를 필요로 하는데, 연산결과가 boolean인 람다식을 사용해도 된다.
IntStream intStream = IntStream.rangeClosed(1, 10); // 1~10
intStream.filter(i => i % 2 == 0).forEach(sysout::print); // 246810
필요하다면 filter를 다른 조건으로 여러번 사용할 수도 있다.
intStream.filter(i => i%2 != 0).filter(i => i%3 != 0).forEach(sysout::print); // 157
5 - 3정렬 sorted()
스트림을 정렬할 때는 sorted()를 사용하면 된다
Stream<T> sorted()
Stream<T> sorted(Comparator<? super T> Comparator)
sorted는 지정된 Comparator로 스트림을 정렬하는데,
Comparator 대신 int값을 반환하는 람다식을 사용하는 것도 가능하다.
Comparator를 지정하지 않으며 스트림 요소의 기본정렬 기준으로 정렬한다
단, 스트림의 요소가 Comparable을 구현한 클래스가 아니면 예외 발생
5 - 4변환 map()
스트림의 요소에 저장된 값 중에서 원하는 필드만 뽑아내거나, 특정 형태로 변환해야 할 때 사용
매개변수로 T타입을 R타입으로 변환해서 반환하는 함수를 지정해야 한다
Stream<R> map(Function<? super T, ? extends R> mapper)
예를 들어 File의 스트림에서 파일의 이름만 뽑아서 출력하고 싶을 때, 아래와 같이 map()을 이용하면 File객체에서 파일의 이름(String)만 간단히 뽑아낼 수 있다.
Stream<File> fileStream = Stream.of(new File("Ex1.java"), new File("Ex1"), new File("Ex1.bak"), new File("Ex2.java"), new File("Ex1.txt"));
Stream<String> filenameStream = fileStream.map(File::getName); // map()으로 Stream<File>을 Stream<String>으로 변환
filenameStream.forEach(sysout::println); // 스트림의 모든 파일 이름을 출력
map역시 중간 연산이므로, 연산 결과는 String을 요소로 하는 스트림이다.
그리고 map도 filter처럼 하나의 스트림에 여러번 적용할 수 있다.
다음 문장은 File의 스트림에서 파일의 확장자만을 뽑은 다음 중복을 제거해서 출력한다.
fileStream.map(File::getName) // Stream<File> > Stream<String>
.filter(s => s.indexOf('.') != -1) // 확장자가 없는 것은 제외
.map(s => s.substring(s.indexOf('.') + 1)) // Stream<String> > Stream<String> ?? 걍 확장자 추출하는 거 같은데 왜 이래 설명돼있는 거지
.map(String::toUpperCase) // 모두 대문자로 변환
.distinct() // 중복 제거
.forEach(sysout::print); // JAVABAKTXT
5 - 5 조회 peek()
연산과 연산 사이 올바르게 처리되었는지 확인하고 싶으면 사용
forEach랑 다르게 스트림의 요소를 소모하지 않으므로 연산 사이에 여러번 끼워도 됨
fileStream.map(File::getName) // Stream<File> >> Stream<String>
.filter(s > s.indexOf('.') != -1) // 확장자 없는 거 제외
.peek(s=>sysout("filename=%s%n", s)) // 파일 명 출력
.map(s => s.substring(s.indexOf('.') + 1)) // 확장자만 추출
.peek(s=>sysout("extension=%s%n", s)) // 확장자를 출력
.forEach(sysout::println);
5 - 6 mapToInt(), mapToLong(), mapToDouble()
map()은 연산의 결과로 Stream<T>타입의 스트림을 반환하는데,
스트림의 요소를 숫자로 변환하는 경우 IntStream과 같은 기본형 스트림으로 변환하는 것이 더 유용할 수 있다.
Stream<T>타입의 스트림을 기본형 스트림으로 변환할 때 사용하는 것
DoubleStream mapToDouble(ToDoubleFunction<? super T> mapper)
IntStream mapToInt(ToDoubleFunction<? super T> mapper)
LongStream mapToLong(ToDoubleFunction<? super T> mapper)
studentStream에서 스트림에 포함된 모든 학생의 성적을 합산해야 한다면,
Stream<Integer> studentScoreStream = studentStream.map(Student::getTotalScore);
>>
IntStream studentScoreStream = studentStream.mapToInt(Student::getTotalScore);
int allTotalScore = studentScoreStream.sum(); // int sum();
count()만 지원하는 Stream<T>와 달리 IntStream같은 기본형 스트림은 숫자를 다루는데 편리한 메서드를 제공한다
int sum() 스트림의 모든 요소의 총합
OptionalDouble average() sum() / (double)count()
OptionalInt max() 스트림 요소 중 제일 큰 값
OptionalInt min() 스트림 요소 중 제일 작은 값
참고 max와 min은 Stream에도 정의되어 있지만, 매개변수로 Comparator를 지정해야 한다는 차이가 있다.
스트림의 요소가 하나도 없을 때 sum()은 0은 출력하면 되지만 다른 메서드들은 그럴 수 없다.
예를 들면 요소가 없어서 평균이 0인지 다양한 요소들의 평균을 구했더니 0이 나온건지 구별할 수 없기 때문이다.
구별하기 위해 단순히 double값을 반환하는 게 아니라 double타입 값을 내부적으로 가지고 있는 OptionalDouble을 반환하는 것이다.
이 메서드들은 최종 연산이기 때문에, 호출 후에 스트림이 닫힘에 주의. 최종연산이니까 당연히 연속 호출도 안됨
IntStream scoreStream = studentStream.mapToInt(Student::getTotalScore);
long totalScore = scoreStream.sum(); // 최종연산 sum때문에 스트림이 닫힘
OptionalDouble average = scoreStream.average(); // 에러, 스트림이 이미 닫힘
sum과 average를 모두 호출해야할 때, 또 만들어야되서 불편하다
그래서 summaryStatistics()가 제공된다
IntSummaryStatistics stat = scoreStream.summaryStatistics();
long totalCount = stat.getCount();
long totalScore = stat.getSum();
double avgScore = stat.getAverage();
int minScore = stat.getMin();
int maxScore = stat.getMax();
IntStream을 Stream<T>로 변환할 때는 mapToObj()를,
Stream<Integer>로 변환할 때는 boxed()를 사용한다
Stream<U> mapToObj(IntFunction<? extends U> mapper)
Stream<Integer> boxed()
참고로 CharSequence에 정의된 chars()는 String이나 StringBuffer에 저장된 문자들을 intStream으로 다룰 수 있게 해준다.
IntStream charStream = "12345".chars(); // default IntStream chars()
int charSum = charStream.(ch => ch-'0').sum(); // charSum = 15
위 코드에서 사용된 map()은 IntStream에 정의된 것으로 IntStream을 결과로 반환한다.
그리고 mapToInt와 함께 자주 쓰는 메서드는 Integer의 parseInt(), valueOf()가 있다.
Stream<String> > IntStream으로 변환할 때, mapToInt(Integer::parseInt)
Stream<Integer> > IntStream으로 변환할 때, mapToInt(Integer::intValue)
5 - 7 flatMap() - Stream<T[]>를 Stream<T>로 변환
스트림의 요소가 배열이거나 map()의 연산결과가 배열인 경우,
즉 스트림의 타입이 Stream<T[]>인 경우, Stream<T>로 바꾸는 것이 더 편할 때가 있다.
그럴 때는 map() 대신 flatMap()을 사용하면 된다
예시
Stream<String[]> strArrStrm = Stream.of(new String[]{"abc", "def", "ghi"}, new String[]{"ABC", "DEF", "GHI"});
각 요소의 문자열들을 합쳐서 문자열이 요소인 스트림, 즉 Stream<Stirng>으로 변환하려면 어떻게 해야할까?
스트림의 요소를 변환해야 하니까 일단 map()을 써야할 것이고, 여기에 배열을 스트림으로 만들어주는 Arrays.stream(T[])를 함께 사용해보자
Stream<Stream<String>> strStrStrm = strArrStrm.map(Arrays::stream);
예상한 것과 달리, Stream<Stirng[]>을 map을 통해 변환한 결과는 Stream<Stirng>이 아니라, Stream<Stream<Stirng>>이다.
각 요소의 문자열들이 합쳐지지 않고, 스트림의 스트림 형태가 되어버렸다.
이 때 map 대신 flatMap을 사용하면 해결된다.
5 - 8 Optional<T>와 OptionalInt
앞서 잠시 언급된 것과 같이 최종 연산의 결과 타입이 Optional인 경우가 있다.
Optional
Optional<T>은 지네릭 클래스로 T타입의 객체를 감싸주는 래퍼 클래스이다.
그래서 Optional타입의 객체에는 모든 타입의 참조변수를 담을 수 있다.
jdk1.8부터 추가
public final class Optional<T>{
private final T value; // T타입의 참조변수
~~~
}
최종 연산의 결과를 그냥 반환하는 게 아니라 Optional객체에 담아서 반환하는 것이다.
이처럼 객체에 담아서 반환하면, 결과가 null인지 매번 if문으로 체크하는 대신,
Optional에 정의된 메서드를 통해 간단히 처리할 수 있다.
이제 널 체크를 위한 if문 없이도 NullPointerException이 발생하지 않는 보다 간결하고 안전한 코드를 작성하는 것이 가능해졌다.
참고 Objects클래스에 있는 isNull(), requireNonNull()같은 메서드가 있는 것도 널 체크를 위한 if문을 메서드 안으로 넣어서 코드의 복잡도를 낮추기 위한 것이다.
Optional객체 생성하기
of() 또는 ofNullable()을 사용한다.
String str = "abc";
Optional<String> optVal = Optional.of(str);
Optional<String> optVal = Optional.of("abc");
Optional<String> optVal = Optional.of(new String("abc"));
만일 참조변수 값이 null일 가능성이 있으면, of() 대신 ofNullable()을 사용해야 한다.
of는 매개변수의 값이 null이면 NullPointerException이 발생하기 때문이다.
Optional<T> 타입의 참조변수를 기본값으로 초기화할 때는 empty()를 사용한다.
null로 초기화하는 것이 가능하지만, empty()로 초기화하는 것이 바람직하다.
참고 empty() 지네릭메서드라 앞에 <T>를 붙였다. 추정 가능하므로 생략할 수 있다
Optional<String> optVal = null; // 널로 초기화
Optional<String> optVal = Optional.<String>empty(); // 빈객체로 초기화
Optional객체의 값 가져오기
get() 사용, 값이 null이면 NoSuchElemntException 발생, 이를 대비해서 orElse()로 대체 가능
Optional<String> optVal = Optional.of("abc");
String str1 = optVal.get(); // optVal에 저장된 값 반환, null이면 예외 발생
String str2 = optVal.orElse(""); // optVal에 저장된 값이 null이면 ""반환
orElse의 변형으로는 null을 대체할 값을 반환하는 람다식을 지정할 수 있는 orElseGet()과,
null일 때 지정된 예외를 발생시키는 orElseThrow()가 있다.
T orElseGet(Supplier<? extends T> other)
T orElseThrow(Supplier<? extends X> exceptionSupplier)
사용하는 방법
String str3 = optVal2.orElseGet(String::new); // () => new String()과 동일
String str4 = optVal2.orElseThrow(NullPointerException::new) // 널이면 예외발생
Stream처럼 Optional객체에도 filter(), map(), flatMap()을 사용할 수 있다.
map()의 연산결과가 Optional<Optional<T>>일 때, flatMap을 사용하면 Optional<T>를 결과로 얻는다.
만일 Optional객체의 값이 null이면, 이 메서드들은 아무 일도 하지 않는다.
우리가 이미 알고 있는 parseInt()는 예외가 발생하기 쉬운 메서드이다.
int result = Optional.of("123").filter(x => x.length() > 0).map(Integer::parseInt).orElse(-1); // result = 123
result = Optional.of("").filter(x => x.length() > 0).map(Integer::parseInt).orElse(-1); // result = -1
만일 예외처리된 메서드를 만든다면 다음과 같을 것이다.
static int optStrToInt(Optional<String> optStr, int defaultValue){
try{
return optStr.map(Integer::parseInt).get();
} catch (Exception e){
return defaultValue;
}
}
isPresent()는 Optional객체의 값이 null이면 false를, 아니면 true를 반환한다.
ifPresent(Consumer<T> block)은 값이 있으면 주어진 람다식을 실행하고, 없으면 아무 일도 하지 않는다
if(str != null){
sysout(str);
}
만일 위와 같은 조건문이 있다면,
if(Optional.ofNullable(str).isPresent()){
sysout(str);
}
이렇게 쓸 수 있다
ifPresent를 이용해서 더 간단히 할 수 있다.
아래의 문장은 참조변수 str이 null이 아닐 때만 값을 출력하고, null이면 아무 일도 일어나지 않는다.
Optional.ofNullable(str).ifPresent(sysout::println);
ifPresent는 Optional<T>를 반환하는 findAny()나 findFirst()와 같은 최종 연산과 잘 어울린다.
Stream 클래스에 정의된 메서드 중, Optional<T>를 반환하는 목록
Optional<T> findAny()
Optional<T> findFirst()
Optional<T> max(comparator<? super T> comparator)
Optional<T> min(comparator<? super T> comparator)
Optional<T> reduce(BinaryOperator<T> accumulator)
이처럼 Optional<T>를 결과로 반환하는 최종 연산 메서드들은 몇 개 없다.
심지어 max와 min같은 메서드들은 reduce를 이용해 작성된 것이다
OptionalInt, OptionalLong, OptionalDouble
IntStream과 같은 기본형 스트림에는 Optional도 기본형을 값으로 하는 OptionalInt, OptionalLong, OptionalDouble을 반환한다.
IntStream에 정의된 메서드들
OptionalInt findAny()
OptionalInt findFirst()
OptionalInt reduce(IntBinaryOperator op)
OptionalInt max()
OptionalInt min()
OptionalDouble average()
반환 타입이 Optional<T>가 아니라는 것을 제외하고는 Stream에 정의된 것과 비슷하다.
Optional객체의 경우 null을 저장하면 비어있는 것과 동일 취급한다.
6. 스트림의 최종 연산
최종 연산은 스트림의 요소를 소모해서 결과를 만들어낸다.
그래서 최종 연산 후에는 스트림이 닫히게 되고 더 이상 사용할 수 없다.
최종 연산의 결과는 스트림 요소의 합과 같은 단일 값이거나, 스트림의 요소가 담긴 배열 또는 컬렉션일 수 있다.
6 - 1 조건 검사 - allMatch(), anyMatch(), noneMatch(), findFirst(), findAny()
스트림의 요소에 대해 지정된 조건에 모든 요소가 일치하는 지,
일부가 일치하는지 아니면 어떤 요소도 일치하지 않는지 확인하는데 사용할 수 있는 메서드들
모든 메서드가 매개변수로 Predicate를 요구하며, 연산결과로 Boolean을 반환한다.
예를 들어 학생들의 성적 정보 스트림에서 총점이 낙제점(총점 100이하)인 학생이 있는 지 확인하는 방법
boolean noFailed = stuStream.anyMath(s => s.getTotalScore() <= 100)
filter()와 함께 사용되어 조건에 맞는 스트림의 요소가 있는지 확인할 때,
병렬 스트림면 findAny, 아니면 findFirst 사용
참고 비어있는 Optional객체는 내부적으로 null을 저장하고 있다.
6 - 2 통계 - count(), sum(), average(), max(), min()
앞서 살펴본 것처럼 IntStream같은 기본형 스트림에는 스트림의 요소들에 대한 통계 정보를 얻을 수 있는 메서드들이 있다.
그러나 기본형 스트림이 아닌 경우에는 통계와 관련된 메서드들이 아래 3개 뿐이다.
참고 기본형 스트림의 min, max와 달리 매개변수로 Comparator를 필요로 함
long count()
Optional<T> max(Comparator<? super T> comparator)
Optional<T> min(Comparator<? super T> comparator)
대부분의 경우 위 메서드를 사용하기보다 기본형 스트림으로 변환하거나, reduce()와 collect()를 사용
6 - 3 리듀싱 - reduce()
스트림의 요소를 줄여나가며 연산을 수행하고 최종 결과 반환
그래서 매개변수의 타입이 BinaryOperator<T>다.
처음 두 요소를 가지고 연산한 결과를 가지고 그 다음 요소와 연산한다.
이 외에도 연산결과의 초기값을 갖는 reduce도 있는데,
이 메서드들은 초기값과 스트림의 첫 번째 요소로 연산을 시작
스트림의 요소가 하나도 없는 경우, 초기값이 반환되므로, 반환 타입이 Optional<T>가 아니라 T이다.
참고 BinaryOperator<T>는 BiFunction의 자손이며, BiFunction<T, T, T>와 동등하다.
reduce()로 스트림의 모든 요소를 다 더하는 과정을 for문으로 표현
int a = identity; // 초기값을 a에 저장
for(int b : stream)
a = a + b; // 모든 요소의 값을 a에 누적
for문을 보고 나면, reduce가 아마 다음과 같이 작성되어 있을 것이라 추측하는 것은 쉬울 것이다.
T reduce(T identity, BinaryOperator<T> accumulator){
T a = identity;
for(T b : stream)
a = accumulator.apply(a, b);
return a;
}
여기서
~~~.reduce(0, (a, b) -> a + b);
이렇게 하면 된다.
reduce를 사용하는 방법은 간단하다.
그저 초기값(identity)과 어떤 연산(BinaryOperator)으로 스트림의 요소를 줄여나갈 것인지만 결정하면 된다.
6 - 4 collect()
스트림의 최종 연산중에서 가장 복잡하면서도 유용하게 활용될 수 있는 것이 collect()이다.
collect()가 스트림의 요소를 수집하려면, 어떻게 수집할 것인가에 대한 방법이 정의되어 있어야 하는데,
이 방법을 정의한 것이 컬렉터(collector)
컬렉터는 Collector인터페이스를 구현한 것으로, 직접 구현할 수도 있고 미리 작성된 것을 사용할 수도 있다.
Collectors 클래스는 미리 작성된 다양한 종류의 컬렉터를 반환하는 static메서드를 가지고 있다.
collect() 스트림의 최종 연산, 매개변수로 컬렉터를 필요로 한다.
Collector 인터페이스, 컬렉터는 이 인터페이스를 구현해야 한다
Collectors 클래스, static메서드로 미리 작성된 컬렉터를 제공한다.
스트림을 컬렉션과 배열로 변환 - toList(), toSet(), toMap(), toCollection(), toArray()
스트림의 모든 요소를 컬렉션에 수집하려면, Collectors클래스의 toList()같은 메서드를 사용
List나 Set이 아닌 특정 컬렉션을 지정하려면, toCollection()에 해당 컬렉션의 생성자 참조를 매개변수로 넣어준다.
List<String> names = stuStream.map(Student::getName).collect(Collectors.toList());
ArrayList<String> list = names.stream().collect(Collectors.toCollection(ArrayList::new));
Map은 키와 값의 쌍으로 저장해야 하므로 객체의 어떤 필드를 키로 사용할지, 값으로 사용할지를 지정해야한다
Map<String, Person> map = personStream.collect(Collectors.toMap(p => p.getRegId(), p => p));
위 문장은 요소 타입이 Person인 스트림에서 사람의 주민번호(regId)를 키로 하고, 값으로 Person객체를 그대로 저장
스트림에 저장된 요소들을 T[]타입의 배열로 변환하려면, toArray()를 사용하면 된다.
단, 해당 타입의 생성자 참조를 매개변수로 지정해줘야 한다.
만일 매개변수를 지정하지 않으면 반환되는 배열의 타입은 Object[]
Student[] stuNames = studentStream.toArray(Student[]::new); // OK
Student[] stuNames = studentStream.toArray(); // 에러
Object[] stuNames = studentStream.toArray(); // OK
6 - 5 통계 - counting(), summingInt(), averagingInt(), maxBy(), minBy()
앞서 살펴본 최종 연산들이 제공하는 통계 정보를 collect()로 똑같이 얻을 수 있다.
collect를 사용하지 않아도 쉽게 얻어지는데 굳이 보여주는 것은 사용법을 보여주기 위함이다.
groupingBy()와 함께 사용할 때 이 메서드들이 필요하다.
Collectors의 static메서드를 호출할 때는 Collectors를 생략하였다. static import되어 있다고 가정하자
long count = stuStream.count();
long count = stuStream.collect(counting()); // Collectors.counting();
long totalScore = stuStream.mapToInt(Student::getTotalScore).sum();
long totalScore = stuStream.collect(summingInt(Student::getTotalScore));
OptionalInt topScore = studentStream.mapToInt(Student::getTotalScore).max();
Optional<Student> topStudent = stuStream.max(Comparator.comparingInt(Student::getTotalScore));
Optional<Student> topStudent = stuStream.collect(maxBy(Comparator.comparingInt(Student::getTotalScore)));
IntSummaryStatistics stat = stuStream.mapToInt(Student::getTotalScore).summaryStatistics();
IntSummaryStatistics stat = stuStream.collect(summarizingInt(Student::getTotalScore));
summingInt와 summarizingInt를 혼동하지 않도록 주의
6 - 6 리듀싱 - reducing()
리듀싱 역시 collect()로 가능하다.
intStream에는 매개변수 3개짜리 collect()만 정의되어 있으므로,
boxed()를 통해 IntStream을 Strema<Integer>로 변환해야 매개변수 1개짜리 collect()를 쓸 수 있다.
Collectors.reducing()에는 3가지 종류가 있따. 세번째 메서드만 제외하고 reduce()와 같다.
세 번째 것은 map()과 reduce()를 하나로 합쳐놓은 것이다.
Collector reducing(BinaryOperator<T> op)
Collector reducing(T identity, BinaryOperator<T> op)
Collector reducing(U identity, Function<T, U> mapper, BinaryOperator<U> op)
메서드 목록의 와일드 카드를 제거하여 간략히 하였다.
6 - 7 문자열 결합 - joining()
문자열 스트림의 모든 요소를 하나의 문자열로 연결해서 반환한다.
구분자를 지정할 수 있고, 접두사와 접미사도 지정 가능
스트림의 요소가 String, StringBuffer처럼 CharSequence의 자손인 경우에만 결합 가능
스트림의 요소가 문자열이 아니면 map()을 이용해 먼저 스트림의 요소를 문자열로 변환해야 한다.
String studentNames = stuStream.map(Student::getName).collect(joining());
String studentNames = stuStream.map(Student::getName).collect(joining(","));
String studentNames = stuStream.map(Student::getName).collect(joining(",", "[", "]"));
만일 map없이 스트림에 바로 joining하면, 스트림의 요소에 toString()을 호출한 결과를 결합한다.
6 - 8 그룹화와 분할 - groupingBy(), partitioningBy()
그룹화는 스트림의 요소를 특정 기준으로 그룹화하는 것을 의미
분할은 스트림의 요소를 두 가지,
지정된 조건에 일치하는 그룹과 일치하지 않는 그룹으로의 분할을 의미
groupingBy()는 스트림의 요소를 Function으로, partitioningBy()는 Predicate로 분류
Collector groupingBy(Function classifier)
Collector groupingBy(Function classifier, Collector downstream)
Collector groupingBy(Function classifier, Supplier mapFactory, Collector downstream)
Collector partitioningBy(Predicate predicate)
Collector partitioningBy(Predicate predicate, Collector downstream)
분류를 어떻게 하냐의 차이만 있을 뿐 동일하다.
스트림을 두 개의 그룹으로 나루면 partitioningBy, 그 외에는 groupingBy 사용
그룹화와 분할의 결과는 Map에 담겨 반환된다.
6 - 9 Collector 구현하기
컬렉터를 작성한다는 것은 Collector 인터페이스를 구현한다는 것을 의미
public interface Collector<T, A, B>{
Supplier<A> supplier(); // 작업 결과를 저장할 공간 제공
BiConsumer<A, T> accumulator(); // 스트림의 요소를 수집(collect)할 방법 제공
BinaryOperator<A> combiner(); // 두 저장공간을 병합할 방법 제공(병렬 스트림)
Function<A, R> finisher(); // 결과를 최종적으로 변환할 방법 제공, 변환이 필요없다면 항등함수 Function.identity()를 반환하면 된다.
Set<Characteristics> Characteristics(); // 컬렉터의 특성이 담긴 Set을 반환
}
직접 구현해야하는 것은 5개의 메서드인데, characteristics를 제외하면 모두 반환 타입이 함수형 인터페이스다.
즉, 4개의 람다식을 작성하면 되는 것이다.
characteristics()는 컬렉터가 수행하는 작업의 속성에 대한 정보를 제공하기 위한 것
Characteristics.CONCURRENT 병렬로 처리할 수 있는 작업
Characteristics.UNORDERED 스트림의 요소의 순서가 유지될 필요가 없는 작업
Characteristics.IDENTITY_FINISH finisher()가 항등 함수인 작업
참고 열거형 Characteristics는 Collector내에 정의되어 있다.
아무런 속성도 지정하고 싶지 않을 때
Set<Characteristics> Characteristics(){
return Collections.emptySet(); // 지정할 특성이 없는 경우 비어있는 Set 반환
}