다량의 데이터 처리 작업(순차적이든 병렬적이든)을 돕고자 자바 8에 추가되었다. 이 API가 제공하는 추상 개념 중 핵심은 두 가지다.
스트림은 데이터 원소의 유한 혹은 무한 시퀀스를 뜻한다
스트림 파이프라인은 이 원소들로 수행하는 연산 단계를 표현하는 개념이다.
파이프라인?
: 일련의 데이터 처리 연산들을 연결하는 프로세스
: 이는 각 단계가 이전 단계의 출력을 다음 단계의 입력으로 사용하며, 데이터 스트림을 효율적으로 처리하기 위해 설계됨.
: 프로그래밍, 특히 스트림 API에서, 파이프라인은 데이터 컬렉션을 처리하는 연산(필터링, 매핑, 정렬 등)의 연속으로 구성됨.
: 이 방식은 코드의 가독성을 향상시키고, 데이터 처리 로직을 명확하게 표현할 수 있도록 도와줌.
스트림 안의 데이터 원소들은 객체 참조(reference)나 기본 타입(int, long, double)을 지원한다.
Stream : 객체 참조타입에 대한 Stream
IntStream : int 타입에 대한 Stream
LongStream : long 타입에 대한 Stream
DoubleStream : double 타입에 대한 Stream
스트림 파이프 라인은 소스 스트림에서 시작해 종단 연산(terminal operation)으로 끝나며,
그 사이에 하나 이상의 중간 연산(intermediate operation)이 있을 수 있다.
종단연산?
: 스트림 파이프라인의 실행을 트리거하고 스트림을 소비하여 결과를 생성하는 연산
: 종단 연산을 수행한 후에는 스트림이 닫히게 되어, 더 이상 해당 스트림에서 데이터를 사용할 수 없다.
: 다양한 종류의 종단 연산이 있으며, 크게 결과를 반환하는 연산, 특정 결과를 소비하는 연산, 그리고 스트림의 요소들에 대해 특정 작업을 수행하는 연산으로 분류할 수 있다.
집계 연산(Aggregate operations):
count(): 스트림의 요소 개수를 반환
min(Comparator): 주어진 비교자를 사용하여 스트림의 최소값을 반환
max(Comparator): 주어진 비교자를 사용하여 스트림의 최대값을 반환
sum(): 스트림의 요소 합계를 반환 (주로 IntStream, LongStream, DoubleStream에서 사용).
average(): 스트림의 요소 평균 값을 반환 (주로 IntStream, LongStream, DoubleStream에서 사용).
결과를 반환하는 연산:
findFirst(): 스트림의 첫 번째 요소를 Optional로 반환
findAny(): 스트림의 어떤 요소든 반환할 수 있으며, Optional로 감싸져 있음
anyMatch(Predicate): 주어진 조건을 만족하는 요소가 하나라도 있으면 true를 반환
allMatch(Predicate): 모든 요소가 주어진 조건을 만족하면 true를 반환
noneMatch(Predicate): 어떤 요소도 주어진 조건을 만족하지 않으면 true를 반환
collect(Collector): 스트림의 요소를 특정 방식으로 수집하고 결과를 반환(예: 리스트, 세트, 맵).
결과를 소비하는 연산:
forEach(Consumer): 스트림의 각 요소에 대해 주어진 작업을 수행
forEachOrdered(Consumer): 스트림의 각 요소에 대해 순서대로 주어진 작업을 수행
기타
toArray(): 스트림의 요소들을 배열로 반환
reduce(BinaryOperator): 스트림의 요소를 결합하여 축소하는 연산을 수행하고, 결과를 반환
중간 연산 (intermediate operation)
: 스트림의 요소들을 변환하거나 필터링하는 등의 처리를 수행하고, 그 결과로 새로운 스트림을 반환함
: 이러한 연산들은 연쇄적으로 연결될 수 있으며, 스트림 파이프라인을 구성하는 데 사용
filter(Predicate<? super T> predicate) : predicate 함수에 맞는 요소만 사용하도록 필터
map(Function<? Super T, ? extends R> function) : 요소 각각의 function 적용
flatMap(Function<? Super T, ? extends R> function) : 스트림의 스트림을 하나의 스트림으로 변환
distinct() : 중복 요소 제거
sort() : 기본 정렬
sort(Comparator<? super T> comparator) : comparator 함수를 이용하여 정렬
skip(long n) : n개 만큼의 스트림 요소 건너뜀
limit(long maxSize) : maxSize 갯수만큼만 출력
스트림 파이프라인은 지연 평가(lazy evaluation)된다.
평가는 종단 연산이 호출될 때 이뤄지며, 종단 연산에 쓰이지 않는 데이터 원소는 계산에 쓰이지 않는다.
무한 스트림을 다룰 수 있게 해줌.
종단 연산이 없는 스트림 파이프라인은 아무 일도 하지 않는 명령어인 no-op과 같으니, 종단 연산을 빼먹는 일이 절대 없도록 해야 함.
스트림 API는 매서드 연쇄를 지원하는 플루언트 API다. 즉, 파이프라인 하나를 구성하는 모든 호출을 연결하여 단 하나의 표현식으로 완성할 수 있다. 파이프라인 여러 개를 연결해 표현식 하나로 만들 수 있다.
기본적으로 스트림 파이프라인은 순차적으로 수행된다.
parallel매서드를 호출하여 파이프라인을 병렬로 실행할 수도 있지만,효과를 볼 수 있는 상황은 많지 않다.
스트림 API는 다재다능하여 사실상 어떠한 계산이라도 해낼 수 있다. 하지만 할 수 있다는 뜻이지, 해야 한다는 뜻은 아니다. 스트림을 제대로 사용하면 프로그램이 짧고 깔끔해지지만, 잘못 사용하면 읽기 어렵고 유지보수도 힘들어진다.
아나그램 그룹을 출력하는 함수
아나그램?
: 철자를 구성하는 알파벳이 같고 순서만 다른 단어를 말함
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);
}
}
> **computeIfAbsent 메서드**
>: Java의 Map 인터페이스에서 제공
>: 키에 해당하는 값이 없을 경우, 주어진 키로 새로운 값을 계산하고 맵에 추가한 후 그 값을 반환
>: 만약 키가 이미 존재하고, 해당 키에 대한 값이 null이 아닌 경우, 맵은 변경되지 않고 기존의 값을 반환.
#### 스트림을 과하게 사용한 경우
```java
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 -> 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)) {
word.collect(groupingBy(word -> alphabetize(word)))
.values().stream()
.filter(group -> group.size() >= minGroupSize)
.forEach(g -> System.out.println(g.size() + ": " + g));
}
}
}
스트림을 처음 쓰기 시작하면 모든 반복문을 스트림으로 바꾸고 싶은 유혹이 생기겠지만, 스트림과 반복문을 적절히 조합하는 게 최선이다.
스트림 부적절한 경우
: 함수 객체로는 할 수 없지만 코드 블록으로는 할 수 있는 경우
코드 블록에서는 범위 안의 지역변수를 읽고 수정할 수 있다. 하지만 람다에서는 final이거나 사실상 final인 변수만 읽을 수 있고, 지역 변수를 수정하는 건 불가능하다
코드 블록에서는 return문을 사용해 매서드에서 빠져나가거나, break나 continue문으로 블록 바깥의 반복문을 종료하거나 반복을 한 번 건너뛸 수 있다. 또한 메서드 선언에 명시된 검사 예외를 던질 수 있다. 하지만 람다는 이 중 어떤 것도 할 수 없다.
스트림이 적절한 경우
원소들의 시퀀스를 일관되게 변환하는 경우
원소들의 시퀀스를 필터링하는 경우
원소들의 시퀀스를 하나의 연산을 사용해 결합하는 경우
원소들의 시퀀스를 컬렉션에 모으는 경우
원소들의 시퀀스에서 특정 조건을 만족하는 원소를 찾는 경우
🍑 결론
스트림과 함수형 프로그래밍에 익숙한 프로그래머라면 스트림방식이 좀 더 명확하다
스트림을 사용할 때가 있고, 반복을 사용할 때가 있다.
스트림과 반복 중 어느 쪽이 나은지 확신하기 어렵다면 둘 다 해보고 더 나은 쪽을 택하는 것이 좋다.
Chapter : 7. 람다와 스트림
Item : 45. 스트림은 주의해서 사용하라
Assignee : pyeong114
🍑 서론
🍑 본론
스트림 API?
다량의 데이터 처리 작업(순차적이든 병렬적이든)을 돕고자 자바 8에 추가되었다. 이 API가 제공하는 추상 개념 중 핵심은 두 가지다.
스트림의 원소들은 어디로부터든 올 수 있다.
스트림 안의 데이터 원소들은 객체 참조(reference)나 기본 타입(int, long, double)을 지원한다.
스트림 파이프 라인은 소스 스트림에서 시작해 종단 연산(terminal operation)으로 끝나며, 그 사이에 하나 이상의 중간 연산(intermediate operation)이 있을 수 있다.
종단연산?
: 스트림 파이프라인의 실행을 트리거하고 스트림을 소비하여 결과를 생성하는 연산 : 종단 연산을 수행한 후에는 스트림이 닫히게 되어, 더 이상 해당 스트림에서 데이터를 사용할 수 없다. : 다양한 종류의 종단 연산이 있으며, 크게 결과를 반환하는 연산, 특정 결과를 소비하는 연산, 그리고 스트림의 요소들에 대해 특정 작업을 수행하는 연산으로 분류할 수 있다.
집계 연산(Aggregate operations):
결과를 반환하는 연산:
결과를 소비하는 연산:
기타
중간 연산 (intermediate operation)
: 스트림의 요소들을 변환하거나 필터링하는 등의 처리를 수행하고, 그 결과로 새로운 스트림을 반환함 : 이러한 연산들은 연쇄적으로 연결될 수 있으며, 스트림 파이프라인을 구성하는 데 사용
스트림 파이프라인은 지연 평가(lazy evaluation)된다.
스트림 API는 매서드 연쇄를 지원하는 플루언트 API다. 즉, 파이프라인 하나를 구성하는 모든 호출을 연결하여 단 하나의 표현식으로 완성할 수 있다. 파이프라인 여러 개를 연결해 표현식 하나로 만들 수 있다.
기본적으로 스트림 파이프라인은 순차적으로 수행된다. parallel매서드를 호출하여 파이프라인을 병렬로 실행할 수도 있지만,효과를 볼 수 있는 상황은 많지 않다.
스트림 API는 다재다능하여 사실상 어떠한 계산이라도 해낼 수 있다. 하지만 할 수 있다는 뜻이지, 해야 한다는 뜻은 아니다. 스트림을 제대로 사용하면 프로그램이 짧고 깔끔해지지만, 잘못 사용하면 읽기 어렵고 유지보수도 힘들어진다.
아나그램 그룹을 출력하는 함수
}
코드를 이해하기 어려운가? 다른 사람도 마찬가지다 이 코드는 확실히 짧지만 읽기는 어렵다. 이처럼 스트림을 과용하면 프로그램이 읽거나 유지보수하기 어려워진다.
스트림을 적절하게 활용한 경우
스트림을 처음 쓰기 시작하면 모든 반복문을 스트림으로 바꾸고 싶은 유혹이 생기겠지만, 스트림과 반복문을 적절히 조합하는 게 최선이다.
스트림 부적절한 경우
: 함수 객체로는 할 수 없지만 코드 블록으로는 할 수 있는 경우
스트림이 적절한 경우
🍑 결론
Referenced by