스트림은 주의해서 사용하라
스트림 API는 다량의 데이터 처리작업을 돕고자 나왔다. 이 API의 핵심 개념은 다음과 같다.
스트림 파이프라인은 소스 스트림, 중간 연산(없을 수도), 종단 연산으로 구성된다.
중간 연산
각 원소에 함수 적용을 하거나 특정 조건을 만족 못하는 원소를 걸러낼 수 있다. 한 스트림을 다른 스트림으로 변환하는데 타입은 변환 전/후 다를 수도, 같을 수도 있다.
종단 연산
중간 연산이 내놓은 스트림에 최후 연산을 수행한다. 원소를 정렬해 컬렉션에 담거나, 특정 원소 하나를 선택하거나 모든 원소를 출력할 수 있다.
평가는 종단 연산이 호출될 때 이뤄지며, 종단 연산에 쓰이지 않는 데이터 원소는 계산에 쓰이지 않는다. 즉, 종단 연산이 없는 스트림 파이프라인은 아무 일도 하지 않는 명령어와 같다.
메서드 연쇄를 지원한다. 즉, 파이프라인 하나를 구성하는 모든 호출을 연결해 단 하나의 표현식으로 완성할 수 있다.
파이프라인은 기본적으로 순차실행이며, parallel
메서드를 사용해 병렬적으로 실행할 수 있다. 하지만 이는 효과를 볼 수 있는 상황이 많지 않다 (아이템 48)
아래는 사전 파일에서 단어를 읽어 사용자가 지정한 값보다 원소 수가 많은 아나그램(철자를 구성하는 알파벳이 같고 순서만 다른 단어) 그룹을 출력하는 코드다.
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);
}
}
위 코드를 간략하게 스트림으로 나타낸다면, 아래와 같다.
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(g -> System.out.println(g.size() + ": " + g)); //종단연산
}
}
private static String alphabetize(String s) {
char[] a = s.toCharArray();
Arrays.sort(a);
return new String(a);
}
}
위에서 alphabetize 또한 스트림으로 나타낼 수 있겠지만, char용 스트림을 지원하지 않아 형변환이 필요하다. (따라서 삼가는 편이 낫다.)
함수 객체(람다나 메서드 참조)로 할 수 없는, 반복 코드로 나타낼 수 있는 예시는 다음과 같다.
final
만 읽을 수 있다.스트림이 안성맞춤인 경우는 다음과 같다.
스트림 연산은 한 값을 다른 값으로 매핑한다면 원래 값을 잃는다. 따라서 스트림 파이프라인의 여러 단계를 통과할 때, 각 단계에서의 값들에 동시 접근하기는 어려운 경우 스트림을 처리하기 힘들다.
static Stream<BigInteger> primes() {
return Stream.iterate(TWO, BigInteger::nextProbablePrime);
}
public static void main(String[] args) {
primes().map(p -> TWO.pow(p.intValueExact()).subtract(ONE)) // p를 2^p-1로 변환
.filter(mersenne -> mersenne.isProbablePrime(50))
.limit(20)
.forEach(System.out::println);
}
p가 소수이면서 2^p - 1
도 소수라면 메르센 소수다. 이때 p를 최종적으로 출력하는 코드를 작성하고자 한다면 이미 파이프라인을 통과했기에 초기 값을 꺼내오긴 힘들다.
Suit와 Rank의 조합을 나타내는 코드다.
//반복문
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;
}
//Stream
private static List<Card> newDeck() {
return Stream.of(Suit.values())
.flatMap(suit ->
Stream.of(Rank.values())
.map(rank -> new Card(suit, rank)))
.collect(toList());
}
위는 정말 취향차이다.. 따라서 숙련도에, 본인이 더 편리하다고 생각하는 방향으로 작성하자 !