모던자바 인액션 5장

su dong·2023년 8월 22일
1

질문거리

스트림연산 : 상태 없음과 있음 -> sorted나 distinct가 내부 상태를 갖는 연산인 이유

Optional은 Integer, String 등의 참조 형식으로 파라미터화할 수 있다. ?란 말의 뜻


스트림 활용

스트림 api가 제공하는 다양한 연산을 알아보자.


5.1 필터링

프레디케이트 필터링과 고유 요소만 필터링 하는 방법

5.1.1 프레디케이트로 필터링

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

List<Dish> vegetarianMenu = menu.stream()
							.filter(Dish::isVegitarian)
                            .collect(toList());

5.1.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)


5.2 스트림 슬라이싱

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

5.2.1 프레디케이트를 이용한 슬라이싱

TAKEWHILE 활용

만약 320칼로리 이하의 음식을 찾을때 어떤 방법을 사용할 수 있을까? filter가 가장 먼저 떠오를 것이다.

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

하지만 위 코드는 한가지 문제점이 있다. 바로 전체 stream을 모두 조회해야한다는 것이다. 만약 소스가 정렬된 상태라면 320칼로리가 넘어가는 탐색을 멈춰설 수 있지 않을까?
정렬된 소스에 대해서 takewhile을 사용하여 다음과 같이 바꿀 수 있다

List<Dish> filterMenu1 = 
specialMenu.stream()
			.takeWhile(dish -> dish.getCalories < 320)
            .collect(toList());

DROPWHILE 활용

320보다 큰 요소를 선택하려면 어떻게 해야할까? dropwhile을 사용하면 이 작업을 수행할 수 있다.

List<Dish> filterMenu1 = 
specialMenu.stream()
			.dropWhile(dish -> dish.getCalories < 320)
            .collect(toList());

5.2.2 스트림 축소

스트림은 주어진 값 이하의 크기를 갖는 스트림을 반환하는 limit(n)메서드를 지원한다.
다음은 320칼로리 이하의 메뉴 3개를 반환하는 코드다.

List<Dish> filteredMenu = 
specialMenu.stream()
			.filter(dish -> dish.getCalories()<320)
            .limit(3)
            .collect(toList());

5.2.3 요소 건너뛰기

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

다음은 300칼로리 이상의 음식 2개를 건너뛴 후 나머지 요리를 반환한다.

List<Dish> dishes = menu.stream()
					.filter(d-> d.getCalories > 300)
                    .skip(2)
                    .collect(toList());


5.3 매핑

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

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

List<Dish> dishNames = menu.stream()
						.map(Dish::getName)
                        .collect(toList());

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

다른 예시를 하나 더 보자

List<Integer> wordLength = words.stream()
							.map(String::length)
                            .collect(toList());

이렇게 연결하는 것도 가능하다

List<Dish> dishNames = menu.stream()
						.map(Dish::getName)
                        .map(String::length)
                        .collect(toList());

5.3.2 스트림 평면화

위를 응용해서 리스트에서 고유 문자로 이뤄진 리스트를 반환해보자.
예를 들어 word = ["Hello","World"]리스트가 있다면 결과로 ["H","e","l","o","W","r","d"]가 반환되어야 한다.

map과 distinct 사용

words.stream()
.map(word -> word.split(""))
.distinct()
.collect(toList());

위 코드에서 map으로 전달한 람다는 각 단어의 String[]을 전달한다는 것이 문제다. 즉 반환한 스트림의 형식은 Stream< String[]>이다. 우리가 원하는 것은 문자열의 스트림을 표현할 Stream< String>이다.

위 문제를 flatMap을 사용해서 해결할 수 있다.

map과 Arrays.stream 활용

List<Stream<String>> collect = words.stream()
            .map(word -> word.split("")) // 각 단어를 별도 배열로 생성
            .map(Arrays::stream) // 각 배열을 별도의 스트림으로 생성
            .distinct()
            .collect(toList());
            
//collect = [java.util.stream.ReferencePipeline$Head@79b4d0f, java.util.stream.ReferencePipeline$Head@6b2fad11]

이 방법도 List<Stream< String>> 을 반환하므로 실패한다.


flatMap 사용

flatMap을 활용하면 다음으로 문제를 해결할 수 있다.

List<String> uniqueCharacters = 
	words.stream()
	    .map(word -> word.split("")) // 각 단어를 별도 배열로 생성
        .flatMap(Arrays::stream) // 생성된 스트림을 하나의 스트림으로 평면화
        .distinct()
        .collect(toList());

flatMap은 각 배열을 스트림이 아니라 스트림의 콘텐츠로 매핑한다. 즉, map(Arrays::stream)과 달리 flatMap은 하나의 평면화된 스트림을 반환한다. 스트림의 각 값을 다른 스트림으로 만든 다음에 모든 스트림을 하나의 스트림으로 연결하는 기능을 수행한다.

다른 예제를 하나 더 보자
number1과 number2가 주어졌을 때 각각을 쌍지어서 나타낼 수 있는 모든 조합을 찾는 문제이다.

.map(j -> new int[]{i,j})를 사용하고 싶은 것이니깐, 그 밖의 map을 flatMap으로 바꿔주면 된다.

그러면 감싸고 있던 Stream이 하나 벗겨지면서 collect 했을 때 int[]만 가져올 수 있다.


5.4 검색과 매칭

특정 속성이 데이터 집합에 있는지 여부를 검색하는 데이터 처리도 자주 사용된다.


5.4.1 프레디케이트가 적어도 한 요소와 일치하는지 확인

프레디케이트가 주어진 스트림에서 적어도 한 요소와 일치하는지 확인할 때 anyMatch 메서드를 이용한다.

if(menu.stream().anyMatch(Dish::isVegetarian)){
	System.out.println("The menu is (somewhat) vegetarian friendlty!!");
}

anyMatch는 불리언을 반환하는 최종 연산이다.


5.4.2 프레디케이트가 모든 요소와 일치하는지 검사

allMatch 메서드는 anyMatch와 달리 스트림의 모든 요소가 주어진 프레디케이트와 일치하는지 검사한다.

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

NONEMATCH

NoneMatch는 allMatch와 반대 연산을 수행한다. 즉 주어진 프레디케이트와 일치하는 요소가 없는지 확인한다.
boolean isHealthy = menu.stream().noneMatch(dish -> dish.getCalories >= 1000);

anyMatch, allMatch, noneMatch는 스트림 쇼트서킷 기법, 즉 자바의 &&, ||와 같은 연산을 활용한다.

쇼트서킷: 모든 스트림의 요소를 처리하지 않고 결과를 반환할 수 있다.


5.4.3 요소 검색

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

Optional<Dish> dish = 
	menu.stream()
    .filter(Dish::isVegetarian)
    .findAny()
    .ifPresent(dish -> System.out.println(dish.getName());
    //값이 있으면 출력되고, 값이 없으면 아무일도 일어나지 않는다. 

findAny는 아무 요소도 반환하지 않을 수 있다. null은 쉽게 에러를 일으킬 수 있으므로 자바 8설계자들은 Optional< T>를 만들었다.(10장에서 자세히 설명)


5.4.4 첫 번째 요소 찾기

숫자 리스트에서 3으로 나누어떨어지는 첫번째 제곱값을 반환하는 코드

List<Integer> someNumbers = Arrays.asList(1,2,3,4,5);
Optional<Integer> firstSquareDivisibleByThree = 
	someNumbers.stream()
			    .filter(i -> i%3 == 0)
                .findFirst();

findAny vs findFirst
병렬처리에서는 첫번째 요소를 찾기 어렵다. 그렇기 때문에 병렬처리에서 순서가 상관 없을 때는 findAny를 사용한다.


5.5 리듀싱

지금까지는 불리언, void, Optional, List등으로 반환하는 것을 알아봤다. 이 절에서는 리듀스 연산을 활용해서, '메뉴의 모든 칼로리 합계를 구하시오', '메뉴에서 칼로리가 가장 높은 요소는' 과 같은 질문에 답을 구하는 방법을 공부한다.

위 작업들을 하기 위해서는 모든 스트림 요소를 사용해야한다.

리듀싱 연산은 모든 스트림 요소를 처리해서 값으로 도출하는 작업이다.


5.5.1 요소의 합

기존의 fore-each문을 활용한 요소의 합

int sum = 0;
for(int x : numbers){
	sum+=x;
}

스트림 reduce를 사용한 코드

reduce는 두개의 인수를 갖는다.
1) 초깃값
2) 두 요소를 조합해서 새로운 값을 만드는 BinaryOperator< T>

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

매서드 참조를 사용할 수도 있다.

int sum = numbers.stream().reduce(0,Integer::sum);

초기값이 없는 reduce

Optional<Integer> sum = numbers.stream().reduce(Integer::sum);

이런 상황이라면 스트림에 아무 요소도 없을 경우 null을 반환하므로 Optional객체로 감싸서 반환된다.


5.5.2 최댓값과 최솟값

최댓값

Optional<Integer> max = numbers.stream().reduce(Integer::max);

최솟값

Optional<Integer> min = numbers.stream().reduce(Integer::min);

리듀스 메서드의 장점

reduce를 이용하면 내부 반복이 추상화되면서 내부 구현에서 병렬로 reduce를 실행할 수 있게 된다.


스트림연산 : 상태 없음과 있음

map, filter등은 입력 스트림에서 각 요소를 받아 결과를 출력 스트림으로 보낸다. 따라서 이들은 보통 상태가 없는 , 즉 내부 상태를 갖지 않는 연산이다.

반면 reduce, sum, max 같은 연산은 결과를 누적할 내부 상태가 필요하다. 스트림에서 처리할 요소 수와 관계 없이 내부 상태의 크기는 한정되어 있다.

반면 sorted, distinct같은 연산은 filter나 map처럼 스트림을 입력으로 받아 다른 스트림을 출력하는 것처럼 보일 수 있다. 하지만 다르다. 이들 또한 과거 이력을 알고 있어야 한다. 예를 들어 어떤 요소를 출력 스트림에 추가하려면 모든 요소가 버퍼에 추가되어 있어야 한다. 연산을 수행하는데 필요한 저장소 크기는 정해져있지 않다. 따라서 데이터 스트림의 크기가 크거나 무한이라면 문제가 생길 수 있다.
이런 연산을 내부 상태를 갖는 연산이라고 한다.(175페이지)


5.7 숫자형 스트림

int calories = menu.stream()
				.map(Dish::getCalories)
                .reduce(0,Integer::sum);

위의 코드는 박싱 비용이 숨어 있다. 내부적으로 합계를 계산하기 전에 Integer를 기본형으로 언박싱해야한다.

다음과 같이 계산할 순 없을까?

int caloires = menu.stream()
				.map(Dish::getCalories)
                .sum();

하지만 위 코드처럼 바로 sum을 호출할 수는 없다. map메서드가 Stream< T>를 생성하기 때문이다. 스트림의 요소 형식은 Integer이지만, 인터페이스에는 sum메서드가 없다.
다행히도 스트림 api는 숫자 스트림을 효율적으로 처리할 수 있도록 기본형 특화 스트림을 제공한다.


5.7.1 기본형 특화 스트림

박싱을 피하기 위한 3가지가 있다. int요소에 특화된 IntStream, double 요소에 특화된 DoubleStream, long 요소에 특화된 LongStream이 그것이다.

각각의 인터페이스는 sum, max 같이 자주 사용하는 숫자 관련 리듀싱 연산 수행 메서드를 제공한다. 또 필요할 때 다시 객체 스트림을 복원하는 기능도 제공한다.

특화 스트림은 오직 박싱 과정에서 일어나는 효율성과 관련있으며 스트림에 추가 기능을 제공하지는 않는다는 사실을 기억하자.


숫자 스트림으로 매핑

int calories = menu.stream() // Stream<Dish>반환
				.mapToInt(Dish::getCalories) // IntStream 반환
                .sum()

intstream을 반환하므로 intStream에서 제공하는 sum 메서드를 이용해서 칼로리 합계를 구할 수 있다. 스트림이 비어있으면 sum은 0을 반환한다.

IntStream에서 제공하는 메서드

range(), rangeClose(), sum(), min(), max(), average()

객체 스트림으로 복원하기

숫자 스트림을 만든 다음에, 원상태인 특화되지 않은 스트림으로 복원할 수 있을까? IntStream의 map연산은 'int를 인수로 받아서 int를 반환하는 람다'를 인수로 받는다. 하지만 정수가 아닌 Dish 같은 다른 값을 반환하고 싶으면 어떻게 해야할까?

다음 예제처럼 boxed 메서드를 이용해서 특화 스트림을 일반 스트림으로 변환할 수 있다.

IntStream intStream = menu.stream().mapToInt(Dish::getCalories); // 스트림을 숫자 스트림으로 변환
Stream<Integer> stream = intStream.boxed(); // 숫자 스트림을 스트림으로 변환

기본값 : OptionalInt

IntStream에서 최대값을 구할 때, 0이라는 기본값 때문에 잘못된 결과가 도출 될 수도 있다. 스트림에 요소가 없는 상황과, 실제 최대값이 0인 상황을 어떻게 구분할 수 있을까?

이전에 값이 존재하는지 여부를 가리킬 수 있는 컨테이너 클래스 Optional을 언급한 적이 있다. Optional은 Integer, String 등의 참조 형식으로 파라미터화할 수 있다. 또한 OptionalInt, OptionalDouble, Optionallong 세 가지 기본형 특화 스트림 버전도 제공한다.

OptionalInt를 사용해서 IntStream의 최대값 요소를 찾아보자.

OptionalInt maxCalories = menu.stream()
								.mapToInt(Dish::getCalories)
                                .max();
                                
int max = maxCalories.orElse(1); // 값이 없을 때 기본 최대값을 명시적으로 설정

5.7.2 숫자 범위

특정 범위의 숫자를 이용해야하는 상황이 자주 발생한다.
range메서드는 시작값과 종료값이 포함되지 않고,
rangeClosed는 시작값과 종료값이 포함된다.

IntStream evenNumbers = IntStream.rangeClosed(1,100)
									.filter(n -> n%2 == 0);
System.out.println(evenNumbers.count());

피타고라스 수 만들기 예제

Stream<int[]> pythagoreanTriples = 
	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)})
     );

5.8 스트림 만들기

지금까지 배운 컬렉션에서 스트림 만들기, 범위 숫자의 스트림 만들기 외의 스트림 만드는 방법에 대해서 알아보자.

5.8.1 값으로 스트림 만들기

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

다음처럼 값을 비울수도 있다.

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

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

자바 9에서는 null이 될 수 있는 개체를 스트림으로 만들 수 있는 새로운 메소드가 추가되었다. 때로는 null이 될 수 있는 객체를 스트림으로 만들어야 한다. 예를 들어 System.getProperty("home")는 제공된 키에 대응되는 속성이 없으면 Null을 반환할 수 있다. 이런 메소드를 스트림에 활용하려면 다음처럼 null을 명시적으로 확인해야 한다.

Stream<String> values = 
Stream.of("config","hoem","user").flapMap(key -> Stream.ofNullable(System.getProperty(key));

5.8.3 배열로 스트림 만들기

int[] numbers = {2,3,5,7,11,13};
int sum = Arrays.stream(numbers).sum();

5.8.4 파일로 스트림 만들기

long uniqueWords = 0;
try(Stream<String> lines = 
	File.lines(Path.get("data.txt"), Charset.defaultCharset())){
    uniqueWords = lines.flatMap(line -> Arrays.stream(line.split(" ")))
    .distinct()
    .count();
}
catch(IOException e){
	//파일열다가 발생한 예외를 처리한다.
}

5.8.5 함수로 무한 스트림 만들기

스트림 API는 함수에서 스트림을 만들 수 있는 두 정적 메서드 Stream.iterate와 Stream.generate를 제공한다. 두 연산을 이용해서 무한스트림 즉 고정된 컬렉션에서 고정된 크리고 스트림을 만들었던 것과 달리 고정되지 않은 스트림을 만들 수 있다. 보통은 무한한 값을 출력하지 않도록 limit(n)함수를 함께 연결해서 사용한다. 이런 스트림을 언바운드 스트림이라고 한다.

Iterate 메서드

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

피보나치 수열 만들어보기

Stream.iterate(new int[]{0,1}, t-> new int[]{t[1], t[0]+t[1]}
  .limit(20)
  .forEach(System.out.println("("+t[0] + "," + t[1]+")"));

자바 9에서는 프레디케이트도 지원한다.

IntStream.iterate(0, n-> n<100, n-> n+4)
.forEach(System.out::println);

IntStream.iterate(0, n-> n+4)
.takeWhile(n -> n<100)
.forEach(System.out::println);

generate 메서드

iterate와 다르게 generate는 생산된 각 값을 연속적으로 계산하지 않는다.
generate는 Supplier< T>를 인수로 받아서 새로운 값을 생산한다.

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

즉, 상태가 없는 메서드에 사용하면 된다. 또한 병렬 코드에서 발행자의 상태가 있으면 안전하지 않다.

피보나치수열을 generate로 만드는 예제를 살펴보자.

IntSupplier fib = new IntSupplier(){
	private int previous = 0;
    private int current = 1;
    public int getAsInt(){
    	int oldPrevious = this.previous;
        int nextValue = this.previous + this.current;
        this.previous = this.current;
        this.current = nextValue;
        return oldPrevious;
    }
};

IntStream.generate(fib).limit(10).forEach(System.out::println);

위 코드에서는 IntSupplier 인스턴스를 만들었다. 만들어진 객체는 기존 피보나치 요소와 두 인스턴스 변수에 어떤 피보나치 요소가 들어있는지 추적하므로 가변 상태 객체이다.
getAsInt를 호출하면 객체 상태가 바뀌며 새로운 값을 생산한다. iterate를 사용했을 때는 각 과정에서 새로운 값을 생성하면서도 기존 상태를 바꾸지 않는 순수한 불변 상태를 유지했다. 스트림을 병렬로 처리하면서 올바른 결과를 얻으려면 불변 상태 기법을 고수해야한다는 사시을 7장에서 배울 것이다.

profile
사람들을 돕는 문제 해결사, 개발자 sudong입니다. 반갑습니다. tkddlsqkr21@gmail.com

0개의 댓글