Java新特性
编码效率
jshell
简介
JShell这个特性,是在JDK 9正式发布的。JShell API和工具提供了一种在 JShell 状态下交互式评估 Java 编程语言的声明、语句和表达式的方法。JShell 的状态包括不断发展的代码和执行状态。为了便于快速调查和编码,语句和表达式不需要出现在方法中,变量和方法也不需要出现在类中。
使用jshell
jshell.exe
| 欢迎使用 JShell – 版本 17.0.10
| 要大致了解该版本, 请键入: /help intro
jshell> System.out.println(“Hello”)
Hello
jshell> int[] nums = new int[]{1, 2, 3, 4}
nums ==> int[4] { 1, 2, 3, 4 }
jshell> for (int num: nums) {System.out.println(num);}
1
2
3
4
文字块
简介
文字块这个特性,首先在JDK 13中以预览版的形式发布。在JDK 14中,改进的文字块再次以预览版的形式发布。最后,文字块在JDK 15正式发布。
文字块的概念很简单,它是一个由多行文字构成的字符串。文字块尝试消除换行、连接符、转义字符的影响,使得文字对齐和必要的占位符更加清晰,从而简化多行文字字符串的表达。
使用文字块
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 public class TextBlocks { public static void main (String[] args) { String stringBlock = "<!DOCTYPE html>\n" + "<html>\n" + " <body>\n" + " <h1>\"Hello World!\"</h1>\n" + " </body>\n" + "</html>\n" ; String textBlock = "" " <!DOCTYPE html> \s <html> <body> <h1>" Hello \ World!"</h1> \s </body> </html> " "" ; System.out.println( "Does the text block equal to the regular string? " + stringBlock.equals(textBlock)); System.out.println( "Does the text block refer to the regular string? " + (stringBlock == textBlock)); } }
保留尾部空格
文字块在编译期间,会删除共享的前导空格和尾部空格 。使用\s,可保留上一个字符到\s之间的空格
换行符
档案类
简介
档案类这个特性,首先在JDK 14中以预览版 的形式发布。在JDK 15中,改进的档案类再次以预览版 的形式发布。最后,档案类在JDK 16正式发布 。
Java档案类是用来表示不可变数据的透明载体,
不可变数据
Java档案类不支持扩展子句,用户不能定制它的父类。隐含的,它的父类是java.lang.Record。父类不能定制,也就意味着我们不能通过修改父类来影响Java档案的行为。
Java档案类是个终极(final)类,不支持子类,也不能是抽象类。没有子类,也就意味着我们不能通过修改子类来改变Java档案的行为。
Java档案类声明的变量是不可变的变量。这就是我们前面反复强调的,一旦实例化就不能再修改的关键所在。
Java档案类不能声明可变的变量,也不能支持实例初始化的方法。这就保证了,我们只能使用档案类形式的构造方法,避免额外的初始化对可变性的影响。
Java档案类不能声明本地(native)方法。如果允许了本地方法,也就意味着打开了修改不可变变量的后门。
透明的载体
档案类在内部缺省了以下方法
构造方法
equals方法
hashCode方法
toString方法
不可变数据的读取方法
档案类的模式有点像String类型行为,定义不可变数据类型。并且其比较时需要重写equals方法、hashCode方法方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public record Circle (double radius) implements Shape { @Override public double getArea () { return Math.PI * radius * radius; } } public class Main { public static void main (String[] args) { Circle circle = new Circle(10.0 ); Circle circle2 = new Circle(10.0 ); System.out.println(circle2.equals(circle)); System.out.println(circle.hashCode() + " : " + circle2.hashCode()); System.out.println(circle.radius()); } }
封闭类
简介
封闭类这个特性,首先在JDK 15中以预览版的形式发布。在JDK 16中,改进的封闭类再次以预览版的形式发布。最后,封闭类在JDK 17正式发布。
封闭类的声明使用 sealed 类修饰符,然后在所有的 extends 和 implements 语句之后,使用 permits 指定允许扩展该封闭类的子类。
封闭类控制了子类的范围,控制了子类的扩展范围。通过把可扩展性的限制放在可以预测和控制的范围内,封闭类和封闭接口打开了全开放和全封闭两个极端之间的中间地带,为接口设计和实现提供了新的可能性。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 public abstract sealed class Shape { public final String id; public Shape (String id) { this .id = id; } public abstract double area () ; public static non-sealed class Circle extends Shape { } public static sealed class Square extends Shape { } public static final class ColoredSquare extends Square { } public static class ColoredCircle extends Circle { } }
许可类的声明需要满足下面的三个条件:
许可类必须和封闭类处于同一模块(module)或者包空间(package)里,也就是说,在编译的时候,封闭类必须可以访问它的许可类;
许可类必须是封闭类的直接扩展类;
许可类必须声明是否继续保持封闭:
许可类可以声明为终极类(final),从而关闭扩展性;
许可类可以声明为封闭类(sealed),从而延续受限制的扩展性;
许可类可以声明为解封类(non-sealed), 从而支持不受限制的扩展性。
需要注意的是,**由于许可类必须是封闭类的直接扩展,因此许可类不具备传递性。**也就是说,上面的例子中,ColoredSquare 是 Square 的许可类,但不是 Shape 的许可类。
模式匹配
简介
Java的模式匹配是一个新型的、而且还在持续快速演进的领域。类型匹配是模式匹配的一个规范。类型匹配这个特性,首先在JDK 14中以预览版的形式发布。在JDK 15中,改进的类型匹配再次以预览版的形式发布。最后,类型匹配在JDK 16正式发布。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 public sealed interface Shape permits Shape .Circle , Shape .Rectangle , Shape .Square { Shape.Rectangle rect = null ; record Circle (double radius) implements Shape { } record Square (double side) implements Shape { } record Rectangle (double length, double width) implements Shape { } static void main (String[] args) { Shape shape = new Shape.Rectangle(10 , 10 ); System.out.println("It should be ture that " + shape + " is a square: " + isSquareImplE(shape)); System.out.println(); shape = new Shape.Circle(10 ); System.out.println("It cannot be ture that " + shape + " is a square: " + (!isSquareImplE(shape))); } static boolean isSquare (Shape shape) { if (shape instanceof Rectangle rect) { System.out.println( "This should be the local rect: " + rect.equals(shape)); return (rect.length == rect.width); } System.out.println( "This should be the field rect: " + (rect == null )); return (shape instanceof Square); } static boolean isSquareImplB (Shape shape) { if (!(shape instanceof Rectangle rect)) { return shape instanceof Square; } return rect.length() == rect.width(); } static boolean isSquareImplC (Shape shape) { return shape instanceof Square || (shape instanceof Rectangle rect && rect.length() == rect.width()); } static boolean isSquareImplD (Shape shape) { return shape instanceof Square || (shape instanceof Rectangle rect || rect.length() == rect.width()); } static boolean isSquareImplE (Shape shape) { return shape instanceof Square | (shape instanceof Rectangle rect & rect.length() == rect.width()); } }
switch表达式
简介
switch表达式这个特性,首先在JDK 12中以预览版的形式发布。在JDK 13中,改进的switch表达式再次以预览版的形式发布。最后,switch表达式在JDK 14正式发布。
假设现在使用switch语句获取月份的天数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 class DaysInMonth { public static void main (String[] args) { Calendar today = Calendar.getInstance(); int month = today.get(Calendar.MONTH); int year = today.get(Calendar.YEAR); int daysInMonth; switch (month) { case Calendar.JANUARY: case Calendar.MARCH: case Calendar.MAY: case Calendar.JULY: case Calendar.AUGUST: case Calendar.OCTOBER: case Calendar.DECEMBER: daysInMonth = 31 ; break ; case Calendar.APRIL: case Calendar.JUNE: case Calendar.SEPTEMBER: case Calendar.NOVEMBER: daysInMonth = 30 ; break ; case Calendar.FEBRUARY: if (((year % 4 == 0 ) && !(year % 100 == 0 )) || (year % 400 == 0 )) { daysInMonth = 29 ; } else { daysInMonth = 28 ; } break ; default : throw new RuntimeException( "Calendar in JDK does not work" ); } System.out.println( "There are " + daysInMonth + " days in this month." ); } }
使用switch表达式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 public class DaysInMonth { public static void main (String[] args) { Calendar instance = Calendar.getInstance(); int month = instance.get(Calendar.MONTH); int year = instance.get(Calendar.YEAR); int daysInMonth = switch (month) { case Calendar.JANUARY, Calendar.MARCH, Calendar.MAY, Calendar.JULY, Calendar.AUGUST, Calendar.OCTOBER, Calendar.DECEMBER -> 31 ; case Calendar.APRIL, Calendar.JUNE, Calendar.SEPTEMBER, Calendar.NOVEMBER -> 30 ; case Calendar.FEBRUARY -> { if (((year % 4 == 0 ) && !(year % 100 == 0 )) || (year % 400 == 0 )) { yield 29 ; } yield 28 ; } default -> throw new RuntimeException("Calendar in JDK does not work" ); }; System.out.println(daysInMonth); }
总结
break语句只能出现在switch语句里,不能出现在switch表达式里;
yield语句只能出现在switch表达式里,不能出现在switch语句里;
switch表达式需要穷举出所有的情景,而switch语句不需要情景穷举;
使用冒号标识符的swtich形式,支持情景间的fall-through;而使用箭头标识符的swtich形式不支持fall-through;
使用箭头标识符的swtich形式,一个case语句支持多个情景;而使用冒号标识符的swtich形式不支持多情景的case语句。
switch匹配
简介
switch的模式匹配这个特性,在JDK 17中以预览版的形式发布。
1 2 3 4 5 6 public static boolean isSquare (Shape shape) { return switch (shape) { case null , Shape.Circle c -> false ; case Shape.Square s -> true ; }; }
代码性能
错误码
异常状况的处理会让代码的效率变低,所以我们不应该使用异常机制来处理正常的状况 。
同一段代码,一个不抛出异常,一个抛出异常。它们之间的执行效率可能差距千倍
以下使用错误码的案例,举例了使用档案类和记录类,使用switch匹配和模式匹配的方式,避免异常抛出的案例。以提高代码执行效率
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 public sealed abstract class Digest { private static final class SHA256 extends Digest { @Override byte [] digest(byte [] message) { return "SHA-256" .getBytes(StandardCharsets.UTF_8); } } private static final class SHA512 extends Digest { @Override byte [] digest(byte [] message) { return "SHA512" .getBytes(StandardCharsets.UTF_8); } } public static Returned<Digest> of (String algorithm) { return switch (algorithm) { case "SHA-256" -> new Returned.ReturnValue<>(new SHA256()); case "SHA-512" -> new Returned.ReturnValue<>(new SHA512()); default -> new Returned.ErrorCode(-1 ); }; } abstract byte [] digest(byte [] message); } public sealed interface Returned <T > permits Returned .ReturnValue , Returned .ErrorCode { record ReturnValue<T>(T returnValue) implements Returned<T> { } record ErrorCode (int errorCode) implements Returned { } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 public class UseCase { public static void main (String[] args) { Returned<Digest> rt = Digest.of("SHA-256" ); if (rt instanceof Returned.ReturnValue<Digest> rv) { Digest d = rv.returnValue(); System.out.println(new String(d.digest("Hello, world!" .getBytes()))); } else if (rt instanceof Returned.ErrorCode ec) { System.out.println("Failed to get instance of SHA-256" ); } } } public class UseCase { public static void main (String[] args) { Returned<Digest> rt = Digest.of("SHA-256" ); switch (rt) { case Returned.ReturnValue rv -> { Digest d = (Digest) rv.returnValue(); d.digest("Hello, world!" .getBytes()); } case Returned.ErrorCode ec -> System.out.println("Failed to get instance of SHA-256" ); } } }
当然使用错误码并不是一个完美的解决方案,错误码不像异常那样拥有调用栈信息,不利于问题定位
外部内存
Java的外部内存接口这个新特性,现在还在孵化期。。像TensorFlow、 Ignite、 Flink以及Netty这样的类库,往往对性能有着偏执的追求。为了避免Java垃圾收集器不可预测的行为以及额外的性能开销,这些产品一般倾向于使用JVM之外的内存来存储和管理数据。这样的数据,就是我们常说的堆外数据(off-heap data)。
ByteBuffer
ByteBuffer是异步编程和非阻塞编程的核心类,几乎所有的Java异步模式或者非阻塞模式的代码,都要直接或者间接地使用ByteBuffer来管理数据。但是ByteBuffer有两个明显的缺陷
没有资源释放的接口。一旦一个ByteBuffer实例化,它占用了内存的释放,就会完全依赖JVM的垃圾回收机制。依赖于垃圾回收机制的资源回收方式,并不能满足像Netty这样的类库的理想需求。
存储空间尺寸的限制。ByteBuffer的存储空间的大小,是使用Java的整数来表示的。所以,它的存储空间,最多只有2G。
外部内存接口沿袭了ByteBuffer的设计思路,但是使用了全新的接口布局。
1 2 3 4 5 6 try (ResourceScope scope = ResourceScope.newConfinedScope()) { MemorySegment segment = MemorySegment.allocateNative(4 , scope); for (int i = 0 ; i < 4 ; i++) { MemoryAccess.setByteAtOffset(segment, i, (byte )'A' ); } }
外部函数
Java的外部函数接口这个新特性,代替了原有的JNI(Java Native Interface)。并在JNI的基础上完善了动态库无法跨平台、JNI脱离JVM语言安全机制的问题
JNI实现
像Java或者Go这样的通用编程语言,都需要和其他的编程语言或者环境打交道,比如操作系统或者C语言。Java是通过Java本地接口(Java Native Interface, JNI)来支持这样的做法的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 public class HelloWorld { static { System.loadLibrary("helloWorld" ); } public static void main (String[] args) { new HelloWorld().sayHello(); } private native void sayHello () ; } #include "jni.h" #include "HelloWorld.h" #include <stdio.h> JNIEXPORT void JNICALL Java_HelloWorld_sayHello (JNIEnv *env, jobject jObj) { printf("Hello World!\n" ); } $ gcc -I$(JAVA_HOME)/include -I$(JAVA_HOME)/include/darwin \ -dynamiclib HelloWorld.c -o libhelloWorld.dylib java -cp . -Djava.library.path=. HelloWorld
外部函数实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 import java.lang.invoke.MethodType;import jdk.incubator.foreign.*;public class HelloWorld { public static void main (String[] args) throws Throwable { try (ResourceScope scope = ResourceScope.newConfinedScope()) { CLinker cLinker = CLinker.getInstance(); MemorySegment helloWorld = CLinker.toCString("Hello, world!\n" , scope); MethodHandle cPrintf = cLinker.downcallHandle( CLinker.systemLookup().lookup("printf" ).get(), MethodType.methodType(int .class, MemoryAddress.class), FunctionDescriptor.of(CLinker.C_INT, CLinker.C_POINTER)); cPrintf.invoke(helloWorld.address()); } } }
禁止空指针
在C语言和Java语言里,存在着大量的空指针。不管我们怎么努力,也不管我们经验多么丰富,总是会时不时地就忘了检查空指针。而忘了检查这样的小错误,很可能就蔓延成严重的事故。所以,空指针发明者称它是一个价值10亿美元的错误。
在jdk17中对NullPointerException做了优化,在msg中包含调用者信息,减少了问题排查难度
1 2 3 4 5 6 7 jshell> String a = null a ==> null | 已创建 变量 a : String jshell> a.equals("b" ) | 异常错误 java.lang.NullPointerException:Cannot invoke "String.equals(Object)" because "REPL.$JShell$13.a" is null | at (#4 :1 )
Optional
在jdk8中可以使用Optional.isPresent()方法判断null,但是isPresent不是一个强制检查的方法,开发还是可以直接使用.get()方法获取值
1 2 3 4 5 6 7 8 private static boolean hasMiddleName ( FullName fullName, String middleName) { if (fullName.middleName().isPresent()) { return fullName.middleName().get().equals(middleName); } return middleName == null ; }
封闭类
这种使用了封闭类和模式匹配的设计,极大地压缩了开发者的自由度,强制要求开发者的代码必须执行空指针的检查,只有这样才能编写下一步的代码。 这种看似放弃了灵活性的设计,恰恰把开发者从低级易犯的错误中解救了出来。不论是对写代码的开发者,还是对读代码的开发者来说,这都是一件好事。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public sealed interface Returned <T > { Returned.Undefined UNDEFINED = new Undefined(); record ReturnValue<T>(T returnValue) implements Returned { } record Undefined () implements Returned { } } private static boolean hasMiddleName (FullName fullName, String middleName) { return switch (fullName.middleName()) { case Returned.Undefined undefined -> false ; case Returned.ReturnValue rv -> { String returnedMiddleName = (String)rv.returnValue(); yield returnedMiddleName.equals(middleName); } }; }
维护维度
模块化
Java 平台模块系统(Java Platform Module System,JPMS),模块化可以帮助各级开发人员在构建、维护和演进软件系统时提高工作效率。软件系统规模越大,我们越需要这样的工程技术。
缺失的访问控制
从这个列表看,Java 语言访问修饰符似乎覆盖了所有的可能性,这好像是一个完备的定义。遗憾的是,Java 语言访问修饰符遗漏了很重要的一种情况,那就是 Java 包之间的关系。Java 包之间的关系,并不是要么全开放,要么全封闭这么简单。
类似于继承类之间的私密通道,Java 包之间也有这种类似私密通道的需求。比如说,我们在 JDK 的标准类库里,可以看到像 java.net 这样的放置公开接口的包,也可以看到像 sun.net 这样的放置实现代码的包。
模块化
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 module jus.crypto { exports co.ivi.jus.crypto; uses co.ivi.jus.crypto.DigestManager; } module jus.crypto.impl { requires jus.crypto; provides co.ivi.jus.crypto.DigestManager with co.ivi.jus.impl.DigestManagerImpl; } module jus.crypto.use { requires jus.crypto; }
https://www.cnblogs.com/IcanFixIt/p/6947763.html