
該圖檔由Alexandr Podvalny在Pixabay上釋出
你好,我是看山。
本文收錄在 《Java 進階》 系列專欄中。
從 2017 年開始,JDK 版本更新政策從原來的每兩年一個新版本,改為每六個月一個新版本,以快速驗證新特性,推動 Java 的發展。從 《JVM Ecosystem Report 2021》 中可以看出,目前開發環境中仍有近半的環境使用 JDK8,有近半的人轉移到了 JDK11,随着 JDK17 的釋出,相信比例會有所變化。
是以,準備出一個系列,配合示例講解,闡述從 JDK8 開始各個版本的新特性。
概覽
JDK8 從 2014 年問世,到現在已是數個年頭。這個版本新增了 Stream API、Lambda 表達式、新時間 API 等各種新特性,相比很多新興語言也不遑多讓。今天就來聊聊 JDK8 中好玩好使的特性功能(完整特性請參見 這裡)。
接口方法
在 JDK8 之前,接口隻能夠定義public abstract方法,預設可以不寫修飾符。當在接口中新增方法定義,該接口的所有實作類都需要新增這個方法的實作,這樣對于更新擴充很不友好。
從 JDK8 開始,我們可以在接口中定義靜态方法和預設方法了,也就是我們可以在接口中定義具有具體操作行為的方法定義,這樣接口的實作類可以有選擇的實作接口方法。
靜态方法
JDK8 之前,靜态方法是類的專屬技能,這樣會引起概念上的一些歧義。比如,我們定義一個生産者Producer接口,所有生産者都繼承該接口,這個時候,我們需要一個靜态方法提供Producer的名字。這個時候,在單獨定義一個類提供一個靜态方法提供名字,可以實作功能,但是略顯複雜。
現在我們直接在Producer生産者接口中定義靜态方法即可:
static String producer() {
return "target: " + System.currentTimeMillis();
}
沿用約定的限定範圍,我們不需要在方法前面加public。這個靜态方法隻能通過接口調用,或者在接口内部直接引用。比如:
final String target = Producer.producer();
預設方法
接口的預設方法定義需要使用default關鍵字,接口中定義的預設方法可以在實作類中重寫。
比如,我們的生産者Producer需要生産東西,我們可以在接口中定義一個預設方法:
default String produce() {
return "NULL";
}
我們可以定義Producer的實作類是Hamburger,可以選擇重寫接口的預設方法,也可以不用重寫。比如:
public class Hamburger implements Producer {
}
使用的時候直接調用:
final Producer producer = new Hamburger();
System.out.println(producer.produce());
這個時候會列印“NULL”。我們還可以在Hamburger中重寫produce方法:
@Override
public String produce() {
return "HAMBURGER";
}
這個時候會列印“HAMBURGER”。
方法引用
我們在使用 Lambda 表達式時,可以使用方法引用,使表達式更短、更易讀。方法引用有四種表達形式:
靜态方法引用
執行個體方法引用
特定類型的執行個體方法引用
構造方法引用
下面我們分别說一下。
靜态方法引用文法是:類名:: 方法名。假設我們需要判斷一個List<String>隊列中所有元素是否為空,通過 Stream API 我們可以這樣判斷:
final List<String> list = Lists.newArrayList("1", "2", "3", null, "4");
final boolean hasNullElement = list.stream()
.anyMatch(x -> Objects.isNull(x));
System.out.println(hasNullElement);
可以看到,anyMath方法中隻調用了Objects.isNull方法,而且方法的入參直接是清單中的元素,此時,我們可以直接使用靜态方法引用,将代碼改寫一下:
final boolean hasNullElementAlso = list.stream().anyMatch(Objects::isNull);
這樣看起來清爽多了。
執行個體方法引用文法是:執行個體:: 方法名。比如,我們有一個清單中全是LocalDate類型資料,現在需要對其進行格式化,傳回一個字元串清單。我們可以這樣使用:
final DateTimeFormatter fmt = DateTimeFormatter.ISO_LOCAL_DATE;
final List<LocalDate> dates = Lists.newArrayList(
LocalDate.MIN,
LocalDate.now(),
LocalDate.MAX
);
final List<String> dateStrs = dates.stream()
.map(d -> fmt.format(d))
.collect(Collectors.toList());
map方法中通過DateTimeFormatter的執行個體對象調用了format方法,入參也是 Lambda 表達式中的元素,這樣就可以使用執行個體方法引用,代碼可以改寫為:
final List<String> dateStrList = dates.stream()
.map(fmt::format)
.collect(Collectors.toList());
這樣寫起來順手多了。
這種方法引用有一個前提條件,就是必須是 Lambda 表達式元素類型對應的方法。文法是:特定類型:: 方法名。比如,我們需要判斷一個全都不為null的字元串清單中,空字元的數量,我們可以這樣寫:
final List<String> nonNullList = Lists.newArrayList("1", "2", "3", "", "4", "");
final long emptyCount = nonNullList.stream()
.filter(x -> x.isEmpty())
.count();
我們可以看到,filter方法中引用的函數是利用 Lambda 表達式元素對象的方法,這個時候我們可以将代碼改寫為:
final long emptyElementCount = nonNullList.stream()
.filter(String::isEmpty)
.count();
這樣能夠清晰的看出是哪個類的方法了。
構造方法引用的文法是:類名::new。在 Java 中,構造方法是一種特殊的方法,是以構造方法的引用與上面幾種方法類似。比如,想要将字元串清單中的元素全部轉換為Integer格式:
final List<String> allIntList = Lists.newArrayList("1", "2", "3", "4");
final List<Integer> ints = allIntList.stream()
.map(x -> new Integer(x))
.collect(Collectors.toList());
我們可以改寫為:
final List<Integer> intList = allIntList.stream()
.map(Integer::new)
.collect(Collectors.toList());
Optional 神器
空指針異常(NullPointException,NPE)是特别低級但又很難避免的異常,說他低級是因為隻要看到這個異常,就能夠很容易的修複,但是我們很難百分之百的避免這個異常的存在。在 JDK8 之前,我們隻能通過類似obj != null這種模闆式方法判斷。在 JDK8 新增的神器Optional可以更加優雅的解決這個問題。
建立 Optional
Optional的構造方法是使用private修飾的,其提供了三個靜态方法,用于建立Optional執行個體,分别是empty、of、ofNullable,建立之後,Optional是不可變的。
我們可以使用empty定義一個具有空值的Optional對象:
final Optional<String> optional = Optional.empty();
使用of定義一個不為空的對象:
final String str = "value";
final Optional<String> optional = Optional.of(str);
這裡需要注意一下,of方法指派時,使用Objects.requireNonNull驗證參數是否為空,為空就會抛出NullPointerException異常。
如果不太确定是否為空,可以使用ofNullable建立對象:
final String str = getSomeStr();
final Optional<String> optional = Optional.ofNullable(str);
使用 Optional
比如,我們需要傳回一個字元串清單List<String>,當結果是null的時候,我們傳回傳回new ArrayList<>()。如果是在 JDK8 之前,我們得這樣寫:
List<String> list = getList();
List<String> listOpt = list != null ? list : new ArrayList<>();
現在,我們可以借助Optional的能力:
List<String> listOpt = Optional.ofNullable(getList())
.orElse(new ArrayList<>());
小試牛刀,還不錯,下面放大招。
假設,我們有一個User類,内部有個Address類,在内部有個street屬性,我們現在想要擷取一個User對象的street值。如果是以前,我們需要各種判斷是否是null,代碼會寫成這樣:
User user = getUser();
if (user != null) {
Address address = user.getAddress();
if (address != null) {
String street = address.getStreet();
if (street != null) {
return street;
}
}
}
return "not specified";
是不是似曾相識,或者以前親手寫過。現在有了Optional,我們就不需要這麼麻煩了:
String result = Optional.ofNullable(getUser())
.map(User::getAddress)
.map(Address::getStreet)
.orElse("not specified");
是不是相當的優雅,map方法傳回的也是Optional對象,是以我們可以無限處理下去。
如果User類中的getAddress方法傳回的本身就是Optional對象,我們可以使用flatMap替換map。
還有一種情況是我們需要捕捉 NPE 的情況,但是需要包裝為其他自定義異常,這個時候可以使用orElseThrow方法:
String value = null;
Optional<String> valueOpt = Optional.ofNullable(value);
String result = valueOpt.orElseThrow(CustomException::new).toUpperCase();
這裡隻是簡單給出幾個例子,更多功能可以參見 《一文掌握 Java8 的 Optional 的 6 種操作》。
文末總結
本文給出了 JDK8 中幾個比較有意思的特性,完整的特性清單可以從
https://openjdk.java.net/projects/jdk8/features檢視。
本文所有代碼都可以通過在公衆号「看山的小屋」回複“java”擷取。
推薦閱讀
一文掌握 Java8 Stream 中 Collectors 的 24 個操作
一文掌握 Java8 的 Optional 的 6 種操作
Java8 的時間庫(1):介紹 Java8 中的時間類及常用 API
Java8 的時間庫(2):Date 與 LocalDate 或 LocalDateTime 互相轉換
Java8 的時間庫(3):開始使用 Java8 中的時間類
Java8 的時間庫(4):檢查日期字元串是否合法