前言: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表達式
先說流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.
這樣做主要是爲了:
- 避免String字符串常量存放在PermGen容易出現性能問題和堆溢出.
- 永久代垃圾回收帶來的不必要的複雜度,且回收效率偏低.
- 類的方法信息等大小比較難界定,所以很難指定永久代的大小,太小容易使永久代溢出,太大容易使老年代溢出.
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.