04.매일 자바와 함께

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 packageLocalDate, 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

Last updated