핵심 요약
Stream과 반복 방식 중 어떤 방식이 좋을지 잘 고려해서 사용하자.
Stream의 특징
- 스트림 API는 다량의 데이터 처리 작업을 위해 자바 8에 추가되었다.
- 소스 스트림(source stream) ➡ 중간 연산(intermediate operation) ➡ 종단 연산(terminal operation) 순으로 진행된다.
- 중간 작업은 filter, map, sorted와 같이 다른 스트림으로 변환하는 작업이다.
- 터미널 작업은 count, collect, forEach, reduce와 같은 결과를 생성한다.
- 대용량 데이터를 처리할 때 효율을 높이기 위해, 오토박싱/언박싱 과정이 필요 없는 기본형 스트림도 제공한다.
- ex. IntStream, LongStream, DoubleStream
- 스트림 파이프 라인은 지연 평가(lazy evaluation)된다.
- 지연 평가는 스트림 파이프라인에서 중간 연산은 종단 연산이 호출될 때까지 실행되지 않는 것을 말한다.
- 즉, 스트림에서 종단 연산을 호출하지 않으면 실제로 처리가 발생하지 않는다. 또한 결과에 필요한 요소만 처리한다.
- 최종 계산에 필요하지 않은 데이터 요소(ex. 필터링된 요소)는 계산되지 않는다.
- 따라서 대규모 데이터 세트를 처리할 때 성능이 향상될 수 있다.
- 병렬 처리에 대한 지원 기능이 내장되어 있다. (기본적으로는 순차적으로 수행된다.)
- CPU의 여러 코어를 활용하여 여러 스레드에서 계산 작업을 분할할 수 있다.
- 그러나 성능 향상이 항상 보장되는 것은 아니므로 신중하게 사용해야 한다.
- 여러 스레드를 관리하는 오버헤드는 때대로 성능을 악화시키기 때문이다.
- 효과를 볼 수 있는 경우가 많지 않다.(아이템 48)
- 적절한 경우 코드의 가독성과 유지 관리 가능성이 향상된다.
스트림
더보기
데이터 원소의 유한 혹은 무한 시퀀스
스트림 파이프라인
더보기
원소들로 수행하는 연산 단계
stream operation 요약

더 다양한 메소드는 아래 자바 공식 문서를 참고하면 된다.
Stream (Java SE 20 & JDK 20)
Type Parameters: T - the type of the stream elements All Superinterfaces: AutoCloseable, BaseStream > A sequence of elements supporting sequential and parallel aggregate operations. The following example illustrates an aggregate operation using Stream and
docs.oracle.com
예제 1
: 사전 파일에서 단어를 읽어 사용자가 지정한 문턱값보다 원소 수가 많은 아나그램 그룹을 출력
(ex. "staple"의 키는 "aelpst"가 되고 "petals"의 키도 "aelpst"가 된다.)
➡스트림을 과용하면 프로그램이 읽거나 유지보수하기 어려워진다.
// 스트림을 사용하지 않고 구현
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 static void main(String[] args) throws IOException {
File dectionary = new File(args[0]);
int minGroupSize = Integer.parseInt(args[1]);
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 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))) // 터미널 작업, 모든 단어를 Map으로 수집
.values().stream() // 맵에서 value 모음을 가져오고 새 스트림을 만듬
.filter(group -> group.size() >= minGroupSize) // 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);
}
}
예제 2
: char을 스트림으로 처리하는 코드
➡ char 값들을 처리할 때는 스트림을 삼가는 편이 낫다.
"Hello world!".chars().forEach(System.out::print); // 721011081081113211...
"Hello World!".chars()는 char가 아닌 int를 반환한다.
"Hello world!".chars().forEach(x -> System.out.print((char) x));
위처럼 형변환을 명시적으로 해줘야 한다.
기존 코드는 필요한 경우에만 스트림으로 리팩토링 하자.
함수 객체(람다, 메소드 참조) vs 반복 코드(코드 블록)
코드 블록과 람다의 차이점
- 코드 블록은 범위 내에서 로컬 변수를 읽고 수정할 수 있다.반면, 람다에서는 final이거나 사실상 final인 변수만 읽을 수 있다. 지역변수를 수정하는 것은 불가능하다.
- 코드 블록은 return, break, continue과 예외를 throw할 수 있다.
반면, 람다는 불가능하다.
스트림이 적합한 경우
- 원소들의 시퀀스를 일관되게 변환한다.
- ex. 정수 리스트에서 각 원소를 제곱하는 연산(map)
- 원소들의 시퀀스를 필터링한다.
- ex. 이름 리스트에서 'A'로 시작하는 이름들만 선택(filter)
- 원소들의 시퀀스를 하나의 연산을 사용해 결합한다.
- ex, 숫자 리스트의 모든 원소를 더할 때(reduce)
- 원소들의 시퀀스를 컬렉션에 모은다.
- ex. 리스트를 세트로 변환하고자 할 때(collect)
- 원소들의 시퀀스에서 특정 조건을 만족하는 원소를 찾는다.
- ex. 숫자 리스트에서 짝수를 찾고자 할 때(anyMatch)
스트림이 적합하지 않은 경우
- 데이터가 파이프라인의 여러 단계를 통과할 때 중간 결과에 접근해야 할 경우
- 스트림 파이프라인은 한 값을 다른 값에 매핑하고 나면 원래의 값을 잃는 구조이기 때문이다.
예시 1 - 메르센 소수
static Stream<BigInteger> primes() { // 2부터 다음 확률적 소수를 순차적으로 생성하는 무한 스트림을 반환
return Stream.iterator(TWO, BigInteger::nextProbablePrime);
}
public static void main(String[] args) { // 메르센 소수를 20개를 출력하는 프로그램
primes().map(p -> TWO.pow(p.intValueExact().subtract(ONE))) // 메르센 소수로 변환
.filter(mersenne -> mersenne.isProbablePrime(50)) // 메르센 수가 소수인지 필터링
.limit(20) // 20개만 선택, 무한 스트림이므로 프로그램은 끝나지 않음
.forEach(System.out::println); // forEach(mp -> System.out.println(mp.bitLength() + ": " + mp));
}
메르센 소수를 생성한 후에 이전 단계에서 생성된 소수에 접근할 수 없다.
애매한 경우 예제 - 데카르트 곱
// 데카르토 곱 계산을 반복 방식으로 구현
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()) // Suit 열거형의 모든 값을 담고 있는 스트림 생성
.flatMap(suit -> Stream.of(Rank.values()) // 각 suit에 대해 새로운 스트림을 생성하고 이를 하나의 스트림으로 결합(즉, 52장의 카드가 있는 스트림이 생성)
.map(rank -> new Card(suit, rank)))
.collect(toList()); // 생성된 스트림의 모든 카드를 수집하여 List로 변환
}
'Language > Java' 카테고리의 다른 글
| [effective java] 아이템 47. 반환 타입으로는 스트림보다 컬렉션이 낮다. (0) | 2023.07.03 |
|---|---|
| [effective java] 아이템 46. 스트림에서는 부작용 없는 함수를 사용하라. (0) | 2023.07.03 |
| [effective java] 아이템 44. 표준 함수형 인터페이스를 사용하라. (0) | 2023.07.03 |
| [effective java] 아이템 43. 람다보다는 메서드 참조를 사용하라. (0) | 2023.07.03 |
| [effective java] 아이템 42. 익명 클래스보다는 람다를 사용하라. (0) | 2023.07.02 |