1. 程式人生 > 程式設計 >Java11新特性解讀

Java11新特性解讀

在去年的9月26日,Oracle官方宣佈Java11正式釋出,這是Java大版本週期變化後的第一個長期支援版本,非常值得關注。Java9和Java10都在很短的時間內就過渡了,所以,Java11將是一個不可忽視的版本。從時間節點看,JDK11的釋出正好處在JDK8免費更新到期的前夕,同時,JDK8、9也將陸續成為"歷史版本"。那麼,關於Java11的新特性到底有哪些呢?容我一一介紹。

區域性型別推斷

什麼是區域性型別推斷?

var str = "helloworld";
System.out.println(str);複製程式碼

區域性變數型別推斷就是左邊的型別直接使用var定義,而不用寫具體的型別,編譯器能根據右邊的表示式自動推斷型別,如上面的str變數使用var定義,編譯器就能通過右邊的"helloworld"自動推斷出這是一個String型別的變數。但是,值得注意的是,這個var並不是一個關鍵字,很多同學看到變數都能使用var來定義,那var還不是關鍵字嗎?事實上,它真的不是一個關鍵字。

int var = 10;
System.out.println(var);複製程式碼

例如上面的這段程式碼是能夠正確執行的,這證明var不是關鍵字。我們還可以通過反編譯來看,例如我們反編譯這樣一段程式碼:

var a = 100;
System.out.println(a);複製程式碼

反編譯得到的結果為:

 byte a = 100;
 System.out.println(a);複製程式碼

從這裡可以看出,var僅僅是一個語法上的改進,在編譯時期便已經將var轉換為了對應的變數型別。然而在使用var定義變數時,必須立刻賦值,例如下面的情況是錯誤的:

var a;複製程式碼

因為在不賦值的情況下,JVM無法推斷當前變數的型別。在類中的成員變數(也叫屬性)不可以使用var來定義,例如下面的情況是錯誤的:

class Student{
    var name = "小明";
    var age = 20;
}複製程式碼

var的好處在lambda表示式中體現得淋漓盡致。我們知道,開啟一個執行緒可以使用lambda表示式來完成:

Thread t = new Thread(() -> System.out.println(Thread.currentThread().getName()));
t.start();複製程式碼

這是一個無參的lambda表示式形式,我們再看一個帶參lambda表示式:

String[] arr = { "program","creek","is","a","java","site" };
Stream<String> stream = Stream.of(arr);
stream.forEach(x -> System.out.print(x + "\t"));複製程式碼

這是一個forEach的用法,其中需要用到變數x,因為這裡它自動推斷出了x的型別為String,所以String被省略了,那麼加上var之後程式碼變成這樣:

String[] arr = { "program","site" };
Stream<String> stream = Stream.of(arr);
stream.forEach((var x) -> System.out.print(x + "\t"));複製程式碼

如果僅僅只是這樣寫,倒是無法看出寫var有什麼優勢,反而覺得有點多此一舉,但是如果要給lambda表示式變數標註註解的話,那麼這個時候var的作用就體現出來了。

String[] arr = { "program","site" };
Stream<String> stream = Stream.of(arr);
stream.forEach((@Nonnull var x) -> System.out.print(x + "\t"));複製程式碼

那麼var的優勢何在呢?因為你要標註註解的話,就必定要寫出x的型別,如下面這段程式是錯誤的:

String[] arr = { "program","site" };
Stream<String> stream = Stream.of(arr);
stream.forEach((@Nonnull x) -> System.out.print(x + "\t"));複製程式碼

但是,我們從何得知x的型別呢?其實我們不用知曉,因為var就能自動推斷,所以,var的好處在這裡就體現出來了。

集合中的新API

在Java9之前,我們要想建立新集合,我們得這樣做:

List<String> list = new ArrayList<String>();
list.add("hello");
list.add("world");
list.add("java");複製程式碼

建立過程略顯麻煩,那麼現在,我們可以通過這樣的方式來建立集合:

List<String> list = List.of("hello","world","java");複製程式碼

但是,請注意了,用這樣的方式來建立的集合,是無法新增元素的,我們可以嘗試著新增一下:

List<String> list = List.of("hello","java");
list.add("test");複製程式碼

執行結果如下:

Exception in thread "main" java.lang.UnsupportedOperationException
    at java.base/java.util.ImmutableCollections.uoe(ImmutableCollections.java:71)
    at java.base/java.util.ImmutableCollections$AbstractImmutableCollection.add(ImmutableCollections.java:75)
    at com.itcast.TestDemo.main(TestDemo.java:8)複製程式碼

那麼這到底是為什麼呢?我們通過原始碼來分析一下。首先我麼看看List類的of()方法:

static <E> List<E> of(E e1,E e2,E e3) {
    return new ImmutableCollections.ListN<>(e1,e2,e3);
}複製程式碼

該方法呼叫了ImmutableCollections類的ListN()生成一個集合並返回,我們看看ListN的原始碼:

static final class ListN<E> extends AbstractImmutableList<E>
            implements Serializable {

        static final List<?> EMPTY_LIST = new ListN<>();

        @Stable
        private final E[] elements;

        @SafeVarargs
        ListN(E... input) {
            // copy and check manually to avoid TOCTOU
            @SuppressWarnings("unchecked")
            E[] tmp = (E[])new Object[input.length]; // implicit nullcheck of input
            for (int i = 0; i < input.length; i++) {
                tmp[i] = Objects.requireNonNull(input[i]);
            }
            elements = tmp;
        }

        @Override
        public boolean isEmpty() {
            return size() == 0;
        }

        @Override
        public int size() {
            return elements.length;
        }

        @Override
        public E get(int index) {
            return elements[index];
        }

        private void readObject(ObjectInputStream in) throws IOException,ClassNotFoundException {
            throw new InvalidObjectException("not serial proxy");
        }

        private Object writeReplace() {
            return new CollSer(CollSer.IMM_LIST,elements);
        }
    }複製程式碼

它是ImmutableCollections類的一個靜態內部類,我們暫且不管它是如何生成集合的,我們找找裡面有沒有add()方法,會發現裡面並不存在add()方法,那麼我們既然能夠呼叫到,那麼add()方法肯定在其父類中。最終,在它的父類AbstractImmutableCollection中找到了add()方法:

// all mutating methods throw UnsupportedOperationException
        @Override public boolean add(E e) { throw uoe(); }
        @Override public boolean addAll(Collection<? extends E> c) { throw uoe(); }
        @Override public void    clear() { throw uoe(); }
        @Override public boolean remove(Object o) { throw uoe(); }
        @Override public boolean removeAll(Collection<?> c) { throw uoe(); }
        @Override public boolean removeIf(Predicate<? super E> filter) { throw uoe(); }
        @Override public boolean retainAll(Collection<?> c) { throw uoe(); }複製程式碼

add()方法呼叫了uoe()方法,而uoe()方法直接丟擲了一個異常:

static UnsupportedOperationException uoe() { return new UnsupportedOperationException(); }複製程式碼

會發現,呼叫uoe()方法的不只add()方法一個,有關於集合新增、修改、刪除的種種操作都會丟擲異常。所以,由of()方法建立的集合是不可以進行這些相關操作的。

流中的新API

上面集合中說到的of()方法同樣可以用在流中。

Stream<Integer> stream = Stream.of(1,2,3,4,5);複製程式碼
Stream stream = Stream.of();
Stream stream2 = Stream.of(null);複製程式碼

而在上面的兩條語句中,第二條語句會產生空指標異常,當然,我們不能允許我們的程式出現這樣的異常,但是你又很有可能會傳入一個null,這樣的情況該如何避免呢?從Java9開始,出現了一個新方法:

Stream stream3 = Stream.ofNullable(null);複製程式碼

該方法允許你傳入一個null值,以此避免空指標異常產生。繼續介紹Stream中的新API。

  1. takeWhile()
    該方法會從流中一直獲取判定器為真的元素,一旦遇到元素為假,就終止處理
Stream<Integer> stream = Stream.of(1,5,6,7);
Stream stream2 = stream.takeWhile(t -> t % 2 != 0);
stream2.forEach(System.out::println);複製程式碼

這段程式的執行結果:

1
3複製程式碼

你若是理解了這個方法的意思,這樣的輸出結果就不難理解。因為當獲取到元素2時,判定器為假,此時會終止處理,所以後面的元素就不會再去處理。     2.dropWhile()那麼這方法和takeWhile()方法相反,它會從流中一直丟棄判定器為真的元素,一旦遇到元素為假,就終止處理

Stream<Integer> stream = Stream.of(1,7);
Stream stream2 = stream.dropWhile(t -> t % 2 != 0);
stream2.forEach(System.out::println);複製程式碼

所以上面程式段的執行結果為:

2
5
6
7複製程式碼
字串中的新API

1.isBlank()判斷字串中的字元是否都為空白2.strip()去除字串首尾的空白3.stripTrailing()去除字串尾部的空白4.stripLeading()去除字串首部的空白5.repeat()複製字串,可以傳入一個int型別值來控制複製次數

我們知道在字串處理方法中,trim()方法也能夠去除字串首尾的空白,那為什麼Oracle還要設計一個重複的方法呢?這必然有它的道理。其實,trim()方法要比strip()方法簡單得多:

    /**
     * Returns a string whose value is this string,with all leading
     * and trailing space removed,where space is defined
     * as any character whose codepoint is less than or equal to
     * {@code 'U+0020'} (the space character).
     */
    public String trim() {
        String ret = isLatin1() ? StringLatin1.trim(value)
                                : StringUTF16.trim(value);
        return ret == null ? this : ret;
    }複製程式碼

通過查閱原始碼中對該方法的註釋發現,trim()方法只能去除Unicode碼值小於等於32的空白字元,而32正好指的是空格,那麼對於全形的空格,trim()方法就無能為力了。所以在功能上,strip()方法更加強大。

HttpAPI

這是Java9開始引入的一個處理HTTP請求的API,該API支援同步和非同步,而在Java11中已經為正式可用狀態。

HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder(URI.create("http://www.baidu.com")).build();
BodyHandler<String> responseBodyHandler = BodyHandlers.ofString();
HttpResponse<String> response = client.send(request,responseBodyHandler);
String body = response.body();
System.out.println(body);複製程式碼

這是一段基本的訪問百度的請求程式碼,當然,它還提供了非同步請求方式:

HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder(URI.create("http://www.baidu.com")).build();
BodyHandler<String> responseBodyHandler = BodyHandlers.ofString();
CompletableFuture<HttpResponse<String>> sendAsync = client.sendAsync(request,responseBodyHandler);
HttpResponse<String> response = sendAsync.get();
String body = response.body();
System.out.println(body);複製程式碼
新版本廢棄了哪些內容

  • 刪除了com.sun.awt.AWTUtilities類
  • 從Oracle JDK中刪除了Lucida字型
    Oracle JDK不再提供任何字型,完全依賴於作業系統上安裝的字型。
  • 刪除了appletviewer啟動器
Epsilon垃圾收集器

JDK上對這個特性的描述是:開發一個處理記憶體分配但不實現任何實際記憶體回收機制的GC,一旦可用堆記憶體用完,JVM就會退出。我們可以來嘗試著使用一下它,首先我們編寫一段程式:

public class EpsilonTest {
    public static void main(String[] args) throws Exception {
        var list = new ArrayList<>();
        boolean flag = true;
        int count = 0;
        while (flag) {
            list.add(new Garbage());
            if (count++ == 500) {
                list.clear();
            }
        }
    }
}

class Garbage {
    private double d1 = 1;
    private double d2 = 2;

    /**
     * GC在清除本物件時會呼叫該方法
     */
    @Override
    protected void finalize() throws Throwable {
        System.out.println(this + " collecting");
    }
}複製程式碼

這是一個無限迴圈的程式,迴圈體不斷建立Garbage物件並放入集合,當迴圈次數達到500時將集合清空,此時的500個物件均為垃圾,會被GC清理,清理時呼叫finalize()方法列印資訊。執行這段程式,結果如下:

...
com.itcast.Garbage@1e9c634c collecting
java.lang.OutOfMemoryError: Java heap space
    at com.itcast.EpsilonTest.main(EpsilonTest.java:11)
com.itcast.Garbage@1174213e collecting
com.itcast.Garbage@2029a4b8 collecting
...複製程式碼

當程式執行到某一刻時,記憶體溢位,程式終止。現在我們來使用一下Epsilon,右鍵選擇類檔案,在Run As右側選擇Run Configurations:在這裡插入圖片描述現在我們將預設的GC換為了Epsilon,再來看看執行結果:

Terminating due to java.lang.OutOfMemoryError: Java heap space複製程式碼

會發現,控制檯只輸出了這麼一句,說明被清除的集合中的物件並沒有被回收,而且記憶體溢位的速度也非常快,這說明該GC是並不會回收垃圾,那麼它有什麼作用呢?它提供完全被動的GC實現,具有有限的分配限制和儘可能低的延遲開銷,但代價是記憶體佔用和記憶體吞吐量,它的主要用途有以下幾個方面:

  • 效能測試(它可以幫助過濾掉GC引起的效能假象)
  • 記憶體壓力測試
  • 非常短的JOB任務
  • VM介面測試
ZGC垃圾回收器

有人說這是JDK11最為矚目的特性,沒有之一,是最重磅的升級,那麼ZGC的優勢在哪裡呢?

  • GC暫停時間不會超過10毫秒
  • 既能處理幾百兆的小堆,也能處理幾個T的大堆
  • 和G1相比,應用吞吐能力不會下降超過15%
  • 為未來的GC功能和利用colord指標以及Load barriers優化奠定了基礎

ZGC是一個併發、基於region、壓縮型的垃圾收集器,只有root掃描階段會STW(strop the world,停止所有執行緒),因此ZGC的停頓時間不會隨著堆的增長和存活物件的增長而變長。用法:-XX:UnlockExperimentalVMOptions -XX:+UseZGC雖然功能如此強大,但很遺憾的是,在Windows系統的JDK中並沒有提供ZGC,所以也就沒有辦法嚐鮮了。

Flight Recorder

這是一個記錄儀,用於診斷程式執行過程,那麼在之前這是一個商業版的特性,是要收費的,從Java11開始,Fight Recorder免費提供使用並開源。它可以匯出事件到檔案中,之後可以用Java Mission Control來分析,也可以在應用啟動時配置java -XX:StartFlightRecording或者在應用啟動之後使用jcmd來錄製,比如:

$ jcmd <pid> JFR.start
$ jcmd <pid> JFR.dump filename=
$ jcmd <pid> JFR.stop複製程式碼
其它

在Java11中,支援一個命令編譯執行檔案,在之前的版本中,我們要想執行一個Java程式,首先得用javac指令編譯,然後用java指令執行。而在新版本中,我們直接使用java指令即可完成編譯執行操作。

在Unicode10版本中,增加了8518個字元,總計達到了136690個字元,這已經超出了char型別的數值範圍,所以在Java11中,新增了CharacterData,使用四個位元組來處理字元。

那麼有關Java11的新特性就介紹到這裡。