아래 내용들은 자바의 정석에 나오는 내용을 발췌한 것이다.
java.util.Optional은 JDK1.8부터 추가되었다.
Optional<T\>
은 지네릭 클래스로 'T타입의 객체'를 감싸는 래퍼 클래스이다.
그래서 Optional타입의 객체는 모든 타입의 참조변수를 담을 수 있다.
public final class Optional<T> {
private final T value; // T타입의 참조변수
...
}
최종 연산의 결과를 그냥 반환하는게 아니라 Optional 객체에 담아서 반환하는 것이다. 이처럼 객체에 담아서 반환을 하면, 반환된 결과가 null인지 매번 if문으로 체크하는 대신 Optional에 정의된 메서드를 통해서 간단히 처리할 수 있다.
이제 널 체크를 위한 if문 없이도 NullPointerException이 발생하지 않는 코드 작성이 가능하다.
Optional 객체를 생성할 때는 of() 또는 ofNullable()을 사용한다.
String str = "abc";
Optional<String> optVal = Optional.of(str);
만약 참조변수의 값이 null일 가능성이 있으면, of() 대신 ofNullable()을 사용해야한다. of()는 매개변수의 값이 null이면 NullPointerException이 발생하기 때문이다.
Optional<String> optVal = Optional.of(null); //NPE 발생
Optional<String> optVal = Optional.ofNullable(null); // OK
Optional<T>타입의 참조변수를 기본값으로 초기화할 때는 empty()를 사용한다. null로 초기화하는 것도 가능하지만, empty()로 초기화하는 것이 바람직하다.
Optional<String> optVal = null ;
Optional<String> optVal = Optional.<String>empty(); // 빈 객체로 초기화
empty()는 지네릭 메서드라서 앞에 <T>를 붙였다. 추정 가능하므로 생략할 수 있다.
Optional객체에 저장된 값을 가져올 때는 get()을 사용한다. 값이 null일 때는 NoSuchElementException
이 발생하며, 이를 대비해서 orElse()로 대체할 값을 지정할 수 있다.
Optional<String> optVal = Optional.of("abc");
String str1 = optVal.get(); //optVal에 저장된 값을 반환. null이면 예외 발생
String str2 = optVal.orElse(""); //optVal에 저장된 값이 null일 때는 ""를 반환
orElse()의 변형으로는 null을 대체할 값을 반환하는 람다식을 지정할 수 있는 orElseGet() 과 null일 때 지정된 예외를 발생시키는 orElseThrow()가 있다.
String str3 = optVal2.orElseGet(String::new); // () -> new String()와 동일
String str4 = optVal2.orElseThrow(NullPointerException::new); //널이면 예외발생
Stream처럼 Optional객체에도 filter(), map(), flatMap()을 사용할 수 있다. map()의 연산결과가 Optional<Optional<T>>일 때, flatMap()을 사용하면 Optional<T>를 결과로 얻는다. 만일 Optional 객체의 값이 null이면, 이 메서드들은 아무 일도 하지 않는다.
int result = Optional.of("")
.filter(x->x.length() > 0)
.map(Integer::parseInt).orElse(-1); // result = -1
우리가 이미 알고 있는 것처럼 parseInt() 는 예외가 발생하기 쉬운 메서드이다. 만일 예외처리된 메서드를 만든다면 다음과 같을 것이다.
static int optStrToInt(Optional<String> optStr, int defaultValue) {
try {
return optStr.map(Integer::parseInt).get();
} catch (Exception e) {
return defaultValue;
}
}
isPresent() 는 Optional 객체의 값이 null이면 false를, 아니면 true를 반환한다. isPresent(Consumer<T> block)은 값이 있으면 주어진 람다식을 실행하고, 없으면 아무일도 하지 않는다.
if(str != null) {
System.out.println(str);
}
위와 같은 조건문을 isPresent()를 이용해서 다음과 같이 쓸 수 있다.
if(Optional.ofNullable(str).isPresent()) {
System.out.println(str);
}
이 코드를 ifPresent()를 이용해서 바꾸면 더 간단히 할 수 있다. 아래의 문장은 참조변수 str이 null이 아닐 때만 출력하고, null이면 아무 일도 일어나지 않는다.
Optional.ofNullable(str).ifPresent(System.out::println);
IntStream과 같은 기본형 스트림에는 Optional도 기본형을 값으로 하는 OptionalInt, OptionalLong, OptionalDouble을 반환한다.
OptionalInt는 다음과 같이 정의도어있다.
public final class OptionalInt {
...
private final boolean isPresent; //값이 저장되어 있으면 true
private final int value; //int 타입의 변수
기본형 int의 기본값은 0이므로 아무런 값도 갖지 않는 OptionalInt에 저장되는 값은 0일 것이다. 그러면 아래의 두 OptionalInt객체는 같은 것일까?
OptionalInt opt = OptionalInt.of(0); //OptionalInt에 0을 저장
OptionalInt opt2 = OptionalInt.empty(); //OptionalInt에 null을 저장
다행히 저장된 값이 없는 것과 0이 저장된 것은 isPresent라는 인스턴스 변수로 구분이 가능하다. isPresent()는 이 인스턴스 변수의 값을 반환한다.
System.out.println(opt.isPresent()); // true
System.out.println(opt2.isPresent()); // false
System.out.println(opt.getAsInt()); // 0
System.out.println(opt2.getAsInt()); // NoSuchElementException 발생
System.out.println(opt.equals(opt2); // false
그러나 Optional객체의 경우 null을 저장하면 비어있는 것과 동일하게 취급한다.
Optional<String> opt = Optional.ofNullable(null);
Optional<String> opt2 = Optional.empty();
System.out.println(opt.equals(opt2)); //true
public class FooBar {
private Optional<String> alpha;
private Optional<String> beta;
private Optional<String> gamma;
private Optional<String> delta;
private Optional<String> epsilon;
private Optional<String> zeta;
private Optional<String> eta;
private Optional<String> theta;
private Optional<String> iota;
...
}
딱봐도 이상하다.
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은 return 타입에만 쓰는 것이 적절하다.
Brian Goetz는 스택오버플로우에서 Optional을 만든 의도에 대해 다음과 같이 말했다.
… it was not to be a general purpose Maybe type, as much as many people would have liked us to do so. Our intention was to provide a limited mechanism for library method return types where there needed to be a clear way to represent “no result” …
출처: Java Optional 바르게 쓰기
정확한 사용 예제까지 담고 있다.
출처: https://johngrib.github.io/wiki/java-optional/
// 안 좋음
Optional<Member> member = ...;
if (member.isPresent()) {
return member.get();
} else {
return null;
}
// 좋음
Optional<Member> member = ...;
return member.orElse(null);
// 안 좋음
Optional<Member> member = ...;
if (member.isPresent()) {
return member.get();
} else {
throw new NoSuchElementException();
}
// 좋음
Optional<Member> member = ...;
return member.orElseThrow(() -> new NoSuchElementException());
orElse(...)에서 ...는 Optional에 값이 있든 없든 무조건 실행된다. 따라서 ...가 새로운 객체를 생성하거나 새로운 연산을 수행하는 경우에는 orElse() 대신 orElseGet()을 써야한다.
// 안 좋음
Optional<Member> member = ...;
return member.orElse(new Member()); // member에 값이 있든 없든 new Member()는 무조건 실행됨
// 좋음
Optional<Member> member = ...;
return member.orElseGet(Member::new); // member에 값이 없을 때만 new Member()가 실행됨
// 좋음
Member EMPTY_MEMBER = new Member();
...
Optional<Member> member = ...;
return member.orElse(EMPTY_MEMBER); // 이미 생성됐거나 계산된 값은 orElse()를 사용해도 무방
Optional은 비싸다. 따라서 단순히 값 또는 null을 얻을 목적이라면 Optional 대신 null 비교를 쓰자.
// 안 좋음
return Optional.ofNullable(status).orElse(READY);
// 좋음
return status != null ? status : READY;
Optional은 비싸다. 그리고 컬렉션은 null이 아니라 비어있는 컬렉션을 반환하는 것이 좋을 때가 많다. 따라서 컬렉션은 Optional로 감싸서 반환하지 말고 비어있는 컬렉션을 반환하자.
// 안 좋음
List<Member> members = team.getMembers();
return Optional.ofNullable(members);
// 좋음
List<Member> members = team.getMembers();
return members != null ? members : Collections.emptyList();
컬렉션을 반환하는 Spring Data JPA Repository 메서드는 null을 반환하지 않고 비어있는 컬렉션을 반환해주므로 Optional로 감싸서 반환할 필요가 없다.
// 안 좋음
public interface MemberRepository<Member, Long> extends JpaRepository {
Optional<List<Member>> findAllByNameContaining(String part);
}
// 좋음
public interface MemberRepository<Member, Long> extends JpaRepository {
List<Member> findAllByNameContaining(String part); // null이 반환되지 않으므로 Optional 불필요
}
Optional은 필드에 사용할 목적으로 만들어지지 않았으며, Serializable을 구현하지 않았다. 따라서 Optional은 필드로 사용하지 말자.
// 안 좋음
public class Member {
private Long id;
private String name;
private Optional<String> email = Optional.empty();
}
// 좋음
public class Member {
private Long id;
private String name;
private String email;
}
of(X)은 X가 null이 아님이 확실할 때만 사용해야 하며, X가 null이면 NullPointerException 이 발생한다.
ofNullable(X)은 X가 null일 수도 있을 때만 사용해야 하며, X가 null이 아님이 확실하면 of(X)를 사용해야 한다.
// 안 좋음
return Optional.of(member.getEmail()); // member의 email이 null이면 NPE 발생
// 좋음
return Optional.ofNullable(member.getEmail());
// 안 좋음
return Optional.ofNullable("READY");
// 좋음
return Optional.of("READY");
@GetMapping("/{id}")
@ResponseStatus(HttpStatus.OK)
public Task getTaskById(@PathVariable Long id) {
Optional<Task> task = taskRepository.getTaskById(id);
if (task.isEmpty()) {
throw new DataNotFoundException();
}
return task.get();
}
위의 코드도 나빠 보이지는 않지만 더 깔끔하게 바꿀 수 있다.
@GetMapping("/{id}")
@ResponseStatus(HttpStatus.OK)
public Task getTaskById(@PathVariable Long id) {
return taskRepository.getTaskById(id).orElseThrow(DataNotFoundException::new);
}
이렇게 되면 훨씬 깔끔하다! 그리고 확실히 리턴값은 Task라는 보장이 생긴다.
참고로 :: 은 이중 콜론 연산자로, 함수를 호출하는 것이다.