【java進階】- jdk8-14新特性你真的瞭解嗎?

前言:jdk從1.0開始到現在已經有24個年頭了,jdk的大版本也整整迭代了14版,平均下來每2年就有一個新版本,事實上自從sun被oracle收購之後,jdk的迭代速度就像搭上了快車,幾乎每半年就是一個版本,截至目前已經發行到jdk14了,然鵝大部分公司到現在用的最新版本也都沒有超過jdk8,由於收費和設計(面向大公司)以及學習成本,使得大多數公司的jdk的版本選擇滯後,但Jdk在迭代的過程中確實提供了非常多好的api以及性能優化,而且這些點也經常被各大公司面試中提及,所以有必要深入去學習一番,並在有機會時使用較新的Jdk版本進行開發,而不是固步自封。累計約數百個更新點,限於篇幅,本篇只總結jdk8-14中最常用的點和比較重要的點。


目錄

 

1.jdk8

1.1api層面

1.1.1stream及lambda表達式

1.1.2complatableFuture

1.1.3Date及Time類

2.jdk9

2.1api層面

2.1.1 提供集合創建不可變集合的of方法

2.1.2提供私有接口方法

2.1.3提供Jshell調試

2.2性能層面

2.2.1統一jvm日誌

2.2.2默認垃圾回收器爲G1GC

2.2.3提供多版本兼容的Jar

2.2.4Linking最小依賴

3.jdk10

3.1api層面

3.1.1局部變量類型推斷

3.2性能層面

3.2.1 G1 FullGC優化

4.jdk11

4.1api層面

4.1.1lambda支持var推斷

4.1.2全新的httpClient

4.2性能層面

4.2.1初次引入ZGC

5.jdk12

5.1api層面

5.1.1switch語句優化

5.2性能層面

5.2.1首次引入shennandoah GC

6.jdk13

6.1api層面

6.1.1switch可以有返回值

6.1.2text blocks

6.2性能層面

6.2.1ZGC

7.jdk14

7.1API層面

7.1.1對Instance of 的增強

7.1.2友好的NPE異常提示

7.1.3switch語句功能擴展

7.2性能層面

7.2.1 ZGC支持Mac操作系統

7.2.2 移除CMS GC


1.jdk8

1.1api層面

1.1.1stream及lambda表達式

先說流stream,在傳統的方式中,如果我們需要對集合進行n箇中間操作再獲取結果,往往需要多次遍歷,非常低效,有了stream之後,通常僅需要一次遍歷即可完成各種操作,而且它支持多線程操作,不再像for循環只能串行執行。另外stream提供了多種實用的方法,比如map,filter,sort,reverse,distinct...可以提高開發效率以及改善代碼優雅,另外使用parallelStream還可以在一場景下提高處理效率。

//對集合中的元素去重+1並按從小到大排序
List<Integer> list = Arrays.asList(1,3,2,4,3);
List<Integer> collect = list.stream()
                            .distinct()
                            .map(i->i+1)
                            .sorted()
                            .collect(Collectors.toList());

上面stream的寫法就是lambda表達式的一種,lambda表達式可以在很多地方使用,常見的比如寫匿名內部類,循環等:

  //匿名內部類   
  Comparator<Integer> comparator = new Comparator<Integer>() {
            @Override
            public int compare(Integer o1, Integer o2) {
                if (o1 > o2) {
                    return 1;
                }
                return 0;
            }
        };

  //lambda 
  Comparator<Integer> comparator1 = (o1, o2) -> {
            if (o1 > o2) {
                return 1;
            }
            return 0;
        };

同樣是比較器,lambda表達式寫的代碼看起來會更加簡潔清爽一些,寫lambda表達式記住一點就好了,就是只要是能根據語境推斷出下文的,都可以簡寫爲lambda表達式的語法,比如上面排序比較器裏compare方法,Integer是可以根據泛型推斷出來的,所以可以簡寫爲o1,o2不用再次指明類型,然後compare方法是固定的,所以可以省略方法名及返回類型簡寫爲括號(),關於Lambda寫法的原則就是能多簡單就多簡單,簡化到不能再簡寫爲止,好在較新版本的IDEA裏會有提示,如果你有強迫症它會促使你寫到最簡,即便是不會寫Lambda表達式,編輯器也會把最優的寫法展示給你,你可以選擇是否替換。

 

1.1.2complatableFuture

complatableFuture默認採用性能更好的ForkJoinPool線程池,而且相比jdk1.5提供的Future類,它具有類似Netty中的future Listener效果,通過future.get()獲取不再阻塞,而是真正意義上的異步,回調,而且也會讓你的代碼看起來更優雅:

例如我想對一段字符串進行轉大寫,然後拼接(假設轉的過程和拼接都比較耗時),如果採用jdk5提供的future.get()來操作,我需要阻塞等到字符串轉爲大寫後才能進行拼接,而jdk8提供的completableFuture則不需要這麼做,轉換和拼接可以異步完成,最終完成後可以在whenComplete中執行後續動作,如果中間耗時過程和步驟比較多,completableFuture的優勢就更能體現了。

        CompletableFuture<String> completableFuture = CompletableFuture.supplyAsync(() -> "abc");
        String value = completableFuture
                .thenApplyAsync(String::toUpperCase)
                .thenApplyAsync(r -> r + "def")
                .whenComplete((r, e) -> System.out.println("完成"))
                .get();
        System.out.println(value);

1.1.3Date及Time類

Jdk在早期版本提供的Date類可謂是臭名昭著,涉及日期相關的操作真的很反人類,當然也完全可以理解,畢竟是早期版本提供的能力,那時候技術還不夠發達,但截至Jdk8那麼漫長的歲月裏Jdk對日期和時間相關類都沒有更新,以致於大部分人都棄用jdk提供的Date相關API而選擇投奔Joda-time,不僅好用,還線程安全,於是Jdk終於在8這個版本提供了一套以LocalDate,LocalDateTime爲核心的日期時間類,線程安全,方法全面,而且不需要引入額外依賴,推薦在開發中使用.API能力比較多,不熟悉的建議去官網多看看,這裏僅貼一個demo看看:

//獲取當前日期和時間        
LocalDateTime localDateTime = LocalDateTime.now();
DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
System.out.println(dtf.format(localDateTime));//2020-04-15 14:13:18

1.1.4接口的默認方法

可以直接在接口裏寫默認的實現,如果接口的實現類沒有實現該方法,編輯器不會強制要求實現類實現該方法,當該方法被調用時,會執行接口中的實現.

public interface TestInterface {
    default String getUserNameById(Long userId) {
        return "defaultUser";
    }
}

1.1.5可重複自定義註解

jdk8中,可以重複使用同一個自定義註解,常見的比如spring提供的@ComponentScan,就可以在一個類上多次使用.自定義的方式可以參考下面示例:

@Name("老王")
@Name("老李")
@Name("老張")
public class TestRepeteAnnotation {
    public static void main(String[] args) {
        Name[] names = TestRepeteAnnotation.class.getAnnotationsByType(Name.class);
        Arrays.stream(names)
                .forEach(name -> System.out.println(name.value()));
                //老王,老李,老張
    }
}
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Names {
    Name[] value();
}
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Repeatable(Names.class)
public @interface Name {
    String value();
}

 1.1.6函數式接口

Functional接口可以不用加@FunctionalInterface註解,但如果加了@FunctionalInterface註解,編輯器會強制要求該接口必須有且只有一個抽象方法,否則會報錯;不加@FunctionalInterface註解,但接口中有超過一個抽象方法或沒有抽象方法,則該接口不再是函數式接口,轉爲普通接口.函數式接口具有支持延遲處理和支持Lambda表達式的優勢,所以推薦使用.

先來看個栗子,當且僅當level爲INFO級別時纔打印日誌,那麼我們來看下下面這段代碼,仔細看還是有一些問題的,比如事實上日誌並沒有被打印,但卻進行了字符串拼接,啥也沒做但耗了拼接字符串的性能,我們想要的是如果日誌級別不滿足,直接啥也不幹.

    public static void main(String[] args) {
        String param1 = "ABC";
        String param2 = "def";
        String param3 = "123";
        log(LevelEnum.WARN, param1 + param2 + param3);
    }
    private static void log(LevelEnum levelEnum, String str) {
        if (Objects.equals(levelEnum, LevelEnum.INFO)) {
            System.out.println(str);
        }
    }

    enum LevelEnum {
        INFO,
        WARN;
    }

上面這段代碼通過函數式接口重構下:

//定義函數式接口,@FunctionalInterface註解可省略
@FunctionalInterface
public interface MyFunctionInterface {
    String concat();
}

 

        public static void main(String[] args) {
            String param1 = "ABC";
            String param2 = "def";
            String param3 = "123";
        
            log(LevelEnum.WARN, () -> {
                System.out.println("lambda方法被執行");
                return param1 + param2 + param3;
            });
            log(LevelEnum.INFO, () -> param1 + param2 + param3);
        }
        //重寫log方法
        private static void log(LevelEnum levelEnum, MyFunctionInterface function) {
            if (Objects.equals(levelEnum, LevelEnum.INFO)) {
            System.out.println(function.concat());
            }
        }

 測試結果印證了函數式接口具有延遲執行的特點,當日志級別爲warn時,判斷條件不滿足,直接不執函數,"lambda方法被執行"這段話沒有被輸出,字符串拼接未執行.

1.2性能層面

1.2.1用MetaSpace取代PermGen

將jvm內存模型中的永久代用元空間取代,在Jdk 6及以前的版本中,類的信息是存放在PermGen中,從jdk7開始,直到jdk8,類的信息全部存放在MetaSpace中,MetaSpace是什麼東西?爲什麼要放在MetaSpace中?

MetaSpace是屬於堆外內存,類似netty的ByteBuf,該部分內存空間不受Jvm管理,沒有垃圾回收,而PermGen是屬於jvm內存,受控於jvm.

這樣做主要是爲了:

  1. 避免String字符串常量存放在PermGen容易出現性能問題和堆溢出.
  2. 永久代垃圾回收帶來的不必要的複雜度,且回收效率偏低.
  3. 類的方法信息等大小比較難界定,所以很難指定永久代的大小,太小容易使永久代溢出,太大容易使老年代溢出.

2.jdk9

2.1api層面

2.1.1 提供集合創建不可變集合的of方法

Jdk9提供了類似guava的不可變集合創建:

List.of(E...e)
Set.of(E...e)
Map.of(K k1,V v1,K k2,V v2...K kn,V vn)
Map.ofEntrys(Map.Entry<? extends K, ? extends V>... entries)

2.1.2提供私有接口方法

Jdk9中的接口可以擁有私有方法

public interface MyInterface{
    default void sayHello(){
        String str = "hello world!";
        print(str);
    }

    private void print(String str){
        System.out.println("str:" + str);
    }
}

2.1.3提供Jshell調試

Jshell是jdk9提供的一個命令行操作工具,類似於py提供的console,可以編寫一些簡單的代碼測試,不需要再創建測試類並寫main方法進行測試了.

2.2性能層面

2.2.1統一jvm日誌

在jdk9中jvm中的衆多組件使用統一的Jvm日誌,可以便於問題的排查,不像以前,各路牛神馬怪,很難定位問題.

可以使用-Xlog + 參數來指定jvm日誌的輸出位置,級別等信息.

2.2.2默認垃圾回收器爲G1GC

G1GC首次出現於JDK7,到JDK9已成爲默認的垃圾回收器,G1GC的設計是爲了取代CMS垃圾回收器的,它具有可預測的停頓模型,避免CMS垃圾回收的碎片,超大堆表現更出色等多重優勢,即便是jvm調優小白,也能通過幾個簡單的參數調優出大師級的效果,因爲它真的很智能!

2.2.3提供多版本兼容的Jar

可以在打包時將項目代碼打包成jdk9,8,7...向下兼容的jar,這樣在不同的jdk版本下都可以運行指定版本的jar包,不至於出現用Jdk9寫的代碼到jre7中就無法運行.

2.2.4Linking最小依賴

可以使用jlink創建程序運行的最小依賴jre,而不是像現在一樣,不論什麼程序,都需要一個完整的Jre才能運行,僅需要依賴程序所需模塊的jre即可,在一些小型設備上,此功能會變得比較實用.

3.jdk10

3.1api層面

3.1.1局部變量類型推斷

jdk10提供了類似js語言那種局部變量的聲明方式,這點還是值得肯定的,語法糖,用起來甜甜的!

//jdk10之前
Integer num = 1;
List<String> strList = new ArrayList<>();

//jdk10以後
var num = 1;
var strList = new ArrayList<>();

3.2性能層面

3.2.1 G1 FullGC優化

G1GC的設計目的是爲了取代CMS GC,同時也在設計初就想極力避免FullGC的發生,所以G1GC在早期的版本中並沒有考慮並行FullGC,但事實上還是會存在一些情況會導致FullGC,所以在jdk10中將G1GC的FullGC改爲並行執行,提升了FullGC的效率.

4.jdk11

4.1api層面

4.1.1lambda支持var推斷

在jdk11中,可以在lambda表達式中使用var來自動推斷類型,當然也可以省略var:

(var x, var y) -> x.process(y)
//省略var
(x, y) -> x.process(y)

這樣可以讓代碼更簡潔優雅.

4.1.2全新的httpClient

提供了類似於apache提供的HttpClient,開發者在使用HttpClient無需再引入apache依賴,同時使用起來也比較爽,Jdk的httpClient採用建造者模式,無論是創建httpClient對象還是發起指定請求,都可以通過鏈式調用來完成,代碼看着更優雅易懂.

//創建client
HttpClient client = HttpRequest.newBuilder()
                   .version(HttpClient.Version.HTTP_2)
                   .connectTimeout(Duration.ofMillis(5000))
                   .build(); 
  
//創建請求體
HttpRequest request = HttpRequest.newBuilder()
                     .header("Content-Type", "application/json")
                     .version(HttpClient.Version.HTTP_2)
                     .uri(URI.create("http://openjdk.java.net/"))
                     .POST(HttpRequest.BodyPublishers.ofString("hello"))
                     .build();

//發起請求
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());

4.2性能層面

4.2.1初次引入ZGC

ZGC是針對超大堆設計的垃圾回收器,相比G1GC,ZGC在大堆上的表現更爲出色,能夠帶來更低的GC停頓,本質上ZGC也只適合大公司玩玩,沒有超大內存和多核心CPU是玩不轉ZGC的.關於ZGC本篇不作深入介紹,後面會單獨寫一篇來介紹.

5.jdk12

5.1api層面

5.1.1switch語句優化

Jdk12對switch語句的優化可以說是非常實用了,可以讓你的代碼更優雅,在此之前每個條件之後必須強制break,讓代碼在視覺上看起來臃腫,贅餘,這也是switch語句在開發中用的不算多的原因之一,不妨先看看原來詬病的語法:


        switch (type) {
            case 1:
                System.out.println(1);
                break;
            case 2:
                System.out.println(2);
                break;
            default:
                System.out.println(3);
        }

如果case的情況比較多,這段代碼就會變得比較醜,如果用jdk12優化以後的switch來實現,就優雅得多了:

        switch(type){
            case 1 -> System.out.println(1);
            case 2 -> System.out.println(1);
            default -> System.out.println(1);
        }

 

5.2性能層面

5.2.1首次引入shennandoah GC

shennandoah GC 是 G1GC的增強版,擁有併發回收和壓縮能力的垃圾回收器,它的STW時間更短.

6.jdk13

6.1api層面

6.1.1switch可以有返回值

switch語句中可以帶返回值了,配合函數使用可以極大減少代碼中的if-else語句,提高代碼可讀性:

        int i = switch(type){
            case 1 -> 1;
            case 2 -> 2;
            default -> 3;
        }

6.1.2text blocks

當我們把一段json串,或者一段sql粘貼進編輯器後,會發現編輯器自動把一些符號給作了轉義,例如:

String sql = "<script>select * from sys_user where account_id=#{accountId} <if test=\"zoneId != null\">and zone_id=#{zoneId}</if></script>"

但在jdk13中,我們可以這麼做,直接把這段sql加入雙引號中即可,不需要做轉義:

String sql = ""<script>select * from mis_admin_role where account_id=#{accountId} <if test="zoneId != null">and zone_id=#{zoneId}</if></script>""

 

6.2性能層面

6.2.1ZGC

對jdk11提供的ZGC作了優化,會把沒用到的內存空間歸還給操作系統,避免內存空間的浪費.

 

7.jdk14

7.1API層面

7.1.1對Instance of 的增強

在jdk14前,我們在強轉之前,一般會這麼做:

        if (obj instanceof String){
            String s = (String) obj;
            //use s todo
        }

是不是會覺得很煩,一般用instanceof就是爲了強轉,然後使用,那麼何不把判斷和強轉合爲一步呢?Jdk14就是這麼做的:

        if (obj instanceof String s){
            //use s todo
        }

7.1.2友好的NPE異常提示

java煩人的nullPointException,終於有了友好的解決,除了在JDK8中已經提供的Optional,現在又多了一個核武器,在發生NPE後,我們僅能知道是第幾行代碼發生了NPE,但並不知道是這一行代碼裏的那個類,字段,導致的空指針,例如:

//a爲null
a.i = 666;

運行後可以看到控制檯Jvm打印的異常信息:

Exception in thread "main" java.lang.NullPointerException
    at Prog.main(Prog.java:5)

 根據該異常信息我們當然可以定位到第5行,然後一眼看出來錯誤信息是因爲a爲null導致的,但如果情況是這樣呢?

a.b.c.i = 666;

那abc誰來背這個空指針的鍋? Jdk14給出瞭解決方案:

Exception in thread "main" java.lang.NullPointerException: 
        Cannot read field "c" because "a.b" is null
    at Prog.main(Prog.java:5)

7.1.3switch語句功能擴展

在jdk12-13分別增強了switch語句,jdk14在此基礎上更進一步,switch語句可以用於函數入參中:

static void test(int k) {
    System.out.println(
        switch (k) {
            case  1 -> "one";
            case  2 -> "two";
            default -> "zero";
        }
    );
}

7.2性能層面

7.2.1 ZGC支持Mac操作系統

ZGC因爲實現藉助了有色指針,在64位操作系統裏,指針的前32位是用來記錄位置信息,暫時沒用的16位(32-48)被ZGC用來進行顏色標記,但在Jdk11中ZGC僅支持linux操作系統,由於目前大量開發使用Mac,需要在mac上進行調試,所以jdk在14中提供了對mac系統的兼容.

7.2.2 移除CMS GC

因爲已經有被設計用來取代CMS GC的 G1GC,而且後來還出現了ZGC及Shenandoah GC,而且各個都表現不俗,所以CMS GC也就失去了用武之地,所以被移除了.

 


以上便是我從jdk8-jdk14提供的新特性裏總結出來的一些個人覺得比較好用的點(當然還有很多性能層面的優化沒有列出來),儘管目前主流依舊是Jdk8,甚至還有一些銀行政府的同行還在用着jdk8之前的版本,但這不是阻止我們學習和使用新jdk的理由,畢竟在新版本的jdk中還是提供了很多很好用的api,也對性能做了很多優化,希望在未來能把這些新特性都用上,而不是死守jdk8.

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章