前言: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.