Java Optional Best Practices

  1. Optional 변수에 null을 할당하지 마라

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // AVOID
    public Optional<Cart> fetchCart() {
    Optional<Cart> emptyCart = null;
    ...
    }

    // PREPER
    public Optional<Cart> fetchCart() {
    Optional<Cart> emptyCart = Optional.empty();
    ...
    }

    빈값으로 초기화하려면 Optional.empty()를 사용하자.


  2. Optional.get()을 사용하기 전에는 반드시 Optional에 값이 있는지 확인해라

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // AVOID
    Optional<Cart> cart = CartRepository.findOne();
    Cart myCart = cart.get();

    // PREPER
    Optional<Cart> cart = CartRepository.findOne();
    if(cart.isPresent()) {
    Cart myCart = cart.get();
    }else {
    //값이 없을 때
    }

    cart값이 없으면 cart.get()은 에러를 발생시킨다. 그래서 항상 get() 호출 이전에 isPresent()로 체크를 해줘야한다. Optional이 생긴이유가 null check에서 벗어나기 위함인데 이렇게 사용한다면 결국 다를게 없다. 아래에 나오는 orElse()나 orElseGet()을 사용하자.


  1. isPresent()-get() 보다는 orElse()

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    public static final String USER_STATUS = "UNKNOWN";

    // AVOID
    public String findUserStatus(long id) {
    Optional<String> status = ... ;

    if (status.isPresent()) {
    return status.get();
    } else {
    return USER_STATUS;
    }
    }

    // PREPER
    public String findUserStatus(long id) {
    Optional<String> status = ... ;
    return status.orElse(USER_STATUS);
    }

  2. orElseGet()

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    public String computeStatus() {
    // status를 계산하여 리턴하는 로직
    }

    // AVOID
    public String findUserStatus(long id) {
    Optional<String> status = ... ;

    if (status.isPresent()) {
    return status.get();
    } else {
    return computeStatus();
    }
    }

    // AVOID
    public String findUserStatus(long id) {
    Optional<String> status = ... ;
    status.orElse(computeStatus());
    }

    // PREPER
    public String findUserStatus(long id) {
    Optional<String> status = ... ;
    return status.orElseGet(this::computeStatus);
    }

    첫번째 AVOID는 isPresent()-get()을 피하라는 것이고, 두번째 AVOID는 얼핏보면 문제 없을 것 같지만 orElse()는 status에 값이 있어도 computeStatus()메소드를 호출하기 때문에 orElseGet()을 쓰는 것이 좋다.


  1. orElseThrow()

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    // AVOID
    public String findUserStatus(long id) {
    Optional<String> status = ... ;

    if (status.isPresent()) {
    return status.get();
    } else {
    throw new NoSuchElementException();
    }
    }

    // PREFER
    public String findUserStatus(long id) {
    Optional<String> status = ... ;
    return status.orElseThrow();
    }

    orElseThrow()는 Java 10부터 사용가능하다.
    이하 버전은 orElseThrow(Supplier<? extends X> exceptionSupplier)를 사용해야한다.


  2. Optional.ifPresent()

    1
    2
    3
    4
    5
    6
    7
    8
    9
    // AVOID
    Optional<String> status = ... ;
    if (status.isPresent()) {
    System.out.println("Status: " + status.get());
    }

    // PREFER
    Optional<String> status ... ;
    status.ifPresent(System.out::println);

  3. lamda 사용으로 Optional 얻기

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    List<Product> products = ... ;

    // AVOID
    Optional<Product> product = products.stream()
    .filter(p -> p.getPrice() < price)
    .findFirst();

    if (product.isPresent()) {
    return product.get().getName();
    } else {
    return "NOT FOUND";
    }

    // PREFER
    return products.stream()
    .filter(p -> p.getPrice() < price)
    .findFirst()
    .map(Product::getName)
    .orElse("NOT FOUND");

    Stream 의 findFirst(), findAny(), reduce() 같은 메소드는 Optional을 리턴한다. 적절히 사용하면
    코드의 분리없이 메소드 체이닝을할 수 있다.


  1. Optional의 과도한 사용
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // AVOID
    public String fetchStatus() {
    String status = ... ;
    return Optional.ofNullable(status).orElse("PENDING");
    }

    // PREFER
    public String fetchStatus() {
    String status = ... ;
    return status == null ? "PENDING" : status;
    }

    단순 값을 가져오는 메서드에 Optional을 사용하는 것은 Optional의 목적에 맞지않는 과도한 사용이다.


  1. 필드 선언에 사용하지마라

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // AVOID
    public class Customer {
    Optioanl<String> zip;
    Optioanl<String> zip = Optional.empty();
    }

    // PREFER
    public class Customer {
    String zip;
    String zip = "";
    }

  2. 생성자, 메서드, Setter 인자로 사용하지마라


  3. 빈 Collection이나 Array를 리턴할 때 Optional을 쓰지마라

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // AVOID
    public Optional<List<String>> fetchCartItems() {
    List<String> items = cart.getItems();
    return Optional.ofNullable(items);
    }

    // PREFER
    public List<String> fetchCartItems() {
    List<String> items = cart.getItems();
    return items == null ? Collections.emptyList() : items;
    }

  4. 기본자료형에 Optional 제네릭을 사용하지마라

    1
    2
    3
    4
    5
    6
    7
    8
    9
    // AVOID
    Optional<Integer> price = Optional.of(50);
    Optional<Long> price = Optional.of(50L);
    Optional<Double> price = Optional.of(50.43d);

    // PREFER
    OptionalInt price = OptionalInt.of(50);
    OptionalLong price = OptionalLong.of(50L);
    OptionalDouble price = OptionalDouble.of(50.43d);

  5. 동등성(Equality) 비교를 위해 unWrap할 필요가없다.

    1
    2
    3
    4
    5
    6
    7
    8
    Optional<String> actual = Optional.of("shoes");
    Optional<String> expected = Optional.of("shoes");

    // AVOID
    assertEquals(expected.get(), actual.get());

    // PREFER
    assertEquals(expected, actual);

    Optional의 equals 메소드는 내부 값을 비교하도록 구현되어있기 때문에 바로 비교하면된다.


14. Optional 값을 변경하거나 필터링할 때는 스트림 API를 이용하자
  • map()
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    Optional<String> lowerName = Optional.of("optional");
    Optional<String> upperName;

    // AVOID
    if(lowerName.isPresent()) {
    upperName = Optional.of(lowerName.get().toUpperCase());
    }else {
    upperName = Optional.empty();
    }

    // PREFER
    upperName = lowerName.map(String::toUpperCase);
  • filter, flatMap()
    1
    2
    3
    4
    5
    String uppercase = items.stream()
    .filter(i -> i.getPrice() > 50)
    .findFirst()
    .flatMap(i -> Optional.of(i.getName()))
    .map(String::toUpperCase).orElse("NOT FOUND");

  1. indentity 기반 연산을 하지마라
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    Product product = new Product();
    Optional<Product> op1 = Optional.of(product);
    Optional<Product> op2 = Optional.of(product);

    // AVOID
    if (op1 == op2) {
    ...
    }

    // PREFER
    if (op1.equals(op2)) {
    ...
    }

    // NEVER DO
    synchronized(op1) {
    ...
    }

    Optional은 Value-based class 이므로 equality (==), identity hash-based, synchronization 연산을 하면 예상과 다르게 동작될 수 있다. equals()를 사용하자.