概述
之前在自学的时候接触过 Lambda 表达式,但那会其实用的并不多,对于其也是大概有个了解,但在阅读公司代码的时候,发现在对于集合的处理时都是转成stream
流再通过foreach
结合 Lambda 表达式进行处理的,所以打算再重新学学这部分的内容
先来看看网上对于它的定义
Lambda 表达式是 Java 8 的重要更新,它支持将代码块作为方法参数、允许使用更简洁的代码来创建只有一个抽象方法的接口的实例。 Lambda 表达式的主要作用就是可以用于简化创建匿名内部类对象,Lambda 表达式的代码块将会用于实现抽象方法的方法体,Lambda 表达式就相当于一个匿名方法
结合上面的描述可以得到一个结论,Lambda 表达式在匿名函数以及集合的 stream 操作有着重要的作用
先来看一段简单的代码
public class TestLambda {
public static void main(String[] args) {
Thread thread = new Thread(new MyRunnable());
thread.start();
thread.close();
}
}
class MyRunnable implements Runnable{
@Override
public void run() {
System.out.println("Hello");
}
}
现在将代码简化一下,使用匿名内部类来实现 Runnable 接口
public class TestLambda {
public static void main(String[] args) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("Hello");
}
});
thread.start();
}
}
然而上面这段代码还不是最简单的,再看下面这个
public class TestLambda {
public static void main(String[] args) {
new Thread(() -> System.out.println("Hello")).start();
}
}
这样对比下来,发现使用 Lambda 表达式确实能极大的让代码更简洁
语法
Lambda 表达式由三部分构成
- 形参列表:形参列表允许省略类型,如果形参列表中只有一个参数,形参列表的圆括号也可以省略
- 箭头(->)
- 代码块:可以是表达式也可以代码块,是函数式接口里方法的实现。代码块可返回一个值或者什么都不反回,这里的代码块块等同于方法的方法体。如果是表达式,也可以返回一个值或者什么都不返回
总结一下就是:
实现的这个接口中的抽象方法中的形参列表 -> 抽象方法的处理
无返回值有形参的方法
public interface MyInterface {
public abstract void show(int a,int b);
}
public class MyTest {
public static void main(String[] args) {
MyInterface myInterface = new MyInterface() {
@Override
public void show(int a, int b) {
System.out.println(a + b);
}
};
//简写1:方法名可以自己推断出来
MyInterface myInterface1 = (int a, int b) -> {
System.out.println(a + b);
};
//简写2:可以省略形参列表中的形参类型
MyInterface myInterface2 = (a, b) -> {
System.out.println(a + b);
};
//简写3:如果抽象方法中只有一行代码,可以省略方法体的大括号,当然,如果不止一行,就不能省略
MyInterface myInterface3 = (a, b) -> System.out.println(a + b);
}
}
有返回值的抽象方法
public interface MyInterface {
public abstract int test(int a,int b);
}
public class MyTest {
public static void main(String[] args) {
MyInterface test1 = new MyInterface() {
@Override
public int test(int a, int b) {
return a - b;
}
//简写1:
MyInterface test3 = (a, b) -> {return a - b;};
//简写3:这个有返回值的方法,不能直接去掉大括号,还需要去掉return关键字
MyInterface test4 = (a, b) -> a - b;
}
}
只有一个形参的抽象方法
public interface MyInterface {
public abstract int show(int a);
}
public class MyTest {
public static void main(String[] args) {
//形参列表中只有一个参数,可以去掉形参的括号
MyInterface myInterface = a -> a-20;
}
}
Lambda表达式作为参数
public class Main {
public static void main(String[] argv) {
engine((x,y)-> x + y);
engine((x,y)-> x * y);
engine((x,y)-> x / y);
engine((x,y)-> x % y);
}
private static void engine(Calculator calculator){
int x = 2, y = 4;
int result = calculator.calculate(x,y);
System.out.println(result);
}
}
@FunctionalInterface
interface Calculator{
int calculate(int x, int y);
}
Lambda 表达式与函数式接口
在以往的资料上,他们大多爱说这么一句话
Lambda 表达式的类型,也被称为目标类型(target type)。Lambda 表达式的目标类型必须是函数式接口(functional interface)
我相信很多想要了解Lambda表达式的人一查资料,发现这么一句话,肯定是一头雾水的。暂且先不讨论这句话的含义,但有一点是可以达成共识的,那就是:Lambda表达式和函数接口一定存在着千丝万缕的关联
事实上确实是这样的,函数式接口和Lambda表达式同为Java8的新特性,它大致可以用以下的文字来形容
函数式接口就是一个有且仅有一个抽象方法,但是可以有多个非抽象方法的接口。可以通过 Lambda 表达式来创建该接口的对象
通过查询 Java 8 的 API 文档,可以发现大量的函数式接口,例如熟知的 Runnable 接口就是一个函数式接口(其中只有一个抽象的run()
方法)。Java 8 还提供了@FunctionalInterface
注解,该注解用于告诉编译器校验接口必须是函数式接口,否则就报错
由于 Lambda 表达式的结果就是被当做对象/实例,因此,可以使用 Lambda 表达式进行赋值
但是下面这个是个错误的实例
Object obj = () -> {
for (int i = 0; i < 100; i++) {
System.out.println(i);
}
};
其实如果已经了解了 Lambda 表达式与函数式接口的关系,大概都不会犯这个错,但考虑到之前有提到过“Lambda 表达式的结果就是被当成对象/实例的”,所以还是把它拿出来说说。这个程序执行之后会报Target type of a lambda conversion must be an interface
,将 Lambda 表达式赋值给 Object 类型的变量,编译器只能推断出它的表达类型为 Object,而 Object 并不是函数式接口,因此就报错了
为了保证 Lambda 表达式的目标类型是明确的函数式接口,有如下三种常见方式:
- 将 Lambda 表达式赋值给函数式接口类型的变量
- 将 Lambda 表达式作为函数式接口类型的参数传给某个方法
- 使用函数式接口对 Lambda 表达式进行强制类型转换
那么以上的错误就可以更改成
//这里就使用函数式接口 Runnable 对 Lambda 表达式进行了强制转换
Object obj = (Runnable)() -> {
for (int i = 0; i < 100; i++) {
System.out.println(i);
}
};
奇奇怪怪的运算符——“::”
一个简单问题的思考
在使用 Lambda 表达式的时候,我们实际上传递进去的代码就是一种解决方案:拿什么参数做什么操作。那么考虑一种情况:如果我们在 Lambda 中所指定的操作方案,已经有地方存在相同方案,那是否还有必要再写重复逻辑?
来看例子:冗余的 Lambda 场景
@FunctionalInterface
public interface Printable {
/**
* 接收一个字符串参数,打印显示它
* @param str 字符串
*/
public abstract void print(String str);
}
-------------------------------------------------------
public class Demo01 {
public static void main(String[] args) {
printString(s -> System.out.println(s));
}
private static void printString(Printable printable) {
printable.print("Hello, World!");
}
}
这段代码的问题在于,对字符串进行控制台打印输出的操作方案,明明已经有了现成的实现,那就是 System.out 对象中的println(String)
方法。既然 Lambda 希望做的事情就是调用println(String)
方法,那何必自己手动调用呢?
解决方式
public class Demo02 {
public static void main(String[] args) {
printString(System.out::println);
}
private static void printString(Printable printable) {
printable.print("Hello, World!");
}
}
方法引用
双冒号::
为引用运算符,而它所在的表达式被称为方法引用。如果 Lambda 要表达的函数方案已经存在于某个方法的实现中,那么则可以通过双冒号来引用该方法作为 Lambda 的替代者
例如上例中,System.out
对象中有一个重载的println(String)
方法恰好就是我们所需要的。那么对于printString
方法的函数式接口参数,对比下面两种写法,完全等效
// Lambda表达式写法
s -> System.out.println(s);
// 方法引用写法
System.out::println
Lambda 中传递的参数,一定是方法引用中的那个方法可以接收的类型,否则会出现编译错误。上例中 println 方法可以接收 String 类型的参数,所以才有两种等价的写法
通过对象名引用成员方法
假如我们已经在一个类中定义了一个方法的具体实现
public class MethodRefObject {
public void printUpperCase(String str) {
System.out.println(str.toUpperCase());
}
}
然后函数式接口定义如下
@FunctionalInterface
public interface Printable {
public abstract void print(String str);
}
拿到参数之后经 Lambda 之手,继而传递给toUpperCase()
方法去处理
public class Demo {
public static void main(String[] args) {
printString(s -> s.toUpperCase());
}
private static void printString(Printable lambda) {
lambda.print("Hello");
}
}
折腾了一大圈发现,其实已经有方法实现了 Lambda 表达式想要实现的功能
这个时候,当需要使用这个 printUpperCase 成员方法来替代 Printable 接口的 Lambda 的时候,已经具有了 MethodRefObject 类的对象实例,则可以通过对象名引用成员方法
public class Demo {
public static void main(String[] args) {
MethodRefObject obj = new MethodRefObject();
printString(obj::printUpperCase);
}
private static void printString(Printable lambda) {
lambda.print("Hello");
}
}
通过类名引用静态方法
Math 类中已经存在了静态方法abs()
,现在需要通过 Lambda 去调用这个方法,有两种表示方式
第一种,使用函数式接口
@FunctionalInterface
public interface CalculationAble {
int calculation(int num);
}
public class Demo {
public static void main(String[] args) {
method(-666, n -> Math.abs(n));
}
private static void method(int num, CalculationAble lambda) {
System.out.println(lambda.calculation(num));
}
}
第二种,可以使用类名来引用静态方法
public class Demo {
public static void main(String[] args) {
method(-666, Math::abs);
}
private static void method(int num, CalculationAble reference) {
System.out.println(reference.calculation(num));
}
}
使用 super 引用成员方法
函数式接口
@FunctionalInterface
public interface GreetAble {
void greet();
}
父类
public class Human {
public void sayHello() {
System.out.println("Hello!");
}
}
子类
public class Man extends Human {
@Override
public void sayHello() {
System.out.println("hey,bro!");
}
/**
* 定义方法method,参数传递GreetAble接口
* @param g 这里传入的是Lambda表达式
*/
public void method(GreetAble g) {
g.greet();
}
/**
* 调用method方法,使用Lambda表达式
*/
public void show(){
// 创建Human对象,调用sayHello方法
method(() -> { new Human().sayHello(); });
// 简化Lambda
method(() -> new Human().sayHello());
// 使用super关键字代替父类对象
method(() -> super.sayHello());
// 再简化
method(supper::sayHello);
}
}
使用 this 引用成员方法
函数式接口
@FunctionalInterface
public interface RichAble {
void buy();
}
一个方法要以函数式接口为参数
public class Husband {
/**
* 结婚
* @param lambda 函数式接口,买东西
*/
private void marry(RichAble lambda) {
lambda.buy();
}
/**
* 要开心
*/
public void beHappy() {
marry(() -> System.out.println("买套房子"));
}
}
开心方法 beHappy 调用了结婚方法 marry ,后者的参数为函数式接口 Richable ,所以需要一个 Lambda 表达式。 但是如果这个 Lambda 表达式的内容已经在本类当中存在了,则可以对 Husband 丈夫类进行修改
public class Husband {
/**
* 买房子
*/
private void buyHouse() {
System.out.println("买套房子");
}
/**
* 结婚
* @param lambda 函数式接口,买东西
*/
private void marry(RichAble lambda) {
lambda.buy();
}
/**
* 要开心
*/
public void beHappy() {
marry(() -> this.buyHouse());
}
}
如果希望去掉 Lambda 表达式,可以使用 this 去引用成员方法
public class Husband03 {
/**
* 买房子
*/
private void buyHouse() {
System.out.println("买套房子");
}
/**
* 结婚
* @param lambda 函数式接口,买东西
*/
private void marry(RichAble lambda) {
lambda.buy();
}
/**
* 要开心
*/
public void beHappy() {
marry(this::buyHouse);
}
}
类构造器引用
方法引用其实还好,至少方法名是固定的。但是构造器的名字与类名相同,根本不固定,所以可以通过类名称::new
的方式引用
一个简单的类
public class Person {
private String name;
public Person(String name) {
this.name = name;
}
public String getName() {
return this.name;
}
public void setName(String name) {
this.name = name;
}
}
函数式接口
@FunctionalInterface
public interface PersonBuilder {
/**
* 创建 Person 对象
* @param name Person对象名
* @return Person对象
*/
Person buildPerson(String name);
}
要使用这个接口就需要通过 Lambda 表达式
public class Demo {
public static void main(String[] args) {
printName("Tom", (name) -> new Person(name));
}
public static void printName(String name, PersonBuilder builder) {
System.out.println(builder.buildPerson(name).getName());
}
}
如果换成类构造器引用,就得这么写
public class Demo {
public static void main(String[] args) {
printName("Tom", Person::new);
}
public static void printName(String name, PersonBuilder builder) {
System.out.println(builder.buildPerson(name).getName());
}
}
如果是数组,那写法就有些区别了
函数式接口
@FunctionalInterface
public interface ArrayBuilder {
/**
* 创建数组的函数式接口
* @param length 数组长度
* @return 存储int类型的数组
*/
int[] buildArray(int length);
}
使用 Lambda 表达式应用接口
public class Demo {
public static void main(String[] args) {
int[] array = initArray(10, length -> new int[length]);
}
private static int[] initArray(int length, ArrayBuilder builder) {
return builder.buildArray(length);
}
}
使用构造器引用应用接口
public class Demo {
public static void main(String[] args) {
int[] array = initArray(10, int[]::new);
}
private static int[] initArray(int length, ArrayBuilder builder) {
return builder.buildArray(length);
}
}
很显然,数组的构造器引用就不能通过类名了,这个需要注意
写在
看到过其他人的文章中有一句话形容 Lambda 表达式非常贴切
Lambda 表达式的原则是“可推导就是可省略”
关于 Lambda 表达式我认为我学习到的内容才是冰山一角,很多东西可能还需要在以后的实践过程中再去熟悉再去体会,后续有其他关于 Lambda 的想要分享的内容还是会在博客中分享
原创文章,作者:wdmbts,如若转载,请注明出处:https://blog.ytso.com/tech/pnotes/272553.html