详解 Lambda 表达式、函数式接口、方法引用
一、Lambda 表达式
1.Lambda 表达式简介
Lambda 表达式即函数式编程,可以将行为进行传递,可以在以后执行一次或多次。使写出更简洁、灵活、紧凑的代码。
2.使用 Lambda 的优化
当需要启动一个线程去完成任务时,通常会通过java.lang.Runnable
接口来定义任务内容,并使用java.lang.Thread
类来启动该线程。
传统写法如下:
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("执行线程任务");
}
}).start();
}
我们可以看到真正希望做到的事情是:将 run 方法体内的代码传递给 Thread 类使用。
把函数作为一个方法的参数(函数作为参数传递进方法中)。
Lambda 表达式写法:
借助 Java8 的全新语法,上述Runnable
接口的匿名内部类写法可以通过更简单的 Lambda 表达式达到等效
public static void main(String[] args) {
new Thread(() -> System.out.println("执行线程任务")).start();
}
这段代码和刚才执行的效果是完全一致的,可以看出我们启动了一个线程,而线程任务的内容以一种更加简洁的形式被指定。
3.Lambda 格式
Lambda 的格式由3个部分组成
其标准格式为:
(参数类型 参数名称 ...) -> { 代码语句 }
- 一些参数 小括号内的语法与传统方法参数列表一致:无参数则留空;多个参数则用逗号分隔。
- 一个箭头
->
是新引入的语法格式,代表指向动作。 - 一段代码 大括号内的语法与传统方法体要求基本一致。实现的为代码逻辑部分,可以是一行代码也可以是一个代码片段
简写格式
当编译器可以自动推导出这个方法的参数类型及返回值时,就可以进行省略。
1.小括号内的参数类型可以省略
(参数名称 …) -> { 代码语句 }
public static void main(String[] args) {
Comparator<String> comparator;
// 原写法
comparator = new Comparator<String>() {
@Override
public int compare(String first, String second) {
int lenNum = first.length() - second.length();
if (lenNum > 0) return 1;
if (lenNum < 0) return 0;
return 0;
}
};
// Lambda 写法
comparator = (first, second) -> {
int lenNum = first.length() - second.length();
if (lenNum > 0) return 1;
if (lenNum < 0) return 0;
return 0;
};
}
2.如果小括号内有且仅有一个参,则小括号可以省略;
参数名称 -> { 代码语句 }
public static void main(String[] args) {
Comparable comparable;
// 原写法
comparable = new Comparable() {
@Override
public int compareTo(Object o) {
if (null == o) {
return 0;
}
return 1;
}
};
// Lambda 写法
comparable = o -> {
if (null == o) {
return 0;
}
return 1;
};
}
3.如果大括号内有且仅有一个语句,则无论是否有返回值,都可以省略大括号、return 关键字及语句分号。
(参数类型 参数名称 …) -> 代码语句
public static void main(String[] args) {
Comparator<String> comparator;
// 原写法
comparator = new Comparator<String>() {
@Override
public int compare(String o1, String o2) {
return o1.length() - o2.length();
}
};
// Lambda 写法
comparator = (first, second) -> first.length() - second.length();
}
4.总结
Lambda 表达式不能独立存在,总是会转换为函数式接口的实例。
二、函数式接口
1.什么是函数式接口
函数接口是有且仅有一个抽象方法的接口,无论是Runnable
、Comparator
接口还是自定义的接口,只有当接口中的抽象方法存在且唯一时,才能用作 Lambda 表达式。
2.Java 8 常 用 函 数 式 接 口
函数式接口 | 入参参数类型 | 返回类型 | 抽象方法名 | 描 述 | 示例 |
Supplier | 无 | T | get | 提供一个 T 类型的值 | 类似工厂方法 |
Consumer | T | void | accept | 处理一个 T 类型的值 | 用作打印入参 |
Function<T, R> | T | R | apply | 有一个 T 类型参数的函数 | 获取person的姓名 |
UnaryOperator | T | T | apply | 类型 T 上的一元操作 | 对输入的数字自增 |
BinaryOperator | T,T | T | apply | 类型 T 上的二元操作 | 对输入的两个数字相乘 |
Predicate | T | boolean | test | 布尔值函数 | 判断person的年龄是否大于18 |
示例:
static class Person {
private String name;
private Integer age;
// 忽略 get set toString
}
public static void main(String[] args) {
// 返回个 Person 对象
Supplier<Person> supplier = () -> new Person("旺财", 18);
// 用作打印入参
Consumer<String> consumer = s -> System.out.println(s);
// 获取 person 对象的名字
Function<Person, String > function = person -> person.getName();
// 输入的数字自增1
UnaryOperator<Integer> unaryOperator = i -> ++i;
// 输入的两个数字做乘法
BinaryOperator<Integer> binaryOperator = (x, y) -> x * y;
// 判断 person 对象的年龄是否大于18岁
Predicate<Person> predicate = person -> person.getAge() > 18;
Person person = new Person("旺财", 18);
System.out.println("①Supplier ==>生成 person 对象 :" + supplier.get());
System.out.print("②consumer ==>打印入参 :"); consumer.accept("莱纳,你坐啊");
System.out.println("③function ==>获取 person 对象的姓名 :" + function.apply(person));
System.out.println("④unaryOperator ==>输入的值自增1 :" + unaryOperator.apply(123456));
System.out.println("⑤binaryOperator ==>输入的两个值相乘 :" + binaryOperator.apply(1234, 5678));
System.out.println("⑥predicate ==>判断 person 对象年龄是否大于18岁 :" + predicate.test(person));
}
结果:
①Supplier ==>生成 person 对象 :Person(name=旺财, age=18)
②consumer ==>打印入参 :莱纳,你坐啊
③function ==>获取 person 对象的姓名 :旺财
④unaryOperator ==>输入的值自增1 :123457
⑤binaryOperator ==>输入的两个值相乘 :7006652
⑥predicate ==>判断 person 对象年龄是否大于18岁 :false
3.自定义函数式接口
实际上也是有且仅有一个抽象方法的自定义的接口。
可以使用 @FunctionalInterface 注解修饰需要的接口,编译器会检测该接口是否只有一个抽象方法,否则,会报错。可以有多个默认方法,静态方法。
定义
@FunctionalInterface
interface TestInterface {
void getMaxNum(Integer a, Integer b);
}
示例
public static void main(String[] args) {
TestInterface testInterface = (a, b) -> a >= b ? a : b;
Integer maxNum = testInterface.getMaxNum(255, 666);
System.out.println("最大值 maxNum :" + maxNum);
// 666
}
三、方法引用
1.方法引用的简介
方法引用相当于简写 了 Lambda 表达式中已存在的方法,可以把 Lambda 表达式重写为方法引用,通过使用操作符 :: 直接访问类或实例的已存在的方法或构造方法。
方法引用可以理解为 Lambda 表达式的另外一种表现形式。
通俗的说就是 Lambda 所要重写的内容和其他已存在的方法体一样,就可以直接拿来使用。
2.方法引用的使用条件
- 被引用的方法参数列表和函数式接口中抽象方法的参数一致
- 接口的抽象方法没有返回值,引用的方法可以有返回值也可以没有
- 接口的抽象方法有返回值,引用的方法必须有相同类型的返回值
3.方法引用的类型
类型 | 语法 | 对应的Lambda表达式 |
静态方法引用 | 类名::staticMethod | (args) -> 类名.staticMethod(args) |
实例方法引用 | inst::instMethod | (args) -> inst.instMethod(args) |
对象方法引用 | 类名::instMethod | (inst,args) -> inst 的类名.instMethod(args) |
构建方法引用 | 类名::new | (args) -> new 类名(args) |
1.静态方法引用
public static void main(String[] args) {
// 函数式接口,其需要重写的方法为: T apply(T t, T u);
BinaryOperator<Integer> binaryOperator = (a, b) -> (a >= b) ? a : b;
}
假设我们要实现的 BinaryOperator 的逻辑内容为输出最大的那个数。
这里我们可以改写成
public static void main(String[] args) {
BinaryOperator<Integer> binaryOperator = (a, b) -> (a >= b) ? a : b;
// 这两个是等价的
binaryOperator = Math::max;
}
这里其实引用的是 Math 类下的静态方法
public static int max(int a, int b) {
return (a >= b) ? a : b;
}
我们发现我们要实现的比较的方法在 Math 类中已经有现成的逻辑内容结构了,所以我们直接引用过来使用。
这里的 Math::max 等价于 (a, b) -> (a >= b) ? a : b
2.实例方法引用
这里我们先建立一个用于引用的类及对应的方法。
class TestInstanceFun {
public int isNull(Object obj) {
if (null == obj)
return 0;
else
return 1;
}
}
具体调用
public static void main(String[] args) {
Comparable comparable = o -> {
if (null == o) {
return 0;
} else {
return 1;
}
};
TestInstanceFun testInstanceFun = new TestInstanceFun();
comparable = testInstanceFun::isNull;
}
这里想要实现对比的 Comparable 的逻辑为 当对象为 null 时,返回0,其他就返回1,
这个和静态方法引用类似,使用 TestInstanceFun 对象的实例去引用即可。
3.对象方法引用
特殊条件:
- 至少要有一个入参,而且方法体中必须只调用该入参的方法而不做其他操作
- 第二个参数开始,为调用的方法的入参列表。
这里我们先建立一个用于测试的函数式接口
@FunctionalInterface
interface TestInterface {
simpleSubString(String originalString, int a, int b);
}
具体调用
public static void main(String[] args) {
// 假设具体的逻辑为 将字符串进行首尾切割。
TestInterface testInterface = (originalString, a, b) -> originalString.substring(a, b);
// 这里可以转换使用 String 的对象方法引用
testInterface = String::substring;
}
引用的 String 的 substring(int beginIndex, int endIndex) 方法为
public String substring(int beginIndex, int endIndex) {
if (beginIndex < 0) {
throw new StringIndexOutOfBoundsException(beginIndex);
}
if (endIndex > value.length) {
throw new StringIndexOutOfBoundsException(endIndex);
}
int subLen = endIndex - beginIndex;
if (subLen < 0) {
throw new StringIndexOutOfBoundsException(subLen);
}
return ((beginIndex == 0) && (endIndex == value.length)) ? this : new String(value, beginIndex, subLen);
}
第一个入参的 originalString 调用其 substring 方法,后面的两个参数 a 和 b,作为 substring 的入参,因此可以转换成 String::substring ,相当于 originalString.substring(a, b) 来使用。
4.构造方法引用
构造器引用与方法引用很类似,只不过方法名为 new。被引用的类必须存在一个构造方法与函数式接口的抽象方法参数列表一致,以及回参为该类的实例。
测试类
class Person {
String name;
String age;
public Person() {
this.name = "default";
this.age = "18";
}
public Person(String name, String age) {
this.name = name;
this.age = age;
}
}
测试函数式接口
@FunctionalInterface
interface TestStructureFun{
Person getPerson(String a, String b);
}
具体调用
public static void main(String[] args) {
TestStructureFun testStructureFun = (a, b) -> new Person(a, b);
// 可以转为这种写法
testStructureFun = Person::new;
}
这里的 Person::new 是 Person 构造器的一个引用。根据上下文推测出,这里引用的是 Person 中两个参数的构造器。
4.总结:
方法引用不能独立存在,总是会转换为函数式接口的实例。
如果有多个同名的重载方法, 编译器就会尝试从上下文中找出你指的那一个方法。
例如该文章中的 静态方法引用 示例中:
Math.max 方法有四个版本,有用于整数的, 有用于 double 值。选择哪一个版本取决于 Math::max 转换为哪个函数式接口的方法参数。
示例里 Math::max 检测到当前调用的方法的两个入参都为 Integer 类型,就转换为对应的方法参数 --> public static int max(int a, int b)