Modern Java In Action 정리
Modern Java In Action을 읽고 내용을 정리해본다.
4장 스트림 소개
스트림(Stream)
데이터 처리 연산을 지원하도록 소스에서 추출된 연속된 요소
- 자바8에 추가된 기능으로 스트림을 이용하면 선언형으로 코드를 구현하여 컬렉션 데이터를 처리할 수 있다. 선언형으로 구현한다는 것은 for 루프나 if 조건문 등의 제어 블록을 사용하지 않고 동작을 지정하는 것이다.
→ 실제 구현은 신경쓰지 않고 사용하는 SQL를 생각하자.- 스트림에서 제공하는 filter, sorted, map, collect 같은 메서드들은 특정 스레딩 모델에 제한되지 않고 스레드와 락을 걱정할 필요없이 편리하게 데이터 병렬처리를 가능하게 해준다.
스트림 기본 구현
- 기본구현에 사용할 Dish 클래스
class Dish{ private final String name; private final boolean vegetarian; private final int calories; private final Type type; public Dish(String name, boolean vegetarian, int calories, Type type) { this.name = name; this.vegetarian = vegetarian; this.calories = calories; this.type = type; } public String getName() { return name; } public boolean isVegetarian() { return vegetarian; } public int getCalories() { return calories; } public Type getType() { return type; } enum Type { MEAT, FISH, OTHER } }
- 스트림 구현에 사용할 Dish 컬렉션
List<Dish> menu = Arrays.asList( new Dish("pork", false, 800, Dish.Type.MEAT), new Dish("beef", false, 700, Dish.Type.MEAT), new Dish("chicken", false, 400, Dish.Type.MEAT), new Dish("french fries", true, 530, Dish.Type.OTHER), new Dish("salmon", false, 450, Dish.Type.FISH) );
- 스트림 기본구현
@Test public void 스트림_기본구현(){ List<String> threeHighCaloricDishNames = menu.stream() // 컬렉션에서 스트림(Stream<Dish>)을 가져온다. .filter(dish -> dish.getCalories() > 300) // 해당 조건의 요소만 추출한다. .map(Dish::getName) // 이름(String) 속성을 스트림(Stream<String)으로 가져온다. .limit(3) // 3개를 제외하고 truncate한다. .collect(Collectors.toList()); // 스트림을 컬렉션(리스트)로 변환한다. System.out.println(threeHighCaloricDishNames); // [pork, beef, chicken] }
- filter, map 메서드는 인자로 Functional Interface 인스턴스를 받기 때문에 람다표현식이나, 메서드참조로 간결하게 코딩이 가능하다. 또한 메서드들은 실행 결과로 스트림을 리턴하기 때문에 파이프라인 형태가 된다.
→ 빌더 패턴과 유사하다. - 서로 연결이 가능한 filter, map, limit는 중간연산이며, collect는 스트림을 닫는 최종연산이다.
스트림과 컬렉션
- 컬렉션은 DVD에 저장된 영화에 비유할 수 있다. 모든 데이터(영화 내용 전부)가 메모리(DVD)에 저장되어 있다.
컬렉션에는 계산된 결과물이 저장되어 있으며, 주 관심사는 특정요소에 접근하여 값을 계산하거나 치환한다. - 스트림은 인터넷으로 스트리밍하는 영화에 비유할 수 있다.
영화 전체를 모두 받는 것이 아니라 미리 몇 프레임만 내려받아 재생이 가능할 수 있다. 스트림은 이론적으로 요청할 때만 요소를 계산하는 고정된 자료구조다. - 스트림은 한번만 탐색이 가능하다.
이미 소비된 스트림을 사용하려 하면 Exception이 발생하므로 다시 생성하여 사용해야 한다. - 컬렉션은 데이터를 순회하기 위해서는 루프문을 이용해 명시적으로 반복해야 한다. → 외부반복
- 스트림은 반복을 알아서 처리(중간값 저장, 최적화, 병렬성 구현 등) 한다. → 내부반복
@Test public void 스트림_내부반복(){ // forEach 메서드에서 반복되며 명시적으로 루프문이 필요없다. menu.stream() .forEach(dish -> System.out.println(dish.getName())); }
스트림은 게으른(lazy) 연산을 지원
- 스트림 처리시 한 요소는 연결된 모든 파이프라인을 타고나서 다음 요소가 처리된다.
@Test public void 스트림_게으른_연산(){ List<String> threeHighCaloricDishNames = menu.stream() .filter((dish) -> { System.out.println("filtering :: " + dish.getName()); return dish.getCalories() > 300; }) .map((dish) -> { System.out.println("mapping :: " + dish.getName()); return dish.getName(); }) .limit(3) .collect(Collectors.toList()); System.out.println(threeHighCaloricDishNames); // [pork, beef, chicken] }
- 콘솔출력으로 스트림 처리 순서를 보면 한 요소씩 filter → map → limit → collect 파이프라인을 타는것을 볼 수 있다. 중간 연산들(filter → map → limit)을 합친 다음에 최종 연산(collect)에서 한 번에 처리한다.
filtering :: pork mapping :: pork filtering :: beef mapping :: beef filtering :: chicken mapping :: chicken [pork, beef, chicken]