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 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
Last updated