2023. 12. 21. 23:02ㆍBook/이펙티브 자바
스트림 API는 다재다능하여 거의 모든 계산을 해낼 수 있다. 하지만 할 수 있다는 뜻이지 해야 한다는 뜻은 아니다.
스트림은 제대로 사용하면 프로그램이 짧고 깔끔해지지만, 잘못 사용하면 읽기 어렵고 유지보수도 힘들어진다. 스트림을 언제 써야 하는지를 규정하는 확고부동한 규칙은 없지만, 참고할 만한 노하우는 있다.
다음 코드를 보자
public class Anagrams {
public static void main(String[] args) {
String[] strings = {"abs", "bsa", "dds", "sdd", "cad", "acd", "dul", "sba"};
int minGroupSize = 2;
Map<String, Set<String>> groups = new HashMap<>();
Iterator<String> s = Arrays.stream(strings).iterator();
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);
}
}
=>
2: [dds, sdd]
2: [acd, cad]
3: [abs, bsa, sba]
위 코드는 strings 배열에서 문자열을 읽어 사용자가 지정한 문턱값보다 원소 수가 많은 아나그램 그룹을 출력한다.
맵에 각 단어를 삽입할 때 자바 8에서 추가된 computeIfAbsent 메서드를 사용했다. 이 메서드는 맵 안에 키가 있는지 찾은 다음, 있으면 단순히 그 키에 매핑된 값을 반환한다. 키가 없으면 건네진 함수 객체를 키에 적용하여 값을 계산해낸 다음 그 키와 값을 매해놓고, 계산된 값을 반환한다.
이제 다음 코드를 보자 스트림을 과하게 사용한 예이다.
public class Anagrams {
public static void main(String[] args) {
String[] strings = {"bsa", "abs", "dds", "sdd", "cad", "acd", "dul", "sba"};
int minGroupSize = 2;
Stream<String> words = Arrays.stream(strings);
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);
}
}
코드를 이해하기 어려울것이다. 스트림을 과용하면 프로그램이 읽거나 유지보수하기 어려워진다. 조금 풀어서보자
Function<String, String> function = (word) -> word.chars().sorted()
.collect(StringBuilder::new,
(sb, c) -> sb.append((char) c),
StringBuilder::append).toString();
words.collect(
groupingBy(function))
.values().stream()
.filter(group -> group.size() >= minGroupSize)
.map(group -> group.size() + ": " + group)
.forEach(System.out::println);
}
문자열을 받아 알파벳순으로 소팅하고 그 소팅된 값이 키, 그루핑된 List<String>값이 value로 Map 객체가 만들어진다.
다음은 적절히 스트림을 사용한 예이다.
public class Anagrams {
public static void main(String[] args) {
String[] strings = {"abs", "bsa", "dds", "sdd", "cad", "acd", "dul", "sba"};
int minGroupSize = 2;
Stream<String> words = Arrays.stream(strings);
words.collect(
groupingBy(Anagrams::alphabetize))
.values().stream()
.filter(group -> group.size() >= minGroupSize)
.forEach(System.out::println);
}
private static String alphabetize(String s) {
char [] a = s.toCharArray();
Arrays.sort(a);
return new String(a);
}
}
이 스트림의 파이프라인에는 중간 연산은 없고 종단 연산에서 모든 단어를 수집해 맵으로 모은다. 이 맵은 단어들을 아나그램끼리 묶어놓은 것으로 앞선 두 프로그램이 생성한 맵과 실질적으로 같다.
이번 아이템에서 보여준 프로그램에서처럼 스트림 파이프라인은 되풀이되는 계산을 함수 객체로 표현한다.
반면 반복 코드에서는 코드 블록을 사용해 표현한다.
함수 객체로는 할 수 없지만 코드 블록으로는 할 수 있는 일들이 있으니, 다음이 그 예다.
- 코드 블록에서는 범위 안의 지역변수를 일고 수정할 수 있다. 하지만 람다에서는 final이거나 사실상 final인 변수만 읽을 수 있고, 지역 변수를 수정하는 건 불가능하다.
- 코드 블록에서는 return문을 사용해 메서드에서 빠져나가거나, break나 continue 문으로 블록 바깥의 반복문을 종료하거나 반복을 한 번 건너뛸 수 있다. 또한 메서드 선언에 명시된 검사 예외를 던질 수 있다. 하지만 람다는 이 중 어떤것도 할 수 없다.
반대로 다음 일들에는 스트림이 좋다.
- 원소들의 시퀀스를 일관되게 변환한다.
- 원소들의 시퀀스를 필터링한다.
- 원소들의 시퀀스를 하나의 연산을 사용해 결합한다.
- 원소들의 시퀀스를 컬렉션에 모은다.
- 워소들의 시퀀스에서 특정 조건을 만족하는 워소를 찾는다.
'Book > 이펙티브 자바' 카테고리의 다른 글
Item50 - 적시에 방어적 복사본을 만들라 (0) | 2023.12.27 |
---|---|
Item49 - 매개변수가 유효한지 검사하라 (1) | 2023.12.26 |
Item43 - 람다보다는 메서드 참조를 사용하라 (0) | 2023.12.19 |
Item42 - 익명 클래스보다는 람다를 사용하라 (1) | 2023.12.18 |
Item41 - 정의하려는 것이 타입이라면 마커 인터페이스를 사용하라 (0) | 2023.12.17 |