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