값을 가지는 인스턴스는 Optional.of(obj)로 얻을 수 있다. 만일 입력 파라미터가 null 이면 바로 예외가 발생된다.
빈 인스턴스는 Optional.empty()로 얻을 수 있는 singleton 객체이다.
Optional.ofNullable(obj)를 사용하면 객체가 있으면 값을 가지는 인스턴스를, null 이면 빈 인스턴스를 돌려준다.
null 참조가 발생할 수 있는 모든 곳을 Optional로 대체해서는 안된다.
명확하게 값이 존재해야 하는 경우에는 기존과 마찬가지로 예외를 통해 명확하게 상황을 인지할 수 있도록 해야 한다.
어디까지나 값의 존재 여부가 선택적인 경우에 동일한 체크로직을 직접 작성해야 되서 발생하는 실수를 방지하는 것이 중요하다.
Optional 또한 기본형 특화 버전을 제공하지만 성능상 이점이 없고 제공되는 메서드도 제한적이므로 되도록 사용을 지양한다.
Optional 객체는 null check를 대체할 수 있는 방법을 제공한다.
.map(function): 값이 존재하는 경우 함수를 호출하고 그 결과가 담긴 Optional 객체로 반환한다. 값이 없다면 빈 Optional 객체를 반환한다.
.flatMap(function): 값이 존재하는 경우 함수를 호출하고 그 결과를 그대로 돌려준다 (즉, Optional 객체로 wrapping 하지 않는다). 단, 함수의 결과값은 Optional 객체여야 한다. 값이 없다면 빈 Optional 객체를 반환한다. 보통 특정 필드의 값을 Optional로 선언한 경우 이중으로 Optional wrapping이 일어나지 않도록 하기 위해 사용한다.
.or(supplier): Optional 객체에 값이 있다면 해당 값을, 없다면 입력 파라미터로 주어진 함수를 실행하여 나온 결과를 돌려준다. orElseGet과의 차이점은 함수의 결과가 Optional 객체라는 점이다.
.orElse(value): Optional 객체에 값이 있다면 해당 값을, 없다면 입력 파라미터로 주어진 기본 값을 돌려준다.
.orElseGet(supplier): Optional 객체에 값이 있다면 해당 값을, 없다면 입력 파라미터로 주어진 함수를 호출하여 나온 결과를 돌려준다. 기본값의 생성을 지연시켜서 불필요한 연산을 줄일 수 있다.
.orElseThrow(supplier): Optional 객체에 값이 있다면 해당 값을, 없다면 입력 파라미터로 주어진 함수를 호출하여 나온 예외 객체를 throw 한다.
.ifPresent(consumer): Optional 객체에 값이 있다면 함수를 실행하고 아니면 아무것도 하지 않는다.
.ifPresentOrElse(consumer, runnable): Optional 객체에 값이 있다면 그 값으로 첫번째 함수를 실행하고 아니면 두번째 함수를 실행한다.
.filter(predicate): Optional 객체에 값이 있다면 조건을 만족하는지 확인한다. 조건을 만족하면 Optional 객체를 그대로 돌려주고 아니면 빈 Optional 객체를 돌려준다. 값이 없다면 조건 확인 없이 빈 Optional 객체를 돌려준다.
.stream():Optional 객체에 값이 있으면 하나의 요소를 가지는 스트림을 생성한다. 값이 없다면 빈 스트림을 돌려준다.
.get(): Optional 객체의 값을 돌려준다. 값이 없는 경우 예외가 발생한다. Optional 객체에 값이 확실하게 존재하는 경우에만 unwrap을 위해서 사용해야 한다.
flatMap을 사용하면 Javascript의 optional chaining과 비슷한 구현이 가능하다.
import static java.time.temporal.TemporalAdjusters.*;
// .with(adjuster)를 사용하여 날짜를 조작한다
// 해당 날짜의 다음 일요일 (해당일이 일요일이면 해당일)을 구한다
LocalDate date = LocalDate.of(2014, 3, 18)
.with(nextOrSame(DayOfWeek.SUNDAY));
직접 구현하는 것도 가능하다.
@FunctionalInterface
public interface TemporalAdjuster {
Temporal adjustInto(Temporal temporal);
}
// 예: 다음 업무일 구하기
TemporalAdjuster nextWorkingDay = (t) -> {
int dayOfWeek = t.get(ChronoField.DAY_OF_WEEK);
if (dayOfWeek >= 5) {
return t.with(next(DayOfWeek.MONDAY));
} else {
return t.plus(1, ChronoUnit.DAYS);
}
};
Formatter API
Thread safe한 날짜, 시간 형식 변환을 제공하는 객체이다.
기본적으로 BASIC_ISO_DATE (yyyymmdd), ISO_LOCAL_DATE (yyyy-mm-dd)를 제공한다.
.ofPattern(string[, locale]): 특정 패턴으로 formatter를 생성한다.
DateTimeFormatterBuilder를 통해서 builder 방식으로 formatter를 생성할 수 있다.
Timezone API
시간대를 나타내는 ZoneId 클래스를 제공한다.
.of(id): 특정 시간대의 ZoneId 인스턴스를 얻는다. id는 지역/도시 형식으로 이루어지며 IANA Time Zone Database에서 제공하는 지역 집합 정보를 기반으로 한다.
.systemDefault(): 기본 타입존에 해당되는 ZoneId 인스턴스를 가져온다.
ZonedDateTime 객체는 LocalDateTime + ZoneId의 조합이다.
ZonedDateTime.of(dateTime, zoneId)으로 생성할 수 있다.
또는 UTC 시간대를 기준으로 시간 차이를 표현하는 ZoneOffset도 제공한다.
.of(offsetString): 특정 시간 차이에 해당되는 ZoneOffset 인스턴스를 얻는다 (예: ZoneOffset.of(”-05:00”)).
OffsetDateTime.of(dateTime, zoneOffset)으로 생성할 수 있다.
디폴트 메서드
인터페이스에 새로운 메서드를 추가할 때 기본 구현을 추가할 수 있다.
기본 구현을 추가함으로써 기존에 해당 인터페이스를 상속 받았던 모든 클래스를 고치지 않고도 새로운 메서드를 추가할 수 있다.
인터페이스에 정적 메서드를 추가할 수 있다.
도구 메서드들을 제공하기 위해서 별도의 유틸리티 클래스를 선언할 필요가 없다.
인터페이스를 이용하여 다중 상속을 구현할 수 있다.
다중 상속 시 순서는 아래와 같이 정해진다.
클래스에 정의된 메서드가 가장 우선권을 갖는다.
그 다음에는 서브 인터페이스에 정의된 메서드가 우선권을 갖는다.
위 규칙으로도 결정되지 않은 경우에는 인터페이스를 상속받은 클래스가 명시적으로 해당 메서드를 오버라이드하고 원하는 메서드를 호출해야 한다.
새로운 super 문법을 제공한다.
SuperClass.super.method() 형식으로 특정 상위 클래스의 메서드를 호출할 수 있다.
하위 클래스에서 특정 상위 클래스의 메서드를 호출할 때 사용한다.
다중 상속의 방식은 선택형 메서드, 동작 다중 상속의 두가지가 있다.
선택형 메서드
UnsupportedOperationException을 던지는 기본 구현을 제공하는 방식이다.
상속하는 클래스에서 빈 구현을 추가할 필요가 없으며 필요한 경우에만 override해서 구현할 수 있다.
동작 다중 상속
서로 겹치지 않는 (디폴트 메서드로 구현된) 기능을 다중 상속하여 손쉽게 재사용 할 수 있다.
이는 마치 ruby의 mixin과 비슷한 형태이다.
이를 위해서는 각 인터페이스가 서로 기능이 중복되지 않는 최소의 인터페이스로 구현되어야 한다.
모듈
Java 9에서는 패키지를 묶는 새로운 단위인 모듈을 정의하고 서로 다른 모듈 간의 의존성을 좀 더 세밀하게 지정할 수 있는 방식을 제공한다.
각 모듈의 source root src/main/java 위치에 module-info.java를 작성하여 모듈을 정의할 수 있다.
모듈의 이름은 일반적으로 패키지의 이름과 유사한 방식으로 정의한다.
일반적으로는 포함되는 패키지의 prefix와 일치하게 명명한다.
몇가지 특수한 키워드 구문을 사용하여 모듈을 정의할 수 있다.
module MODULE_NAME { ... }: 모듈을 정의한다.
requires [transitive] MODULE_NAME : 특정 모듈에 의존성이 있음을 명시한다. transitive를 명시할 경우 만약 다른 모듈이 현재 모듈을 사용한다면 자동으로 특정 모듈에 대한 의존성이 추가된다.
exports PACKAGE_NAME [to MODULE_NAMES...] : 특정 패키지를 모듈 외부로 노출한다. 단, 노출 대상이 되는 것은 public 제한자를 가진 것들만 해당된다. 노출 대상이 되는 모듈을 제한하고 싶다면 to MODULE_NAME으로 지정할 수 있다.
opens PACKAGE_NAME [to MODULE_NAMES...] : 특정 패키지를 모듈 외부로 노출한다. 제한자와 관계없이 모든 항목이 노출되므로 reflection을 통해 모든 항목에 접근 가능하다. 노출 대상이 되는 모듈을 제한하고 싶다면 to MODULE_NAME으로 지정할 수 있다.
provides INTERFACE_NAME with CLASS_NAMES...: 특정 인터페이스의 구현체를 지정한다.
uses INTERFACE_NAME: 특정 인터페이스의 구현체들을 사용함을 명시한다.
이렇게 명시된 인터페이스의 구현체들을 ServiceLoader를 통해서 접근하여 사용할 수 있다.
구현체는 0개 또는 하나 이상이 지정될 수 있으므로 그 중에서 필요한 구현체를 직접 선택하여 사용해야 한다.
package greeting.client;
import java.util.ServiceLoader;
import greeting.api.MessageService;
public class TestClient {
public static void main(String[] args) {
ServiceLoader<MessageService> ms =
ServiceLoader.load(MessageService.class);
for (MessageService m : ms) {
System.out.println(m.getMessage());
}
}
}
다음은 모듈 정의의 예시이다.
module com.example.foo {
requires com.example.foo.http;
requires transitive com.example.foo.network;
requires java.logging;
exports com.example.foo.bar;
exports com.example.foo.internal to com.example.foo.probe;
opens com.example.foo.quux;
opens com.example.foo.internal
to com.example.foo.network, com.example.foo.probe;
provides com.example.foo.spi.Intf with com.example.foo.Impl;
uses com.example.foo.spi.Intf;
}
자동 모듈은 module-info.java를 통해서 모듈을 정의하지 않은 경우 자동으로 생성되는 모듈을 뜻하며 모든 패키지를 open 한 것과 동일하다.