[자바] 스트림

June·2021년 2월 16일
1

자바

목록 보기
34/36

스트림이란?

지금까지 우리는 많은 수의 데이터를 다룰 때, 컬렉션이나 배열에 데이터를 담고 원하는 결과를 얻기 위해 for문과 Iterator를 이용해서 코드를 작성해왔다. 그러나 이러한 방식으로 작성된 코드는 너무 길고 알아보기 어렵다. 그리고 재사용성도 떨어진다.

또 다른 문제는 데이터 소스마다 다른 방식으로 다뤄야한다는 것이다. Collection이나 Iterator 같은 인터페이스를 이용해서 컬렉션을 다루는 방식을 표준화하기는 했지만, 각 컬렉션 클래스에는 같은 기능의 메서드들이 중복해서 정의되어 있다. 예를 들어 List를 정렬할 때는 Collections.sort()를 사용해야하고, 배열을 Collections.sort()를 사용해야하고, 배열을 정렬할 때는 Arrays.sort()를 사용해야 한다.

이러한 문제점들을 해결하기 위해서 만든 것이 스트림(Stream)이다. 스트림은 데이터 소스를 추상화하고, 데이터를 다루는데 자주 사용되는 메서드들을 정의해 놓았다. 데이터 소스를 추상화하였다는 것은 데이터 소스가 무엇이던 간에 같은 방식으로 다룰 수 있게 되었다는 것과 코드의 재사용성이 높아진다는 것을 의미한다. 스트림을 이용하면, 배열이나 컬레션뿐만 아니라 파일에 저장된 데이터도 모두 같은 방식으로 다룰 수 있다.

예를 들어, 문자열 배열과 같은 내용의 문자열을 저장하는 List가 있을 때,

String[] strArr = {"aaa", "ddd", "ccc"};
List<String> strList = Arrays.asList(strArr);

이 두 데이터 소스를 기반으로 하는 스트림은 다음과 같이 생성한다.

Stream<String> strStream1 = strList.stream();
Stream<String> strStream2 = Arryas.stream(strArr);

이 두 스트림으로 데이터 소스의 데이터를 읽어서 정렬하고 화면에 출력하는 방법은 다음과 같다. 데이터 소스가 정렬되는 것은 아니라는 것에 유의하자.

strStream1.sorted().forEach(System.out::println);
strStream2.sorted().forEach(System.out::println);

두 스트림의 데이터 소스는 서로 다르지만, 정렬하고 출력하는 방법은 완전히 동일하다.

Arrays.sort(strArr);
Collections.sort(strList);

for (String str : strArr)
    System.out.println(str);

for (String str : strList)
    System.out.println(str);

스트림은 데이터 소스를 변경하지 않는다

스트림은 데이터 소스로부터 데이터를 읽기만 할 뿐, 데이터 소스를 변경하지 않는다는 차이가 있다. 필요하다면, 정렬된 결과를 컬렉션이나 배열에 담아서 반환할 수도 있다.

//정렬된 결과를 새로운 List에 담아서 반환한다.
List<String> sortedList = strStream2.sorted().collect(Collectors.toList());

스트림은 일회용이다

스트림은 Iterator처럼 일회용이다. Iterator로 컬렉션의 요소를 모두 읽고 나면 다시 사용할 수 없는 것처럼, 스트림도 한번 사용하면 닫혀서 다시 사용할 수 없다. 필요하다면 스트림을 다시 생성해야 한다.

strStream1.sorted().forEach(System.out::println);
int numOfStr = strStream1.count(); //에러. 스트림이 이미 닫혔음.

스트림은 작업을 내부 반복으로 처리한다.

스트림을 이용한 작업이 간결할 수 있는 비결중의 하나가 바로 '내부 반복'이다. 내부 반복이라는 것은 반복문을 메서드의 내부에 숨길 수 있다는 것을 의마한다. forEach()는 스트림에 정의된 메서드 중의 하나로 매개변수에 대입된 람다식을 데이터 소스의 모든 요소에 적용한다.

스트림의 연산

스트림이 제공하는 연산은 중간 연산과 최종 연산으로 분류할 수 있는데, 중간 연산은 연산결과를 스트림으로 반환하기 때문에 중간 연산을 연속해서 연결할 수 있다. 반면에 최종 연산은 스트림의 요소를 소모하면서 연산을 수행하므로 단 한번만 연산이 가능하다.

지연된 연산

스트림 연산에서 한 가지 중요한 점은 최종 연산이 수행되기 전까지는 중간 연산이 수행되지 않는다는 것이다. 스트림에 대해 distinct()나 sort() 같은 중간 연산을 호출해도 즉각적인 연산이 수행되는 것은 아니라는 것이다. 중간 연산을 호출하는 것은 단지 어떤 작업이 수행되어야하는지를 지정해주는 것일 뿐이다. 최종 연산이 수행되어야 비로소 스트림의 요소들이 중간 연산을 거쳐 최종 연산에서 소모된다.

Stream<Integer>와 IntStream

요소의 타입이 T인 스트림은 기본적으로 Stream<T>이지만, 오토박싱&언박싱으로 인한 비효율을 줄이기 위해 데이터 소스의 요소를 기본형으로 다루는 스트림, IntStream, LongStream, DoubleStream이 제공된다.

병렬 스트림

스트림으로 데이터를 다룰 때의 장점 중 하나가 바로 병렬 처리가 쉽다는 것이다. 병렬 스트림은 내부적으로 fork&join 프레임웍을 이용해서 자동적으로 연산을 병렬로 수행한다. 우리가 할일이라고는 그저 스트림에 parallel()이라는 메서드를 호출해서 병렬로 연산을 수행하도록 지시하면 될 뿐이다. 반대로 병렬로 처리되지 않게 하려면 sequential()을 호출하면 된다. 모든 스트림은 기본적으로 병렬 스트림이 아니므로 sequential()을 호출할 필요가 없다. 이 메서드는 parallel()을 호출한 것을 취소할 때만 사용한다.

병렬 처리가 항상 더 빠른 결과를 얻게 해주는 것이 아니다.

스트림 만들기

스트림의 소스가 될 수 있는 대상은 배열, 컬렉션, 임의의 수 등 다양하다.

컬렉션

컬렉션의 최고 조상인 Collectionstream()이 정의되어 있다. 그래서 Collection의 자손인 List와 Set을 구현한 컬렉션 클래스들은 모두 이 메서드로 스트림을 생성할 수 있다. stream()은 해당 컬렉션을 소스로 하는 스트림을 반환한다.

Stream<T> Collection.stream()

예를 들어 List로부터 스트림을 생성하는 코드는 다음과 같다.

List<Integer> List = Arrays.asList(1,2,3,4,5);
Stream<Integer> intStream = list.stream();

forEach()는 지정된 작업을 스트림의 모든 요소에 대해 수행한다.

intStream.forEach(System.out::println); //스트림의 모든 요소를 출력한다.
intStream.forEach(System.out::println); //에러. 스트림이 이미 닫혔다. 

한 가지 주의할 점은 forEach()가 스트림의 요소를 소모하면서 작업을 수행하므로 같은 스트림에 forEach()를 두 번 호출할 수 없다는 것이다. 그래서 스트림의 요소를 한번 더 출력하려면 스트림을 새로 생성해야 한다. forEach()에 의해 스트림의 요소가 소모되는 것이지, 소스의 요소가 소모되는 것은 아니기 때문에 같은 소스로부터 다시 스트림을 생성할 수 있다.

배열

배열을 소스로 하는 스트림을 생성하는 메서드는 다음과 같이 Stream과 Arrays에 static 메서드로 정의되어있다.

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  IntStream.range(int begin, int end)
IntStream  IntStream.rangeClosed(int begin, int end)

range()의 경우 경계의 끝인 end가 범위에 포함되지 않고, rangeClosed()의 경우는 포함된다.

IntStream intStream = IntStream.range(1,5); //1,2,3,4
IntStream intStream = IntStream.rangeClosed(1,5); //1,2,3,4,5

임의의 수

난수를 생성하는데 사용하는 Random 클래스에는 아래와 같은 인스턴스 메서드들이 포함되어 있다. 이 메서드들은 해당 타입의 난수들로 이루어진 스트림을 반환한다.

IntStream ints()
LongStream longs()
DoubleStream doubles()

이 메서드들이 반환하는 스트림은 크기가 정해지지 않은 '무한 스트림(infinite stream)'이므로 limit()도 같이 사용해서 스트림의 크기를 제한해 주어야 한다. limit()은 스트림의 개수를 지정하는데 사용되며, 무한 스트림을 유한 스트림으로 만들어 준다.

IntStream intStream = new Random().ints(); //무한 스트림
intStream.limit(5).forEach(System.out::println); //5개의 요소만 출력

람다식 - 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값으로 해서 계산을 반복한다. 아래의 evenStream은 0부터 시작해서 값이 2씩 계속 증가한다.

Stream<Integer> evenStream = Stream.iterate(0, n -> n+2); //0, 2, 4, 6

generate()도 iterate()처럼, 람다식에 의해 계산되는 값을 요소로하는 무한 스트림을 생성해서 반환하지만, iterate()와 달리, 이전 결과를 사용해서 다음 요소를 계산하지 않는다.

Stream<Double> randomStream = Stream.generate(Math::random);
Stream<Integer> oneStream = Stream.generate(() -> 1);

그리고 generate()에 정의된 매개변수의 타입은 Supplier<T>이므로 매개변수가 없는 람다식만 허용된다. 한 가지 주의할 점은 iterate()와 generate()에 의해 생서된 스트림을 아래와 같이 기본형 스트림 타입의 참조변수로 다룰 수 없다는 것이다.

IntStream evenStream  = Stream.iterate(0, n->n+2); //에러
DoubleStream randomStream = Stream.generate(Math::random);	//에러

굳이 필요하다면, 아래와 같이 mapToInt()와 같은 메서드로 변환을 해야한다.

IntStream evenStream = Stream.iterate(0, n->n+2).mapToInt(Integer::valueOf);
Stream<Integer> stream = evenStream.boxed(); //IntStream -> Stream<Integer>

반대로 IntStream타입의 스트림을 Stream<Integer>타입으로 변환하려면, boxed()를 사용하면 된다.

빈 스트림

요소가 하나도 없는 빈 스트림을 생성할 수도 있다. 스트림에 연산을 수행한 결과가 하나도 없을 때, null보다 빈 스트림을 반환하는 것이 낫다.

Stream emptyStream = Stream.empty(); //empty()는 빈 스트림을 생성해서 반환한다.
long count = emptyStream.count();

count()는 스트림 요소의 개수를 반환하며, 위의 문장에서 변수 count 값은 0이 된다.

두 스트림의 연결

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); //두 스트림을 하나로 연결

스트림의 중간 연산

스트림 자르기 - skip(), limit()

skip()limit()은 스트림의 일부를 잘라낼 때 사용하며, 사용법은 아주 간단하다. skip(3)은 처음 3개의요소를 건너뛰고, limit(5)는 스트림의 요소를 5개로 제한한다.

예를 들어 10개의 요소를 가진 스트림에 skip(3)과 limit(5)을 순서대로 적용하면 4번쨰 요소부터 5개의 요소를 가진 스트림이 반환된다.

IntStream intStream = IntStream.rangeClosed(1, 10); //1~10의 요소를 가진 스트림
intStream.skip(3).limit(5).forEach(System.out::print); //45678

스트림의 요소 걸러내기 - filter(), distinct()

distinct()스트림에서 중복된 요소들을 제거하고, filter()주어진 조건(Predicate)에 맞지 않는 요소를 걸러낸다.

Stream<T> filter(Predicate<? super T> predicate)
Stream<T> distinct()

distinct()의 사용 방법은 간단하다.

IntStream intStream = IntStream.of(1,2,2,3,3,3,4,5,5,6);
itnStream.distinct().forEach(System.out::print); //123456

filter()는 매개변수로 Predicate를 필요로 하는데, 아래와 같이 연산결과가 boolean인 람다식을 사용해도 된다.

IntStream intStream = IntStream.rangeClosed(1, 10); //1~10
intStream.filter(i -> i%2 == 0).forEach(System.out::print); //246810

필요하다면 filter()를 다른 조건으로 어려 번 사용할 수도 있따.

//아래의 두 문장은 동일한 결과를 얻는다
intStream.filter(i -> i%2 != 0 && i % 3 != 0).forEach(System.out::print); // 157
intStream.filter(i -> i%2).filter(i->i%3 !=0).forEach(System.out::print); // 157

정렬

스트림을 정렬할 때는 sorted()를 사용하면 된다.

Stream<T> sorted()
Stream<T> sorted(Comparator<? super T> comparator)

sorted()지정된 Comparator로 스트림을 정렬하는데, Comparator대신 int값을 반환하는 람다식을 사용하는 것도 가능하다. Comparator를 지정하지 않으면 스트림 요소의 기본 정렬 기준(Comparable)으로 정렬한다. 단, 스트림의 요소가 Comparable을 구현한 클래스가 아니면 예외가 발생한다.

Stream<String> strStream = Stream.of("dd", "aaa", "CC", "cc", "b");
strStream.sorted().forEach(System.out::print); //CCaaabccdd

정렬 조건을 추가할 때는 thenComparing()을 사용한다. 예를 들어 학생 스트림(studentSteam)을 반별, 성적순, 그리고 이름순으로 정렬하여 출력하려면 다음과 같다.

studentStream.sorted(Comparator.comparing(Student::getBan)
			.thenComparing(Student::getTotalScore)
            .thenComparing(Student::getName)
            .forEach(System.out.println);

다음의 예제는 학생의 성적을 반별 오름차순, 총점별 내림차순으로 정렬하여 출력한다.

import java.util.*;
import java.util.stream.*;

class StreamEx1 {
    public static void main(String[] args) {
        Stream<Student> studentStream = Stream.of(
                       new Student("이자바", 3, 100),
                       new Student("김자바", 1, 200),
                       new Student("안자바", 2, 100),
                       new Student("박자바", 2, 150),
                       new Student("소자바", 1, 200),
                       new Student("나자바", 3, 290),
                       new Student("감자바", 3, 180)
                    );
        studentStream.sorted(Comparator.comparing(Student::getBan)
              .thenComparing(Comparator.naturalOrder()))
              .forEach(System.out::println);
    }
}
class Student implements Comparable<Student> {
    String name;
    int ban;
    int totalScore;
    
    Student(String name, int ban, int totalScore) {
        this.name = name;
        this.ban = ban;
        this.totalScore = totalScore;
    }
    
    public String toString() {
        return String.format("[%s, %d, %d]", name, ban, totalScore);
    }
    
    String getName() {
        return name;
    }
    
    int getBan() {
        return ban;
    }
    
    int getTotalScore() {
        return totalScore;
    }
    
    //총점 내림차순을 기본정렬로 한다.
    public int CompareTo(Student s) {
        return s.totalScore - this.totalScore;
    }
}

Comparable을 구현해서 총점별 내림차순 정렬이 STudent 클래스의 기본 정렬이 되도록 했다.

변환 - map()

스트림의 요소에 저장된 값 중에서 원하는 필드만 뽑아내거나 특정 형태로 변환해야 할 때가 있다. 이 때 사용하는 것이 바로 map()이다.

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"));

//map()으로 Stream<File>을 Stream<String>으로 변환
Stream<String> filenameStream = fileStream.map(File::getName);
filenameStream.forEach(System.out::println); //스트림의 모든 파일이름을 출력

map()역시 중간 연산이므로 연산결과는 String을 요소로하는 스트림이다. map()으로 Stream<File>을 Stream<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(System.out::print);

조회 - peek()

연산과 연산 사이에 올바르게 처리되었는지 확인하고 싶다면, peek()를 사용하자. forEach()와 달리 스트림의 요소를 소모하지 않으므로 연산 사이에 여러 번 끼워 넣어도 문제가 되지 않는다.

fileStream.map(File::getName)
    filter(s -> s.indexOf('.') != -1) //확장자가 없는 것은 제외
    .peek(s->System.out.printf("filename = %s%n", s)) //파일명을 출력한다
    .map(s -> s.substring(s.indexOf('.')+1) //확장자만 추출
    .peek(s -> System.out.printf("extension = %s%n", s)) //확장자만 출력
    .forEach(System.out::println);

mapToInt(), mapToLong(), mapToDouble()

map()은 연산의 결과로 Stream<T\>타입의 스트림을 반환하는데, 스트림의 요소를 숫자로 반환하는 경우 IntStream과 같은 기본형 스트림으로 변환하는 것이 더 유용할 수 있다. 앞서 사용했던 studentStream에서, 스트림에 포함된 모든 학생의 성적을 합산해야 한다면, map()으로 학생의 총점을 뽑아서 새로운 스트림을 만들어 낼 수 있다.

Stream<Integer> studentScoreStream = studentStream.map(Student::getTotalScore);

그러나 이럴 때는 애초부터 mapToInt()를 사용해서 Stream<Intger\>가 아닌 IntStream 타입의 스트림을 생성해서 사용하는 것이 더 효율적이다. 성적을 더할 때, Integer를 int로 변환할 필요가 없기 때문이다.

count()만 지원하는 Stream<T\>와 달리 IntStream과 같은 기본형 스트림은 아래와 같이 숫자를 다루는데 편리한 메서드들을 제공한다.

int sum()
OptionalDouble average()
OptionalInt max()
OptionalInt min()

스트림의 요소가 하나도 없을 때, sum()은 0을 반환하면 그만이지만 다른 메서드들은 단순히 0을 반환할 수 없다. 여러 요소들을 합한 평균이 0일 수도 있기 때문이다. 이를 구분하기 위해 단순히 double 값을 반환하는 대신, double 타입의 값을 내부적으로 가지고 있는 OptionalDouble을 반환하는 것이다.

그리고 이 메서드들은 최종연산이기 때문에 호출 후에 스트림이 닫힌다.

InstStreamStream<T>로 변환할 때는 mapToObj()Stream<Integer>로 변환할 때는 boxed()를 사용한다.

아래는 로또번호를 생성해서 출력하는 코드인데, mapToObj()를 이용해서 IntStreamStream<String\>으로 변환하였다.

IntStream intStream = new Random.ints(1, 46);
Strea<String> lottoStream = intStream.distinct().limit(6).sorted().mapToObj(i -> i + ",");
lottoSTream.forEach(System.out::print);
import java.util.*;
import java.util.stream.*;

class StreamEx3 {
    public static void main(String[] args) {
        Student[] stuArr = {
            new Student("이자바", 3, 300),
            new Student("김자바", 1, 100),
            new Student("안자바", 2, 200),
            new Student("박자바", 3, 300),
            new Student("나자바", 1, 300),
            new Student("소자바", 3, 100)
        };
        
        Stream<Student> stuStream = Stream.of(stuArr);
        
        stuStream.sorted(Comparator.comparing(Student::getBan)
                    .thenComparing(Comparator.naturalOrder()))
                    .forEach(System.out::println);
        
        stuStream = Stream.of(stuArr); //스트림 다시 생성
        IntStream = stuScoreStream = stuStream.mapToInt(Student::getTotalScore);
        
        IntSummaryStatistics stat = stuScoreStream.summaryStatistics();
        System.out.println("count=" + stat.getCount());
        System.out.println("sum=" + stat.getSum());
        System.out.println("average=%.2f%n", stat.getAverage());
        System.out.println("min="+stat.getMin());
        System.out.println("max="+stat.getMax());
    }
}

class Student implements Comparable<Stundent> {
    String name;
    int ban;
    int totalScore;
    Student (String name, int ban, int totalScore) {
        this.name = name;
        this.ban = ban;
        this.totalScore = totalScore;
    }
    
    public String toSTring() {
        return String.format("[%s, %d, %d]", name, ban, totalScore).toString();
    }
    
    String getName() {
        return name; 
    }
    int getBan() {
        return ban;
    }
    int getTotalScore() {
        return totalScore;
    }
    
    public int compareTo(Student s) {
        return s.totalScore - this.totalScore;
    }
}

flatMap() - Stream<T[]>를 Stream<T>로 변환

스트림의 요소가 배열이거나 map()의 연산결과가 배열인 경우, 즉 스트림의 타입이 Stream<T[]>인 경우, Stream<T>로 다루는 것이 더 편리할 때가 있다. 그럴 때는 map()대신 flatMap()을 사용하면 된다.

예를 들어 아래와 같이 요소가 문자열 배열(String[])인 스트림이 있을 때,

Stream<String[]> strArrStrm = Stream.of(
    new String[]{"abc", "def", "ghi"},
    new String[]{"ABC", "GHI", "JKLMN"}
);

각 요소의 문자열들을 합쳐서 문자열이 요소인 스트림, 즉 Stream<String>으로 만들려면 어떻게 해야 할까?

먼저 스트림의 요소를 변환해야하니까 일단 map()을 써야할 것이고 여기에 배열을 스트림으로 만들어주는 Arrays.stream(T[])를 함께 사용해보자.

Stream<Stream<String>> strStrStream = strArrStrm.map(Arrays:stream);

예상한 것과 달리 Stream<String[]>을 map(Arrays::stream>으로 변환한 결과는 Stream<String>이 아닌 Stream<Stream<String>>이다. 즉, 스트림의 스트림인 것이다. 이때 간단히 map()을 아래와 같이 flatMap()으로 바꾸기만하면 우리가 원하는 결과를 얻을 수 있다.

Stream<String> strStrStream = strArrStrm.flatMap(Arrays:stream);

스트림의 최종 연산

최종 연산은 스트림의 요소를 소모해서 결과를 만들어낸다. 그래서 최종 연산 후에는 스트림이 닫히게 되고 더 이상 사용할 수 없다. 최종 연산의 결과는 스트림 요소의 합과 같은 단일 값이거나, 스트림의 요소가 담긴 배열 또는 컬렉션일 수 있다.

forEach()

forEach()peek()와 달리 스트림의 요소를 소모하는 최종연산이다. 반환 타입이 void 이므로 스트림의 요소를 출력하는 용도로 많이 사용된다.

조건 검사 - allMatch(), anyMatch(), noneMatch(), findFirst(), findAny()

이 메서드들은 매개변수로 predicate를 요구하며, 연산결과로 boolean을 반환한다.

예를들어 총점이 낙제점인 학생이 있는지 확인하는 방법은 다음과 같다.

boolean noFailed = stuStream.anyMatch(s->s.getTotalScore <= 100)

이오ㅔ에도 스틀미의 요소 중에서 조건에 일치하는 첫 번째 것을 반환하는 findFirst()가 있는데, 주로 filter()와 함께 사용되어 조건에 맞는 스트림의 요소가 있는지 확인하는데 사용된다. 병렬 스트림인 경우에는 findFirst() 대신 findAny()를 사용해야한다.

Optional<Student> stu = stuStream.filter(s -> s.getTotalScore() <= 100).findFirst();
Optional<Student> stu = parallelStream.filter(s->s.getTotalScore() <= 100).findAny();

findAny()findFirst()의 반환 타입은 Optional<T>이며, 스트림의 요소가 없을 때는 비어있는 Optional 객체를 반환한다.

통계 - count(), sum(), average(), max(), min()

기본형 스트림이 아닌 경우에는 통계와 관련된 메서드는 3개 뿐이다.

  • count()
  • max
  • min

대부분의 경우에는 위의 메서드를 사용하기보다 기본형 스트림으로 변환하거나, 아니면 reduce()collect()를 통해 정보를 얻는다.

reduce()

스트림의 요소를 줄여나가며 연산을 수행하고 결과를 반환한다. 그래서 매개변수의 타입이 BinaryOperator<T>인 것이다. 처음 두 요소를 가지고 연산한 결과를 가지고 그 다음 요소와 연산한다.

이 과정에서 스트림의 요소를 하나씩 소모하며, 스트림의 모든 요소를 소모하게되면 그 결과를 반환한다.

이외에도 연산결과의 초기값을 갖는 reduce()도 있는데, 이 경우 스트림의 요소가 하나도 없는 경우, 초기값이 반환되므로, 반환 타입이 Optional<T>가 아니라 T다.

앞서 소개한 최종 연산 count()와 sum() 등은 내부적으로 모두 reduce()를 사용해서 작성한 것이다.

int count = intstream.reduce(0, (a,b) -> a + 1);
int sum = intStream.reduce(0, (a, b) -> a+b);
int max = intStream.reduce(Integer.MIN_VALUE, (a,b) -> a>b ? a:b);
int min = intStream.reduce(Integer.MAX_VALUE, (a,b) -> a<b ? a:b);

collect()

collect()는 스틀미의 요소를 수집하는 최종 연산으로 앞서 배운 리듀싱(reducing)과 유사하다. collect()가 스트림의 요소를 수집하려면, 어떻게 수집할 것인가에 대한 방법이 정의되어 있어야 하는데, 이 방법을 정의한 것이 바로 컬렉터(collector)다.

컬렉터는 Collector 인터페이스를 구현한 것으로, 직접 구현할 수도 있고 미리 작성된 것을 사용할 수도 있다. Collectors 클래스는 미리 작성된 다양한 조율의 컬렉터를 반환하는 static 메서드를 가지고 있으며, 이 클래스를 통해 제공되는 컬레터만으로도 많은 일을 할 수 있다.

collect()는 매개변수의 타입이 Collector인데, 매개변수가 Collector를 구현한 클래스의 객체이어야 한다는 뜻이다. 그리고 collect()는 이 객체에 구현된 방법대로 스트림의 요소를 수집한다.

스트림을 컬렉션과 배열로 변환 - 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.getId(), p ->p));

스트렘이 저장된 요소들을 T[] 타입의 배여롤 변환하려면, toArray()를 사용하면 된다. 단, 해당 타입의 생성자 참조를 매개면수로 지정해줘야 한다. 만일 매개변수를 지정하지 않으면 반환되는 배열의 타입은 Object[]이다.

Student[] stuNames = studentStream.toArray(Student[]::new); // OK
Student[] stuNames = studentStream.toArray(); // 에러.
Object[] stuNames = studentStream.toArray(); // OK

통계 - counting(), summingInt(), averagingInt(), maxBy(), minBy()

앞서 살펴보았단 최종 연산들이 제공하는 통계 정보를 collect()로 똑같이 얻을 수 있다.

리듀싱

리듀싱 역시 collect로 가능하다.

int grandTotal = stuStream.map(Student::getTotalScore).reduce(0, Integer::sum);
int grandTotal = stuStream.collect(reducing(0, Student::getTotalScore, Integer::sum));

문자열 결합 - joining()

문자열 스트림의 모든 요소를 하나의 문자열로 연결해서 반환한다. 스트림의 요소가 String이나 StringBuffer처럼 CharSequence의 자손인 경우에만 결합이 가능하므로 스트림의 요소가 문자열이 아닌 경우에는 먼저 map()을 이용해서 스트림의 요소를 문자열로 변환해야 한다.

String studentNames = stuStream.map(Student::getName).collect(joining());

그룹화와 분할 - groupingBy(), partitioningBy()

지금까지는 기존의 다른 연산으로도 대체가능한 경우에 대해서 설명했기 때문에 collect()가 왜 필요한지 잘 느끼지 못했을 것이다. 그러나 이제부터 본격적으로 collect의 유용함을 알게될 것이다.

그룹화스트림의 요소를 특정 기준으로 그룹화하는 것을 의미하고, 분할은 스트림의 요소를 두 가지, 지정된 조건에 일치하는 그룹과 일치하지 않는 그룹으로의 분할을 의미한다. groupingBy는 스트림의 요소를 Function으로, partitioningBy는 Predicate로 분류한다.

partitioningBy()에 의한 분류

Map<Boolean, List<Student>> stuBySex = stuStream.collect(partitioningBy(Student::isMale)); // 설별로 분할
List<Student> maleStudent = stuBySex.get(true);
List<Student> femaleStudent = stuBySex.get(false);

이번에 counting()을 추가해서 남학생의 수와 여학생의 수를 구해보자.

Map<Boolean, Long> stuNumBySex = stuStream.collect(partitioningBy(Student::isMale, counting());

그러면 남학생 1등과 여학생 1등은 어떻게 구할 수 있을까?

Map<Boolean, Optional<Student>> topScoreBySex = stuStream
.collect(partitioningBy(Student::isMale, maxBy(comparingInt(Student::getScore))));
System.out.println("남학생 1등 : " + topScoreBySex.get(true));

maxBy()는 반환타입이 Optional<Student>라서 위와 같은 결과가 나왔다.

groupingBy에 의한 분류

가장 간단한 그룹화를 해보자. stuStream을 반 별로 그룹지어 Map에 저장하는 방법

Map<Integer, List<Student>> stuByBan = stuStream.collect(groupingBy(Student::getBan)); //toList()가 생략됨

groupingBy()로 그룹화를 하면 기본적으로 List<T>에 담는다. 그래서 위의 문장은 아래 문장의 생력된 형태이다. 원한다면 toList() 대신 toSet()이나 toCollection(HashSet::new)을 사용할 수도 있다.

Map<Integer, List<Student>> stuByBan = stuStream.collect(groupingBy(Student::getBan, toList())); // toList() 생략가능

이번에 조금 복잡하게 stuStream을 성적의 등급(Student.Level)으로 그룹화해보자. 아래의 문장은 모든 학생을 세 등급으로 분류하여 집계한다.

Map<Student.Level, Long> stuByLevel = stuStream.collect(groupingBy(s -> {
										if (s.getScore() >= 200)
										else if (s.getScore() >= 100)
										else}, counting())
                                       );

groupingBy() 를 여러 번 사용하면, 다수준 그룹화가 가능하다. 만일 학년별로 그룹화가 한후에 다시 반별로 그룹화하고 싶으면 다음과 같이 한다.

Map<Integer, Map<Integer, List<Student>>> stuByHakAndBan = stuStream
									.collect(groupingBy(Student::getHak, groupingBy(Student::getBan)));

Collector 구현하기

0개의 댓글