람다와 스트림


42. 익명 클래스보다는 람다를 사용하자.

자바에서 함수 타입을 표현
추상 메서드를 하나만 담은 인터페이스익명 클래스람다식

Collections.sort(words, new Comparator<String>() {
    public int compare(String s1, String s2) {
        return Integer.compare(s1.length(), s2.length());
    }
});

익명 클래스는 코드가 너무 길어 자바는 함수형 프로그래밍에 적합하지 않았다.
자바8에서 람다식을 사용하게 되면서 특별 대우를 받게 되었다.

Collections.sort(words,
    (s1, s2) -> Integer.compare(s1.length(), s2.length()));

매개변수, 반환값 타입을 컴파일러가 문맥을 살펴 추론해준다.


람다식이 많이 쓰이면서 익명 클래스가 잘 쓰이지 않았지만, 람다로 대체할 수 없는 곳이 있다.




43. 람다보다는 메서드 참조를 사용하자.

람다가 익명 클래스보다 나은 점 중 가장 큰 특징은 간결함이다.
람다보다 더 간결하게 만드는 방법이 있는데, 메서드 참조이다.

map.merge(key, 1, (count, incr) -> count + incr);

깔끔해 보이지만 위 코드에 count, incr 매개변수는 크게 하는 일 없이 공간을 꽤 차지한다.

자바 8이 되면서 Integer 클래스(와 모든 기본 타입의 박싱 타입)는
이 람다와 기능이 같은 정적 메서드 sum을 제공하기 시작했다.

람다 대신 이 메서드의 참조를 전달하면 똑같은 결과를 더 간결하게 표현할 수 있다.

map.merge(key, 1, Integer::sum);

람다로 할 수 없는 일은 메서드 참조로도 할 수 없다. (예외가 있긴함)


5가지 메서드 참조

메서드 참조 유형 같은 기능을 하는 람다
정적 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]




44. 표준 함수형 인터페이스를 사용하자.

자바가 람다를 지원하면서 상위 클래스의 기본 메서드를 재정의해 원하는 동작을 구현하는 템플릿 메서드 패턴의 매력이 크게 줄었다.
이를 대체하는 해법은 같은 효과의 함수 객체를 받는 정적 팩토리나 생성자를 제공하는 것이다.
즉 함수 객체를 매개변수로 받는 생성자와 메서드를 더 많이 만들어야 한다.
이때 함수형 매개변수 타입을 올바르게 선택해야 한다.


예) LinkedHashMap removeEldesEntry(): return 값이 true이면 가장 오래된 원소를 제거한다.

protected boolean removeEldesEntry(Map.Entry<K, V> eldet) {
    return size() > 100;
}

위 코드도 잘 동작하지만, LinkedHashMap을 다시 구현한다면 함수 객체를 받는 정적 팩토리나 생성자를 제공했을 것이다.
removeEldestEntry는 size()를 호출해 맵 안의 원소 수를 알아내는데
removeEldestEntry가 인스턴스 메서드라서 가능한 방식이다.

하지만 생성자에 넘기는 함수 객체는 이 맵의 인스턴스 메서드가 아니다.
팩토리나 생성자를 호출할 때는 맵의 인스턴스가 존재하지 않기 때문이다.
따라서 맵은 자기 자신도 함수 객체에 건네줘야 한다.
이를 반영한 함수형 인터페이스는 다음과 같이 선언할 수 있다.

@FunctionalInterface interface EldesEntryRemovalFunction<K, V> {
    boolean remove(Map<K, V> map, Map.Entry<K, V> eldest);
}

이 인터페이스도 잘 동작하지만, 굳이 사용할 이유는 없다.
자바 표준 라이브러리에 이미 같은 인터페이스가 있다.
java.util.function 패키지를 보면 다양한 표 준 함수형 인터페이스가 있다.
필요한 용도에 맞는게 있다면, 직접 구현하지 않고 표준 함수형 인터페이스를 활용하면 된다.

이 예제의 LinkedHashMap에서 직접 만든 메서드가 아닌
표준 인터페이스인 BiPredicate<Map<K, V>, Map.Entry<K, V>>를 사용할 수 있다.


표준 함수형 인터페이스 / 직접 구현한 함수형 인터페이스




45. 스트림을 주의해서 사용하자.

스트림 API는 다량의 데이터 처리 작업을 돕고자 자바8에 추가되었다.

이 API가 제공하는 추상 개념 중 핵심은 다음과 같다.


사전 파일에서 단어를 읽어 애너그램 그룹을 출력하는 프로그램을 만들어보자.
예) “staple” → “aelpst”, “petals”, …

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();
                // 맵에 각 단어를 추가하는데, 자바 8에 추가된 computeIfAbsent 사용 
                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);
    }
}
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);
        }
    }
}

스트림을 과용하면 프로그램을 읽어나 유지보수하기 어려워진다.

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);
    }
}


스트림 주의사항


함수 객체로 할 수 없지만 코드 블록에서는 할 수 있는 것


스트림이 잘 맞는 로직



46. 스트림에서는 부작용 없는 함수를 사용하자.

스트림 패러다임의 핵심은 계산을 일련의 변환으로 재구성하는 부분이다.
스트림 연산에 건네는 함수 객체는 모두 부작용이 없어야 한다.

예) 텍스트 파일에서 단어별 수를 세어 빈도표 만들기

Map<String, Long> freq = new HashMap<>();
try (Stream<String> words = new Scanner(file).tokens()) {
  words.forEach(word -> {
    freq.merge(word.toLowerCase(), 1L, Long::sum);
  });
}

스트림, 람다, 메서드 참조를 사용했고 결과도 원하는대로 나온다.
그러나 스트림 코드라고 할 수 없다. 스트림 API의 이점을 살리지 못한 반복적 코드다.

forEach 연산은 종단 연산 중 기능이 가장 적고 가장 ‘덜’ 스트림답다.
대놓고 반복적이라 병렬화할 수도 없다.
forEach 연산은 스트림 계산 결과를 보고할 때만 사용하고, 계산하는 데는 쓰지 않는게 좋다.

Map<String, Long> freq;
try (Stream<String> words = new Scanner(file).tokens()) {
  freq = words
    .collect(groupingBy(String::toLowerCase, counting()));
}



47. 반환 타입으로는 스트림보다 컬렉션이 낫다.

Array형태의 Linear한 자료구조를 반환하는 메서드는 수없이 많다.
이런 메서드의 반환타입으로 아래와 같은 타입을 사용했다.

기본은 Collection 타입이다. for-each 문에서만 쓰이거나, (contain(Object) 같은) 일부 Collection 메서드를 구현 할 수 없을 때는 Iterable 인터페이스를 사용한다. 성능에 민감한 상황이면, E[] 형태의 배열을 주로 사용해 왔다.

자바 8이 스트림이라는 개념을 들고오면서 선택이 더욱 복잡해지게 되었다.