CS 스터디 12주차
Stream API
JAVA Stream
Java 8버전 이상부터는 Stream API를 지원하기 시작했다.
Stream을 통해 Java에서도 람다를 사용한 함수형 프로그래밍이 가능해졌고
이 부분은 기존의 Collection과 Stream은 데이터 계산 시점에서 차이가 난다.
그럼 Collection이란 정확히 뭐길래?
Stream 또 뭐길래? 두가지가 데이터 계산 시점의 차이가 존재한다고 하는걸까.
Collection
Collection은 모든 값을 메모리에 저장하는 자료구조로 데이터를 추가하기 전에 모든 계산이 완료되어 있어야만 한다.
외부 반복을 통해 사용자가 직접 반복 작업을 거쳐 요소를 가져올 수 있으며 예를 들어 for-each 문을 사용할 수 있다.
Stream
Stream은 반대로 요청할 때만 요소를 계산한다.
내부 반복을 사용하므로, 추출할 요소만 선언해주면 알아서 반복 처리를 진행하며, Stream에 요소를 따로 추가하거나 제거하는 작업은 불가능하다.
조금 더 쉽게 비교해보자면 Collection은 휴대폰에 저장된 음악파일을 재생하는거라면
Stream은 필요할 때 검색해서 듣는 스트리밍 서비스 정도의 차이가 있다.
외부 반복 & 내부 반복
Collection은 외부 반복을 사용하고, Stream은 내부 반복을 사용한다.
내부 반복은 작업을 병렬 처리하기 때문에 최적화된 순서로 처리를 하고,
외부 반복은 명시적으로 컬렉션 항목을 하나씩 가져와서 처리하기 때문에 성능면에서는 내부 반복이 더 효율적이다.
만약 Collection에서 Stream처럼 병렬성을 이용하려고 한다면 개발자가 직접 synchronized를 통해 관리해야 한다.
Stream 연산
Stream의 연산 과정은 '중간 연산'과 '최종 연산'으로 나뉜다.
중간 연산: filter, map, limit 등 파이프라이닝이 가능한 연산들입니다. 중간 연산은 Stream을 반환
최종 연산: count, collect 등 Stream을 닫는 연산입니다. 최종 연산은 Stream을 소비
이렇게 나누는 이유는 중간 연산들이 Stream을 반환해야 하기 때문인데,
모든 중간 연산을 한꺼번에 병합하여 처리한 다음에서야 최종 연산에서 한꺼번에 처리하게 된다.
예시를 통해 이해도를 높혀보려고 한다.
예시: Item 중에서 가격이 1000 이상인 이름을 5개 선택하기
List<String> items = item.stream()
.filter(d -> d.getPrices() >= 1000)
.map(d -> d.getName())
.limit(5)
.collect(Collectors.toList());
이 예제에서 만약에 Collection을 사용했다면,
가격이 1000 이상인 아이템을 먼저 찾고, 그 다음 이름만 따로 저장한 후 5개를 선택해야 했겠지만
stream을 사용함으로써 하나의 과정으로 병합되어 처리된다.
( filter와 map은 각각 다른 연산이지만, 병합처리되는것!)
연산 최적화는 물론 가독성 면에서도 Stream이 더 좋아보인다.
Stream 중간 연산
1. filter(Predicate<T> predicate): Predicate를 인자로 받아 true인 요소를 포함한 스트림 반환
2. distinct(): 중복 필터링
3. limit(long maxSize): 주어진 사이즈 이하 크기를 갖는 스트림 반환
4. skip(long n): 처음 요소 n개를 제외한 스트림 반환
5. map(Function<? super T,? extends R> mapper): 매핑 함수의 결과로 구성된 스트림 반환
6. flatMap(Function<? super T,? extends Stream<? extends R>> mapper): 스트림의 콘텐츠로 매핑, map과 달리 평면화된 스트림 반환
중간 연산은 모두 스트림을 반환하니까 참고!
Stream 최종 연산
1. boolean allMatch(Predicate<? super T> predicate): 모든 스트림 요소가 Predicate와 일치하는지 검사
2. boolean anyMatch(Predicate<? super T> predicate): 하나라도 일치하는 요소가 있는지 검사
3. boolean noneMatch(Predicate<? super T> predicate): 매치되는 요소가 없는지 검사
4. Optional<T> findAny(): 현재 스트림에서 임의의 요소 반환
5. Optional<T> findFirst(): 스트림의 첫 번째 요소 반환
6. <R> R reduce(R identity, BinaryOperator<R> accumulator): 모든 스트림 요소를 처리해 값을 도출. 두 개의 인자를 가짐
7. <R, A> R collect(Collector<? super T,A,R> collector): 스트림을 reduce하여 list, map, 정수 형식 컬렉션을 만듦
8. void forEach(Consumer<? super T> action): 스트림 각 요소를 소비하며 람다 적용
9. long count(): 스트림 요소 개수 반환
Optional 클래스
값의 존재 여부를 표현하는 컨테이너 클래스로 null로 인한 버그를 막을 수 있는 장점이 있다고한다.
boolean isPresent(): Optional이 값을 포함할 때 true 반환
중요한건 아니지만 Tech Interview에 있길래 슬쩍 추가해놓는다.
Stream 활용 예제
1. map()
map()은 스트림의 각 요소를 주어진 함수에 적용한 결과로 구성된 스트림을 반환한다.
List<String> names = Arrays.asList("Sehoon", "Songwoo", "Chan", "Youngsuk", "Dajung");
names.stream()
.map(name -> name.toUpperCase())
.forEach(name -> System.out.println(name));
2. filter()
filter()는 주어진 Predicate를 만족하는 요소들로만 구성된 스트림을 반환한다.
List<String> startsWithS = names.stream()
.filter(name -> name.startsWith("S"))
.collect(Collectors.toList());
3. reduce()
reduce()는 스트림의 요소들을 결합하여 단일 결과를 도출한다.
Stream<Integer> numbers = Stream.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
Optional<Integer> sum = numbers.reduce((x, y) -> x + y);
sum.ifPresent(s -> System.out.println("sum: " + s));
4. collect()
collect()는 스트림의 요소들을 수집하여 다른 자료구조로 변환한다.
System.out.println(names.stream()
.map(String::toUpperCase)
.collect(Collectors.joining(", ")));
Stream API를 활용하면 코드의 가독성, 간결성, 그리고 성능 면에서 많은 이점을 누릴 수 있기 때문에
Java 8 이후 버전이라면 적극 활용해보는 것도 좋을 것 같다.
'🪄Interview > ✏️Study' 카테고리의 다른 글
[CS STUDY INTERVIEW] 14주차 - 템플릿 메서드 패턴 (1) | 2024.06.19 |
---|---|
[CS STUDY INTERVIEW] 13주차 - 어댑터 패턴 (0) | 2024.06.05 |
[CS STUDY INTERVIEW] 11주차 - Error & Exception (0) | 2024.05.11 |
[CS STUDY INTERVIEW] 10주차 - GC(Garbage Collection) (0) | 2024.05.04 |
[CS STUDY INTERVIEW] 9주차 - JVM(Java Virtual Machine) (0) | 2024.04.20 |