[Effective Java] 7 - item45 스트림은 주의해서 사용하라
Stream API
다량의 데이터 처리 작업을 위해 Java 8부터 추가되었다.
Stream API는 스트림과 스트림 파이프라인의 2가지 추상 개념으로 구성되며, 메서드 연쇄를 지원하는 fluent API다.
스트림
데이터 원소의 유한한 / 무한한 시퀀스
- 원소들은 어디에서든 올 수 있다. ex) 컬렉션, 배열, 파일, regex matcher, 난수 생성기, 다른 스트림 등…
- 내부 테이터 원소들은 객체 참조나 기본 타입 값(
int
,long
,double
)이다.
스트림 파이프라인
스트림 원소들로 수행하는 연산 단계
- 소스 스트림에서 시작해, 하나 이상의 중간 연산을 거쳐 종단 연산으로 끝난다.
- 각 중간 연산은 한 스트림을 다른 스트림으로 변환할 수 있다.
- 지연 평가 : 평가는 종단 연산이 호출될 때 이루어진다.
- 순차적으로 수행된다.
사용 예시
제대로 사용하면 프로그램이 짧고 깔끔해지지만, 잘못 사용하면 가독성이 떨어지고 유지보수 또한 힘들다.
다음은 반복문을 사용해 File I/O로 Anagram을 구현한 예제이고, 출력은 다음과 같다.
public class Anagrams {
public static void main(String[] args) {
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);
}
} catch (FileNotFoundException e) {
e.printStackTrace();
}
for(Set<String> group : groups.values()) {
if(group.size() >= minGroupSize) {
System.out.println(group.size() + ": " + group);
}
}
}
static String alphabetize(String s) {
char[] a = s.toCharArray();
Arrays.sort(a);
return new String(a);
}
}
동일한 프로그램을 Stream API를 사용해 구현할 수 있다.
다음은 Stream을 과하게 사용한 예시이다.
public class StreamAnagram {
public static void main(String[] args) throws IOException {
File dectionary = new File(args[0]);
int minGroupSize = Integer.parseInt(args[1]);
// depth가 2 이상이 될 경우, 코드가 장황해지고 이해하기 어렵다.
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);
}
}
}
public class NewAnagram {
public static void main(String[] args) throws IOException {
File dectionary = new File(args[0]);
int minGroupSize = Integer.parseInt(args[1]);
// depth가 최대 2를 넘어가지 않도록 코드를 작성하였다.
// alphabetize() 함수를 사용해 가독성을 높였다.
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 Stream
반복문은 반복문 내의 코드 블록을 사용해 표현하고, Stream 파이프라인은 되풀이되는 계산을 함수 객체 (주로 람다 혹은 메서드 참조)로 표현한다.
반복문에서는 다음과 같은 기능을 수행할 수 있다.
- 범위 내 지역변수를 읽거나 수정할 수 있다.
- 람다에서는
final
이거나 사실상final
에 가까운 변수만 읽어올 수 있다. 수정은 불가능하다.
- 람다에서는
return
,break
,continue
등을 사용할 수 있다.- 람다 혹은 메서드 참조에서는 불가능하다.
반면 Stream은 원소들의 시퀀스에서 다음과 같은 기능을 한다.
- 필터링
- 일관되게 변환
- 하나의 연산을 사용해 결합 (최대•최솟값)
- 컬렉션에 모음
- 특정 조건 만족하는 원소 찾기
결론
Stream API와 반복문 각각의 장단점이 있으므로 이에 맞게 구현 방식을 선택하자. 어떤 방식으로 구현해도 상관 없는 경우에는, 보다 이해도가 높고 선호하는 방식을 따르자.
Leave a comment