스트림 API는 다량의 데이터 처리 작업을 돕고자 자바 8에 추가되었다.
스트림 API 는 다재다능하여 사실상 어떠한 계산이라도 해낼 수 있지만, 제대로 사용하지 않는다면 읽기 어렵고 유지보수도 힘든 코드를 생산할 수 있다.
public class Anagrams {
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
를 사용하면 각 키에 다수의 값을 매핑하는 맵을 쉽게 구현할 수 있다. public static void main(String[] args) throws IOException {
Path dictionary = Paths.get(args[0]);
int minGroupSize = Integer.parseInt(args[0]);
Map<String, Set<String>> groups = new HashMap<>();
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);
}
}
이런식으로 스트림을 과용하면 프로그램이 읽거나 유지보수하기 어려워진다.
다음 코드는 스트림을 적당히 사용한 예시이다.
public static void main(String[] args) throws IOException {
Path dictionary = Paths.get(args[0]);
int minGroupSize = Integer.parseInt(args[0]);
Map<String, Set<String>> groups = new HashMap<>();
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);
}
스트림을 본 적이 없더라도 이 코드는 이해하기 쉬울 것이다.
자바가 기본 타입인 char 용 스트림을 지원하지 않는다.
"Hello world!".chars.forEach(System.out::print);
이 스트림의 결과로 hello world! 가 출력될 것 같지만 이상한 뭉치의 숫자들을 출력한다.
이는 "hello world!".chars()
가 반환하는 스트림의 원소는 char 가 아닌 int 값이기 때문이다.
"Hello world!".chars.forEach(System.out.print((char) x));
이렇게 하면 되지만 그냥 char 값들을 처리할 때는 되도록이면 스트림을 삼가자.
그래서 이런 경우들에는 스트림이 맞지 않아서 쓰면 안좋다는 말이다.
메르센 소수를 출력하는 프로그램을 작성해보자. 메르센 수는 2^p-1 형태의 수이고, p 가 소수이면 해당 메르센 수도 소수일 때 메르센 소수라고 한다.
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(System.out::println);
}
이 때 우리가 메르센 소수 앞에 지수(p)를 출력하고 싶다고 해보자.
이 값은 초기 스트림에서 이미 사용된 값으로 종단연산에서는 접근 할 수 없으므로, 앞서 말했드싱 매핑을 거꾸로 수행해 메르센 수의 지수를 구해낼 수 있다.
.forEach(mp -> System.out.println(mp.bitLength() + ": " +mp);
카드 덱을 초기화하는 작업을 생각해보자.
카드는 숫자(rank)와 무늬(suit)를 묶은 불변 값 클래스이고, 숫자와 무늬는 모두 열거타입이라고 하자.
이 작업은 두 집합의 원소들로 만들 수 있는 가능한 모든 조합을 계산하는 문제이다.
private static List<Card> new Deck(){
List<Card> result = new ArrayList<>();
for(Suit suit : suit.values())
for(Rank rank : Rank.values())
result.add(new Card(suit, rank));
return result;
}
private static List<Card> new Deck(){
return Stream.of(Suit.values())
.flatMap(suit ->
Stream.of(Rank.values())
.map(rank -> new Card(suit, rank)))
.collect(toList());
}
아마 보통의 프로그래머라면 첫번째 방식이 익숙하겠지만, 스트림이 익숙한 개발자는 두번째 방식을 선호할 수도 있다. 더 나아보이는 방식을 동료들과 협의하여 사용하자.
다음은 텍스트 파일에서 단어별 수를 세어 빈도표를 만드는 작업이다.
Map<String, Long> freq = new HashMap<>();
try(Stream<String> words = new Scanner(file).tokens()){
words.forEach(word -> {
freq.merge(word.toLowerCase(), 1L, Long::sum);
});
}
Map<String, Long> freq;
try(Stream<String> words = new Scanner(file).tokens()) {
freq = words
.collect(groupingBy(String::toLowerCase, counting()));
}
List<String> topTen = freq.keySet().stream()
.sorted(comparing(freq::get).reversed())
.limit(10)
.collect(toList());
toMap(keyMapper, valueMapper)
이다.private static final Map<String, Operation> stringToEnum =
Stream.of(values()).collect(
toMap(Object::toString, e->e));
Map<Artist, Album> topHits = albums.collect(
toMap(Album::artist, a->a, maxBy(Comparing(Album::sales))));
words.collect(groupingBy(word -> alphabetize(word)))
Map<String, Long> freq = words
.collect(groupingBy(String::toLowerCase, counting()));
앞으로 살펴볼 Collectors 메서드는 특이하게도 '수집'과는 관련이 없다.
joining(",","[","]")
이런식으로 사용한 수집기는 [came, saw, conqured] 처럼 컬렉션을 출력한 듯한 문자열을 생성할 수 있다.