11.1 값이 없는 상황을 어떻게 처리할까?
public String getCarInsuranceName(Person person) {
return person.getCar().getInsurance().getName();
}
- 차를 소유하지 않은 사람도 많기 때문에 호출하게 되면 getInsurance는 null참조의 정보를 반환하려고 하고 런타임 에러가 발생될 것
11.1.1 보수적인 자세로 NullPointerException 줄이기
public String getCarInsuranceName(Person person) {
if(person != null) {
Car car = person.getCar();
if(car != null) {
Insurance insurance = car.getInsurance();
if(insurance != null) {
return insurance.getName();
}
}
}
return "Unknown";
}
- 위 코드는 변수를 참조할때마다 null을 확인
- 따라서 중첩된 if가 추가되면서 코드 들여쓰기 수준이 증가
- 이와같은 반복 패턴 코드를 '깊은 의심(deep doubt)'라 부름
- 이를 반복하다보면 코드의 구조가 엉망이 되고 가독성도 떨어짐
public String getCarInsuranceName(Person person) {
if(person == null) {
return "Unknown";
}
Car car = person.getCar();
if(car == null) {
return "Unknown";
}
Insurance insurance = car.getInsurance();
if(insurance == null) {
return "Unknown";
}
return insurance.getName();
}
- 위 코드처럼 중첩 if 블록을 없앨 수 있지만 너무 많은 출구(return)이 생겼기 때문에 유지보수가 어려워지고 실수가 발생하기 쉬움
11.1.2 null 때문에 발생하는 문제
- 에러의 근원이다 : NullPointerException은 자바에서 가장 흔한 에러
- 코드를 어지럽힌다 : 중첩된 null 확인 코드로 가독성 저하
- 아무 의미가 없다 : null은 아무 의미도 표현하지 않음
- 자바 철학에 위배된다 : 자바는 개발자로부터 모든 포인터를 숨겼지만, null은 예외
- 형식 시스템에 구멍을 만든다 : null은 무형식이며 정보를 포함하고있지 않으므로 모든 참조 형식에 null을 할당할 수 있음 이렇게 할당된 null이 전파되면 애초에 null이 어떤 의미로 사용되었는지 알수 없음
11.1.3 다른 언어는 null 대신 무얼 사용하나?
- 그루비 같은 언어에서는 안전 내비게이션 연산자(?.)를 도입해서 null 문제를 해결
def carInsuranceName = person?.car?.insurance?.name
- null이 할당될 수 있는 값에 안전 네비게이션 연산자를 이용하면 null 참조 예외 걱정 없이 객체에 접근할 수 있음
- 이때 null인 참조가 있으면 결과로 null이 반환
- 하스켈, 스칼라 등의 함수형 언어는 다른 관점에서 null 문제에 접근
- 하스켈의 Maybe와 스칼라의 Optional은 주어진 형식의 값을 갖거나 아무 값도 갖지 않을 수 있는 구조를 가짐 따라서 null 참조 개념은 자연스럽게 사라짐
11.2 Optional 클래스 소개
- Optional은 선택형값을 캡슐화하는 클래스
- 값이 있으면 Optional 클래스는 값을 감싸고, 값이 없으면 Optional.empty 메서드로 Optional을 반환
- null을 참조하면 NullPointerException이 발생하지만 Optional.empty()는 Optional 객체이므로 이를 다양한 방식으로 활용할 수 있음
11.3 Optional 패턴 적용
11.3.1 Optional 객체 만들기
빈 Optional
- Optional.empty로 빈 Optional 객체를 만들 수 있음
null이 아닌 값으로 Optional 만들기
- 정적 팩토리 메서드 Optional.of로 null이 아닌 값을 포함하는 Optional을 만들 수 있음
Optional<Car> optCar = Optional.of(car);
- 이제 car가 null이라면 즉시 NullPointerException이 발생
null값으로 Optional 만들기
- 정적 팩토리 메서드 Optional.ofNullable로 null 값을 저장할 수 있는 Optional을 만들 수 있음
Optional<Car> optCar = Optional.ofNullable(car);
- 이제 car가 null이면 빈 Optional 객체가 반환
11.3.2 맵으로 Optional의 값을 추출하고 변환하기
String name = null;
if(insurance != null) {
name = insurance.getName();
}
위의 코드를 다음과 같이 Optional로 사용할 수 있음
Optional<Insurance> optInsurance = Optional.ofNullable(insurance);
Optioanl<String> name = optInsurance.map(Insurance::getName);
- Optional이 값을 포함하면 map의 인수로 제공된 함수가 값을 바꾸고 비어있으면 아무 일도 일어나지 않음
11.3.3 flatMap으로 Optional 객체 연결
public class Person {
private Optional<Car> car;
public Optional<Car> getCar() {
return car;
}
}
- 위 코드에서 optPerson의 형식은 Optional이므로 map 메서드를 호출 가능
- 하지만 getCar는 Optional 형식의 객체를 반환 즉 map 연산의 결과는 Optional<Optional> 형식의 객체가 되어 컴파일되지 않음
- 위 문제를 flatMap을 사용해 해결 flatMap은 인수로 받은 함수를 적용해서 생성된 각각의 스트림에서 콘텐츠만 평준화하여 남김
Optional로 자동차의 보험회사 이름 찾기
public String getCarInsuranceName(Optional<Person> person) {
return = person.flatMap(Person::getCar)
.flatMap(car::getInsurance)
.map(Insurance::getName)
.orElse("Unknown");
}
- null을 확인하느라 조건 분기문을 추가해서 코드를 복잡하게 만들지 않으면서도 쉽게 이해할 수 있는 코드를 완성
- Optional을 인수로 받거나 Optioanl을 반환하는 메서드를 정의한다면 결과적으로 이 메서드가 빈 값을 받거나 빈 결과를 반환할 수 있음을 잘 문서화해서 제공하는 것과 같음
11.3.4 Optional 스트림 조작
public Set<String> getCarInsuranceNames(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로 값이 감싸있으면 이 과정이 조금 더 복잡해짐
11.3.5 디폴트 액션과 Optional 언랩
Optional 클래스는 Optional 인스턴스에 포함된 값을 읽는 다양한 방법을 제공
- get()은 값을 읽는 가장 간단하면서 가장 안전하지 않은 메서드
- 래핑된 값이 있으면 해당 값을 반환하고 없으면 NoSuchElementException을 반환
- orElse(T other) 메서드는 Optional이 값을 포함하지 않을 때 기본 값을 제공
- orElseGet(Supplier<? extends T> other)은 orElse 메서드에 대응하는 게으른 버전의 메서드
- Optional에 값이 없을때만 Supplier가 실행
- 디폴트 메서드를 만드는데 시간이 걸리거나 Optional이 비어있을 때만 기본값을 생성하고자 할 때 사용
- orElseThrow(Supplier<? extends X> exceptionSupplier)는 Optional이 비어있을때 지정한 예외를 발생
- ifPresent(Consumer<? super T> consumer)는 값이 존재할 때 인수로 넘겨준 동작을 실행
- ifPresentOrElse(Consumer<? super T> action, Runnable emptyAction)은 Optional이 비어있을 때만 실행할 수 있는 Runnable을 인수로 받음
11.3.6 두 Optional 합치기
public Insurance findCheapestInsurance(Person person, Car car) {
return cheapestCompany;
}
public Optional<Insurance> nullSafeFindCheapestInsurance(
Optional<Person> person, Optional<Car> car) {
if (person.isPresent() && car.isPresent()) {
return Optional.of(findCheapestInsurance(person.get(), car.get()));
} else {
return Optional.empty();
}
}
- 두 Optional을 인수로 받아서 둘 중 하나라도 비어있으면 빈 Optional를 반환
- person과 car의 시그니처만으로 둘 다 아무 값도 반환하지 않을 수 있다는 정보를 명시적을 주지만, 구현 코드는 null 확인 코드와 크게 다르지 않음
11.3.7 필터로 특정값 거르기
Insurance insurance = ...;
if(insurance != null && "CambridgeInsurance".equals(insurance.getName())){
System.out.println("ok");
}
- 위 코드처럼 객체의 메서드를 호출해서 프로퍼티를 확인해야 하는 경우 다음과 같이 재구현할 수 있음
Optional<Insurance> optInsurance = ...;
optInsurance.filter(insurance -> "CambridgeInsurance".equals(insurance.getName()))
.ifPresent(x -> System.out.println("ok"));
11.4 Optional을 사용한 실용 예제
11.4.1 잠재적으로 null이 될 수 있는 대상을 Optional로 감싸기
- Object value=map.get("key");
- null이 반환될 수 있는 코드를 Optional로 감싸서 개선
Optional<Object> value=Optional.ofNullable(map.get("key"));
11.4.2 예외와 Optional 클래스
null을 확인할 때는 if문을 사용했지만 예외를 발생하시키는 메서드는 try/catch 블록을 사용
public static Optional<Integer> stringToInt(String s) {
try {
return Optional.of(Integer.parseInt(s));
} catch (NumberFormatException e) {
return Optional.empty();
}
}
- 정수로 변환할 수 없는 문자열은 빈 Optional로 return하도록 구현
11.4.3 기본형 Optional을 사용하지 말아야 하는 이유
- Optional도 기본형으로 특화된 OptionalInt, OptionalLong, OptionalDouble 등의 클래스를 제공
- 하지만 Optional의 최대 요소 수는 한개이므로 기본형 특화 Optional로 성능을 향상시킬 수 없음
- 또한 기본형 특화 Optional은 map, flatMap, filter등을 제공하지 않고, 생성한 결과를 다른 일반 Optional과 혼용할 수 없음