어떤 사람이 컬렉션에서 칼로리가 적은 요리만 고르고 싶어 한다.
SELECT name FROM dishes WHERE calorie < 400
으로 나타낼 수 있다.컬렉션으로도 이와 비슷한 기능을 만들 수 있지 않을까?
💡 시간을 절약하고, 편리한 삶을 누릴 수 있는 방법은 무엇일까? → 스트림!!
lowCaloricDishes
라는 “가비지 변수”를 사용했다.List<Dish> lowCaloricDishes = new ArrayList<>();
for (Dish dish: menu) {
if (dish.getCalories() < 400) {
lowCaloricDishes.add(dish);
}
}
Collections.sort(lowCaloricDishes, new Comparator<Dish>() {
public int compare(Dish dish1, Dish dish2) {
return Integer.compare(dish1.getCalories(), dish2.getCalories());
}
});
Lish<String> lowCaloricDishesName = new ArrayList<>();
for (Dish dish: lowCaloricDishes) {
lowCaloricDishesName.add(dish.getName());
}
import static java.util.Comparator.comparing;
import static java.util.stream.Collectors.toList;
List<String> lowCaloricDishesName = menu.stream()
.filter(d -> d.getCalories() < 400)
.sorted(comparing(Dish::getCalories))
.map(Dish::getName) //요리명 추출
.collect(toList()); //리스트에 저장
stream()
을 parallelStream()
으로 바꾸면 이 코드를 멀티코어 아키텍처에서 병렬 실행할 수 있다!List<String> lowCaloricDishesName = menu.parallelStream()
.filter(d -> d.getCalories() < 400)
.sorted(comparing(Dish::getCalories))
.map(Dish::getName) //요리명 추출
.collect(toList()); //리스트에 저장
filter
, sorted
, map
, collect
를 말한다.Map<Dish.Type, List<Dish>> dishesByType =
menu.stream().collect(groupingBy(Dish::getType));
💡 Java 8의 스트림 API의 특징
- 선언형 : 더 간결하고 가독성이 좋아진다.
- 조립할 수 있음 : 유연성이 좋아진다.
- 병렬화 : 성능이 좋아진다.
public static final 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("rice", true, 350, Dish.Type.OTHER),
new Dish("season fruit", true, 120, Dish.Type.OTHER),
new Dish("pizza", true, 550, Dish.Type.OTHER),
new Dish("prawns", false, 400, Dish.Type.FISH),
new Dish("salmon", false, 450, Dish.Type.FISH)
);
public 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; }
public enum Type { MEAT, FISH, OTHER }
@Override
public String toString() {
return name;
}
}
→ 이제 스트림 API를 사용하는 방법을 자세히 살펴보자.
Java 8 컬렉션에는 스트림을 반환하는 stream()
메서드가 추가되었다.
스트림은 ‘데이터 처리 연산을 지원하도록 소스에서 추출된 연속된 요소’로 정의할 수 있다.
List<String> threeHighCaloricDishNames
= menu.stream() //스트림 얻기
.filter(dish -> dish.gerCalories() > 300)
.map(Dish::getName) //요리명 추출
.limit(3) //개수를 3개로 제한
.collect(toList()); //결과를 리스트로 저장
System.out.println(threeHighCaloricDishNames); //[pork, beef, chicken]
DVD에 어떤 영화가 저장되어 있다고 가정. (DVD에 전체 자료구조가 저장되어 있으므로 얘도 컬렉션.)
이번에는 인터넷 스트리밍으로 비디오를 시청한다고 가정. (스트리밍 → 스트림.)
스트리밍으로 비디오를 재생할 때는 사용자가 시청하는 부분의 몇 프레임을 미리 내려받는다.
→ 이 경우, 다른 값을 처리하지 않은 상태에서 미리 내려받은 프레임을 재생할 수 있다.
🌱 컬렉션과 스트림의 가장 큰 차이점은 “데이터를 언제 계산하느냐”이다.
컬렉션
→ 컬렉션은 현재 자료구조가 모든 값을 메모리에 저장하는 자료구조다.
→ 즉, 모든 요소는 컬렉션에 추가하기 전에 계산되어야 한다.
→ 적극적으로 생산된다. (생산자 중심: 팔기도 전에 창고를 가득 채움)
스트림
→ 이론적으로 요청할 때만 요소를 계산하는 고정된 자료구조다.
→ 이는 사용자가 요청하는 값만 스트림에서 추출한다는 뜻으로, 생산자와 소비자 관계를 형성한다.
→ 또한 스트림은 게으르게 만들어지는 컬렉션과 같다.
List<String> title = Arrays.asList("Java8", "In", "Action");
Stream<String> s = title.stream();
s.forEach(System.out::println); //title의 각 단어를 출력
s.forEach(System.out::println); //IllegalStateException 발생
List<String> names = new ArrayList<>();
for(Dish dish: menu) {
names.add(dish.getName());
}
List<String> names = new ArrayList<>();
Iterator<String> iterator = menu.iterator();
while (iterator.hasNext()) { //명시적 반복
Dish dish = iterator.next();
names.add(dish.getName());
}
List<String> names = menu.stream()
.map(Dish::getName)
.collect(toList());
스트림의 연산은 크게 두 가지로 구분할 수 있다.
- 스트림을 이어서 연결할 수 있는 중간 연산
- 스트림을 닫는 최종 연산
중간 연산의 특징은 단말 연산을 스트림 파이프라인에 실행하기 전까지는 아무 연산도 하지 않는다는 것.
즉, 게으르게 처리한다. → 합쳐진 중간 연산을 최종 연산으로 한 번에 처리한다!
public class HighCaloriesNames {
public static void main(String[] args) {
List<String> names = 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(toList());
System.out.println(names);
}
}
최종 연산은 스트림 파이프라인에서 결과를 도출한다.
보통 최종 연산에 의해 List, Integer, void 등 스트림 이외의 결과를 반환한다.
menu.stream().forEach(System.out::println);
스트림 이용 과정은 아래 세 가지로 요약할 수 있다.
- 질의를 수행할 데이터 소스(like 컬렉션)
- 스트림 파이프라인을 구성할 중간 연산 연결
- 스트림 파이프라인을 실행하고 결과를 만들 최종 연산
build()
메서드를 호출한다. (= 최종 연산)public class StreamBasic {
public static void main(String... args) {
// 자바 7
getLowCaloricDishesNamesInJava7(Dish.menu).forEach(System.out::println);
System.out.println("---");
// 자바 8
getLowCaloricDishesNamesInJava8(Dish.menu).forEach(System.out::println);
}
public static List<String> getLowCaloricDishesNamesInJava7(List<Dish> dishes) {
List<Dish> lowCaloricDishes = new ArrayList<>();
for (Dish d : dishes) {
if (d.getCalories() < 400) {
lowCaloricDishes.add(d);
}
}
List<String> lowCaloricDishesName = new ArrayList<>();
Collections.sort(lowCaloricDishes, new Comparator<Dish>() {
@Override
public int compare(Dish d1, Dish d2) {
return Integer.compare(d1.getCalories(), d2.getCalories());
}
});
for (Dish d : lowCaloricDishes) {
lowCaloricDishesName.add(d.getName());
}
return lowCaloricDishesName;
}
public static List<String> getLowCaloricDishesNamesInJava8(List<Dish> dishes) {
return dishes.stream()
.filter(d -> d.getCalories() < 400)
.sorted(comparing(Dish::getCalories))
.map(Dish::getName)
.collect(toList());
}
}