天天看點

JDK 每半年就會更新一次新特性,再不掌握就要落伍了:JDK8 的新特性

JDK 每半年就會更新一次新特性,再不掌握就要落伍了:JDK8 的新特性

該圖檔由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<>());
      

小試牛刀,還不錯,下面放大招。

JDK 每半年就會更新一次新特性,再不掌握就要落伍了:JDK8 的新特性

假設,我們有一個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):檢查日期字元串是否合法