- 스트림은 데이터 원소의 유/무한 시퀀스 를 뜻함.
- soruce stream 부터 terminal operation 까지의 과정이라고 생각하면 된다.
- lazy evaluation 구조이고, 실제 evaluation 은 'terminal operation' 시점에 이루어진다.
- 즉 terminal operation 이 없는 stream 은 no-op 와 동일하다.
- fluent API 구조를 띈다.
- 잘못 쓰면 유지보수성을 오히려 해친다
- Iterative 방법
// Prints all large anagram groups in a dictionary iteratively (Page 204)
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), // groups 에 키가 없을 때에만, 초기 treeset 생성
(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 : jdk 의 interface default 활용의 예
default V computeIfAbsent(K key,
Function<? super K, ? extends V> mappingFunction) {
Objects.requireNonNull(mappingFunction);
V v;
if ((v = get(key)) == null) {
V newValue;
if ((newValue = mappingFunction.apply(key)) != null) {
put(key, newValue);
return newValue;
}
}
return v;
}
- Stream 활용 예
// Overuse of streams - don't do this! (page 205)
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);
}
}
// Tasteful use of streams enhances clarity and conciseness (Page 205)
// alphabetize 의 분리, map 을 활용하던 출력전용 부분을 forEach 로 이동
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(group -> System.out.println(group.size() + ": " + group));
}
...
}
-
stream 사용시 lambda 를 자연스럽게 활용하게 되는데, 변수명은 최대한 구체적으로 지정하여 코드 가독성에 도움을 줄 수 있다. (variable type 이 눈에 안보이기때문)
-
char 자료형의 stream representation 의 문제점
// Does not produce the expected result
// String 의 chars 는 IntStream 반환함.
"Hello world!".chars().forEach(System.out::print); // 721011081081113211911111410810033
System.out.println();
"Hello world!".chars().forEach(x -> System.out.print(x + " ")); // 72 101 108 108 111 32 119 111 114 108 100 33
System.out.println();
// Fixes the problem
"Hello world!".chars().forEach(x -> System.out.print((char) x)); // Hello world!
System.out.println();
- 결론은 기존 코드의 stream 변환시, 새 코드가 더 나아 보일 때에만 적용하자
- 지역 변수 수정이 불가능, final 이거나 사실상의 final 인 경우만 읽을 수 있다.
- loop 시 활용되는 return. break, continue 등의 flow control 을 할 수 없다.
- 사용할 function 의 signature 에 Exception 을 걸 수 없다.
- element sequence 를 일관되게 변경, 필터링, 단일 연산의 결합(sum, min/max 등)으로 사용
- element sequence 를 collect 한다
- element sequence 중 조건을 만족하는 element 를 찾는다
// 참고 - 단순 numeric looping 의 경우 for 문보다 성능이 떨어진다 함 : https://homoefficio.github.io/2016/06/26/for-loop-%EB%A5%BC-Stream-forEach-%EB%A1%9C-%EB%B0%94%EA%BE%B8%EC%A7%80-%EB%A7%90%EC%95%84%EC%95%BC-%ED%95%A0-3%EA%B0%80%EC%A7%80-%EC%9D%B4%EC%9C%A0/
int oddSquareSum = IntStream.range(1, 100)
.map(x -> x * x)
.filter(x -> x % 2 != 0)
.reduce((x1, x2) -> x1 + x2)
.getAsInt();
System.out.println(oddSquareSum);
// Generating the first twent Mersenne primes using streams (Page 208)
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));
}
// Iterative Cartesian product computation
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;
}
// Stream-based Cartesian product computation
private static List<Card> newDeck() {
return Stream.of(Suit.values())
.flatMap(suit ->
Stream.of(Rank.values())
.map(rank -> new Card(suit, rank)))
.collect(toList());
}
- 결론 : 알려진 권장 지침이나 안티패턴 샘플들을 참고하고, 정 없으면 둘 다 해보고 주위 의견을 나누어서 결정해라.
- 가독성을 위해 stream 을 stream 답게 사용 하자는 이야기
// Uses the streams API but not the paradigm--Don't do this!
Map<String, Long> freq = new HashMap<>();
try (Stream<String> words = new Scanner(file).tokens()) {
words.forEach(word -> { // stream 의 forEach 를 data modification 용으로 쓰지 말자는 이야기
freq.merge(word.toLowerCase(), 1L, Long::sum);
});
}
// Proper use of streams to initialize a frequency table (
Map<String, Long> freq;
try (Stream<String> words = new Scanner(file).tokens()) {
freq = words
.collect(groupingBy(String::toLowerCase, counting()));
}
// Pipeline to get a top-ten list of words from a frequency table
List<String> topTen = freq.keySet().stream()
.sorted(comparing(freq::get).reversed())
.limit(10)
.collect(toList());
- https://docs.oracle.com/javase/8/docs/api/java/util/stream/Collectors.html
- toList
- toSet
- toMap
- groupingBy
- joining
// Accumulate names into a List
List<String> list = people.stream().map(Person::getName).collect(Collectors.toList());
// Accumulate names into a TreeSet
Set<String> set = people.stream().map(Person::getName).collect(Collectors.toCollection(TreeSet::new));
// Convert elements to strings and concatenate them, separated by commas
String joined = things.stream()
.map(Object::toString)
.collect(Collectors.joining(", "));
// Compute sum of salaries of employee
int total = employees.stream()
.collect(Collectors.summingInt(Employee::getSalary)));
// Group employees by department
Map<Department, List<Employee>> byDept
= employees.stream()
.collect(Collectors.groupingBy(Employee::getDepartment));
// Compute sum of salaries by department
Map<Department, Integer> totalByDept
= employees.stream()
.collect(Collectors.groupingBy(Employee::getDepartment,
Collectors.summingInt(Employee::getSalary)));
// Partition students into passing and failing
Map<Boolean, List<Student>> passingFailing =
students.stream()
.collect(Collectors.partitioningBy(s -> s.getGrade() >= PASS_THRESHOLD));
- stream 은 iteration 을 지원하지 않음
- ...
- parallel() 로 단순하게 병렬화를 지원한다고 하지만, 불안정한 점이 존재
- 병렬화 실행 계획을 찾는데 실패하게 되면 cpu 자원만 점유한 채로 결과를 정상적으로 내놓지 못한다.
// Parallel stream-based program to generate the first 20 Mersenne primes - HANGS!!! (Page 222)
public class ParallelMersennePrimes {
public static void main(String[] args) {
primes().map(p -> TWO.pow(p.intValueExact()).subtract(ONE))
.parallel()
.filter(mersenne -> mersenne.isProbablePrime(50))
.limit(20) // 이부분 제외하면 정상 동작함
.forEach(System.out::println);
}
static Stream<BigInteger> primes() {
return Stream.iterate(TWO, BigInteger::nextProbablePrime);
}
}
- 효율적인 스트림 병렬화의 조건