Java SE 10 新增特性

Java SE 10 新增特性

作者:Grey

原文地址:Java SE 10 新增特性

源碼

源倉庫: Github:java_new_features

鏡像倉庫: GitCode:java_new_features

類型推斷

無需定義變量類型,通過var關鍵字結合初始化的值,可以推測出變量類型

package git.snippets.jdk10;

/**
 * 類型推斷
 *
 * @author <a href="mailto:[email protected]">Grey</a>
 * @date 2022/8/17
 * @since 10
 */
public class TypeRefEnhance {
    public static void main(String[] args) {
        var a = 2; // a表示int
        System.out.println(a);
        var b = "hello"; // b 表示String
        System.out.println(b);
        var date = new java.util.Date();
        System.out.println(date);
        var obj = new Customer("Grey"); // 自定義對象
        System.out.println(obj);
        var sum = new TypeRefEnhance().add(1, 23);
        System.out.println(sum);
        var var = 3;
        System.out.println(var);
    }

    public int add(int a, int b) {
        return a + b;
    }

    static class Customer {
        String name;

        public Customer(String n) {
            name = n;
        }

        @Override
        public String toString() {
            return "Customer{" +
                    "name=" + name +
                    '}';
        }
    }
}

var 看似好用,但是請謹慎使用,比如

var x = someFunction()

是因為如果不追蹤someFunction()方法的返回類型,讀者就不可能知道x的類型。多年來,人們對動態類型語言提出了類似的抱怨。

所以,記住我們的目標是編寫可讀的代碼。

在Java中,var是一個特殊的新類型,你仍然可以在你的代碼的其他地方使用var,比如作為一個變量或類名。這使得Java能夠與Java 10之前的代碼保持向後兼容,所以如下定義是沒問題的

var var = 3;

不可變集合 API

如下代碼

package git.snippets.jdk10;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;

/**
 * 集合API增強
 *
 * @author <a href="mailto:[email protected]">Grey</a>
 * @date 2021/11/29
 * @since 10
 */
public class CollectionEnhance {
    public static void main(String[] args) {
        var vegetables = new ArrayList<>(List.of("Brocolli", "Celery", "Carrot"));
        var unmodifiable = Collections.unmodifiableList(vegetables);
        vegetables.set(0, "Radish");
        var v = unmodifiable.get(0);
        // 以下這行會報錯
        unmodifiable.set(0, "XXX");
        System.out.println(v);
        System.out.println(unmodifiable);
    }
}

根據Java 10Collections的最新定義,unmodifiableList返回一個不可修改的視圖集合。所以unmodifiable.set(0, "XXX");會直接報錯

Exception in thread "main" java.lang.UnsupportedOperationException
 at java.base/java.util.Collections$UnmodifiableList.set(Collections.java:1308)
 at git.snippets.jdk10.CollectionEnhance.main(CollectionEnhance.java:22)

Java 10增加了兩個新的API來實現這一點,也就是說,創建完全不能修改的集合。

第一個APIcopyOf,用來製作集合的不可修改的副本。

    static void copyOfTest() {
        var list = List.of("a", "b", "c");
        var copyList = List.copyOf(list);
        list.add("d");
        // 由於copyList是副本, 所以copyList不會受到list的影響,打印出[a,b,c]
        System.out.println(copyList);
        System.out.println(list);
        // 由於是不可變集合,所以這裡會報錯
        copyList.add("d");
    }

這與用Collections.unmodifiableList包裝一個列表是不同的。 copyOf是創建副本(源集合改變不會影響副本集合),而Collections.unmodifiableList是生成視圖(源集合改變會影響視圖)。

第二個APIStream包中的Collectors類增加的三個新方法。現在你可以使用toUnmodifiableListtoUnmodifiableSettoUnmodifiableMap在生成一個不可修改的集合。代碼如下

    static void unmodifiedTest() {
        List<String> list = List.of("b", "a", "b", "c");
        List<String> c1 = list.stream().collect(Collectors.toUnmodifiableList());
        System.out.println(c1);
        // 會報錯
//        c1.add("c");
//        System.out.println(c1);
        Set<String> c2 = list.stream().collect(Collectors.toUnmodifiableSet());
        System.out.println(c2);
        // 會報錯
//        c2.add("a");
//        System.out.println(c2);
        // 會報錯
//        c2.add("e");
//        System.out.println(c2);
    }

注意,雖然這些方法的名字可能會讓你想起Collections.unmodifiableList等,但這些新方法產生的是真正的不可修改的列表,而Collections.unmodifiableList則返回一個不可修改的視圖。

Unicode 語言標籤擴展

Java SE 10實現了最新的LDML 規範中指定的更多的擴展。

主要增加了下面幾個擴展方法。

java.time.temporal.WeekFields::of
java.util.Calendar::{getFirstDayOfWeek,getMinimalDaysInWeek}
java.util.Currency::getInstance
java.util.Locale::getDisplayName
java.util.spi.LocaleNameProvider
java.text.DateFormat::get*Instance
java.text.DateFormatSymbols::getInstance
java.text.DecimalFormatSymbols::getInstance
java.text.NumberFormat::get*Instance
java.time.format.DateTimeFormatter::localizedBy
java.time.format.DateTimeFormatterBuilder::getLocalizedDateTimePattern
java.time.format.DecimalStyle::of

嘗試一下。

package git.snippets.jdk10;

import java.util.Calendar;
import java.util.Currency;
import java.util.Locale;

/**
 * unicode擴展
 * @since 10
 */
public class UnicodeTest {
    public static void main(String[] args) {
        Currency chinaCurrency = Currency.getInstance(Locale.CHINA);
        Currency usCurrency = Currency.getInstance(Locale.US);
        System.out.println("本地貨幣:" + chinaCurrency);
        System.out.println("US.貨幣:" + usCurrency);

        String displayName = Locale.getDefault().getDisplayName();
        String displayLanguage = Locale.getDefault().getDisplayLanguage();
        String displayCountry = Locale.getDefault().getDisplayCountry();
        System.out.println("本地名稱:" + displayName);
        System.out.println("本地語言:" + displayLanguage);
        System.out.println("本地國家:" + displayCountry);
        int firstDayOfWeek = Calendar.getInstance().getFirstDayOfWeek();
        System.out.println("本地每周第一天:" + firstDayOfWeek);
    }
}

輸出結果。

本地貨幣:CNY
US.貨幣:USD
本地名稱:中文 (中國)
本地語言:中文
本地國家:中國
本地每周第一天:1

G1 性能增強

早在 Java 9 時就已經引入了 G1 垃圾收集器,G1 的優點很多。而在 Java 10 中還是做了小小調整,當 G1 的並發收集線程不能快速的完成全 GC 時,就會自動切換到並行收集,這可以減少在最壞情況下的 GC 速度。

類數據共享

Java SE 5引入了類數據共享(CDS),以改善小型Java應用程序的啟動時間。

JVM第一次啟動時,由引導類加載器加載的任何東西都被序列化並存儲在磁盤上的一個文件中,可以在JVM的未來啟動中重新加載。這意味着JVM的多個實例共享類元數據,因此它不必每次都加載它們。

共享數據緩存意味着小型應用程序的啟動時間有了很大的改善,因為在這種情況下,核心類的相對大小要大於應用程序本身。

Java SE 10將此擴展到包括系統類加載器和平台類加載器。為了利用這一點,你只需要添加以下參數

-XX:+UseAppCDS

Java SE 10還允許你把你自己的應用程序特定的類也存儲到類-數據共享緩存中,可能會減少你的啟動時間。

基本上,這是一個三步走的過程。第一步是創建應該被歸檔的類的列表,用適當的標誌啟動你的應用程序,並指出你希望列表被存儲的位置。

java -Xshare:off -XX:+UseAppCDS -XX:DumpLoadedClassList=myapp.lst \
  -cp $CLASSPATH $MAIN_CLASS

然後,用這個清單,你將創建一個CDS檔案

java -Xshare:dump -XX:+UseAppCDS -XX:SharedClassListFile=myapp.lst \
  -XX:SharedArchiveFile=myapp.jsa \
  -cp $CLASSPATH

最後,運行你的應用程序,使用該存檔

java -Xshare:on -XX:+UseAppCDS -XX:SharedArchiveFile=hello.jsa \
    -cp $CLASSPATH $MAIN_CLASS

更多內容參考:JEP 310: Application Class-Data Sharing

新的即時編譯器Graal

即時編譯器(JIT)是Java的一部分,它在運行時將Java位元組碼轉換為機器代碼。最初的JIT編譯器是用C++編寫的,現在被認為相當難以修改。

Java SE 9引入了一個新的實驗性接口,稱為JVM編譯器接口或JVMCI。新接口的設計使得用純Java重寫JIT編譯器成為可能。Graal是由此產生的JIT編譯器,完全用Java編寫。

Graal目前是一個實驗性的JIT編譯器。在未來的Java版本之前,只有Linux/x64機器可以使用它。要啟用Graal,請在啟動應用程序時在命令行參數中添加這些標誌。

-XX:+UnlockExperimentalVMOptions -XX:+UseJVMCICompiler

更多內容參考:graalvm

Thread-Local Handshakes

在服務性操作中,比如為所有線程收集堆棧跟蹤或執行垃圾回收,當JVM需要暫停一個線程時,它需要停止所有線程。有時,這些被稱為 “stop-the-world”的暫停。這是由於JVM想要創建一個全局安全點,一旦JVM完成工作,所有應用線程都可以從這個安全點重新開始。

但在Java SE 10中,JVM可以將任意數量的線程放入安全點,並且線程在執行規定的 “握手”後可以繼續運行。這導致JVM一次只需暫停一個線程,而以前則必須暫停所有線程。

更多參考:JEP 312: Thread-Local Handshakes

容器感知

JVM現在知道它何時在Docker容器內運行。這意味着應用程序現在擁有關於docker容器分配給內存、CPU和其他系統資源的準確信息。

以前,JVM會查詢主機操作系統來獲得這些信息,這就造成了一個問題。

例如,假設你想創建一個基於Javadocker鏡像,其中運行的JVM被分配了容器所指定的25%的可用內存。在一個擁有2G內存的盒子上,運行一個配置為0.5G內存的容器,Java SE 9和更早的版本會錯誤地根據2G的數字而不是0.5G來計算Java進程的堆大小。

但是,現在,在Java SE 10中,JVM能夠從容器控制組(cgroups)中查找這些信息。

有一些命令行選項可以指定Docker容器內的JVM如何分配內部內存。例如,為了將內存堆設置為容器組的大小,並限制處理器的數量,你可以傳入這些參數

-XX:+UseCGroupMemoryLimitForHeap -XX:ActiveProcessorCount=2

隨着容器成為部署服務的標準方式,這意味着開發者現在有一種基於容器的方式來控制他們的Java應用如何使用資源。

指定替代的內存分配

通過允許用戶指定替代的內存設備來分配堆,Java正朝着更加異構的內存系統發展。

一個直接的用例是能夠在非易失性DIMMNVDIMM)模塊上分配堆,這在大數據應用中是常用的。

另一個用例是在同一台機器上運行許多JVM進程。在這種情況下,讓那些需要較低讀取延遲的進程映射到DRAM上,其餘的進程映射到NVDIMM上。

可以在你的啟動參數中添加如下標誌

-XX:AllocateHeapAt=<path>

這裡的path通常是一個內存映射的目錄。

更多

Java SE 7及以後各版本新增特性

參考資料

Java Language Updates

Java 新特性教程

What』s New in Java 10?

Java 10 Features (with Examples)

Java 10 Features and Enhancements

New Features in Java 10

graalvm

JEP 310: Application Class-Data Sharing

Java 10