peaches-book-study / effective-java

이펙티브 자바 3/E
0 stars 2 forks source link

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

Open pyeong114 opened 3 months ago

pyeong114 commented 3 months ago

Chapter : 7. 람다와 스트림

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

Assignee : pyeong114


🍑 서론

🍑 본론

스트림 API?

다량의 데이터 처리 작업(순차적이든 병렬적이든)을 돕고자 자바 8에 추가되었다. 이 API가 제공하는 추상 개념 중 핵심은 두 가지다.


스트림의 원소들은 어디로부터든 올 수 있다.


스트림 안의 데이터 원소들은 객체 참조(reference)나 기본 타입(int, long, double)을 지원한다.


스트림 파이프 라인은 소스 스트림에서 시작해 종단 연산(terminal operation)으로 끝나며, 그 사이에 하나 이상의 중간 연산(intermediate operation)이 있을 수 있다.

종단연산?

: 스트림 파이프라인의 실행을 트리거하고 스트림을 소비하여 결과를 생성하는 연산 : 종단 연산을 수행한 후에는 스트림이 닫히게 되어, 더 이상 해당 스트림에서 데이터를 사용할 수 없다. : 다양한 종류의 종단 연산이 있으며, 크게 결과를 반환하는 연산, 특정 결과를 소비하는 연산, 그리고 스트림의 요소들에 대해 특정 작업을 수행하는 연산으로 분류할 수 있다.

집계 연산(Aggregate operations):

중간 연산 (intermediate operation)

: 스트림의 요소들을 변환하거나 필터링하는 등의 처리를 수행하고, 그 결과로 새로운 스트림을 반환함 : 이러한 연산들은 연쇄적으로 연결될 수 있으며, 스트림 파이프라인을 구성하는 데 사용


스트림 파이프라인은 지연 평가(lazy evaluation)된다.


스트림 API는 매서드 연쇄를 지원하는 플루언트 API다. 즉, 파이프라인 하나를 구성하는 모든 호출을 연결하여 단 하나의 표현식으로 완성할 수 있다. 파이프라인 여러 개를 연결해 표현식 하나로 만들 수 있다.

List<String> myList = Arrays.asList("apple", "banana", "apricot", "orange");
List<String> filteredList = myList.stream()
    .filter(s -> s.startsWith("a"))
    .map(String::toUpperCase)
    .collect(Collectors.toList());

기본적으로 스트림 파이프라인은 순차적으로 수행된다. parallel매서드를 호출하여 파이프라인을 병렬로 실행할 수도 있지만,효과를 볼 수 있는 상황은 많지 않다.

스트림 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 메서드**
>: Java의 Map 인터페이스에서 제공
>: 키에 해당하는 값이 없을 경우, 주어진 키로 새로운 값을 계산하고 맵에 추가한 후 그 값을 반환
>: 만약 키가 이미 존재하고, 해당 키에 대한 값이 null이 아닌 경우, 맵은 변경되지 않고 기존의 값을 반환. 

#### 스트림을 과하게 사용한 경우
```java
public class Anagrams {
    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 Anagrams {
    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)) {
            word.collect(groupingBy(word -> alphabetize(word)))
                .values().stream()
                .filter(group -> group.size() >= minGroupSize)
                .forEach(g -> System.out.println(g.size() + ": " + g));
        } 
    }
}

스트림을 처음 쓰기 시작하면 모든 반복문을 스트림으로 바꾸고 싶은 유혹이 생기겠지만, 스트림과 반복문을 적절히 조합하는 게 최선이다.

스트림 부적절한 경우

: 함수 객체로는 할 수 없지만 코드 블록으로는 할 수 있는 경우

스트림이 적절한 경우

🍑 결론


Referenced by