-
메서드 연쇄를 지원하는 플루언트 API(fluent API)
-
즉, 파이프라인 하나를 구성하는 모든 호출을 연결하여 단 하나의 표현식으로 완성할 수 있음
-
파이프라인 여러 개를 연결해 표현식 하나로 만들 수도 있음
-
-
기본적으로 스트림 파이프라인은 순차적으로 수행됨
-
파이프라인을 병렬로 실행하려면 파이프라인을 구성하는 스트림 중 하나에서 parallel 메서드를 호출해주면 됨
-
그러나 효과를 볼 수 있는 상황은 많지 않음
-
-
스트림 API는 다재다능하여 사실상 어떠한 계산이라도 해낼 수 있음
-
하지만 할 수 있다는 뜻이지, 해야한다는 뜻은 아님
-
스트림을 제대로 사용하면 프로그램이 짧고 깔끔해짐
-
잘못 사용하면 읽기 어렵고 유지보수도 힘들어짐
-
-
-
스트림을 언제 써야하는지를 규정하는 확고부동한 규칙은 없음
- 그래도 참고할만한 노하우는 있음
예시 : 사전 파일에서 단어를 읽어 사용자가 지정한 문턱값보다 원소 수가 많은 아나그램(anagram)*
그룹을 출력하는 프로그램
아나그램
철자를 구성하는 알파벳이 같고 순서만 다른 단어들
ex) staple, petals -> aelpst이 프로그램에서 사용되는 아나그램 그룹
자료구조 : 맵
맵의 키 : 단어를 구성하는 철자들을 알파벳순으로 정렬한 값 ex) aelpst
맵의 값 : 같은 키를 공유한 단어들을 담은 집합 ex) staple, petals
public class Anagrams {
public static void main(String[] args) throws IOException {
File dictionary = new File(args[0]); // 사전 파일
int minGroupSize = Integer.parseInt(args[1]); // 사용자가 지정한 원소 수 문턱값
// key : 알파벳 순으로 정렬한 값, value : 같은 키를 공유한 단어들을 담은 집합
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);
}
}
//minGroupSize 보다 원소 수가 많은 아나그램 그룹 출력
for (Set<String> group : groups.values()) {
if (group.size() >= minGroupSize) {
System.out.println(group.size() + " : " + group);
}
}
}
// 아나그램 그룹의 key를 만들어줌
private static String alphabetize(String s) {
char[] a = s.toCharArray();
Arrays.sort(a);
return new String(a);
}
}
-
JAVA 8에 추가된 computeIfAbsent 메서드 사용
-
public V computeIfAbsent(K key, Function<? super K, ? extends V>)
-
key 값이 존재하는 경우 : Map 안에 있는 value 반환
-
key 값이 존재하지 않는 경우 : Map 에 새로운 key 와 value(람다 함수 실행 결과) 반환
-
-
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)) { // try-with-resources 문을 사용해 파일을 닫음
// 사전을 여는 부분을 제외하고 프로그램 전체가 단 하나의 표현식으로 처리
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);
}
}
}
-
확실히 짧지만 읽기가 어려움
-
스트림에 익숙하지 않은 프로그래머에게 더욱 어려움
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)) { // try-with-resources 문을 사용해 파일을 닫음
words.collect(groupingBy(Anagrams::alphabetize)) // alphabetize 메서드로 단어들을 그룹화함
.values().stream()
.filter(group -> group.size() >= minGroupSize) // 문턱값보다 작은 것을 걸러냄
.forEach(g -> System.out.println(g.size() + " : " + g)); // 필터링이 끝난 리스트 출력
}
}
// 아나그램 그룹의 key를 만들어줌
private static String alphabetize(String s) {
char[] a = s.toCharArray();
Arrays.sort(a);
return new String(a);
}
}
-
스트림을 적절히 활용하면 깔끔하고 명료해짐
-
람다의 매개변수 이름을 잘 지어야 파이프라인의 가독성이 유지됨
-
람다에서 타입 이름을 자주 생략하기 때문
-
forEach() 안 g의 경우 group이라고 변경하는 것이 좋음
-
-
도우미 메서드를 적절히 활용해야함
-
파이프라인에서는 타입 정보가 명시되지 않거나 임시 변수를 자주 사용하기 때문
-
alphabetize : 단어의 철자를 알파벳 순으로 정렬하는 별도의 메서드 생성
- 스트림으로 구현했다면 명확성이 떨어지고 잘못 구현될 가능성이 커짐
-> 자바가 char용 스트림을 지원하지 않기 때문
- 스트림으로 구현했다면 명확성이 떨어지고 잘못 구현될 가능성이 커짐
-
스트림 파이프라인 : 함수 객체(주로 람다나 메서드 참조)로 표현함
반복 코드 : 코드블록을 사용해 표현
-
범위 안의 지역변수를 읽고, 수정할 수 있음
- 🙅♀️ 람다에서는 final이거나 사실상 final인 변수만 읽을 수 있고, 지역 변수를 수정하는건 불가능함
-
코드 블록에서는
-
return을 사용해 메서드에서 빠져나갈 수 있음
-
break나 continue 문으로 블록 바깥의 반복문을 종료하거나 반복을 한 번 건너뛸 수 있음
-
메서드 선언에 명시된 검사 예외를 던질 수 있음
-
🙅♀️ 람다로는 이 중 아무것도 할 수 없음
-
-
원소들의 시퀀스를 일관되게 변환하기
-
원소들의 시퀀스를 필터링하기
-
원소들의 시퀀스를 하나의 연산(더하기, 연결하기, 최솟값 구하기 등)을 사용해 결합하기
-
원소들의 시퀀스를 컬렉션에 모으기
-
원소들의 시퀀스에서 특정 조건을 만족하는 원소 찾기
-
파이프라인의 여러 단계에서의 값들에 동시에 접근하기
- 스트림 파이프라인은 일단 한 값을 다른 값에 매핑하고 나면 원래의 값은 잃는 구조이기 때문
스트림과 반복 방식은 각각에 알맞은 일이 있음
수 많은 작업은 이 둘을 조합했을 때 가장 멋지게 해결됨
🌟 스트림과 반복 중 어느 쪽이 나은지 확신하기 어렵다면 둘 다 해보고 더 나은 쪽을 택하라