前言: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
先说流stream,在传统的方式中,如果我们需要对集合进行n个中间操作再获取结果,往往需要多次遍历,非常低效,有了stream之后,通常仅需要一次遍历即可完成各种操作,而且它支持多线程操作,不再像for循环只能串行执行。另外stream提供了多种实用的方法,比如map,filter,sort,reverse,distinct...可以提高开发效率以及改善代码优雅,另外使用parallelStream还可以在一场景下提高处理效率。
//对集合中的元素去重+1并按从小到大排序
List list = Arrays.asList(1,3,2,4,3);
List collect = list.stream()
.distinct()
.map(i->i+1)
.sorted()
.collect(Collectors.toList());
上面stream的写法就是lambda表达式的一种,lambda表达式可以在很多地方使用,常见的比如写匿名内部类,循环等:
//匿名内部类
Comparator comparator = new Comparator() {
@Override
public int compare(Integer o1, Integer o2) {
if (o1 > o2) {
return 1;
}
return 0;
}
};
//lambda
Comparator comparator1 = (o1, o2) -> {
if (o1 > o2) {
return 1;
}
return 0;
};
同样是比较器,lambda表达式写的代码看起来会更加简洁清爽一些,写lambda表达式记住一点就好了,就是只要是能根据语境推断出下文的,都可以简写为lambda表达式的语法,比如上面排序比较器里compare方法,Integer是可以根据泛型推断出来的,所以可以简写为o1,o2不用再次指明类型,然后compare方法是固定的,所以可以省略方法名及返回类型简写为括号(),关于Lambda写法的原则就是能多简单就多简单,简化到不能再简写为止,好在较新版本的IDEA里会有提示,如果你有强迫症它会促使你写到最简,即便是不会写Lambda表达式,编辑器也会把最优的写法展示给你,你可以选择是否替换。
complatableFuture默认采用性能更好的ForkJoinPool线程池,而且相比jdk1.5提供的Future类,它具有类似Netty中的future Listener效果,通过future.get()获取不再阻塞,而是真正意义上的异步,回调,而且也会让你的代码看起来更优雅:
例如我想对一段字符串进行转大写,然后拼接(假设转的过程和拼接都比较耗时),如果采用jdk5提供的future.get()来操作,我需要阻塞等到字符串转为大写后才能进行拼接,而jdk8提供的completableFuture则不需要这么做,转换和拼接可以异步完成,最终完成后可以在whenComplete中执行后续动作,如果中间耗时过程和步骤比较多,completableFuture的优势就更能体现了。
CompletableFuture 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);
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.
这样做主要是为了:
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)
Jdk9中的接口可以拥有私有方法
public interface MyInterface{
default void sayHello(){
String str = "hello world!";
print(str);
}
private void print(String str){
System.out.println("str:" + str);
}
}
Jshell是jdk9提供的一个命令行操作工具,类似于py提供的console,可以编写一些简单的代码测试,不需要再创建测试类并写main方法进行测试了.
在jdk9中jvm中的众多组件使用统一的Jvm日志,可以便于问题的排查,不像以前,各路牛神马怪,很难定位问题.
可以使用-Xlog + 参数来指定jvm日志的输出位置,级别等信息.
G1GC首次出现于JDK7,到JDK9已成为默认的垃圾回收器,G1GC的设计是为了取代CMS垃圾回收器的,它具有可预测的停顿模型,避免CMS垃圾回收的碎片,超大堆表现更出色等多重优势,即便是jvm调优小白,也能通过几个简单的参数调优出大师级的效果,因为它真的很智能!
可以在打包时将项目代码打包成jdk9,8,7...向下兼容的jar,这样在不同的jdk版本下都可以运行指定版本的jar包,不至于出现用Jdk9写的代码到jre7中就无法运行.
可以使用jlink创建程序运行的最小依赖jre,而不是像现在一样,不论什么程序,都需要一个完整的Jre才能运行,仅需要依赖程序所需模块的jre即可,在一些小型设备上,此功能会变得比较实用.
jdk10提供了类似js语言那种局部变量的声明方式,这点还是值得肯定的,语法糖,用起来甜甜的!
//jdk10之前
Integer num = 1;
List strList = new ArrayList<>();
//jdk10以后
var num = 1;
var strList = new ArrayList<>();
G1GC的设计目的是为了取代CMS GC,同时也在设计初就想极力避免FullGC的发生,所以G1GC在早期的版本中并没有考虑并行FullGC,但事实上还是会存在一些情况会导致FullGC,所以在jdk10中将G1GC的FullGC改为并行执行,提升了FullGC的效率.
在jdk11中,可以在lambda表达式中使用var来自动推断类型,当然也可以省略var:
(var x, var y) -> x.process(y)
//省略var
(x, y) -> x.process(y)
这样可以让代码更简洁优雅.
提供了类似于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 response = client.send(request, HttpResponse.BodyHandlers.ofString());
ZGC是针对超大堆设计的垃圾回收器,相比G1GC,ZGC在大堆上的表现更为出色,能够带来更低的GC停顿,本质上ZGC也只适合大公司玩玩,没有超大内存和多核心CPU是玩不转ZGC的.关于ZGC本篇不作深入介绍,后面会单独写一篇来介绍.
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);
}
shennandoah GC 是 G1GC的增强版,拥有并发回收和压缩能力的垃圾回收器,它的STW时间更短.
switch语句中可以带返回值了,配合函数使用可以极大减少代码中的if-else语句,提高代码可读性:
int i = switch(type){
case 1 -> 1;
case 2 -> 2;
default -> 3;
}
当我们把一段json串,或者一段sql粘贴进编辑器后,会发现编辑器自动把一些符号给作了转义,例如:
String sql = ""
但在jdk13中,我们可以这么做,直接把这段sql加入双引号中即可,不需要做转义:
String sql = """"
对jdk11提供的ZGC作了优化,会把没用到的内存空间归还给操作系统,避免内存空间的浪费.
在jdk14前,我们在强转之前,一般会这么做:
if (obj instanceof String){
String s = (String) obj;
//use s todo
}
是不是会觉得很烦,一般用instanceof就是为了强转,然后使用,那么何不把判断和强转合为一步呢?Jdk14就是这么做的:
if (obj instanceof String s){
//use s todo
}
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)
在jdk12-13分别增强了switch语句,jdk14在此基础上更进一步,switch语句可以用于函数入参中:
static void test(int k) {
System.out.println(
switch (k) {
case 1 -> "one";
case 2 -> "two";
default -> "zero";
}
);
}
ZGC因为实现借助了有色指针,在64位操作系统里,指针的前32位是用来记录位置信息,暂时没用的16位(32-48)被ZGC用来进行颜色标记,但在Jdk11中ZGC仅支持linux操作系统,由于目前大量开发使用Mac,需要在mac上进行调试,所以jdk在14中提供了对mac系统的兼容.
因为已经有被设计用来取代CMS GC的 G1GC,而且后来还出现了ZGC及Shenandoah GC,而且各个都表现不俗,所以CMS GC也就失去了用武之地,所以被移除了.
以上便是我从jdk8-jdk14提供的新特性里总结出来的一些个人觉得比较好用的点(当然还有很多性能层面的优化没有列出来),尽管目前主流依旧是Jdk8,甚至还有一些银行政府的同行还在用着jdk8之前的版本,但这不是阻止我们学习和使用新jdk的理由,毕竟在新版本的jdk中还是提供了很多很好用的api,也对性能做了很多优化,希望在未来能把这些新特性都用上,而不是死守jdk8.