[아이템 45] 스트림은 주의해서 사용하라
2023. 5. 17. 08:46ㆍJava/Effective Java
스트림 API
- 스트림 API는 다량의 데이터 처리 작업(순차적이든 병렬적이든)을 돕고자 자바8에 추가되었다.
- 이 API가 제공하는 추상 개념 중 핵심은 두 가지다.
- 스트림(Stream)은 데이터 원소의 유한 혹은 무한 시퀀스(sequence)를 의미
- 스트림 파이프라인(Stream Pipeline)은 이 원소들로 수행하는 연산단계를 표현하는 개념
- 스트림 안의 데이터 원소들은 객체 참조(reference)나 기본 타입(int, long, double)을 지원
- 메서드 연쇄를 지원하는 플루언트 API다.
- 파이프라인 하나를 구성하는 모든 호출을 연결하여 단 하나의 표현식으로 완성할 수 있다.
- 파이프라인 여러 개를 연결해 표현식 하나로 만들 수 있다.
스트림 파이프라인
- 소스 트림에서 시작해 종단 연산으로 끝나며, 그 사이에 하나 이상의 중간 연산이 있을 수 있다.
- 각 중간 연산은 스트림을 어떠한 방식으로 변환한다.
- 스트림 파이프라인은 지연 평가 된다.
- 평가는 종단 연산이 호출될 때 이루어지며, 종단 연산에 쓰이지 않은 데이터 원소는 계산에 쓰이지 않는다
- 이러한 지연 평가가 무한 스트림을 다룰 수 있게 해주는 열쇠
Stream 예제 - 아나그램(anagram)
1) 일반 loop를 이용한 코드
public static void main(String[] args) {
File dectionary = new File(args[0]);
int minGroupSize = Integer.parseInt(args[1]);
Map<String, Set<String>> groups = new HashMap<>();
try (Scanner s = new Scanner(dectionary)) {
while(s.hasNext()) {
String word = s.next();
groups.computeIfAbsent(alphabetize(word),
(unused) -> new TreeSet<>()).add(word);
}
} catch (FileNotFoundException e) {
e.printStackTrace();
}
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);
}
2) Stream을 과도하게 쓴 코드
- 스트림을 과용하면 프로그램이 읽거나 유지보수 하기 어려워진다.
public static void main(String[] args) throws IOException {
File dectionary = new File(args[0]);
int minGroupSize = Integer.parseInt(args[1]);
try(Stream<String> words = Files.lines(dectionary.toPath())) {
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);
}
}
3) Stream을 적절히 활용한 코드
- 스트림을 적절하게 사용하면 명료해진다.
- 람다에서는 타입 추론 기능을 사용하기 때문에 주로 타입 이름을 생략한다. 따라서 매개변수 이름을 잘 지어야 스트림 파이프라인의 가독성이 유지된다.
- 연산에 적절한 이름을 지어 주고 도우미 메서드를 적정히 활용하는 것은 일반 반복코드보다 스트림 파이프라인에서 훨씬 크다
public static void main(String[] args) throws IOException {
File dectionary = new File(args[0]);
int minGroupSize = Integer.parseInt(args[1]);
try(Stream<String> words = Files.lines(dectionary.toPath())) {
words.collect(groupingBy(word -> alphabetize(word)))
.values().stream()
.filter(group -> group.size() >= minGroupSize)
.forEach(group -> System.out.println(group.size() + ": " + group));
}
}
코드 블럭 vs 람다
- 코드블럭에서는 범위 안의 지역변수를 읽고 수정할 수 있다.
- 람다에서는 final이거나 사실상 final인 변수만 읽을 수 있다. 지역 변수를 수정하는 것은 불가능하다.
- 코드 블럭에서는 return을 이용해 메서드를 빠져나가 거나 break, continue문을 통해 블럭 바깥의 반복문을 종료 할 수 있다.
- 메서드 선언에 명시된 예외(Exception)을 던질 수 있다.
- 람다로는 모든 것이 불가능하다.
스트림이 적합한 경우
- 원소들의 시퀀스를 일관되게 변환한다.
- 원소들의 시퀀스를 필터링한다.
- 원소들의 시퀀스를 하나의 연산을 사용해 결합한다. (더하기, 연결하기, 최소값 등..)
- 원소들의 시퀀스를 컬렉션에 모은다
- 원소들의 시퀀스에서 특정 조건을 만족하는 원소를 찾는다.
요약
- 스트림과 반복 중 어느 쪽이 나은지 확신하기 어렵다면 둘 다 해보고 더 나은 쪽을 택하는 것이 좋다.
'Java > Effective Java' 카테고리의 다른 글
[아이템 47] 반환 타입으로는 스트림보다 컬렉션이 낫다 (0) | 2023.05.31 |
---|---|
[아이템 46] 스트림에서는 부작용 없는 함수를 사용하라 (0) | 2023.05.30 |
[아이템 43] 람다보다는 메서드 참조를 사용하라 (0) | 2023.05.13 |
[아이템 42] 익명 클래스보다는 람다를 사용하라 (0) | 2023.05.12 |
[아이템 5] 자원을 직접 명시하지 말고 의존 객체 주입을 사용하라 (0) | 2021.03.23 |