suyeonme
[Java] Optional 클래스 본문
Optional 클래스란?
자바 8에서 제공하는 java.util.Optional<T>클래스로, Optional은 선택형값을 캡슐화하는 클래스이다. 값이 있으면 Optional 클래스는 값을 감싸고 값이 없으면 Optional.empty 메서드로 Optional 값을 반환한다. null을 참조하려 하면 NullPointerException이 발생하지만 Optional.empty는 Optional 객체이므로 이를 다양한 방식으로 핸들링 할 수 있다.
NullPointerException을 방지하기 위한 null 확인 코드
null을 참조하려하면 런타임에 NullPointerException이 발생한다. NullPointerException을 방지하기 위해서 아래와 같이 null 확인 코드를 작성한다. 아래와 같이 작성하는 경우, 중첩된 if가 추가되면서 코드 들여쓰기 수준이 증가하여 반복패턴의 코드를 작성하게 된다.
public String getCarInsuranceName(Person person) {
if(person != null) {
Car car = person.getCar();
if(car != null) {
// ...
}
}
}
Optional 클래스의 메서드 종류
메서드 | 설명 |
empty | 빈 Optional 인스턴스 반환 |
get | 값이 존재하면 Optional이 감싸고있는 값을 반환하고, 그렇지않으면 NoSuchElementException을 반환 |
of | 값이 존재하면 값을 감싸는 Optional을 반환하고, 값이 null이면 NullPointerException을 발생 |
ofNullable | 값이 존재하면 값을 감싸는 Optional을 반환하고, 값이 null이면 빈 Optional을 반환 |
or | 값이 존재하면 값을 감싸는 Optional을 반환하고, 그렇지않으면 Supplier에서 만든 Optional을 반환 |
orElse | 값이 존재하면 값을 반환하고 그렇지 않으면 기본값을 반환 |
orElseGet | 값이 존재하면 값을 반환하고 그렇지않으면 Supplier에서 제공하는 값을 반환 |
or | 값이 존재하면 값을 반환하고, 그렇지않으면 Supplier에서 생성한 예외를 발생 |
ifPresent, ifPresentOrElse | 값이 존재하면 지정된 Consumer를 실행하고, 그렇지않으면 아무일도 일어나지않음 |
isPresent | 값이 존재하면 true를 반환하고 그렇지 않으면 false를 반환 |
map | 값이 존재하면 매핑 함수를 적용 |
filter | 값이 존재하며 프레디케이트와 일치하면 값을 포함하는 Optional을 반환하고 값이 없거나 프레디케이트와 일치하지않으면 빈 Optional을 반환 |
flatMap | 값이 존재하면 인수로 제공한 함수를 적용한 결과 Optional을 반환하고, 그렇지않으면 빈 Optional을 반환 |
stream | 값이 존재하면 존재하는 값만 포함하는 스트림을 반환하고, 그렇지 않으면 빈 스트림을 반환 |
map 메서드 예시
map과 같은 메서드 사용시, Optional 객체를 최대 요소의 개수가 한개 이하인 데이터 컬렉션으로 생각할 수 있다. Optional이 값을 포함하면 map의 인수로 제공된 함수로 값을 바꾸고 그렇지 않으면 아무 동작도 수행하지 않는다.
// if문으로 값이 null인지 확인후 보험회사의 이름 추출
String name = null;
if(insurance != null) {
name = insurance.getName();
}
// map을 이용하여 보험회사의 이름 추출 (GOOD)
Optional<Insurance> optionalInsurance = Optional.ofNullable(insurance);
Optional<String> name = optionalInsurance.map(Insurance::getName);
flatMap 메서드 예시
map을 이용하여 아래와 같이 작성한 경우, map 연산의 결과는 Optional<Optional<Car>>형식의 중첩 Optional 객체가 되므로 컴파일되지 않는다. 이러한 경우 flatMap 메서드를 사용하여 생성된 모든 스트림을 하나의 스트림으로 병합되어 평준화시킬 수 있다.
* 평준화: 두 Optional을 합치는 기능을 수행하면서 둘중 하나라도 null이면 빈 Optional을 생성하는 연산
// BAD
Optional<Person> optPerson = Optional.of(person);
Optional<String> name =
optPerson.map(Person::getCar)
.map(Car::getInsurance)
.map(Insurance::getName);
// flatMap을 사용하여 스트림 평준화 (GOOD)
Optional<Person> optPerson = Optional.of(person);
person.flatMap(Person::getCar)
.flatMap(Car::getInsurance)
.map(Insurance::getName)
.orElse("Unknown"); // 결과 Optional이 비어있으면 기본값 사용
filter 메서드 예시
// BAD
Insurance insurance = ...;
if(insurance != null && "CambridgeInsurance".equals(insurance.getName())) {
System.out.println("ok");
}
// GOOD
Optional≤Insurance> insurance = ...;
insurance.filter(insurance -> "CambridgeInsurance".equals(insurance.getName())
.ifPresent(x -> System.out.println("ok"))
stream 메서드 예시
stream 메서드는 각 Optional이 비었는지 아닌지에 따라 Optional을 0개 이상의 항목을 포함하는 스트림으로 변환한다. 따라서 stream 메서드를 이용하여 한단계의 연산으로 값을 포함하는 Optional을 언랩(unwrap)하고 비어있는 Optional은 제거할 수 있다.
Stream<Optional<String>> stream = ...;
Set<String> result = stream.filter(Optional::isPresent)
.map(Optional::get)
.collect(toSet());
// 사람 목록을 이용해 가입한 보험회사 이름 찾기
Public Set<String> getCarInsuranceName(List<Person> persons) {
return persons.stream()
.map(Person::getCar)
.map(optCar -> optCar.flatMap(Car::getInsurance))
.map(optIns -> optIns.map(Insurance::getName))
.flatMap(Optional::stream) // *
.collect(toSet());
}
데이터 직렬화 모델에 Optional 클래스 사용 예시
Optional 클래스는 필드 형식으로 사용할 것을 가정하지 않았으므로 Serializable 인터페이스를 구현하지 않는다. 따라서 직렬화 모델이 필요한 경우 Optional로 값을 반환받을 수 있는 메서드를 추가할 수 있다.
public class Person {
private Car car;
public Optional<Car> getCarAsOptional() {
return Optional.ofNullable(car);
}
}
nullsafe 메서드 구현 예시
첫번째 Optional에 flatMap을 호출했으므로 첫번째 Optional이 비어있다면 인수로 전달한 람다 표현식이 실행되지않고 그대로 빈 Optional을 반환한다.
// BAD
public Optional<Insurance> nullSafeFindCheapestInsurance(
Optional<Person> person, Optional<Car> car) {
if(person.isPresent() && car.isPresent()) {
return Optional.of(findCheapestInsurance(person.get(), car.get()));
}
}
// GOOD
public Optional<Insurance> nullSafeFindCheapestInsurance(
Optional<Person> person, Optional<Car> car) {
return person.flatMap(p -> car.map(c -> findCheapestInsurance(p, c)));
}
잠재적으로 null이 될 수 있는 대상을 Optional로 감싸기
Map의 get 메서드는 요청한 키에 대응하는 값을 찾지못했을 때 null을 반환한다. 따라서 map에서 반환하는 값을 Optional로 감싸서 이를 개선할 수 있다.
Object value = map.get("key"); // BAD
Optional<Object> value = Optional.ofNullable(map.get("key")); // GOOD
Optional을 반환하는 유틸리티 메서드 생성 예시
값을 제공할 수 없을 때 null을 반환하는 대신 예외를 발생시키는 경우도 있다. 이러한 경우 try/catch블록을 사용하여 예외를 발생한다. 이러한 경우 예외를 발생시키는 함수가 Optional을 반환하도록 유틸리티 메서드를 구현할 수 있다.
public static Optional<Integer> stringToInt(String s) {
try {
return Optional.of(Integer.parseInt(s)); // 문자열을 정수로 변환가능하면 정수로 변환한 값이 포함된 Optional을 반환
} catch(NumberFormatException e) {
return Optional.empty(); // 그렇지 않으면 빈 Optional을 반환
}
}
기본형 특화 Optional 클래스
스트림처럼 Optional도 기본형으로 특화된 OptionalInt, OptionalLong, OptionalDouble등의 클래스를 제공한다. 하지만 아래의 이유로 기본형 Optional 클래스의 사용을 권장하지는 않는다.
- Optional의 최대 요소수는 한개이므로 Optional에서는 기본형 특화 Optional 클래스로 성능을 개선할 수 없다.
- 기본형 특화 Optional 클래스는 map, flatMap, filter등을 지원하지 않는다.
- 기본형 특화 Optional 클래스로 생성한 결과는 다른 일반 Optional과 혼용할 수 없다
'프로그래밍👩🏻💻 > Java' 카테고리의 다른 글
[Java] String, SpringBuffer, StringBuilder의 차이점 (0) | 2023.04.16 |
---|---|
[Java] CheckedException, UnCheckedException이란? (0) | 2023.04.02 |
[Java] 메서드 참조(Method Reference)란? (0) | 2023.03.01 |
[Java] 람다 표현식(Lamda Expression)과 함수형 인터페이스(Functional Interface) (0) | 2023.03.01 |
[Java] Boxing, Unboxing이란? (0) | 2023.03.01 |