책을 읽으며 중요하다고 생각되는 내용들을 정리하였다.
책을 정리해도 내용이 많아서 넘버링을 하였다.
1장 자바 8, 9, 10, 11: 무슨 일이 일어나고 있는가
2장 동작 파라미터화 코드 전달하기
3장 람다 표현식
4장 스트림 소개
5장 스트림 활용
6장 스트림으로 데이터 수집
7장 병렬 데이터 처리와 성능
8장 컬렉션 API 개선
9장 리팩터링, 테스팅, 디버깅
병렬 연산을 지원하는 스트림 API 제공
메서드에 코드를 전달할 수 있다.
동작 파라미터화
함수가 일급 시민이 된다.
익명함수 람다 도입
public static List<Apple> filterGreenApples(List<Apple> inventory) {
List<Apple> result = new ArrayList<>();
for (Apple apple : inventory) {
if ("grren".equals(apple.getColor())) {
result.add(apple);
}
}
return result;
}
public static List<Apple> filterHeavyApples(List<Apple> inventory) {
List<Apple> result = new ArrayList<>();
for (Apple apple : inventory) {
if (apple.getWeight() > 150) {
result.add(apple);
}
}
return result;
}
filterGreenApples(List<Apple> inventory)
는 녹색 사과만 담는다.filterHeavyApples(List<Apple> inventory)
는 150 이상 무거운 사과만 담는다.if
조건문을 제외하고, 코드가 중복된다.
자바 8 메서드 전달
public static List<Apple> filterApples(List<Apple> inventory, Predicate<Apple> p) {
List<Apple> result = new ArrayList<>();
for (Apple apple : inventory) {
if (p.test(apple)) {
result.add(apple);
}
}
return result;
}
public static boolean isGreenApple(Apple apple) {
return "green".equals(apple.getColor());
}
public static boolean isHeavyApple(Apple apple) {
return apple.getWeight() > 150;
}
isGreenApple(Apple apple)
,isHeavyApple(Apple apple)
과 같은 동작을 필터링 메서드에 넘겨준다.
Predicate<Apple> p
로 받는다.if (p.test(apple))
를 통해 넘겨준 동작을 수행한다.
public static void main(String[] args) {
List<Apple> inventory = List.of(
new Apple(80, "green"),
new Apple(155, "green"),
new Apple(120, "red")
);
List<Apple> greenApples = filterApples(inventory, FilteringApples::isGreenApple);
System.out.println(greenApples); // [Apple{color='green', weight=80}, Apple{color='green', weight=155}]
List<Apple> heavyApples = filterApples(inventory, FilteringApples::isHeavyApple);
System.out.println(heavyApples); // [Apple{color='green', weight=155}]
}
실행 결과
자바 8 람다 전달
클래스의 메서드를 매번 정의하지 않고, 익명 메서드를 만들어 파라미터로 전달할 수 있다.
자주 사용하지 않고, 동작이 간단한 메서드는 람다로 표현하는 것이 간결하다.
List<Apple> greenApples2 = filterApples(inventory, a -> "green".equals(a.getColor()));
System.out.println(greenApples2); // [Apple{color='green', weight=80}, Apple{color='green', weight=155}]
List<Apple> heavyApples2 = filterApples(inventory, a -> a.getWeight() > 150);
System.out.println(heavyApples2); // [Apple{color='green', weight=155}]
기존의 컬렉션은 for-each
루프를 이용해 외부 반복을 해왔다.
스트림은 내부 반복을 통해 모든 데이터를 처리한다.
데이터 필터링, 추출, 그룹화와 같은 기능이 있다.
쉽게 병렬화 할 수 있다.
구현 클래스에서 구현하지 않아도 되는 메서드를 인터페이스에 추가할 수 있다.
이를 통해 기존의 인터페이스 구현체들의 코드를 건드리지 않고도 인터페이스를 자유롭게 확장할 수 있다.
<T>
클래스NullPointer
예외를 피할 수 있도록 도와준다.
값이 없는 상황 (null
) 을 어떻게 처리할 지 명시적으로 구현하는 메서드를 포함한다.
동작 파라미터화란 아직은 실행되지 않은 코드 블록을 의미한다.
나중에 프로그램에서 호출한다.
자주 바뀌는 요구사항에 효과적으로 대응할 수 있다.
public static List<Apple> filterApplesByColor(List<Apple> inventory, Color color) {
List<Apple> result = new ArrayList<>();
for (Apple apple : inventory) {
if (apple.getColor() == color) {
result.add(apple);
}
}
return result;
}
public static List<Apple> filterApplesByWeight(List<Apple> inventory, int weight) {
List<Apple> result = new ArrayList<>();
for (Apple apple : inventory) {
if (apple.getWeight() > weight) {
result.add(apple);
}
}
return result;
}
- 사과를 색으로 구분하는 메서드와
- 사과를 무게로 구분하는 메서드
- 만약 다른 구분 조건이 필요하다면, 또 다른 중복 메서드를 만들어야 한다.
필터링 조건마다 참 거짓을 반환하는 Predicate
클래스를 구현하여 메서드에 전달
하나의 메서드가 다른 동작을 수행하도록 재활용 할 수 있다.
클래스를 구현해야 하기에 로직과 관련없는 코드가 많이 추가된다.
public interface ApplePredicate {
boolean test(Apple apple);
}
public class AppleGreenColorPredicate implements ApplePredicate {
@Override
public boolean test(Apple apple) {
return Color.GREEN == apple.getColor();
}
}
public class AppleHeavyWeightPredicate implements ApplePredicate {
@Override
public boolean test(Apple apple) {
return apple.getWeight() > 150;
}
}
전략 디자인 패턴
public static List<Apple> filterApples(List<Apple> inventory, ApplePredicate p) {
List<Apple> result = new ArrayList<>();
for (Apple apple : inventory) {
if (p.test(apple)) { // 사과 검사 조건을 캡슐화
result.add(apple);
}
}
return result;
}
public static void main(String... args) {
List<Apple> greenApples2 = filterApples(inventory, new AppleGreenColorPredicate());
}
ApplePredicate p
를 파라미터로 받아 유연하게 필터링
Predicate
을 익명 클래스로 구현하여 메서드에 전달
클래스를 직접 구현하는 것보다 코드의 양을 줄일 수 있다.
그러나 여전히 많은 공간을 차지한다.
List<Apple> redApples2 = filterApples(inventory, new ApplePredicate() {
@Override
public boolean test(Apple apple) {
return Color.RED == apple.getColor();
}
});
익명 클래스는 사용시 혼란스러운 부분이 있다.
public class MeaningOfThis {
public final int value = 4;
public void doIt() {
int value = 6;
Runnable r = new Runnable() {
public final int value = 5;
public void run() {
int value = 10;
System.out.println(this.value);
}
};
r.run();
}
public static void main(String[] args) {
MeaningOfThis m = new MeaningOfThis();
m.doIt(); // 출력 결과 : 5
}
}
run()
메서드 내의System.out.println(this.value)
의this
는Runnable
익명 클래스를 참조한다.
Predicate
를 람다 표현식을 사용해 메서드에 전달
List<Apple> redApples3 = filterApples(inventory, apple -> Color.RED == apple.getColor());
메서드로 전달할 수 있는 익명 함수를 단순화 한 것
특정 클래스에 종속되지 않으므로 메서드가 아니라 함수라고 부른다.
파라미터, 화살표, 바디로 이루어진다.
정확히 하나의 추상 메서드를 지정하는 인터페이스
람다 표현식으로 함수형 인터페이스의 추상 메서드를 구현할 수 있다.
@FunctionalInterface
애노테이션을 인터페이스 레벨에 붙여 함수형 인터페이스임을 명시할 수 있다.
@FunctionalInterface
public interface BufferedReaderProcessor {
String process(BufferedReader b) throws IOException;
}
함수형 인터페이스 생성
Java
8
Lambdas
In
Action
텍스트 파일 생성
public class ExecuteAround {
private static final String FILE = ExecuteAround.class.getResource("./data.txt").getFile();
public static void main(String[] args) throws IOException {
String oneLine = processFile((BufferedReader b) -> b.readLine()); // 한 줄만 읽는 동작 전달
System.out.println(oneLine); // Java
System.out.println();
String twoLines = processFile((BufferedReader b) -> b.readLine() + b.readLine()); // 두 줄을 읽는 동작 전달
System.out.println(twoLines); // Java8
}
public static String processFile(BufferedReaderProcessor p) throws IOException {
try (BufferedReader br = new BufferedReader(new FileReader(FILE))) { // 자원 준비, 정리과정
return p.process(br); // 핵심 로직 동작
}
}
}
processFile(BufferedReaderProcessor p)
메서드의BufferedReaderProcessor p
를 나타내는 람다를 전달하여 핵심 로직 동작을 유연하게 변경
java.util.function
패키지에서 여러가지 함수형 인터페이스를 제공한다.Predicate<T>
(T t) -> boolean
test
추상 메서드를 정의
진위 여부를 판별하는 뉘앙스
public class PredicateExample {
public static void main(String[] args) {
List<String> strings = List.of("", "a", "b", "", "c");
List<String> nonEmptyStrings = filter(strings, s -> !s.isEmpty());
System.out.println(nonEmptyStrings); // "a", "b", "c"
}
public static <T> List<T> filter(List<T> list, Predicate<T> p) {
List<T> results = new ArrayList<>();
for (T t : list) {
if (p.test(t)) { // 전달받은 람다를 실행
results.add(t);
}
}
return results;
}
}
Consumer<T>
(T t) -> Void
accept
추상 메서드를 정의
특정 동작을 시키는 뉘앙스
public class ConsumerExample {
public static void main(String[] args) {
forEach(
List.of(1, 2, 3, 4, 5),
i -> System.out.println(i)
);
}
public static <T> void forEach(List<T> list, Consumer<T> c) {
for (T t : list) {
c.accept(t); // 전달받은 람다를 실행
}
}
}
Function<T, R>
(T t) -> (R r)
apply
추상 메서드를 정의
입력을 출력으로 매핑하는 뉘앙스
public class FunctionExample {
public static void main(String[] args) {
List<Integer> mapped = map(
List.of("lambdas", "in", "actions"),
s -> s.length()
);
System.out.println(mapped);
}
public static <T, R> List<R> map(List<T> list, Function<T, R> f) {
List<R> result = new ArrayList<>();
for (T t : list) {
result.add(f.apply(t)); // 전달받은 람다를 실행
}
return result;
}
}
기본형 특화 함수형 인터페이스
제네릭 파라미터에는 참조형만 사용할 수 있다.
박싱 (기본형 ➜ 참조형) 은 비용이 든다.
박싱한 값은 힙에 저장되므로, 메모리를 소비한다.
값을 가져올 때, 메모리를 탐색하는 과정이 추가로 필요하다.
이러한 오토 박싱을 막기 위해 기본형을 입출력으로 사용하는, 함수형 인터페이스가 존재한다.
IntPredicate
, DoublePredicate
, IntConsumer
, IntFunction
등이 있다.
기본 제공된 함수형 인터페이스 목록: https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/function/package-summary.html
람다 표현식 자체에는 람다가 어떤 함수형 인터페이스를 구현하는지의 정보가 없다.
filter(inventory, (Apple a) -> a.getWeight() > 150); // 람다 전달
filter(List<Apple> inventory, Predicate<Apple> p) // 메서드 시그니처
- 메서드 시그니처의
Predicate<Apple> p
는Apple -> boolean
함수 디스크립터를 갖는다.- 람다 시그니처는
Apple -> boolean
으로 메서드의 함수 디스크립터와 일치한다.- 따라서 형식검사가 성공적으로 완료된다.
public class TargetTypingExample {
public static void main(String[] args) {
execute(() -> System.out.println("ho")); // java: reference to execute is ambiguous, 컴파일 불가
execute((Walker) () -> System.out.println("ho")); // 컴파일 가능
execute((Runner) () -> System.out.println("ho")); // 컴파일 가능
}
public static void execute(Walker walker) {
walker.walk();
}
public static void execute(Runner runner) {
runner.run();
}
}
@FunctionalInterface
interface Walker {
void walk();
}
@FunctionalInterface
interface Runner {
void run();
}
execute
메서드는 동일한 함수 디스크립터를 갖고, 메서드 오버로딩을 사용했다.- 첫 번째
execute
실행의 경우, 어떤 메서드를 실행할 지 몰라 컴파일 에러 발생- 두 번째, 세 번째처럼 타입 캐스팅을 통해 컴파일 에러 해결 가능
람다식에 파라미터로 넘겨지지 않고 외부에 정의된 지역 변수를 자유 변수라 한다.
자유 변수는 final
이거나 한번도 변경되지 않아야만 사용할 수 있다.
int freeVariable = 1234;
Runnable r = () -> System.out.println(freeVariable); // 컴파일 에러 발생
freeVariable = 4321;
public Supplier<Integer> incrementer(int start) {
return () -> start++; // 컴파일 에러 발생
}
public class VariableExample {
int instanceVariable = 10000;
public Supplier<Integer> incrementer() {
return () -> instanceVariable++;
}
}
인스턴스 변수는 힙에 저장되기 때문에 멀티쓰레드 환경에서도 람다가 가장 최신의 변수값에 접근할 수 있다.
지역 변수는 스택에 저장된다.
지역 변수가 존재하는 스레드가 사라져서 변수 할당이 해제되어도 람다는 살아남아 변수를 사용해야 한다.
이를 위해 람다는 지역 변수의 복사본을 만들어 사용한다.
final
제약 조건이 필요하다.특정 메서드만을 호출하는 람다의 축약형
정적 메서드 참조, 객체의 인스턴스 메서드 참조, 생성자와 같은 특별한 형식의 메서드 참조등이 있다.
클래스명::메서드명
, 객체명::메서드명
, 클래스명::new
와 같은 형식으로 사용한다.
public static void main(String[] args) {
List<Apple> inventory = Arrays.asList(
new Apple(80, Color.GREEN),
new Apple(155, Color.GREEN),
new Apple(120, Color.RED)
);
// inventory.sort(Comparator.comparing(apple -> apple.getWeight())); // 람다 표현
inventory.sort(Comparator.comparing(Apple::getWeight)); // 메서드 참조 표현
// [Apple{color=GREEN, weight=80}, Apple{color=RED, weight=120}, Apple{color=GREEN, weight=155}]
System.out.println(inventory);
}
Comparator, Function, Predicate
와 같은 함수형 인터페이스는 유틸리티 메서드를 제공한다.
default method
를 사용하여 함수형 인터페이스가 추가적인 메서드를 제공할 수 있다.
유틸리티 메서드를 이용해 여러개의 람다 표현식을 조합할 수 있다.
Comparator
inventory.sort(Comparator.comparing(Apple::getWeight).reversed()); // 정렬결과를 역순으로 뒤집는다.
// [Apple{color=GREEN, weight=155}, Apple{color=RED, weight=120}, Apple{color=GREEN, weight=80}]
System.out.println(inventory);
inventory.sort(Comparator.comparing(Apple::getWeight)
.reversed() // 무게를 내림차순으로 정렬
.thenComparing(Apple::getName)); // 두 사과의 무게가 같으면 이름순으로 오름차순 정렬
.thenComparing(차순위 정렬조건)
을 통해 정렬조건을 추가할 수 있다.
Predicate
조합
Predicate<Apple> redApple = apple -> apple.getColor() == Color.RED;
Predicate<Apple> notRedApple = redApple.negate();
Predicate<Apple> redAndHeavyApple = redApple.and(apple -> apple.getWeight() > 150);
Predicate<Apple> redAndHeavyAppleOrGreen = redApple.and(apple -> apple.getWeight() > 150)
.or(apple -> apple.getColor() == Color.GREEN);
.negate()
로 특정Predicate
반전.and(Predicate)
로 두 개 이상의Predicate
를 AND 로 연결.or(Predicate)
로 두 개 이상의Predicate
를 OR 로 연결
Function
조합 Function<Integer, Integer> f = x -> x + 1;
Function<Integer, Integer> g = x -> x * 2;
Function<Integer, Integer> h = f.andThen(g); // f -> g
System.out.println(h.apply(1)); // 4
Function<Integer, Integer> i = f.compose(g); // f(g(x)), g -> f
System.out.println(i.apply(1)); // 3
데이터를 그룹화하고 처리하는데 사용하는 컬렉션은 두가지 한계점이 있다.
데이터를 처리, 가공할 때 가독성이 떨어진다.
병렬 처리 코드의 구현이 어렵다.
이러한 한계점을 극복하기 위해 자바8에 스트림 (Streams) API 가 추가되었다.
데이터 처리 연산을 지원하도록 소스에서 추출된 연속된 요소
대부분의 스트림 연산의 결과는 스트림 타입을 반환한다.
내부 반복을 지원한다.
public static List<String> getLowCaloricDishesNamesInJava7(List<Dish> dishes) {
ArrayList<Dish> lowCaloricDishes = new ArrayList<>();
for (Dish dish : dishes) {
if (dish.getCalories() < 400) {
lowCaloricDishes.add(dish);
}
}
ArrayList<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 dish : lowCaloricDishes) {
lowCaloricDishesName.add(dish.getName());
}
return lowCaloricDishesName;
}
public static List<String> getLowCaloricDishesNamesInJava8(List<Dish> dishes) {
return dishes.stream()
.filter(dish -> dish.getCalories() < 400)
.sorted(Comparator.comparing(Dish::getCalories))
.map(Dish::getName)
.collect(Collectors.toList());
}
- 요리 리스트에서 저칼로리 식단을 정렬한 후, 이름을 뽑아오는 예제
getLowCaloricDishesNamesInJava7(List<Dish> dishes)
는 고전적인 방식getLowCaloricDishesNamesInJava8(List<Dish> dishes)
는 스트림을 이용한 방식
선언형
조립할 수 있음
병렬화
컬렉션은 DVD이고, 스트림은 인터넷 스트리밍이다.
DVD (컬렉션) 은 전체 영화가 저장되어 있다.
인터넷 스트리밍 (스트림) 은 비디오를 재생할 때 사용자가 시청하는 부분의 몇 프레임을 미리 내려 받는다.
다른 대부분의 값은 아직 처리하지 않았다.
모든 프레임을 저장할 수 없다.
스트림은 딱 한번만 탐색할 수 있다.
컬렉션은 사용자가 직접 for
문 등으로 요소를 반복해야 한다.
반면 스트림은 반복을 알아서 처리한다.
내부 반복
스트림이 스스로 최적화를 수행한다.
스트림 연산은 크게 두 가지로 구분한다.
중간 연산
스트림을 반환하므로 연결할 수 있다.
스트림 파이프라인을 만든다.
최종 연산을 연결하기 전까지는 아무런 연산도 수행하지 않는다. (게으르다)
filter
, map
, limit
, sorted
, distinct
등이 있다.
최종 연산
스트림을 닫는 연산으로 연결할 수 없다.
스트림 파이프라인을 실행하고 결과를 만들어 반환한다.
forEach
, count
, collect
등이 있다.
.filter(Predicate)
Predicate
를 인수로 전달하여 일치하는 요소들만 걸러낸 스트림 생성 Stream<Dish> vegetarianMenuStream = menu.stream()
.filter(Dish::isVegetarian);
.distinct()
List<Integer> numbers = List.of(1, 1, 2, 2, 3, 3, 4);
Stream<Integer> distinctStream = numbers.stream()
.distinct();
.takeWhile(Predicate)
Predicate
를 불만족하면 반복작업을 중단하고 현재 작업 지점까지의 요소들을 스트림으로 반환
이후 남은 요소들에 대해 작업 수행 X
Stream<Dish> slicedMenu1 = menu.stream()
.takeWhile(dish -> dish.getCalories() < 320);
.dropWhile(Predicate)
Predicate
를 불만족하면 반복작업을 중단하고 현재 작업 지점까지의 요소들을 모두 버리고 나머지 요소들을 스트림으로 반환
남은 요소들에 대해 작업 수행 X
Stream<Dish> slicedMenu2 = menu.stream()
.dropWhile(dish -> dish.getCalories() < 320);
.limit(n)
n
개 이하의 요소를 갖는 스트림을 반환 Stream<Dish> limitedMenu = menu.stream()
.limit(3);
.skip(n)
n
개의 요소를 제외한 스트림을 반환 Stream<Dish> skippedMenu = menu.stream()
.skip(3);
.map(Function)
Stream<Integer> mappedNameLength = menu.stream()
.map(Dish::getName)
.map(String::length);
Dish
객체 ➜Dish
객체의 이름 ➜ 이름의 길이 로 각 요소들을 변환- Chaining 할 수 있다.
.flatMap(Function)
변환 함수를 적용해서 생성된 각각의 스트림에서 콘텐츠만 남긴다.
map(Function)
의 경우 각각의 스트림의 스트림이 만들어진다.해당 콘텐츠들만 모아서 하나의 스트림으로 평면화한다.
{"Hello", "World"}
➜ {'H', 'e', 'l', 'o', 'W', 'r', 'd'}
로 만들고 싶은 경우
List<String[]> collect = words.stream()
.map(word -> word.split(""))
.distinct()
.collect(Collectors.toList());
map
의 결과는String
배열 스트림이다.- 이 방식으로는 목적을 이룰 수 없다.
List<String> wanted = words.stream()
.map(word -> word.split(""))
.flatMap(array -> Arrays.stream(array)) // 각 배열을 각 스트림으로 변환하고, 하나의 스트림으로 합친다.
.distinct()
.collect(Collectors.toList());
flatMap
의 결과는 하나로 평탄화된 스트림이다.- 이 방식으로 목적을 이룰 수 있다.
.anyMatch(Predicate)
Predicate
를 만족하는 것이 있으면 true 반환 if (menu.stream().anyMatch(Dish::isVegetarian)) {
// ...
}
.allMatch(Predicate)
Predicate
를 만족하면 true 반환 if (menu.stream().allMatch(dish -> dish.getCalories() < 1000)) {
// ...
}
.noneMatch(Predicate)
Predicate
를 만족하지 않는다면 true 반환 if (menu.stream().noneMatch(dish -> dish.getCalories() >= 1000)) {
// ...
}
.findAny()
임의의 요소 1개를 반환한다.
병렬 실행 시 제약이 적다.
Optional<Dish> any = menu.stream()
.findAny();
.findFirst()
스트림의 첫 번째 요소를 반환한다.
병렬 실행 시 첫 번째 요소를 찾기 어렵다.
Optional<Dish> first = menu.stream()
.findFirst();
allMatch
, noneMatch
, findFirst
, findAny
, limit
메서드들은 단락 평가 (쇼트 서킷) 를 수행한다.
and 연산과 or 연산의 수행 방식과 유사하다.
결과가 이미 결정되면 나머지 요소들을 검사하지 않고 즉시 결과를 반환한다.
모든 스트림 요소를 처리해서 하나의 값으로 도출하는 연산
.reduce(초기값, BinaryOperator<T>)
로 이루어진다.
BinaryOperator<T>
는 두 요소를 조합해서 새로운 값을 만든다. Integer total = numbers.stream()
.reduce(0, (sum, cur) -> sum + cur);
reduce(0, Integer::sum)
으로 간소화할 수 있다.- 초기값을 설정하지 않으면
Optional
객체를 반환한다.
- 스트림에 아무 요소도 없는 상황을 대비한다.
Optional<Integer> maximum = numbers.stream()
.reduce((max, cur) -> Integer.max(max, cur));
Optional<Integer> minimum = numbers.stream()
.reduce((min, cur) -> Integer.min(min, cur));
내부 상태를 갖지 않는 연산
특별히 기억해야할 상태가 없다.
map
, filter
등
한정된 내부 상태를 갖는 연산
결과를 누적할 작은 크기의 내부 상태가 필요하다.
reduce
, sum
, max
등
크기가 정해지지 않은 내부 상태를 갖는 연산
과거의 이력을 알고 있어야 한다.
데이터 스트림의 크기가 무한이라면 문제가 발생한다.
sorted
, distinct
등
menu.stream()
.map(Dish::getCalories) // Stream<Integer>
.sum(); // 컴파일 에러
int calories = menu.stream()
.mapToInt(Dish::getCalories) // Stream<Integer> -> IntStream
.sum();
Stream<T>
인터페이스는sum
메서드가 없다.IntStream
인터페이스는sum
메서드를 지원한다.
박싱 비용을 피하고 숫자 스트림을 효율적으로 처리할 수 있도록 기본형 특화 스트림을 제공한다.
IntStream
, DoubleStream
, LongStream
이 있다.
.mapToInt(Function)
, mapToDouble(Function)
, mapToLong(Function)
메서드를 사용해 Stream<T>
를 기본형 특화 스트림으로 반환한다.
sum
, max
, min
, average
와 같은 자주 사용하는 유틸리티 메서드를 제공한다.
max
, min
, average
의 경우 OptionalInt
, OptionalDouble
을 반환한다.기본형 특화 스트림을 객체 스트림으로 복원하는 기능도 제공한다.
.boxed()
메서드 사용특정 범위의 숫자 생성
IntStream
, LongStream
의 .range(시작, 끝)
, .rangeClosed(시작, 끝)
메서드로 만들 수 있다. IntStream.range(1, 5)
.forEach(System.out::print); // 1234
IntStream.rangeClosed(1, 5)
.forEach(System.out::print); // 12345
크기가 고정되지 않은 스트림을 만들 수 있다.
.iterate(초기값, UnaryOperator<T>)
람다를 만족하는 새로운 값을 끊임없이 생산
limit(n)
메서드를 이용해 크기를 제한하여 사용해야 한다.
Stream.iterate(0, n -> n + 2)
.limit(5)
.forEach(System.out::print); // 02468
.generate(Supplier<T>)
람다를 실행하여 새로운 값을 끊임없이 생산
limit(n)
메서드를 이용해 크기를 제한하여 사용해야 한다.
Stream.generate(() -> 1)
.limit(5)
.forEach(System.out::print); // 11111
스트림 API: https://docs.oracle.com/en/java/javase/19/docs/api/java.base/java/util/stream/Stream.html
collect
최종 연산을 사용해 다양한 최종 결과를 도출할 수 있다.
reduce
연산으로도 동일한 작업을 수행할 수 있지만 가독성이 떨어진다.
private static Map<Currency, List<Transaction>> groupImperatively() {
Map<Currency, List<Transaction>> transactionsByCurrencies = new HashMap<>();
for (Transaction transaction : transactions) {
Currency currency = transaction.getCurrency();
List<Transaction> transactionsForCurrency = transactionsByCurrencies.get(currency);
if (transactionsForCurrency == null) {
transactionsForCurrency = new ArrayList<>();
transactionsByCurrencies.put(currency, transactionsForCurrency);
}
transactionsForCurrency.add(transaction);
}
return transactionsByCurrencies;
}
명령형 버전의 통화별로 트랜잭션을 그룹화하는 코드
private static Map<Currency, List<Transaction>> groupFunctionality() {
return transactions.stream()
.collect(Collectors.groupingBy(Transaction::getCurrency));
}
collect
를 사용해 통화별로 트랜잭션을 그룹화하는 코드- 더욱 간결하고 가독성 있게 표현한다.
.collect(Collector 인터페이스 구현)
으로 원하는 스트림 결과물을 만든다.
함수형 프로그래밍에서는 무엇을 원하는지만 명시한다.
어떤 방식으로 만들지는 신경쓸 필요없다.
자주 사용하는 Collector
인스턴스를 손쉽게 생성할 수 있는 정적 팩토리 메서드를 제공한다.
Collectors.toList()
Collectors.counting()
private static long calculateTotalCount() {
return Dish.menu.stream().collect(Collectors.counting());
}
Collectors.summarizingInt(mapper)
개수, 합계, 최대, 최소, 평균 정보를 수집하여 반환한다.
long, double 에 대응하는 summarizingLong(mapper)
, summarizingDouble(mapper)
도 있다.
private static IntSummaryStatistics calculateMenuStatistics() {
return menu.stream().collect(Collectors.summarizingInt(Dish::getCalories));
}
public static void main(String[] args) {
System.out.println("통계: " + calculateMenuStatistics());
// 통계: IntSummaryStatistics{count=9, sum=4300, min=120, average=477.777778, max=800}
}
Collectors.joining(구분자)
toString
메서드를 호출해 추출한 모든 문자열을 하나의 문자열로 연결해서 반환한다. private static String getShortMenu() {
return menu.stream().map(Dish::getName).collect(Collectors.joining());
}
private static String getShortMenuCommaSeparated() {
return menu.stream().map(Dish::getName).collect(Collectors.joining(", "));
}
public static void main(String[] args) {
System.out.println(getShortMenu());
// porkbeefchickenfrench friesriceseason fruitpizzaprawnssalmon
System.out.println(getShortMenuCommaSeparated());
// pork, beef, chicken, french fries, rice, season fruit, pizza, prawns, salmon
}
구분자를 넣지 않으면 모두 붙어서 출력된다.
Collectors.toCollection(컬렉션 supplier)
List<Integer> numbers = List.of(1, 2, 3, 4, 5);
ArrayList<Integer> collect = numbers.stream()
.collect(Collectors.toCollection(ArrayList::new));
하나 이상의 특성으로 데이터 집합을 분류해서 그룹화 할 수 있다.
Collectors.groupingBy(분류함수)
스트림의 각 요소에 분류함수를 적용해 key
를 만든다.
앞서 만든 key
에 매핑되는 value
는 분류함수 적용 전의 원래 요소이다.
이렇게 그룹화된 Map
을 반환한다.
private static Map<Type, List<Dish>> groupDishesByType() {
return menu.stream().collect(Collectors.groupingBy(Dish::getType));
}
private static Map<CaloricLevel, List<Dish>> groupDishesByCalorie() {
return menu.stream().collect(Collectors.groupingBy(dish -> {
if (dish.getCalories() <= 400) return CaloricLevel.DIET;
else if (dish.getCalories() <= 700) return CaloricLevel.NORMAL;
else return CaloricLevel.FAT;
}));
}
복잡한 분류 기준이 필요한 경우
groupDishesByCalorie()
메서드처럼 분류함수를 구현하면 된다.
Collectors.filtering(Predicate, downstream)
우선 그룹화를 한 뒤, 그룹의 요소들을 필터링한다.
각 그룹별로 스트림이 생성된다.
필터링한 요소들을 downstream 에 넣어 반환한다.
예를 들어, 500 칼로리가 넘는 요리를 타입으로 그룹화하려고 한다.
private static Map<Type, List<Dish>> groupCaloricDishesByTypeUsingFilter() {
return menu.stream()
.filter(dish -> dish.getCalories() > 500)
.collect(Collectors.groupingBy(Dish::getType));
}
public static void main(String[] args) {
System.out.println(groupCaloricDishesByTypeUsingFilter());
// {OTHER=[french fries, pizza], MEAT=[pork, beef]}
}
filter
메서드를 통해 500 칼로리가 넘는 요리가 제거되어,FISH
타입 자체가 그룹에서 사라진다.
private static Map<Type, List<Dish>> groupCaloricDishesByType() {
return menu.stream().collect(Collectors.groupingBy(
Dish::getType,
Collectors.filtering(dish -> dish.getCalories() > 500, Collectors.toList())
));
}
public static void main(String[] args) {
System.out.println(groupCaloricDishesByType());
// {OTHER=[french fries, pizza], MEAT=[pork, beef], FISH=[]}
}
Collectors.groupingBy
의 두 번째 파라미터로Collectors.filtering
을 넘길 수 있다.- 이 경우 그룹화된 요소들을 필터링해 재그룹화 한다.
- 비어있는
FISH
타입을 그룹에서 확인할 수 있다.
Collectors.mapping(mapper, downstream)
우선 그룹화를 한 뒤, 그룹의 요소들을 변환한다.
각 그룹별로 스트림이 생성된다.
변환한 요소들을 downstream에 넣어 반환한다.
private static Map<Type, List<String>> groupDishNamesByType() {
return menu.stream().collect(Collectors.groupingBy(
Dish::getType,
Collectors.mapping(Dish::getName, Collectors.toList()) // List<Dish> -> List<String>
));
}
Collectors.flatMapping(mapper, downstream)
우선 그룹화를 한 뒤, 각 그룹내의 요소들을 변환한다.
각 그룹별로 그룹 스트림이 생성된다.
각 그룹내의 요소들의 변환 결과물이 스트림일 경우
그룹별로 변환된 요소들(스트림) 을 하나의 그룹 스트림으로 묶어 평탄화한다.
평탄화한 요소들을 downstream 에 넣어 반환한다.
private static Map<Type, Set<String>> groupDishTagsByType() {
Map<String, List<String>> dishTags = Map.of(
"pork", List.of("greasy", "salty"),
"beef", List.of("salty", "roasted"),
"chicken", List.of("fried", "crisp"),
"french fries", List.of("greasy", "fried"),
"rice", List.of("light", "natural"),
"season fruit", List.of("fresh", "natural"),
"pizza", List.of("tasty", "salty"),
"prawns", List.of("tasty", "roasted"),
"salmon", List.of("delicious", "fresh"));
return menu.stream().collect(Collectors.groupingBy(
Dish::getType,
Collectors.flatMapping(dish -> dishTags.get(dish.getName()).stream(), Collectors.toSet())
));
}
public static void main(String[] args) {
System.out.println(groupDishTagsByType());
// {OTHER=[salty, greasy, natural, light, tasty, fresh, fried],
// MEAT=[salty, greasy, roasted, fried, crisp],
// FISH=[roasted, tasty, fresh, delicious]}
}
downstream
으로 Collectors.groupingBy(...)
를 사용하여 다수준으로 그룹화할 수 있다. private static Map<Type, Map<CaloricLevel, List<Dish>>> groupDishByTypeAndCaloricLevel() {
return menu.stream().collect(Collectors.groupingBy(
Dish::getType, // 첫 번째 수준의 분류 함수
Collectors.groupingBy(dish -> { // 두 번재 수준의 분류 함수
if (dish.getCalories() <= 400) return CaloricLevel.DIET;
else if (dish.getCalories() <= 700) return CaloricLevel.NORMAL;
else return CaloricLevel.FAT;
})
));
}
public static void main(String[] args) {
System.out.println(groupDishByTypeAndCaloricLevel());
// {
// OTHER={NORMAL=[french fries, pizza], DIET=[rice, season fruit]},
// MEAT={NORMAL=[beef], DIET=[chicken], FAT=[pork]},
// FISH={NORMAL=[salmon], DIET=[prawns]}
// }
}
- 외부맵은 첫 번째 수준의 분류함수에서 분류한
key
값FISH, MEAT, OTHER
를 갖는다.- 내부맵은 두 번째 수준의 분류함수에서 분류한
key
값NORMAL, DIET, FAT
을 갖는다.
downstream
으로 Collectors.counting()
을 사용하여 그룹별로 개수를 계산할 수 있다. private static Map<Type, Long> countDishesInGroups() {
return menu.stream().collect(Collectors.groupingBy(
Dish::getType,
Collectors.counting()
));
}
public static void main(String[] args) {
System.out.println(countDishesInGroups());
// {OTHER=4, MEAT=3, FISH=2}
}
downstream
으로 Collectors.maxBy(Comparator)
, Collectors.minBy(Comparator)
를 사용해 각 그룹에서의 최대, 최소를 구할 수 있다. private static Map<Type, Optional<Dish>> mostCaloricDishesByType() {
return menu.stream().collect(Collectors.groupingBy(
Dish::getType,
Collectors.maxBy(Comparator.comparingInt(Dish::getCalories))
));
}
public static void main(String[] args) {
System.out.println(mostCaloricDishesByType());
// {OTHER=Optional[pizza], MEAT=Optional[pork], FISH=Optional[salmon]}
}
Collectors.maxBy
의 결과가Optional
이므로, 출력 결과도 다음과 같다.
private static Map<Type, Dish> mostCaloricDishesByTypeWithoutOptionals() {
return menu.stream().collect(Collectors.groupingBy(
Dish::getType,
Collectors.collectingAndThen( // 컬렉터가 반환한 결과를 변환한다.
Collectors.maxBy(Comparator.comparingInt(Dish::getCalories)),
Optional::get
)
));
}
public static void main(String[] args) {
System.out.println(mostCaloricDishesByTypeWithoutOptionals());
// {OTHER=pizza, MEAT=pork, FISH=salmon}
}
Collectors.collectingAndThen
은 컬렉터와 변환 함수를 파라미터로 받는다.
- 컬렉터의 각 요소들에 변환함수를 적용한, 새로운 컬렉터를 반환한다.
- 이를 통해
Optional
을 제거할 수도 있다.
downstream
으로 Collectors.summingInt(mapper)
, Collectors.summingLong(mapper)
, Collectors.summingDouble(mapper)
를 사용해 각 그룹에서의 총 합계를 구할 수 있다. private static Map<Type, Integer> sumCaloriesByType() {
return menu.stream().collect(Collectors.groupingBy(
Dish::getType,
Collectors.summingInt(Dish::getCalories)
));
}
public static void main(String[] args) {
System.out.println(sumCaloriesByType());
// {OTHER=1550, MEAT=1900, FISH=850}
}
Predicate
를 분류함수로 사용하는 특수한 그룹화
맵의 key
형식은 Boolean
으로 최대 두 개의 그룹으로 분류된다.
Collectors.partitioningBy(Predicate)
private static Map<Boolean, List<Dish>> partitionByVegetarian() {
return menu.stream().collect(Collectors.partitioningBy(Dish::isVegetarian));
}
public static void main(String[] args) {
System.out.println(partitionByVegetarian());
// {false=[pork, beef, chicken, prawns, salmon],
// true=[french fries, rice, season fruit, pizza]}
}
private static Map<Boolean, Dish> mostCaloricPartitionedByVegetarian() {
return menu.stream().collect(Collectors.partitioningBy(
Dish::isVegetarian,
Collectors.collectingAndThen(
Collectors.maxBy(Comparator.comparingInt(Dish::getCalories)),
Optional::get
)
));
}
public static void main(String[] args) {
System.out.println(mostCaloricPartitionedByVegetarian());
// {false=pork, true=pizza}
}
그룹화함수와 유사하게 사용가능하다.
Collector
인터페이스를 구현하여 직접 커스텀 컬렉터를 만들 수 있다.
예를 들어, 소수를 찾아 분할하는 커스텀 컬렉터를 만들 수 있다.
public class PrimeNumbersCollector
implements Collector<Integer, Map<Boolean, List<Integer>>, Map<Boolean, List<Integer>>> {
// 수집될 항목, 중간 결과 누적자, 최종 연산 결과
@Override
public Supplier<Map<Boolean, List<Integer>>> supplier() { // 비어있는 컨테이너 만들기
return () -> new HashMap<>() {{
put(true, new ArrayList<Integer>());
put(false, new ArrayList<Integer>());
}};
}
@Override
public BiConsumer<Map<Boolean, List<Integer>>, Integer> accumulator() { // 누적자 컨테이너에 요소 추가하기
return (accumulator, candidate) -> {
List<Integer> primes = accumulator.get(true);
accumulator.get(isPrime(primes, candidate))
.add(candidate);
};
}
@Override
public BinaryOperator<Map<Boolean, List<Integer>>> combiner() { // 누적자 컨테이너들을 병합 (병렬 스트림의 경우 해당)
/* return (map1, map2) -> {
map1.get(true).addAll(map2.get(true));
map1.get(false).addAll(map2.get(false));
return map1;
};
*/ // 알고리즘이 순차적이기 때문에 실제 병렬로 사용할 수 없다.
return null;
}
@Override
public Function<Map<Boolean, List<Integer>>, Map<Boolean, List<Integer>>> finisher() { // 누적자 컨테이너를 최종 결과 컨테이너로 변환
return Function.identity();
}
/**
* 스트림을 병렬로 리듀스할 수 있는지,
* 스트림에 어떤 최적화를 할 수 있는지 힌트를 제공한다.
*
* UNORDERED: 리듀싱 결과는 스트림 요소의 방문 순서나 누적 순서에 영향을 받지 않는다.
* CONCURRENT: 다중 스레드에서 accumulator 함수를 동시에 호출할 수 있으며, 병렬 리듀싱을 수행할 수 있다.
* IDENTITY_FINISH: finisher 메서드가 반환하는 함수는 단순히 identity 적용이므로, 이를 생략할 수 있다.
*/
@Override
public Set<Characteristics> characteristics() {
return Collections.unmodifiableSet(EnumSet.of(Characteristics.IDENTITY_FINISH));
}
private static boolean isPrime(List<Integer> primes, Integer candidate) {
double candidateRoot = Math.sqrt(candidate);
return primes.stream()
.takeWhile(i -> i <= candidateRoot)
.noneMatch(i -> candidate % i == 0);
}
}
소수를 찾아 분할하여 반환하는
PrimeNumbersCollector
커스텀 컬렉터
public static Map<Boolean, List<Integer>> partitionPrimesWithCustomCollector(int n) {
return IntStream.rangeClosed(2, n).boxed()
.collect(new PrimeNumbersCollector());
}
public static void main(String[] args) {
System.out.println(partitionPrimesWithCustomCollector(10));
// {false=[4, 6, 8, 9, 10], true=[2, 3, 5, 7]}
}
.collect(new PrimeNumbersCollector())
와 같이 커스텀 컬렉터 구현체를 넣어준다.
Collectors
클래스 API: https://docs.oracle.com/en/java/javase/19/docs/api/java.base/java/util/stream/Collectors.html
.parallel()
을 통해 순차 스트림을 병렬 스트림으로 변환할 수 있다.
.sequential()
로 병렬 스트림을 순차 스트림으로 변환할 수 있다.여러개의 스레드에서 데이터를 처리할 수 있도록 스트림 요소를 여러 청크로 분할한 스트림이다.
병렬 스트림이 순차 스트림이나 고전적인 반복 형식에 비해 무조건적으로 성능이 좋지는 않다.
public static long iterativeSum(long n) {
long result = 0;
for (long i = 0; i <= n; i++) {
result += i;
}
return result;
}
- 고전적인 반복 방식
- 2960 msecs 소요
public static long sequentialSum(long n) {
return Stream.iterate(1L, i -> i + 1)
.limit(n)
.reduce(0L, (sum, add) -> sum + add);
}
- 순차 스트림 사용
- 79371 msecs 소요
public static long parallelSum(long n) {
return Stream.iterate(1L, i -> i + 1)
.limit(n)
.parallel()
.reduce(0L, (sum, add) -> sum + add);
}
- 병렬 스트림 사용
- 135078 msecs 소요
2가지 문제점에 의해 병렬 버전이 나쁜 성능을 갖게 된다.
반복 결과로 박싱된 객체가 만들어지므로 숫자를 더하려면 언박싱을 해야 한다.
Stream.iterate
같은 반복 작업은 병렬로 수행할 수 있는 독립 단위로 나누기 어렵다.
iterate
는 본질적으로 순차적이다.더욱 특화된 LongStream.rangeClosed
메서드를 사용해 2가지 문제점을 해결한다.
기본형 long
을 직접 사용해 박싱 & 언박싱 오버헤드가 사라진다.
쉽게 청크로 분할할 수 있는 숫자 범위를 생산한다.
public static long rangedSum(long n) {
return LongStream.rangeClosed(1, n)
.reduce(0L, (sum, add) -> sum + add);
}
LongStream.rangeClosed
를 사용한 순차 스트림- 2966 msecs
public static long parallelRangedSum(long n) {
return LongStream.rangeClosed(1, n)
.parallel()
.reduce(0L, (sum, add) -> sum + add);
}
LongStream.rangeClosed
를 사용한 병렬 스트림- 990 msecs
- 순차 실행, 고전적인 반복 방식보다 빠른 병렬 리듀싱을 만들었다.
멀티 코어 간의 데이터 이동 비용은 생각보다 비싸다.
따라서 코어 간 데이터 전송 시간보다 훨씬 오래걸리는 작업만 병렬로 수행해야 한다.
소량의 데이터에서는 비효율적이다.
공유된 상태를 바꾸는 알고리즘은 병렬 스트림 사용 시 제대로 동작하지 않는다.
public class Accumulator {
private long total = 0;
public void add(long value) {
total += value;
}
public long getTotal() {
return total;
}
}
상태를 갖는 누적자 클래스
public static long sideEffectSum(long n) {
Accumulator accumulator = new Accumulator();
LongStream.rangeClosed(1, n)
.forEach(accumulator::add);
return accumulator.getTotal();
}
- 순차 스트림 사용
- 2967 msecs 소요
- 50000005000000 결과 반환
public static long sideEffectParallelSum(long n) {
Accumulator accumulator = new Accumulator();
LongStream.rangeClosed(1, n)
.parallel()
.forEach(accumulator::add);
return accumulator.getTotal();
}
- 병렬 스트림 사용
- 787 msecs 소요
- 17020621348761 결과 반환
- 다수의 스레드에서 동시에 데이터에 접근하는 데이터 레이스 문제가 발생한다.
total += value
는atomic
연산이 아니다.
limit
나 findFirst
처럼 요소의 순서에 의존하는 연산은 순차 스트림보다 병렬 스트림에서 성능이 떨어진다.
findAny()
를 사용하거나
순서가 상관없다면 .unordered()
를 먼저 호출한 뒤 순서에 의존하는 연산을 사용하여 성능을 높일 수 있다.
public static long findFirst(long n) {
return LongStream.rangeClosed(1, n)
.parallel()
.findFirst()
.getAsLong(); // 느리다.
}
public static long findAny(long n) {
return LongStream.rangeClosed(1, n)
.parallel()
.findAny()
.getAsLong(); // 상대적으로 빠르다.
}
public static long findUnorderedFirst(long n) {
return LongStream.rangeClosed(1, n)
.parallel()
.unordered()
.findFirst()
.getAsLong(); // 상대적으로 빠르다.
}
LinkedList
보다 ArrayList
를 효율적으로 분할할 수 있다.
LinkedList
는 분할하기 위해 모든 요소를 탐색해야 한다.
ArrayList
는 모든 요소 탐색없이 분할할 수 있다.
병렬 스트림은 내부적으로 포크/조인 프레임워크를 사용한다.
RecursiveTask
의 추상 메서드 compute()
를 구현하여 병렬 처리를 수행한다.
태스크를 서브 태스크로 분할하려는 로직과
각 서브태스크의 결과를 합치는 로직으로 이루어진다.
public class ForkJoinSumCalculator extends RecursiveTask<Long> {
// RecursiveTask<R> 을 상속하여 스레드 풀을 이용한다.
private static final long THRESHOLD = 10_000;
private final long[] numbers;
private final int start;
private final int end;
public ForkJoinSumCalculator(long[] numbers) {
this(numbers, 0, numbers.length);
}
public ForkJoinSumCalculator(long[] numbers, int start, int end) {
this.numbers = numbers;
this.start = start;
this.end = end;
}
@Override
protected Long compute() {
int length = end - start;
if (length <= THRESHOLD) { // 서브 태스크가 작아지면, 더 이상 쪼개지 않고 순차 실행
return computeSequentially();
}
ForkJoinSumCalculator leftTask = new ForkJoinSumCalculator(numbers, start, start + length / 2);
leftTask.fork(); // ForkJoinPool 의 다른 스레드로 비동기 실행
ForkJoinSumCalculator rightTask = new ForkJoinSumCalculator(numbers, start + length / 2, end);
Long rightResult = rightTask.compute(); // 현재 스레드로 동기 실행
Long leftResult = leftTask.join(); // 왼쪽 서브 태스크의 결과를 읽어온다. (없으면 기다림)
return leftResult + rightResult; // 왼쪽, 오른쪽 서브 태스크의 결과를 합쳐서 반환한다.
}
private long computeSequentially() { // 순차 실행
long sum = 0;
for (int i = start; i < end; i++) {
sum += numbers[i];
}
return sum;
}
}
RecursiveTask<R>
의 서브 클래스인ForkJoinSumCalculator
- 스레드 풀을 사용할 수 있다.
public class ForkJoinSumRunner {
public static void main(String[] args) {
long result = forkJoinSum(1000_0000L);
System.out.println(result);
}
public static long forkJoinSum(long n) {
long[] numbers = LongStream.rangeClosed(1, n).toArray();
ForkJoinTask<Long> task = new ForkJoinSumCalculator(numbers);
return new ForkJoinPool().invoke(task); // 새로운 ForkJoinPool 을 만들고, invoke 메서드로 task를 전달
}
}
ForkJoinSumCalculator
를 실행하는Main
클래스forkJoinSum(long n)
메서드를 통해 1 ~ n 까지의long
배열을 만들고 새로운ForkJoinPool
에task
를 전달한다.
join
메서드는 태스크의 결과를 얻을 때까지 호출자를 블록시킨다.
join
을 호출해야 병렬성을 누릴 수 있다.ForkJoinPool
의 invoke
메서드는 병렬 계산을 시작할 때 한 번만 사용해야 한다.
RecursiveTask
내에서 invoke
메서드를 호출해선 안된다.양쪽 작업 모두 fork
를 호출하는 것보단 한 쪽 작업엔 compute
를 호출하는 것이 효과적이다.
이를 통해 두 서브 태스크 중 한 서브 태스크는 같은 스레드를 재사용할 수 있다.
불필요한 태스크 할당 오버헤드를 제거한다.
포크/조인 프레임워크를 사용하여 병렬 처리하는 것이 순차 처리보다 무조건적으로 빠르지 않다.
가장 효율적인 최소 서브 태스크 크기를 결정하는 것은 경험의 산물이다.
이론상으로는 코어 개수만큼 서브 태스크를 분할하면 불필요한 태스크 할당 오버헤드를 줄이고 효율적일 것 같다.
그러나 실제 현업에서는 같은 크기로 분할된 서버 태스크가 같은 시간에 종료되지 않는다.
따라서 작업이 일찍 끝난 스레드는 놀게되어 병렬성이 떨어진다.
포크/조인 프레임워크는 작업 훔치기를 통해 이러한 문제점을 해결한다.
작업이 모두 끝난 스레드는 작업이 쌓여 있는 스레드의 작업을 훔쳐온다.
이러한 작업 훔치기를 통해 스레드간 작업 부하를 비슷한 수준으로 유지한다.
최소 서브 태스크 크기가 너무 크면 작업 훔치기의 효과를 보지 못한다.
이미 실행하고 있는 작업은 훔칠 수 없기 때문이다.
따라서 최소 서브 태스크 크기를 작게 나누어야 한다.
Iterator
처럼 요소 탐색 기능을 제공한다.
병렬 작업에 특화되어 있다.
public static int countWordsIteratively(String s) {
int counter = 0;
boolean lastSpace = true;
for (char c : s.toCharArray()) {
if (Character.isWhitespace(c)) {
lastSpace = true;
} else {
if (lastSpace) counter++;
lastSpace = Character.isWhitespace(c);
}
}
return counter;
}
private static final String SENTENCE =
" Nel mezzo del cammin di nostra vita "
+ "mi ritrovai in una selva oscura"
+ " che la dritta via era smarrita ";
public static void main(String[] args) {
System.out.println(countWordsIteratively(SENTENCE)); // 19
}
고전적인 방식의 반복
public class WordCounter {
private final int counter;
private final boolean lastSpace;
public WordCounter(int counter, boolean lastSpace) {
this.counter = counter;
this.lastSpace = lastSpace;
}
public WordCounter accumulate(Character c) {
if (Character.isWhitespace(c)) {
return lastSpace ?
this : // 공백이 연속되는 경우
new WordCounter(counter, true); // 단어 다음 공백인 경우, 공백 상태로 변경
} else {
return lastSpace ?
new WordCounter(counter + 1, false) : // 공백 다음 단어가 시작되는 경우, 단어 개수 증가
this; // 단어가 계속 유지되는 경우
}
}
public WordCounter combine(WordCounter wordCounter) {
return new WordCounter(counter + wordCounter.counter, false); // 두 WordCounter의 counter (단어 개수) 를 더한다.
// 합치기만 수행하므로, lastSpace는 아무값이나 상관없다.
}
public int getCounter() {
return counter;
}
}
단어 개수를 세기위한 불변 클래스
public static int countWords(String s) {
Stream<Character> stream = IntStream.range(0, s.length())
.mapToObj(i -> s.charAt(i));
WordCounter wordCounter = stream.reduce(new WordCounter(0, true), // identity(초기값)
WordCounter::accumulate, // accumulator
WordCounter::combine); // combiner
return wordCounter.getCounter();
}
private static final String SENTENCE =
" Nel mezzo del cammin di nostra vita "
+ "mi ritrovai in una selva oscura"
+ " che la dritta via era smarrita ";
public static void main(String[] args) {
System.out.println(countWords(SENTENCE)); // 19
}
- 순차 스트림 사용
.reduce(초기값, accumulator, combiner)
로 리듀싱할 수 있다.
accumulator
는 요소들을 처리하는 방식이다.combiner
는 병렬 처리 시, 각각의 스레드에 나뉘어져 있는 스트림들을 합치는 방식이다.
public static int countWordsParallel(String s) {
Stream<Character> stream = IntStream.range(0, s.length())
.mapToObj(i -> s.charAt(i));
WordCounter wordCounter = stream.parallel()
.reduce(new WordCounter(0, true), // identity(초기값)
WordCounter::accumulate, // accumulator
WordCounter::combine); // combiner
return wordCounter.getCounter();
}
private static final String SENTENCE =
" Nel mezzo del cammin di nostra vita "
+ "mi ritrovai in una selva oscura"
+ " che la dritta via era smarrita ";
public static void main(String[] args) {
System.out.println(countWordsParallel(SENTENCE)); // 25
}
- 병렬 스트림 사용
- 제대로 동작하지 않는다.
- 순차 스트림을 병렬 스트림으로 바꿀때 스트림 분할 위치에 따라 잘못된 결과가 나올 수 있다.
- 하나의 단어를 둘로 계산하는 상황이 발생할 수 있다.
따라서 예제를 병렬로 올바르게 처리하고 싶다면 문자열을 임의의 위치에서 분할하지 말고, 단어가 끝나는 위치에서만 분할해야 한다.
Spliterator
를 만들어야 한다.public class WordCounterSpliterator implements Spliterator<Character> {
private final String string; // 초기에 주어진 전체 문자열
private int currentChar = 0; // 현재 Spliterator 의 문자열 시작 위치
private WordCounterSpliterator(String string) {
this.string = string;
}
@Override
public boolean tryAdvance(Consumer<? super Character> action) { // 순차적으로 요소를 소비하며 탐색할 요소가 남아있으면 true 반환 (Iterator 동작과 동일)
action.accept(string.charAt(currentChar++));
return currentChar < string.length();
}
@Override
public Spliterator<Character> trySplit() { // Spliterator 를 분할하여 두 번째 Spliterator 를 생성한다.
int currentSize = string.length() - currentChar;
if (currentSize < 10) {
return null; // 문자열이 충분히 작으므로, 더 이상 쪼개지 않고 null 반환
}
for (int splitPos = currentSize / 2 + currentChar; splitPos < string.length(); splitPos++) { // splitPos 를 문자열의 중간으로 일단 이동한 뒤,
if (Character.isWhitespace(string.charAt(splitPos))) { // 공백을 만날때까지 한 칸씩 이동
Spliterator<Character> spliterator = new WordCounterSpliterator(string.substring(currentChar, splitPos)); // 현재 Spliterator 의 시작 위치 부터 splitPos 까지를 담당하는 두 번째 Spliterator 생성
currentChar = splitPos; // 현재 Spliterator 의 시작 위치를 분할 위치로 변경
return spliterator;
}
}
return null; // 단어의 길이가 문자열 중간부터 끝까지 인 경우 분할 x
}
@Override
public long estimateSize() { // 탐색해야 할 요소 개수 정보를 제공
return string.length() - currentChar;
}
/**
* Spliterator 특성 집합 제공
*
* ORDERED: 요소에 정해진 순서가 있다.
* DISTINCT: 다른 위치의 두 요소는 항상 같지 않다.
* SORTED: 탐색된 요소는 정의된 정렬 순서를 따른다.
* SIZED: 정확한 크기 값을 반환한다.
* NONNULL: 탐색하는 모든 요소는 null 이 아니다.
* IMMUTABLE: 소스는 불변으로, 요소 탐색 동안 요소를 추가, 삭제, 변경할 수 없다.
* COUCURRENT: 소스를 여러 스레드에서 동시에 고칠 수 있다.
* SUBSIZED: 현재 소스와 분할되는 모든 소스들은 SIZED 특성을 갖는다.
*/
@Override
public int characteristics() {
return ORDERED + SIZED + SUBSIZED + NONNULL + IMMUTABLE;
}
}
커스텀
Spliterator
인WordCounterSpliterator
tryAdvance(Consumer<? super Character> action)
- 현재 인덱스에 해당하는 요소를
Consumber
에 제공한 다음 인덱스 증가- 증가된 인덱스 위치가 소스의 전체길이보다 작으면 참을 반환한다.
- 참이면 반복 탐색할 요소가 남아있음을 의미
trySplit()
- 자료구조를 분할하는 로직으로 가장 중요한 메서드
- 더 이상 분할할 수 없으면
null
을 반환- 분할할 수 있다면 새로운
Spliterator
를 만들어 반환
public static int countWordsParallelUsingSpliterator(String s) {
Spliterator<Character> spliterator = new WordCounterSpliterator(s);
Stream<Character> stream = StreamSupport.stream(spliterator, true);
WordCounter wordCounter = stream.reduce(new WordCounter(0, true),
WordCounter::accumulate,
WordCounter::combine);
return wordCounter.getCounter();
}
private static final String SENTENCE =
" Nel mezzo del cammin di nostra vita "
+ "mi ritrovai in una selva oscura"
+ " che la dritta via era smarrita ";
public static void main(String[] args) {
System.out.println(countWordsParallelUsingSpliterator(SENTENCE)); // 19
}
StreamSupport.stream(spliterator, 병렬처리 여부)
- 커스텀
Spliterator
로 자료구조를 분할하고 병렬 스트림을 만든다.
작은 컬렉션 객체를 쉽게 만들 수 있는 방법을 제공한다.
리스트 팩토리
List.of(요소1, 요소2, ...)
를 통해 불변리스트 생성
요소를 추가하거나 제거할 수 없다.
.set(인덱스, 변경내용)
으로 요소를 수정할 수 없다.
그러나 요소가 참조인 경우, 참조가 가리키는 데이터의 변경은 막을 수 없다.
of
메서드는 인수를 1개부터 10개까지 받을 수 있도록 10개의 오버로드 버전이 존재한다.
인수가 10개를 초과하면 of(E... elements)
메서드를 사용해 가변인수로 받는다.
집합 팩토리
Set.of(요소1, 요소2, ...)
를 통해 불변 집합 생성
중복된 요소로 집합을 만드려고 하면 예외 발생
이외의 성질은 리스트 팩토리와 동일하다.
맵 팩토리
Map.of(key1, value1, key2, value2, ...)
를 통해 불변 맵 생성
중복된 key
로 맵을 만드려고 하면 예외 발생
이외의 성질은 리스트 팩토리와 동일하다.
List
인터페이스에서 사용할 수 있다.
새로운 결과를 만드는 스트림과 다르게 기존 컬렉션을 변경한다.
.removeIf(Predicate)
Predicate
를 만족하는 요소를 전부 제거한다. ArrayList<String> example = new ArrayList<>(List.of("hyun", "yoon", "yeon", "hyun"));
for (String each : example) { // 예외 발생
if (each.equals("hyun")) {
example.remove("hyun");
}
}
System.out.println(example);
ConcurrentModificationException
이 발생한다.
ArrayList<String> example = new ArrayList<>(List.of("hyun", "yoon", "yeon", "hyun"));
for (Iterator<String> iterator = example.iterator(); iterator.hasNext();) {
String each = iterator.next();
if (each.equals("hyun")) {
example.remove("hyun");
}
}
System.out.println(example);
- 위에서 사용한
for-each
루프는Iterator
객체를 사용하므로 내부적으로는 위와 같은 코드이다.
- 두 개의 기본 객체가 컬렉션을 관리한다.
Iterator
객체를 통해 소스 탐색Collection
객체를 통해 요소 제거
- 따라서
Iterator
와Collection
의 상태가 서로 동기화되지 않아 예외 발생
ArrayList<String> example = new ArrayList<>(List.of("hyun", "yoon", "yeon", "hyun"));
for (Iterator<String> iterator = example.iterator(); iterator.hasNext(); ) {
String each = iterator.next();
if (each.equals("hyun")) {
iterator.remove();
}
}
System.out.println(example); // [yoon, yeon]
Iterator
객체의remove()
를 호출함으로서 문제를 해결할 수 있다.- 그러나 코드 가독성이 떨어진다.
ArrayList<String> example = new ArrayList<>(List.of("hyun", "yoon", "yeon", "hyun"));
example.removeIf(each -> each.equals("hyun"));
System.out.println(example); // [yoon, yeon]
removeIf
를 사용하여 가독성있게 문제를 처리할 수 있다.
.replaceAll(UnaryOperator)
UnaryOperator
인수로 전달하여 얻은 결과물이 새로운 요소가 된다. ArrayList<String> example = new ArrayList<>(List.of("hyun", "yoon", "yeon", "hyun"));
example.replaceAll(each -> each + "2");
System.out.println(example); // [hyun2, yoon2, yeon2, hyun2]
.sort(Comparator 구현체)
Comparator
구현에 맞게 리스트를 정렬한다. ArrayList<String> example = new ArrayList<>(List.of("hyun", "yoon", "yeon", "hyun"));
example.sort((s1, s2) -> s1.compareTo(s2));
System.out.println(example); // [hyun, hyun, yeon, yoon]
알파벳순으로 정렬하는
Comparator
를 전달하였다.
Map
인터페이스에서 사용할 수 있다.
.forEach(BiConsumer)
BiConsumer
인수로 전달하여 반복 처리한다. Map<String, Integer> example = Map.of("hyun", 27, "yoon", 26, "yeon", 26);
example.forEach((k, v) -> System.out.println(k + " is " + v + " years old")); // yeon is 26 years old
// yoon is 26 years old
// hyun is 27 years old
Map
요소 정렬
Map
을 Set<Entry>
로 변환한 뒤 정렬할 수 있다.
Map.Entry.ComparingByKey()
Set<Entry>
를 key
값을 기준으로 정렬Map.Entry.ComparingByValue()
Set<Entry>
를 value
값을 기준으로 정렬 Map<String, Integer> example = Map.of("hyun", 27, "yoon", 26, "yeon", 25);
example.entrySet() // Map<..> -> Set<Entry<..>>
.stream() // Set<Entry<..>> -> Stream<Entry<..>>
.sorted(Map.Entry.comparingByKey())
.forEach(System.out::println); // hyun=27
// yeon=25
// yoon=26
example.entrySet() // Map<..> -> Set<Entry<..>>
.stream() // Set<Entry<..>> -> Stream<Entry<..>>
.sorted(Map.Entry.comparingByValue())
.forEach(System.out::println); // yeon=25
// yoon=26
// hyun=27
.entrySet()
을 통해Map
을Set<Entry>
로 변환한다.
.getOrDefault(key, 기본값)
key
가 존재하지 않으면 null
대신 기본값을 반환한다. Map<String, Integer> example = Map.of("hyun", 27, "yoon", 26, "yeon", 25);
Integer hyunAge = example.getOrDefault("hyun", -1);
System.out.println(hyunAge); // 27
Integer ayaanAge = example.getOrDefault("ayaan", -1);
System.out.println(ayaanAge); // -1
.computeIfAbsent(key, Function)
만약 key
에 해당하는 value
가 있으면 value
를 반환한다.
Function
실행 X만약 key
에 해당하는 value
가 없거나 null
이면 key
를 Function
에 인수로 전달한다.
Function
실행의 결과물이 입력으로 넣은 key
의 value
가 된다.
key
와 value
를 Map
에 저장한다.
앞서 구한 value
를 반환한다.
Map<String, Integer> example = new HashMap<>() {{
put("hyun", 27);
put("yoon", 26);
put("yeon", 25);
put("ayaan", null);
}}; // {yeon=25, ayaan=null, hyun=27, yoon=26}
// key 에 대응되는 value 가 존재하는 경우
Integer hyunAge = example.computeIfAbsent("hyun", (key) -> {
System.out.println(key + "가 없으므로 계산중!");
return key.length() * 10;
});
System.out.println(hyunAge); // 27
System.out.println(example); // {yeon=25, ayaan=null, hyun=27, yoon=26}
// key 가 존재하지 않는 경우 (value 가 존재하지 않는 경우)
Integer sullyAge = example.computeIfAbsent("sully", (key) -> {
System.out.println(key + "가 없으므로 계산중!");
return key.length() * 10;
}); // sully가 없으므로 계산중! 출력
System.out.println(sullyAge); // 50
System.out.println(example); // {yeon=25, ayaan=null, hyun=27, sully=50, yoon=26}
// key 가 존재하나 대응되는 value 가 null 인 경우
Integer ayaanAge = example.computeIfAbsent("ayaan", (key) -> {
System.out.println(key + "가 없으므로 계산중!");
return key.length() * 10;
}); // ayaan가 없으므로 계산중! 출력
System.out.println(ayaanAge); // 50
System.out.println(example); // {yeon=25, ayaan=50, hyun=27, sully=50, yoon=26}
.remove(key, value)
key
에 대응되는 value
가 일치하는 경우에만 Map
에서 요소를 삭제한다.
key
가 존재하더라도 value
가 다르면 삭제하지 않는다.
Map<String, Integer> example = new HashMap<>(Map.of("hyun", 27, "yoon", 26, "yeon", 25));
example.remove("hyun"); // key가 존재하면 바로 삭제
System.out.println(example); // {yeon=25, yoon=26}
example.remove("yoon", 25); // key에 대응되는 value 가 일치하지 않는다.
System.out.println(example); // {yeon=25, yoon=26}
example.remove("yoon", 26); // key에 대응되는 value 가 일치한다.
System.out.println(example); // {yeon=25}
.replaceAll(BiFunction)
key
와 value
를 인수로 받는 BiFunction
을 파라미터로 갖는다.
모든 value
가 BiFunction
의 결과물로 변경된다.
Map<String, Integer> example = new HashMap<>(Map.of("hyun", 27, "yoon", 26, "yeon", 25));
example.replaceAll((key, value) -> key.length() * 100 + value);
System.out.println(example); // {hyun=427, yeon=425, yoon=426}
.replace(key, 변경할 value)
Map
에 key
가 존재하면 value
를 바꾼다.
변경 성공 시, 변경 전의 값을 반환한다.
.replace(key, 이전 value, 변경할 value)
Map
에 key
가 존재하고 이전 value
도 일치하면 value
를 바꾼다. Map<String, Integer> example = new HashMap<>(Map.of("hyun", 27, "yoon", 26, "yeon", 25));
Integer hyunAge = example.replace("hyun", 26);
System.out.println(hyunAge); // 27 (변경 전의 값을 가져온다.)
System.out.println(example); // {hyun=26, yeon=25, yoon=26}
boolean changed = example.replace("yoon", 10, 100);
System.out.println(changed); // false
System.out.println(example); // {hyun=26, yeon=25, yoon=26}
boolean changed2 = example.replace("yoon", 26, 100);
System.out.println(changed2); // true
System.out.println(example); // {yeon=25, hyun=26, yoon=100}
.entrySet().removeIf(Predicate)
List
와 동일하게 동작한다.
Predicate
를 만족하는 Map
의 요소들을 제거한다.
Set<Entry>
의 변경사항은 Map
에 반영되고, 반대의 경우도 성립한다.하나의 추상 메서드를 구현하는 익명 클래스는 대부분의 경우 람다 표현식으로 리팩터링 할 수 있다.
그러나 익명 클래스와 람다 표현식의 다음 3가지 차이점을 주의해야 한다.
익명 클래스의 this
는 익명 클래스 자신을 가리키지만, 람다에서의 this
는 람다를 감싸는 클래스를 가리킨다.
익명 클래스는 자신을 감싼 클래스의 변수를 가릴 수 있지만 (shadow variable) 람다는 가릴 수 없다.
익명 클래스는 인스턴스화 할 때 명시적으로 형식이 정해지는 반면, 람다의 형식은 콘텍스트에 의존한다.
@FunctionalInterface
public interface Mock {
void mocking();
}
- 함수형 인터페이스인
Mock
mocking()
은() -> ()
함수 디스크립터를 갖는다.
public class Container {
public void start() {
int a = 10;
foo(() -> System.out.println("ho")); // 콘텍스트 오버로딩에 의한 모호함 -> 컴파일 에러 발생
foo(new Mock() {
@Override
public void mocking() {
System.out.println("ho"); // 실행 잘됨
}
});
}
public void foo(Mock mock) {
mock.mocking();
}
public void foo(Runnable r) {
r.run();
}
}
- 람다는 콘텍스트 오버로딩에 의한 모호함 때문에 컴파일 에러가 발생한다.
- 명시적 형변환을 통해 모호함을 제거해야 한다.
foo((Mock) () -> System.out.println("ho"));
- 익명 클래스는 클래스를 명시하기 때문에 모호함 없이 잘 동작한다.
조건부 연기 실행
// 클라이언트
logger.log(Level.FINER, () -> "문제 발생: " + generateErrorReport()); // 로그 레벨과 메시지 생성 동작을 전달
// Logger 내부 동작
public void log(Level level, Supplier<String> msgSupplier) {
if (logger.isLoggable(level)) { // 만약 log의 level 이 적절한 경우
logger.log(level, msgSupplier.get()); // 연기된 메시지 생성 동작을 수행하여 메시지 전달
}
}
public void log(Level level, String message) {
logPrint(level, message); // 레벨과 메시지를 받아 로그 출력
}
- 로그 메시지의 레벨이 적절한 경우에만 메시지 생성 동작을 수행한다.
- 로그 메시지의 레벨이 적절하지 않은 경우, 아무런 동작도 하지 않는다.
실행 어라운드
자주 변하는 핵심 동작이 변하지 않는 준비, 종료과정 사이에 있다면
자주 변하는 핵심 동작을 람다로 변환한다.
public class ExecuteAround {
private static final String FILE = ExecuteAround.class.getResource("./data.txt").getFile();
public static void main(String[] args) throws IOException {
String oneLine = processFile((BufferedReader b) -> b.readLine()); // 한 줄만 읽는 동작 전달
System.out.println(oneLine);
System.out.println();
String twoLines = processFile((BufferedReader b) -> b.readLine() + b.readLine()); // 두 줄을 읽는 동작 전달
System.out.println(twoLines);
}
public static String processFile(BufferedReaderProcessor p) throws IOException {
try (BufferedReader br = new BufferedReader(new FileReader(FILE))) { // 자원 준비, 정리과정
return p.process(br); // 핵심 로직 동작
}
}
}
BufferedReaderProcessor p
가 자주 변하는 핵심 동작이다.
- 전략 ➔ 인터페이스
ConcreteStrategyA
,ConcreteStrategyB
➔ 알고리즘 (전략 인터페이스 구현체)- 런타임에 적절한 알고리즘을 선택하는 기법
public interface ValidationStrategy {
boolean execute(String s);
}
전략 인터페이스
public class IsAllLowerCase implements ValidationStrategy {
@Override
public boolean execute(String s) {
return s.matches("[a-z]+");
}
}
public class IsNumeric implements ValidationStrategy {
@Override
public boolean execute(String s) {
return s.matches("\\d+");
}
}
전략 인터페이스를 구현한 알고리즘
public class Validator {
private final ValidationStrategy strategy;
public Validator(ValidationStrategy strategy) {
this.strategy = strategy;
}
public boolean validate(String s) {
return strategy.execute(s);
}
}
전략 객체를 사용하는 클라이언트
public class StrategyMain {
public static void main(String[] args) {
Validator numericValidator = new Validator(new IsNumeric());
System.out.println(numericValidator.validate("aaaa")); // false
Validator lowerCaseValidator = new Validator(new IsAllLowerCase());
System.out.println(lowerCaseValidator.validate("aaaa")); // true
}
}
실행 결과
ValidationStrategy
는 함수형 인터페이스며 Predicate(String)
과 동일한 함수 디스크립터를 갖는다.
(String s) -> boolean
따라서 전략을 구현하는 클래스를 만들 필요없이 람다 표현식을 전달하여 간결하게 표현할 수 있다.
public class StrategyMain {
public static void main(String[] args) {
Validator numericValidator2 = new Validator((s) -> s.matches("\\d+"));
System.out.println(numericValidator2.validate("aaaa")); // false
Validator lowerCaseValidator2 = new Validator((s) -> s.matches("[a-z]+"));
System.out.println(lowerCaseValidator2.validate("aaaa")); // true
}
}
public abstract class OnlineBanking {
public void processCustomer(int id) { // 알고리즘 템플릿
Customer c = Database.getCustomerWithId(id);
makeCustomerHappy(c); // 유연하게 고치고 싶은 부분
}
protected abstract void makeCustomerHappy(Customer c); // 유연하게 고치고 싶은 부분
}
processCustomer(int id)
➔ 알고리즘 템플릿makeCustomerHappy(c)
➔ 유연하게 고치고 싶은 부분
public class HyunOnlineBanking extends OnlineBanking {
@Override
protected void makeCustomerHappy(Customer c) {
System.out.println(c.getName() + "님, 현 은행에 오신걸 환영합니다");
}
}
public class YoonOnlineBanking extends OnlineBanking {
@Override
protected void makeCustomerHappy(Customer c) {
System.out.println(c.getName() + "님, 윤 은행에 오신걸 환영합니다");
}
}
각 은행 지점마다
OnlineBanking
클래스를 상속받아makeCustomerHappy(Customer c)
메서드가 원하는 동작을 수행하도록 구현한다.
public static void main(String[] args) {
HyunOnlineBanking hyunOnlineBanking = new HyunOnlineBanking();
hyunOnlineBanking.processCustomer(1); // yeon님, 현 은행에 오신걸 환영합니다
YoonOnlineBanking yoonOnlineBanking = new YoonOnlineBanking();
yoonOnlineBanking.processCustomer(1); // yeon님, 윤 은행에 오신걸 환영합니다
}
public class OnlineBankingLambda {
public void processCustomer(int id, Consumer<Customer> makeCustomerHappy) { // 알고리즘 템플릿
Customer c = Database.getCustomerWithId(id);
makeCustomerHappy.accept(c); // 유연하게 고치고 싶은 부분
}
}
processCustomer(int id, Consumer<Customer> makeCustomerHappy)
➔ 알고리즘 템플릿Consumer<Customer> makeCustomerHappy
파라미터
- 유연하게 고치고 싶은 부분을 람다로 받아 원하는 시점에 실행한다.
public class OnlineBankingMain {
public static void main(String[] args) {
OnlineBankingLambda onlineBanking1 = new OnlineBankingLambda();
onlineBanking1.processCustomer(1, (c) -> System.out.println(c.getName() + "님, 제1은행에 오신걸 환영합니다."));
// yeon님, 제1은행에 오신걸 환영합니다.
OnlineBankingLambda onlineBanking2 = new OnlineBankingLambda();
onlineBanking2.processCustomer(1, (c) -> System.out.println(c.getName() + "님, 제2은행에 오신걸 환영합니다."));
// yeon님, 제2은행에 오신걸 환영합니다.
}
}
템플릿 클래스를 상속 받지 않고 직접 람다 표현식을 전달하여 다양한 동작 추가
한 객체 (Subject
) 가
다른 객체들 (Observer
) 에게 자동으로 알림을 보내야 하는 상황에 사용하는 기법
public interface Observer {
void notify(String tweet);
}
public class NYTimes implements Observer {
@Override
public void notify(String tweet) {
if (tweet != null && tweet.contains("money")) {
System.out.println("Breaking news in NY! " + tweet);
}
}
}
public class Guardian implements Observer {
@Override
public void notify(String tweet) {
if (tweet != null && tweet.contains("queen")) {
System.out.println("Yet more new from London..." + tweet);
}
}
}
public class LeMonde implements Observer {
@Override
public void notify(String tweet) {
if (tweet != null && tweet.contains("wine")) {
System.out.println("Today cheese, wine and news! " + tweet);
}
}
}
Observer
인터페이스와 구현체들tweet
의 특정 단어에 반응한다.
public interface Subject {
void registerObserver(Observer o);
void notifyObservers(String tweet);
}
public class Feed implements Subject {
private final List<Observer> observers = new ArrayList<>();
@Override
public void registerObserver(Observer o) {
this.observers.add(o);
}
@Override
public void notifyObservers(String tweet) {
observers.forEach(o -> o.notify(tweet));
}
}
Subject
인터페이스와 구현체Observer
들을 등록, 관리한다.Observer
들에게 특정 이벤트 (tweet
) 가 발생했음을 알린다.
public class ObserverMain {
public static void main(String[] args) {
Feed feed = new Feed();
feed.registerObserver(new NYTimes());
feed.registerObserver(new Guardian());
feed.registerObserver(new LeMonde());
feed.notifyObservers("The queen said her favourite book is Java 8 & 9 in Action!"); // Guardian Observer 가 반응
feed.notifyObservers("Give me money!!!"); // NYTimes Observer 가 반응
feed.notifyObservers("boola boola foo"); // 아무도 반응 x
feed.notifyObservers("The queen likes wine~"); // LeMonde Observer 와 Guardian Observer 가 반응
}
}
실행 결과
Yet more new from London...The queen said her favourite book is Java 8 & 9 in Action! Breaking news in NY! Give me money!!! Yet more new from London...The queen likes wine~ Today cheese, wine and news! The queen likes wine~
Observer
인터페이스는 하나의 메서드 notify
만을 가진다.
함수형 인터페이스이다.
구현 클래스를 만들지 않고 람다 표현식을 전달해 Observer
가 실행할 동작을 지정할 수 있다.
Observer
가 상태를 가지거나, 여러 메서드를 정의하거나, 복잡한 내부로직을 갖는다면 기존 클래스 구현방식이 낫다.
public class ObserverMain {
public static void main(String[] args) {
Feed feedLambda = new Feed();
feedLambda.registerObserver(tweet -> {
if (tweet != null && tweet.contains("money")) {
System.out.println("Breaking news in NY! " + tweet);
}
});
feedLambda.registerObserver(tweet -> {
if (tweet != null & tweet.contains("queen")) {
System.out.println("Yet more new from London..." + tweet);
}
});
feedLambda.registerObserver(tweet -> {
if (tweet != null && tweet.contains("wine")) {
System.out.println("Today cheese, wine and news! " + tweet);
}
});
feedLambda.notifyObservers("The queen said her favourite book is Java 8 & 9 in Action!");
feedLambda.notifyObservers("Give me money!!!");
feedLambda.notifyObservers("boola boola foo");
feedLambda.notifyObservers("The queen likes wine~");
}
}
실행결과
Yet more new from London...The queen said her favourite book is Java 8 & 9 in Action! Breaking news in NY! Give me money!!! Yet more new from London...The queen likes wine~ Today cheese, wine and news! The queen likes wine~
- 앞선 실행결과와 동일하다.
A 객체가 어떤 작업을 처리한 다음 B 객체에게 결과를 전달하고
B 객체가 어떤 작업을 처리한 다음 C 객체에게 결과를 전달하는 방식으로
여러 작업 처리 객체의 동작 체인을 만들 때 사용하는 기법
public abstract class ProcessingObject<T> {
protected ProcessingObject<T> successor; // 후임자
public void setSuccessor(ProcessingObject<T> successor) {
this.successor = successor;
}
public T handle(T input) {
T r = handleWork(input);
if (successor != null) {
return successor.handle(r); // 후임자가 있으면 현재 객체가 처리한 작업 결과를 후임자 객체에게 넘긴다.
}
return r; // 후임자가 없으면 현재 객체가 처리한 작업 결과를 반환한다.
}
abstract protected T handleWork(T input); // 실제 작업 처리
}
- 작업 처리 추상 클래스로 Chain of Responsibility 패턴을 구성한다.
T
는 작업 처리 결과물의 타입
handle(T input)
- 작업 처리 객체가 자신의 작업을 끝내면 다음 작업 처리 객체 (
successor
) 로 결과 전달- 다음 작업 처리 객체가 없으면 자신의 작업 결과물을 반환
handleWork(T input)
- 실제 작업 처리 내용
- 해당 메서드를 구현하여 다양한 작업 처리 객체를 만든다.
public class HeaderTextProcessing extends ProcessingObject<String> {
@Override
protected String handleWork(String input) {
return "From Hyun: " + input;
}
}
public class SpellCheckerProcessing extends ProcessingObject<String> {
@Override
protected String handleWork(String input) {
return input.replaceAll("labda", "lambda");
}
}
HeaderTextProcessing
input
텍스트에 Header 를 부착하는 작업 수행SpellCheckerProcessing
input
텍스트의labda
(오타) 를lambda
로 전부 고치는 작업 수행
람다 표현식으로 작업들을 정의하고,
람다 표현식을 조합하여 Chain of Responsibility 패턴을 구현할 수 있다.
andThen
메서드 사용public class ChainOfResponsibilityMain {
public static void main(String[] args) {
UnaryOperator<String> headerProcessing = text -> "From Hyun: " + text;
UnaryOperator<String> spellCheckerProcessing = text -> text.replaceAll("labda", "lambda");
Function<String, String> pipeline = headerProcessing.andThen(spellCheckerProcessing);
String result = pipeline.apply("Aren't labdas really sexy?!");
System.out.println(result); // From Hyun: Aren't lambdas really sexy?!
}
}
스트림의 파이프라인 연산을 디버깅하기 위해 사용
forEach
는 최종 연산이므로 파이프라인 중간에 디버깅 용도로 사용할 수 없다.
peek
는 forEach
와 유사하게 동작하지만 요소를 소비하지 않는다.
public class Peek {
public static void main(String[] args) {
List<Integer> result = Stream.of(1, 2, 3, 4, 5, 6, 7, 8)
.peek(i -> System.out.println("origin stream: " + i)) // 소스의 처음 요소 출력
.map(i -> i + 20)
.peek(i -> System.out.println("after map: " + i)) // map 동작 후 요소 출력
.filter(i -> i % 2 == 0)
.peek(i -> System.out.println("after filter: " + i)) // filter 동작 후 요소 출력
.limit(3)
.peek(i -> System.out.println("after limit: " + i)) // limit 동작 후 요소 출력
.collect(Collectors.toList());
System.out.println(result); // [22, 24, 26]
}
}
실행결과 분석
origin stream: 1 after map: 21 (필터 통과 못함) origin stream: 2 after map: 22 after filter: 22 after limit: 22 (전부 통과) origin stream: 3 after map: 23 (필터 통과 못함) origin stream: 4 after map: 24 after filter: 24 after limit: 24 (전부 통과) origin stream: 5 after map: 25 (필터 통과 못함) origin stream: 6 after map: 26 after filter: 26 after limit: 26 (전부 통과) (limit 3을 채웠으므로 연산 종료) [22, 24, 26]
- 이를 통해 스트림은 요소를 하나씩 하나씩 전체 파이프라인을 통과시키며 처리함을 알 수 있다.
출처
모던 자바 인 액션 (라울-게이브리얼 우르마, 마리오 푸스코, 앨런 마이크로프트)
람다의 자유 변수 사용 (cksdnr066 님)
https://velog.io/@cksdnr066/Java-%EC%99%9C-%EB%9E%8C%EB%8B%A4%EA%B0%80-%EC%82%AC%EC%9A%A9%ED%95%98%EB%8A%94-%EC%A7%80%EC%97%AD%EB%B3%80%EC%88%98%EB%8A%94-Effectively-Final-%EC%9D%B4%EC%96%B4%EC%95%BC-%ED%95%98%EB%8A%94%EA%B0%80
모던 개발자 현..,.,, 탄생 ✨