스터디플래너/공부하기

[자바의 정석] Ch.14 람다와 스트림

2022. 9. 10. 11:57

2.4 Optional<T>와 OptionalInt

Optional은 null이 아닌 값을 가질 수도 있고 가지지 않을 수도 있는 객체를 담는 클래스이다. 만약 값이 있다면 isPresent()메소드를 사용할 때 true를, 값이 없다면 빈 것으로 판단하여 false를 반환한다. orElse()메소드를 사용하면 값이 있다면 그 값을, 없을 때는 기본값을 반환한다. 값이 Null인지 아닌지 알 수 없을 때 ofNuallble() 메소드를 사용하면 null일 때 NullPointerException으로 처리하지 않고 Optional 객체를 만들 수 있다. 덕분에 if(a == null)와 같이 null인지 여부를 확인하기  위해 조건문을 여러 개 작성하여 코드를 복잡하게 작성할 필요가 없어졌다.

 

Optional을 이해하기위해 검색하다가 찾은 사이트이다. 왜 Optional이라는 클래스가 생겼는지 이해할 수 있어 읽어보는게 좋을 것 같다.

https://www.daleseo.com/java8-optional-before/

 

자바8 Optional 1부: 빠져나올 수 없는 null 처리의 늪

Engineering Blog by Dale Seo

www.daleseo.com

https://www.daleseo.com/java8-optional-after/

 

자바8 Optional 2부: null을 대하는 새로운 방법

Engineering Blog by Dale Seo

www.daleseo.com

https://www.daleseo.com/java8-optional-effective/

 

자바8 Optional 3부: Optional을 Optional답게

Engineering Blog by Dale Seo

www.daleseo.com

 

Optional객체 생성하기

Stream<String> lineStream = Arrays.stream(lineArr);
lineStream.flatMap(line -> Stream.of(line.split(" +")))
        .map(String::toLowerCase)
        .distinct()
        .sorted()
        .forEach(System.out::println);

바로 앞 예제에서 이런 코드를 사용했다. 만약 of()의 매개변수가 null이라면 lineStream을 출력해도 null이라 Null Pointer Exception(NPE)를 출력할 것이다. 따라서 위에서 언급했던 ofNullable()을 사용해야 한다. 

Optional isNull = Optional.empty();

Optional 객체를 생성할 때는 참조변수 값을 Optional.empty()로 초기화한다. empty()는 empty Optional instance를 반환한다.

 

Optional객체의 값 가져오기

 Optional 객체에 저장된 값을 읽어올 때 get() 메소드를 사용할 수 있다. 만약 null이 아니라는데 전재산을 걸 수 있다면 get()을 사용해도 좋지만 그렇지 않다면 orElse()메소드를 사용하여 대체할 기본값을 정해두는 것이다. get()메소드를 사용하여 Optional에 저장된 값을 받아와 출력하는데 null이 저장되어 있다면 NPE가 발생하여 Optional 객체를 생성한 이유가 없기 때문이다. 그 외에도 만약 값이 존재한다면 그 값을 반환하고 그렇지 않으면 supplying 함수를 이용하여 결과값을 반환할 수 있는 orElseGet()과 대체값도 싫고 supplying 함수를 이용한 결과값도 싫다면 원하는대로 예외처리할 수 있는 orElseThrow()도 있다.

 

 Optional객체도 Stream처럼 filter(), map(), flatMap()을 사용할 수 있다. 만약 Optional객체의 값이 null이라면 메서드는 아무 것도 하지 않는다. 처음에 언급한 isPresent()는 Optional 객체의 값이 null이면 false, 아니면 true를 반환하는데 1단계처럼 조건식을 사용하는 것보다 2단계처럼 사용하는 것이 좋다. ifPresent() 메소드를 사용하면 앞 두 문장보다 훨씬 간단하게 사용할 수 있다. 만약 null이 아니라면 값을 출력하고 null이라면 아무것도 하지 않는다. ifPresent()는 findAny(), findFirst()와 같은 최종 연산과 잘 어울린다고 한다.

// 1단계
if(str != null)
	System.out.println(str);

// 2단계
if(Optional.ofNullable(str).isPresent())
	System.out.println(str);

// 3단계
Optional.ofNullable(str).ifPresent(System.out::println);

OptionalInt, OptionalLong, OptionalDouble

기본형 스트림인 IntStream, LongStream, DoubleStream에서도 Optional을 기본값으로 하는 OptionalInt, OptionalLong, OptionalDouble을 반환한다. 다른 메소드 이름은 동일하나 Optional값을 반환하는 get()메소드의 경우 각각 getAsInt(), getAsLong(), getAsDouble()로 이름이 조금 다르니 이 메소드만 주의하면 된다.

더보기
import java.util.*;
import java.util.stream.*;

public class StreamEx {
    public static void main(String[] args) {
        Optional<String> optStr = Optional.of("abcde");
        Optional<Integer> optInt = optStr.map(String::length);
        System.out.println("optStr = " + optStr.get());
        System.out.println("optInt = " + optInt.get());

        int result1 = Optional.of("123")
                      .filter( x -> x.length() > 0)
                      .map(Integer::parseInt).get();

        int result2 = Optional.of("")
                      .filter( x -> x.length() > 0)
                      .map(Integer::parseInt).orElse(-1);

        System.out.println("result1 = " + result1);
        System.out.println("result2 = " + result2);

        Optional.of("456").map(Integer::parseInt)
                          .ifPresent( x -> System.out.printf("result3 = %d%n", x));

        OptionalInt optInt1 = OptionalInt.of(0);
        OptionalInt optInt2 = OptionalInt.empty();

        System.out.println(optInt1.isPresent());
        System.out.println(optInt2.isPresent());

        System.out.println(optInt1.getAsInt());
        //System.out.println(optInt2.getAsInt());
        System.out.println("optInt1 = " + optInt1);
        System.out.println("optInt2 = " + optInt2);
        System.out.println("optInt1.equals(optInt2)?" + optInt1.equals(optInt2));

        Optional<String> opt = Optional.ofNullable(null);
        Optional<String> opt2 = Optional.empty();
        System.out.println("opt = " + opt);
        System.out.println("opt2 = " + opt2);
        System.out.println("opt.equals(opt2)?" + opt.equals(opt2));

        int result3 = optStrToInt(Optional.of("123"), 0);
        int result4 = optStrToInt(Optional.of(""), 0);

        System.out.println("result3 = " + result3);
        System.out.println("result4 = " + result4);
    }

    static int optStrToInt(Optional<String> optStr, int defaultValue){
        try{
            return optStr.map(Integer::parseInt).get();
        }catch (Exception e){
            return defaultValue;
        }
    }
}

 

2.5 스트림의 최종 연산

스트림의 최종연산은 스트림의 요소를 소모해서 결과를 만든다. 최종 연산 후에는 스트림이 닫혀 더이상 사용할 수 없다. 

forEach()

반환타입이 void로 스트림의 요소를 출력하는 용도로 많이 사용한다.

조건 검사 - allMatch(), anyMatch(), noneMatch(), findFirst(), findAny()

boolean allMatch(Predicate<? super T> predicate)
boolean anyMatch(Predicate<? super T> predicate)
boolean noneMatch(Predicate<? super T> predicate)

위 메소드를 이용하면 스트림 요소에 대해 지정된 조건에 모든 요소가 일치하는지, 일부가 일치하는지 아니면 아무것도 일치하지 않는지 확인할 수 있다.

filter()와 함께 사용하여 스트림 요소 중 조건에 일치하는 첫번째 요소를 반환하는 findFirst()가 있고, 병렬 스트림의 경우 findAny()를 사용해야 한다. findFirst()와 findAny()의 반환 타입은 Optional<T>이다.

통계 - count(), sum(), average(), max(), min()

long count()
Optional<T> max(Comparator<? super T> comparator)
Optional<T> min(Comparator<? super T> comparator)

기본형 스트림이 아닌 경우 위 세 가지 통계 메소드만 사용할 수 있다. 다른 통계 메소드는 기본형 스트림으로 변환하거나 reduce() collect()를 사용해서 통계 정보를 얻어야 한다.

리듀싱 - reduce()

Optional<T> reduce(BinaryOperator<T> accumulator)
T reduce(T identity, BinaryOperator<T> accumulator)
U reduce(U identity, BiFunction<U, T, U> accumulator, BinaryOperator<U> combiner)

 reduce()는 BinaryOperator<T>를 매개변수로 받아 스트림의 요소를 줄여나가면서 연산을 수행하고 최종 결과를 반환하는 메소드이다. 스트림을 하나씩 소모하고 모든 요소를 소모하면 그 결과를 반환한다. 초기값을 갖는 reduce()는 초기값과 스트렘의 첫 번째 요소로 연산을 시작한다. 스트림의 요소가 하나도 없다면 초기값이 반환된다. combiner는 병렬 스트림에 의해 처리된 결과를 합칠 때 사용하기 위해 사용하는 것이다.

통계 메소드인 count(), max(), min()도 내부적으로는 redcue를 이용한다.

 

 

2.6 collect()

Collector 인터페이스는 입력 요소로 계산한 결과값으로 다시 계산하여 모든 입력 요소를 처리한 뒤, 조건에 따라 계산된 결과를 최종 결과값으로 한다. 리듀싱은 순차적으로 수행되거나 병렬적으로 수행한다. Collectors 클래스는 Collecotor 인터페이스를 구현한 것이다.  여러가지 조건에 따라 요소를 요약하고, 계산된 요소를 컬렉션으로 계산하는 등 여러가지 유용한 reduction 작업을 구현한다. collect()는 스트림 요소를 수집하려면 어떻게 수집할 것인가에 대한 방법을 정의한 것이다. sort() 메소드를 사용할 때 Comparator 클래스가 필요한 것처럼 collect() 메소드를 사용할 때는 Collector가 필요하다. collect()메소드의 매개변수 타입이 Collector인데, 매개변수가 Collector를 구현한 클래스의 객체여야 한다는 뜻이다.

 

스트림을 컬렉션과 배열로 변환 - toList(), toSet(), toMap(), toCollection(), toArray()

Collectors클래스의 toList(), toSet(), toMap(), toCollection(), toArray()와 같은 메소드를 사용하면 스트림의 모든 요소를 컬렉션으로 수집할 수 있다. List나 Set가 아니라 특정 컬렉션을 지정하려면 toCollection()의 매개변수로 해당 컬렉션의 생성자 참조를 넣어주면 된다. Map은 키와 값이 쌍으로 필요하므로 어떤 필드를 키로 사용하고 어떤 필드를 값으로 할 지 정해줘야 한다.

List<String> name = stuStream.map(Student::getName).collect(Collectors.toList());
ArrayList<String> list = names.stream().collect(Collectors.toCollection(ArrayList::new));
Map<String, Person> map = personStream.collect(Collectors.toMap( p -> p.getRegId(), p -> p));

스트림에 저장된 요소를 'T[]' 타입의 배열로 변환하려면 toArray()를 사용하면 된다. 해당 타입의 생성자 참조를 매개변수로 지정하거나 Object[]타입으로 받지 않으면 에러가 발생한다.

 

통계 - counting(), summingInt(), averagingInt(), maxBy(), minBy()

 collect()를 이용하여 count(), sum(), average(), max(), min() 메소드를 사용하여 얻은 통계 정보를 얻을 수 있다. 나중에 groupingBy()와 함께 사용하면 counting(), summingInt(), averageInt(), maxBy(), minBy() 메소드가 왜 필요한 지 알 수 있다.

long count = stuStream.count();
long count = stuStream.collect(counting());

 

리듀싱 - reducing()

IntStream에는 collect()메소드를 사용할 때 Supplier<R> supplier, ObjIntConsumer<R> accumulator, BiConsumer<R, R> combiner 총 세 가지 매개변수가 필요하다. boxed()를 이용하면 IntStream을 Stream<Integer>로 변환하면 매개변수 1개짜리 collect()를 쓸 수 있다.

OptionalInt max = intStream.reduce(Integer::max);
Optional<Integer> max = intStream.boxed().collect(reducing(Integer::max));

 

 

문자열 결합 - joinning()

문자열 스트림의 모든 요소를 하나의 문자열로 연결해서 반환한다. 구분자를 지정할 수있고 접두사와 접미사도 지정할 수 있다. 스트림의 요소가 String이나  StringBuffer처럼 CharSequence의 자손인 경우에만 결합이 가능하여 문자열이 아닌 경우라면 map()을 이용하여 스트림의 요소를 문자열로 변환해야 한다.

그룹화와 분할 - groupingBy(), partitioningBy()

groupingBy()는 스트림의 요소를 Function으로, partitioningBy()는 Predicate로 분류한다. 그룹화와 분할의 결과는 Map에 담겨 반환한다.

partitioningBy()에 의한 분류

groupingBy()에 의한 분류

 

2.7 Collector 구현하기

Collecor 인터페이스를 구현할 때 새로운 결과값을 담을 방법을 제공하는 supplier(),  새로운 데이터 요소를 결과값으로 합할 방법을  accumulator(), 두 개의 결과값을 하나로 결합할 방법을 제공하는 combiner(), 결과를 최종적으로 변환시킬 finisher()를 정해야 한다. 네 개의 메서드 모두 반환타입이 함수형 인터페이스로 람다식 네 개가 필요하다.

 추가로 characteristics()는 컬렉터가 수행하는 작업의 속성에 대한 정보를 제공하는 것인데 병렬로 처리할 수 있는 작업 정보 Character.CONCURRENT, 스트림의 요소의 순서가 유지될 필요가 없는 작업 Characteristics.UNORDERED, finisher()가 항등함수인 Chrateristics.IDENTIFY.FINISH가 있다. 만약 아무 속성도 지정하지 않으려면 1번과 같이 하고 해당하는 것은 2번처럼 Set에 담아 반환하도록 구현하면 된다.

// 1번
Set<Characteristics> characteristics(){
	return Collections.emptySet();
}


// 2번
public Set<Characteristics> characteristics(){
	return Collections.unmodifiableSet(EnumSet.of(
    				Collectors.Characteristics.CONCURRENT,
		            Collectors.Characteristics.UNORDERED
            });
}

2.8 스트림의 변환

1. 스트림 → 기본형 스트림

2. 기본형 스트림  스트림

3. 기본형 스트림  개본형 스트림

4. 스트림  부분 스트림

5. 두 개의 스트림  스트림

6. 스트림의 스트림  스트림

7. 스트림  병렬 스트림

 

8. 스트림  컬렉션

9. 컬렉션  스트림

10. 스트림  Map

11. 스트림  배열