Modern Java In Action
Every day with Java
Optional Class
Optional 형식을 통해 도메인 모델의 의미를 명확히 만들고, null 참조 대신 값이 없는 상황을 표현해 보자.
Null 참조의 문제점
- 에러의 근원: NullPointerException
- 코드를 어지럽힘: null 확인 코드
- 아무 의미가 없음: null 은 아무 의미도 표현하지 않는다.
- 자바 철학에 위배: 자바는 개발자로부터 모든 포인터를 숨겼지만 null 포인터는 예외
- 형식 시스템에 구멍을 만듦: null의 의미를 알 수 없음
java.util.Optional<T>
- 값이 있을 경우 Optional 클래스는 값을 감싼다.
- 값이 없으면 Optional.empty
Optional 적용 패턴
Optional 객체 만들기
- 빈 Optional - Optional<Car> optCar = Optional.empty();
- null이 아닌 Optional - Optional<Car> optCar = Optional.of(car);
- null 값으로 Optional 만들기 - Optional<Car> optCar = Optional.ofNullable(car);
Map으로 Optional 값을 추출하고 변환하기
Optional<Insurance> optInsurance = Optional.ofNullable(insurance);
Optional<String> name = optInsurance.map(Insurance::getName);
flatMap으로 Optional 객체 연결
Optional<Person> optPerson = Optional.of(person);
Optional<String> name = optPerson.flatMap(Person::getCar)
    							.flatMap(Car::getInsurance)
							    .map(Insurance::getName)
    							.orElse("Unkown");
Optional의 직렬화 불가
- Optional은 Serializable Interface를 구현하지 않는다. 
- Optional 클래스를 필드 형식으로 사용할 수 없으니, Optional 로 값을 반환받을 수 있는 메서드를 추가하자. - public class Person { private Car car; public Optional<Car> getCarAsOptional() { return Optional.ofNullable(car); } }
Optional 스트림 조작
public Set<String> getCarInsuranceNames(List<Person> persons) {
    Stream<Optional<String>> stream =  persons.stream()
        .map(Person::getCar) //return Stream<Optional<Car>>
        .map(optCar -> optCar.flatMap(Car::getInsurance)) //return Optional<Insurance>
        .map(optInsurance -> optInsurance.map(Insurance::getName)) //return Optional<String> mapping
        .flatMap(Optional::stream) //return Stream<Optional<String>>
        .collect(toSet());
    
    return stream.filter(Optional::isPresent) //null이 아닌 값만 전달
        		.map(Optional::get)
        		.collect(toSet());
}
Default Action & Optional unwrap
- get(): Optional 에 값이 반드시 있을 경우 사용하자. (없을 경우 NoSuchElementException 발생)
- orElse(T other): Optional이 값을 포함하지 않을 때 기본값 제공
- orElseGet(Supplier<? extends T> other): Optional 이 비어있을 경우 기본값 생성
- orElseThrow(Supplier<? extends X> exceptionSupplier): Optional이 비어있을 때 예외 발생
- ifPresent(Comsumer<? super T> consumer): 값이 존재할 경우 인수로 넘겨준 동작 실행
- ifPresentOrElse(Comsumer<? super T> action, Runnable emptyAction): Optional 이 비었을 때 실행할 수 있는 Runnable을 인수로 받음
두 Optional 합치기
- Before
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();
    }
}
- After
public Optional<Insurance> nullSafeFindCheapestInsurance(Optional<Person> person, Optional<Car> car) {
    return person.flatMap(p -> car.map(c -> findCheapestInsurance(p, c)));
}
필터로 특정 값 거르기
- Optional 에 값이 있을 경우 filter 동작
Optional<Insurance> optInsurance = Optional.of(insurance);
optInsurance.filter(insurance ->
                   "CambridgeInsurance".equals(insurance.getName()))
    			.ifPresent(x -> System.out.pringln("ok"));
int minAge = 20;
Optional<Person> optPerson = Optional.of(person);
//Person이 minAge 이상의 나이일 경우에만 보험회사 이름 반환
Optional<String> name = optPerson.filter(p -> p.getAge() >= minAge)
							    .flatMap(Person::getCar)
    							.flatMap(Car::getInsurance)
							    .map(Insurance::getName)
    							.orElse("Unkown");
Reference
Optional 활용
잠재적으로 null이 될 수 있는 대상을 Optional로 감싸기
Optional<Object> value = Optional.ofNullable(map.get("key"));
예외와 Optional 클래스
- 예외를 빈 Optional로 처리하기 - //OptionalUtility.java public static Optional<Integer> stringToInt(String s) { try { return Optional.of(Integer.parseInt(s)); } catch { return Optional.empty(); } }
기본형 Optional 을 사용하지 말자
- 기본형 Optional 에는 OptionalInt,OptionalLong,OptionalDouble등이 있다.- 이 기본형 특화 Optional은 다른 일반 Optional과 혼용할 수 없다.
 
응용
- Optional로 프로퍼티에서 지속 시간 읽기 - public int readDuration(Properties props, String name) { return Optional.ofNullable(props.getProperty(name)) //null일 경우 Optional 처리 .flatMap(OptionalUtility::stringToInt) //OptionalUtility.stringToInt 메서드 참조 .filter(i -> i > 0) //음수 필터링 .orElse(0); //기본값 0 }
Date & Time API
java.time
- java.time package 는 LocalDate,LocalTime,LocalDateTime,Instant,Duration,Period등 새로운 클래스를 제공
LocalDate
- 시간을 제외한 날짜를 표현하는 불변 객체 
- 생성 - LocalDate date = LocalDate.of(2022, 1, 1); //현재 날짜 정보 LocalDate today = LocalDate.now(); //parse 정적 메서드 사용 LocalDate date = LocalDate.parse("2022-01-01");
- 사용 - int year = date.getYear(); // 2022 int monthValue = date.getMonthValue(); // 1 Month month = date.getMonth(); // JANUARY int day = date.getDayOfMonth(); // 1 DayOfWeek dow = date.getDayOfWeek(); // SATURDAY int len = date.lengthOfMonth(); // 31 (days in JANUARY) boolean leap = date.isLeapYear(); // false (not a leap year), 윤년 여부 System.out.println(date); //2022-01-01 //TemporalField를 이용한 LocalDate 값 읽기 int year = date.get(ChronoField.YEAR); // 2022 int month = date.get(ChronoField.MONTH_OF_YEAR); // 1 int day = date.get(ChronoField.DAY_OF_MONTH); // 1
LocalTime
- 날짜를 제외한 시간을 표현하는 불변 객체 
- 생성 - LocalTime time = LocalTime.of(12, 34, 56); // 12:34:56 //parse 정적 메서드 사용 LocalTime time = LocalTime.parse("12:34:56");
- 사용 - int hour = time.getHour(); // 12 int minute = time.getMinute(); // 34 int second = time.getSecond(); // 56
LocalDateTime
- 날짜와 시간을 모두 표현 
- 생성 - //2022-01-01T12:34:56 LocalDateTime dt1 = LocalDateTime.of(2022, Month.JANUARY, 1, 12, 34, 56); // LocalDate + LocalTime LocalDateTime dt2 = LocalDateTime.of(date, time); // LocalDate <- atTime LocalDateTime dt3 = date.atTime(12, 34, 56); // LocalDate <- LocalTime LocalDateTime dt4 = date.atTime(time); // LocalTime <- LocalDate LocalDateTime dt5 = time.atDate(date);
- 사용 - LocalDate date1 = dt1.toLocalDate(); LocalTime time1 = dt1.toLocalTime();
Instant
- 기계 전용 유틸리티 
- Unix epoch time 기준으로 특정 지점까지의 시간을 초로 표현 
- 나노초(10억분의 1초)의 정밀도 제공 - Instant.ofEpochSecond(3); Instant.ofEpochSecond(3, 0); Instant.ofEpochSecond(2, 1_000_000_000); //1초 후의 나노초 Instant.ofEpochSecond(4, -1_000_000_000); //4초 전의 나노초
Duration
- 두 시간 객체 사이의 지속시간 Docs - Duration d1 = Duration.between(time1, time2); Duration d2 = Duration.between(dateTime1, dateTime2); Duration d3 = Duration.between(instant1, instant2); //시간 객체를 사용하지 않고 생성 Duration threeMinutes = Duration.ofMinutes(3); Duration threeMinutes = Duration.ofMinutes(3, ChronoUnit.MINUTES);
Period
- 두 시간 객체 사이의 지속 시간을 년,월,일로 표현할 경우 Docs - Period tenDays = Period.between(LocalDate.of(2022, 1, 1), LocalDate.of(2022, 1, 11)); //시간 객체를 사용하지 않고 생성 Period tenDays = Period.ofDays(10); Period threeWeeks = Period.ofWeeks(3); Period twoYearsSixMonthsOneDay = Period.of(2, 6, 1);
간격을 표현하는 날짜와 시간 클래스의 공통 메서드
- between
- from
- of
- parse
- addTo
- get
- isNegative
- isZero
- minus
- multipliedBy
- negated
- plus
- subtractFrom