Java8新特性
约 4901 字大约 16 分钟
2025-01-15
1.新特性列表
- Lambda 表达式:Lambda 允许把函数作为一个方法的参数(函数作为参数传递到方法中)
- Stream API:新添加的Stream API(java.util.stream) 把真正的函数式编程风格引入到Java中
- 方法引用:方法引用提供了非常有用的语法,可以直接引用已有Java类或对象(实例)的方法或构造器。与lambda联合使用,方法引用可以使语言的构造更紧凑简洁,减少默认方法,默认方法就是一个在接口里面有了一个实现的方法
- Date Time API: 加强对日期与时间的处理
- Optional 类: Optional 类已经成为 Java 8 类库的一部分,用来解决空指针异常。
2.Lambda 表达式
2.1 函数式思想
- 在数学中,函数就是有输入量、输出量的一套计算方案,也就是“拿数据做操作”
- 面向对象思想强调“必须通过对象的形式来做事情”
- 函数式思想则尽量忽略面向对象的复杂语法:“强调做什么,而不是以什么形式去做” 而我们要学习的Lambda表达式就是函数式思想的体现
2.2 举例
需求:启动一个线程,在控制台输出一句话:多线程程序启动了
2.2.1 创建对象
定义一个类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();
}
}
2.2.2 匿名内部类
- 匿名内部类中重写run()方法的代码分析
- 方法形式参数为空,说明调用方法时不需要传递参数
- 方法返回值类型为void,说明方法执行没有结果返回
- 方法体中的内容,是我们具体要做的事情
public class LambdaDemo {
public static void main(String[] args) {
//匿名内部类的方式改进
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("多线程程序启动了...");
}
}).start();
}
}
2.2.3 Lambda
public class LambdaDemo {
public static void main(String[] args) {
//Lambda表达式改进
new Thread(() -> {
System.out.println("多线程程序启动了...");
}).start();
}
}
2.3 Lambda表达式的标准格式
- 组成Lambda表达式的三要素:形式参数,箭头,代码块
- Lambda表达式的格式
- 格式:(形式参数) -> {代码块}
- 形式参数:如果有多个参数,参数之间用逗号隔开;如果没有参数,留空即可
- ->:由英文中画线和大于符号组成,固定写法。代表指向动作
- 代码块:是我们具体要做的事情,也就是以前我们写的方法体内容
- Lambda表达式的使用前提
- 有一个接口
- 接口中有且仅有一个抽象方法
2.4 练习
2.4.1 抽象方法无参无返回值
- 定义一个接口(Eatable),里面定义一个抽象方法:void eat();
public interface Eatable {
void eat();
}
- 定义一个测试类(EatableDemo),在测试类中提供两个方法
- 一个方法是:useEatable(Eatable e)
- 一个方法是主方法,在主方法中调用useEatable方法
通过实现类方法
实现类实现Eatable,并重写eat方法
class EatableImpl implements Eatable{
@Override
public void eat() {
System.out.println("一天一苹果,医生远离我");
}
}
public class EatableDemo {
public static void main(String[] args) {
//在主方法中调用useEatable方法
Eatable e = new EatableImpl();
useEatable(e);
}
private static void useEatable(Eatable e){
e.eat();
}
}
匿名内部类
public class EatableDemo {
public static void main(String[] args) {
//匿名内部类
useEatable(new Eatable() {
@Override
public void eat() {
System.out.println("一天一苹果,医生远离我");
}
});
}
private static void useEatable(Eatable e){
e.eat();
}
}
lambda
Eatable e是一个接口,并只含有一个抽象方法,无参无返回值方法:void eat()
以下lambda实现方式相当于实现该接口,并实现该方法
public class EatableDemo {
public static void main(String[] args) {
//用Lambda表达式
useEatable(() -> {
System.out.println("一天一苹果,医生远离我");
});
}
private static void useEatable(Eatable e){
e.eat();
}
}
2.4.2 抽象方法带参无返回值
- 定义一个接口(Flyable),里面定义一个抽象方法,void fly(String s);
public interface Flyable{
void fly(String s);
}
- 定义一个测试类(FlyableDemo),在测试类中提供两个方法
- 一个方法是:useFlyable(Flyable f)
- 一个方法是主方法,在主方法中调用useFlyable方法
匿名内部类
public class FlyableDemo {
public static void main(String[] args) {
//在主方法中调用useFlyable方法
//匿名内部类
useFlyable(new Flyable() {
@Override
public void fly(String s) {
System.out.println(s);
System.out.println("飞机自驾游");
}
});
}
private static void useFlyable(Flyable f){
f.fly("风和日丽,晴空万里");
}
}
lambda
public class FlyableDemo {
public static void main(String[] args) {
//Lambda
useFlyable((String s)-> {
System.out.println(s);
System.out.println("飞机自驾游");
});
}
private static void useFlyable(Flyable f){
f.fly("风和日丽,晴空万里");
}
}
2.5 Lambda的省略模式
省略规则:
- 参数类型可以省略。但是有多个参数的情况下,不能只省略一个
- 如果参数有且仅有一个,那么小括号可以省略
- 如果代码块的语句只有一条,可以省略大括号和分号,甚至是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);
}
}
2.6 lambad表达式与匿名内部类的区别
匿名内部类 | lambda | |
---|---|---|
创建的对象类型 | 接口、抽象类、具体类 | 接口 |
使用限制 | 接口可有一个或多个抽象方法 | 接口有且只有一个抽象方法 |
实现原理 | 编译之后,产生一个单独的.class字节码文件 | 编译之后,没有一个单独的.class字节码文件。对应的字节码会在运行的时候动态生成 |
3.方法引用
3.1 介绍
- 定义:方法引用是指通过方法的名字来指向一个方法
- 优点:方法引用可以使语言的构造更紧凑简洁,减少冗余代码
- 与lambda的比较:方法引用(MethodReference)是Lambda表达式的另一种格式,在某些场景下可以提高代码的可读性
3.2 方法引用的类型
方法引用可以分为分四类:调用类的静态方法、调用传入的实例参数的方法、调用已经存在的实例的方法、调用类的构造函数
以下将从四种类型的方法引用出发,探讨lambda与方法引用的区分点
3.2.1 调用类的静态方法
- 语法
//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 类型的值。
3.2.2 调用传入的实例参数的方法
- 语法
//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
方法。
3.2.3 调用已经存在的实例的方法
- 语法
//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 中的一个函数式接口,它不接受任何参数,但返回一个结果。
3.2.4 调用类的构造函数
- 语法
//lambda
(args) -> new ClassName(args)
//方法引用
ClassName::new
- 举例
// Lambda 表达式
Supplier<List<String>> listSupplierLambda = () -> new ArrayList<>();
// 方法引用
Supplier<List<String>> listSupplier = ArrayList::new;
Supplier
是 Java 中的一个函数式接口,它不接受任何参数,但返回一个结果。
3.3 练习
3.3.1 练习一
import java.util.List;
import java.util.ArrayList;
public class Java8Tester {
public static void main(String args[]){
List<String> names = new ArrayList();
names.add("Google");
names.add("Runoob");
names.add("Taobao");
names.add("Baidu");
names.add("Sina");
names.forEach(System.out::println);
}
}
实例中我们将 System.out::println 方法作为静态方法来引用。
属于调用类的静态方法一类
结果:
Google
Runoob
Taobao
Baidu
Sina
3.3.2 练习二
Consumer<String> consumer1 = new Consumer<String>() {
@Override
public void accept(String s) {
System.out.println(s);
}
};
//lambda表达式
Consumer<String> consumer2 = ;
//方法引用
Consumer<String> consumer3 = ;
答案:
//lambda
Consumer<String> consumer2 = (s)->{
System.out.println(s);
}
//方法引用
Consumer<String> consumer3 = System.out::println;
属于调用类的静态方法一类
4.Stream流
4.1 介绍
Stream
是一种用于处理集合(如集合类 List
, Set
)的高级抽象,它允许以声明性方式处理数据。Stream
API的核心作用是通过函数式编程风格(如过滤、映射、减少等)来简化集合操作,提升代码的可读性和可维护性。
作用:
- 简化数据处理:一种声明性、函数式的方式处理集合数据,而无需编写显式的循环和条件判断代码;
filter()
进行条件过滤,使用map()
进行数据转换,使用reduce()
进行归约计算 - 简化代码:使用
Stream
进行多个操作时,可以直接通过链式调用来完成,无需显式创建和更新中间变量
4.2 流的操作类型
要想操作流,首先需要有一个数据源,可以是数组或者集合。每次操作都会返回一个新的流对象,方便进行链式操作,但原有的流对象会保持不变。
流的操作可以分为两种类型:
- 中间操作,可以有多个,每次返回一个新的流,可进行链式操作
- 终端操作,只能有一个,每次执行完,这个流也就用光光了,无法执行下一个操作,因此只能放在最后
来举个例子。
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();
中间操作不会立即执行,只有等到终端操作的时候,流才开始真正地遍历,用于映射、过滤等。通俗点说,就是一次遍历执行多个操作,性能就大大提高了。
4.3 举例
4.3.1 创建流
- 如果是数组的话,可以使用
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);
}
4.3.2 操作流
Stream 类提供了很多有用的操作流的方法
①过滤
通过
filter()
方法可以从流中筛选出我们想要的元素。
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);
}
}
filter()
方法接收的是一个 Predicate(Java 8 新增的一个函数式接口,接受一个输入参数返回一个布尔值结果)类型的参数,因此,我们可以直接将一个 Lambda 表达式传递给该方法,比如说element -> element.contains("王")
就是筛选出带有“王”的字符串。forEach()
方法接收的是一个 Consumer(Java 8 新增的一个函数式接口,接受一个输入参数并且无返回的操作)类型的参数,类名 :: 方法名
是 Java 8 引入的新语法(方法引用),System.out
返回 PrintStream 类,println 方法你应该知道是打印的。(该类型为调用类的静态方法)- 输出结果:
王力宏
②映射
如果想通过某种操作把一个流中的元素转化成新的流中的元素,可以使用 map()
方法。
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
③匹配
Stream 类提供了三个方法可供进行元素匹配,它们分别是:
anyMatch()
,只要有一个元素匹配传入的条件,就返回 true。allMatch()
,只有有一个元素不匹配传入的条件,就返回 false;如果全部匹配,则返回 true。noneMatch()
,只要有一个元素匹配传入的条件,就返回 false;如果全部不匹配,则返回 true。
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
④组合
reduce()
方法的主要作用是把 Stream 中的元素组合起来,它有两种用法:
Optional<T> reduce(BinaryOperator<T> accumulator)
没有起始值,只有一个参数,就是运算规则,此时返回 Optional。
T reduce(T identity, BinaryOperator<T> accumulator)
有起始值,有运算规则,两个参数,此时返回的类型和起始值类型一致。
来看下面这个例子。
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
4.3.3 转换流
既然可以把集合或者数组转成流,那么也应该有对应的方法,将流转换回去——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]
[周杰伦, 王力宏, 陶喆, 林俊杰]
周杰伦, 王力宏, 陶喆, 林俊杰
5.函数式接口
5.1 定义
- 函数式接口(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的函数式编程,该包中的函数式接口有:
5.2 举例
可以用注解**@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));
}
}