言成言成啊 | Kit Chen's Blog

方法引用

1 冗余的Lambda场景

来看一个简单的函数式接口以应用Lambda表达式:

1
2
3
4
5
@FunctionalInterface
public interface Printable {
//定义字符串的抽象方法
void print(String s);
}

Printable 接口当中唯一的抽象方法 print 接收一个字符串参数,目的就是为了打印显示它。那么通过Lambda 来使用它的代码很简单:

1
2
3
4
5
6
7
8
9
10
public class Demo01Printable {
// 定义一个方法,参数传递Printable接口,对字符串进行打印
public static void printString(Printable p) {
p.print("Hello World");
}
public static void main(String[] args) {
//调用printString方法,方法的参数Printable是一个函数式接口,所以可以传递Lambda表达式
printString(s->System.out.println(s));
}
}

其中 printString方法只管调用 Printable 接口的 print 方法,而并不管 print 方法的具体实现逻辑会将字符串打印到什么地方去。而 main 方法通过Lambda表达式指定了函数式接口Printable的具体操作方案为:拿到 String(类型可推导,所以可省略)数据后,在控制台中输出它。

2 问题分析

这段代码的问题在于,对字符串进行控制台打印输出的操作方案,明明已经有了现成的实现,那就是 System.out 对象中的 println(String) 方法。既然Lambda希望做的事情就是调用 println(String) 方法,那何必自己手动调用呢?

3 用方法引用改进代码

能否省去Lambda的语法格式(尽管它已经相当简洁)呢?只要“引用”过去就好了:

1
2
3
4
5
6
7
8
9
10
11
public class Demo01Printable {
// 定义一个方法,参数传递Printable接口,对字符串进行打印
public static void printString(Printable p) {
p.print("Hello World");
}
public static void main(String[] args) {
//调用printString方法,方法的参数Printable是一个函数式接口,所以可以传递Lambda表达式
//printString(s->System.out.println(s));
printString(System.out::println);
}
}

请注意其中的双冒号 ::写法,这被称为“方法引用”,而双冒号是一种新的语法。

注意:

  1. System.out对象是已经存在的
  2. println方法也是已经存在的

所以我们可以使用方法引用来优化Lambda表达式,可以使用System.out方法直接调用println方法

4 方法引用符

双冒号::为引用运算符,而它所在的表达式被称为方法引用。

如果Lambda要表达的函数方案已经存在于某个方法的实现中,那么则可以通过双冒号来引用该方法作为Lambda的替代者。

语义分析

例如上例中, System.out 对象中有一个重载的 println(String) 方法恰好就是我们所需要的。那么对于 printString 方法的函数式接口参数,对比下面两种写法,完全等效:

  • Lambda表达式写法:s -> System.out.println(s);
  • 方法引用写法: System.out::println

第一种语义是指:拿到参数之后经Lambda之手,继而传递给 System.out.println 方法去处理。

第二种等效写法的语义是指:直接让 System.out 中的 println 方法来取代Lambda。两种写法的执行效果完全一样,而第二种方法引用的写法复用了已有方案,更加简洁。

注:Lambda 中,传递的参数一定是方法引用的那个方法可以接收的类型,否则会抛出异常

推导与省略

如果使用Lambda,那么根据“可推导就是可省略”的原则,无需指定参数类型,也无需指定的重载形式——它们都将被自动推导。

而如果使用方法引用,也是同样可以根据上下文进行推导。

函数式接口是Lambda的基础,而方法引用是Lambda的孪生兄弟。

下面这段代码将会调用 println 方法的不同重载形式,将函数式接口改为String类型的参数:

1
2
3
4
5
@FunctionalInterface
public interface Printable {
//定义字符串的抽象方法
void print(String s);
}

由于上下文变了之后可以自动推导出唯一对应的匹配重载,所以方法引用没有任何变化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class Demo01Printable {
// 定义一个方法,参数传递Printable接口,对字符串进行打印
public static void printString(Printable p) {
p.print("Hello World");
}

public static void main(String[] args) {
//调用printString方法,方法的参数Printable是一个函数式接口,所以可以传递Lambda表达式
printString(s->System.out.println(s));

/*
* 分析:
* Lambda表达式的目的,
* 就是打印参数传递的字符串,把参数s传递给了System.out的对象,调用out对象中的方法println对字符串进行输出
* 注意:
* 1.System.out对象是已经存在的 2.println方法也是已经存在的
* 所以我们可以使用方法引用来优化Lambda表达式
* 可以使用System.out方法直接调用println方法
*/
printString(System.out::println);
}
}

这次方法引用将会自动匹配到 println(String) 的重载形式。

5 通过对象名引用成员方法

这是最常见的一种用法,与上例相同。如果一个类中已经存在了一个成员方法:

1
2
3
4
5
6
public class Demo02MethodRerObject {
//定义一个静态成员方法,传递字符串,把字符串按照大写输出
public void printUpperCaseString(String str) {
System.out.println(str.toUpperCase());
}
}

函数式接口仍然定义为:

1
2
3
4
5
@FunctionalInterface
public interface Printable {
//定义字符串的抽象方法
void print(String s);
}

那么当需要使用这个printUpperCase成员方法来替代Printable接口的Lambda的时候,已经具有了Demo02MethodRefObject类的对象实例,则可以通过对象名引用成员方法,代码为:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Demo03ObjectMethodReference {
//定义一个方法,方法的参数传递Printable接口
public static void printString(Printable p) {
p.print("hello");
}
public static void main(String[] args) {
//调用printString方法,方法的参数Printable是一个函数式接口,所以可以传递Lambda表达式
printString(s->new Demo02MethodRerObject().printUpperCaseString(s));

//使用方法引用来优化
printString(new Demo02MethodRerObject()::printUpperCaseString);
}
}

注意:如果里面的方法不是静态的话,则需要对象名::方法,这样使用;如果是静态方法,则可以直接使用类名::方法,这就是所谓的“可推导就是可省略”

6 通过类名称引用静态方法

由于在java.lang.Math类中已经存在了静态方法abs,所以当我们需要通过Lambda来调用该方法时,有两种写 法。首先是函数式接口:

1
2
3
4
public interface Calcable {
//定义一个抽象方法,传递一个整数,对整数进行绝对值计算
public abstract int calcAbs(int num);
}

使用Lambda表达式和方法引用来实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Demo04StaticMethodReference {
//定义一个方法,传递计算绝对值的整数,和函数式接口Calcable
public static int method(int num,Calcable c) {
return c.calcAbs(num);
}
public static void main(String[] args) {
//调用method方法,传递计算绝对值的整数,和Lambda表达式
System.out.println(method(-10,num->Math.abs(num)));
/*
* 使用方法引用
* 1.Math类是存在的
* 2.abs方法是静态的
*/
System.out.println(method(-10,Math::abs));
}
}

在这个例子中,下面两种写法是等效的:

  • Lambda表达式: n -> Math.abs(n)
  • 方法引用: Math::abs

7 通过super方法引用成员方法

如果存在继承关系,当Lambda中需要出现super调用时,也可以使用方法引用进行替代。首先是函数式接口:

1
2
3
4
5
@FunctionalInterface
public interface Greetable {
//定义一个见面的方法
void greet();
}

然后是父类Human的内容:

1
2
3
4
5
6
class Human {
//定义一个说你好的方法
public void sayHello() {
System.out.println("hello 我是Human!");
}
}

最后是子类Man的内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Man extends Human {

@Override
public void sayHello() {
System.out.println("Hello 我是Man!");
}
//定义一个方法参数传递Greetable
public void method(Greetable g) {
g.greet();
}
public void show() {
//调用method方法,方法的参数Greetable是一个函数式接口,所以可以传递Lambda表达式
// method(()->{
// //创建父类Human对象
// Human h=new Human();
// //调用父类的sayHello
// h.sayHello();
// });
//因为有子父类关系,所以存在一个关键字super,代表父类,所以我们可以直接使用super调用父类的成员方法
// method(()->super.sayHello());
//使用方法引用
method(super::sayHello);
}
}

最后是主方法

1
2
3
4
5
public class Demo05SuperMethodReference {
public static void main(String[] args) {
new Man().show();
}
}

在这个例子中,下面两种写法是等效的:

  • Lambda表达式: () -> super.sayHello()
  • 方法引用: super::sayHello

8 通过this引用成员方法

this代表当前对象,如果需要引用的方法就是当前类中的成员方法,那么可以使用this::成员方法的格式来使用方 法引用。首先是简单的函数式接口:

1
2
3
4
5
@FunctionalInterface
public interface Richable {
//定义一个想买啥就买啥的方法
void buy();
}

下面是一个丈夫 Husband 类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Husband {
//定义一个买房子的方法
public void buyHouse() {
System.out.println("北京二环内买一套四合院");
}
//定义一个结婚的方法,参数传递Richable接口
public void marry(Richable r) {
r.buy();
}
//定义一个非常高兴的方法
public void soHappy() {
// marry(()->{
// this.buyHouse();
// });
marry(this::buyHouse);
}
}

开心方法 beHappy 调用了结婚方法 marry ,后者的参数为函数式接口 Richable ,所以需要一个Lambda表达式。 但是如果这个Lambda表达式的内容已经在本类当中存在了,则可以对 Husband 丈夫类进行修改:

主方法:

1
2
3
4
5
public class Demo06ThisMethodReference {
public static void main(String[] args) {
new Husband().soHappy();
}
}

在这个例子中,下面两种写法是等效的:

  • Lambda表达式: () -> this.buyHouse()
  • 方法引用:this::buyHouse

9 类的构造器引用

由于构造器的名称与类名完全一样,并不固定。所以构造器引用使用 类名称::new 的格式表示。首先是一个简单的 Person 类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class Person {
private String name;

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public Person(String name) {
super();
this.name = name;
}

public Person() {
super();
// TODO Auto-generated constructor stub
}
}

然后是用来创建 Person 对象的函数式接口:

1
2
3
4
5
@FunctionalInterface
public interface PersonBuilder {
//定义一个方法,根据传递的姓名传递Person对象返回
Person builderPerson(String name);
}

要使用这个函数式接口,可以通过Lambda表达式,但是通过构造器引用,有更好的写法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Demo07ClassConstructor {
//定义一个方法,参数传递姓名和PersonBuilder接口,方法中通过姓名创建Person对象
public static void printName(String name,PersonBuilder pb) {
Person person=pb.builderPerson(name);
System.out.println(person.getName());
}
public static void main(String[] args) {
//调用printName方法,方法的参数PersonBuilder接口是一个函数式接口,所以可以传递Lambda表达式
// printName("胡列娜",name->new Person(name));

//使用方法引用,构造方法new Person(String name)已知,创建对象方式new已知
//就可以使用Person引用new创建对象
printName("胡列娜最美",Person::new);
}
}

在这个例子中,下面两种写法是等效的:

  • Lambda表达式: name -> new Person(name)
  • 方法引用: Person::new

10 数组的构造器引用

数组也是 Object 的子类对象,所以同样具有构造器,只是语法稍有不同。如果对应到Lambda的使用场景中时, 需要一个函数式接口:

1
2
3
4
5
@FunctionalInterface
public interface ArrayBuilder {
//创建一个int类型数组的方法,参数传递数组的长度,返回创建好的int类型数组
int[] builderArray(int length);
}

在应用该接口的时候,可以通过Lambda表达式,但是更好的写法是使用数组的构造器引用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class Demo08ArrayConstructor {
/*
* 定义一个方法,方法的参数传递一个创建数组的长度和ArrayBuilder接口
* 方法内部根据传递的长度,使用ArrayBuilder中的方法创建数组并且返回
*/
public static int[] createArray(int length,ArrayBuilder ab) {
return ab.builderArray(length);
}
public static void main(String[] args) {
//调用createArray方法,传递数组的长度和Lambda表达式
// System.out.println(createArray(10,length->new int[length]).length);

/*
* 使用方法引用
* 已知创建的数组就是int[]数组
* 已知数组的长度是length
* 就可以使用方法引用
*/
System.out.println(createArray(10,int[]::new).length);
}
}

在这个例子中,下面两种写法是等效的:

  • Lambda表达式: length -> new int[length]
  • 方法引用: int[]::new
最后修改:2020-04-21 21:17:29
原文链接:https://meethigher.top/blog/2020/method-reference/
付款码 捐助 分享
翻墙之后才能评论哦
阅读量