Java 8에 도입된 강력한 기능으로, 람다를 활용할 수 있는 기술 중 하나입니다. Java 8 이전에는 배열 또는 컬렉션 인스턴스를 다루는 방법은 for 또는 forEach 문을 돌면서 요소 하나씩을 꺼내서 다루는 방법이이었습니다. 간단한 경우라면 상관없지만 로직이 복잡해 질수록 코드의 양이 많아져 여러 로직이 섞이게 되고, 메소드를 나눌 경우 루프를 여러 번 도는 경우가 발생합니다.
Stream은 '데이터의 흐름' 입니다. 배열 또는 컬렉션 인스턴스에 함수 여러 개를 조합해서 원하는 결과를 필터링하고 가공된 결과를 얻을 수 있습니다. 또한 람다를 이용해서 코드의 양을 줄이고 간결하게 표현할 수 있습니다. 즉, 배열과 컬렉션을 함수형으로 처리할 수 있습니다.
Stream<Interger> 대신 IntStream이 제공되어서, 오토박싱과 언박싱 등의 불필요한 과정이 생략되고 숫자의 경우 유용한 메소드를 추가로 제공한다. (.sum()``.average() 등)스트림에 대한 내용은 크게 세 가지로 나눌 수 있습니다.
1. 생성하기 : 스트림 인스턴스 생성.
2. 가공하기 : 필터링(filtering) 및 맵핑(mapping) 등 우너하는 결과를 만들어가는 중간 작업(intermediate operations).
3. 결과 만들기 : 최종적으로 결과를 만들어내는 작업(terminal operations).
전체 → 맵핑 → 필터링1 → 필터링2 → 결과 만들기 → 결과물
보통 배열과 컬렉션을 이용해서 스트림을 만들지만 이 외에도 다양한 방법으로 스트림을 만들 수 있습니다.
스트림을 이용하기 위해서는 먼저 생성을 해야합니다. 스트림은 배열 또는 컬렉션 인스턴스를 이용해서 생성할 수 있습니다. 배열은 다음과 같이 Arrays.stream 메소드를 사용합니다.
String[] arr = new String[]{"a", "b", "c"};
Stream<String> stream = Arrays.stream(arr);
Stream<String> streamOfArrayPart = Arrays.stream(arr, 1, 3); // 1~2 요소 [b,c]
컬렉션 타입(Collection, List, Set)의 경우 인터페이스에 추가된 디폴트 메소드 stream을 이용해서 스트림을 만들 수 있습니다.
public interface Collection<E> extends Iterable<E> {
default Stream<E> stream() {
return StreamSupport.stream(spliterator(), false);
}
// ...
}
그러면 다음과 같이 생성할 수 있습니다.
List<String> list = Arrays.asList("a", "b", "c");
Stream<String> stream = list.stream();
Stream<String> parallelStream = list.parallelStream(); // 병렬 처리 스트림
비어 있는 스트림(empty streams)도 생성할 수 있습니다. 언제 빈 스트림이 필요할까요? 빈 스트림은 요소가 없을 때 null 대신 사용할 수 있습니다.
public Stream<String> streamOf(List<String> list) {
return list == null || list.isEmpty()
? Stream.empty()
: list.stream();
}
전체 요소 중에서 다음과 같은 API를 이용해서 내가 워하는 것만 뽑아낼 수 있습니다. 이러한 가공 단계를 중간 작업(intermediate operations)이라고 하는데, 이러한 작업은 스트림을 리턴하기 때문에 여러 작업을 이어 붙여서(chaining) 작성할 수 있습니다.
List<String> names = Arrays.asList("Eric", "Elena", "Java");
아래 나오는 예제 코드는 위와 같은 리스트를 대상으로 합니다.
필터(filter)는 스트림 내 요소들을 하나씩 평가해서 걸러내는 작업입니다. 인자로 받는 Predicate는 boolean을 리턴하는 함수형 인터페이스로 평가식이 들어가게 됩니다.
Stream<T> filter(Predicate<? super T> predicate);
간단한 예제입니다.
Stream<String> stream = names.stream()
.filter(name -> name.contains("a"));
// [Elena, Java]
스트림의 각 요소에 대해서 평가식을 실행하게 되고 'a'가 들어간 이름만 들어간 스트림이 리턴됩니다.
맵(map)은 스트림 내 요소들을 하나씩 특정 값으로 변환해줍니다. 이 때 값을 변환하기 위한 람다를 인자로 받습니다.
<R> Stream<R> map(Function<? super T, ? extends R> mapper);
스트림에 들어가 있는 값이 input이 되어서 특정 로직을 거친 후 output이 되어 (리턴되는) 새로운 스트림에 담기게 됩니다. 이러한 작업을 맵핑(mapping)이라고 합니다.
간단한 예제입니다. 스트림 내 String의 toUpperCase 메소드를 실행해서 대문자로 변환한 값들이 담긴 스트림을 리턴합니다.
Stream<String> stream = names.stream()
.map(String::toUpperCase);
// [ERIC, ELENA, JAVA]
다음처럼 요소 내 들어있는 Product 객체의 수량을 꺼내올 수도 있습니다. 각 '상품'을 '상품의 수량'으로 맵핑하는 거죠.
Stream<Interger> stream = productList.stream()
.map(Product::getAmount);
// [23, 14, 13, 23, 13]
map 이외에도 조금 더 복잡한 flatMap 메소드도 있습니다.
<R> Stream<R> flatMap(Function<? super T, ? extends Stream<? extends R>> mapper);
인자로 mapper를 받고 있는데, 리턴 타입이 Stream 입니다. 즉, 새로운 스트림을 생성해서 리턴하는 람다를 넘겨야합니다. flatMap은 중첩 구조를 한 단계 제거하고 단일 컬렉션으로 만들어주는 역할을 합니다. 이러한 작업을 플래트닝(flattening)이라고 합니다.
다음과 같은 중첩된 리스트가 있습니다.
List<List<String>> list = Arrays.asList(Arrays.asList("a"),
Arrays.asList("b"));
// [[a], [b]]
이를 flatMap을 사용해서 중첩 구조를 제거한 후 작업할 수 있습니다.
List<String flatList = list.stream()
.flatMap(Collection::stream)
.collect(Collectors.toList());
// [a, b]
이번엔 객체에 적용해보겠습니다.
students.stream()
.flatMapToInt(student -> IntStream.of(student.getKor(),
student.getEng(),
student.getMath()))
.average().ifPresent(avg -> System.out.println(Math.round(avg * 10)/10.0));
위 예제에서는 학생 객체를 가진 스트림에서 학생의 국영수 점수를 뽑아 새로운 스트림을 만들어 평균을 구하는 코드입니다. 이는 map 메소드 자체만으로는 한번에 할 수 없는 기능입니다.
참고 사이트:
https://futurecreator.github.io/2018/08/26/java-8-streams