[Java] Optional 정리

Optional\

Optional<T>은 메소드의 결과가 명백하게 결과 없음을 반환할 이유가 있고, null을 반환하게 될 시 문제가 될 수 있는 경우에 사용할 수 있다.

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.

Optional 선언

값이 없는 optional

1
Optional<Foo> foo = Optional.empty();

Optional 클래스에서는 static final으로 빈 Optional을 정의하고 이를 리턴하고 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public final class Optional<T> {
/**
* Common instance for {@code empty()}.
*/
private static final Optional<?> EMPTY = new Optional<>()

...

public static<T> Optional<T> empty() {
@SuppressWarnings("unchecked")
Optional<T> t = (Optional<T>) EMPTY;
return t;
}
}

값이 있는 optional

1
Optional<Foo> foo = Optional.of(new Foo());

새로운 Optional 클래스를 생성해서 내려준다.

1
2
3
public static <T> Optional<T> of(T value) {
return new Optional<>(value);
}

값의 유무가 불투명한 optional

1
2
Optional<Foo> foo = Optional.ofNullable(new Foo());
Optional<Foo> nullFoo = Optional.ofNullable(null);

내부에서 null 여부를 확인하고 null이라면 empty()를, 아니라면 of(value)를 호출해 결과를 내려준다.

1
2
3
public static <T> Optional<T> ofNullable(T value) {
return value == null ? empty() : of(value);
}

중간 연산

filter

filter()의 조건을 통해 필터링을 할 수 있다. filter()는 매개변수로 Predicate 함수형 인터페이스를 받으며 Predicate는 파라미터는 1개 리턴타입은 boolean인 인터페이스이다.

1
2
3
4
Optional<Foo> foo = Optional.ofNullable(new Foo(3));
foo.filter(f -> f.getIntValue() > 3);
//foo의 intValue가 3이므로 결과는 false
//그러므로 결과는 Optional.empty()와 같음

map

map()을 통해 mapping이 가능하다. map()은 매개변수로 Function<T,R> 함수형 인터페이스를 받으며 T는 파라미터 타입 R은 리턴 타입이다.

1
2
Optional<Foo> foo = Optional.ofNullable(new Foo(3));
foo.map(Foo::getIntValue); //결과는 3

flatMap

flatMap()을 통해서도 mapping이 가능하다. flatMap()은 매개변수로 Function<T,R> 함수형 인터페이스를 받으며 T는 파라미터 타입 R은 리턴 타입이다. flatMap()map()과 유사하지만, 반환하려하는 객체가 Optional<T>로 감싸져 있을 때 사용한다.

1
2
Optional<Foo> foo = Optional.ofNullable(new Foo(Optional.of(new Bar())));
foo.flatMap(Foo::getOptionalBar);

종단 연산

get

내부의 값을 반환 받고 싶을 때 사용한다. 만약 value가 null이라면 NoSuchElementException을 던진다.

1
2
Optional<Foo> foo = Optional.ofNullable(new Foo(3));
foo.get(); //결과는 3

orElse

orElse(other)는 value가 null일 경우 other를 아닐경우 value를 내려준다.

1
2
3
4
5
Optional<Foo> nullFoo = Optional.empty();
Optional<Foo> foo = Optional.of(new Foo(3));

nullFoo.orElse(new Foo(2)); //Foo(2)와 동일
foo.orElse(new Foo(2)); //Foo(3)와 동일

orElseGet

orElseGet()null일 경우 Supplier를 통해 결과값을 내려받는다. Supplier는 파라미터는 없고 리턴값이 있는 함수형 인터페이스다.

1
2
3
4
5
6
7
8
9
10
11
Optional<Foo> nullFoo = Optional.empty();
Optional<Foo> foo = Optional.of(new Foo(3));

nullFoo.orElseGet(() -> {
System.out.println("Generating new Foo");
return new Foo(2);
}); //Foo(2)와 동일
foo.orElseGet(() -> {
System.out.println("Generating new Foo");
return new Foo(2);
}); //Foo(3)와 동일

orElseThrow

orElseThrow()null일 경우 Supplier를 통해 예외를 던진다. Throwable을 상속한 예외를 리턴값으로 내려주기 때문에 예외를 던져야만 한다.

1
2
3
4
5
6
7
8
9
10
11
Optional<Foo> nullFoo = Optional.empty();
Optional<Foo> foo = Optional.of(new Foo(3));

nullFoo.orElseThrow(() -> {
System.out.println("NPE");
return new NullPointerException();
}); //예외를 던짐
foo.orElseThrow(() -> {
System.out.println("NPE");
return new NullPointerException();
}); //예외를 던지지 않음

ifPresent

ifPresent()null이 아닐 경우 Consumer<T>를 통해 어떤 작업을 수행한다.

1
2
3
4
5
6
7
8
9
Optional<Foo> nullFoo = Optional.empty();
Optional<Foo> foo = Optional.of(new Foo(3));

nullFoo.ifPresent(f -> {
System.out.println(f.getIntValue());
}); //실행 안됨
foo.ifPresent(f -> {
System.out.println(f.getIntValue());
}); // 3

Anti Patterns

1. null을 Optional에 할당하지 않기

optional은 단순히 컨테이너일 뿐이다. null을 표현하고 싶다면 .empty()를 사용하자.

1
2
3
- Optional<Foo> nullFoo = null;

+ Optional<Foo> nullFoo = Optional.empty();

2. Optional.get()을 사용하기 전에 값이 존재하는지 확인하기

NPE를 마주하고 싶지 않다면 피하자.

1
2
3
4
5
6
7
8
9
10
Optional<Foo> foo = ???? //비어 있을 수도 있음

- foo.get();

+ if(foo.isPresent()) {
+ Foo myFoo = foo.get();
+ // myFoo를 사용한 작업
+ } else {
+ // myFoo가 필요 없는 작업
+ }

3. 값이 없을때 고정 값을 내려주는 행동은 Optional.orElseGet()으로 하기

값이 존재 하지 않을 때는 orElse() 보다도 orElseGet()이 더 좋다. Supplierlazy evaluation이기 때문이다.

1
2
3
4
5
6
7
8
9
Optional<Foo> foo = ????

- if(foo.isPresent()) {
- return foo.get();
- } else {
- return MAX_VALUE_FOO;
- }

+ return foo.orElseGet(this::MAX_VALUE_FOO);

4. 값이 없을때 예외를 던지고 싶다면 Optional.orElseThrow()로 하기

Java10 에서는 Supplier가 없는 Optional.orElseThrow()도 존재하지만 명확한 예외를 던져주는게 좋다.

1
2
3
4
5
6
7
8
9
Optional<Foo> foo = ????

- if(foo.isPresent()) {
- return foo.get();
- } else {
- throw new IllegalStateException();
- }

+ return foo.orElseThrow(IllegalAccessError::new);

5. 값이 없을때 작업을 수행하지 않는다면 ifPresnet() 쓰기

이와 같은 맥락으로 Java9 부터는 ifPresentOrElse()가 있다.

1
2
3
4
5
6
7
Optional<String> str = Optional.of("hello world");

- if(foo.isPresent()) {
- System.out.println(str.get());
- }

+ str.ifPresent(System.out::println);

6. 값이 없을 때 Optional를 리턴하고 싶으면 or() 쓰기

Java9부터 해당이 된다.

1
2
3
4
5
6
7
8
9
10
11
Optional<String> str = ???

- if(str.isPresent()) {
- return str;
- } else {
- return Optional.of("VALUE WAS EMPTY");
- } // 이 방법이 최선

- return str.orElseGet(() -> Optional.of("VALUE WAS EMPTY")); // 존재하지도 않는 방법

+ return str.or(() -> Optional.of("VALUE WAS EMPTY"));

7. Optional을 필드에 정의하지 않기

OptionalSerializable을 구현하지 않았기 때문에 해서는 안된다. 단, JacksonOptional을 직렬화 하는 기능을 구현하고 있음.

1
2
3
4
5
6
7
8
9
public class Foo {
- [access_modifier] [static] [final] Optional<String> zip;
- [access_modifier] [static] [final] Optional<String> zip = Optional.empty();
- ...

+ [access_modifier] [static] [final] String zip;
+ [access_modifier] [static] [final] String zip = "";
+ ...
}

8. 생성자 아규먼트로 Optional 사용하지 않기

Optional을 생성자 아규먼트로 사용하는 행동은 또 다른 보일러 플레이트 코드를 양산하게 된다. 같은 맥락으로 Setter나 여타 다른 메소드의 파라미터에서도 마찬가지다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Foo {
private String lastName; // null이면 안됨
- private Optional<String> firstName; // null 일수도 있음
+ private String firstName;

- public Foo(String lastName, Optional<String> firstName) {
+ public Foo(String lastName, String firstName) {
this.lastName = Objects.requireNonNull(lastName, () -> "Last Name can not be null");
this.firstName = firstName;
}

public Optional<String> getFirstName() {
- return firstName;
+ return Optional.ofNullable(firstName);
}
}

9. Optional로 빈 컬렉션이나 배열을 반환하지 않기

1
2
3
4
List<String> names = attendence.getNames();

- return Optional.ofNullable(names);
+ return names == null ? Collections.emptyList() : names;

10. int, long, double에는 Non-Generic Optional 쓰기

OptionalInt OptionalLong OptionalDouble값과 isPresent라는 boolean 필드를 포함하는 객체이다.

1
2
- Optional<Integer> myInt = Optional.of(1);
+ OptionalInt myInt = OptionalInt.of(1);

11. 동등성 검사는 랩핑을 벗길 필요가 없다

다음은 Optional.equals()의 구현이다. 내부적으로 랩핑을 벗겨서 검사해준다.

1
2
3
4
5
6
7
8
9
10
11
12
13
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}

if (!(obj instanceof Optional)) {
return false;
}

Optional<?> other = (Optional<?>) obj;
return Objects.equals(value, other.value);
}
1
2
3
4
5
  Optional<Foo> foo1 = Optional.of(new Foo("fooString"));
Optional<Foo> foo2 = Optional.of(new Foo("fooString"));

- assertEquals(foo1.get(), foo2.get());
+ assertEquals(foo1, foo2);

12. Optional을 Stream에서 사용할때는 Optional.stream()을 쓰자

Java9 이상부터 지원한다.

1
2
3
4
  List<Foo> fooList = ...

- fooList.stream().map(Foo::getFooString).filter(Optional::isPresent).map(Optional::get).collect(Collectors.toList());
+ fooList.stream().map(Foo::getFooString).flatMap(Optional::stream).collect(Collectors.toList());

참조

Author: Song Hayoung
Link: https://songhayoung.github.io/2021/03/11/Languages/Java/optionalAtJava8/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.