ch7 람다와스트림 - LenKIM/everyone-is-effective-java-study GitHub Wiki
데이터 원소의 시퀀스 처리를 라이브러리 차원에서 지원하기 시작.
하나의 함수를 가진 인터페이스의 인스턴스를 함수객체.
왜? 익명 클래스보다 람다를 써야 하나?
함수객체를 만드는 주요수단으로 익명클래스를 만드는데 익명클래스는 무엇인가?
Collections.sort(words, new Comparator<String>(){
public int compare(String s1, String s2){
return Integer.compare(s1.length(), s2.length());
}
})
Comparator
인터페이스가 장렬을 담당하는 추상 전략을 뜻하는데, 문자열을 정렬하는 구체적인 전략을 익명 클래스로 구현. 위 코드의 단점은 코드가 너무 길기 때문에 적합하지 않다
라는 것.
람다를 활용함으로써 간결해지는 이점을 얻는다.
Collections.sort(words, (s1, s2) -> Integer.compare(s1.length(), s2.length()))
다시 생각해보면, Comparator
노출되지 않는데, 문제가 없는걸까? 가독성이 떨어지는것은 아닐까?
그렇다. 람다는 이름이 없고 문서화도 못 한다. 따라서 코드 자체로 동작이 명확히 설명되지 않거나 코드 줄 수가 많아지면 람다를 쓰지 말아야 한다. 람다를 사용하는건 최대 3줄까지.
이런 문제가 타입추론에 속하는 문제이고, 타입을 명시해야 코드가 더 명확할 때만 제외하고는, 람다의 모든 매개변수 타입은 생략되어야 한다.
public enum Operation {
PLUS("+") {
public double apply(double x, double y) { return x + y; }
},
MINUS("-") {
public double apply(double x, double y) { return x - y; }
},
TIMES("*") {
public double apply(double x, double y) { return x * y; }
},
DIVIDE("/") {
public double apply(double x, double y) { return x / y; }
};
private final String symbol;
Operation(String symbol) { this.symbol = symbol; }
@Override public String toString() { return symbol; }
public abstract double apply(double x, double y);
// 코드 34-7 열거 타입용 fromString 메서드 구현하기 (216쪽)
private static final Map<String, Operation> stringToEnum =
Stream.of(values()).collect(
toMap(Object::toString, e -> e));
// 지정한 문자열에 해당하는 Operation을 (존재한다면) 반환한다.
public static Optional<Operation> fromString(String symbol) {
return Optional.ofNullable(stringToEnum.get(symbol));
}
}
public enum Operation {
PLUS ("+", (x, y) -> x + y),
MINUS ("-", (x, y) -> x - y),
TIMES ("*", (x, y) -> x * y),
DIVIDE("/", (x, y) -> x / y);
private final String symbol;
private final DoubleBinaryOperator op;
Operation(String symbol, DoubleBinaryOperator op) {
this.symbol = symbol;
this.op = op;
}
@Override public String toString() { return symbol; }
public double apply(double x, double y) {
return op.applyAsDouble(x, y);
}
// 아이템 34의 메인 메서드 (215쪽)
public static void main(String[] args) {
double x = Double.parseDouble(args[0]);
double y = Double.parseDouble(args[1]);
for (Operation op : Operation.values())
System.out.printf("%f %s %f = %f%n",
x, op, y, op.apply(x, y));
}
}
마지막으로, 람다는 자신을 참조할 수 없다.
람다에서의 this 키워드는 바깥 인스턴스를 가리킨다. 반면 익명 클래스에서의 this는 익명 클래스의 인스턴스 자신을 가리킨다. 그래서 함수 자체가 자신을 참조해야 한다면 반드시 익명 클래스를 써야 한다.
람다도 익명 클래스처럼 직렬화 형태가 구현별로 다를 수 있다. 따라서 람다를 직렬화하는 일은 극히 삼가야 한다. 직렬화해야만 하는 함수 객체가 있다면(가령 Comparator) private 정적 중첩 클래스의 인스턴스를 사용하자.
tip
익명 클래스는(함수형 인터페이스가 아닌) 타입의 인스턴스를 만들 때만 사용하라. 람다는 작은 함수 객체를 아주 쉽게 표현할 수 있어(이전 자바에서는 실용적이지 않던)함수형 프로그래밍의 지평을 열었다.
메서드 참조가 무엇인가?
자바에서 함수 객체를 심지어 람다보다도 더 간결하게 만드는 방법이 있는데, 그것이 바로 메서드 참조.
map.merge(key, 1, (count,incr) -> count + incr);
위 코드가
map.merge(key,1,Integer::sum)
차이점을 살펴보면, count,incr
이 두 개가 단지 합한다는 내용만 있다. 이를 Integer::sum
으로 만든다.
sum이라는 이름이 좀 더 명확하게 다가오지만, count, incr 라는 변수가 없어지므로써 읽기 쉬움이 없어질 수도 있다.
보통 메서드 참조가 이득이지만, 항상 그런 것은 아니다. 때론 람다가 메서드 참조보다 간결할 때가 있다. 주로 메서드와 람다가 같은 클래스에 있을 때 그렇다.
service.execute(GoshThisClassNameIsHumongous::action);
이를 람다로 대체하면
service.execute(() -> action());
무엇이 더 나아보이는가?
메서드 참조할 수 있는 유형은 다섯가지가 있다.
메서드 참조 유형 | 예 | 같은 기능을 하는 람다 |
---|---|---|
정적 | Integer::parseInt | str -> Integer.parseInt(str) |
한정적(인스턴스) | Instant.now()::isAfter | Instant then = Instant.now(); t -> then.isAfter(t) |
비한정적(인스턴스) | String::toLowerCase | str -> str.toLowerCase() |
클래스 생성자 | TreeMap<K,V>::new | () -> new TreeMap<K,V>() |
배열 생성자 | int[]::new | len -> new int[len] |
메서드 참조 쪽이 짫고 명확하다면 메서드 참조를 쓰고, 그렇지 않을 때만 람다를 사용하라.
LinkedHashMap
에서 protected 메서드인 removeEldestEntry
재정의하면 캐시로 사용할 수 있다.
protected boolean removeEldestEntry(Map.Entry<K,V> eldest){
return size() > 100;
}
만약 함수형 인터페이스를 만들어서 해결한다면?
@FunctionalInterface interface EldestEntryRemovalFunction<k,V>{
boolean remove(Map<K,V> map, Map.Entry<k,V> eldest);
}
자바표준 라이브러리에 이미 같은 모양의 인터페이스가 있다. java.util.function
패키지.
EldestEntryRemovalFunction
이거 대신, BiPredicate<Map<K,V>, Map.Entry<K,V>>
/**
* Represents a function that accepts one argument and produces a result.
*
* <p>This is a <a href="package-summary.html">functional interface</a>
* whose functional method is {@link #apply(Object)}.
*
* @param <T> the type of the input to the function
* @param <R> the type of the result of the function
*
* @since 1.8
*/
@FunctionalInterface
public interface Function<T, R> {
}
java.util.function
에는 총 43개의 인터페이스.
기본 인터페이스 6개만 기억하면 나머지는 충분히 유추해낼 수 있다. 이 기본 인터페이스들은 모두 참조 타입용.
인터페이스 | 함수 시그니처 | 예 |
---|---|---|
UnaryOperator | T apply(T t) | String::toLowerCase |
BinaryOperator | T apply(T t1, T t2) | BigInteger:add |
Predicate | boolean test(T t) | Collection::isEmpty |
Function<T,R> | R apply(T t) | Arrays::asList |
Supplier | T get() | Instant::now |
Consumer | void accept(T t) | System.out::println |
tip) 각각의 인터페이스의 활용을 DO에서 예시로 찾아보기
@FunctionalInterface 사용해야 하는 이유?
- 해당 클래스의 코드나 설명 문서를 읽을 이에게 그 인터페이스가 람다용으로 설계된 것임을 알려줌.
- 해당 인터페이스가 추상 메서드를 오직 하나만 가지고 있어야 컴파일되게 해준다.
- 그 결과 유지보수 과정에서 누군가 실수로 메서드를 추가하지 못하게 막아준다.
@FunctionalInterface 를 사용할 때 주의할점으로, 서로 다른 함수형 인터페이스를 같은 위치의 인수로 받는 메서드들을 다중정의해서는 안 된다. 클라이언트에게 불필요한 모호함만 안겨줄 뿐이며, 이 모호함으로 인해 실제로 문제가 된다.
다량의 데이터 처리 작업(순차적이든 병렬적이든)을 돕고자 자바 8에 추가됨.
이 API가 제공하는 추상 개념 중 핵심은 두 가지.
- 스트림(Stream)은 데이터 원소의 유한 혹은 무한 시퀀스(sequence)를 뜻함.
- 스트림 파이프라인은 이원소들로 수행하는 연산 단계를 표현하는 개념
대표적으로 컬렉션, 배열, 파일, 정규표현식 패턴 매처(matcher), 난수 생성기, 혹은 스트림.
int, long, double 이렇게 세 가지 지원
스트림 파이프라인은 지연 평가(lazy evalutaion)
. 평가는 종단 연산이 호출될 때 이뤄지며, 종단 연산에 쓰이지 않는 데이터 원소는 계산에 쓰이지 않는다.
종단 연산?
스트림 AP는 메서드 연쇄를 지원하는 플로언트 API(fluent API) 즉, 파이프라인 하나를 구성하는 모든 호출을 연결하여 단 하나의 표현식으로 완성할 수 있다. 파이프라인 여러 개를 연결해 표현식 하나로 만들 수 있다.
아래 프로그램은 사전 파일에서 단어를 읽고 사용자가 지정한 문턱값보다 원소 수가 많은 아나그램(anagram)그룹을 출력. 아나그램이란 철자를 구성하는 알파벳이 같고 순서만 다른 단어를 말한다.
import java.io.File;
import java.io.IOException;
import java.util.*;
// 코드 45-1 사전 하나를 훑어 원소 수가 많은 아나그램 그룹들을 출력한다. (269-270쪽)
public class IterativeAnagrams {
public static void main(String[] args) throws IOException {
File dictionary = new File(args[0]);
int minGroupSize = Integer.parseInt(args[1]);
Map<String, Set<String>> groups = new HashMap<>();
try (Scanner s = new Scanner(dictionary)) {
while (s.hasNext()) {
🔴🔴🔴String word = s.next();
groups.computeIfAbsent(alphabetize(word),
(unused) -> new TreeSet<>()).add(word);🔴🔴🔴
}
}
for (Set<String> group : groups.values())
if (group.size() >= minGroupSize)
System.out.println(group.size() + ": " + group);
}
private static String alphabetize(String s) {
char[] a = s.toCharArray();
Arrays.sort(a);
return new String(a);
}
}
computeIfAbsent
메서드를 사용해 해당 메서드는 맵 안에 키가 있는지 찾은 다음, 있으면 단순히 그 키에 매핑된 값을 반환한다. 키가 없으면 건네진 함수 객체를 키에 적용하여 값을 계산해낸 다음 그 키와 값을 매핑해놓고, 계산된 값을 반환.
다음 코드를 살펴보자.
// 코드 45-2 스트림을 과하게 사용했다. - 따라 하지 말 것! (270-271쪽)
public class StreamAnagrams {
public static void main(String[] args) throws IOException {
Path dictionary = Paths.get(args[0]);
int minGroupSize = Integer.parseInt(args[1]);
try (Stream<String> words = Files.lines(dictionary)) {
words.collect(
groupingBy(word -> word.chars().sorted()
.collect(StringBuilder::new,
(sb, c) -> sb.append((char) c),
StringBuilder::append).toString()))
.values().stream()
.filter(group -> group.size() >= minGroupSize)
.map(group -> group.size() + ": " + group)
.forEach(System.out::println);
}
}
}
스트림을 과용하면 프로그램이 읽거나 유지보수하기 어려워진다.
적절히 사용하는 코드란 무엇인가?
// 코드 45-3 스트림을 적절히 활용하면 깔끔하고 명료해진다. (271쪽)
public class HybridAnagrams {
public static void main(String[] args) throws IOException {
Path dictionary = Paths.get(args[0]);
int minGroupSize = Integer.parseInt(args[1]);
try (Stream<String> words = Files.lines(dictionary)) {
words.collect(groupingBy(word -> alphabetize(word)))
.values().stream()
.filter(group -> group.size() >= minGroupSize)
.forEach(g -> System.out.println(g.size() + ": " + g));
}
}
private static String alphabetize(String s) {
char[] a = s.toCharArray();
Arrays.sort(a);
return new String(a);
}
}
tip)
- 람다에서는 타입 이름을 자주 생략하므로 매개변수 이름을 잘 지어야 스트림 파이프라인의 가독성이 유지된다.
- 도우미 메서드를 적절히 활용하는 일의 중요성은 일반 반복코드에서보다는 스트림 파이프라인에서 휠씬 크다.
또한 char 값들을 처리할 때는 스트림을 삼가는 편이 좋다.
"Hello world!!.char().forEach(System.out::print)"
계산을 하게 되면 2176387278 숫자가 출력된다. 이처럼 예상하지 못한 결과가 나온다.
그리고 스트림을 처음 쓰기 시작하면 모든 반복문을 스트림으로 바꾸고 싶은 유혹이 일겠지만, 서두리지 않는 게 좋다. 왜냐하면 가독성과 유지보수 측면에서는 손해를 볼 수 있다. 그러니 기존 코드는 스트림을 사용하도록 리팩터링하되, 새 코드가 더 나아 보일 때만 만영하자.
- 코드 블록에서는 범위 안의 지역변수를 읽고 수정할 수 있다. 하지만 람다에서는
final
이거나 사실상final
변수만 읽을 수 있고, 지역변수를 수정하는 건 불가능하다. - 코드 블록에서는
return
문을 사용해 메서드에서 빠져나가거나,break
나continue
문으로 블록 바깥의 반복문을 종료하거나 반복을 한 번 건너뛸 수 있다. 또는 메서드 선언에 명시된 검사 예외를 던질 수 있다. 하지만 람다로는 이 중 어떤 것도 할 수 없다.
다음은 스트림으로 하기에 아주 좋은 일들
- 원소들의 시퀀스를 일관되게 변환한다.
- 원소들의 시퀀스를 필터링
- 원소들의 시퀀스를 하나의 연산을 사용해 결합
- 원소들의 시퀀스를 컬렉션으로 모은다.
- 원소들의 시퀀스에서 특정 조건을 만족하는 원소를 찾는다.
반면, 스트림을 쓰기 어려운 일은?
한 데이터가 파이프라인의 여러 단계(stage)를 통과할 때 이 데이터의 각 단계에서의 값들에 동시에 접근하기 어려운 경우.
예를 들어 20개의 메르센 소수(Mersenne prime)
여기서 p가 소수이면 해당 메르센 수도 소수일 수 있는데, 이때의 수를 메르센 소수라 한다.
// 스트림을 사용해 처음 20개의 메르센 소수를 생성한다. (274쪽)
public class MersennePrimes {
static Stream<BigInteger> primes() {
return Stream.iterate(TWO, BigInteger::nextProbablePrime);
}
public static void main(String[] args) {
primes().map(p -> TWO.pow(p.intValueExact()).subtract(ONE))
.filter(mersenne -> mersenne.isProbablePrime(50))
.limit(20)
.forEach(mp -> System.out.println(mp.bitLength() + ": " + mp));
}
}
여기서 문제가 되는 부분은
.forEach(mp -> System.out.println(mp.bitLength() + ": " + mp));
이 부분 왜냐면? p를 출력하길 원하지만, 종단연산forEach
에 접근할 수 없기 때문이다.
이번에는 카드 텍을 초기화하는 작업을 보자.
// 반복 방식과 스트림 방식으로 두 리스트의 데카르트 곱을 생성한다. (275-276쪽)
public class Card {
public enum Suit { SPADE, HEART, DIAMOND, CLUB }
public enum Rank { ACE, DEUCE, THREE, FOUR, FIVE, SIX, SEVEN,
EIGHT, NINE, TEN, JACK, QUEEN, KING }
private final Suit suit;
private final Rank rank;
@Override public String toString() {
return rank + " of " + suit + "S";
}
public Card(Suit suit, Rank rank) {
this.suit = suit;
this.rank = rank;
}
private static final List<Card> NEW_DECK = newDeck();
// 코드 45-4 데카르트 곱 계산을 반복 방식으로 구현 (275쪽)
private static List<Card> newDeck() {
List<Card> result = new ArrayList<>();
for (Suit suit : Suit.values())
for (Rank rank : Rank.values())
result.add(new Card(suit, rank));
return result;
}
public static void main(String[] args) {
System.out.println(NEW_DECK);
}
}
아래 코드에서는 중간 연산으로 사용된 flatMap은 스트림의 원소 각각을 하나의 스트림으로 매핑한 다음 그 스트림들을 다시 하나의 스트림으로 합친다. 이를 평탄화(flattening)
// 코드 45-5 데카르트 곱 계산을 스트림 방식으로 구현 (276쪽)
private static List<Card> newDeck() {
return Stream.of(Suit.values())
.flatMap(suit ->
Stream.of(Rank.values())
.map(rank -> new Card(suit, rank)))
.collect(toList());
}
flatmap
과 map
의 차이를 기억하기!
스트림 패러다임의 핵심은 계산을 일련의 변환(transformation)으로 재구성하는 부분
이때 각 변환 단계는 가능한 한 이전 단계의 결과를 받아 처리해야하는 순수 함수여야 한다.
순수 함수란 오직 입력만이 결과에 영향을 주는 함수. 다른 가변 상태를 참조하지 않고, 함수 스스로도 다른 상태를 변경하지 않음. 이렇게 하려면 스트림 연산에 건네는 함수 객체는 모두 side effect 없어야 한다.
// 코드 46-1 스트림 패러다임을 이해하지 못한 채 API만 사용했다 - 따라 하지 말 것! (277쪽)
Map<String, Long> freq = new HashMap<>();
try (Stream<String> words = new Scanner(file).tokens()) {
words.forEach(word -> {
freq.merge(word.toLowerCase(), 1L, Long::sum);
});
}
스트림, 람다, 메서드 참조, 결과도 올바르다. 하지만 절대 스트림 코드라 할 수 없다 왜?
스트림 코드를 가장한 반복적 코드다. 길고, 읽기 어렵고, 유지보수에도 좋지 않다. forEach가 그저 스트림이 수행한 연산 결과를 보여주는 일 이상을 하는 것을 볼 수 있다.
// 코드 46-2 스트림을 제대로 활용해 빈도표를 초기화한다. (278쪽)
Map<String, Long> freq;
try (Stream<String> words = new Scanner(file).tokens()) {
freq = words
.collect(groupingBy(String::toLowerCase, counting()));
}
System.out.println(freq);
Stream에서 forEach 연산은 스트림 계산 결과를 보고할 때만 사용하고, 계산하는 데는 쓰지 말자.
위 코드에서 collect
는 꼭 배워야하는 개념으로,
java.util.stream.Collectors
클래스는 축소(reduction) 전략을 캡슐화한 블랙박스 객체라고 생각할 것.
수집기를 사용하면 스트림의 원소를 손쉽게 컬렉션으로 모을 수 있는데,
- toList()
- toSet()
- toCollection(collectionFactory)
3가지가 존재한다.
// 코드 46-3 빈도표에서 가장 흔한 단어 10개를 뽑아내는 파이프라인 (279쪽)
List<String> topTen = freq.keySet().stream()
.sorted(comparing(freq::get).reversed())
.limit(10)
.collect(toList());
System.out.println(topTen);
https://docs.oracle.com/javase/10/docs/api/java/util/stream/Collectors.html
stream.Collectors 의 API문서를 읽기 추천.
-
comparing(freq::get).reversed()
여기서comparing
메서드는 키 추출 함수를 받는 비교자 생성 메서드(comparable), 한정적 메서드 참조이자, 위에서 키 추출 함수로 쓰인freq::get
은 입력받은 단어(키)를 빈도표에서 찾아(추출) 그 빈도를 반환.
stream.Collectors
toMap(keyMapper, valueMapper)
: 스트림원소를 키에 매핑하는 함수와 값에 매핑하는 함수를 인수로 받는다.
public static final Map<String, Operation> stringToEnum =
String.of(values()).collect(
toMap(Object::toString, e -> e));
toMap
형태는 스트림의 각 원소가 고유한 키에 매핑되어 있을 때 적합. 다수가 같은 키를 사용한다면 IllegalStateException
던짐
//각 키와 해당 키의 특정 원소를 연관 짓는 맵을 생성하는 수집기
Map<Artiest, Album> topHits = albums.collect(
toMap(Album::artist, a->a, maxBy(comparing(Album::sales))));
위 코드에서 말로 풀어보면
"앨범 스트림을 맵으로 바꾸는데, 이 맵은 각 음악가와 그 음악가의 베스트 앨범을 짝지은 것이다."
또는 인수가 3개일 경우,
toMap(keyMapper, valueMapper, (oldVal, newVal) -> newVal)
이런식일 경우, 마지막에 쓴 값을 취하는 수집기
groupingBy
입력으로 분류 함수(classifier)를 받고 출력으로는 원소들을 카테고리별로 모아 놓은 맵을 담은 수집기를 반환
분류 함수는 입력받은 원소가 속하는 카테고리를 반환. 그리고 이 카테고리가 해당 원소의 맵 키로 쓰인다.
다중정의된 groupingBy 중 형태가 가장 간단한 것은 분류 함수 하나를 인수로 받아 맵을 반환.
반환된 맵에 담긴 각각의 값은 해당 카테고리에 속하는 원소들을 모두 담은 리스트.
words.collect(groupingBy(word -> alphabetize(word)))
결과는 맵으로 반환될 텐데, 만약 맵으로 반환하고 싶지 않을 수있다. 그럴 경우, 다운스트림 명시.
groupingBy
가 반환하는 수집기가 리스트 외의 값을 갖는 맵을 생성하게 하려면, 분류 함수와 함께 다운스트림(downstream) 수집기도 명시해야 한다. 여기서 다운스트림 수집기의 역할은 해당 카테고리의 모든 원소를 담은 스트림으로부터 값을 생성하는 일이다.
Map<String, Long> freq = words.collect(groupingBy(String::toLowerCase, counting()));
위와 같은 코드의 이해는 키값을 모두 toLowertCase
로 만들고, 이를 counting
라고 이해하면 된다.
마지막 세번째 버전은 다운스트림 수집기에 더해 맵 팩터리도 지정할 수 있게 해준다.
즉, mapFactory 매개변수가 downStream 매개변수보다 앞에 놓인다.
세번째 버전의 예로는, TreeSet인 TreeMap을 반환하는 수집기를 만들 수 있다.
partitioningBy
: 분류 함수자리에 프레디키드를 받고 키가 Boolean인 맵을 반환
groupingBy와는 반대이다.
counting과 유사한 함수로 summing, averaging, summarizing 이 있다.
나머지 3개가 더 남았는데, minBy, maxBy 는 인수로 받은 비교자를 이ㅛㅇ해 스트림에서 값이 가장 작은 혹은 가장 큰 원소를 찾아 반환한다.
왜 스트림보다 컬렉션이 나을까?
for(ProcessHandler ph: ProcessHandle.allProcesses()::iterator){
//프로세스를 처리한다.
}
// 위 코드는 컴파일 오류를 낸다.
// method refercne not expected here
이 오류를 바로 잡으려면 메서드 참조를 매개변수화된 Iterable로 적절히 형변환해줘야 한다.
for(ProcessHandler ph: (Iterable<ProcessHandle>)ProcessHandle.allProcesses()::iterator){
//프로세스를 처리한다.
}
너무 난잡하고 직관성이 떨어진다. 다행히 어탭터 메서드를 사용하면 좋아보임.
// 스트림 <-> 반복자 어댑터 (285-286쪽)
public class Adapters {
// 코드 47-3 Stream<E>를 Iterable<E>로 중개해주는 어댑터 (285쪽)
public static <E> Iterable<E> iterableOf(Stream<E> stream) {
return stream::iterator;
}
// 코드 47-4 Iterable<E>를 Stream<E>로 중개해주는 어댑터 (286쪽)
public static <E> Stream<E> streamOf(Iterable<E> iterable) {
return StreamSupport.stream(iterable.spliterator(), false);
}
}
for(ProcessHandler ph: iterableOf(ProcessHandle.allProcesses()){
//프로세스를 처리한다.
}
두번째 streamOf
메서드를 통해 iterable을 stream으로 변환할 수 있다.
반환하는 시퀀스의 크기가 메모리에 올려도 안전할 만큼 작다면 ArrayList나 HashSet같은 표준 컬렉션 구현체를 반환하는 게 최선일 수있다. 하지만 단지 컬렉션을 반환한다는 이유로 덩치 큰 시퀀스를 메모리에 올려서는 안 된다.
덩치가 큰 시퀀스라면 AbstractList를 이용하면 훌륭한 전용 컬렉션을 손숩게 구현할 수 있다.
import java.util.*;
public class PowerSet {
// 코드 47-5 입력 집합의 멱집합을 전용 컬렉션에 담아 반환한다. (287쪽)
public static final <E> Collection<Set<E>> of(Set<E> s) {
List<E> src = new ArrayList<>(s);
if (src.size() > 30)
throw new IllegalArgumentException(
"집합에 원소가 너무 많습니다(최대 30개).: " + s);
return new AbstractList<Set<E>>() {
@Override public int size() {
// 멱집합의 크기는 2를 원래 집합의 원소 수만큼 거듭제곱 것과 같다.
return 1 << src.size();
}
@Override public boolean contains(Object o) {
return o instanceof Set && src.containsAll((Set)o);
}
@Override public Set<E> get(int index) {
Set<E> result = new HashSet<>();
for (int i = 0; index != 0; i++, index >>= 1)
if ((index & 1) == 1)
result.add(src.get(i));
return result;
}
};
}
public static void main(String[] args) {
Set s = new HashSet(Arrays.asList(args));
System.out.println(PowerSet.of(s));
}
}
public class ParallelMersennePrimes {
public static void main(String[] args) {
primes().map(p -> TWO.pow(p.intValueExact()).subtract(ONE))
.filter(mersenne -> mersenne.isProbablePrime(50))
.limit(20)
.forEach(System.out::println);
}
static Stream<BigInteger> primes() {
return Stream.iterate(TWO, BigInteger::nextProbablePrime);
}
}
스트림 병렬화는 아무때나 막할 수 있는가? No
위 코드에서 .parallel() // 스트림 병렬화
추가하게 되면, 무한 루프에 빠진다.
무슨일일까?
이유는 스트림 라이브러리가 이 파이프라인을 병렬화하는 방법을 찾아내지 못했기 때문이다. 환경이 아무리 좋더라도 데이터 소스가 Stream.iterate
거나 중간 연산으로 limit
를 쓰면 파이프라인 병렬화로는 성능 개선을 기대할 수 없다. 위 코드는 2문제 모두 가지고 있다.
대체로 스트림의 소스가 ArrayList, HashMap, HashSet, ConcurrentHashMap의 인스턴스거나 배열, int 범위, long 범위 일때 병렬화의 효과가 가장 좋다. 이 자료구조들은 모두 데이터를 원하는 크기로 정확하고 손쉽게 나눌 수 있어서 일을 다수의 스레드에 분배하기에 좋다는 특징이 있다.
나누는 작업은 Spliterator가 담당하며, Spliterator 객체는 Stream이나 Iterable의 spliterator메서드로 얻어올 수 있다.
이 자료구조들의 또 다른 중요한 공통점은 원소들을 순차적으로 실행할 때의 참조 지역성이 띄어나다는 것.
종단 연산 중 병렬화에 가장 적합한 것은 축소다. 축소는 파이프라인에서 만들어진 모든 원소를 하나로 합치는 작업으로 Stream의 reduce 메서드 중 하나, 혹은 min max count sum 같이 완성된 형태로 제공되는 메서드 중 하나를 선택해 반환되는 메서드에 적합.
조건이 잘 갖춰지면 parallel 메서드 호출 하나로 거의 프로세서 코어 수에 비레하는 성능 향상을 만끽할 수 있지만, 스트림 병렬화는 오직 성능 최적화 수단임을 기억하자. 변경 전후로 반드시 성능을 테스트하여 병렬화를 사용할 가치가 있는지 확인해야 한다.