본문 바로가기

Dev.BackEnd/JAVA

[Java8] 3. Optional Class

3. Optional Class

NPE(NullPointerException) 때문에 고생한 경험이 한 두 번이 아닐거라 생각한다. 그래서 Java8 에서는 하스켈, 스칼라 등의 함수형 언어에서 사용되고 있는 ‘선택형 값’ 개념의 영향을 받아 Optional<T> 라는 새로운 클래스를 제공한다. 값이 없는 상황을 모델링하는 것이다.

Optional은 선택형 값을 캡슐화하는 클래스이다. 값이 존재하면 그 값을 감싼다. 값이 없는 경우에는 Optional.empty 메서드로 Optional을 반환한다. empty 메서드는 Optional의 특별한 싱글턴 인스턴스를 반환하는 정적 팩토리 메서드이다. null 레퍼런스와 Optional.empty()는 의미상으로 비슷하지만 실제로 차이점이 많다. null을 참조하려 하면 NPE가 발생하지만 empty 메서드의 반환 값은 Optional이라는 객체이므로 이 객체를 활용할 수 있다.

Optional로 감싸진 값을 얻기
Optional 클래스는 여러 정적 팩토리 메서드를 제공한다.
1) ofNullable( T value )
public static <T> Optional<T> ofNullable(T value) {
return value == null ? empty() : of(value);
}
2) of( T value )
public static <T> Optional<T> of(T value) {
return new Optional<>(value);
}
3) empty( )
public static<T> Optional<T> empty() {
@SuppressWarnings("unchecked")
Optional<T> t = (Optional<T>) EMPTY;
return t;
}
private static final Optional<?> EMPTY = new Optional<>();

또한 인스턴스 메서드도 제공한다.
1) get( )
public T get() {
if (value == null) {
throw new NoSuchElementException("No value present");
}
return value;
}
가장 간단한 메서드이지만 가장 안전하지 않은 메서드이다. 래핑된 값이 있으면 해당 값을 반환하고 값이 없으면 NoSuchElementException을 발생시킨다.

2) orElse(T other)
public T orElse(T other) {
return value != null ? value : other;
}
Optional이 값이 포함하지 않을 때 디폴트 값을 제공할 수 있다.

3) orElseGet(Supplier<? extends T> other)
public T orElseGet(Supplier<? extends T> other) {
return value != null ? value : other.get();
}
lazy 한 version의 메소드로 Optional에 값이 없을 때만 Supplier가 실행된다.

4) orElseThrow(Supplier<? extends X> exceptionSupplier)
public <X extends Throwable> T orElseThrow(Supplier<? extends X> exceptionSupplier) throws X {
if (value != null) {
return value;
} else {
throw exceptionSupplier.get();
}
}
Optional 값이 비어있을 때 예외를 발생시킨다. get도 예외를 발생시키지만 이 메서드는 어떤 예외를 발생시킬 지 선택할 수 있다.

5) ifPresent(Consumer<? super T> consumer)
public void ifPresent(Consumer<? super T> consumer) {
if (value != null)
consumer.accept(value);
}
값이 존재할 때 인수로 넘겨준 동작을 실행할 수 있다. 값이 없으면 아무 일도 일어나지 않는다.

6) isPresent( )
public boolean isPresent() {
return value != null;
}
해당 Optional 인스턴스에 값이 존재하는지를 확인하고 이를 boolean 값으로 반환한다.


어떻게 사용할 것인가
잠재적으로 null이 될 수 있는 대상을 Optional로 감싼다. 기존의 자바 API에서는 null을 반환하면서 요청한 값이 없거나 어떤 문제로 계산에 실패했음을 알린다. 하지만 null을 반환하는 것보다는 Optional을 반환하는 것이 더 바람직하다.
java 8 이전 code>>
Object value = map.get("key");
java 8 code>>
Optional<Object> value = Optional.ofNullable(map.get("key"));
이와 같은 코드를 이용해서 null일 수 있는 값을 Optional로 안전하게 변환할 수 있다.

또는 OptionalUtils를 만들어서 사용할 수도 있다. Integer의 parseInt 라는 정적 메서드는 문자열을 정수로 바꾸지 못할 때 NumberFormatException을 발생시킨다. 정수로 변환할 수 없는 문자열 문제를 빈 Optional로 해결할 수 있다. parseInt가 Optional을 반환하도록 모델링 할 수 는 없으니 Util로 만들어서 parseInt 대신 utils의 정적 메소드를 사용한다.
public static Optional<Integer> stringToInt(String s) {
try {
return Optional.of(Integer.parseInt(s));
} catch (NumberFormatException e) {
return Optional.empty();
}
}


기본형 Optional을 사용하자 마라!
Stream처럼 Optional도 기본형으로 특화된 OptionalInt, OptionalLong 등의 클래스를 제공한다. Optional<Integer> 대신 OptionalInt를 사용할 수 있는 것이다. 하지만 Optional의 최대 요소 수는 한 개이므로 Optional에서는 기본형 특화 클래스로 성능을 개선할 수 없다. 또한 기본형 특화 Optional은 map, flatmap, filter 등과 같은 메서드를 지원하지 않는다.

응용 예제
java 8 이전 code>>
public int readDuration(Properties prop, String name) {
String value = prop.getProperty(name);
if (value != null) {
try {
int i = Integer.parseInt(value);
if (i > 0) {
return i;
}
} catch (NumberFormatException e) {
}
}
return 0;
}
java 8 code>>
public int readDurationByOptional(Properties prop, String name) {
return Optional.ofNullable(prop.getProperty(name))
.flatMap(OptionalUtils::stringToInt)
.filter(i -> i > 0)
.orElse(0);
}



3.end