Study/Modern-Java-In-Action

Modern Java Ch05. Stream 활용

hongeeii 2023. 1. 20.
728x90
반응형

https://sigridjin.medium.com/java-stream-api%EB%8A%94-%EC%99%9C-for-loop%EB%B3%B4%EB%8B%A4-%EB%8A%90%EB%A6%B4%EA%B9%8C-50dec4b9974b

참고자료

 

Java Stream API는 왜 for-loop보다 느릴까?

The Korean Commentary on ‘The Performance Model of Streams in Java 8" by Angelika Langer

sigridjin.medium.com

  • 필터링, 슬라이싱, 매칭
  • 검색, 매칭, 리듀싱
  • 특정 범위의 숫자와 같은 숫자 스트림 사용하기
  • 다중 소스로부터 스트림 만들기
  • 무한 스트림

필터링

1. Predicate로 필터링

filter 메서드는 Predicate(boolean을 반환하는 함수)를 인수로 받아서 Predicate와 일치하는 모든 요소를 포함하는 스트림을 반환한다.

   List<Dish> vegetarianMenu = Menu.menu.stream()
                .filter(Dish::isVegetarian) // 채식 요리인지 확인하는 메서드 참조
                .collect(Collectors.toList());

2. 고유 요소 필터링

고유 요소로 이루어진 스트림을 반환한는 distinct 메서드도 지원한다.
고유 여부는 스트림에서 만든 객체의 hashCode, equals로 결정됨.

List<Integer> numbers = Arrays.asList(1, 2, 1, 3, 3, 2, 4);
        numbers.stream()
        .filter(i -> i % 2 == 0) // 짝수를 선택 2,2,4
        .distinct() // 중복 제거
        .forEach(System.out::println); // 2, 4

슬라이싱

스트림의 요소를 선택하거나 스킵하는 다양한 방법이 있다.

1. Predicate를 이용한 슬라이싱

java9는 스트림의 요소를 효과적으로 선택할 수 있도록 takeWhile, dropWhile 두 가지 새로운 메서드를 지원한다.

  • takeWhile : 리스트가 정렬이 되어있을 때, filter 연산을 이용하면 전체 스트림을 반복하면서 각 요소에 predicate를 적용하게 된다.
    takeWhile은 predicate가 false가 나왔을 때 반복 작업을 중단할 수 있다.
    작은 리스트에서는 별거 아닐것처럼 보일 수 있지만 아주 많은 요소를 포함하는 스트림에서는 상당한 차이가 될 수 있다.
List<Dish> sortedDishMenu =  Menu.getMenu().stream()
        .sorted(Comparator.comparing(Dish::getCalories)) // 칼로리가 낮은 순서대로 정렬
        .collect(Collectors.toList());

        List<Dish> slicedMenu1 = sortedDishMenu.stream()
                .takeWhile(dish->dish.getCalories()<320)
        .collect(Collectors.toList());
  • dropWhile : takeWhile의 정반대.
    predicate가 처음으로 거짓이 되는 지점까지 발견된 요소를 버림.
    거짓이 되면 그지점에서 작업을 중단하고 나머지 요소를 반환.
    dropWhile은 무한한 남은 요소를 가진 무한 스트림에서도 동작.
List<Dish> slicedMenu2 = sortedDishMenu.stream()
                .dropWhile(dish->dish.getCalories()<320)
                .collect(Collectors.toList());

2. 스트림 축소 - limit

스트림은 주어진 값 이하의 크기를 갖는 새로운 스트림을 반환하는 limit(n) 메서드를 지원.
스트림이 정렬되어 있어있지 않다면 limit의 결과도 정렬되지 않음.

 List<Dish> slicedMenu3 = sortedDishMenu.stream()
                .filter(dish->dish.getCalories()>320)
                .limit(3)
                .collect(Collectors.toList());

3. 요소 건너뛰기 - skip

스트림은 처음 n개 요소를 제외한 스트림을 반환하는 skip(n) 메서드를 지원한다.
n개 이하의 요소를 포함하는 스트림에 skip(n)을 호출하면 빈 스트림이 반환한다.

List<Dish> slicedMenu5 = Menu.getMenu().stream()
                .filter(d->d.getCalories() > 300)
                .skip(2).limit(2)
                .collect(Collectors.toList());

Mapping

특정 객체에서 특정 데이터를 선택할 수 있다.

1. map

함수를 인수로 받는 map 메서드는각요소에적용되며함수를 적용한 결과가새로운 요소로 매핑된다.
예를 들면 Dish::getName을 map 메서드로 전달해 스트림의 요리명을 추출할 수 있다.

 List<String> dishNames = Menu.getMenu().stream()
                .map(Dish::getName)
                .collect(Collectors.toList());

getName은 문자열을 반환하므로 map 메서드의 출력 스트림은 String 형식을 갖는다.

// dish를 요리명으로 변환하고, 다시 요리명의 길이를 추출하는 예제
List<Integer> dishNameLength = Menu.getMenu().stream()
               .map(Dish::getName)
               .map(String::length)
               .collect(Collectors.toList());   

2. flatMap - 스트림의 평면화

flatMap은 각 요소를 스트림의 아니라 스트림의 콘텐츠로 매핑한다.
스트림의 각 값을 다른 스트림으로 만든 다음에 모든 스트림을 하나의 스트림으로 연결하는 기능을 수행한다.

 

quiz!!

두 개의 숫자 리스트가 있을 때 모든 숫자 쌍의 리스트를 반환하시오.
예를 들어 두 개의 리스트 [1,2,3]과 [3,4]가 주어지면 [(1,3),(1,4),(2,4),(3,3),(3,4)]를 반환해야 한다.

List<Integer> num1 = Arrays.asList(1, 2, 3);
		List<Integer> num2 = Arrays.asList(3, 4);

		List<Stream<Integer[]>> s = num1.stream()
                                       .map(i -> num2.stream().map(j -> new Integer[] { i, j }))
				                           .collect(Collectors.toList());

		s.stream().forEach(d -> {
			System.out.println(d.map(arr -> Arrays.asList(arr)).collect(Collectors.toList()));
		});
---------------------------------------------------
   result : [[1, 3], [1, 4]]
            [[2, 3], [2, 4]]
            [[3, 3], [3, 4]]
   
   
		List<Integer[]> s2 = num1.stream()
                                  .flatMap(i -> num2.stream().map(j -> new Integer[] { i, j }))
                                  .collect(Collectors.toList());

		System.out.println(s2.stream().map(Arrays::asList).collect(Collectors.toList()));
---------------------------------------------------
   result : [[1, 3], [1, 4], [2, 3], [2, 4], [3, 3], [3, 4]]

검색과 매칭

특정 속성이 데이터 집합에 있는지 여부를 검색하는 데이터 처리도 자주 사용됨.
anyMatch, allMatch, noneMatch, findFirst, findAny 등 다양한 유틸리티 메서드를 제공.

1. anyMatch - predicate가 적어도 한 요소와 일치하는지 확인

if (Menu.getMenu().stream().anyMatch(Dish::isVegetarian))
            System.out.println("this is vegetarian friendly!!");

anyMatch는 boolean을 반환하므로 최종 연산이다.

2. allMatch - predicate가 모든 요소와 일치하는지 검사

boolean isHealthy = Menu.getMenu().stream().allMatch(dish -> dish.getCalories() < 1000);	

3. noneMatch - allMatch와 반대연산, 주어진 predicate와 일치하는 요소가 없는지 확인.

boolean isHealthy2 = Menu.getMenu().stream()
                .noneMatch(dish -> dish.getCalories() >= 1000);											
  • 쇼트 서킷 anyMatch, allMatch, noneMatch 세 메서드는 스트림 쇼트서킷 기법, 자바의 &&, || 와 같은 연산을 활용.
    and 표현식에서 하나라도 거짓이라는 결과가 나오면 나머지 표현식의 결과와 상관없이 전체 결과도 거짓이된다.
    이런 상황을 쇼트서킷이라 부름.
    원하는 요소를 찾았으면 즉시 결과를 반환할 수 있게 함.
    무한한 요소를 가진 스트림을 유한한 크기로 줄일 수 있는 유용한 연산.

4. findAny - 요소 검색

현재 스트림에서 임의의 요소를 반환.
쇼트 서킷을 이용해서 결과를 찾는 즉시 리턴.

Optional<Dish> dish = Menu.getMenu().stream()
                .filter(Dish::isVegetarian)
                .findAny();	

5. findFirst - 첫 번째 요소 찾기

정렬된 연속 데이터로부터 생성된 스트림에서 임의의 요소가 아닌 첫 번째 요소를 찾을 수 있다.

// 숫자 리스트에서 3으로 나누어 떨어지는 첫 번째 제곱값을 반환하는 코드
List<Integer> someNumbers = Arrays.asList(1,2,3,4,5);
        Optional<Integer> firstElement = someNumbers.stream().map(n->n*n)
        		.filter(n -> n%3==0)
        		.findFirst();
        
        firstElement.ifPresent(System.out::println); // 9	

findFirst는 병렬 실행에서는 첫 번째 요소를 찾기 어렵다.
요소의 반환 순서가 상관없다면 병렬 스트림에서는 제약이 적은 findAny를 사용한다.

  • Optional Optional 클래스(java.util.Optional)는 값의 존재나 부재 여부를 표현하는 컨테이너 클래스(wrapper)
    findAny나 findFirst는 아무 요소도 반환하지 않을 수 있다.
    null은 쉽게 에러를 일으킬 수 있으므로 Optional를 이용하여 null 확인 관련 버그를 피할 수 있다. (자세한 설명은 10장..)
    Optional은 값이 존재하는지 확인하고, 값이 없을 때 어떻게 처리할지 강제하는 기능을 제공한다.
    • isPreset() : Optional이 값을 포함하면 true, 포함하지 않으면 false
    • ifPresent(Consumer block) : 값이 있으면 주어진 block을 실행. Consumer는 T형식의 인수를 받아 void를 반환.
    • T get() : 값이 존재하면 반환하고 없으면 noSuchElementException을 일으킴
    • T orElse(T other) : 값이 있으면 반환하고, 없으면 기본값을 반환.

reduce - 리듀싱

지금 까지 살펴본 최종 연산은 allMatch(boolean), forEach(void), findAny(Optional), collect 등을 살펴 보았다.
reduce 연산을 이용해서 더 복잡한 질의를 표현하는 방법을 살펴본다.
모든 스트림 요소를 처리해서 값으로 도출하는 것을 리듀싱 연산이라 한다.
함수형 프로그래밍 언어로는 fold(폴드)라고 부른다.

1. 요소의 합

List<Integer> numbers = Arrays.asList(4, 5, 3, 9);
// forEach루프를 이용해서 리스트의 숫자 요소를 더하는 코드	
int sum = 0; // 초기 값
for (int x : numbers)
    sum += x; // 연산(+)
	
	
// reduce 연산을 이요한 코드
int sum2 = numbers.stream().reduce(0, (a, b) -> a + b);
	
// 모든 요소의 곱셈
int sum2 = numbers.stream().reduce(0, (a, b) -> a * b);
	
// Integer.sum
public static int sum(int a, int b) {
        return a + b;
    }

int sum2 = numbers.stream().reduce(0, Integer :: sum); // 이렇게도 표현 가능

reduce는 두 개의 인수를 갖는다.

  • 초기값 0
  • 두 요소를 조합해서 새로운 값을 만드는 BinaryOperator (<T,T>->T) 
  • 초기값 없음 

    초기값을 받지 않도록 오버로드된 reduce도 있다.
    이 reduce는 Optional을 반환한다.
Optional<Integer> sum3 = numbers.stream().reduce((a, b) -> a + b);	

스트림에 아무 요소도 없을 때 초기값이 없기때문에 reduce는 연산을 할 수 없다.
연산을 할 수 없음을 가르키도록 Optional 객체로 감싼 결과를 반환한다.

2. 최대값과 최솟값

reduce연산은 새로운 값을 이용해서 스트림의 모든 요소를 소비할 때까지 랍다를 반복 수행하면서 최댓값을 생산한다.

// Integer.max
public static int max(int a, int b) {
        return Math.max(a, b);
    }
	
// 최댓값
Optional<Integer> max = numbers.stream().reduce(Integer :: max);
// 최솟값
Optional<Integer> max = numbers.stream().reduce(Integer :: min);

quiz time!!!!!

map과 reduce를 이용해서 스트림의 요리 개수를 계산하시오.

// map과 reduce를 연결하는 기법을 맵 기듀스(map-reduce) 패턴이라 하며, 쉽게 병렬화하는 특징 덕분에 유명해졌다
Optional<Integer> reduceQuiz = Menu.getMenu().stream()
				.map(dish -> 1)
				.reduce(Integer :: sum);

reduceQuiz.ifPresent(System.out::println);	// 9
	
// 최종연산 count를 이용하는 방법
System.out.println(Menu.getMenu().stream().count());
  • reduce 메서드의 장점과 병렬화
    for-each or iterator vs stream reduce
    reduce를 이용하면 내부 반복이 추상화되면서 내부 구현에서 병렬로 redcue를 실행할 수 있게 된다.
    만약에 반복적인 합계(iterator)를 이용한다면 sum 변수를 공유해야하므로 쉽게 병렬하기가 어려워 진다. ( + 입력 분할, 분할된 입력 더함, 더한 값을 합침).
    stream reduce를 이용하면 parallelStream()으로 병렬 수행을 간단하게 할 수 있음.
  • 스트림 연산 : 상태 없음과 상태 있음
    • 스트림 프로세싱에는 상태 기반과 무상태 스트림 처리가 있다.
    • 차이점이란 실시간 데이터 처리를 위하여 이전에 분석된 데이터의 결과가 필요한지이다.
    • 이렇게 상태를 유지할 스트림 프로세싱은 이벤트를 처리하고 그 결과를 저장할 상태 저장소가 필요하다.
    • 이와는 반대로 무상태 스트림 처리는 이전 스트림의 처리 결과와 관계없이 현재 데이터로만 처리를 한다.
  • 중간 연산과 최종 연산

실전 연습

// 거래자
public class Trader {
	private final String name;
	private final String city;
	
	...
}
	
	
// 트랜잭션
public class Transaction {
	private final Trader trader;
	private final int year;
	private final int value;

	...
}


Trader raoul = new Trader("Raoul", "Cambridge");
	Trader mario = new Trader("Mario", "Milan");
	Trader alan = new Trader("Alan", "Cambridge");
	Trader brian = new Trader("Brian", "Cambridge");

List<Transaction> transactions = Arrays.asList(
	new Transaction(brian, 2011, 300),
	new Transaction(raoul, 2012, 1000), 
	new Transaction(raoul, 2011, 400), 
	new Transaction(mario, 2012, 710),
	new Transaction(mario, 2012, 700), 
	new Transaction(alan, 2012, 950)
);	

실전 연습 문제 풀이

// 1. 2011년에 일어난 모든 트랜잭션을 찾아 값을 오름차순으로 정리
List<Transaction> quiz1 = transactions.stream()
	.filter(tr -> tr.getYear() == 2011)
	.sorted(Comparator.comparing(Transaction::getValue))
	.collect(Collectors.toList());
//[Transaction [trader=Trader [name=Brian, city=Cambridge], year=2011, value=300], 
//Transaction [trader=Trader [name=Raoul, city=Cambridge], year=2011, value=400]]
		 	 
// 2. 거래자가 근무하는 모든 도시를 중복없이 나열
List<String> quiz2 = transactions.stream()
	.map(Transaction::getTrader)
	.map(Trader::getCity)  // map(tr -> tr.getTrader().getCity) 로 사용
	.distinct()
	.collect(Collectors.toList());
// [Cambridge, Milan]
		 
// 3. 케임브리지에서 근무하는 모든 거래자를 찾아서 이름순으로 정렬
List<String> quiz3 = transactions.stream()
	.map(Transaction :: getTrader)
	.filter(trader -> trader.getCity().equals("Cambridge"))
	.map(Trader :: getName)
	.sorted(Comparator.comparing(String :: toString))      // .sorted()으로 사용가능
	.collect(Collectors.toList());
// [Alan, Brian, Raoul, Raoul] 
		
// 4. 모든 거래자의 이름을 알파벳순으로 정렬해서 반환
List<String> quiz4 = transactions.stream()
	.map(Transaction :: getTrader)
	.map(Trader :: getName)
	.sorted(Comparator.comparing(String :: toString))	// .sorted()으로 사용가능
	.collect(Collectors.toList()); 
// [Alan, Brian, Mario, Mario, Raoul, Raoul]
		 
// 5. 밀라노에 거래자가 있는가?
boolean quiz5 = transactions.stream()
	.map(Transaction :: getTrader)
	.anyMatch(trader -> trader.getCity().equals("Milan"));
// true

// 6. 케임브리지에 거주하는 거래자의 모든 트랜잭션값을 출력
transactions.stream()
	.filter(tr -> tr.getTrader().getCity().equals("Cambridge"))
	.map(Transaction :: getValue)
	.forEach(System.out :: println);
// Transaction [trader=Trader [name=Brian, city=Cambridge], year=2011, value=300] 300
// Transaction [trader=Trader [name=Raoul, city=Cambridge], year=2012, value=1000] 1000
// Transaction [trader=Trader [name=Raoul, city=Cambridge], year=2011, value=400] 400
// Transaction [trader=Trader [name=Alan, city=Cambridge], year=2012, value=950] 950
		
// 7. 전체 트랜잭션 중 최대값은 얼마인가
Optional<Integer> quiz7 = transactions.stream()
	.map(Transaction :: getValue)
	.reduce(Integer :: max);
quiz7.ifPresent(System.out :: println); // 1000
		
// 8. 전체 트랜잭션 중 최솟값은 얼마인가
Optional<Integer> quiz8 = transactions.stream()
	.map(Transaction :: getValue)
	.reduce(Integer :: min);
quiz8.ifPresent(System.out :: println); // 300
	
// Optional<Transaction> quiz7_ = transactions.stream().min(Comparator.comparing(Transaction :: getValue)); 이렇게 사용 가능

6. 숫자형 스트림

 Menu.menu.stream().map(Dish::getCalories).reduce(Integer:: sum); // 박싱 비용이 숨어있음. 계산하기전에 Integer을 기본형으로 언박싱해야함
==> Menu.menu.stream().map(Dish::getCalories).sum(); // sum()메서드 없음

스트림 API는 숫자 스트림을 효율적으로 처리할 수 있도록 기본형 특화 스트림(primitive stream specialization)을 제공한다.

기본형 특화 스트림

기본형 특화 스트림으로 IntStream, DoubleStream, LongStream이 존재하며 각각의 인터페이스에는 숫자 스트림의 합계를 계산하는 sum, 최댓값 요소를 검색하는 max 같이 자주 사용하는 숫자 관련 리듀싱 연산 메서드를 제공한다.

1. 숫자 스트림으로 매핑

스트림을 특화 스트림으로 변환할 때는 mapToInt, mapToDouble, mapToLong 세 메서드를 가장 많이 사용한다.

int sum =  Menu.menu.stream().mapToInt(Dish::getCalories).sum();	// 스트림이 비어있다면 sum = 0;

IntStream은 max, min, average 등 다양한 유틸리티 메서드도 지원한다.

객체 스트림으로 복원하기

boxed메서드를 이용하면 특화 스트림을 일반 스트림으로 변환할 수 있다.

IntStream intStream = Menu.menu.stream().mapToInt(Dish::getCalories);
Stream<Integer> stream = intStream.boxed();

Optional도 기본형에 대하여 지원한다. OptionalInt, OptionalDouble, optionalLong 세 가지 기본형 특화 스트림 버전의 Optional이 제공된다.

OptionalInt maxCalories = Menu.menu.stream().mapToInt(Dish::getCalories).max();
int max = maxCalories.orElse(1);

숫자 범위

특정 범위의 숫자를 이용해야 할 때 range와 rangeClosed 메서드를 사용할 수 있다. 이는 IntStream, LongStream 두 기본형 특화 스트림에서 지원된다. range는 열린 구간을 의미하며, rangeClosed는 닫힌 구간을 의미한다.

long count = IntStream.range(1, 10).count(); // 9
long count2 = IntStream.rangeClosed(1, 10).count(); // 10	

7. 스트림 만들기

컬렉션에서 스트림을 얻었고, 범위의 숫자에서 스트림을 만들었다.
더 다양한 방식으로 스트림을 만드는 방법이 있다.

값으로 스트림 만들기

정적 메서드 Stream.of 을 이용하여 스트림을 만들 수 있다. 정적 메서드 'Stream.emtpy'를 이용해서 스트림을 비울 수 있다.

 Stream<String> stream = Stream.of("Modern", "Java", "In", "Action");
stream.map(String::toUpperCase).forEach(System.out::println);	

Stream<String> emtyStream = Stream.empty(); 

null이 될 수 있는 객체로 스트림 만들기

자바 9부터 지원되며 Stream.ofNullable 메서드를 이용하여 null이 될 수 있는 객체를 지원하는 스트림을 만들 수 있다.
객체가 null 이라면 빈 스트림

배열로 스트림 만들기

배열을 인수로 받는 정적 메서드 Arrays.stream 을 이용하여 스트림을 만들 수 있다.

 int[] numbers = { 2, 3, 5, 7, 11, 13 };
int sum = Arrays.stream(numbers).sum();	 // IntStream으로 변환

파일로 스트림 만들기

파일을 처리하는 등의 I/O 연산에 사용하는 자바의 NIO API(비블록 I/O)도 스트림 API를 활용할 수 있도록 업데이트되었다. java.nio.file.Files의 많은 정적 메서드가 스트림을 반환한다. 예를 들어 Files.lines는 주어진 파일의 행 스트림을 문자열로 반환한다.


스트림 인터페이스는 AutoCloseable 인터페이스를 구현하기 때문에 try-with-resource 구문으로 finally 대신 try 블록 내의 자원은 자동으로 관리된다.

함수로 무한 스트림 만들기

Stream.iterate와 Stream.generate를 통해 함수를 이용하여 무한 스트림을 만들 수 있다.
iterate와 generate에서 만든 스트림은 요청할 때마다 주어진 함수를 이용해서 값을 만든다.
따라서 무제한으로 값을 계산할 수 있지만, 보통 무한한 값을 출력하지 않도록 limit(n) 함수를 함께 연결해서 사용한다.
무한 스트림을 언바운드 스트림이라고 표현함.
스트림이 컬렉션과의 가장 큰 차이점

  • Stream.iterate
public static<T> Stream<T> iterate(final T seed, final UnaryOperator<T> f)
 
  • Stream.generate
public static<T> Stream<T> generate(Supplier<T> s)  
Stream.generate(Math::random).limit(4).forEach(System.out::println);

Stream.iterate(0, n -> n + 2).limit(10).forEach(System.out::println);	
728x90
반응형

추천 글