-
Notifications
You must be signed in to change notification settings - Fork 2
Description
Chapter : 7. 람다와 스트림
Item : 45. 스트림은 주의해서 사용하라
Assignee : pyeong114
🍑 서론
🍑 본론
스트림 API?
다량의 데이터 처리 작업(순차적이든 병렬적이든)을 돕고자 자바 8에 추가되었다. 이 API가 제공하는 추상 개념 중 핵심은 두 가지다.
- 스트림은 데이터 원소의 유한 혹은 무한 시퀀스를 뜻한다
- 스트림 파이프라인은 이 원소들로 수행하는 연산 단계를 표현하는 개념이다.
파이프라인?
: 일련의 데이터 처리 연산들을 연결하는 프로세스
: 이는 각 단계가 이전 단계의 출력을 다음 단계의 입력으로 사용하며, 데이터 스트림을 효율적으로 처리하기 위해 설계됨.
: 프로그래밍, 특히 스트림 API에서, 파이프라인은 데이터 컬렉션을 처리하는 연산(필터링, 매핑, 정렬 등)의 연속으로 구성됨.
: 이 방식은 코드의 가독성을 향상시키고, 데이터 처리 로직을 명확하게 표현할 수 있도록 도와줌.
List<String> discountedProducts = products.stream()
.filter(product -> product.getPrice() >= 1000)
.map(product -> new Product(product.getName(), product.getPrice() * 0.9))
.filter(discountedProduct -> discountedProduct.getPrice() >= 800)
.map(Product::getName)
.sorted()
.collect(Collectors.toList());스트림의 원소들은 어디로부터든 올 수 있다.
- 컬렉션
- 배열
- 파일
- 정규표현식 패턴 매처
- 난수 생성기
- 다른 스트림
스트림 안의 데이터 원소들은 객체 참조(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다. 즉, 파이프라인 하나를 구성하는 모든 호출을 연결하여 단 하나의 표현식으로 완성할 수 있다. 파이프라인 여러 개를 연결해 표현식 하나로 만들 수 있다.
List<String> myList = Arrays.asList("apple", "banana", "apricot", "orange");
List<String> filteredList = myList.stream()
.filter(s -> s.startsWith("a"))
.map(String::toUpperCase)
.collect(Collectors.toList());기본적으로 스트림 파이프라인은 순차적으로 수행된다.
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이 아닌 경우, 맵은 변경되지 않고 기존의 값을 반환.
스트림을 과하게 사용한 경우
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문으로 블록 바깥의 반복문을 종료하거나 반복을 한 번 건너뛸 수 있다. 또한 메서드 선언에 명시된 검사 예외를 던질 수 있다. 하지만 람다는 이 중 어떤 것도 할 수 없다.
스트림이 적절한 경우
- 원소들의 시퀀스를 일관되게 변환하는 경우
- 원소들의 시퀀스를 필터링하는 경우
- 원소들의 시퀀스를 하나의 연산을 사용해 결합하는 경우
- 원소들의 시퀀스를 컬렉션에 모으는 경우
- 원소들의 시퀀스에서 특정 조건을 만족하는 원소를 찾는 경우
🍑 결론
- 스트림과 함수형 프로그래밍에 익숙한 프로그래머라면 스트림방식이 좀 더 명확하다
- 스트림을 사용할 때가 있고, 반복을 사용할 때가 있다.
- 스트림과 반복 중 어느 쪽이 나은지 확신하기 어렵다면 둘 다 해보고 더 나은 쪽을 택하는 것이 좋다.