|
|
|
|
公众号矩阵

跟妹妹聊到 Java 16 新特征,真香!

2021年3月16日,甲骨文正式发布了Java 16!想当年JDK1.6新出的场景和历历在目,一瞬间,版本已经变成了16,真正体会了一把什么叫做光阴似箭,沧海桑田。虽然目前大部分的场合,Java8还占着主导地位,但我猜想各位Javaer应该对Java16的新特性也大有兴趣吧!

作者:三太子敖丙来源:三太子敖丙|2021-03-19 08:54

Java 16新特性


2021年3月16日,甲骨文正式发布了Java 16!想当年JDK1.6新出的场景和历历在目,一瞬间,版本已经变成了16,真正体会了一把什么叫做光阴似箭,沧海桑田。虽然目前大部分的场合,Java8还占着主导地位,但我猜想各位Javaer应该对Java16的新特性也大有兴趣吧!

看完之后我觉得这次更新还是很有意思的,我就精选几个Java16的新特性,供大家一饱眼福!(大家可以自己建个项目用起来试试)

支持模式匹配的instanceof

想想你是怎么用instanceof的吧,一个例子:

  1. if (obj instanceof String) { 
  2.     String s = (String) obj;    // grr... 
  3.     ... 

这代码是不是让人蛋疼,我都知道是个String了,还让我强转一下,该改进一下啦~

  1. if (obj instanceof String s) { 
  2.     //这里s随你用了 从类型判断,到变量定义,类型转换,一气呵成,爽不爽? 

进一步的,还可以这么用,模式变量s在判断条件里直接使用

  1. if (obj instanceof String s && s.length() > 5) { 
  2.     flag = s.contains("jdk"); 

不过要小心,下面的用法是错误的(原因就不多解释啦):

  1. if (obj instanceof String s || s.length() > 5) {    // Error! 
  2.     ... 

Records类型

我们对Java最大的意见是啥?当然是太繁琐了,一个简单的功能,繁重的语法要整出好几十行,不急,改进这就来了,看看新的Recodes类型吧!

假设你现在有这么一个类:


这是一个典型的不可变的数据对象,equals ()方法, hashCode()方法,toString()方法其实都是比较通用的。但是我们不得不为它多写那么几行代码。虽然有IDE的鼎力协助,但是看上去还是不怎么爽(如果没有IDE,更要哭了)。不过没事Records来了!用Records来表示上面的类,你只需要:

  1. record Point(int x, int y) { } 

是不是特别简单,感觉心里特别爽?record类和普通的class不太一样,它会帮你隐式生成一些字段和构造函数。比如上面的record就会编译成这样:

  1. record Point(int x, int y) { 
  2.     // 隐式生成字段 
  3.     private final int x; 
  4.     private final int y; 
  5.     // 隐式生成构造函数,并带上所有的参数 
  6.     Point(int x, int y) { 
  7.         this.x = x; 
  8.         this.y = y; 
  9.     } 

ZGC并发线程处理

ZGC是The Z Garbage Collector,是JDK 11中推出的一款低延迟垃圾回收器,它尝试解决GC之痛,也就是停顿。

同时,它也将面向以TB为单位的超大规模的内存。在Java 16中,ZGC的线程栈处理的众多操作,从检查点移动到了并发阶段,这样意味着停顿更小了。(后面准备出个zgc相关的文章,顺便测一波这次的停顿优化时长)

弹性元空间

在Java虚拟机中,元空间用来保存一些类的元信息,并且,元空间中的数据是可以被垃圾回收的。但不幸的是,空闲的未被使用的元空间并不会归还给操作系统,这就导致了内存浪费。

这个新特性就是为了解决这个问题,它使得虚拟机可以从元空间中归还未使用的内存,从而更加有效得利用物理内存。同时有一个新的虚拟机参数可以用来控制这种回收的执行强度:-XX:MetaspaceReclaimPolicy=(balanced|aggressive|none)

Unix Domain套接字

Unix Domain套接字本身提供给了一套兼容于网络编程,但是更加可靠、高效、安全的进程间通信方式,它不需要经过网络协议栈,不需要打包拆包、计算校验和、维护序号和应答等,只是将应用层数据从一个进程拷贝到另一个进程。

在Java 16中,已经可以直接使用这种套接字(Unix-domain (AF_UNIX),虽然叫做UNIX套接字,windows 10和Windows Server 2019也是可以使用的)了。为了支持Unix Domain套接字,新增了专门的java.net.UnixDomainSocketAddress类,下面看一下它的使用:

 

新的打包工具

提供了一个新的打包工具jpackage,用来打包独立的Java应用程序。这个工具可以生成windows上的exe和msi,MacOS上的pkg和dmg,以及linux上的deb和rpm。它很好的给用户提供了一键式安装Java程序的好方法。

比如,对于非模块化的应用,可以这么打包:

  1. jpackage --name myapp --input lib --main-jar main.jar 
  2. 或者 直接指定main class 
  3. jpackage --name myapp --input lib --main-jar main.jar --main-class myapp.Main 

对于模块化的应用:

  1. jpackage --name myapp --module-path lib -m myapp 
  2. 或者直接指定main class 
  3. jpackage --name myapp --module-path lib -m myapp/myapp.Main 

值对象错误使用的警告

值对象,比如java.lang.Integer, java.lang.Double之类的不变对象,在废弃构造函数的基础上,进一步标记为forRemoval(不要再使用它们的构造函数了哦!)。

同时,如果在值对象上进行同步,将会被警告,比如:

  1. Double d = 20.0; 
  2. synchronized (d) { ... } // javac和hotsopt都会警告 
  3. Object o = d; 
  4. synchronized (o) { ... } // HotSpot 会警告 

在虚拟机层面,还提供了参数可以在虚拟机层面控制报错行为:

  • -XX:DiagnoseSyncOnValueBasedClasses=1 将这种同步行为视为致命错误
  • -XX:DiagnoseSyncOnValueBasedClasses=2 打开日志,在控制台和飞行记录仪中记录这种同步行为

默认限制使用JDK内部API

对于一些JDK内部的API,作出了更严格的限制。比如 com.sun.*, jdk.*, and org.*这些包里的API,从Java 16开始,默认已经禁止使用了。因此,鼓励大家使用标准API,而不是内部API(点击这里查看可以替换的内部API)。比如说下面这句代码,在Java 16中将报错:

  1. System.out.println(sun.security.util.SecurityConstants.ALL_PERMISSION); 

错误示例:

  1. Exception in thread "main" java.lang.IllegalAccessError: class Test 
  2.   (in unnamed module @0x5e481248) cannot access class 
  3.   sun.security.util.SecurityConstants (in module java.base) because 
  4.   module java.base does not export sun.security.util to unnamed 
  5.   module @0x5e481248 

同时,JDK还提供--illegal-access参数用来控制对内部API的使用:

  • --illegal-access=permit 允许使用内部API
  • --illegal-access=warn 允许使用内部API,不过每次使用会得到一个警告
  • --illegal-access=debug 允许使用内部API,会更详细的打印每一个错误的调用堆栈,用它你就可以找到你在哪里有不正确的调用,就可以修复那些不合适的使用
  • --illegal-access=deny 禁止使用内部API

孵化项目:向量API

我们知道,像Go这样的后起之秀,已经在内部使用了AVX指令,性能飙升。Java在这方面也不甘示弱,在Java 16中,向量API作为一个孵化项目,允许我们直接使用SIMD指令来提高性能(如果有效使用,这波就带你起飞了)。

让我们先一睹为快吧!

下面是一个简单的标量计算:

  1. void scalarComputation(float[] a, float[] b, float[] c) { 
  2.    for (int i = 0; i < a.length; i++) { 
  3.         c[i] = (a[i] * a[i] + b[i] * b[i]) * -1.0f; 
  4.    } 

重点来了,使用AVX2带你起飞:

  1. //256位的向量浮点运算 
  2. static final VectorSpecies<Float> SPECIES = FloatVector.SPECIES_256; 
  3.  
  4. void vectorComputation(float[] a, float[] b, float[] c) { 
  5.     int i = 0; 
  6.     int upperBound = SPECIES.loopBound(a.length); 
  7.     for (; i < upperBound; i += SPECIES.length()) { 
  8.         // FloatVector va, vb, vc; 
  9.         var va = FloatVector.fromArray(SPECIES, a, i); 
  10.         var vb = FloatVector.fromArray(SPECIES, b, i); 
  11.         var vc = va.mul(va). 
  12.                     add(vb.mul(vb)). 
  13.                     neg(); 
  14.         vc.intoArray(c, i); 
  15.     } 
  16.  
  17.     for (; i < a.length; i++) { 
  18.         c[i] = (a[i] * a[i] + b[i] * b[i]) * -1.0f; 
  19.     } 

孵化项目:外部链接API

你是不是有抱怨过JNI太难用了?没关系,JNI的进化版来了,这就是外部链接器!它提供了一个静态的,纯Java的访问本地native 代码的方法,它将极大简化我们调用本地代码的过程。

新的API:

  • LibraryLookup::ofDefault:返回被JVM加载的库,可以看到所有这些库的符号
  • LibraryLookup::ofPath:加载指定路径
  • LibraryLookup::ofLibrary :根据库名,加载库

下面代码展示了,使用Java调用clang库中的clang_getClangVersion()方法:

  1. LibraryLookup libclang = LibraryLookup.ofLibrary("clang"); 
  2. LibraryLookup.Symbol clangVersion = libclang.lookup("clang_getClangVersion"); 

另外一个重要的类是C链接器:

  1. interface CLinker { 
  2.     MethodHandle downcallHandle(LibraryLookup.Symbol func, 
  3.                                 MethodType type, 
  4.                                 FunctionDescriptor function); 
  5.     MemorySegment upcallStub(MethodHandle target, 
  6.                              FunctionDescriptor function); 

downcallHandle()表示在Java中调用本地方法;upcallStub()方法在native方法中调用java代码。

下面的代码展示了如何在Java代码中,调用C函数size_t strlen(const char *s):

  1. MethodHandle strlen = CLinker.getInstance().downcallHandle( 
  2.         LibraryLookup.ofDefault().lookup("strlen"), 
  3.         MethodType.methodType(long.class, MemoryAddress.class), 
  4.         FunctionDescriptor.of(C_LONG, C_POINTER) 
  5.     ); 

上面代码首先找到strlen符号;然后描述它的签名。最后使用downcallHandle()得到表示strlen()函数的MethodHandle对象;最后,就可以调用strlen()方法啦~

  1. try (MemorySegment str = CLinker.toCString("Hello")) { 
  2.    long len = strlen.invokeExact(str.address()); // 5 

反过来,也可以把Java函数作为参数传递给C函数进行回调:

比如有一个C函数:

  1. void qsort(void *base, size_t nmemb, size_t size
  2.            int (*compar)(const void *, const void *)); 

要调用这个C函数,必须准备一个函数指针compar,为了使用它,我们首先要得到它的MethodHandle:

  1. MethodHandle qsort = CLinker.getInstance().downcallHandle( 
  2.         LibraryLookup.ofDefault().lookup("qsort"), 
  3.         MethodType.methodType(void.class, MemoryAddress.class, long.class, 
  4.                               long.class, MemoryAddress.class), 
  5.         FunctionDescriptor.ofVoid(C_POINTER, C_LONG, C_LONG, C_POINTER) 
  6.     ); 

接着,用一个纯Java类来实现compar函数:

  1. class Qsort { 
  2.     static int qsortCompare(MemoryAddress addr1, MemoryAddress addr2) { 
  3.             return MemoryAccess.getIntAtOffset(MemorySegment.ofNativeRestricted(),  
  4.                                                addr1.toRawLongValue()) -  
  5.                    MemoryAccess.getIntAtOffset(MemorySegment.ofNativeRestricted(), 
  6.                                                addr2.toRawLongValue()); 
  7.     } 

然后需要一个MethodHandle指向上述函数:

  1. MethodHandle comparHandle 
  2.     = MethodHandles.lookup() 
  3.                    .findStatic(Qsort.class, "qsortCompare"
  4.                                MethodType.methodType(int.class, 
  5.                                                      MemoryAddress.class, 
  6.                                                      MemoryAddress.class)); 

然后,使用upcallStub()方法,得到一个函数指针(应该说是用Java描述的C函数指针):

  1. MemorySegment comparFunc 
  2.     = CLinker.getInstance().upcallStub(comparHandle, 
  3.                                             FunctionDescriptor.of(C_INT, 
  4.                                                                   C_POINTER, 
  5.                                                                   C_POINTER)); 
  6. ); 

最后,把这个"C函数指针"传给C函数:

  1. try (MemorySegment array = MemorySegment.allocateNative(4 * 10)) { 
  2.     array.copyFrom(MemorySegment.ofArray(new int[] { 0, 9, 3, 4, 6, 5, 1, 8, 2, 7 })); 
  3.     qsort.invokeExact(array.address(), 10L, 4L, comparFunc.address()); 
  4.     int[] sorted = array.toIntArray(); // [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 ] 

好了,这就是使用外部链接API来使用本地代码的全过程,是不是很酷炫呢?

预览功能:密封类

类的继承是面向对象的一个重要特性,但是滥用继承对对象模型的建模也是非常不利的。对于这一点,Java还有较大的改进空间,密封类,正式对对象继承的一种重大改进。

首先,来看JDK内部的一个例子:

  1. package java.lang; 
  2.  
  3. abstract class AbstractStringBuilder { ... } 
  4. public final class StringBuffer  extends AbstractStringBuilder { ... } 
  5. public final class StringBuilder extends AbstractStringBuilder { ... } 

AbstractStringBuilder有两个子类StringBuffer和StringBuilder。但是,我们的代码却无法继承AbstractStringBuilder,因为AbstractStringBuilder是包内可见的,并不是public的。在很多场合,我们的对象模式其实并不希望彻底公开,我们有时候仅仅希望只有一些指定的类可以继承,而不是可以任由继承扩展。这就是密封类的设计初衷。

密封类/接口在声明的时候,就可以指定哪些类可以从这里继承,比如:

  1. package com.example.geometry; 
  2.  
  3. public abstract sealed class Shape 
  4.     permits Circle, Rectangle, Square { ... } 

注意新关键字sealed,它表示被修饰的类或者接口是密封的,紧接着使用permits关键字,指定哪些子类可以继承它,这里只有Circle,Rectangle和Square可以从Shape继承(继承类和密封类必须要在同一个模块,如果在unamed模块,就需要在同一个package)。

使用密封类,还有一些限制,比如:

1.子类必须是直接继承,而不是间接的

2.子类必须说明如果处理得到的密封属性,三选一,必选一个:

  • 子类标记为final,一了百了
  • 子类也作为sealed类,并做有限的继承扩展
  • 子类申明为non-sealed,公开使用(这种情况,密封类的初衷就被打破了,继承关系就不可控了)

下面的代码说明了这3种情况:

  1. package com.example.geometry; 
  2. //使用final 
  3. public final class Circle extends Shape { ... } 
  4. //使用sealed 
  5. public sealed class Rectangle extends Shape  
  6.     permits TransparentRectangle, FilledRectangle { ... } 
  7. public final class TransparentRectangle extends Rectangle { ... } 
  8. public final class FilledRectangle extends Rectangle { ... } 
  9. //使用non-sealed 
  10. public non-sealed class Square extends Shape { ... } 

写在最后的话

看了那么多Java 16的新功能,心里是不是有点小激动?不要犹豫了,下一个试试?

阿丙友情提示:不要在自己的项目中尝试最新版本,等稳定后再尝试,可以在自己的demo和自己项目上尝试。

我是敖丙,你知道的越多,不知道的越多,我们下期见。

【编辑推荐】

  1. 5分钟让你理解K8S必备架构概念,以及网络模型
  2. 92年百度程序员被抓,给我们警示什么?
  3. 开源云盘利器:Nextcloud 21私有云盘搭建
  4. 更纯净,微软 Windows10 21H2 重大更新将减少系统臃肿软件数量
  5. 996工作制究竟是好是坏?
【责任编辑:姜华 TEL:(010)68476606】

点赞 0
分享:
大家都在看
猜你喜欢

订阅专栏+更多

数据湖与数据仓库的分析实践攻略

数据湖与数据仓库的分析实践攻略

助力现代化数据管理:数据湖与数据仓库的分析实践攻略
共3章 | 创世达人

7人订阅学习

云原生架构实践

云原生架构实践

新技术引领移动互联网进入急速赛道
共3章 | KaliArch

36人订阅学习

数据中心和VPDN网络建设案例

数据中心和VPDN网络建设案例

漫画+案例
共20章 | 捷哥CCIE

228人订阅学习

订阅51CTO邮刊

点击这里查看样刊

订阅51CTO邮刊

51CTO服务号

51CTO官微