스트림
Java 8 API에 추가된 기능으로 스트림을 이용하면 선언형으로 컬렉션 데이터를 처리할 수 있다.
스트림이란 데이터 처리 연산을 지원하도록 소스에서 추출된 연속된 요소이다.
또한, 스트림을 이용하면 멀티스레드 코드를 구현하지 않아도 데이터를 투명하게 병렬로 처리할 수 있다.
- 선언형으로 간결하고 가독성이 좋다.
- 조립할 수 있어 유연성이 좋다.
- 병렬화로 성능이 좋다.
- 반복자와 마찬가지로 스트림도 한 번만 탐색할 수 있다. 즉, 탐색된 스트림 요소는 소비된다.
- 스트림 연산끼리 파이프라이닝을 구성할 수 있다. 그 덕에 게으름, 쇼트서킷 같은 최적화도 얻을 수 있다.
List<String> title = newArrayList("Java8", "In", "Action");
Stream<String> streamTitles = titles.stream();
streamTitles.forEach(System.out::println);
// Java.langIllegalStateException: 스트림이 이미 소비되었거나 닫힘
streamTitles.forEach(System.out::println);
※ 쇼트서킷
전체 스트림을 처리하지 않았더라도 결과를 반환할 수 있다.
예를 들어 && 연산으로 여러 개의 boolean 연산이 있는 경우, 하나라도 false가 나오면 나머지 결과와 상관없이
전체 결과가 false가 된다.
연산
중간연산
다른 스트림을 반환한다. 따라서 중간 연산을 연결해서 질의를 만들 수 있다.
중간 연산은 단말 연산을 스트림 파이프라인에 실행하기 전까지는 아무 연산도 수행하지 않는다. 즉 게으르다.
중간 연산을 합친 다음, 합쳐진 중간 연산을 최종 연산으로 한 번에 처리한다.
filter
프레디케이트를 인수로 받아서 프레디케이트와 일치하는 모든 스트림을 반환한다.
List<Dish> specialMenu = Arrays.asList(
new Dish("apple", true, 120, FRUIT)
new Dish("rice", false, 150, OTHER)
new Dish("onion", true, 200, VEGETABLE)
new Dish("chicken", true, 350, MEAT)
new Dish("french fries", true, 400, OTHER)
);
// result : onion
List<Dish> vegetarianMenu = menu.stream()
.filter(dish::isVegetarian).collect(toList());
distinct
고유 요소로 이루어진 스트림을 반환한다.
// result : 1, 2, 3, 4
List<Integer> numbers = Arrays.asList(1, 2, 1, 3, 2, 4);
numbers.stream().distinct().forEach(System.out::println);
takewhile
프레디케이트가 처음으로 거짓이 되는 지점까지의 스트림을 반환한다.
List<Dish> specialMenu = Arrays.asList(
new Dish("apple", true, 120, FRUIT)
new Dish("rice", false, 150, OTHER)
new Dish("chicken", true, 350, MEAT)
new Dish("french fries", true, 400, OTHER)
);
// specialMenu 리스트가 이미 칼로리 순으로 정렬되어 있을 경우
// takeWhile을 사용해서 칼로리가 320 이상인 요소를 발견하면 작업을 중단한다.
// 그리고 중단 이전의 값을 추출한다.
// result : apple rice
List<Dish> slicedMenu = specialMenu.stream()
.takeWhile(dish -> dish.getCalories() < 320)
.collect(toList());
limit
주어진 값 이하의 크기를 갖는 새로운 스트림을 반환한다.
List<Dish> specialMenu = Arrays.asList(
new Dish("apple", true, 120, FRUIT)
new Dish("rice", false, 150, OTHER)
new Dish("chicken", true, 350, MEAT)
new Dish("french fries", true, 400, OTHER)
);
// result : apple, rice, chicken
List<Dish> slicedMenu = specialMenu.stream()
.limit(3)
.collect(toList());
map
인수로 제공된 함수는 각 요소에 적용되며 함수를 적용한 결과가 새로운 요소로 매핑된다.
List<Dish> specialMenu = Arrays.asList(
new Dish("apple", true, 120, FRUIT)
new Dish("rice", false, 150, OTHER)
new Dish("chicken", true, 350, MEAT)
new Dish("french fries", true, 400, OTHER)
);
// Name으로 매핑 -> Name length로 매핑
// result : 5, 4, 7, 11
List<Dish> dishNameLengths = specialMenu.stream()
.map(Dish::getName)
.map(Strig::length)
.collect(toList());
최종 연산
- forEach : 스트림의 각 요소를 소비하면서 람다를 적용한다.
- count : 스트림의 요소 개수를 반환한다.
- collect : 스트림을 리듀스 해서 리스트, 맵, 정수 형식의 컬렉션을 만든다.
- anyMatch : 스트림에서 적어도 한 요소와 일치하는지 검사한다.
- allMatch : 스트림의 모든 요소가 주어진 프레디케이트와 일치하는지 검사한다.
- noneMatch : 주어진 프레디케이트와 일치하는 요소가 없는지 확인한다.
- findAny : 현재 스트림에서 임의의 요소를 반환한다.
- findFirst : 현재 스트림에서 첫 번째 요소를 반환한다.
컬렉션과의 차이점
컬렉션과 스트림 모두 연속된 요소 형식의 값을 저장하는 자료구조의 인터페이스를 제공한다.
데이터를 언제 계산하는지가 둘의 차이다.
컬렉션
- 현재 자료구조가 포함하는 모든 값을 메모리에 저장하는 구조이다.
- 즉, 컬렉션의 모든 요소는 컬렉션에 추가하기 전에 계산되어야 한다.
- 적극적 생성, 생산자 중심(팔기도 전에 창고를 가득 채움)
- ex) DVD
- 외부 반복 : 사용자가 직접 요소를 반복해야 한다. 명시적으로 컬렉션 항목을 하나씩 가져와서 처리한다.
스트림
- 요청할 때만 요소를 계산하는 고정된 자료구조이다.
- 사용자가 요청하는 값만 스트림에서 추출한다.
- 게으른 생성, 요청 중심 제조, 즉석 제조
- ex) 스트리밍
- 내부 반복 : 반복 과정을 사용자가 신경 쓰지 않아도 된다.
- 작업을 투명하게 병렬로 처리, 더 최적화된 다양한 순서로 처리가 가능하다.
정리
List를 사용하여 연산을 처리할 때 기존의 for문보다는 Stream을 쓰는 습관을 만들고 있다.
확실히 선언형이라 간결하고 가독성이 좋은 장점이 있는 느낌이다.
하지만, 무조건 Stream을 쓰는 것은 오히려 for문보다 성능 저하가 발생할 수 있다고도 하니 적절하게 사용하는 게 좋을 것 같다.
1~2ms 정도의 속도, 성능이 크리티컬 한 애플리케이션에서는 특히 주의가 필요해 보인다.
'Study > Modern Java8 in Action' 카테고리의 다른 글
[Java in Action] Default Method, Static Method 란? (0) | 2021.01.17 |
---|---|
[Java in Action] Collection API (List & Map & Set) (0) | 2021.01.17 |
[Java in Action] 병렬 데이터 처리(Parallel Stream)와 성능 (0) | 2021.01.17 |
[Java in Action] 람다, 함수형 인터페이스, 메서드 레퍼런스 (0) | 2021.01.17 |
[Java in Action] 동작 파라미터화 적용하여 리팩토링 (0) | 2020.03.17 |