摘要

在使用Lambda表达式的时候,我们实际上传递进去的代码就是一种解决方案:拿什么参数做什么操作。

那么考虑一种情况:如果我们在Lambda中所指定的操作方案,已经有地方存在相同方案,那是否还有必要再写重复逻辑?

正文

1 冗余的Lambda场景

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

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

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

java
 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的语法格式(尽管它已经相当简洁)呢?只要“引用”过去就好了:

java
 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类型的参数:

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

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

java
 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 通过对象名引用成员方法

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

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

函数式接口仍然定义为:

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

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

java
 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来调用该方法时,有两种写 法。首先是函数式接口:

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

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

java
 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调用时,也可以使用方法引用进行替代。首先是函数式接口:

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

然后是父类Human的内容:

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

最后是子类Man的内容

java
 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);
	}
}

最后是主方法

java
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::成员方法的格式来使用方 法引用。首先是简单的函数式接口:

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

下面是一个丈夫 Husband 类:

java
 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 丈夫类进行修改:

主方法:

java
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 类:

java
 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 对象的函数式接口:

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

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

java
 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的使用场景中时, 需要一个函数式接口:

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

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

java
 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