본문 바로가기

연재작/프로그래밍 언어

Java - Stream 뽀개기 (1) (feat. 함수형 인터페이스)

 

Stream이란?

자바에서 대량의 데이터를 가지고 있는 Collection 자료들의 효율적인 병렬처리를 위해서 주어지는 API이다.

 

Stream은 크게 3가지 구조로 나눌 수 있다.

  1. Stream의 생성 (Collection ➡️ Stream)
  2. 중간 연산 (Stream ➡️ Stream)
  3. 최종 소비 연산 (Stream ➡️ Collection)

 

1. Stream 생성

Stream은 무조건 Collection의 구현체들로부터 생성된다.

그래서 아래와 같은 생성의 모습을 보인다.

List<String> list = Arrays.asList("a", "b", "c"); // ArrayList로 구현
Stream<String> stream = list.stream();

 

2. 중간 연산

  • 스트림을 변환하거나 필터링 하는 작업을 수행한다.
  • 항상 Stream을 반환한다.

아래는 중간 연산에 사용되는 메서드들의 종류이다.

 

filter : 각 element에 대해 boolean값이 참인 것만을 사용.

따라서 boolean을 반환하는 predicate의 함수형 인터페이스를 인자로 받는다.

Stream<T> filter(Predicate<? super T> predicate)
	// predicate는 T(타입) -> Boolean 을 반환하는 functional interface
Stream<String> stream = Stream.of("apple", "banana", "cherry");
Stream<String> filteredStream = stream.filter(s -> s.startsWith("a"))

 

 

map : 각 element를 다른 타입으로 바꾼다.

<R> Stream<R> map(Function<? super T, ? extends R> mapper);
// map의 문서를 살펴보면 위와 같이 선언되어 있고,
// <R>라는 타입을 사용하겠다는 선언과 함께
// Function이라는 함수형 인터페이스를 인자로 받고 있다.
// T또는 T상위를 받아 R또는 R의 하위를 리턴하는 mapper이다.
Stream<String> stream = Stream.of("apple", "banana", "cherry");
Stream<Integer> lengthStream = stream.map(String::length);

 

flatMap : 스트림의 각 요소를 Stream으로 변환하고, 중첩된 스트림을 하나의 스트림으로 통합.

<R> Stream<R> flatMap(Function<? super T, ? extends Stream<? extends R>> mapper)
// T를 받아 Stream<R>로 반환하는 Function 이라는 함수형 인터페이스를 인자로 받는다
Stream<List<String>> stream = Stream.of(Arrays.asList("a", "b"), Arrays.asList("c", "d"));
Stream<String> flatMappedStream = stream.flatMap(List::stream);  // "a", "b", "c", "d"로 병합
List::stream
(list -> list.stream()) //위와 아래는 같은 형태이다.

flat, 말그대로 평평하게 하는 느낌이라고 볼 수 있다.

Stream의 Stream으로 중첩된 상태를 만든 후, 하나의 단일 Stream으로 통합하는 느낌이다.

혹은 2레이어짜리 stream을 1레이어짜리 stream으로 재구성하는 느낌이다.

 

distinct : 중복된 요소를 제거한 스트림을 반환한다.

Stream<T> distinct()
Stream<String> stream = Stream.of("apple", "banana", "apple");
Stream<String> distinctStream = stream.distinct();  // "apple", "banana"만 포함

 

sorted : 순서를 정렬한 스트림을 반환한다.

Stream<T> sorted()
Stream<String> stream = Stream.of("banana", "apple", "cherry");
Stream<String> sortedStream = stream.sorted();  // "apple", "banana", "cherry"로 정렬

Stream<T> sorted(Comparator<? super T> comparator)
Stream<String> stream = Stream.of("banana", "apple", "cherry");
Stream<String> sortedStream = stream.sorted(Comparator.reverseOrder());  // 역순 정렬

 

comparator라는 비교자를 통해서 sort를 진행한다.

 

Comparator?

이는 비교를 해주는 함수형 인터페이스로, 두 개의 값을 비교해서 숫자인 값을 반환한다.

첫 번째 비교자가 두 번째 비교자보다 작아서 음수를 반환하게 될 경우는 오름차순이 된다.

 

굳이 comparator를 사용하지 않아도, 직접 람다 함수를 통해서 구현 할 수도 있다.

가령, 위의 예시에서 객체의 길이의 역순으로(내림차순으로) 정렬하고 싶다면

Stream<String> stream = Stream.of("banana", "apple", "cherry");
Stream<String> sortedStream = stream.sorted((a, b) -> b.length() - a.length());  // 역순 정렬

 

와 같이 람다함수로 할 수도 있다.

다만 compartor가 표준화된 함수형 인터페이스이기 때문에, 굳이 람다함수로 대체하지 않아도 된다.

 

int maxLocation = cars.stream()
                .max(Comparator.comparingInt(Car::getCurrentLocation))
                .map(Car::getCurrentLocation)
                .orElseThrow(() -> new IllegalStateException("No cars available"));

// 위와 아래는 같은 연산을 하고, 같은 결과를 반환한다.

int maxLocation = cars.stream()
                .max((a, b) -> a.getCurrentLocation() - b.getCurrentLocation())
                .map(each -> each.getCurrentLocation())
                .orElseThrow(() -> new IllegalStateException("No cars available"));

// 이례적으로 아래처럼 순서를 바꿔도 똑같다는 것을 알 수 있다.

int maxLocation = cars.stream()
				.map(each -> each.getCurrentLocation())
                .max((a, b) -> a - b)
                .orElseThrow(() -> new IllegalStateException("No cars available"));