📌 Java 8 이전
- Exception
- 진짜 예외적인 경우에서만 사용해야 한다. (Item 69)
- 예외 생성 시에 스택 추적 전체를 캡처하므로 비용이 비싸다.
- null 반환
- null이 반환될 일이 절대 없다고 확신하지 않는 한, 클라이언트가 null-guard를 해주어야 한다.
- 만약, 이를 놓치면 실제 원인과는 전혀 상관없는 코드에서 NullPointerException이 발생할 수 있다.
📌 Optional<T>
💡 Optional은 검사 예외와 취지가 비슷하다. (Item 71)
- Optional은 원소를 최대 1개 가질 수 있는 '불변' 컬렉션이다. (Collection<T>를 구현하지는 않았다.)
- T를 반환하거나, 반환할 값이 없을 때 Optional<T>를 반환하도록 선언하면 된다.
- 절대 null을 반환하지마라. Optional을 도입한 취지를 완전히 무시하는 행위다.
- 반환값이 없을 수도 있음을 API 사용자에게 명확히 알려줄 수 있다.
- 비검사 예외와 달리, 검사 예외는 클라이언트 측에서 반드시 대처하는 코드를 작성해야 한다.
- 마찬가지로 null만 반환했을 때와 달리, Optional을 받은 클라이언트가 대처 방법을 선택해야 한다.
📌 기존 방식과 차이점
예시는 Item 30에서 사용했던 코드다.
public static <E extends Comparable<E>> E max(Collection<E> c) {
if (c.isEmpty())
throw new IllegalArgumentException("컬렉션이 비어 있습니다.");
E result = null;
for (E e : c)
if (result == null || e.compareTo(result) > 0)
result = e;
return result;
}
public static <E extends Comparable<E>> Optional<E> max(Collection<E> c) {
if (c.isEmpty())
return Optional.empty();
E result = null;
for (E e : c)
if (result == null || e.compareTo(result) > 0)
result = e;
return Optional.of(result);
}
- 적절한 Optional 정적 팩터리 메서드를 사용해주면 해결된다.
- Opitonal.of()에는 null을 인자로 넘기면 안 된다. 대신 Optional.ofNullable()을 사용하면 된다.
📌 Stream과 Optional
public static <E extends Comparable<E>> Optional<E> max(Collection<E> c) {
return c.stream().max(Comparator.naturalOrder());
}
- Stream 종단 연산 중 상당수가 Optional을 반환한다.
- 비교자를 명시적으로 전달해야 하긴 하지만, 앞의 코드와 동일하게 동작하는 메서드를 구현할 수 있다.
📌 Optional method
1️⃣ orElse()
String lastWordInLexicon = max(words).orElse("단어 없음...");
- 기본값을 정해두는 방법
- max(words)가 비어 있는 Optional을 반환하면 "단어 없음..."을 값으로 취한다.
2️⃣ orElseThrow()
Toy myToy = max(toys).orElseThrow(TemperTantrumException::new);
- 비어 있는 Optional을 받으면 예외를 발생시킨다.
- 실제 예외가 아닌 예외 팩터리를 사용하여 예외 생성 비용을 절감할 수 있다.
3️⃣ get()
Element lastNobleGas = max(Elements.NOBLE_GASES).get();
- Optional에 항상 값이 채워져 있다고 확신한다면 곧바로 값을 꺼내 쓸 수도 있다.
- 그러나 잘못 판단한 경우 NoSuchElementException이 발생한다.
4️⃣ orElseGet()
public T orElse(T other)
public static String orElseBenchmark() {
return Optional.of("jayang").orElse(getRandomName());
}
public T orElseGet(Supplier<? extends T> other)
public static String orElseGetBenchmark() {
return Optional.of("jayang").orElseGet(() -> getRandomName());
}
- 기본값 설정 비용이 아주 커서 부담스러운 경우, orElseGet()을 사용하면 된다.
- 값이 필요할 때 Supplier<T>를 사용해 생성하므로 초기 설정 비용을 낮출 수 있다.
5️⃣ 여러 고급 메서드
- filter
- 필터링 조건을 만족하는 요소만을 포함하는 Optional 반환한다.
Optional<Integer> optional = Optional.of(10);
Optional<Integer> filtered = optional.filter(num -> num > 5);
filtered.ifPresent(System.out::println); // 출력: 10
- map
- Optional 내부의 값을 변환하여 새로운 Optional을 반환한다.
- 만약 Optional이 비어 있다면, 비어 있는 Optional을 그대로 반환한다.
Optional<String> optional = Optional.of("Hello");
Optional<Integer> mapped = optional.map(String::length);
mapped.ifPresent(System.out::println); // 출력: 5
- flatMap
- Optional 내부의 값을 다른 Optional로 매핑하고, 결과적으로 하나의 Optional을 반환한다.
Optional<String> optional = Optional.of("Hello");
Optional<Character> flatMapped = optional.flatMap(str -> {
if (str.length() > 0) {
return Optional.of(str.charAt(0));
} else {
return Optional.empty();
}
});
flatMapped.ifPresent(System.out::println); // 출력: H
- ifPresent
- Optional 내부의 값을 소비하고, 값이 존재하는 경우에만 주어진 동작을 실행한다.
Optional<String> optional = Optional.of("Hello");
optional.ifPresent(System.out::println); // 출력: Hello
6️⃣ isPresent()
💡 해당 메서드로 원하는 모든 작업을 수행할 수 있지만, 대부분은 앞서 언급한 메서드들로 대체 가능하다.
// isPresent
Optional<ProcessHandle> parentProcess = ProcessHandle.current().parent();
System.out.println("부모 PID: " + (parentProcess.isPresent() ?
String.valueOf(parentProcess.get().pid()) : "N/A"));
// map
System.out.println("부모 PID: " + parentProcess.map(h -> String.valueOf(h.pid())).orElse("N/A"));
- 부모 프로세스가 존재하면 부모 프로세스 ID를 출력하고, 없으면 "N/A"를 출력한다.
- map 메서드는 비어있는 Optional을 그대로 반환하므로, 형변환을 위한 별도의 isPresent() 호출이 필요 없었다.
Stream<Optional<String>> streamOfOptionals = Stream.of(
Optional.empty(), Optional.of("둘리"),
Optional.empty(), Optional.of("도우너"),
Optional.empty(), Optional.of("또치")
);
// 1. isPresent
streamOfOptionals.filter(Optional::isPresent)
.map(Optional::get)
.forEach(System.out::println);
// 2. flatMap
streamOfOptionals.flatMap(Optional::stream).forEach(System.out::println);
- Stream<Optional<T>>에서 비어 있지 않은 Optional만 뽑아낸다.
- Java 9의 Optional.stream() 메서드(Adapter)는 값이 있으면 원소로 담은 스트림으로, 없다면 빈 스트림으로 변환한다.
📌 주의 사항
1️⃣ Collection, Stream, Array, Optional 같은 컨테이너 타입은 Optional로 감싸면 안 된다.
- 빈 Optional<List<T>>를 반환하기보다 빈 List<T>를 반환하라. (Item 54)
- 빈 컨테이너를 그대로 반환하면, 클라이언트에서 Optional 처리 코드를 추가하지 않아도 된다.
2️⃣ 결과가 없을 수 있고, 클라이언트가 이 상황을 특별하게 처리해야 한다면 Optional<T>를 반환하라.
- Optional을 생성하고 처리하는 과정이 성능 저하를 유발할 수도 있다.
- 성능이 중요한 상황에서는 세심히 측정하여 사용 여부를 판단해야 한다. (Item 67)
3️⃣ 박싱된 기본 타입을 담은 Optional을 반환하지 마라
- 이는 값을 두 겹으로 감싼 것과 동일하다.
- 자바 API는 OptionalInt, OptionalLong, OptionalDouble을 따로 제공하고 있다.
- '덜 중요한 기본 타입'용인 Boolean, Byte, Character, Short, Float은 예외일 수 있다.
4️⃣ Optional을 Collection의 키, 값, 원소가 배열의 원소로 사용하는 게 적절한 상황은 거의 없다.
- Optional을 반환하고, 반환된 Optional을 처리하는 것 외엔 별 쓰임이 없다.
- Map의 value로 사용하면, key가 없다는 사실을 나타내는 방법이 2가지가 된다. (복잡도 증가)
- key 자체가 없는 경우
- key는 있지만 value가 빈 Optional인 경우
- Intance field로 Optional을 지정하면, 필수 필드를 갖는 클래스와, 이를 확장해 선택적 필드를 추가한 하위 클래스를 따로 만들어야 함을 암시한다. ('나쁜 냄새'다.)
- Map의 value로 사용하면, key가 없다는 사실을 나타내는 방법이 2가지가 된다. (복잡도 증가)
마지막 이야기에 설명을 덧붙이자면 다음과 같다.
class Person {
private String name;
private Optional<Integer> age;
public Person(String name, Optional<Integer> age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public Optional<Integer> getAge() {
return age;
}
}
- 위 클래스에서 age는 Optional<Integer>로 선언되어 선택적 필드로 간주된다.
- Person 클래스를 확장한 하위 클래스는 age 필드를 선택적으로 가지는 하위 클래스와, 필수적으로 가지는 하위 클래스를 따로 만들어야할 수도 있다.
class Employee extends Person { // age를 선택적으로 가짐
private String department;
public Employee(String name, Optional<Integer> age, String department) {
super(name, age);
this.department = department;
}
public String getDepartment() {
return department;
}
}
class FullTimeEmployee extends Employee { // age를 필수적으로 가짐
public FullTimeEmployee(String name, int age, String department) {
super(name, Optional.of(age), department);
}
}
class PartTimeEmployee extends Employee { // age를 가지지 않음
public PartTimeEmployee(String name, String department) {
super(name, Optional.empty(), department);
}
}