본문 바로가기
Java

스트림 (Stream) API

by 밀러 (miller) 2022. 1. 13.

자바로 알고리즘 문제를 풀다가 특정 코드에 함수형 프로그래밍을 적용하고 싶어, 스트림을 공부하게 되었다. 자바스크립트의 기본 자료형인 배열에서 지원하는 map, filter, reduce 와 같이 사용이 쉬운 편은 아니지만, 막상 공부해보고 나니 파이썬의 max, min, sorted 와 같이 기본적으로 제공하는 연산함수에서 key 인자에 사용하는 람다식보다 훨씬 체계적이라고 느꼈다. 스트림을 공부하면서 정리한 내용을 공유해보고자 한다.

 

본 포스트는 자바의 정석 14장 - 스트림의 내용을 정리했습니다.

 

스트림 (Stream)

스트림 (Stream) 은 데이터 소스를 추상화하고, 데이터를 다루는데 자주 사용되는 메서드를 정의해 놓았다. 데이터 소스를 추상화했다는 것은, 데이터 소스가 무엇이든 간에 같은 방식으로 다룰 수 있게 되었다는 것과 코드의 재사용성이 높아진다는 것을 의미한다.

 

스트림은 다음과 같은 특징을 갖는다.

 

  • 스트림은 데이터 소스를 변경하지 않는다.
  • 스트림은 일회용이다.
  • 스트림은 작업을 내부 반복으로 처리한다.

중간 연산과 최종 연산

스트림의 연산은 중간 연산최종 연산으로 분류할 수 있다. 중간 연산은 연산 결과를 스트림으로 반환하므로 중간 연산을 메서드로 연결할 수 있지만, 최종 연산은 스트림의 요소를 소모하면서 연산을 수행하므로 1번만 연산이 가능하다.

 

stream.distinct().limit(5).sorted().forEach(System.out::println)

// 중간 연산 : distinct(), limit(), sorted()
// 최종 연산 : foreach()

 

중간 연산은 호출해도 즉각적인 연산이 수행되지 않고, 최종 연산이 수행되어야 중간 연산을 수행해 스트림의 요소들이 최종 연산에서 소모되는 지연된 연산이다.

기본형 스트림

기본적으로 요소의 타입이 TStream<T> 을 제공하지만, 오토박싱과 언방식으로 인한 비효율을 줄이기 위해 Stream<Integer>, Stream<Long>, Stream<Double> 대신 효율적인 IntStream, LongStream, DoubleStream 을 제공한다.

 

스트림 생성

스트림의 소스가 될 수 있는 대상은 배열, 컬렉션, 임의의 수 등 다양하다. 각 소스로부터 스트림을 생성하는 방법에 대해 알아보자.

 

컬렉션

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

 

// 메서드
Stream<T> Collection.stream()

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

 

배열

배열으로부터 스트림을 생성하는 메서드는 Stream 또는 Arraysstatic 메서드로 정의되어 있다.

 

// 메서드
Stream<T> Stream.of(T.. values)
Stream<T> Stream.of(T[])
Stream<T> Arrays.stream(T[])

// 사용
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"});

 

기본형 배열을 소스로하는 스트림도 IntStream, LongStream, DoubleStreamArraysstatic 메서드로 정의되어 있다.

 

// 메서드
IntStream IntStream.of(int... values)
IntStream IntStream.of(int[]);
Intstream Array.stream(int[]);

 

특정 범위의 정수

IntStreamLongStream 은 지정된 범위의 연속된 정수를 스트림으로 생성해서 반환하는 rangerangeClosed 를 제공한다.

 

IntStream.range(int begin, int end); // 경계 끝 미포함
IntStream.rangeClosed(int begin, int end); // 경계 끝 포함

 

빈 스트림

요소가 하나도 없는 비어있는 스트림을 생헝할 수 있다.

 

Stream emptyStream = Stream.empty();

 

스트림의 중간 연산

함수형 프로그래밍에서 사용하는 map, filter 와 함께 distinct, sorted, peek, skip, limit 등이 등을 사용할 수 있다.

 

스트림 매핑하기 (map)

스트림의 요소 중 원하는 필터만 뽑아내거나 요소를 특정 형태로 변환해야할 때 map() 을 사용한다. 메서드의 인자에 매개변수 T 타입을 R 타입으로 변환해서 반환하는 함수를 지정해야한다.

 

// 메서드
Stream<R> map(Function<? super T, ? extends R> mapper);

// 사용
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);
filenameStream.forEach(System.out::println);

 

** 위에서 mapper 와 같이 함수를 인자로 받는 매개변수의 경우, 람다식 혹은 메서드 참조 입력 가능

 

스트림 필터링하기 (filter)

filter 는 매개변수로 Predicate 를 필요로 하는데, 연산결과가 boolean 인 람다식을 사용할 수도 있다. 그리고 distinct 로는 스트림에서 중복된 요소들을 제거할 수 있다.

 

// 메서드
Stream<T> filter(Predicate<? super T> predicate);
Stream<T> distinct();

// 사용
IntStream intStream = InStream.rangeClosed(1, 10);
intStream.filter(i -> i % 2 == 0).forEach(System.out::println);

 

스트림 정렬하기 (sorted)

스트림을 정렬할 때는 sorted() 를 사용하고, sorted 는 타입 변수 T 에 지정된 Comparator 로 스트림을 정렬하며, Comparator 를 지정하지 않으면 T 의 기본 정렬 기준 (Comparable) 로 정렬한다.

 

// 메서드
Stream<T> sorted();
Stream<T> sorted(Comparator<? super T> comparator);

// 사용
strStream.sorted(); // 기본 정렬
strStream.sorted(Comparator.naturalOrder()); // 기본 정렬
strStream.sorted(Comparator.reverseOrder()); // 역순 정렬
strStream.sorted(Comparator.comparing(String::length)); // 길이순 정렬

strStream.sorted((s1, s2) -> s1.compareTo(s2)); // 알파벳순 정렬
strStream.sorted(String::compareTo); // 위와 동일

 

Comparator 인터페이스에 static 메서드와 디폴트 메서드를 사용할 경우 정렬이 쉬워진다. 해당 메서드들은 모두 Comparator<T> 를 반환하며, 가장 기본적인 메서드는 comparing() 이다. 만약 스트림의 요소가 Comparable 을 구현한 경우 매개변수 1개짜리가 사용되지만, 그렇지 않은 경우에는 추가적인 매개변수로 정렬기준 Comparator 를 따로 지정해줘야한다.

 

// 메서드
comparing(Function<T, U> keyExtractor)
comparing(Function<T, U> keyExtractor, Comparator<U> keyCompartor)

// 사용
strStream.sorted(Comparator.comparing(String::length));

 

스트림 조회하기 (peek)

연산과 연산 사이에 올바르게 처리되었는지 확인하은 경우에, 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);

 

스트림 자르기 (skip, limit)

skip(n) 은 처음 n 개의 요소를 스킵하고, limit(n) 은 스트림의 전체 요소를 n 개로 리밋한다.

 

Stream<T> skip(long n);
Stream<T> limit(long maxSize);

 

 

스트림의 최종 연산

forEach

반환 타입이 void 이므로 스트림의 요소를 출력하는 용도로 많이 사용한다.

 

void forEach(Consumer<? super T> action);

 

조검 검사

매개변수로 predicate 를 요구하며, 연산결과로 boolean 을 반환

 

  • allMatch : 스트림의 요소에 대한 지정된 조건에 모든 요소가 일치하는지 검사
  • anyMatch : 스트림의 요소에 대한 지정된 조건에 일부 요소가 일치하는지 검사
  • noneMatch : 스트림의 요소에 대한 지정된 조건에 어떤 요소도 일치하지 않는지 검사
// 메서드
boolean allMatch(Predicate<? super T> predicate)
boolean anyMatch(Predicate<? super T> predicate)
boolean noneMatch(Predicate<? super T> predicate)

// 사용
boolean anyFailed = studentStream.anyMatch(s -> s.getTotalScore() <= 100);

 

  • findFirst : 조건이 일치하는 첫번째 스트림의 요소를 반환 (주로 filter 와 함께 사용)
// 메서드
Optional<Student> student = studentStream.filter(s -> s.getTotalScore <= 100).findFirst();

 

통계 계산

기본형 스트림이 아닌 경우, 통계와 관련된 메서드로 count, min, max 가 사용가능하다. 이외의 경우, reducecollect 를 이용해서 통계 정보를 얻을 수 있다.

 

// 메서드
long count();
Optional<T> max(Comparator<? super T> comparator);
Optional<T> min(COmparator<? super T> comparator);

 

reduce()

reduce() 는 스트림의 요소를 줄여나가면서 연산을 수행하고 최종결과를 반환한다. 따라서 매개변수의 타입이 BinaryOperator<T> 이고, 처음 두 요소를 가지고 연산한 결과를 가지고 그 다음 요소와 연산한다. 이 과정에서 스트림의 요소를 하나씩 소모하게 되며, 스트림의 모든 요소를 소모하게 되면 그 결과를 반환한다.

 

매개변수로 BinaryOperator<T> 만을 갖는 reduce() 와, 초기값 (identity) 을 함께 갖는 reduce() 가 존재한다. 이 메서드는 초기값과 스트림의 첫번째 요소롤 연산을 시작한다. 단, 스트림의 요소가 하나도 없는 경우, 초기값이 반환되므로 반환 타입이 Optional<T> 가 아닌 T 이다.

 

// 메서드
Optional<T> reduce(BinaryOperator<T> accumulator); // 초기값이 주어지지 않는 경우
T reduce(T identity, BinaryOperator<T> accumulator); // 초기값이 주어지는 경우

// 사용
// 인자 1개만 (Binary Operator 만) 지정하는 경우
OptionalInt max = intStream.reduce((a, b) -> a > b ? a : b);
OptionalInt min = intStream.reduce((a, b) -> a < b ? a : b);

OptionalInt max = intStream.reduce(Integer::max);
OptionalInt min = intStream.reduce(Integer::min);

// 인자 2개를 (identity 와 함께) 지정하는 경우
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);

 

reduce 의 사용 방법은 간단하다. 초기값 (identity) 와 어떤 연산 (Binary Operator) 으로 스트림의 요소를 줄여나갈지만 결정하면 된다!

 

collect()

collect() 는 스트림의 요소를 수집하는데, 컬렉터 (collector) 가 어떻게 수집할 것인가에 대한 방법을 정의해 두었다. 컬렉터는 Collector 인터페이스를 구현한 것으로, 직접 구현할 수도 있고 미리 작성된 것을 사용할 수도 있는데, Collectors 클래스는 미리 작성된 다양한 종류의 컬렉터를 반환하는 static 메서드를 가지고 있다.

 

용어를 정리하면 다음과 같다.

 

  • collect() : 스트림의 최종 연산. 매개변수로 컬렉터를 필요로 한다.
  • Collector : 인터페이스. 컬렉터는 이 인터페이스를 구현해야한다.
  • Collectors : 클래스. static 메서드로 미리 작성된 컬렉터를 제공한다.

collect() 는 매개변수의 타입이 Collector 인데, 매개변수가 Collector 를 구현한 클래스의 객체이어야 한다는 뜻이다. 그리고 collect() 는 이 객체에 구현된 방법으로 스트림의 요소를 수집한다. 마치 sort() 할 때 Comparator 가 필요한 것 처럼, collect()Collector 가 필요하다.

 

Object collect(Collector collector);

 

스트림을 컬렉션과 배열로 변환

스트림은 컬렉션과 배열로 변환하기 위해 다음과 같이 Collectors 클래스의 static 메서드를 활용할 수 있다.

 

  • Collectors.toList()
  • Collectors.toSet()
  • Collectors.toMap()
  • Collectors.toArray()

스트림의 모든 요소를 컬렉션에 수집하려면, Collectors.toList() 와 같은 메서드를 사용하면 된다. toMap 은 키와 값의 쌍으로 저장해야하므로 객체의 어떤 필드를 키로 사용할지와 값으로 사용할지를 지정해줘야 한다. toArray 로 스트림에 저장된 요소들을 T[] 타입의 배열로 변환하려면, 해당 타입의 생성자 참조 (T::new)를 매개변수로 지정해줘야 한다. (매개변수를 지정하지 않으면 반환되는 배열의 타입은 Object[] 이다.)

 

List<String> names = studentStream.map(Student::getName)
    .collect(Collectors.toList());

Map<String, Person> list = names.stream()
    .collect(Collectors.toMap(p->p.getRegId(), p->p));

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

 

기본형 스트림

스트림의 요소를 숫자로 변환하는 경우 IntStream 과 같은 기본형 스트림으로 변환하는 것이 더 유용할 수 있다. 예를 들어, 스트림에 포함된 모든 학생의 성적을 합산하는 경우 Stream<Integer> 가 아닌 IntStream 타입으로 스트림을 생성하면 Integerint 로 변환할 필요가 없으모로 효율적이다.

 

중간연산

Stream<T> 에서 IntStream 과 같이 기본형 스트림으로 변환할 때 mapToInt, mapToLong, mapToDouble 을 사용한다.

Stream<T>IntStream

// 메서드
IntStream mapToDouble(ToIntFunction<? super T> mapper);
LongStream mapToDouble(ToLongFunction<? super T> mapper);
DoubleStream mapToDouble(ToDoubleFunction<? super T> mapper);

// 사용
IntStream studentScoreStream = studentStream.mapToInt(Student::getTotalScore);

 

반대로 IntStream 과 같이 기본형 스트림에서 Stream<T> 로 변환할 때 mapToObj 를, Stream<Integer> 로 변환할 때 boxed() 를 사용한다.

Stream<T>IntStream

// 메서드
Stream<U> mapToObj(IntFunction<? extends U> mapper);
Stream<Integer> boxed();

// 사용
IntStream intStream = new Random().ints(1, 46);
Stream<String> lottoStream = intStream.distinct().limit(6).sorted().mapToObj(i -> i + ",");
lottoStream.forEach(System.out::println);

 

특히 mapToInt 에서 자주 사용되는 메서드는 다음과 같다.

  • Stream<String>IntStream 변환할 때, mapToInt(Integer::parseInt)
  • Stream<Integer>IntStream 변환할 때, mapToInt(Integer::intValue)

최종연산

그리고 최종연산으로count(), max(), min() 만 지원하는 Stream<T> 와 달리, IntStream 과 같은 기본형 스트림은 숫자를 다루는데 더 편리한 메서드들을 제공한다.

 

int sum = intStream.sum();
OptionalDouble average = intStream.average();
OptionalInt max = intStream.max();
OptionalInt min = intStream.min();

// 통계 산출량 제공하는 객체 생성
IntSumaryStatistics stat = intStream.summaryStatistics();

 

 

Optional

스트림 클래스에서 정의된 메서드 중에서 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);

 

여기서 minmaxreduce 로 작성된 메서드이므로, 스트림 클래스에서 정의된 메서드 전체에서 Optional<T> 를 반환하는 최종 연산 메서드는 적다.

 

Optional<T> 를 반환받고 나서 처리할 수 있는 방법은 크게 3가지로 나눌 수 있다.

 

  • 값 반환 → get(), orElse()
  • 확인 → isPresent()
  • 확인 + 실행 → ifPresent()

댓글