[Effective Java] 아이템 45. 스트림은 주의해서 사용하라

2022. 11. 4. 22:40· BE/Java
목차
  1. 아이템 45. 스트림은 주의해서 사용하라
  2. 스트림
  3. 스트림 파이프라인
  4. 스트림 API
  5. char 스트림
  6.  
  7. 유의점
  8. 함수 객체 vs 반복 코드
  9. 스트림으로 처리하기 어려운 작업
  10. 정리
반응형

EFFECTIVE JAVA(이펙티브 자바)

 

이 포스팅에서 작성하는 내용은 EFFECTIVE JAVA(이펙티브자바) 에서 발췌하였습니다.


아이템 45. 스트림은 주의해서 사용하라

 

스트림

  • 다량의 데이터 처리 작업(순차 or 병렬)을 돕고자 자바 8에 추가되었다.
  • 스트림(stream)은 데이터 원소의 유한 혹은 무한 시퀀스를 뜻한다.
  • 스트림 파이프라인(stream pipeline)은 이 원소들로 수행하는 연산 단계를 표현하는 개념이다.
  • 스트림의 원소들은 어디서로부터 올 수 있다. ex) 컬렉션, 배열, 파일, 정규표현식 패턴 matcher, 난수생성기 등
  • 스트림 안의 데이터 원소들은 객체 참조나 기본 타입 값이다. 기본 타입 값으로는 int, long, double 세 가지를 지원한다.

 

스트림 파이프라인

  • 소스 스트림에서 시작해 종단 연산으로 끝나며, 그 사이에 하나 이상의 중간 연산이 있을 수 있다.
  • 각 중간 연산은 스트림을 어떠한 방식으로 변환한다. ex) 각 원소에 함수를 적용하거나 특정 조건을 만족 못하는 원소를 걸러냄
  • 중간 연산들은 모두 한 스트림을 다른 스트림으로 변환하는데, 변환된 스트림의 원소 타입은 변환 전 스트림의 원소 타입과 같을 수도 다를 수도 있다.
  • 종단 연산은 마지막 중간 연산이 내놓은 스트림에 최후 연산을 진행한다. ex) 원소를 정렬해 컬렉션에 담거나, 특정 원소 하나를 선택하거나, 모든 원소를 출력하는 등
  • 스트림 파이프라인은 지연 평가 된다.
    • 평가는 종단 연산이 호출될 때 이뤄지며, 종단 연산에 쓰이지 않는 데이터 원소는 계산에 쓰이지 않는다.
    • 이러한 지연 평가가 무한 스트림을 다를 수 있게 해준다.
    • 종단 연산이 없는 스트림 파이프라인은 아무 일도 하지 않는 no-op과 같으니, 종단 연산을 빼먹지 말자.

 

스트림 API

  • 메서드 연쇄를 지원하는 플루언트(fluent) API다. 즉, 파이프라인 하나를 구성하는 모든 호출을 연결하여 단 하나의 표현식으로 완성할 수 있으며, 파이프라인 여러 개를 연결해 표현식 하나로 만들 수도 있다.
  • 스트림을 제대로 사용하면 프로그램이 짧고 간결해지지만, 잘못 사용하면 읽기 어렵고 유지보수도 힘들어진다.

 

 

ex 1) 사전 하나를 탐색해 원소 수가 많은 아나그랩 그룹들을 출력한다.

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);
    }
}
  • 이 프로그램은 사전 파일에서 단어를 읽어 사용자가 지정한 문턱값보다 원소 수가 많은 아나그램 그룹을 출력한다.
  • 맵에 각 단어를 삽입할 때 자바 8에서 추가된 computeIfAbsent 메서드를 사용했다.
    • computeIfAbsent : 맵 안에 키가 있는 지 확인한 다음, 있으면 그 키에 매핑된 값을 반환한다. 키가 없다면 건네진 함수 객체를 키에 적용해 값을 계산한 다음 그 키와 값을 매핑하고 계산된 값을 반환한다.

 

ex 2) ex 1의 코드를 최대한 스트림으로 작성한 코드

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);
        }
    }
}
  • 이렇게 스트림을 과용하면 읽거나 유지보수하기 어려워진다.

 

 

ex 3) ex 1의 코드를 스트림으로 적절히 활용한 코드

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 -> alphabetize(word)))
                    .values().stream()
                    .filter(group -> group.size() >= minGroupSize)
                    .forEach(group -> System.out.println(group.size() + ": " + group));
        }
    }

		// alphabetize 메서드는 ex 1의 코드와 같다.
}
  • 이렇게 스트림을 적절히 활용하면, 원래 코드보다 짧을 뿐 아니라 명확해진다.
  • 참고로, 람다 매개변수의 이름은 주의해서 정해야 한다. (람다에서는 타입 이름을 자주 생략하므로 매개변수 이름을 잘 지어야 스트림 파이프라인의 가독성이 유지된다.)

 

 

char 스트림

  • 자바가 기본타입인 char용 스트림을 지원하지 않는다.
  • “Hello world!”.chars()가 반환하는 스트림의 원소는 char가 아닌 int 값이다.
  • char 값들을 처리할 때는 스트림을 삼가하는 편이 좋다.
"Hello world!".chars().forEach(System.out.print);

// 예상 출력 : Hello world!
// 실제 출력 : 7210110810811132119111111410810033

// 올바르게 print 메서드를 호출하려면 아래와 같이 명시적 형변환이 필요하다.
"Hello world!".chars().forEach(x -> System.out.print((char) x));

 

유의점

  • 스트림과 반복문을 적절히 조합하여 기존 코드는 스트림을 사용하도록 리펙터링하되, 새 코드가 나아보일 때만 사용하자.

 

 

함수 객체 vs 반복 코드

  • 스트림 파이프라인은 되풀이되는 계산을 함수 객체(람다 or 메서드 참조) 로 표현한다.
  • 반복 코드에서는 코드 블록을 사용해 표현한다.
  • 함수 객체로는 할 수 없지만 코드 블록으로는 할 수 있는 일들이 있으며 그 예는 아래와 같다.
    • 코드 블록에서는 범위 안의 지역변수를 읽고 수정할 수 있지만, 람다에서는 final이거나 사실상 final 변수만 읽을 수 있고, 지역 변수를 수정할 수 없다.
    • 코드 블록에서는 return문이나 break, continue로 반복문을 제어하고 검사 예외를 던 질 수 있지만, 람다는 모두 불가능하다.
  • 아래의 경우 중 하나를 수행한다면 스트림을 적용하기 좋다
    • 원소들의 시퀀스를 일관되게 변환한다.
    • 원소들의 시퀀스를 필터링한다.
    • 원소들의 시퀀스를 하나의 연산을 사용해 결합한다.(더하기, 연결하기, 최솟값 구하기 등)
    • 원소들의 시퀀스를 컬렉션에 모은다.(아마도 공통된 속성을 기준으로 묶는다)
    • 원소들의 시퀀스에서 특정 조건을 만족하는 원소를 찾는다.

 

스트림으로 처리하기 어려운 작업

  • 파이프라이느이 여러 단계를 통과할 때 이 데이터의 각 단계에서의 값들에 동시에 접근하기 어렵다.
  • 스트림 파이프라인은 한 값을 다른 값에 매핑하고 나면 원래의 값을 잃는 구조이기 때문

 

 

정리

  • 스트림과 반복 방식 둘 다 알맞게 사용해야한다.
  • 이 둘을 적절히 조합하면 좋다.
  • 스트림과 반복 중 어느 쪽이 나은지 확신하기 어렵다면 둘 다 해보고 더 나은쪽을 택하자.
반응형

'BE > Java' 카테고리의 다른 글

[Effective Java] 아이템 47. 반환 타입으로는 스트림보다 컬렉션이 낫다.  (0) 2022.11.08
[Effective Java] 아이템 46. 스트림에서는 부작용 없는 함수를 사용하라  (0) 2022.11.07
[Effective Java] 아이템 44. 표준 함수형 인터페이스를 사용하라  (0) 2022.11.03
[Effective Java] 아이템 43. 람다보다는 메서드 참조를 사용하라  (0) 2022.11.02
[Effective Java] 아이템 42. 익명 클래스보다는 람다를 사용하라  (0) 2022.11.01
  1. 아이템 45. 스트림은 주의해서 사용하라
  2. 스트림
  3. 스트림 파이프라인
  4. 스트림 API
  5. char 스트림
  6.  
  7. 유의점
  8. 함수 객체 vs 반복 코드
  9. 스트림으로 처리하기 어려운 작업
  10. 정리
'BE/Java' 카테고리의 다른 글
  • [Effective Java] 아이템 47. 반환 타입으로는 스트림보다 컬렉션이 낫다.
  • [Effective Java] 아이템 46. 스트림에서는 부작용 없는 함수를 사용하라
  • [Effective Java] 아이템 44. 표준 함수형 인터페이스를 사용하라
  • [Effective Java] 아이템 43. 람다보다는 메서드 참조를 사용하라
멍목
멍목
개발 관련 새롭게 알게 된 지식이나 좋은 정보들을 메모하는 공간입니다.
반응형
멍목
김멍목의 개발블로그
멍목
전체
오늘
어제
  • 분류 전체보기 (514)
    • BE (190)
      • Spring (21)
      • Java (141)
      • Kotlin (6)
      • JPA (22)
    • FE (33)
      • Javascript (16)
      • Typescript (0)
      • React (5)
      • Vue.js (9)
      • JSP & JSTL (3)
    • DB (32)
      • Oracle (22)
      • MongoDB (10)
    • Algorithm (195)
    • Linux (8)
    • Git (6)
    • etc (42)
    • ---------------------------.. (0)
    • 회계 (4)
      • 전산회계 2급 (4)
    • 잡동사니 (2)

블로그 메뉴

  • 홈
  • 관리

공지사항

인기 글

태그

  • 알고리즘 공부
  • 자기 개발
  • 자기 공부
  • java 8
  • MongoDB with Node.js
  • 더 자바 Java 8
  • JPA 공부
  • 코테 공부
  • 자바 개발자를 위한 코틀린 입문
  • MongoDB 공부
  • 자기개발
  • 프로젝트로 배우는 Vue.js 3
  • 코테공부
  • 자기공부
  • 알고리즘공부
  • 자바공부
  • vue3 공부
  • 이펙티브자바
  • Java to Kotlin
  • 전산회계 2급 준비
  • 더 자바 애플리케이션을 테스트하는 다양한 방법
  • 코틀린
  • junit5
  • 이펙티브 자바
  • JPA
  • Effective Java
  • Oracle
  • 자바 테스팅 프레임워크
  • MongoDB 기초부터 실무까지
  • 자바 공부

최근 댓글

최근 글

hELLO · Designed By 정상우.v4.2.0
멍목
[Effective Java] 아이템 45. 스트림은 주의해서 사용하라
상단으로

티스토리툴바

개인정보

  • 티스토리 홈
  • 포럼
  • 로그인

단축키

내 블로그

내 블로그 - 관리자 홈 전환
Q
Q
새 글 쓰기
W
W

블로그 게시글

글 수정 (권한 있는 경우)
E
E
댓글 영역으로 이동
C
C

모든 영역

이 페이지의 URL 복사
S
S
맨 위로 이동
T
T
티스토리 홈 이동
H
H
단축키 안내
Shift + /
⇧ + /

* 단축키는 한글/영문 대소문자로 이용 가능하며, 티스토리 기본 도메인에서만 동작합니다.