[모던 자바 인 액션] chap05. 스트림 활용

2023. 3. 5. 11:08Java

내용
필터링, 슬라이싱, 매칭
검색, 매칭, 리듀싱
특정 범위의 숫자와 같은 숫자 스트림 사용하기
다중 소스로부터 스트림 만들기
무한 스트림
// 외부 반복
List<Dish> vegetrianDishes = new ArrayList<>();
	for(Dish d : menu) {
		if(d.isVegetarian())
			vegetrianDishes.add(d);
	}

// 내부 반복
List<Dish> vegetrianDishes = menu.stream()
		.filter(Dish::isVegetarian)
		.collect(Collectors.toList());

 

1. 필터링

1. 프레디케이트로 필터링

filter 메서드는 프레디케이트를 인수로 받아서 프레디케이트와 일치하는 모든 요소를 포함하는 스트림을 반환한다.

List<Dish> vegetrianDishes = menu.stream()
		.filter(Dish::isVegetarian)	// 채식 요리인지 확인하는 메서드 참조
		.collect(Collectors.toList());
2. 고유 요소 필터링

스트림은 고유 요소로 이루어진 스트림을 반환하는 distinct 메서드도 지원한다.

List<Integer> numbers = Arrays.asList(1, 2, 1, 3, 3, 2, 4);
numbers.stream()
		.filter(i -> i%2 == 0)
		.distinct()
		.forEach(System.out::println);

 

2. 스트림 슬라이싱

1. 프레디케이트를 이용한 슬라이싱
자바9는 스트림의 요소를 효과적으로 선택할 수 있도록 takeWhile, dropWhile 두 가지 새로운 메서드를 지원한다.
  • TAKEWHILE활용 - 코드를 보면 스페셜 메뉴는 이미 칼로리 별로 정렬이 되있는 상태다. 리스트가 이미 정렬되있다는 사실을 이용해 320 칼로리보다 크거나 같은 요리가 나왔을 때 반복 작업을 중단할 수 있다. 작은 리스트에서는 이와 같은 동작이 별거 아닌 것처럼 보일 수 있지만 아주 많은 요소를 포함하는 큰 스트림에서는 상당한 차이가 될 수 있다. takeWhile을 이용하면 무한 스트림을 포함한 모든 스트림에 프레디케이트를 적용해 스트림을 슬라이스할 수 있다.
List<Dish> specialMenu = Arrays.asList(
                new Dish("season fruit", true, 120, Dish.Type.OTHER),
                new Dish("prawns", false, 300, Dish.Type.FISH),
                new Dish("rice", true, 350, Dish.Type.OTHER),
                new Dish("chicken", false, 400, Dish.Type.MEAT),
                new Dish("french fries", true, 530, Dish.Type.OTHER));

        List<Dish> filterMenu = specialMenu.stream()
                .filter(dish -> dish.getCalories() < 320)
                .collect(Collectors.toList());

        List<Dish> sliceMenu = specialMenu.stream()
                .takeWhile(dish -> dish.getCalories() < 320)
                .collect(Collectors.toList());
        // season fruit, prawns 반환
  • DROPWHILE 활용 - 나머지 요소를 선택하려면 dropWhile을 이용해 이 작업을 완료할 수 있다. dropWhile은 프레디케이트가 처음으로 거짓이 되는 지점까지 발견된 요소를 버린다. 프레디케이트가 거짓이 되면 그 지점에서 작업을 중단하고 남은 요소를 모두 반환한다. dropWhile은 무한한 남은 요소를 가진 무한 스트림에서도 동작한다.
List<Dish> sliceMenu2 = specialMenu.stream()
                .dropWhile(dish -> dish.getCalories() < 320)
                .collect(Collectors.toList());
           // rice, chicken, frenchi fries 반환
2. 스트림 축소

스트림은 주어진 값 이하의 크기를 갖는 새로운 스트림을 반환하는 limit(n) 메서드를 지원한다. 스트림이 정렬되어 있으면 최대 요소 n개를 반환한다. 정렬되지 않은 스트림에도 limit을 사용할 수 있다. 정렬되어 있지 않다면 limit의 결과도 정렬되지 않은 상태로 반환한다.

List<Dish> filterMenu = specialMenu.stream()
                .filter(dish -> dish.getCalories() > 320)
                .limit(3)	// 3개 반환
                .collect(Collectors.toList());
                // rice, chicken, french fries
3. 요소 건너뛰기

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

List<Dish> filterMenu = specialMenu.stream()
                .filter(dish -> dish.getCalories() < 320)
                .skip(2)
                .collect(Collectors.toList());
                // 빈 스트림

 

3. 매핑

스트림 API의 map과 flatMap 메서드는 특정 데이터를 선택하는 기능을 제공한다.

 

1. 스트림의 각 요소에 함수 적용하기

스트림은 함수를 인수로 받는 map 메서드를 지원한다. 인수로 제공된 함수는 각 요소에 적용되며 함수를 적용한 결과가 새로운 요소로 매핑된다.

List<String> dishNames = menu.stream()
                .map(Dish::getName)	// getName은 String을 반환하기때문에, 
 				//	map 메서드의 출력 스트림은 Strema<String> 형식을 갖는다.
                .collect(Collectors.toList());
        
List<String> words = Arrays.asList("Modern", "Java", "In", "Action");
List<Integer> wordLengths = words.stream()
                .map(String::length) // String.length는 Stream<Integer> 형식 반환
                .collect(Collectors.toList());
                
List<Integer> dishNameLengths = menu.stream()
                .map(Dish::getName)
                .map(String::length)
                .collect(Collectors.toList());
// map 메서드 연결로 Integer을 뽑아낸다.

 

2. 스트림 평면화

["Hello", "Wolrd"] -> ["H", "e", "l", "o", "W", "r", "d"] 로 만들어보자.

List<String> words = Arrays.asList("Hello", "World");

words.stream()
	.map(word -> word.split(""))
    .distinct()
    .collect(toList());
  	// Stream<String[]>
String[] arrayOfWords = {"Goodbye", "World"};
Stream<String> streamOfWords = Arrays.stream(arrayOfWords);

words.stream()
	.map(word -> word.split(""))
    .map(Arrays::stream)		// List<Stream<String>>
    .distinct()
    .collect(toList());
List<String> uniqueCharacters = words.stream()
	.map(word -> word.split(""))
    .flatMap(Arrays::stream)		// Stream<String>
    .distinct()
    .collect(toList());

flatMap 메서드는 스트림의 각 값을 다른 스트림으로 만든 다음에

모든 스트림을 하나의 스트림으로 연결하는 기능을 수행한다.

 

4. 검색과 매칭

1. 프레디케이트가 적어도 한 요소와 일치하는지 확인
if(menu.stream().anyMatch(Dish::isVegetarian)) {
	System.out.pritnln("The menu is (somewhat) vegetarian friendly!!");
}

anyMatch 적어도 한 요소와 일치하는지 검사한다.

 

2. 프레디케이트가 모든 요소와 일치하는지 검사
boolean isHealthy = menu.stream()
	.allMatch(dish -> dish.getCalories() < 1000);

allMatch 모든 요소가 일치하는지 검사한다.

boolean isHealthy = menu.stream()
	.noneMatch(d -> d.getCalories() >= 1000);

noneMatch 일치하는 요소가 없는지 확인한다.

 

3. 요소 검색
Optional<Dish> dish = menu.stream()
	.filter(Dish::isVegetarian)			// Stream<Dish>
    	.findAny();

findAny 현재 스트림에서 임의의 요소를 반환한다.

 

4. 첫 번째 요소 찾기
List<Integer> someNumbers = Arrays.asList(1, 2, 3, 4, 5);
Optional<Integer> first = someNumbers.stream()
	.map(n -> n * n)
    	.filter(n -> n % 3 == 0)
    	.findFirst();		// 9
findAny와 findFirst는 병렬성 때문에 사용하는 것이다. 병렬 실행에서는 첫 번째 요소를 찾기 어렵다. 따라서 요소의 반환 순서가 상관없다면 제약이 적은 findAny를 사용하는게 좋다!

 

5. 리듀싱

모든 스트림 요소를 처리해서 값으로 도출하는 연산리듀싱 연산이라고 한다. 함수형 프로그래밍에서는 이 과정이 마치 종이(스트림)를 작은 조각이 될 때까지 반복해서 접는 것과 비슷하다는 의미로 폴드라고 부른다.

 

1. 요소의 합
int sum = 0;
for(int x : numbers)
	sum += x;
// 이 코드에서는 초깃값 0, 리스트의 요소를 조합하는 연산 + 총 2개를 사용했다.    

int sum = numbers.stream().reduce(0, (a, b) -> a + b);

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

  • 초깃값 0
  • 두 요소를 조합해서 새로운 값을 만드는 BinaryOperator<T>
int product = numbers.stream().reduce(1, (a, b) -> a * b);
// 곱셈도 적용 가능하다.

int sum = numbers.stream().reduce(0, Integer::sum);
// Integer가 지원하는 sum을 이용하면 더 편하게 구현이 가능해진다.

 

2. 최댓값과 최솟값
Optional<Integer> max = numbers.stream().reduce(Integer::max);
// 최댓값 출력
Optional<Integer> min = numbers.stream().reduce(Integer::min);
// 최솟값 출력

 

6. 숫자형 스트림

int calories = menu.stream()
	.map(Dish::getCalories)
    	.reduce(0, Integer::sum);
        // 이 코드에는 박싱 비용이 숨어있다.

 

1. 기본형 특화 스트림
int calories = menu.stream()
	.mapToInt(Dish::getCalories)		// IntStream
    	.sum();
        
IntStream intStream = menu.stream().mapToInt(Dish::getCalories);	// Stream<Integer> -> IntStream
Stream<Integer> stream = intStream.boxed(); 	// IntStream -> Stream<Integer>

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

이 외에도 DoubleStream, LongStream을 제공한다.

 

2. 숫자 범위
IntStream evenNumbers = IntStream.rangeClosed(1, 100)
	.filter(n -> n % 2 == 0);		// 2, 4, 6 ... 100
System.out.println(evenNumbers.count());	// 50

rangeClosed(int a, int b)는 a, b의 범위를 포함하지만, range(int a, int b)는 a,b의 범위를 포함하지 않는다.

 

3. 활용 : 피타고라스 수
Stream<int[]> pythoagoreanTriples = IntStream.rangeClosed(1, 100).boxed()
	.flatMap(a -> IntStream.rangeClosed(a, 100)
    		.filter(b -> Math.sqrt(a*a + b*b) % 1 == 0)
            	.mapToObj(b -> new int[]{a, b, (int)Math.sqrt(a*a + b*b)})
    );
    
pythoagoreanTriples.limit(5)
	.forEach(item -> System.out.println(t[0] + ", " + t[1] + ", " + t[2]));
    
// 3, 4, 5
// 5, 12, 13
// 6, 8, 10
// 7, 24, 25
// 8, 15, 17

// 위 코드 개선? -> 정수를 무조건 두번 곱해 검증하는 방식이다.
Stream<double[]> pythoagoreanTriples2 = IntStream.rangeClosed(1, 100).boxed()
	.flatMap(a -> IntStream.rangeClosed(a, 100)
            .mapToObj(b -> new double[]{a, b, Math.sqrt(a*a + b*b)}
            .filter(t -> t[2] % 1 == 0)		// 반드시 정수여야 한다.
    );

 

8. 스트림 만들기

1. 값으로 스트림 만들기
Stream<String> stream = Steram.of("Modern ", "Java ", "In ", "Action");
// 문자열로 된 Stream 생성
stream.map(String::toUpperCase)
	.forEach(System.out::println);
    // MODERN JAVA IN ACTION
   
Stream<String> emptyStream = Stream.empty();		// 빈 String Stream 생성

 

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

** 자바 9에서 새롭게 추가된 메서드다.

String homeValue = System.getProperty("home");
Stream<String> homeValueStream = homeValue == null ? Stream.empty() : Stream.of(homeValue);
// 자바 9 이전의 프로퍼티 스트림 객체 생성 코드

Stream<String> values = Stream.of("config", "home", "user")
	.flatMap(key -> Stream.ofNullable(System.getProperty(key)));
// 자바 9 이후의 스트림 객체 생성 코드    

Stream<String> values = Stream.of("config", "home", "user")
	.flatMap(key -> Stream.ofNullable(System.getProperty(key)));
// 프로퍼티의 생성 코드

 

3. 배열로 스트림 만들기
int[] numbers = {2, 3, 5, 7, 11, 13};
int sum = Arrays.stream(numbers)	// IntStream을 반환한다.
	.sum();
// sum = 141

Arrays.stream 은 배열을 정수로 받는다.

 

4. 파일로 스트림 만들기
long uniqueWords = 0;
try(Stream<String> lines = Files.lines(Paths.get("data.txt"),
	Charset.defaultCharset())) {		// AutoCloseable을 구현해야만 함.
    long uniqueWords = lines.flatMap(line -> Arrays.stream(line.split(" ")))
        .distinct()
        .count();
} catch(IOException e) {
	...
}

 

5. 함수로 무한 스트림 만들기

iterate 메서드

Stream.iterate(0, n -> n+2)
	.limit(10)
    .forEach(System.out::println);
// 끝이 없이 연산하기때문에 무한 스트림이다. limit을 걸어 제한을 둬야한다.

Stream.iterate(new int[]{0, 1}, t -> new int[]{t[1], t[0] + t[1]})
	.limit(10)
    .forEach(t -> System.out.println("(" + t[0] + "," + t[1] + ")"));
// (0, 1), (1, 1), (1, 2) ...
// 피보나치 수열을 만들수도 있다.

Stream.iterate(new int[]{0, 1}, t -> new int[]{t[1], t[0] + t[1]})
	.limit(10)
    .map(t -> t[0])
    .forEach(System.out::println);
// 0, 1, 1, 2, 3 ...    
// map을 만들어 앞 글자로 일반적인 피보나치 수열을 만들수도 있다.

Stream.iterate(0, n -> n + 4)
        .takeWhile(n -> n < 100)
        .forEach(System.out::println);
// takeWhile을 이용해 조건까지 연산을 시킬수도 있다.

 

generate 메서드

Stream.generate(Math::random)
	.limit(5)
    .forEach(System.out::println);

** generate 메서드는 가변상태 객체다. 병렬 환경에 안좋기때문에, 굳이 병렬 환경에 적용해야한다면 iterate를 쓰는것이 좋다.