시뻘건 개발 도전기

Optional 본문

프로그래밍/JAVA

Optional

시뻘건볼때기 2022. 8. 12. 13:55
반응형

Stream의 종결처리 과정을 이해하기 위해 Optional을 필수로 알아야하는 도구이다.

 

Optional이 어떤 문제를 해결해주는걸까?

우리가 가장 많은 고민을 했던 NPE(NullPointException)가 Oprional의 사용 목적의 핵심이다. NPE는 Runtime error이기 때문에 실행 전까지 발견하기 쉽지가 않다. 현업에서도 종종 NPE가 발생해서 빠른 대응이 필요할 때도 있다.

 

@Getter
@Builder
@ToString
class User {
    private int id;
    private String name;
    private String email;
    private boolean isVerified;
}

public class Exercise {
    public static void main(String[] args) {
        User user1 = User.builder()
                .id(1)
                .name("홍길동")
                .isVerified(true)
                .build();

        User user2 = User.builder()
                .id(2)
                .isVerified(false)
                .build();

        System.out.println(isEquals(user1, user2)); // false
        System.out.println(isEquals(user2, user1)); // NullPointerException
    }

    public static boolean isEquals(User user1, User user2) {
        return user1.getName().equals(user2.getName())
                && user1.isVerified() == user2.isVerified()
                && user1.getId() == user2.getId();
    }
}

상기 코드와 같이 파라미터  순서에 따라서도 NPE 발생 빈도에 영향을 줄 수 있다. 이러한 문제를 해결해주기 위해 Optional을 사용한다.

 

그렇다면 Optional은 어떻게 사용해야할까?

/**
 * Returns an {@code Optional} with the specified present non-null value.
 *
 * @param <T> the class of the value
 * @param value the value to be present, which must be non-null
 * @return an {@code Optional} with the value present
 * @throws NullPointerException if value is null
 */
public static <T> Optional<T> of(T value) {
    return new Optional<>(value);
}

/**
 * Returns an {@code Optional} describing the specified value, if non-null,
 * otherwise returns an empty {@code Optional}.
 *
 * @param <T> the class of the value
 * @param value the possibly-null value to describe
 * @return an {@code Optional} with a present value if the specified value
 * is non-null, otherwise an empty {@code Optional}
 */
public static <T> Optional<T> ofNullable(T value) {
    return value == null ? empty() : of(value);
}

/**
 * Returns an empty {@code Optional} instance.  No value is present for this
 * Optional.
 *
 * @apiNote Though it may be tempting to do so, avoid testing if an object
 * is empty by comparing with {@code ==} against instances returned by
 * {@code Option.empty()}. There is no guarantee that it is a singleton.
 * Instead, use {@link #isPresent()}.
 *
 * @param <T> Type of the non-existent value
 * @return an empty {@code Optional}
 */
public static<T> Optional<T> empty() {
    @SuppressWarnings("unchecked")
    Optional<T> t = (Optional<T>) EMPTY;
    return t;
}

 

주석으로 볼 수 있듯이 아래와 같이 정의할 수 있다.

  • Null이 아닌 Object를 담을 때는 Optional.of()
  • 빈 Optional을 만들 때는 Optional.empty()
  • Null일 수도 있고 아닐 수도 있는 Object를 담을 때는 Optional.ofNullable()

 

Optional을 만들 수 있게 되었고 이제 안에 있는 값을 어떻게 사용하고 활용해야할까?

/**
 * Return {@code true} if there is a value present, otherwise {@code false}.
 *
 * @return {@code true} if there is a value present, otherwise {@code false}
 */
public boolean isPresent() {
    return value != null;
}

/**
 * Return the value if present, otherwise return {@code other}.
 *
 * @param other the value to be returned if there is no value present, may
 * be null
 * @return the value, if present, otherwise {@code other}
 */
public T orElse(T other) {
    return value != null ? value : other;
}

/**
 * Return the value if present, otherwise invoke {@code other} and return
 * the result of that invocation.
 *
 * @param other a {@code Supplier} whose result is returned if no value
 * is present
 * @return the value if present otherwise the result of {@code other.get()}
 * @throws NullPointerException if value is not present and {@code other} is
 * null
 */
public T orElseGet(Supplier<? extends T> other) {
    return value != null ? value : other.get();
}

/**
 * Return the contained value, if present, otherwise throw an exception
 * to be created by the provided supplier.
 *
 * @apiNote A method reference to the exception constructor with an empty
 * argument list can be used as the supplier. For example,
 * {@code IllegalStateException::new}
 *
 * @param <X> Type of the exception to be thrown
 * @param exceptionSupplier The supplier which will return the exception to
 * be thrown
 * @return the present value
 * @throws X if there is no value present
 * @throws NullPointerException if no value is present and
 * {@code exceptionSupplier} is null
 */
public <X extends Throwable> T orElseThrow(Supplier<? extends X> exceptionSupplier) throws X {
    if (value != null) {
        return value;
    } else {
        throw exceptionSupplier.get();
    }
}

 

 

때에 따라서 많이 사용되는 메소드다.

  • 담겨있는 Object가 Null인지 아닌지 체크하기 위한 isPresent()
  • 담겨있는 값 추출하기 위한
    • get() : Null이면 error : NoSuchElementException: No value present
    • orElse() : Null이면 other 리턴
    • orElseGet() : Null이면 supplier로 공급되는 값 리턴
    • orElseThrow() : Null이면 throw exceptionSupplier

 

예제 코드를 Optional을 사용하여 아래와 같이 개선할 수 있을 것이다.

public class Exercise {
    public static void main(String[] args) {
        User user1 = User.builder()
                .id(1)
                .name("홍길동")
                .isVerified(true)
                .build();

        User user2 = User.builder()
                .id(2)
                .isVerified(false)
                .build();

        try {
            System.out.println(isEquals(user2, user1)); // Exception
        } catch (Exception e) {
            System.out.println(e);  // java.lang.Exception: 이름이 없네?
        }
    }

    public static boolean isEquals(User user1, User user2) throws Exception {
        return Optional.ofNullable(user1.getName()).orElseThrow(() -> new Exception("이름이 없네?"))
                .equals(user2.getName())
                && user1.isVerified() == user2.isVerified()
                && user1.getId() == user2.getId();
    }
}

 

 

Optional을 왜 사용하는지 알겠는데 어떻게 응용 할 수 있을까?

아래 method를 통해 알아보자.

/**
 * If a value is present, invoke the specified consumer with the value,
 * otherwise do nothing.
 *
 * @param consumer block to be executed if a value is present
 * @throws NullPointerException if value is present and {@code consumer} is
 * null
 */
public void ifPresent(Consumer<? super T> consumer) {
    if (value != null)
        consumer.accept(value);
}

/**
 * If a value is present, apply the provided mapping function to it,
 * and if the result is non-null, return an {@code Optional} describing the
 * result.  Otherwise return an empty {@code Optional}.
 *
 * @apiNote This method supports post-processing on optional values, without
 * the need to explicitly check for a return status.  For example, the
 * following code traverses a stream of file names, selects one that has
 * not yet been processed, and then opens that file, returning an
 * {@code Optional<FileInputStream>}:
 *
 * <pre>{@code
 *     Optional<FileInputStream> fis =
 *         names.stream().filter(name -> !isProcessedYet(name))
 *                       .findFirst()
 *                       .map(name -> new FileInputStream(name));
 * }</pre>
 *
 * Here, {@code findFirst} returns an {@code Optional<String>}, and then
 * {@code map} returns an {@code Optional<FileInputStream>} for the desired
 * file if one exists.
 *
 * @param <U> The type of the result of the mapping function
 * @param mapper a mapping function to apply to the value, if present
 * @return an {@code Optional} describing the result of applying a mapping
 * function to the value of this {@code Optional}, if a value is present,
 * otherwise an empty {@code Optional}
 * @throws NullPointerException if the mapping function is null
 */
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));
    }
}

/**
 * If a value is present, apply the provided {@code Optional}-bearing
 * mapping function to it, return that result, otherwise return an empty
 * {@code Optional}.  This method is similar to {@link #map(Function)},
 * but the provided mapper is one whose result is already an {@code Optional},
 * and if invoked, {@code flatMap} does not wrap it with an additional
 * {@code Optional}.
 *
 * @param <U> The type parameter to the {@code Optional} returned by
 * @param mapper a mapping function to apply to the value, if present
 *           the mapping function
 * @return the result of applying an {@code Optional}-bearing mapping
 * function to the value of this {@code Optional}, if a value is present,
 * otherwise an empty {@code Optional}
 * @throws NullPointerException if the mapping function is null or returns
 * a null result
 */
public<U> Optional<U> flatMap(Function<? super T, Optional<U>> mapper) {
    Objects.requireNonNull(mapper);
    if (!isPresent())
        return empty();
    else {
        return Objects.requireNonNull(mapper.apply(value));
    }
}
  • 담겨 있는 값이 Null이 아니라면 consumer 실행하는 ifPresent()
  • 담겨있는 값이 Null이 아니라면 mapper로 공급된 함수를 실행하는 map()
    • 함수의 리턴타입에 따라서 담겨있는 값의 타입도 바뀔 수 있다.
  • mapper의 리턴 타입이 또다른 Optional인 경우 사용하는 flatMap()
Optional<User> user = Optional.ofNullable(getUser(false));
user.ifPresent(System.out::println);    //

user = Optional.ofNullable(getUser(true));
user.ifPresent(System.out::println);    // User(id=1, name=홍길동, email=null, isVerified=false)

Optional<Integer> id = Optional.ofNullable(getUser(true))
        .map(User::getId);
id.ifPresent(System.out::println);      // 1

String myName = Optional.ofNullable(getUser(true))
        .map(User::getName)
        .map(name -> "my name is " + name)
        .orElse("empty name");

System.out.println(myName);             // my name is 홍길동
반응형

'프로그래밍 > JAVA' 카테고리의 다른 글

STREAM 활용  (0) 2022.08.23
Stream  (0) 2022.07.25
Method Reference  (0) 2022.07.14
Functional Interface  (0) 2022.07.06
Lambda Expression  (0) 2022.07.06
Comments