Java 8终于要被淘汰了!速通Java 8~24新特性,又能面试吹牛皮了

B站影视 韩国电影 2025-09-05 11:32 1

摘要:记得我从大一开始学的就是 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 绝对是 Java 历史上最重要的稳定版本,也是这么多年来最受欢迎的 Java 版本,甚至有专门的书籍来讲解 Java 8。这个版本最大的变化就是引入了函数式编程的概念,给 Java 这门传统的面向对象语言增加了新的玩法。

Lambda 表达式可以说是 Java 8 的杀手级特性。在这个特性出现之前,我们要实现一个简单的回调函数,只能通过匿名内部类的方式,代码又臭又长。

举些例子,比如给按钮添加点击事件、或者创建一个新线程执行操作,必须要自己 new 接口并且编写接口的定义和实现代码。

// Java 8 之前的写法,给按钮添加点击事件button.addActionListener(new ActionListener { @Override public void actionPerformed(ActionEvent e) { System.out.println("按钮被点击了"); }});// 使用线程的传统写法Thread thread = new Thread(new runnable { @Override public void run { System.out.println("线程正在运行"); }});

Lambda 表达式的出现,让代码变得简洁优雅,告别匿名内部类!

// Java 8 Lambda 写法button.addActionListener(e -> System.out.println("按钮被点击了"));Thread thread = new Thread( -> System.out.println("线程正在运行"));

Lambda 表达式的语法非常灵活,可以根据参数个数和方法代码的复杂度选择不同的写法:

// 无参数的 LambdaRunnable r = -> System.out.println("Hello Lambda!");// 单个参数(可以省略括号)Consumerprinter = s -> System.out.println(s);// 多个参数BinaryOperatoradd = (a, b) -> a + b;Comparatorcomparator = (a, b) -> a.compareTo(b);// 复杂的方法体(需要大括号和 return)Functionprocessor = input -> { String processed = input.trim.toLowerCase; if (processed.isEmpty) { return "空字符串"; } return "处理后的字符串:" + processed;};

举个例子:

Listnames = Arrays.asList("鱼皮", "编程导航", "面试鸭");// 使用 Lambda 表达式names.forEach(name -> System.out.println(name));// 使用方法引用(更简洁)names.forEach(System.out::println);

实际开发中,方法引用经常用于获取某个 Java 对象的属性。比如使用 MyBatis Plus 来构造数据库查询条件时,经常会看到下面这种代码:

LambdaQueryWrapperlambdaQueryWrapper = new LambdaQueryWrapper;lambdaQueryWrapper.eq(User::getName, "鱼皮");

方法引用有几种不同的形式,包括静态方法引用、实例方法引用、构造器引用,适用于不同的场景。

// 静态方法引用Liststrings = Arrays.asList("1", "2", "3");Listnumbers = strings.stream .map(Integer::parseInt) // 等于 s -> Integer.parseInt(s) .collect(Collectors.toList);// 实例方法引用Listwords = Arrays.asList("hello", "world", "java");ListupperWords = words.stream .map(String::toUpperCase) // 等于 s -> s.toUpperCase .collect(Collectors.toList);// 构造器引用ListnameList = Arrays.asList("鱼皮", "编程导航", "面试鸭");List

persons = nameList.stream .map(Person::new) // 等于 name -> new Person(name) .collect(Collectors.toList);

展开来说,函数式接口定义了 Lambda 表达式的参数和返回值类型,而 Lambda 表达式提供了这个接口的具体实现。两者相辅相成,让 Java 函数式编程伟大!

Java 8 为我们提供了很多内置的函数式接口,让函数式编程变得简单直观。列举一些常用的函数式接口:

1)Predicate 用于条件判断:

// PredicateisNotEmpty = isEmpty.negate; // 取反Listnumbers = Arrays.asList(1, 2, 3, 4, 5, 6);ListevenNumbers = numbers.stream .filter(isEven) .collect(Collectors.toList);

2)Function 接口用于数据转换,支持函数组合,让代码逻辑更清晰:

// Function用于转换FunctionaddPrefix = s -> "前缀-" + s;FunctionaddSuffix = s -> s + "-后缀";Functioncombined = addPrefix.andThen(addSuffix);String result = combined.apply("鱼皮"); // "前缀-鱼皮-后缀"

3)Consumer 和 Supplier 接口分别用于消费和提供数据:

// Consumerprinter = System.out::println;Consumerlogger = s -> log.info("处理数据:{}", s);// 组合消费ConsumercombinedConsumer = printer.andThen(logger);// Supplier用于提供数据SupplierrandomId = -> UUID.randomUUID.toString;Suppliernow = LocalDateTime::now;max = Integer::max;BinaryOperator

虽然实际开发中,我们更多的是使用 Java 内置的函数式接口,但大家还是要了解一下自定义函数式接口的写法,有个印象。

// 创建自定义函数式接口@Functionalinterfacepublic interface Calculator { double calculate(double a, double b);}

使用自定义函数式接口,代码会更简洁:

// 使用自定义函数式接口Calculator addition = (a, b) -> a + b;Calculator subtraction = (a, b) -> a - b;

自定义函数式接口时,需要注意:

1)函数式接口必须是接口类型,不能是类、抽象类或枚举。

2)必须且只能包含一个抽象方法。否则 Lambda 表达式可能无法匹配接口。

3)建议使用 @FunctionalInterface注解。

虽然这个注解不是强制的,但加上后编译器会帮你检查是否符合函数式接口的规范(是否只有一个抽象方法),如果不符合会报错。

4)可以包含默认方法 default 和静态方法 static

函数式接口允许有多个默认方法和静态方法,因为它们不是抽象方法,不影响单一抽象方法的要求。

// 创建自定义函数式接口@FunctionalInterfacepublic interface Calculator { double calculate(double a, double b); // 可以有默认方法 default double add(double a, double b) { return a + b; } // 可以有静态方法 static Calculator multiply { return (a, b) -> a * b; }}

Stream API 是 Java 8 另一个重量级特性,它让集合处理变得既优雅又高效。(学大数据的同学应该对它不陌生)

在 Stream API 出现之前,我们处理集合数据只能通过传统的循环,需要大量的样板代码。

比如过滤列表中的数据、将小写转为大写并排序:

Listwords = Arrays.asList("apple", "banana", "cherry");// 传统的处理方式Listresult = new ArrayList;for (String word : words) { if (word.length > 5) { String upperCase = word.toUpperCase; result.add(upperCase); }}Collections.sort(result);

如果使用 Stream API,可以让同样的逻辑变得更简洁直观:

// 使用 Stream 的方式Listresult = words.stream .filter(word -> word.length > 5) // 过滤长度大于 5 的单词 .map(String::toUpperCase) // 转换为大写 .sorted // 排序 .collect(Collectors.toList); // 收集结果

这就是 Stream 的作用。Stream 不是数据结构,而是 像工厂流水线 一样处理数据的工具。数据从一端进入,经历过滤、转换、排序等一系列加工步骤后,最终输出我们想要的结果。这种 链式调用 让代码读起来就像自然语言一样流畅。

Stream 的操作分为中间操作和终端操作。中间操作是 “懒惰” 的,只有在遇到终端操作时才会真正执行。

filter 过滤和 map 映射都是中间操作,比如下面这段代码,并不会对列表进行过滤和转换:

Listnumbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);numbers.stream .filter(n -> n > 3) // 中间操作:过滤大于3的数字 .map(n -> n * n) // 中间操作:平方

一些常用的中间操作:

给上面的代码加上一个终端操作 collect 后,才会真正执行:

Listnumbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);// 演示中间操作和终端操作numbers.stream .filter(n -> n > 3) // 中间操作:过滤大于3的数字 .map(n -> n * n) // 中间操作:平方 .collect(Collectors.toList); // 终端操作:收集结果

一些常用的终端操作:

Map> partitioned = numbers.stream .filter(n -> n > 3) // 中间操作:过滤大于3的数字 .map(n -> n * n) // 中间操作:平方 .collect(Collectors.partitioningBy(n -> n % 2 == 0)); // 终端操作:按奇偶分组

2)使用 Stream 内置的统计功能,对数据进行统计:

// 统计操作IntSummaryStatistics stats = numbers.stream .mapToInt(Integer::intValue) .summaryStatistics;System.out.println("数量:" + stats.getCount);System.out.println("总和:" + stats.getSum);System.out.println("平均值:" + stats.getAverage);System.out.println("最大值:" + stats.getMax);System.out.println("最小值:" + stats.getMin);

3)按照对象的某个字段进行分组计算:

Listpeople = Arrays.asList( new Person("张三", 25, "北京"), new Person("鱼皮", 18, "上海"), new Person("李四", 25, "北京"), new Person("老二", 35, "上海"));// 按城市分组Map> byCity = people.stream .collect(Collectors.groupingBy(Person::getCity));// 按城市分组并统计年龄MapavgAgeByCity = people.stream .collect(Collectors.groupingBy( Person::getCity, Collectors.averagingInt(Person::getAge) ));// 按城市分组并收集姓名Map> namesByCity = people.stream .collect(Collectors.groupingBy( Person::getCity, Collectors.mapping(Person::getName, Collectors.toList) ));

学过数据库的同学应该对这种操作并不陌生,其实 SQL 语句中的很多操作都可以通过 Stream 实现。这也是 Stream 的典型应用场景 —— 对数据库中查出的数据进行业务层面的运算。

并行流是 Stream API 的另一个强大特性,它可以自动利用多核 CPU 处理器加速数据处理任务的执行。

在此之前,我们要实现并行处理集合数据,需要手动管理线程池和任务分割,代码复杂且容易出错。

但有了 Stream API,一行代码就能创建并行流,比如过滤并计算数据的总和:

largeList = IntStream.rangeClosed(1, 1000000) .boxed .collect(Collectors.toList);// 并行处理,只需要改一个方法调用long parallelCount = largeList.parallelStream .filter(n -> isPrime(n)) .count;

并行流底层使用了 Fork/Join 框架,简单来说就是把大任务拆分成小任务,分配给多个线程同时执行,最后把结果合并起来。这个过程对开发者完全透明,只需要调用 parallelStream 即可。

但也正因如此,实际开发中,要谨慎使用并行流!

因为它使用的是 JVM 全局的 ForkJoinPool.commonPool,默认线程数等于 CPU 核心数减 1。如果某个并行流任务阻塞了线程,会影响其他并行流的性能。

而且并行流不一定就更快,特别是对于简单操作或小数据集,切换线程的开销可能超过并行带来的收益。

因此,并行流更适合大数据量、CPU 密集型任务(如复杂计算、图像处理),不适合 I/O 密集型任务(如网络请求)。而且只要涉及到并发场景,就要考虑到线程安全问题。


【实用】Optional

nullPointerException(NPE)一直是 Java 程序员的噩梦,学 Java 的同学应该都被它折磨过。

之前,我们只能通过大量的 if 语句检查 null 来避免空指针异常,不仅代码又臭又长,而且稍微不注意就漏掉了。

// 传统的空值检查public String getDefaultName(User user) { if (user != null) { String name = user.getName; if (name != null && !name.isEmpty) { return name.toUpperCase; } } return "unknown";}

Optional 类的引入就是为了优雅地处理可能为空的值,可以先把它理解为 “包装器”,把可能为空的对象封装起来。

创建 Optional 对象:

// 创建 Optional 对象Optionaloptional1 = Optional.of("Hello"); // 不能为 nullOptionaloptional2 = Optional.ofNullable(getName); // 可能为 nullOptionaloptional3 = Optional.empty; // 空的 Optional

Optional 提供了多种处理空值的方法:

// 检查是否有值if (optional.isPresent) { System.out.println(optional.get);}// 更优雅的方式,如果对象存在则输出optional.ifPresent(System.out::println);

还可以设置默认值策略,比如空值时抛出异常:

// 提供默认值String result1 = optional.orElse("默认值");String result2 = optional.orElseGet( -> generateDefaultValue);String result3 = optional.orElseThrow( -> new IllegalStateException("值不能为空"));

除了前面这些基本方法外,Optional 甚至提供了一套完整的 API 来处理空值场景!

跟 Stream API 类似,你可以对 Optional 封装的数据进行过滤、映射等操作:

optional .filter(s -> s.length > 5) .map(String::toUpperCase) .ifPresentOrElse( System.out::println, // 有值时执行 -> System.out.println("没有值") // 无值时执行 );

鱼皮经常使用 Optional 来简化空值判断:

int pageNum = Optional.ofNullable(params.getPageNum) .orElseThrow( -> new RuntimeException("pageNum不能为空"));

如果不用 Optional,就要写下面这段代码:

int pageNum;if (params.getPageNum != null) { pageNum = params.getPageNum;} else { throw new RuntimeException("pageNum不能为空");}

此外,Optional 的一个典型应用场景是在集合中进行安全查找:

Listnames = Arrays.asList("张三", null, "李四", "", "王五");// 使用 Optional 进行安全的查找OptionalfoundName = names.stream .filter(Objects::nonNull) .filter(name -> name.startsWith("张")) .findFirst;foundName.ifPresentOrElse( name -> System.out.println("找到了:" + name), -> System.out.println("没有找到匹配的名字"));

Java 8 引入的新日期时间 API 解决了旧版 Date 和 Calendar 类的很多问题,比如线程安全、可变性、时区处理等等。

传统的日期处理方式:

// 旧版本的复杂日期处理Calendar cal = Calendar.getInstance;cal.set(2024, Calendar.JANUARY, 15); // 注意月份从0开始Date date = cal.getTime;SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");String dateStr = sdf.format(date); // 线程不安全

使用新的日期时间 API,代码会更简洁:

// 当前日期时间LocalDate today = LocalDate.now; // 2025-09-01LocalTime now = LocalTime.now; // 14:30:25.123LocalDateTime dateTime = LocalDateTime.now; // 2025-09-01T14:30:25.123// 指定的日期时间LocalDate specificDate = LocalDate.of(2025, 09, 01);LocalTime specificTime = LocalTime.of(14, 30, 0);LocalDateTime specificDateTime = LocalDateTime.of(2025, 09, 01, 14, 30, 0);

典型的应用场景是从字符串解析日期,一行代码就能搞定:

// 从字符串解析LocalDate parsedDate = LocalDate.parse("2025-09-01");LocalDateTime parsedDateTime = LocalDateTime.parse("2025-09-01T14:30:25");// 自定义格式解析DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm:ss");LocalDateTime customParsed = LocalDateTime.parse("2025/09/01 14:30:25", formatter);

还有日期和时间的计算,也变得更直观、见名知意:

LocalDate today = LocalDate.now;// 基本的日期计算LocalDate nextWeek = today.plusWeeks(1);LocalDate lastMonth = today.minusMonths(1);LocalDate nextYear = today.plusYears(1);// 时间段计算LocalDate startDate = LocalDate.of(2024, 1, 28);LocalDate endDate = LocalDate.of(2025, 9, 1);Period period = Period.between(startDate, endDate);System.out.println("相差 " + period.getMonths + " 个月 " + period.getDays + " 天");// 精确时间差计算LocalDateTime start = LocalDateTime.now;LocalDateTime end = LocalDateTime.of(2025, 09, 01, 14, 30, 0);Duration duration = Duration.between(start, end);System.out.println("执行时间:" + duration.toMillis + " 毫秒");

还支持时区处理和时间戳处理,不过这段代码就没必要记了,现在有了 AI,直接让它生成时间日期操作就好。

// 带时区的日期时间ZonedDateTime beijingTime = ZonedDateTime.now(ZoneId.of("Asia/Shanghai"));ZonedDateTime newYorkTime = ZonedDateTime.now(ZoneId.of("America/New_York"));// 时区转换ZonedDateTime beijingToNewYork = beijingTime.withZoneSameInstant(ZoneId.of("America/New_York"));// 获取所有可用时区ZoneId.getAvailableZoneIds.stream .filter(zoneId -> zoneId.contains("Shanghai")) .forEach(System.out::println);// 时间戳处理Instant instant = Instant.now;long epochSecond = instant.getEpochSecond;ZonedDateTime fromInstant = instant.atZone(ZoneId.systemDefault);

总之,有了这套 API,我们不需要使用第三方的时间日期处理库,也能解决大多数问题。

Java 8 引入的接口默认方法解决了接口演化的问题。

在默认方法出现之前,如果你想给一个被广泛使用的接口添加新方法,就会影响所有已有的实现类。想象一下,如果要给 Collection 接口添加一个新方法,ArrayList、LinkedList 等所有的实现类都需要修改,成本很大。

默认方法让接口可以在 不破坏现有代码的情况下添加新功能

举个例子,如果想要给接口增加一个 drawWithBorder 方法:

public interface Drawable { // 已有抽象方法 void draw; // 默认方法 default void drawWithBorder { System.out.println("绘制边框"); draw; System.out.println("边框绘制完成"); }}

使用默认方法后,实现类可以选择重写默认方法,也可以直接使用:

// 实现类可以选择重写默认方法public class Circle implements Drawable { @Override public void draw { System.out.println("绘制圆形"); } // 可以重写默认方法 @Override public void drawWithBorder { System.out.println("绘制圆形边框"); draw; }}

Java 8 为 Collection 接口添加了 stream、removeIf 等方法,都是默认方法:

需要注意的是,如果一个类实现多个接口,并且这些接口有相同的默认方法时,需要显式解决冲突:

interface A { default void hello { System.out.println("Hello from A"); }}interface B { default void hello { System.out.println("Hello from B"); }}// 实现类必须重写冲突的方法class C implements A, B { @Override public void hello { // 可以调用特定接口的默认方法 A.super.hello; B.super.hello; // 或者提供自己的实现 System.out.println("Hello from C"); }}

类似的,Java 8 还支持接口的静态方法,前面讲函数式接口的时候有提到。


Java 9

【了解】模块系统

在模块系统出现之前,传统 Java 应用只能依赖 classpath 来管理依赖,所有的类都在同一个类路径下,任何类都可以访问任何其他类,这种 “全局可见性” 在大型项目中会导致代码耦合严重、依赖关系混乱、运行时才发现 ClassNotFoundException 等问题。

模块系统允许我们将代码组织成模块,每个模块都有明确的依赖关系和导出接口,让大型应用的架构变得更加清晰和可维护。

模块系统通过 module-info.java 文件来定义模块的边界,明确声明哪些包对外开放,哪些依赖是必需的,这样就形成了强封装的架构。

比如一个用户管理模块只暴露用户服务接口,而内部的数据访问层对其他模块完全不可见,这种设计让系统的层次结构更加清晰,也避免了意外的跨层调用。

module user.management { // 只导出 service 包,dao 包对外不可见 exports com.company.user.service; // 依赖其他模块 requires java.base; requires database.connection;}

此外,模块系统还带来了更好的性能优化,JVM 可以在启动时只加载必需的模块,减少内存占用和启动时间(适合云原生应用)。

但是,模块系统在企业中用的比较少,目前大多数企业还是使用传统的 Maven/Gradle + JAR 包的方式管理依赖,改造项目的成本 > 模块系统带来的实际收益,所以仅作了解就好。

JShell 是 Java 9 引入的一个交互式工具,在这个工具出现之前,我们要测试一小段 Java 代码,必须创建完整的类和 main 方法,编译后才能运行。

有了 JShell,我们可以像使用 Python 解释器一样使用 Java,对于学习调试有点儿用(但不多)。

直接在命令行输入 jshell 就能使用了:

Java 9 为集合类添加了便捷的工厂方法,能够轻松创建不可变集合。

在这之前,创建不可变集合还是比较麻烦的,很多开发者会选择依赖第三方库(比如 Google Guava)。

传统的不可变集合创建方式:

oldList = new ArrayList;oldList.add("苹果");oldList.add("香蕉");oldList.add("鱼皮");ListimmutableList = Collections.unmodifiableList(oldList);// 或者使用 Google Guava 等第三方库ListguavaList = ImmutableList.of("苹果", "香蕉", "鱼皮");

有了 Java 9 的工厂方法,创建不可变集合简直不要太简单!

fruits = List.of("苹果", "香蕉", "鱼皮");Setnumbers = Set.of(1, 2, 3, 4, 5);Mapscores = Map.of( "张三", 85, "鱼皮", 92, "狗剩", 78);

这些集合是真正不可变的,任何修改操作都会抛出 UnsupportedOperationException 异常。

如果想创建包含大量元素的不可变 Map,可以使用 ofEntries 方法:

MaplargeMap = Map.ofEntries( Map.entry("key1", "value1"), Map.entry("key2", "value2"), Map.entry("key3", "value3") // ... 可以有任意多个);

思考一个问题,如果某个接口中的默认方法需要复用代码,你会怎么做呢?

比如让你来优化下面这段代码:

public interface Calculator { default double calculateRectangleArea(double width, double height) { // 重复的验证逻辑 if (width

你会把重复的验证逻辑写在哪里呢?

答案很简单,写在一个外部工具类里,或者在接口内再写一个通用的验证方法:

public interface Calculator { // 通用的验证方法 default void validate(double x, double y) { if (x

但这种方式存在一个问题,validate 作为 default 方法,它会成为接口的公共 API,所有实现类都能访问到!其实这个方法只需要在接口内可以使用就够了。

Java 9 解决了这个问题,允许在接口中定义私有方法(以及私有静态方法)。

public interface Calculator { // 私有方法 private void validate(double x, double y) { if (x

这样一来,接口内部可以优雅地复用代码,同时保持接口对外的简洁性。

这里也能看出 Java 的演进很谨慎,先允许 default 方法(Java 8),再允许 private 方法(Java 9),每一步都有明确的设计考量。

Java 9 改进了 try-with-resources 语句,在这之前,我们不能在 try 子句中使用外部定义的变量,必须在 try 括号内重新声明,会让代码变得冗余。

// Java 9 之前public void readFile(String filename) throws IOException { BufferedReader reader = Files.newBufferedReader(Paths.get(filename)); try (BufferedReader br = reader) { // 需要重新赋值 br.lines.forEach(System.out::println); }}

Java 9 的改进让代码更加简洁:

// Java 9public void readFile(String filename) throws IOException { BufferedReader reader = Files.newBufferedReader(Paths.get(filename)); try (reader) { // 直接使用 effectively final 变量 reader.lines.forEach(System.out::println); }}

而且还可以同时使用多个变量:

public void processFiles(String file1, String file2) throws IOException { var reader1 = Files.newBufferedReader(Paths.get(file1)); var reader2 = Files.newBufferedReader(Paths.get(file2)); try (reader1; reader2) { // 可以使用多个变量 String line1 = reader1.readLine; String line2 = reader2.readLine; while (line1 != null && line2 != null) { System.out.println(line1 + " | " + line2); line1 = reader1.readLine; line2 = reader2.readLine; } }}


Java 10

用过弱类型编程语言的朋友应该知道,不用自己声明变量的类型有多爽。

但是对于 Java 这种强类型语言,我们经常要写下面这种代码,一个变量类型写老长(特别是在泛型场景下):

Map> complexMap = new HashMap;Iterator>> iterator = complexMap.entrySet.iterator;

好在 Java 10 引入了 var 关键字,支持局部变量的类型推断,编译器会根据初始化表达式自动推断变量的类型,让代码可以变得更简洁。

var complexMap = new HashMap>;var list = new ArrayList;var iterator = complexMap.entrySet.iterator;// 使用 var 的 for-each 循环for (var entry : complexMap.entrySet) { var key = entry.getKey; var value = entry.getValue; // 处理逻辑}

但是,var 关键字是一把双刃剑,不是所有程序员都喜欢它。毕竟代码中都是 var,丢失了一定的可读性,尤其是下面这种代码,你不能直观地了解变量的类型:

var data = getData;

而且使用 var 时,还要确保编译器能正确推断类型,下面这几种写法都是错误的:

所以我个人其实是没那么喜欢用这个关键字的,纯个人偏好。

Java 10 扩展了类数据共享功能,允许应用程序类也参与共享(Application Class-Data Sharing)。在此之前,只有 JDK 核心类可以进行类数据共享,应用程序类每次启动都需要重新加载和解析。

类数据共享的核心思路是:将 JDK 核心类和应用程序类的元数据都打包到共享归档文件中,多个 JVM 实例同时映射同一个归档文件,通过 共享读取 优化应用启动时间和减少内存占用。


⭐️ Java 11

Java 11 是继 Java 8 之后的第二个 LTS 版本,这个版本的重点是提供更好的开发体验和更强大的标准库功能,特别是在字符串处理、文件操作和 HTTP 客户端方面,增加了不少新方法。

HTTP 请求是后端开发常用的能力,之前我们只能基于内置的 HttpURLConnection 自己封装,或者使用 Apache HttpClient、OkHttp 第三方库。

还记得我第一次去公司实习的时候,就看到代码仓库内有很多老员工自己封装的 HTTP 请求代码,写法各异。。。

// 传统的 HttpURLConnection 使用方式URL url = new URL("https://codefather.cn");HttpURLConnection connection = (HttpURLConnection) url.openConnection;connection.setRequestMethod("GET");connection.setRequestProperty("Accept", "application/json");int responseCode = connection.getResponseCode;BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream));// 更多繁琐的代码...

Java 11 将 HTTP 客户端 API 正式化,新的 HTTP 客户端提供了现代化的、支持 HTTP/2 和 WebSocket 的客户端实现,让网络编程变得简单。

// 创建 HTTP 客户端HttpClient client = HttpClient.newBuilder .connectTimeout(Duration.ofSeconds(10)) .followRedirects(HttpClient.Redirect.NORMAL) .build;// 构建 GET 请求HttpRequest getRequest = HttpRequest.newBuilder .uri(URI.create("https://codefather.cn")) .header("Accept", "application/json") .header("User-Agent", "Java-HttpClient") .timeout(Duration.ofSeconds(30)) .GET .build;// POST 请求HttpRequest postRequest = HttpRequest.newBuilder .uri(URI.create("https://api.example.com/users")) .header("Content-Type", "application/json") .POST(HttpRequest.BodyPublishers.ofString(jsonData)) .build;

支持发送同步和异步请求,能够轻松获取响应结果:

// 同步发送请求HttpResponseresponse = client.send(getRequest, HttpResponse.BodyHandlers.ofString);System.out.println("状态码: " + response.statusCode);System.out.println("响应头: " + response.headers.map);System.out.println("响应体: " + response.body);// 异步发送请求client.sendAsync(getRequest, HttpResponse.BodyHandlers.ofString) .thenApply(HttpResponse::body) .thenAccept(System.out::println);

还支持自定义响应处理和 WebSocket 请求:

// 自定义响应处理HttpResponsecustomResponse = client.send(getRequest, responseInfo -> { if (responseInfo.statusCode == 200) { return HttpResponse.BodySubscribers.ofString(StandardCharsets.UTF_8); } else { return HttpResponse.BodySubscribers.discarding; } });// WebSocket 支持WebSocket webSocket = HttpClient.newHttpClient .newWebSocketBuilder .buildAsync(URI.create("ws://localhost:8080/websocket"), new WebSocket.Listener { @Override public void onOpen(WebSocket webSocket) { System.out.println("WebSocket 连接已打开"); webSocket.sendText("Hello WebSocket!", true); } @Override public CompletionStage onText(WebSocket webSocket, CharSequence data, boolean last) { System.out.println("收到消息: " + data); return null; } }) .join;

上面这些代码都不用记,现在直接把接口文档甩给 AI,让它来帮你生成请求代码就好。

Java 11 为 String 类添加了许多实用的方法,让字符串处理变得更加方便。

我估计很多现在学 Java 的同学都已经区分不出来哪些是新增的方法、哪些是老方法了,反正能用就行~

1)基本的字符串检查和处理:

String text = " Hello World \n\n";String emptyText = " ";String multiLine = "第一行\n第二行\n第三行";// isBlank 检查字符串是否为空或只包含空白字符System.out.println(emptyText.isBlank); // trueSystem.out.println("hello".isBlank); // falseSystem.out.println("".isBlank); // true

2)strip 系列方法

相比传统的 trim 更加强大,能够处理 Unicode 空白字符:

// strip 系列方法,去除空白字符System.out.println("'" + text.strip + "'"); // 'Hello World'System.out.println("'" + text.stripLeading + "'"); // 'Hello World \n\n'System.out.println("'" + text.stripTrailing + "'"); // ' Hello World'

3)lines 方法,让多行字符串处理更简单:

// 将字符串按行分割成 StreammultiLine.lines .map(line -> "处理: " + line) .forEach(System.out::println);long lineCount = multiLine.lines.count;System.out.println("总行数: " + lineCount);

4)repeat 方法,可以重复字符串:

System.out.println("Java ".repeat(3)); // "Java Java Java "System.out.println("=".repeat(50)); // 50个等号System.out.println("*".repeat(0)); // 空字符串

即便如此,我还是更喜欢使用 Hutool 或者 Apache Commons 提供的字符串工具类。

提到字符串处理,鱼皮建议大家安装 StringManipulation 插件,便于我们开发时对字符串进行各种转换(比如小写转为驼峰):

Java 11 为文件操作新增了更便捷的方法,不需要使用 FileReader / FileWriter 这种复杂的操作了。

基本的文件读写操作,一个方法搞定:

// 写入文件String content = "这是一个测试文件\n包含多行内容\n中文支持测试";Path tempFile = Files.writeString( Paths.get("temp.txt"), content, StandardCharsets.UTF_8);// 读取文件String readContent = Files.readString(tempFile, StandardCharsets.UTF_8);System.out.println("读取的内容:\n" + readContent);

支持流式读取文件,适合文件较大的场景:

try (Streamlines = Files.lines(tempFile)) { lines.filter(line -> !line.isBlank) .map(String::trim) .forEach(System.out::println);}

Java 11 为 Optional 类添加了 isEmpty 方法,和之前的 isPresent 正好相反,让空值检查更直观。

Java 12 和 13 主要引入了一些预览特性,其中最重要的是 Switch 表达式和文本块,这些特性在后续版本中得到了完善和正式化。

Java 14

Java 14 将 Switch 表达式正式化,并引入了 Records、instanceof 模式匹配作为预览特性。

Java 14 将 Switch 表达式转正了,让条件判断变得更简洁和安全。

在这之前,传统的 switch 语句存在不少问题,比如需要手动添加 break 防止穿透、赋值不方便等:

String dayType;switch (day) { case MONDAY: case TUESDAY: case WEDNESDAY: case THURSDAY: case FRIDAY: dayType = "工作日"; break; case SATURDAY: case SUNDAY: dayType = "周末"; break; default: dayType = "未知"; break;}// 赋值不方便int score;switch (grade) { case 'A': System.out.println("优秀!"); score = 90; break; case 'B': score = 80; break; default: score = 0;}

在 Java 14 之后,可以直接这么写:

// Java 14 的简洁写法String dayType = switch (day) { case MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY -> "工作日"; case SATURDAY, SUNDAY -> "周末"; default -> "未知";};// 支持复杂逻辑的 yield 关键字int score = switch (grade) { case 'A' -> { System.out.println("优秀!"); yield 90; // 使用 yield 返回值 } case 'B' -> 80; default -> 0;};

上述代码中,我们使用了 Switch 表达式增强的几个特性:

箭头语法:使用 -> 替代冒号,自动防止 fall-through(不用写 break 了)多标签支持:case A, B, C -> 一行处理多个条件表达式求值:可以直接使用 yield 关键字返回值并赋给变量

这样一来,多条件判断变得更优雅了!还能避免忘记 break 导致的逻辑错误。

// 实际应用示例:根据月份判断季节String season = switch (month) { case 12, 1, 2 -> "冬季"; case 3, 4, 5 -> "春季"; case 6, 7, 8 -> "夏季"; case 9, 10, 11 -> "秋季"; default -> throw new IllegalArgumentException("无效月份: " + month);};

Java 14 改进了 NullPointerException 的错误信息。JVM 会提供更详细的堆栈跟踪信息,指出导致异常的具体位置和原因,让调试变得更加容易。

Java 15

Java 15 将文本块正式化,新增了 Hidden 隐藏类,并引入了 Sealed 类作为预览特性。

这可能是我最喜欢的特性之一了,因为之前每次复制多行文本到代码中,都会给我转成这么一坨:

需要大量的字符串拼接、转义字符,对于 HTML、SQL 和 JSON 格式来说简直是噩梦了。

有了 Java 15 的文本块特性,多行字符串简直不要太爽!直接用三个引号 """ 括起来,就能以字符串本来的格式展示。

文本块会保持代码的缩进、而且内部的引号不需要转义。

配合 String 的格式化方法,就能轻松传入参数生成复杂的字符串模板:

Java 15 引入了 Hidden 隐藏类特性,这是一个 专为框架和运行时环境设计 的底层机制,主要是为了优化 动态生成短期类(比如 Lambda 表达式、动态代理)的性能问题,普通开发者无需关心。

在 Lambda 表达式、AOP 动态代理、ORM 映射等场景中,框架会动态生成代码载体(比如方法句柄、临时代理类),这些载体需要关联类的元数据才能运行。如果生成频繁,传统类的元数据会被类加载器追踪,需要等待类加载器卸载才能回收,导致元空间堆积和 GC 压力。

Hidden 类的特点是对其定义类加载器之外的所有代码都不可见,由于不可发现且链接微弱,JVM 垃圾回收器能够更高效地卸载隐藏类及其元数据,从而防止短期类堆积对元空间造成压力,优化了需要动态生成大量类的性能。

Java 16

Java 16 正式发布了 Records 和 instanceof 模式匹配这 2 大特性,让代码更简洁易读。

以前,我们如果想创建一个 POJO 对象来存一些数据,需要编写大量的样板代码,包括构造函数、getter 方法、equals、hashCode 等等,比较麻烦。

// Java 16 之前创建数据类的方式public class Person { private final String name; private final int age; private final String email; public Person(String name, int age, String email) { this.name = name; this.age = age; this.email = email; } public String getName { return name; } public int getAge { return age; } public String getEmail { return email; } @Override public boolean equals(Object obj) { if (this == obj) return true; if (obj == null || getClass != obj.getClass) return false; Person person = (Person) obj; return age == person.age && Objects.equals(name, person.name) && Objects.equals(email, person.email); } @Override public int hashCode { return Objects.hash(name, age, email); }}

即使通过 Lombok 插件简化了代码,估计也要十几行。

有了 Java 16 的 Records,创建数据包装类简直不要太简单,一行代码搞定:

public record Person(String name, int age, String email) {}

Records 自动提供了所有必需的方法,使用方式完全一样!

Person person = new Person("鱼皮", 25, "yupi@yuyuanweb.com");System.out.println(person.name); // 自动生成的访问器System.out.println(person.age);System.out.println(person.email);System.out.println(person.toString); // 自动生成的 toString

此外,Records 还支持自定义方法和验证逻辑,只不过个人建议这种情况下不如老老实实用 “类” 了。

public record BankAccount(String accountNumber, double balance) { // 构造函数中添加验证 public BankAccount { if (balance 100000; } // 静态工厂方法 public static BankAccount createSavingsAccount(String accountNumber) { return new BankAccount(accountNumber, 0.0); }}

Java 16 正式推出了 instanceof 的模式匹配,让类型检查和转换变得更优雅。

传统的 instanceof 使用方式,需要显示转换对象类型:

Object obj = xxx;if (obj instanceof String) { String str = (String) obj; // 需要显式转换 return "字符串长度: " + str.length;}

有了 instanceof 模式匹配,可以直接在匹配类型时声明变量:

if (obj instanceof String str) { return "字符串长度: " + str.length;}

但是要注意,str 变量的作用域被限定在 if 条件为 true 的代码块中,符合最小作用域原则。

// 传统写法Listresult = stream .filter(s -> s.length > 3) .collect(Collectors.toList);// Java 16 简化写法Listresult = stream .filter(s -> s.length > 3) .toList; // 返回不可变 List

还提供了 mapMulti 方法,跟 flatMap 的作用一样,将一个元素映射为 0 个或多个元素,但是某些场景下比 flatMap 更灵活高效。

当需要从一个元素生成多个元素时,flatMap 需要先创建一个中间 Stream,而 mapMulti 可以通过传入的 Consumer 直接 “推送” 多个元素,避免了中间集合或 Stream 的创建开销。

words = List.of("hello", "world", "java");Listchars = words.stream .flatMap(word -> word.chars .mapToObj(c -> (char) c)) .toList;// Java 16 的 mapMulti 方式Listchars = words.stream .mapMulti((word, consumer) -> { for (char c : word.toCharArray) { consumer.accept(c); // 直接向下游推送元素 } }) .toList;

OK 以上就是本期内容, Java 17~24 新特性记得看新文章,学会的话记得点赞三连支持,我们下期见。

编程学习交流:编程导航:https://www.codefather.cn/

简历快速制作:老鱼简历:https://laoyujianli.com

✏️ 面试刷题神器:面试鸭:https://mianshiya.com

AI 学习指南:AI知识库:https://ai.codefather.cn/

来源:程序员鱼皮

相关推荐