22.12.19 Optional 클래스
Optional<T> 클래스는 Integer나 Double 클래스처럼 'T'타입의 객체를 포장해 주는
래퍼 클래스(Wrapper class)입니다.
따라서 Optional 인스턴스는 모든 타입의 참조 변수를 저장할 수 있습니다.
이러한 Optional 객체를 사용하면 예상치 못한 NullPointerException 예외를 제공되는 메소드로 간단히 회피할 수 있습니다.
즉, 복잡한 조건문 없이도 널(null) 값으로 인해 발생하는 예외를 처리할 수 있게 됩니다.
[ Optional 생성하기 ]
Optional<String> optional = Optional.empty();
System.out.println(optional); // Optional.empty
System.out.println(optional.isPresent()); // false
// Optional의 value는 절대 null이 아니다.
Optional<String> optional = Optional.of("MyName");
// Optional의 value는 값이 있을 수도 있고 null 일 수도 있다.
Optional<String> optional = Optional.ofNullable(getName());
String name = optional.orElse("anonymous"); // 값이 없다면 "anonymous" 를 리턴
[ Optional 정리 ]
Optional은 null 또는 값을 감싸서 NPE(NullPointerException)로부터 부담을 줄이기 위해 등장한 Wrapper 클래스이다. Optional은 값을 Wrapping하고 다시 풀고, null 일 경우에는 대체하는 함수를 호출하는 등의 오버헤드가 있으므로 잘못 사용하면 시스템 성능이 저하된다. 그렇기 때문에 메소드의 반환 값이 절대 null이 아니라면 Optional을 사용하지 않는 것이 좋다. 즉, Optional은 메소드의 결과가 null이 될 수 있으며, null에 의해 오류가 발생할 가능성이 매우 높을 때 반환값으로만 사용되어야 한다. 또한 Optional은 파라미터로 넘어가는 등이 아니라 반환 타입으로써 제한적으로 사용되도록 설계되었다.
[orElse와 orElseGet의 차이]
Optional API의 단말 연산에는 orElse와 orElseGet 함수가 있다. 비슷해 보이는 두 함수는 엄청난 차이가 있다.
실제로 Optional 코드를 보면 다음과 orElse와 orElseGet이 각각 구현되어 있음을 확인할 수 있다.
public final class Optional<T> {
... // 생략
public T orElse(T other) {
return value != null ? value : other;
}
public T orElseGet(Supplier<? extends T> other) {
return value != null ? value : other.get();
}
}
[orElse와 orElseGet의 차이 예시 코드]
public void findUserEmailOrElse() {
String userEmail = "Empty";
String result = Optional.ofNullable(userEmail)
.orElse(getUserEmail());
System.out.println(result);
}
public void findUserEmailOrElseGet() {
String userEmail = "Empty";
String result = Optional.ofNullable(userEmail)
.orElseGet(this::getUserEmail);
System.out.println(result);
}
private String getUserEmail() {
System.out.println("getUserEmail() Called");
return "mangkyu@tistory.com";
}
위의 함수를 각각 실행해보면 다음과 같은데, 이러한 결과가 발생한 이유를 자세히 살펴보도록 하자.
// 1. orElse인 경우
getUserEmail() Called
Empty
// 2. orElseGet인 경우
Empty
먼저 OrElse인 경우에는 다음과 같은 순서로 처리가 된다.
위와 같이 동작하는 이유는 Optional.orElse()가 값을 파라미터로 받고, orElse 파라미터로 값을 넘겨주기 위해 getUserEmail()이 호출되었기 때문이다. 하지만 함수형 인터페이스(함수)를 파라미터로 받는 orElseGet에서는 동작이 달라진다.
orElseGet에서는 파라미터로 넘어간 값인 getUserEmail 함수가 Null이 아니므로 .get에 의해 함수가 호출되지 않는다. 만약 Optional의 값으로 null이 있다면, 다음과 같은 흐름에 의해 orElseGet의 파라미터로 넘어온 getUserEmail()이 실행될 것이다.
public void findUserEmailOrElseGet() {
String result = Optional.ofNullable(null)
.orElseGet(this::getUserEmail);
System.out.println(result);
}
private String getUserEmail() {
System.out.println("getUserEmail() Called");
return "mangkyu@tistory.com";
}
filter()
filter 메소드의 인자인 람다식이 true이면 Optional 객체를 그대로 통과시키고, false이면 Optional.empty()를 리턴하여 추가로 처리가 되지 않도록 한다.
String result1 = Optional.of("ABCDE").filter((val) -> val.contains("ABC")).orElse("Does not contain ABC");
System.out.println(result1); //ABCDE
String result2 = Optional.of("CDEFG").filter((val) -> val.contains("ABC")).orElse("Does not contain ABC");
System.out.println(result2); //Does not contain ABC
String result3 = Optional.of("abcde").map(String::toUpperCase).orElse("fail");
System.out.println(result3); //ABCDE
ifPresent()
람다식을 인자로 받는데, 값이 존재할 때만 람다식이 적용된다. 값이 존재하지 않으면 실행되지 않는다.
Optional.of("ABCDE").ifPresent(System.out :: println); //결과 : ABCDE
Optional.ofNullable(null).ifPresent(System.out :: println); //결과 : 아무것도 출력되지 않음
orElseThrow()
연산을 끝낸 후에도 Optional 객체가 비어 있다면 예외 공급자 함수를 통해 예외를 발생시킨다.
String result1 = Optional.of("CDEFG").filter((val) -> val.contains("ABC")).orElseThrow(NoSuchElementException::new);
System.out.println(result1);
//결과
Exception in thread "main" java.util.NoSuchElementException
at java.base/java.util.Optional.orElseThrow(Optional.java:385)