반응형

가독성 높이는 습관 - Optional 살펴보기

 

Optional (JDK 1.8+)

[T] : Optional<T>

[null] : Optional.empty()

 

 

Optional 장점

  • Null을 직접 핸들링하지 않음
  • Null 여부를 타입만으로 나타낼 수 있음
  • Chaining을 통한 중간 및 종단 처리 가능

 

 

Optional 내부 구현

// private constructor
private Optional() {
    this.value = null;
}

private Optional(T value) {
    this.value = Objects.requireNonNull(value);
}
// static factory method
private static final Optional<?> EMPTY = new Optional<>();

public static<T> Optional<T> empty() {
    @SuppressWarnings("unchecked")
    Optional<T> t = (Optional<T>) EMPTY;
    return t;
}

public static <T> Optional<T> of(T value) {
    return new Optional<>(value);
}

public static <T> Optional<T> ofNullable(T value) {
    return value == null ? empty() : of(value);
}
  • Optional 사용만 한다고 해서 NPE을 피할 수 있는 것은 아님
  • 만약, null이 필요한 상황이나 null이 가능한 객체를 Optional로 감쌀 때는 'of'가 아닌 'ofNullable' 메서드를 사용해야 함

 

pulbic boolean isPresent() {
    return value != null;
}
public void ifPresent(Consumer<? super T> consumer) {
    if (value != null)
        consumer.accept(value);
}
  • Null이 아닐 때만 해당 메서드가 실행되므로, 해당 함수는 null에 안전한 함수

 

public T get() {
    if (value == null) {
        throw new NoSuchElementException("No value present");
    }
    return value;
}
  • get은 Optional의 메서드 중 가장 위험한 함수
  • 가능하다면 사용하지 말 것
  • 항상 값이 존재할 때만 호출되어야 하므로 isPresent()로 확인 후 사용

 

public T ofElse(T other) {
    return value != null ? value : other;
}
  • get에 비해 약간 더 안전함
  • Optional이 비었을 때 ofElse 파라미터로 전달된 other 값을 반음 (default 값 반환)

 

public T orElseGet(Supplier<? extends T> other) {
    return value != null ? value : other.get();
}
  • orElse와 다르게 파라미터를 Supplier로 받는다는 차이가 있음
  • default 값이 아닌 Supplier function으로 값을 받도록 설계된 이유는 orElseGet 내부 시그니처에서 other.get()
  • 즉, 넘겨받는 Supplier가 실제 필요할 때까지 해당 함수가 실행되지 않음
  • 만약에 default 값이 DB를 조회한다거나 다른 API를 호출한다고 했을 때 해당 값의 평가 자체가 지연되므로,
  • 실제 해당 default 값이 필요할 때까지 코드의 수행을 지연시킬 수 있음

 

public <X extends Throwable> T orElseThrow(Supplier<? extends X> exceptionSupplier) throw X {
    if (value != null) {
        return value;
    } else {
        throw exceptionSupplier.get();
    }
}
  • null일 때 exception을 발생시키고, null이 아닐 때 내부값을 리턴
  • 메서드의 가독성 향상을 위해 자주 사용

 

public Optional<T> filter(Predicate<? super T> predicate) {
    Objects.requireNonNull(predicate);
    if (!isPresent())
        return this;
    else
        return predicate.test(value) ? this : empty();
}

public<U> Optional<U> map(Function<? super T, ? extends U> mapper) {
    Objects.requireNonNull(mapper);
    if (!isPresent())
        return empty();
    else
        return Optional.ofNullable(mapper.apply(value));
}

public<U> Optional<U> flatMap(Function<? super T, Optional<U>> mapper) {
    Objects.requireNonNull(mapper);
    if (!isPresent())
        return empty();
    else
        return Object.requireNonNull(mapper.apply(value));
}
  • 세가지 메서드들의 첫 로직이모두 동일
  • 첫 로직은 넘겨받은 파라미터가 null이면 NoSuchElementException이 발생하고,
  • 정상적이라면 Optional이 비어있는지 판단하고 만약 해당 값이 비어있다면 Optional.empty()를 리턴
  • Optional 값이 비어있다면 넘겨받은 predicate 혹은 Function 자체가 수행되지 않고,
  • 비어있는 Optional을 리턴하기 때문에 얼마든지 체이닝을 사용하여도  null에 안전한 메서드 작성 가능
  • filter를 거치게 되면 넘겨받은 predicate가 참일 때만 다음 체인으로 넘어하고 거짓일 때는 비어있는 Optional을 반환함
  • map연산의 경우 파라미터로 받은 Function을 이용하여 내부 값을 컨버팅하고, 컨버팅 결과를 Optional로 감싸서 리턴하고 ofNullable이므로 비어있는 경우 Optional.empty()가 리턴됨
  • flatMap은 map과 유사하게 생겼지만 두가지가 다른 점이 있음. 넘겨주는 Function 리턴 값이 제네릭이 아니라 Optional 제네릭이라는 점과 Function의 결과를 Objects.requireNonNull로 한번 더 확인하기 때문에 Function의 결과값이 Optional.empty()가 아닌 null이라면 NoSuchElementException이 발생하는 특징이 있음

 

 

Oracle Docs

https://docs.oracle.com/javase/9/docs/api/java/util/Optional.html 

 

Optional (Java SE 9 & JDK 9 )

If a value is present, returns the result of applying the given Optional-bearing mapping function to the value, otherwise returns an empty Optional. This method is similar to map(Function), but the mapping function is one whose result is already an Optiona

docs.oracle.com

API Note:

Optional is primarily intended for use as a method return type where there is a clear need to represent "no result", and where using null is likely to cause errors. A variable whose type is Optional should nevet itself be null; it should always point an Optional instance.

Opational은 주로 "결과 없음"을 명확하게 나타내야 하고 null을 사용하면 오류가 발생할 수 있는 메서드 반환 유형으로 사용됩니다. Optional 변수는 null일 수 없으며 항상 Optional 인스턴스를 가리켜야 합니다.

 

1. "결과 없음" 상태가 가능하고, null 반환 시 에러가 발생할 수 있는 곳의 메서드 리턴 값

2. null 대신 Optional.empty()

 

 


 

 

 


+ Java 8 : functional interface 

 

Function

  • 1개의 파라미터를 받아서 1개의 결과를 반환하는 functional interface
  • Function<T, R>로 사용할 수 있으며 T는 받는 파라미터, R은 반환하는 값

 

실제 인터페이스

@FunctionalInterface
public interface Function<T, R> {
    R apply(T t);
}
  • Function을 주로 사용하는 대표적인 예는 Stream에서 map을 들 수 있음
  • map은 Stream의 형태를 Stream으로 변경하는 메서드
  • 파라미터로 Function interface를 받음

 

map의 메서드

/**
* Returns a stream consisting of the results of applying the given
* function to the elements of this stream.
*
* <p>This is an <a href="package-summary.html#StreamOps">intermediate
* operation</a>.
*
* @param <R> The element type of the new stream
* @param mapper a <a href="package-summary.html#NonInterference">non-interfering</a>,
*               <a href="package-summary.html#Statelessness">stateless</a>
*               function to apply to each element
* @return the new stream
*/
<R> Stream<R> map(Function<? super T, ? extends R> mapper);
  • 여기서 T는 기존 Stream의 제네릭 타입, R은 변환하고자 하는 타입
  • 따라서, R은 어떠한 타입도 될 수 있음

 

사용방법

private Function<Integer, String> stringMap = integer -> String.valueOf(integer);

@Test
public void FunctionTest() {
    Stream<Integer> integerStream = Stream.of(1, 2);
    List<String> collect = integerStream.map(stringMap).collect(Collectors.toList());
    System.out.println("collect = " + collect);
}
  • Functional interface를 별도로 구현
  • 일반적으로 map(String::valueOf)로 처리 가능

 

 

Supplier

  • 값을 생성하기 위해서 사용하는 functional interfaced
  • 함수형이므로 lazy하게 처리 가능
  • Supplier<T>의 형태를 가지며, T는 반환 값의 타입

 

Interface

@FunctionalInterface
public interface Supplier<T> {
    T get();
}
  • Supplier가 대표적으로 사용되는 곳은 Stream의 generater 메서드
  • generate는 Stream 객체를 만드는 메서드

 

generate 메서드

/**
* Returns an infinite sequential unordered stream where each element is
* generated by the provided {@code Supplier}.  This is suitable for
* generating constant streams, streams of random elements, etc.
*
* @param <T> the type of stream elements
* @param s the {@code Supplier} of generated elements
* @return a new infinite sequential unordered {@code Stream}
*/
public static<T> Stream<T> generate(Supplier<T> s) {
Objects.requireNonNull(s);
return StreamSupport.stream(
        new StreamSpliterators.InfiniteSupplyingSpliterator.OfRef<>(Long.MAX_VALUE, s), false);
}
  • generate는 Supplier<T>를 파라미터로 받으며, Supplier<T>에서 반환된 'T'값을 사용
  • Supplier<T>는 선언되었을 때 사용되는 것이 아니라,
  • generate에서 사용될 때 값을 반환하는 형식으로 함수형 패러다임의 lazy를 따름

 

사용 예제

@Test
public void SupplierTest() {
    int i = 5;

    Supplier<Integer> integerSupplier = () -> i * i;
    Optional<Integer> first = Stream.generate(integerSupplier).findFirst();
    System.out.println("first.get() = " + first.get());
}

 

 

Consumer

  • Supplier와는 반대로 단일 파라미터를 받고, 리턴 값이 없는 functional interface
  • Consumer<T>로 사용하며, 'T'는 입력받는 단일 객체의 타입

 

인터페이스

@FunctionalInterface
public interface Consumer<T> {

    /**
     * Performs this operation on the given argument.
     *
     * @param t the input argument
     */
    void accept(T t);
}
  • 대표적으로 사용되는 곳은 foreach 구문
  • for를 대신해서 list를 간단하게 처리할 때 사용

 

foreach 내부 로직

/**
* Performs the given action for each element of the {@code Iterable}
* until all elements have been processed or the action throws an
* exception.  Unless otherwise specified by the implementing class,
* actions are performed in the order of iteration (if an iteration order
* is specified).  Exceptions thrown by the action are relayed to the
* caller.
*
* @implSpec
* <p>The default implementation behaves as if:
* <pre>{@code
*     for (T t : this)
*         action.accept(t);
* }</pre>
*
* @param action The action to be performed for each element
* @throws NullPointerException if the specified action is null
* @since 1.8
*/
default void forEach(Consumer<? super T> action) {
    Objects.requireNonNull(action);
    for (T t : this) {
        action.accept(t);
    }
}
  • Counsumer라는 functional interface를 구현한 로직을 for문으로 실행

 

foreach와 Consumer 사용 예제

@Test
public void ConsumerTest() {
        Consumer<Integer> consumer = integer -> System.out.println(integer); 
        List<Integer> integers = Arrays.asList(1, 2, 3, 4, 5);
        integers.forEach(consumer);
}

 

 

Predicate

  • T에 대한 조건에 대해서 true / false를 반환하는 functional interface
  • Predicate<T>로 사용되며 'T'는 파라미터
  • 해당 파라미터에 대해서 true / false를 반환하도록 작성

 

인터페이스

@FunctionalInterface
public interface Predicate<T> {

    /**
     * Evaluates this predicate on the given argument.
     *
     * @param t the input argument
     * @return {@code true} if the input argument matches the predicate,
     * otherwise {@code false}
     */
    boolean test(T t);
}
  • Predicate가 사용되는 가장 대표적인 Stream은 filter
  • Stream에서 Filter는 Stream의 요소 중 통과할 요소와 제거할 요소를 구분해주는 함수형 명령어

 

Filter 메서드

/**
* Returns a stream consisting of the elements of this stream that match
* the given predicate.
*
* <p>This is an <a href="package-summary.html#StreamOps">intermediate
* operation</a>.
*
* @param predicate a <a href="package-summary.html#NonInterference">non-interfering</a>,
*                  <a href="package-summary.html#Statelessness">stateless</a>
*                  predicate to apply to each element to determine if it
*                  should be included
* @return the new stream
*/
Stream<T> filter(Predicate<? super T> predicate);
  • filter는 Stream의 각 요소에 대해서 조건의 만족 유무에 따라서 통과 혹은 제거하는 명령어

 

람다식 표현 예제

@Test
public void predicateTest() {
    Predicate<Integer> justOne = integer -> integer == 1;

        Stream<Integer> integerStream = Stream.of(1, 2);
        Stream<Integer> filteredStream = integerStream.filter(justOne);
        System.out.println("collect = " + filteredStream.collect(toList()));
}

 

 

UnaryOperator

  • 입력받은 파라미터 타입과 리턴 타입이 동일한 functional interface
  • UnaryOperator는 Function interface를 확장함
  • Function interface는 T → R, UnaryOperator는 T → T

 

인터페이스

@FunctionalInterface
public interface UnaryOperator<T> extends Function<T, T> {
    ...
}
  • ArrayList에서 흔하게 찾아 볼 수 있음

 

replaceAll 메서드

  • 해당 메서드는 ArrayList의 elements들을 일괄로 특정 식을 통해서 변경하는 메서드
default void replaceAll(UnaryOperator<E> operator) {
    Objects.requireNonNull(operator);
    final ListIterator<E> li = this.listIterator();
    while (li.hasNext()) {
        li.set(operator.apply(li.next()));
    }
}
  • Iterator로 해당 list를 순회하면서 UnaryOperator의 수식을 각 element에 적용

 

예제

@Test
public void unaryOperatorTest() {
    UnaryOperator<Integer> doubleOp = i -> i * 2;
    List<Integer> list = Arrays.asList(1, 2);
    list.replaceAll(doubleOp);
    System.out.println("list = " + list);
}
  • 출력결과 : list = [2, 4]

 

 

BinaryOperator

  • 2개의 동일한 타입의 파라미터로 1개의 동일한 리턴 값을 받아오는 functional interface
  • BiFunction interface를 확장한 interface로 (T, U) → R을 응용, (T, T) → T로 사용

 

인터페이스

@FunctionalInterface
public interface BiFunction<T, U, R> {

    /**
     * Applies this function to the given arguments.
     *
     * @param t the first function argument
     * @param u the second function argument
     * @return the function result
     */
    R apply(T t, U u);
}

@FunctionalInterface
public interface BinaryOperator<T> extends BiFunction<T,T,T> {
    ...
}
  • Stream의 reduce에서 찾을 수 있음
  • reduce는 Stream의 elemens들이 중첩하여 결과를 만드는 메서드

 

reduce 메서드

Optional<T> reduce(BinaryOperator<T> accumulator);

 

예제

@Test
public void binaryOperatorTest() {
    BinaryOperator<Integer> operator = (first, second) -> first + second;
    Stream<Integer> integerStream = Stream.of(1, 2, 3, 4, 5);
    Optional<Integer> reduce = integerStream.reduce(operator);
    System.out.println("reduce = " + reduce.get());
}
반응형

'cs > java-spring-boot' 카테고리의 다른 글

[Zero-base] 9-12. Side effect 줄이기  (0) 2022.03.17
[Zero-base] 9-11. Optional 제대로 사용하기  (0) 2022.03.17
[Zero-base] 9-9. null 핸들링  (0) 2022.03.16
[Zero-base] 9-8. null  (0) 2022.03.16
[Zero-base] 9-7. enum  (0) 2022.03.16

+ Recent posts