핵심 정리
값을 반환하지 못할 가능성이 있는 메소드라면 옵셔널을 반환하는 것을 고려해보자. 그러나 옵셔널 반환에는 성능 저하가 따를 수 있으니, 성능에 민감한 경우 null을 반환하거나 예외를 던지는 것이 나을 수 있다. 또한, 옵셔널을 반환값 이외의 용도로 쓰는 경우는 매우 드물다.
메소드가 특정 조건에서 값을 반환할 수 없을 때
: 자바 8 이전
방법 1 : 예외 처리
👉 좋지 않은 방법
- 예외는 정말 예외적인 상황에서만 사용해야 한다.(item 69)
- 메소드의 정상적인 동작 흐름을 중단시킨다.
- 스택 추적을 생성하므로 성능 측면에서 비용이 발생한다.
방법 2 : 반환 타입이 객체 참조라면, null 반환
👉 좋지 않은 방법
- null 체크를 해야 하는 코드를 추가적으로 작성해야 한다.
- 괴로운 NullPointerException이 발생할 수 있다.
예시 : Collection에서 최댓값 찾기 (예외 처리)
// 컬렉션에서 최댓값을 구한다(컬렉션이 비엇으면 예외를 던진다)
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 = Objects.requireNonNull(e);
return result;
}
위 코드는 빈 컬렉션을 넘기면 IllegalArgumentException이 발생한다.
메소드가 특정 조건에서 값을 반환할 수 없을 때
: 자바 8 이후 Optional<T> 등장
Optional<T>
- null이 아닌 T 타입 참조를 하나 담거나 아무것도 담지 않는다.
- 최대 원소를 1개 가질 수 있는 불변 컬렉션이다.
- Optional<T>가 Collection<T>를 구현하진 않았지만, 원칙적으로 그렇다는 말이다.
- 예외 처리 메소드보다 유연하고 사용하기 쉽다.
- null을 반환하는 메소드보다 오류 가능성이 작다.
예시 : Collection에서 최댓값 찾기 (Optional 반환)
// 컬렉션에서 최댓값을 구해 Optional<E>로 반환
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 = Objects.requireNonNull(e);
return Optional.of(result); // 정적 팩터리를 사용해 옵셔널 생성
- Optional.empty()
- 빈 옵셔널을 반환한다.
- Optional.of()
- 값이 든 옵셔널을 반환한다.
- null을 넣으면 NulPointerException이 발생한다.
- Optional.ofNullable()
- null 값도 허용하는 옵셔널이다.
🚨 주의해야 할 점
옵셔널을 반환하는 메소드에서는 절대 null을 반환하지 말자.
Optional의 주된 취지는 메소드가 반환하는 값이 null일 가능성을 명확하게 표현하고, 그 결과로 발생할 수 있는 NPE를 미연에 방지하는 것이다.
예시 : Collection에서 최댓값 찾기 (스트림 사용 및 옵셔널 반환)
// 컬렉션에서 최댓값을 구해 Optional<E>로 반환한다. - 스트림 버전
public static <E extends Comparable<E>> Optional<E> max(Collection<E> c) {
return c.stream().max(Comparator.naturalOrder());
Optional의 메소드를 통해 값이 없을 경우의 처리를 명시적으로 정의할 수 있다.
: Optional.orElse(), Optional.orElseGet(), Optional.orElseThrow()
Optional.orElse()
// 옵셔널 활용 1 - 기본값을 정해둘 수 있다.
String lastWordInLexicon = max(words).orElse("단어 없음...");
Optional.orElseThrow()
// 옵셔널 활용 2 - 원하는 예외를 던질 수 있다.
Toy myToy = max(toys).orElseThrow(TemperTantrumException::new);
- 위 예시는 예외 객체를 직접 전달하는 것이 아니라, 예외를 생성하는 함수를 전달하였다.
- 따라서 실제로 예외가 필요한 상황이 될 때까지 예외 객체를 생성하는 비용을 피할 수 있다.
- 👉 예외 생성 비용 절약
- 따라서 실제로 예외가 필요한 상황이 될 때까지 예외 객체를 생성하는 비용을 피할 수 있다.
Optional.get()
// 옵셔널 활용 3 - 항상 값이 채워져 있다고 가정한다.
Element lastNobleGas = max(Elements.NOBLE_GASES).get();
- Optional 객체가 감싸고 있는 값을 반환한다.
- 위 예시는 Optional<Element>를 반환한다.
- 만약 Optional이 값을 가지고 있지 않다면, NoSuchElementException이 발생한다.
- 따라서 Optional이 반드시 값을 가지고 있음을 확신할 때만 사용해야 한다.
- 그렇지 않다면 Optional.orElse(), Optional.orElseGet(), Optional.orElseThrow() 등의 메소드를 사용하여 값이 없을 때의 처리를 명시적으로 지정하는 것이 좋다.
Optional.orElseGet()
: 기본값을 설정하는 비용이 큰 경우 유용하다.
- Optional에 값이 없을 때만 Supplier<T> 인터페이스의 get() 메소드를 호출한다.
- Supplier<T> 인터페이스는 Java8에서 추가된 함수형 인터페이스로, 어떠한 인자를 받지 않지만 T 타입의 결과를 반환하는 get() 메소드를 정의하고 있다.
// orElseGet 예시
Optional<String> optionalValue = getOptionalValue(); // getOptionalValue() 메소드는 Optional<String>을 반환
// optionalValue가 값이 있으면 그 값을 사용하고, 없으면 "default"를 사용
String value = optionalValue.orElseGet(() -> "default");
- 위 예시는 값이 없을 때만 비싼 연산을 수행하도록 할 수 있으므로, 불필요한 연산을 줄일 수 있다.
- 단순한 기본값이 아니라 복잡한 연산을 수행해야 하는 경우, orElseGet 메소드를 사용하여 기본값을 생성하는 연산을 지연시키는 것이 좋다.
기타 메소드 - Optional.filter()
기타 메소드 - Optional.map()
기타 메소드 - Optional.flatMap()
기타 메소드 - Optional.ifPresent()
- Optional 인스턴스가 값을 가지고 있으면 true, 아니면 false를 반환한다.
- 대부분의 경우 isPresent를 사용하는 것보다 orElse, orElseGet 등의 메소드를 사용해 처리하는 것이 더 깔끔하고 효율적이다.
예제 - isPresent보다 orElse, orElseGet 등의 메소드를 사용하는 것이 더 깔끔하고 효율적
// isPresent 활용
Optional<ProcessHandle> parentProcess = ph.parent(); // 자바 9에 추가된 ProcessHandle 클래스
System.out.println("부모 PID: " + (parentProcess.isPresent() ?
String.valueOf(parentProcess.get().pid()) : "N/A"));
- 위 예시의 ph.parent() 메소드는 부모 프로세스의 ProcessHandle을 Optional로 감싸서 반환한다.
- Optional이 값을 가지고 있으면, 해당 프로세스의 PID를 출력하고, 그렇지 않으면 "N/A"를 출력한다.
// map 활용
System.out.println("부모 PID: " +
ph.parent().map(h -> String.valueOf(h.pid())).orElse("N/A"));
- ph.parent().map(h -> String.valueOf(h.pid())
- 부모 프로세스의 PID를 문자열로 변환한 결과를 담은 새로운 Optional을 생성한다.
- orElse("N/A")
- Optional이 값을 가지고 있으면 그 값을 반환하고, 그렇지 않으면 "N/A"를 반환한다.
- isPresent()와 get()를 사용하는 것보다 map()과 orElse() 메소드를 함께 사용하면 더 간결하고 명확해진다.
Stream과 Optional을 함께 사용할 경우
예시 1 - Java 8 이후
// Stream<Optional<T>> 타입의 스트림이 있을 때, 이 스트림에서 채워진 Optional의 값을 추출
streamOfOptionals
.filter(Optional::isPresent) // 스트림에서 값이 존재하는 옵셔널만 선택
.map(Optional::get); // 선택된 옵셔널에서 값을 추출하여 새로운 스트림 생성
예시 2 - Java 9 이후(Optional.stream())
- 자바 9 이후 이러한 작업을 더 간편하게 할 수 있는 Optional.stream() 메소드가 추가되었다.
- 이 메소드는 옵셔널을 스트림으로 변환하는 어댑터로, 옵셔널에 값이 있으면 그 값을 원소로 담은 스트림을, 값이 없다면 빈 스트림을 반환한다.
// 예시1을 Optional.stream() 메소드를 이용해 단순화
streamOfOptionals
.flatMap(Optional::stream);
- flatMap(Optional::stream)
- 스트림의 각 옵셔널을 스트림으로 변환 후, 이 스트림들을 합쳐 하나의 스트림으로 생성한다.
- 값이 없는 옵셔널은 빈 스트림으로 변환되므로 결과 스트림에는 채워진 옵셔널의 값만이 포함된다.
Optional 주의사항
: 잘못 사용할 경우 코드 복잡성이 커지고 예상치 못한 문제가 발생할 수 있다.
컬렉션, 스트림, 배열, 옵셔널 같은 컨테이너 타입을 옵셔널로 감싸면 안 된다.(item 54)
- 컨테이너 자체가 값이 없을 때를 표현하는 방법이 존재하기 때문이다.
- 예를 들어, 컬렉션은 빈 컬렉션을, 배열은 길이가 0인 배열을 반환하여 값이 없음을 표현할 수 있다.
- 이런 경우에 Optional을 사용하면 불필요하게 코드가 복잡해진다.
결과가 없을 수 있으며, 클라이언트가 이 상황을 특별하게 처리해야 한다면 Optional<T>를 반환한다.
- 굳이 Optional을 사용할 필요가 없으므로 ..
박싱된 기본 타입을 담은 옵셔널을 반환하지 말자.
- 기본 타입을 박싱한 후에 이를 다시 Optional로 감싸면, 필요 이상의 메모리와 성능 손실이 발생할 수 있다.
- 기본 타입을 옵셔널로 감싸야 할 경우에는 OptionalInt, OptionalLong, OptionalDouble 등의 전용 클래스를 사용하는 것이 좋다.
- Boolean, Byte, Character, Short, Float는 예외이다.
옵셔널을 키, 값, 원소, 배열의 원소로 사용하면 안 된다.
- Optional을 맵의 값으로 사용하면 키가 없다는 것을 표현하는 방법이 두 가지가 되어 복잡성이 커지며, 오류 가능성이 높아진다.
- 마찬가지로, Optional을 컬렉션의 원소나 배열의 원소로 사용하는 것도 좋지 않다.
인스턴스 필드로 Optional을 사용하는 예외적인 경우
- Optional은 반환 타입으로 주로 사용되며, 일반적으로 인스턴스 필드로 사용하는 것은 권장되지 않는다.
- cuz 메모리 소비와 성능 이슈
- 한 가지 예외적인 경우는 선택적 필드를 가진 클래스의 경우 Optional을 사용하는 것이 더 바람직할 수 있다.
import java.util.Optional;
public class NutritionFacts {
private final int servingSize; // (필수)
private final int servings; // (필수)
private final Optional<Integer> calories; // (선택)
private final Optional<Integer> fat; // (선택)
private final Optional<Integer> sodium; // (선택)
public static class Builder {
// 필수 매개변수
private final int servingSize;
private final int servings;
// 선택 매개변수 - 기본값으로 초기화
private Optional<Integer> calories = Optional.empty();
private Optional<Integer> fat = Optional.empty();
private Optional<Integer> sodium = Optional.empty();
public Builder(int servingSize, int servings) {
this.servingSize = servingSize;
this.servings = servings;
}
public Builder calories(int val) {
calories = Optional.of(val);
return this;
}
public Builder fat(int val) {
fat = Optional.of(val);
return this;
}
public Builder sodium(int val) {
sodium = Optional.of(val);
return this;
}
public NutritionFacts build() {
return new NutritionFacts(this);
}
}
private NutritionFacts(Builder builder) {
servingSize = builder.servingSize;
servings = builder.servings;
calories = builder.calories;
fat = builder.fat;
sodium = builder.sodium;
}
// getter 메소드
public Optional<Integer> getCalories() {
return calories;
}
// 기타 getter 메소드
}
'Language > Java' 카테고리의 다른 글
[effective java] 아이템 57. 지역변수의 범위를 최소화하라. (0) | 2023.07.17 |
---|---|
[effective java] 아이템 56. 공개된 API 요소에는 항상 문서화 주석을 작성하라. (0) | 2023.07.17 |
[effective java] 아이템 54. null이 아닌, 빈 컬렉션이나 배열을 반환하라. (0) | 2023.07.17 |
[effective java] 아이템 53. 가변인수는 신중히 사용하라. (0) | 2023.07.17 |
[effective java] 아이템 52. 다중정의는 신중히 사용하라. (0) | 2023.07.17 |