Stream이란 사전적 의미로 '물의 흐름'인데, 개발 용어로는 '연속적인 데이터의 흐름'을 의미한다.
JAVA 8 에서 함수형 프로그래밍의 기능을 지원하기 위해 도입되었다.
이름이 "A"로 시작되고 길이가 4 이상인 이름을 찾아 정렬하여 출력하는 로직으로 예를 들어본다.
[for-loop로 처리하는 방식]
List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "David");
List<String> namesStartedA = new ArrayList<>();
for (String name : names) {
if (name.startsWith("A") && name.length() >= 4) {
namesStartedA.add(name);
}
}
Collections.sort(namesStartedA);
for (String name : namesStartedA) {
System.out.println(name);
}
[stream으로 처리하는 방식]
names.stream()
.filter(name -> name.startsWith("A") && name.length() >= 4)
.sorted()
.forEach(System.out::println);
두 코드를 보면 알겠지만 stream으로 작성된 코드가 훨씬 간결하고 가독성이 높다.
공식문서에 작성된 특징을 알아보자
스트림 생성 - 중간 연산 - 최종 연산이라는 순서를 가짐유한한 크기를 가지는 Collection 과는 달리 Stream은 무한한 사이즈가 가능
Stream은 한 번 생성되고 최종 연산이 되면 닫힘
filter()나 map() 같은 연산은 이전 요소의 정보를 필요로 하지 않으므로 무상태 연산이라 함distinct()나 sorted()와 같은 연산은 이전 요소의 처리 결과 정보를 가지고 있어야하므로 상태 연산이라 함그렇다면 언제 for문을 사용하고 언제 stream을 사용할까?
가독성이 더 좋기 때문에 반복이 필요할 때 무조건 stream을 사용해야하나?
하지만 그것은 틀렸습니다.
요소들의 시퀀스에게 일관된 작업을 적용할 때 Stream을 사용하는 게 유리하다.
람다식 내에서는 final 변수(혹은 변경되지 않는 변수)만 읽을 수 있고,
return 이나 break 가 불가능하다.
또, stream의 목적은 데이터를 가공하기 위한 기능이어서 단순 반복 처리는 for문을 사용하는 게 낫다.
정답은 없으며 취향 차이이다!
팀원 간 혹은 개인 판단 하에 더 나은 것을 적용하자!!
Arrays.stream() 메서드를 이용해서 생성할 수 있다.
String[] arr = new String[]{"a", "b", "c"};
Stream<String> stream = Arrays.stream(arr);
Collection 인터페이스의 디폴트 메소드인 stream을 이용해서 생성할 수 있다.
List<String> list = Arrays.asList("a", "b", "c");
Stream<String> stream = list.stream();
Primitive Type의 스트림을 기본적으로 제공한다.
IntStream intStream = IntStream.range(0, 5); // [0, 1, 2, 3, 4]
DoubleStream doubleStream = DoubleStream.of(1.0, 2.5, 4.0);
static <T> Stream<T> generate(Supplier<T> s) { ... }
Supplier<T>로 스트림을 생성할 수 있다.
다만 주의해야할 점은 Infinite Stream이 생성되기 때문에 크기를 제한하는 limit() 같은 short-circuit의 기능이 있는 연산을 꼭 사용해야 한다.
static <T> Stream<T> iterate(T seed, UnaryOperator<T> f)
static <T> Stream<T> iterate(T seed, Predicate<? super T> hasNext, UnaryOperator<T> next)
초기값과 그 값을 인자로 하는 람다식을 이용해서 스트림을 만든다.
이 역시 Infinite Stream을 생성하기 때문에 사이즈 제한을 해야 한다.
중간 연산은 스트림을 리턴하기 때문에 메서드 체이닝으로 이어 작성할 수 있다.
<R> Stream<R> map(Function<? super T, ? extends R> mapper)
스트림 내 요소들을 mapper 인자로 받은 연산을 통해 특정 값들로 변환하여 새로운 스트림을 만든다.
int[] arr = new int[]{1, 2, 3, 4};
Arrays.stream(arr)
.map(it -> it * 10); // [10, 20, 30, 40]
<R> Stream<R> flatMap(Function<? super T,? extends Stream<? extends R>> mapper)
map() 메서드와 비슷하지만 다른 점은 리턴 값이 Stream인 Fuction 함수를 인자로 넘겨야 합니다.
이 메서드는 중첩 구조를 한 단계 벗겨내서 flat하게 만들어 반환해줍니다.
String[] str = {"Hello", "World"};
Arrays.stream(str)
.map(it -> it.split("")
.flatMap(Arrays::stream); // [H, e, l, l, o, W, o, r, l, d]
Stream<T> filter(Predicate<? super T> predicate)
스트림 내 요소들을 순회하며 predicate 인자로 들어온 연산의 결과가 true 인 요소들만 있는 스트림을 반환한다.
IntStream.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
.filter(it -> it % 2 == 0); // [2, 4, 6, 8, 10]
Stream<T> sorted()
Stream<T> sorted(Comparator<? super T> comparator)
인자 없는 메서드 호출 시 기본적으로 오름차순으로 정렬한다.
가공한 스트림을 끝내서 결과를 반환하는 메서드들로 이루어져 있다.
기본적으로 stream보다 for-loop의 성능이 더 좋다고 한다.
Effective Java의 공저자인 Langer의 컨퍼런스에서 for-loop와 stream 성능을 비교한 자료가 있다.
이 내용에 대해서 정리가 잘 된 블로그가 있어 참조한다.