이펙티브 자바, 쉽게 정리하기 - item 45. 스트림은 주의해서 사용하라
스트림의 특징
아래에서 사용하는
평가(evaluation)
란 용어를 사용한다. 이 용어에 대한 설명은 Java Stream API 에서 평가 (evaluation) 란? 포스팅을 참조하면 된다.
- 배열과 같은 시퀀스형 데이터를 처리하는데 특화되어 있다.
소스 스트림(source stream)
->중간 연산(intermediate operation)
->종단 연산(terminal operation)
순으로 진행된다.- 각
중간 연산(intermediate operation)
은 스트림을 어떠한 방식으로변환(transform)
한다.
- 각
- 스트림 파이프라인은 지연평가되며, 평가는
종단 연산(terminal operation)
이 호출될 때 이뤄진다.- 종단 연산이 쓰이지 않는 데이터 원소는 계산에 쓰이지 않는다.
- 종단 연산이 없는 스트림 파이프라인은 아무 일도 하지 않는 명령어인
no-op
과 같다.
스트림은 병렬 연산을 지원하지만 실제 효용성이 있는 경우는 적다.
아나그램의 예로 살펴보기
- 아나그램이란, 스펠링의 위치만 바꿔서 다른 단어를 만들 수 있는 단어를 말한다.
- 이를테면
stop
은s
,t
,o
,p
인데 위치만 바꾸면s
,p
,o
,t
을 만들 수 있다.
스트림을 사용하지 않은 버전의 아나그램 처리
public class Item45Test {
@Test
public void anagramTest() {
List<String> words = new ArrayList<>();
words.add("stop");
words.add("spot");
words.add("trim");
words.add("meet");
words.add("ball");
words.add("free");
Map<String, Set<String>> groups = new HashMap<>();
// 그룹핑하기
for (String word : words) {
groups.computeIfAbsent(alphabetize(word), (unused) -> new TreeSet<>()).add(word);
}
// 필터링하기
for (Set<String> group : groups.values()) {
if(group.size() >= 2) {
System.out.println(group.size() + ": " + group);
}
}
}
private String alphabetize(String s) {
char[] a = s.toCharArray();
Arrays.sort(a);
return new String(a);
}
}
- 단어를 알파벳으로 쪼갠 뒤 알파벳을 정렬하고 정렬된 알파벳을 기준으로 그룹핑했다.
- ex.
stop
->['o', 'p', 's', 't']
->opst
- ex.
spot
->['o', 'p', 's', 't']
->opst
- 둘은 같은 그룹에 묶인다.
- ex.
- 그룹 내 원소의 숫자가 2개 이상이라면 출력한다.
스트림을 사용한 버전 (과용)
@Test
public void anagramTest2() {
List<String> words = new ArrayList<>();
words.add("stop");
words.add("spot");
words.add("trim");
words.add("meet");
words.add("ball");
words.add("free");
words.stream().collect(
Collectors.groupingBy(
word -> word.chars().sorted()
.collect(StringBuilder::new, (sb, c) -> sb.append((char) c), StringBuilder::append).toString()))
.values().stream()
.filter(group -> group.size() >= 2)
.map(group -> group.size() + ": " + group)
.forEach(System.out::println);
}
private String alphabetize(String s) {
char[] a = s.toCharArray();
Arrays.sort(a);
return new String(a);
}
alphabetize()
함수를 사용하지 않고, 내부에서 어떻게든 처리했다.- 적절하게 코드의 캡슐화가 이뤄지지 않아 스트림을 사용하지 않았을 때보다 코드를 읽기 힘들어졌다.
스트림을 사용한 버전 (적절)
@Test
public void anagramTest3() {
List<String> words = new ArrayList<>();
words.add("stop");
words.add("spot");
words.add("trim");
words.add("meet");
words.add("ball");
words.add("free");
words.stream().collect(
Collectors.groupingBy(this::alphabetize))
.values()
.stream()
.filter(group -> group.size() >= 2)
.map(group -> group.size() + ": " + group)
.forEach(System.out::println);
}
- 위의 버전에서
alphabetize()
를 메서드화 시켜 적절한 캡슐화를 진행했다.- 의미를 가진 스트림 내부 람다 코드를 메서드로 추출함으로써 코드의 추상수준이 높아지고 코드가 깔끔해졌다.
- 메서드 체이닝 형식이 보기 깔끔하다.
- 여러
for
블럭이 있을 때보다 결과가 연계된다는 것이 확실히 잘 느껴진다. - 취향에 따라 다르지만 코드가 더 깔끔해보인다.
- 여러
사실
char
값을 처리할 때는 스트림을 삼가는게 좋다.char
는 그냥 출력하면int
가 출력되고,(char)
를 통해 캐스팅해야만 문자열이 나와 다루기 까다롭고char
를 이용하면 코드가 더러워질 확률이 높아진다.
람다 혹은 스트림을 사용하면 안되는 경우
- 람다 외부 지역변수에 접근해야 한다면,
final
만 읽을 수 있으므로 사용하지 않는 게 좋다. return
,break
,continue
처럼 중간에 작업을 끊어야 한다면 사용하지 않는 게 좋다.- 처리 과정 중 이전 단계의 값에 접근해야 하는 경우 사용하지 않는 게 좋다.
람다 혹은 스트림이 권장되는 경우
시퀀스를 연계해서 처리하는 로직을 만드려고 하는 경우에 적합하다.
- 원소들의 시퀀스를 일관되게 변경한다. (
map()
) - 원소들의 시퀀스를 필터링한다. (
filter()
) - 원소들의 시퀀스를 하나의 연산을 사용해 결합한다. (
collect
) - 원소들의 시퀀스를 컬렉션에 모은다. (
collect
) - 원소들의 시퀀스에서 특정 조건을 만족하는 원소를 찾는다.
메르센 소수 예제로 살펴보기
private Stream<BigInteger> primes() {
return Stream.iterate(TWO, BigInteger::nextProbablePrime);
}
@Test
public void mersenne() {
primes()
.map(p -> TWO.pow(p.intValueExact()).subtract(ONE))
.filter(mersenne -> mersenne.isProbablePrime(50))
.limit(10)
.forEach(System.out::println);
// .forEach(mp -> System.out.println(mp.bitLength() + ": " + mp));
// p를 알고 싶을 때
}
- 메르센 소수란
2^p - 1
에서p
가 소수일 때 해당 메르센 소수도 소수일 수 있는데 이 때의 수를 메르센 소수라고 한다. - 위는 처음 10개의 메르센 소수를 구하는 것을 스트림으로 구현해본 것이다.
데카르트 곱 예제로 살펴보기
스트림을 쓰지 않았을 때
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;
}
스트림을 썼을 때
private static List<Card> newDeck() {
return Stream.of(Suit.values())
.flatMap(suit -> Stream.of(Rank.values())
.map(rank -> new Card(suit, rank)))
.collect(toList());
}
- 중첩된
for
문보다 한결 생각하기 쉬워진다.
핵심 정리
- 스트림과 반복 중 어느쪽이 나은지 생각해보고, 확신하기 어렵다면 둘 다 해보고 나은 쪽을 선택하자.
- 함께 코드를 작성해나갈 동료들의 수준도 잘 고려하자.
반응형
'Java > 이펙티브 자바' 카테고리의 다른 글
이펙티브 자바, 쉽게 정리하기 - item 46. 스트림에서는 부작용 없는 함수를 사용하라 (0) | 2023.03.30 |
---|---|
이펙티브 자바, 쉽게 정리하기 - item 44. 표준 함수형 인터페이스를 사용하라 (0) | 2023.03.29 |
이펙티브 자바, 쉽게 정리하기 - item 43. 람다보다는 메서드 참조를 사용하라 (2) | 2022.06.13 |
이펙티브 자바, 쉽게 정리하기 - item 42. 익명 클래스보다는 람다를 사용하라 (0) | 2022.06.13 |
이펙티브 자바, 쉽게 정리하기 - item 41. 정의하려는 것이 타입이라면 마커 인터페이스를 사용하라 (0) | 2022.05.24 |