ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Optional
    StaticPL/JAVA 2020. 6. 24. 16:39

    1. Overview

    Optional is a container object used to contain not-null objects. Optional object is used to represent null with absent value. This class has various utility methods to facilitate code to handle values as ‘available’ or ‘not available’ instead of checking null values. It is introduced in Java 8 and is similar to what Optional is in Guava.

    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 never itself be null; it should always point to an Optional instance.

    2. Description

    2.1 Advantages

    • Do not have to deal directly with nulls that can cause NPE.
    • Don't have to manually do null checks.
    • You can explicitly express the possibility that the variable may be null. (Thus, unnecessary defense logic can be reduced.)

    2.2 Creating Optional Object

    2.2.1 Use its empty() static method:

    Obtain an empty Optional object that contains null. This empty object is an optional internally created singleton instance.

    Optional<Member> maybeMember = Optional.empty();

    2.2.2 Use its of() static method:

    Create an Optional object containing a non-null object.
    If null is passed, NPE is thrown, so use with caution.

    Optional<Member> maybeMember = Optional.of(aMember);

    2.2.3 use the ofNullable() static method:

    Creates an Optional object containing objects that are not sure whether it is null or not. You can think of it as a method that combines Optional.empty() and Optional.ofNullable(value). If null is passed, NPE is not thrown and an empty Optional object is obtained as in Optional.empty(). If you are not sure whether the object is null or not, you should use this method.

    Optional<Member> maybeMember = Optional.ofNullable(aMember);
    Optional<Member> maybeNotMember = Optional.ofNullable(null);

    2.3 Accessing objects contained in Optional

    The Optional class provides various instance methods to retrieve the object it contains.
    All of the methods below return the same value when an object containing Optional exists.
    On the other hand, if the Optional is empty (i.e. contains null), it behaves differently.
    Therefore, I will only explain the parts that work differently for the empty optional.

    2.3.1 get()

    For empty Optional objects, throw NoSuchElementException.

    2.3.2 orElse(T other)

    For empty Optional objects, returns the argument passed.

    2.3.3 orElseGet(Supplier<? extends T> other)

    For empty Optional objects, it returns the object created by the passed-in functional argument.
    You can think of it as a lazy version of orElse(T other).
    You can expect a performance advantage over orElse(T other) since the function is only called when it is empty.

    2.3.4 orElseThrow(Supplier<? extends X> exceptionSupplier)

    For empty Optional objects, it throws an exception created through the passed-in functional argument.

    So far, we have quickly looked at the main methods provided by Optional.
    Now let’s talk about how to use these methods.

    3. Example

    3.1 Using orElse()/orElseGet()/orElseThrow() instread of isPresent()/get()

    If you are a developer who can understand and use Optional correctly, you should be able to write the code of the first example in one line as follows.
    In other words, the idea of treating null as a conditional statement must be completely changed to functional thinking.

    // Bad
    Optional<Member> member = ...;
    if (member.isPresent()) {
        return member.get();
    } else {
        return null;
    }
    
    // Good
    Optional<Member> member = ...;
    return member.orElse(null);
    
    
    
    // Bad
    Optional<Member> member = ...;
    if (member.isPresent()) {
        return member.get();
    } else {
        throw new NoSuchElementException();
    }
    
    // Good
    Optional<Member> member = ...;
    return member.orElseThrow(() -> new NoSuchElementException());
    // Bad
    public String getCityOfMemberFromOrder(Order order) {
    	Optional<Order> maybeOrder = Optional.ofNullable(order);
    	if (maybeOrder.isPresent()) {
    		Optional<Member> maybeMember = Optional.ofNullable(maybeOrder.get());
    		if (maybeMember.isPresent()) {
    			Optional<Address> maybeAddress = Optional.ofNullable(maybeMember.get());
    			if (maybeAddress.isPresent()) {
    				Address address = maybeAddress.get();
    				Optinal<String> maybeCity = Optional.ofNullable(address.getCity());
    				if (maybeCity.isPresent()) {
    					return maybeCity.get();
    				}
    			}
    		}
    	}
    	return "Seoul";
    }
    
    // Good
    public String getCityOfMemberFromOrder(Order order) {
    	return Optional.ofNullable(order)
    			.map(Order::getMember)
    			.map(Member::getAddress)
    			.map(Address::getCity)
    			.orElse("Seoul");
    }
    // Bad
    public Member getMemberIfOrderWithin(Order order, int min) {
    	if (order != null && order.getDate().getTime() > System.currentTimeMillis() - min * 1000) {
    		return order.getMember();
    	}
    }
    
    // Good
    public Optional<Member> getMemberIfOrderWithin(Order order, int min) {
    	return Optional.ofNullable(order)
    			.filter(o -> o.getDate().getTime() > System.currentTimeMillis() - min * 1000)
    			.map(Order::getMember);
    }

    3.2 Using orElseGet(() -> new ...) instead of orElse(new ...)

    When method1(method2()) is executed, method2() is executed before method1() and always. Therefore, it is natural that new ... is always executed in orElse (new ...).

    However, it may be due to the name, but strangely, if you use orElse(new ...) without thinking intentionally, you may feel that new ... will be executed only when there is no value in Optional.

    Anyway, if there is no value in Optional, the value executed as the argument of orElse() is returned, so it has a meaning. However, if there is a value in Optional, the value executed as the argument of orElse() is ignored and discarded. Therefore, orElse(...) should only be used when ... is an already created or already calculated value without triggering a new object creation or new operation.

    In orElseGet(Supplier), Supplier is executed only when there is no value in Optional. Therefore, there is no unnecessary overhead because a new object is created or a new operation is performed only when the optional has no value. Of course, there will be overhead for lambda expressions or method references, but it will be minimal compared to performing unnecessary object creation or operations.

    // Bad
    Optional<Member> member = ...;
    // new member() is executed unconditionally, whether member has value or not
    return member.orElse(new Member());  
    
    // Good
    Optional<Member> member = ...;
    // new Member() is executed only when member has no value
    return member.orElseGet(Member::new);  
    
    // Good
    Member EMPTY_MEMBER = new Member();
    ...
    Optional<Member> member = ...;
    // You can use orElse() for already created or calculated values
    return member.orElse(EMPTY_MEMBER);  

    3.3 Compare null instead of optional if you just want to get the value

    // Bad
    return Optional.ofNullable(status).orElse(READY);
    
    // Good
    return status != null ? status : READY;

    3.4 Return empty collection instead of optional

    // Bad
    List<Member> members = team.getMembers();
    return Optional.ofNullable(members);
    
    // Good
    List<Member> members = team.getMembers();
    return members != null ? members : Collections.emptyList();

    For the same reason, when declaring Spring Data JPA Repository method, it is not recommended to wrap the collection in an optional way as follows. The Spring Data JPA Repository method that returns a collection returns an empty collection instead of null, so there is no need to wrap it up as an Optional.

    // Bad
    public interface MemberRepository<Member, Long> extends JpaRepository {
        Optional<List<Member>> findAllByNameContaining(String part);
    }
    
    // Good
    public interface MemberRepository<Member, Long> extends JpaRepository {
        List<Member> findAllByNameContaining(String part);  // null이 반환되지 않으므로 Optional 불필요
    }

    3.5 Do not use Optional as a field

     

    // Bad
    public class Member {
    
        private Long id;
        private String name;
        private Optional<String> email = Optional.empty();
    }
    
    // Good
    public class Member {
    
        private Long id;
        private String name;
        private String email;
    }

    3.6 Prohibit use of Optional as constructor or method argument

     

    // Bad
    public class HRManager {
        
        public void increaseSalary(Optional<Member> member) {
            member.ifPresent(member -> member.increaseSalary(10));
        }
    }
    hrManager.increaseSalary(Optional.ofNullable(member));
    
    // Good
    public class HRManager {
        
        public void increaseSalary(Member member) {
            if (member != null) {
                member.increaseSalary(10);
            }
        }
    }
    hrManager.increaseSalary(member);

    3.7 Prohibit use of Optional as an element of a collection

    Collections can contain many elements. Therefore, it is better not to use the expensive Optional as an element, and to check the null when removing or using the element. In particular, Map provides methods including null checks such as getOrDefault(), putIfAbsent(), computeIfAbsent(), and computeIfPresent(), so it is better to use the method provided by Map instead of using Optional as an element of Map.

    // Bad
    Map<String, Optional<String>> sports = new HashMap<>();
    sports.put("100", Optional.of("BasketBall"));
    sports.put("101", Optional.ofNullable(someOtherSports));
    String basketBall = sports.get("100").orElse("BasketBall");
    String unknown = sports.get("101").orElse("");
    
    // Good
    Map<String, String> sports = new HashMap<>();
    sports.put("100", "BasketBall");
    sports.put("101", null);
    String basketBall = sports.getOrDefault("100", "BasketBall");
    String unknown = sports.computeIfAbsent("101", k -> "");

    3.8 OptionalInt, OptionalLong, OptionalDouble instead of Optional<T>

    If the value to be included in the optional is int, long, or double, do not use Optional<Integer>, Optional<Long>, Optional<Double>, which causes Boxing/Unboxing, and use OptionalInt, OptionalLong, OptionalDouble.

    // Bad
    Optional<Integer> count = Optional.of(38);  // boxing 발생
    for (int i = 0 ; i < count.get() ; i++) { ... }  // unboxing 발생
    
    // Good
    OptionalInt count = OptionalInt.of(38);  // boxing 발생 안 함
    for (int i = 0 ; i < count.getAsInt() ; i++) { ... }  // unboxing 발생 안 함

     

    4. Reference

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

    https://www.geeksforgeeks.org/java-8-optional-class/

    https://www.tutorialspoint.com/java8/java8_optional_class.htm

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

    https://www.baeldung.com/java-optional

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

    http://homoefficio.github.io/2019/10/03/Java-Optional-%EB%B0%94%EB%A5%B4%EA%B2%8C-%EC%93%B0%EA%B8%B0/

    https://www.baeldung.com/java-optional-or-else-vs-or-else-get

    'StaticPL > JAVA' 카테고리의 다른 글

    Comparable, Comparator, and Collection Sort  (0) 2020.03.15
    Non-blocking, Lock-free operations  (0) 2020.02.27
    Semaphore  (0) 2020.02.27
    ReentrantReadWriteLock  (0) 2020.02.27
    ReentrantLock, lockInterruptibly, and tryLock  (0) 2020.02.27

    댓글

Designed by Tistory.