Skip to content

Latest commit

 

History

History
265 lines (225 loc) · 10 KB

Item45_48.md

File metadata and controls

265 lines (225 loc) · 10 KB

Item 45. 스트림은 주의해서 사용해라

  • 스트림은 데이터 원소의 유/무한 시퀀스 를 뜻함.
  • 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 변환시, 새 코드가 더 나아 보일 때에만 적용하자

code block 에서 가능하나 stream 내부에서 사용되는 function 에서 불가능한 것

  • 지역 변수 수정이 불가능, final 이거나 사실상의 final 인 경우만 읽을 수 있다.
  • loop 시 활용되는 return. break, continue 등의 flow control 을 할 수 없다.
  • 사용할 function 의 signature 에 Exception 을 걸 수 없다.

stream 활용에 적절한 경우

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

stream 으로 처리하기 까다로운 케이스

// 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());
}
  • 결론 : 알려진 권장 지침이나 안티패턴 샘플들을 참고하고, 정 없으면 둘 다 해보고 주위 의견을 나누어서 결정해라.

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

  • 가독성을 위해 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());

Collectors 지원 기능 목록

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

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

  • stream 은 iteration 을 지원하지 않음
  • ...

Item 48. 스트림 병렬화는 주의하여 사용하라

  • 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);
    }
}
  • 효율적인 스트림 병렬화의 조건

부록

Intellij Stream Debugger