Java新特性
约 5031 字大约 17 分钟
2025-01-15
Java17 新特性
JDK17 是继 JDK8 和 JDK11 之后的一个最新支持稳定版本
- Spring6 和 SpringBoot3 最低支持 JDK17 ,不支持 JDK8;
- JDK17 增加了文本块、switch 类型匹配、密封类 sealed class ,彻底删除了 AOT 和 JIT 编译器
文本块
在 Java17 之前的版本里,当定义一个字符串,比如一个 JSON 数据:
public void lowVersion() {
String text = "{\n" +
" \"name\": \"Java\",\n" +
" \"age\": 18,\n" +
" \"address\": \"北京市\"\n" +
"}";
System.out.println(text);
}这种方式定义具有几个问题:
- 双引号需要进行转义;
- 为了字符串的可读性需要通过+号连接;
Java17 新增的文本块语法,通过三个双引号可以定义一个文本块,并且结束的三个双引号不能和开始的在同一行,可读性更好
private void highVersion() {
String text = """
{
"name": "小黑",
"age": 18,
"address": "北京市"
}
""";
System.out.println(text);
}输出结果为:
{
"name": "小黑",
"age": 18,
"address": "北京市"
}switch 表达式
在 JDK17 之前版本里,swith 没有返回值,每个 case 都需要加上 break ,代码冗余,如下
private static void lowVesion(Fruit fruit) {
switch (fruit) {
case APPLE, PEAR:
System.out.println("普通水果");
break;
case MANGO, AVOCADO:
System.out.println("进口水果");
break;
default:
System.out.println("未知水果");
}
}JDK17 版本之后,将冒号(:)替换为箭头(->),并且 switch 表达式默认不会失败,所以不需要 break
private static void withSwitchExpression(Fruit fruit) {
switch (fruit) {
case APPLE, PEAR -> System.out.println("普通水果");
case MANGO, AVOCADO -> System.out.println("进口水果");
default -> System.out.println("未知水果");
}
}同时,switch 表达式也可以返回一个值,如下:
private static void withReturnValue(Fruit fruit) {
String text = switch (fruit) {
case APPLE, PEAR -> "普通水果";
case MANGO, AVOCADO -> "进口水果";
default -> "未知水果";
};
System.out.println(text);
}补充:如果需要在 case 里面做多件事情,在返回值之前进行打印挫折,可以通过大括号来作为 case 块,最后的返回值使用关键字 yield 进行返回
private static void withYield(Fruit fruit) {
String text = switch (fruit) {
case APPLE, PEAR -> {
System.out.println("给的水果是: " + fruit);
yield "普通水果";
}
case MANGO, AVOCADO -> "进口水果";
default -> "未知水果";
};
System.out.println(text);
}移除 AOT 和 JIT 编译器
在 JDK 17 中,Java 官方决定移除 AOT(Ahead-Of-Time)和 JIT(Just-In-Time)编译器
- AOT 编译器的使用率较低:尽管 AOT 编译器(允许在应用程序启动之前进行编译)最初被提出作为提升启动速度和减少运行时编译的一个解决方案,但实际上,很多开发者和应用程序并没有广泛采用它。性能提升不如预期,因此在 JDK 17 中被移除
- 技术复杂性和维护成本:AOT 编译器的开发和维护需要额外的精力,同时也增加了 JDK 的复杂度。因为 AOT 和 JIT 编译器都试图做类似的事情,在 JDK 17 中移除 AOT 可以减少重复的功能和维护负担。
新增伪随机数生成器:RandomGenerator
在 JDK 17 中,Java 引入了新的伪随机数生成器接口 RandomGenerator。它提供了比传统 java.util.Random 更加灵活和强大的功能。新的 API 提供了更广泛的随机数生成选项,包括多种算法和更高效的实现。
特性
- 支持不同的随机数生成算法。
- 提供更好的性能和可扩展性。
- 更容易与现有的随机数生成器进行交互。
import java.util.random.RandomGenerator;
RandomGenerator generator = RandomGenerator.of("L64X128MixRandom");
int randomInt = generator.nextInt();废弃 Applet
在 JDK 17 中,Applet API 被正式标记为废弃。Applet 是一种过时的技术,它用于在浏览器中运行小型 Java 应用程序。现代 Web 技术,如 HTML5、JavaScript、以及各种前端框架,已经取代了 Applet 的功能。
Java8 新特性
- Lambda 表达式:Lambda 允许函数作为参数传递到方法中
- Stream API:新添加的 Stream API(java.util.stream) 把真正的函数式编程风格引入到 Java 中
- 方法引用:方法引用提供了非常有用的语法,可以直接引用已有 Java 类或对象(实例)的方法或构造器。与 lambda 联合使用,方法引用可以使语言的构造更紧凑简洁,减少默认方法,默认方法就是一个在接口里面有了一个实现的方法
- Date Time API: 加强对日期与时间的处理
- Optional 类: Optional 类已经成为 Java 8 类库的一部分,用来解决空指针异常
Lambda 表达式
函数式思想
- 在数学中,函数就是有输入量、输出量的一套计算方案,也就是“拿数据做操作”
- 面向对象思想强调“必须通过对象的形式来做事情”
- 函数式思想则尽量忽略面向对象的复杂语法:“强调做什么,而不是以什么形式去做” 而我们要学习的 Lambda 表达式就是函数式思想的体现
举例
需求:启动一个线程,在控制台输出一句话:多线程程序启动了
- 创建对象
定义一个类 MyRunnable 实现 Runnable 接口,并重写 run()方法
//创建MyRunnable类的对象
class MyRunnable implements Runnable{
@Override
public void run() {
System.out.println("多线程程序启动了...");
}
}
public class LambdaDemo {
public static void main(String[] args) {
//实现类的方式实现需求
MyRunnable my = new MyRunnable();
//创建Thread类的对象,把MyRunnable的对象作为构造参数传递
Thread t = new Thread(my);
//启动线程
t.start();
}
}- 匿名内部类
匿名内部类中重写 run()方法的代码分析
- 方法形式参数为空,说明调用方法时不需要传递参数
- 方法返回值类型为 void,说明方法执行没有结果返回
- 方法体中的内容,是我们具体要做的事情
public class LambdaDemo {
public static void main(String[] args) {
//匿名内部类的方式改进
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("多线程程序启动了...");
}
}).start();
}
}- Lambda
public class LambdaDemo {
public static void main(String[] args) {
//Lambda表达式改进
new Thread(() -> {
System.out.println("多线程程序启动了...");
}).start();
}
}标准格式
组成 Lambda 表达式的三要素:形式参数,箭头,代码块
Lambda 表达式的格式
- 格式:(形式参数) -> {代码块}
- 形式参数:如果有多个参数,参数之间用逗号隔开;如果没有参数,留空即可
- ->:由英文中画线和大于符号组成,固定写法。代表指向动作
- 代码块:是我们具体要做的事情,也就是以前我们写的方法体内容
Lambda 表达式的使用前提
- 有一个接口
- 接口中有且仅有一个抽象方法
1、抽象方法无参无返回值
- 定义一个接口(Eatable),里面定义一个抽象方法:void eat();
public interface Eatable {
void eat();
}Eatable e 是一个接口,并只含有一个抽象方法,无参无返回值方法:void eat()
Lambda 表达式:
public class EatableDemo {
public static void main(String[] args) {
useEatable(() -> {
System.out.println("一天一苹果,医生远离我");
});
}
private static void useEatable(Eatable e){
e.eat();
}
}2、抽象方法带参无返回值
定义一个接口(Flyable),里面定义一个抽象方法,void fly(String s);
public interface Flyable{
void fly(String s);
}Lambda 表达式:
public class FlyableDemo {
public static void main(String[] args) {
useFlyable((String s)-> {
System.out.println(s);
System.out.println("飞机自驾游");
});
}
private static void useFlyable(Flyable f){
f.fly("风和日丽,晴空万里");
}
}省略模式规则
- 参数类型可以省略。但是有多个参数的情况下,不能只省略一个
- 如果参数有且仅有一个,那么小括号可以省略
- 如果代码块的语句只有一条,可以省略大括号和分号,甚至是 return
举例如下
- 定义一个接口(Flyable),里面定义一个抽象方法:void fly(String s);
public interface Flyable{
void fly(String s);
}- 定义一个接口(Addable),里面定义一个抽象方法:int add(int x,int y);
public interface Addable {
int add(int x,int y);
}
/*Lambda表达式的省略模式*/
public class LambdaDemo {
public static void main(String[] args) {
//useAddable((int x,int y) -> {
// return x+y;
//});
//1.参数的类型可以省略,但是有多个参数的情况下,不能只省略一个
useAddable((x,y) -> {
return x+y;
});
//useFlyable((String s) ->{
// System.out.println(s);
//});
//2.如果参数有且只有一个,那么小括号可以省略
useFlyable( s->{
System.out.println(s);
});
//3.如果代码块的语句只有一条,可以省略大括号和分号
useFlyable(s -> System.out.println(s));
//4.如果代码块的语句只有一条,可以省略大括号和分号;如果有return,return也要省略
useAddable((x,y) -> x+y );
}
private static void useFlyable(Flyable f){
f.fly("风和日丽,晴空万里");
}
private static void useAddable(Addable a){
int sum = a.add(10,20);
System.out.println(sum);
}
}Lambda 表达式与匿名内部类区别:
| 匿名内部类 | lambda | |
|---|---|---|
| 创建的对象类型 | 接口、抽象类、具体类 | 接口 |
| 使用限制 | 接口可有一个或多个抽象方法 | 接口有且只有一个抽象方法 |
| 实现原理 | 编译之后,产生一个单独的.class 字节码文件 | 编译之后,没有一个单独的.class 字节码文件。对应的字节码会在运行的时候动态生成 |
方法引用
介绍
定义:方法引用是指通过方法的名字来指向一个方法
优点:方法引用可以使语言的构造更紧凑简洁,减少冗余代码
与 lambda 的比较:方法引用(MethodReference)是 Lambda 表达式的另一种格式,在某些场景下可以提高代码的可读性
类型
方法引用可以分为分四类:调用类的静态方法、调用传入的实例参数的方法、调用已经存在的实例的方法、调用类的构造函数
以下将从四种类型的方法引用出发,探讨 lambda 与方法引用的区分点
- 调用类的静态方法
语法
//lambda
(args) -> Class.staticMethod(args)
//方法引用
Class::static_method符合上面形式的调用,不管有多少参数,都省略掉,编译器自动会帮我们传入
举例
// Lambda 表达式
Function<Integer, String> intToStringLambda = (num) -> String.valueOf(num);
// 方法引用
Function<Integer, String> intToString = String::valueOf;
Function<Integer, String>表示一个函数式接口(Functional Interface),它接受一个 Integer 类型的参数,并返回一个 String 类型的结果。具体来说,
Function<Integer, String>是 Java 中的函数式接口,它包含一个抽象方法apply,用于将输入的 Integer 类型参数转换为输出的 String 类型结果。在这种情况下,intToString就是一个函数,可以将 Integer 类型的值转换为 String 类型的值。
- 调用传入的实例参数的方法
语法
//lambda
(obj, args) -> obj.instanceMethod(args)
//方法引用
ObjectType::instanceMethod举例
String str = "Hello World";
// Lambda 表达式
BiFunction<String, Integer, String> substringLambda = (str, index) -> str.substring(index);
// 方法引用
BiFunction<String, Integer, String> substring = String::substring;实现
BiFunction接口的apply方法,直接引用了String类的substring方法。
- 调用已经存在的实例的方法
语法
//lambda
(args) -> obj.instanceMethod(args)
//方法引用
obj::instanceMethod举例
// Lambda 表达式
String str = "Hello World";
Supplier<Integer> strLengthLambda = () -> str.length();
// 方法引用
String str = "Hello World";
Supplier<Integer> strLength = str::length;
Supplier是 Java 中的一个函数式接口,它不接受任何参数,但返回一个结果。
- 调用类的构造函数
语法
//lambda
(args) -> new ClassName(args)
//方法引用
ClassName::new举例
// Lambda 表达式
Supplier<List<String>> listSupplierLambda = () -> new ArrayList<>();
// 方法引用
Supplier<List<String>> listSupplier = ArrayList::new;
Supplier是 Java 中的一个函数式接口,它不接受任何参数,但返回一个结果。
属于调用类的静态方法一类
Stream 流
介绍
Stream 是一种用于处理集合(如集合类 List, Set)的高级抽象,它允许以声明性方式处理数据。Stream API 的核心作用是通过函数式编程风格(如过滤、映射、减少等)来简化集合操作,提升代码的可读性和可维护性。
作用:
- 简化数据处理:一种声明性、函数式的方式处理集合数据,而无需编写显式的循环和条件判断代码;
filter()进行条件过滤,使用map()进行数据转换,使用reduce()进行归约计算 - 简化代码:使用
Stream进行多个操作时,可以直接通过链式调用来完成,无需显式创建和更新中间变量
要想操作流,首先需要有一个数据源,可以是数组或者集合。每次操作都会返回一个新的流对象,方便进行链式操作,但原有的流对象会保持不变。
流的操作可以分为两种类型:
- 中间操作,可以有多个,每次返回一个新的流,可进行链式操作
- 终端操作,只能有一个,每次执行完,这个流也就用光光了,无法执行下一个操作,因此只能放在最后
来举个例子。
List<String> list = new ArrayList<>();
list.add("武汉加油");
list.add("中国加油");
list.add("世界加油");
list.add("世界加油");
long count = list.stream().distinct().count();
System.out.println(count);- 中间操作
distinct() 方法是一个中间操作(去重),它会返回一个新的流(没有共同元素)。
- 终端操作
count() 方法是一个终端操作,返回流中的元素个数
long count();中间操作不会立即执行,只有等到终端操作的时候,流才开始真正地遍历,用于映射、过滤等。通俗点说,就是一次遍历执行多个操作,性能就大大提高了。
举例
- 创建流
如果是数组的话,可以使用 Arrays.stream() 或者 Stream.of() 创建流;
如果是集合的话,可以直接使用 stream() 方法创建流,因为该方法已经添加到 Collection 接口中。
public class CreateStreamDemo {
public static void main(String[] args) {
String[] arr = new String[]{"武汉加油", "中国加油", "世界加油"};
Stream<String> stream = Arrays.stream(arr);
stream = Stream.of("武汉加油", "中国加油", "世界加油");
List<String> list = new ArrayList<>();
list.add("武汉加油");
list.add("中国加油");
list.add("世界加油");
stream = list.stream();
}
}查看 Stream 源码的话,你会发现 of() 方法内部其实调用了 Arrays.stream() 方法。
public static<T> Stream<T> of(T... values) {
return Arrays.stream(values);
}- 操作流
Stream 类提供了很多有用的操作流的方法
- filter() :过滤,可以从流中筛选出我们想要的元素
- map() :把一个流中的元素转化成新的流中的元素
- macth :匹配,Stream 类提供了三个方法可供进行元素匹配:
anyMatch(),只要有一个元素匹配传入的条件,就返回 trueallMatch(),只有有一个元素不匹配传入的条件,就返回 false;如果全部匹配,则返回 truenoneMatch(),只要有一个元素匹配传入的条件,就返回 false;如果全部不匹配,则返回 true
- reduce() :将 Stream 中的元素组合起来
举例1:
public class FilterStreamDemo {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("周杰伦");
list.add("王力宏");
list.add("陶喆");
list.add("林俊杰");
Stream<String> stream = list.stream().filter(element -> element.contains("王"));
stream.forEach(System.out::println);
}
}输出结果:
王力宏举例2:
public class MapStreamDemo {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("周杰伦");
list.add("王力宏");
list.add("陶喆");
list.add("林俊杰");
Stream<Integer> stream = list.stream().map(String::length);
stream.forEach(System.out::println);
}
}map() 方法接收的是一个 Function(Java 8 新增的一个函数式接口,接受一个输入参数 T,返回一个结果 R)类型的参数,此时参数 为 String 类的 length 方法(该类型为调用传入的实例参数的方法),也就是把 Stream<String> 的流转成一个 Stream<Integer> 的流。
- 输出结果:
3
3
2
3举例3
public class MatchStreamDemo {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("周杰伦");
list.add("王力宏");
list.add("陶喆");
list.add("林俊杰");
boolean anyMatchFlag = list.stream().anyMatch(element -> element.contains("王"));
boolean allMatchFlag = list.stream().allMatch(element -> element.length() > 1);
boolean noneMatchFlag = list.stream().noneMatch(element -> element.endsWith("沉"));
System.out.println(anyMatchFlag);
System.out.println(allMatchFlag);
System.out.println(noneMatchFlag);
}
}因为“王力宏”以“王”字开头,所以 anyMatchFlag 应该为 true;因为“周杰伦”、“王力宏”、“陶喆”、“林俊杰”的字符串长度都大于 1,所以 allMatchFlag 为 true;因为 4 个字符串结尾都不是“沉”,所以 noneMatchFlag 为 true。
- 输出结果
true
true
true举例四:
public class ReduceStreamDemo {
public static void main(String[] args) {
Integer[] ints = {0, 1, 2, 3};
List<Integer> list = Arrays.asList(ints);
Optional<Integer> optional = list.stream().reduce((a, b) -> a + b); //lambda写法
Optional<Integer> optional1 = list.stream().reduce(Integer::sum); //方法引用写法
System.out.println(optional.orElse(0)); //6
System.out.println(optional1.orElse(0)); //6
int reduce = list.stream().reduce(6, (a, b) -> a + b); //有起始值6
System.out.println(reduce);
int reduce1 = list.stream().reduce(6, Integer::sum); //有起始值6
System.out.println(reduce1);
}
}运算规则可以是 Lambda 表达式(比如 (a, b) -> a + b),也可以是类名::方法名(比如 Integer::sum)。
程序运行的结果如下所示:
0、1、2、3 在没有起始值相加的时候结果为 6;有起始值 6 的时候结果为 12。
6
6
12
12- 转换流
既然可以把集合或者数组转成流,那么也应该有对应的方法,将流转换回去——collect() 方法就满足了这种需求。
public class CollectStreamDemo {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("周杰伦");
list.add("王力宏");
list.add("陶喆");
list.add("林俊杰");
String[] strArray = list.stream().toArray(String[]::new);
System.out.println(Arrays.toString(strArray)); //[周杰伦, 王力宏, 陶喆, 林俊杰]
List<Integer> list1 = list.stream().map(String::length).collect(Collectors.toList());
List<String> list2 = list.stream().collect(Collectors.toCollection(ArrayList::new));
System.out.println(list1); //[3, 3, 2, 3]
System.out.println(list2); //[周杰伦, 王力宏, 陶喆, 林俊杰]
String str = list.stream().collect(Collectors.joining(", ")).toString();
System.out.println(str); //周杰伦, 王力宏, 陶喆, 林俊杰
}
}toArray() 方法可以将流转换成数组,你可能比较好奇的是 String[]::new,它是什么东东呢?来看一下 toArray() 方法的源码。
<A> A[] toArray(IntFunction<A[]> generator);也就是说 String[]::new 是一个 IntFunction,一个可以产生所需的新数组的函数,可以通过反编译字节码看看它到底是什么:
String[] strArray = (String[])list.stream().toArray((x$0) -> {
return new String[x$0];
});
System.out.println(Arrays.toString(strArray));也就是相当于返回了一个指定长度的字符串数组。
当我们需要把一个集合按照某种规则转成另外一个集合的时候,就可以配套使用 map() 方法和 collect() 方法。
List<Integer> list1 = list.stream().map(String::length).collect(Collectors.toList());通过 stream() 方法创建集合的流后,再通过 map(String:length) 将其映射为字符串长度的一个新流,最后通过 collect() 方法将其转换成新的集合。
Collectors 是一个收集器的工具类,内置了一系列收集器实现,比如说 toList() 方法将元素收集到一个新的 java.util.List 中;比如说 toCollection() 方法将元素收集到一个新的 java.util.ArrayList 中;比如说 joining() 方法将元素收集到一个可以用分隔符指定的字符串中。
- 输出结果
[周杰伦, 王力宏, 陶喆, 林俊杰]
[3, 3, 2, 3]
[周杰伦, 王力宏, 陶喆, 林俊杰]
周杰伦, 王力宏, 陶喆, 林俊杰函数式接口
定义
- 函数式接口(Functional Interface)就是一个有且仅有一个抽象方法,但是可以有多个非抽象方法的接口。
- 函数式接口可以被隐式转换为 lambda 表达式。
- Lambda 表达式和方法引用(实际上也可认为是 Lambda 表达式)上。
如定义了一个函数式接口如下:
@FunctionalInterface
interface GreetingService
{
void sayMessage(String message);
}那么就可以使用 Lambda 表达式来表示该接口的一个实现(注:JAVA 8 之前一般是用匿名类实现的):
GreetingService greetService1 = message -> System.out.println("Hello " + message);函数式接口可以对现有的函数友好地支持 lambda。
JDK 1.8 之前已有的函数式接口:
- java.lang.Runnable
- java.util.concurrent.Callable
- java.security.PrivilegedAction
- java.util.Comparator
- java.io.FileFilter
- java.nio.file.PathMatcher
- java.lang.reflect.InvocationHandler
- java.beans.PropertyChangeListener
- java.awt.event.ActionListener
- javax.swing.event.ChangeListener
JDK1.8 新增加的函数接口:
- java.util.function
java.util.function 它包含了很多类,用来支持 Java 的函数式编程,该包中的函数式接口有:
举例
可以用注解**@FunctionalInterface**自定义一个函数式接口。一旦定义了功能接口,就只能有一个抽象方法。由于您只有一个抽象方法,因此您可以编写多个静态方法和默认方法。
下面是为两个数字相乘而编写的 FunctionalInterface 的编程示例。
@FunctionalInterface
interface FuncInterface {
public int multiply(int a, int b);
}
public class Java8 {
public static void main(String args[]) {
FuncInterface Total = (a, b) -> a * b;
System.out.println("Result: "+Total.multiply(30, 60));
}
}版权所有
版权归属:haipeng-lin