为什么是JDK17
“你发任你发,我用 Java8”。虽然业界现在对于 JDK8 后每半年更新一次的 JDK 新版本都保持着比较谨慎的态度,但是 JDK17是一个 Java 程序员不得不去关注的新版本。 最直观的原因是,作为现代 Java 应用基石的 Spring 和 SpringBoot ,都在新版本中走出了抛弃 JDK8,支持 JDK17 的这一步。
你或许不一定需要像技术极客一样去紧追 JDK 各种令人眼花缭乱的最新特性,但是 JDK17 却是每个 Java 程序员必须走出的下一个里程碑。不光是因为 JDK17 是 JDK8 后一个重要的 LTS 长期支持版本,更是因为在应用生态构建方面,JDK17 会比之前的 JDK11 更加成熟。
个人觉得,跳过 JDK11,直接入手 JDK17,对 JDK8 时代的程序员来说是一个比较实惠的选择。而至于后续的 JDK21 版本,除了虚拟线程比较亮眼外,其他特性相比 JDK17,感觉不痛不痒。因此,接下来,楼兰将在 JDK8 的基础上,带你全面认识一下 JDK17。
语法层面新特性
先从一些无关痛痒的小的语法增强带你来走进 JDK17。
1、文本块
文本块功能,文本块指多行的字符串,使用连续的三个双引号来包围一段带换行的文字,它避免了换行转义的需要,并支持String.format。
-
同时添加了两个新的转义字符:
- , 置于行尾,用来将两行连接为一行
\s: 单个空白字符
示例代码:
String query =
"""
SELECT `EMP_ID`, `LAST_NAME` FROM `EMPLOYEE_TB` \s
WHERE `CITY` = '%s' \
ORDER BY `EMP_ID`, `LAST_NAME`;
""";
System.out.println("===== query start =====");
System.out.println(String.format(query, "合肥"));
System.out.println("===== query stop =====");
打印结果:
SELECT `EMP_ID`, `LAST_NAME` FROM `EMPLOYEE_TB`
WHERE `CITY` = '合肥' ORDER BY `EMP_ID`, `LAST_NAME`;
2 、Switch 表达式增强
从 JDK8 到 JDK17,Switch 表达式做了很大的增强,再也不是简单的if-else的替代品了。
扩展switch语句,使其既可以作为语句使用,也可以作为表达式使用,并且两种形式都可以用“传统”或“简化”的作用域和控制流行为。同时添加了yield关键字,提供break 与switch返回值的功能。
示例 1:可以将多个匹配写到一起。
switch (name) {
case "李白", "杜甫", "白居易" -> System.out.println("唐代诗人");
case "苏轼", "辛弃疾" -> System.out.println("宋代诗人");
default -> System.out.println("其他朝代诗人");
}
示例 2:每个分支直接返回一个值。
int tmp = switch (name) {
case "李白", "杜甫", "白居易" -> 1;
case "苏轼", "辛弃疾" -> 2;
default -> {
System.out.println("其他朝代诗人");
yield 3;
}
};
3、instanceof的模式匹配
instances 增加了模式匹配的功能,如果变量类型经过instances判断能够匹配目标类型,则对应分支中无需再做类型强转。
示例代码:
if (o instanceof Integer i && i > 0) {
System.out.println(i.intValue());
} else if (o instanceof String s && s.startsWith("t")) {
System.out.println(s.charAt(0));
}
4、var 局部变量推导
对于某些可以直接推导出类型的局部变量,可以使用var进行声明。
var nums = new int[] {
1, 2, 3, 4, 5};
var sum = Arrays.stream(nums).sum();
System.out.println("数组之和为:" + sum);
这个特性仁者见仁,智者见智。 Java 的强类型语法更能保护代码安全。
模块化及类封装
public class RecordTest {
@Test
public void getPoint() throws IllegalAccessException {
Point p = new Point(10,20);
for (Method method : p.getClass().getMethods()) {
System.out.println(method);
}
for (Field field : p.getClass().getDeclaredFields()) {
System.out.println(field);
// 不允许通过反射修改值。
// field.setAccessible(true);
// field.set(p,30);
}
System.out.println(p.x()+"===="+p.y());
}
}
record记录类的实现原理,其实大致相当于给每个属性添加了private final声明。这样就不允许修改。另外,从字节码也能看到,对于record类,同时还实现了toString,hashcode,equals方法,而这些方法都被声明成了final,进一步阻止应用定制record相关的业务逻辑。
2 、隐藏类 Hidden Classes
从 JDK15 开始,JDK 引入了一个很有意思的特性,隐藏类。隐藏类是一种不能被其他类直接使用的类。隐藏类不再依赖于类加载器,而是通过读取目标类字节码的方式,创建一个对其他类字节码隐藏的class对象,然后通过反射的方式创建对象,调用方法。
我们先来一个示例理解一下什么是隐藏类,再来思考隐藏类有什么用处。
比如先编写一个普通的测试类
public class HiddenClass {
public String sayHello(String name) {
return "Hello, " + name;
}
public static void printHello(String name) {
System.out.printf("""
Hello, %s !
Hello, HiddenClass !
%n""", name);
}
}
传统方式下,要使用这个类,就需要经过编译,然后类加载的整个过程。但是隐藏类机制允许直接从编译后的class字节码入手,并且绕过整个类加载的复杂过程,直接使用这个类。
比如,我们可以使用下面的方法获取class字节数组:
public void printHiddenClassBytesInBase64(){
//编译后的 class 文件地址
String classPath = "/Users/roykingw/DevCode/JDK17Demo/demoModule/target/classes/com/roy/hidden/HiddenClass.class";
try {
byte[] bytes = Files.readAllBytes(Paths.get(classPath));
System.out.println(Base64.getEncoder().encodeToString(bytes));
} catch (IOException e) {
e.printStackTrace();
}
}
这样就可以拿到一串编码后的class文件的字节码。接下来,就可以用这个字节码直接生成这个类。例如:
public void testInvokeHiddenClass() throws Throwable {
//class文件的字节码
String CLASS_INFO = "yv66vgAAAD0ANgoAAgADBwAEDAAFAAYBABBqYXZhL2xhbmcvT2JqZWN0AQAGPGluaXQ+AQADKClWEgAAAAgMAAkACgEAF21ha2VDb25jYXRXaXRoQ29uc3RhbnRzAQAmKExqYXZhL2xhbmcvU3RyaW5nOylMamF2YS9sYW5nL1N0cmluZzsJAAwADQcADgwADwAQAQAQamF2YS9sYW5nL1N5c3RlbQEAA291dAEAFUxqYXZhL2lvL1ByaW50U3RyZWFtOwgAEgEAI0hlbGxvLCAlcyAhCkhlbGxvLCBIaWRkZW5DbGFzcyAhCiVuCgAUABUHABYMABcAGAEAE2phdmEvaW8vUHJpbnRTdHJlYW0BAAZwcmludGYBADwoTGphdmEvbGFuZy9TdHJpbmc7W0xqYXZhL2xhbmcvT2JqZWN0OylMamF2YS9pby9QcmludFN0cmVhbTsHABoBABpjb20vcm95L2hpZGRlbi9IaWRkZW5DbGFzcwEABENvZGUBAA9MaW5lTnVtYmVyVGFibGUBABJMb2NhbFZhcmlhYmxlVGFibGUBAAR0aGlzAQAcTGNvbS9yb3kvaGlkZGVuL0hpZGRlbkNsYXNzOwEACHNheUhlbGxvAQAEbmFtZQEAEkxqYXZhL2xhbmcvU3RyaW5nOwEACnByaW50SGVsbG8BABUoTGphdmEvbGFuZy9TdHJpbmc7KVYBAApTb3VyY2VGaWxlAQAQSGlkZGVuQ2xhc3MuamF2YQEAEEJvb3RzdHJhcE1ldGhvZHMPBgApCgAqACsHACwMAAkALQEAJGphdmEvbGFuZy9pbnZva2UvU3RyaW5nQ29uY2F0RmFjdG9yeQEAmChMamF2YS9sYW5nL2ludm9rZS9NZXRob2RIYW5kbGVzJExvb2t1cDtMamF2YS9sYW5nL1N0cmluZztMamF2YS9sYW5nL2ludm9rZS9NZXRob2RUeXBlO0xqYXZhL2xhbmcvU3RyaW5nO1tMamF2YS9sYW5nL09iamVjdDspTGphdmEvbGFuZy9pbnZva2UvQ2FsbFNpdGU7CAAvAQAISGVsbG8sIAEBAAxJbm5lckNsYXNzZXMHADIBACVqYXZhL2xhbmcvaW52b2tlL01ldGhvZEhhbmRsZXMkTG9va3VwBwA0AQAeamF2YS9sYW5nL2ludm9rZS9NZXRob2RIYW5kbGVzAQAGTG9va3VwACEAGQACAAAAAAADAAEABQAGAAEAGwAAAC8AAQABAAAABSq3AAGxAAAAAgAcAAAABgABAAAAAwAdAAAADAABAAAABQAeAB8AAAABACAACgABABsAAAA7AAEAAgAAAAcrugAHAACwAAAAAgAcAAAABgABAAAABQAdAAAAFgACAAAABwAeAB8AAAAAAAcAIQAiAAEACQAjACQAAQAbAAAAQAAGAAEAAAASsgALEhEEvQACWQMqU7YAE1exAAAAAgAcAAAACgACAAAACQARAA0AHQAAAAwAAQAAABIAIQAiAAAAAwAlAAAAAgAmACcAAAAIAAEAKAABAC4AMAAAAAoAAQAxADMANQAZ";
byte[] classInBytes = Base64.getDecoder().decode(CLASS_INFO);
Class<?> proxy = MethodHandles.lookup()
.defineHiddenClass(classInBytes, true, MethodHandles.Lookup.ClassOption.NESTMATE)
.lookupClass();
// 输出类名
System.out.println(proxy.getName());
// 输出类有哪些函数
for (Method method : proxy.getDeclaredMethods()) {
System.out.println(method.getName());
}
// 2. 调用对应的方法
MethodHandle mhPrintHello = MethodHandles.lookup().findStatic(proxy, "printHello", MethodType.methodType(void.class, String.class));
mhPrintHello.invokeExact("loulan");
Object proxyObj = proxy.getConstructors()[0