摘要:记得我从大一开始学的就是 Java 8,当时还叫做新特性;后来 Java 11 出了,我用 Java 8;Java 17 出了,我用 Java 8;Java 21 出了,我还用 Java 8。
Java 8 终于要被淘汰了!
记得我从大一开始学的就是 Java 8,当时还叫做新特性;后来 Java 11 出了,我用 Java 8;Java 17 出了,我用 Java 8;Java 21 出了,我还用 Java 8。
随你怎么更新,我用 Java 8!
我之前带大家做项目的时候,还是强烈建议大家用 Java 8 的,为什么现在说 Java 8 要被淘汰了呢?
在我看来主要是因为业务和生态变了,尤其是这几年 AI 发展,很多老项目都要接入 AI、新项目直接面向 AI 开发,为了追求开发效率,我们要用 AI 开发框架(比如 Spring AI、LangChain4j),而这些框架要求的版本几乎都是 >= 17, 所以我们团队自己的业务也从 Java 8 迁到 Java 21 了。
另外也是因为有些新版本的 Java 特性确实很香,学会之后无论是开发效率还是性能都能提升一大截。
所以我做了本期干货内容,讲通 Java 8 ~ Java 24 的新特性,洋洋洒洒一万多字!建议收藏,看完后你就约等于学完了十几个 Java 版本~
⭐️ 推荐观看视频版,体验更佳:https://bilibili.com/video/BV1haamzUE8m
免费 Java 教程 + 新特性大全:https://codefather.cn/course/java
Java 8~16 新特性看上篇文章:带你速通 Java 8~16 新特性
Java 17 是目前 Java 最主流的 LTS 版本,比例已经超越了 Java 8!现在很多新的 Java 开发框架和类库支持的最低 JDK 版本就是 17(比如 AI 开发框架 LangChain4j)。
在很多 Java 开发者的印象中,一个类要么完全开放继承(任何类都能继承),要么完全禁止继承(final 类)。
// 选择1:完全开放继承public class Shape { // 问题:不知道会有哪些子类,难以进行穷举}// 选择2:完全禁止继承public final class Circle { // 问题:即使在同一个模块内也无法继承}其实这样是没办法精确控制继承关系的,在设计 API 或领域模型时可能会遇到问题。
Java 17 将 Sealed 密封类转正,让类的继承关系变得更可控和安全。
比如我可以只允许某几个类继承:
public sealed class Shape permits Circle, Rectangle, Triangle { // 只允许这三个类继承}但是,被允许继承的子类必须选择一种继承策略:
1)final:到我为止,不能再继承了
public final class Circle extends Shape {}2)sealed:我也要控制谁能继承我
public sealed class Triangle extends Shape permits RightTriangle {}3)non-sealed:我开放继承,任何人都可以继承我
public non-sealed class Rectangle extends Shape {}强制声明继承策略是为了 确保设计控制权的完整传递。如果不强制声明,sealed 类精确控制继承的价值就会被破坏,任何人都可以通过继承子类来绕过原始设计的限制。
注意,虽然看起来 non-sealed 打破了这个设计,但这也是设计者的主动选择。如果不需要强制声明,设计者可能会无意中失去控制权。
有了 Sealed 类后,某个接口可能的实现类型就尽在掌握了,可以让 switch 模式匹配变得更加安全:
// 编译器知道所有可能的子类型,可以进行完整性检查public double calculateArea(Shape shape) { return switch (shape) { case Circle c -> Math.PI * c.getRadius * c.getRadius; case Rectangle r -> r.getWidth * r.getHeight; case Triangle t -> 0.5 * t.getBase * t.getHeight; // 编译器确保我们处理了所有情况,无需 default 分支 };}Java 17 引入了全新的随机数生成器 API,提供了更优的性能和更多的算法选择:
// 传统的随机数Random oldRandom = new Random;int oldValue = oldRandom.nextInt(100);// 新的随机数生成器RandomGenerator generator = RandomGenerator.of("L32X64MixRandom");int newValue = generator.nextInt(100);Java 17 进一步强化了对 JDK 内部 API 的封装,一些之前可以通过反射访问的内部类现在完全不可访问,比如:
sun.misc.Unsafecom.sun.* 包下的类jdk.internal.* 包下的类虽然这提高了 JDK 的安全性和稳定性,但可能需要迁移一些依赖内部 API 的老代码。
Java 18个人感觉 Java 18 提供的功能都没什么用,简单了解一下就好。
Java 18 引入了一个简单的 Web 服务器,主要用于开发和测试。
# 启动简单的文件服务器,服务当前目录jwebserver# 指定端口和目录jwebserver -p 8080 -d /path/to/your/files# 绑定到特定地址jwebserver -b 127.0.0.1 -p 9000Nginx 不香么,我要用这个东西?
Java 18 将 UTF-8 设为默认字符集,解决了很多字符编码相关的问题,Java 程序在不同平台上的行为会更加一致。
// 这些操作现在默认使用 UTF-8 编码FileReader reader = new FileReader("file.txt");FileWriter writer = new FileWriter("file.txt");PrintStream out = new PrintStream("output.txt");在这之前,Java 使用的是 系统默认字符集,会导致同一段代码在不同操作系统上可能产生完全不同的结果。
Java 18 引入了 @snippet 标签,可以让 JavaDoc 生成的代码示例更美观,而且支持从外部文件引入代码片段。
/*** 计算两个数的最大公约数* * {@snippet :* int a = 48;* int b = 18;* int gcd = MathUtils.gcd(a, b);* System.out.println("GCD: " + gcd); // @highlight substring="GCD"* }* * @param a 第一个数* @param b 第二个数* @return 最大公约数*/public static int gcd(int a, int b) { // 实现代码...}/*** 从外部文件引入代码片段* * {@snippet file="examples/QuickSort.java" region="main-algorithm"}* * @param arr 要排序的数组*/public static void quickSort(int arr) { // 实现代码...}不过这年头还有开发者阅读 JavaDoc 么?
Java 19 和 20 主要是为一些重大特性做准备,包括虚拟线程、Record 模式、Switch 模式匹配等。
Java 21Java 21 是鱼皮做新项目时使用的首选 LTS 版本。这个版本发布了很多重要特性,其中最重要的是 Virtual Threads 虚拟线程。
这是 Java 并发编程的革命性突破,也是很多 Java 开发者选择 21 的理由。
什么是虚拟线程呢?
想象一下,你是一家餐厅的老板。传统的线程就像是餐厅的服务员,假设每个服务员同时只能服务一桌客人。如果有 1000 桌客人,你就需要 1000 个服务员,但这显然不现实。餐厅地方不够,也负担不起那么多员工的工钱。
在传统的 Java 线程模型中也是如此。如果每个线程都对应操作系统的一个真实线程,创建成本很高、内存占用也大。当需要处理大量并发请求时,系统可能很快就会被拖垮。
举个例子,假设开 1000 个线程同时处理网络请求:
public void handleRequests { for (int i = 0; i { // 发送网络请求,需要等待响应 String result = httpClient.get("https://codefather.cn"); System.out.println("收到响应: " + result); }).start; }}创建 1000 个线程会消耗大量系统资源(因为对应 1000 个操作系统线程),而且大部分时间线程都在等待网络响应,很浪费。
而虚拟线程就像是给餐厅引入了一个智能调度系统。服务员不再需要傻傻地等在客人桌边等菜上桌,而是可以在等待的时候去服务其他客人。当某桌的菜准备好了,系统会自动安排一个空闲的服务员去处理。
我们可以开一个虚拟线程执行器执行同样的一批任务,这里我用的执行器会为每个任务生成一个虚拟线程来处理:
public void handleRequestsWithVirtualThreads { try (var executor = Executors.newVirtualThreadPerTaskExecutor) { for (int i = 0; i { // 同样的网络请求代码 String result = httpClient.get("https://codefather.cn"); System.out.println("收到响应: " + result); }); } }}同样是 1000 个,但是 1000 个虚拟线程只需要很少的系统资源(比如映射到 8 个操作系统线程上);而且当虚拟线程等待网络响应时,会让出底层的操作系统线程,操作系统线程就会自动切换去执行其他虚拟线程和任务。
总结一下 Virtual Threads 的核心优势。首先是 超级轻量。一个传统线程可能需要几 MB 的内存,而一个虚拟线程只需要几 KB。你可以轻松创建百万级别的虚拟线程而不用担心系统资源。
其次是 编程简单。你不需要学习复杂的异步编程模式,跟创建一个普通线程的代码类似,一行代码就能提交异步任务。当遇到阻塞的 I/O 操作时,虚拟线程会自动让出底层的操作系统线程。
// 直接创建虚拟线程public void handleSingleUser(Long userId) { Thread.ofVirtual.start( -> { // 要异步执行的任务 User user = userService.findById(userId); processUser(user); });}相关面试题:什么是协程?Java 支持协程吗?
Java 14 版本推出了 Switch 表达式,能够一行处理多个条件;Java 21 版本进一步优化了 Switch 的能力,新增了模式匹配特性,能够更轻松地根据对象的类型做不同的处理。
没有 Switch 模式匹配时,我们需要利用 instanceof 匹配类型:
public String processMessage(Object message) { if (message instanceof String) { String textMessage = (String) message; return "文本消息:" + textMessage; } else if (message instanceof Integer) { Integer numberMessage = (Integer) message; return "数字消息:" + numberMessage; } else if (message instanceof List) { List listMessage = (List) message; return "列表消息,包含 " + listMessage.size + " 个元素"; } else { return "未知消息类型"; }}有了模式匹配,这段代码可以变得很优雅,直接在匹配对象类型的同时声明了变量(跟 instanceof 模式匹配有点像):
public String processMessage(Object message) { return switch (message) { case String text -> "文本消息:" + text; case Integer number -> "数字消息:" + number; case List list -> "列表消息,包含 " + list.size + " 个元素"; case null -> "空消息"; default -> "未知消息类型"; };}此外,模式匹配还支持 条件判断,让处理逻辑更加精细,相当于在 case ... when ... 中写 if 条件表达式(感觉有点像 SQL 的语法)。
// 根据字符串长度采用不同处理策略public String processText(String text) { return switch (text) { case String s when s.length "短文本:" + s; case String s when s.length "中等文本:" + s.substring(0, 5); case String s -> "长文本:" + s.substring(0, 10); };}Record 模式让数据的解构变得更简单直观,可以一次性取出 record 中所有需要的信息。
举个例子,先定义一些简单的 Record:
public record Person(String name, int age) {}public record Address(String city, String street) {}public record Employee(Person person, Address address, double salary) {}使用 Record 模式可以直接解构这些数据,不用一层一层取了:
public String analyzeEmployee(Employee emp) { return switch (emp) { // 一次性提取所有需要的信息 case Employee(Person(var name, var age), Address(var city, var street), var salary) when salary > 50000 -> String.format("%s(%d岁)是高薪员工,住在%s%s,月薪%.0f", name, age, city, street, salary); case Employee(Person(var name, var age), var address, var salary) -> String.format("%s(%d岁)月薪%.0f,住在%s", name, age, salary, address.city); };}Listtasks = new ArrayList;tasks.addFirst("鱼皮的任务"); // 添加到开头tasks.addLast("小阿巴的任务"); // 添加到结尾String firstStr = tasks.getFirst; // 获取第一个String lastStr = tasks.getLast; // 获取最后一个String removedFirst = tasks.removeFirst; // 删除并返回第一个String removedLast = tasks.removeLast; // 删除并返回最后一个Listreversed = tasks.reversed; // 反转列表除了 List 之外,SequencedMap 接口(比如 LinkedHashMap)和 SequencedSet 接口(比如 LinkedHashSet)也新增了类似的方法。本质上都是实现了有序集合接口:
Java 21 中的分代 ZGC 可以说是垃圾收集器领域的一个重大突破。ZGC 从 Java 11 开始就以其超低延迟而闻名,但是它并没有采用分代的设计思路。
在这之前,ZGC 对所有对象一视同仁,无论是刚创建的新对象还是存活了很久的老对象,都使用同样的收集策略。这虽然保证了一致的低延迟,但在内存分配密集的应用中,效率并不是最优的。
分代 ZGC 的核心思想是基于一个现象 —— 大部分对象都是 “朝生夕死” 的。它将堆内存划分为年轻代和老年代两个区域,年轻代的垃圾收集可以更加频繁和高效,因为大部分年轻对象很快就会死亡,收集器可以快速清理掉这些垃圾;而老年代的收集频率相对较低,减少了对长期存活对象的不必要扫描。
长期以来,Java 程序员想要调用 C/C++ 编写的本地库,只能依赖 JNI(Java Native Interface)。但说实话,JNI 的使用体验并不好,需要手写胶水代码、维护头文件和构建脚本、处理 JNIEnv 和复杂类型转换,一旦接口频繁变更,维护成本较高。
外部函数与内存 API(FFM API)提供了标准化、类型安全的方式来从 Java 直接调用本地代码。FFM API 现在支持几乎所有主流平台,性能相比 JNI 可能有一定提升,特别是在频繁调用本地函数的场景下。
大家不用记忆具体是怎么使用的,只要知道有这个特性就足够了。
在开发中,我们可能会遇到这样的情况:有些变量我们必须声明,但实际上并不会使用到它们的值。
在这之前,我们只能给这些不使用的变量起一个名字,代码会显得有些多余。举些例子:
try { processData;} catch (IOException ignored) { // 只关心异常发生,不关心异常对象 System.out.println("处理数据时出错了");}String result = switch (obj) { case Integer i -> "这是整数: " + i; case String s -> "这是字符串: " + s; case Double unused -> "这是浮点数"; // 不需要使用具体的值 default -> "未知类型";};有了未命名变量特性,可以使用下划线 _ 表示不使用的变量代码,意图更清晰:
try { processData;} catch (IOException _) { // 不关心异常对象 System.out.println("处理数据时出错了");}String result = switch (obj) { case Integer i -> "这是整数: " + i; case String s -> "这是字符串: " + s; case Double _ -> "这是浮点数"; // 只关心类型,不关心值 default -> "未知类型";};// 在解构中也很有用if (point instanceof Point(var x, var _)) { // 只关心 x 坐标 System.out.println("x 坐标是: " + x);}Java 22 引入了分代 ZGC,但当时你需要通过特殊的 JVM 参数来启用它:
java -XX:+UseZGC -XX:+UnlockExperimentalVMOptions -XX:+UseGenerationalZGC MyApp而在 Java 23 中,分代模式成为了 ZGC 的默认行为。
虽然听起来只是个小改动,但这个改变的背后是大量的性能测试和实际应用验证的结果。Oracle 的工程师们发现,分代 ZGC 在绝大多数应用场景中都能带来显著的性能改善,特别是在内存分配密集的应用中,性能提升可能达到数倍之多。
类文件 API 是一个专为框架和工具开发者设计的强大特性。长期以来,如果你想要在运行时动态生成、分析或修改 Java 字节码,就必须依赖像 ASM、Javassist 或者 CGLIB 这样的第三方库。
而且操作字节码需要深入了解底层细节,学习难度很大,我只能借助 AI 来搞定。
有了类文件 API,操作字节码变得简单了一些:
public byte generateClass { return ClassFile.of.build(ClassDesc.of("com.example.GeneratedClass"), cb -> { // 添加默认构造函数 cb.withMethod("", MethodTypeDesc.of(ConstantDescs.CD_void), ACC_PUBLIC, mb -> { mb.withCode(codeb -> { codeb.aload(0) .invokespecial(ConstantDescs.CD_Object, "", MethodTypeDesc.of(ConstantDescs.CD_void)) .return_; }); }); // 添加 sayHello 方法 cb.withMethod("sayHello", MethodTypeDesc.of(ConstantDescs.CD_String), ACC_PUBLIC, mb -> { mb.withCode(codeb -> { codeb.ldc("Hello from generated class!") .areturn; }); }); });}读取和分析现有的类文件也很简单:
public void analyzeClass(byte classBytes) { ClassModel cm = ClassFile.of.parse(classBytes); System.out.println("类名: " + cm.thisClass.asInternalName); System.out.println("方法列表:"); for (MethodModel method : cm.methods) { System.out.println(" - " + method.methodName.stringValue + method.methodType.stringValue); }}第三方字节码库可能需要一段时间才能跟上新特性的变化,而官方的类文件 API 则能够与语言特性同步发布,确保开发者能够使用最新的字节码功能。
Stream API 自 Java 8 引入以来,极大地改变了我们处理集合数据的方式,但是在一些特定的场景中,传统的 Stream 操作就显得力不从心了。Stream Gatherers 正是对 Stream API 的一个重要扩展,它解决了现有 Stream API 在某些复杂数据处理场景中的局限性,补齐了 Stream API 的短板。
如果你想实现一些复杂的数据聚合操作,比如滑动窗口或固定窗口分析,可以直接使用 Java 24 内置的 Gatherers。
// 1. 滑动窗口 - windowSliding(size)Listprices = Arrays.asList(100.0, 102.0, 98.0, 105.0, 110.0);ListmovingAverages = prices.stream .gather(Gatherers.windowSliding(3)) // 创建大小为 3 的滑动窗口 .map(window -> { // window 是 List类型,包含 3 个连续元素 return window.stream .mapToDouble(Double::doubleValue) .average .orElse(0.0); }) .collect(Collectors.toList);System.out.println("移动平均值: " + movingAverages);// 移动平均值: [100.0, 101.66666666666667, 104.33333333333333]还有更多方法,感兴趣的同学可以自己尝试:
除了内置的 Gatherers 外,还可以自定义 Gatherer,举一个最简单的例子 —— 给每个元素添加前缀。先自定义一个 Gatherer:
GathereraddPrefix = Gatherer.ofSequential( -> null, // 不需要状态,所以初始化为 null (state, element, downstream) -> { // 给每个元素添加 "前缀-" 并推送到下游 downstream.push("前缀-" + element); return true; // 继续处理下一个元素 } // 不需要 finisher,省略第三个参数);Gatherer.ofSequential 方法会返回 Gatherer 接口的实现类:
然后就可以愉快地使用了:
Listnames = Arrays.asList("鱼皮", "编程", "导航");ListprefixedNames = names.stream .gather(addPrefix) .collect(Collectors.toList);System.out.println(prefixedNames);// 输出: [前缀-鱼皮, 前缀-编程, 前缀-导航]这个例子展示了 Gatherer 的最基本形态:
不需要状态:第一个参数返回 null,因为我们不需要维护任何状态简单转换:第二个参数接收每个元素,做简单处理后推送到下游无需收尾:省略第三个参数,因为不需要最终处理虽然这个例子用 map 也能实现,但它帮助我们理解了 Gatherer 的基本工作机制。
这就是 Stream Gatherers 强大之处,它能够维护复杂的内部状态,并根据业务逻辑灵活地向下游推送结果,让原本需要手动循环的复杂逻辑变得简洁优雅。
Stream Gatherers 的另一个优势是它和现有的 Stream API 完全兼容。你可以在 Stream 管道中的任何位置插入 Gatherer 操作,就像使用 map、filter 或 collect 一样自然,让复杂的数据处理变得既强大又优雅。
OK 以上就是本期内容,学会的话记得点赞三连支持,我们下期见。
编程学习交流:编程导航:https://www.codefather.cn/
简历快速制作:老鱼简历:https://laoyujianli.com
✏️ 面试刷题神器:面试鸭:https://mianshiya.com
AI 学习指南:AI知识库:https://ai.codefather.cn/
来源:程序员鱼皮