반응형
이 포스팅에서 작성하는 내용은 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 |