方法引用

  • 概述
  • 静态方法引用
  • 特定对象实例方法引用
  • 特定类型任意对象实例方法引用
  • 构造方法引用
  • 总结


概述

方法引用是jdk8及之后的一个概念,方法引用是依赖于lambda表达式(或者说函数式接口)而存在的,其本质和方法调用是一样的。当我们要实现的方法的行为(其实就是方法体里的内容)和别处已存在的方法的行为一致时,就可以引用(调用)别处的方法,不用自己再写一遍。但它们在形式上还是有区别的,具体区别如下:

  1. 方法引用是双冒号“::”,方法调用是点号“.”或“方法调方法”
  2. 方法引用只要方法名,方法调用要方法名和形式参数
  3. 能用方法引用的地方都可以用方法调用,反之不一定

那什么时候用方法引用,下面是官网文档中的一句话:

a lambda expression does nothing but call an existing method.

说直白点,就是当lambda表达式要完成的操作和某个已存在的方法完全一样时,我们就可以用方法引用。官网中还说方法引用使代码更简洁,由于引用了一个存在的方法名,从而让代码更易理解等等。其实实际来说方法引用就一个好处:让代码更简洁。本来lambda表达式就让人很难理解了,再配一个方法引用,那就更难理解了。直接用匿名内部类和方法调用的方法写代码,这才是最容易让人理解的方式。
官网给了方法引用的四种形式,具体如下:

Kinds of Method References
There are four kinds of method references:

Kind

Example

Reference to a static method

ContainingClass::staticMethodName

Reference to an instance method of a particular object

containingObject::instanceMethodName

Reference to an instance method of an arbitrary object of a particular type

ContainingType::methodName

Reference to a constructor

ClassName::new

种类

例子

静态方法引用

类型::静态方法名

特定对象实例方法引用

对象::实例方法

特定类型任意对象的实例方法引用

类型::实例方法

构造方法引用

类型::new

具体怎么使用方法引用,其实只要理解了开头的一句话:依赖于lambda表达式而存在的,其本质和方法调用是一样的,就知道怎么用了:在有lambda表达式的地方用,和方法调用一样,只不过省略了方法参数和返回值。看下面例子基本就明白了。
定义一个人类Ren (注意Ren类中带名字的构造有一行输出人名的语句,方便举例构造方法引用,staticfun和fun两个方法)和测试类TestDoubleColon。

package com.bean;

public class Ren {
	private String id;
	private String name;
	private int age;
	
	public Ren() {
		
	}
	
	public Ren(String name) {
		this.name = name;
		System.out.println(name);
	}

	public Ren(String id, String name, int age) {
		this.id = id;
		this.name = name;
		this.age = age;
	}
	
	public static <T> void staticfun(T t) {
		System.out.println(t);
	}
	public <T> void fun(T t) {
		System.out.println(t);
	}
	public String getId() {
		return id;
	}
	public void setId(String id) {
		this.id = id;
	}
	public String getName() {
		return name;
	}
	public void setName(String name) {
		this.name = name;
	}
	public int getAge() {
		return age;
	}
	public void setAge(int age) {
		this.age = age;
	}

	@Override
	public String toString() {
		return "Ren [id=" + id + ", name=" + name + ", age=" + age + "]";
	}
}

在测试类中,定义一个list集合,往里放三个人,再定义一个forEachList静态方法(用于验证特定类型任意对象的实例方法引用)

package com.test;

import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;
import java.util.function.Function;

import com.bean.Ren;

public class TestDoubleColon {
	public static void main(String[] args) {
		List<Ren> list = new ArrayList<>();
		Ren r = null;
		r = new Ren("i1","aa",10);
		list.add(r);
		
		r = new Ren("i2","bb",14);
		list.add(r);
		
		r = new Ren("i3","cc",12);
		list.add(r);
	}

	public static <T> void forEachList(List<T> list,BiConsumer<T, T> bi) {
		for (T t : list) {
            bi.accept(t,t);
        }
	}
}

静态方法引用

在测试类的main方法中添加如下代码片段,打印集合中所有的人:

list.forEach(Ren::staticfun);//静态方法引用
	//list.forEach(n->Ren.staticfun(n));//lambda表达式写法
	//list.forEach(new Consumer<Ren>() {//传统写法
	//	@Override
	//	public void accept(Ren t) {
	//		Ren.staticfun(t);
	//	}
	//});

为什么可以这样写,因为List集合自带的forEach方法接收一个Consumer函数式接口作为参数,这个函数式接口的方法形式是一个单参数无返回值的方法,而定义的staticfun静态方法也满足单参数无返回值的形式,所以就被编译器识别出来直接调用。一句话:方法引用本质就是方法调用

特定对象实例方法引用

特定对象实例方法引用和静态方法引用的区别很明显:一个需要创建对象去调用方法,一个不需要。

list.forEach(new Ren()::fun);//特定对象的实例方法引用
		//list.forEach(n->n.fun(n));//lambda表达式写法
		//list.forEach(Ren::fun);//这样写会报错,错误原因不能用调用静态方法的方式去调用非静态方法
		//list.forEach(new Consumer<Ren>() {//传统写法
		//	@Override public void accept(Ren t) { 
		//		new Ren().fun(t); 
		//	} 
		 //});

为什么这样写的原因也一样:方法引用本质就是方法调用

特定类型任意对象实例方法引用

特定类型任意对象实例方法引用,个人觉得比较难理解的地方在于它用静态方法引用的形式去调用非静态方法,让人不知道为什么能这样写,在什么情况下这样写。下面用两个例子来说明:

  1. 用测试类中定义好的forEachList方法来说明
forEachList(list, Ren::fun);//特定类型任意对象的实例方法引用
		//forEachList(list, (n1,n2)->n1.fun(n2));//lambda表达式写法,n1和n2可以任意调换
		//foreachlist(list, new BiConsumer<Ren, Ren>() {
		//	@Override
		//	public void accept(Ren n1, Ren n2) {
		//		n1.fun(n2);
		//		//n1.fun(n1);
		//		//n2.fun(n1);
		//		//n2.fun(n2);
		//	}
		//});

虽然这个例子不太合适(浪费了一个参数),但很能说明问题。还记得上面特定对象实例方法引用的例子中的一个被注释掉的写法的报错原因吧

//list.forEach(Ren::fun);//这样写会报错,错误原因不能用调用静态方法的方式去调用非静态方法

但是在forEachList这个地方用就是好的,为什么?先来看看这两种写法的区别:
List集合自带的forEach方法接收一个Consumer函数式接口,接口的方法形式是单一参数无返回值类型

default void forEach(Consumer<? super T> action) {
        Objects.requireNonNull(action);
        for (T t : this) {
            action.accept(t);
        }
    }

测试类中的forEachList方法接收一个BiConsumer函数式接口,接口的方法形式是两个参数无返回值类型

public static <T> void forEachList(List<T> list,BiConsumer<T, T> bi) {
		for (T t : list) {
            bi.accept(t,t);
        }
	}

两个函数式接口里的方法形式不一样(一个单一参数,一个俩参数)导致同一种写法一个不行,一个可行。根本原因还要从这句话:方法引用本质就是方法调用,来说明。首先,说List集合中的forEach方法,它的Consumer接口中的方法需要一个入参,而报错的这种写法“list.forEach(Ren::fun);”,是直接引用Ren类中的一个非静态方法,我们知道要调用非静态方法,需要创建改方法的实例对象来调用,但是由于Consumer接口方法中就一个入参,对象实例没法传入方法,编译器会当成静态方法那样去调用,然而一调用发现是非静态的,于是编译器提示错误;再来说测试类中forEachList方法,它的BiConsumer接口中需要两个入参,所以例子中的这种写法“forEachList(list, Ren::fun);”,编译器会把其中一个参数当作方法的调用者,这样就满足了非静态方法的调用语法,所以编译通过,能正常输出。

  1. 用jdk中自带的Stream类的map方法来说明
list.stream().map(Ren::getName).forEach(System.out::println);//特定类型的任意对象的实例方法引用
		//list.stream().map(n->n.getName()).forEach(n->System.out.println(n));//lambda表达式写法
		//list.stream().map(new Function<Ren, String>() {//传统写法
		//	@Override
		//	public String apply(Ren t) {
		//		return t.getName();
				
		//	}
		//}).forEach(new Consumer<String>() {
		//	@Override
		//	public void accept(String t) {
		//		System.out.println(t);
		//	}
		//});

这个例子是最恰当的了,特定类型任意对象实例方法引用其实就是为这种情况准备的,来看Stream中的map方法:

<R> Stream<R> map(Function<? super T, ? extends R> mapper);

接收一个类型,然后转化为另一种类型,该方法具体实现由jdk自己完成,我们不用管。我们只要知道方法是干什么的就行了,其实就是数学上的映射,对于给定的x,我们通过给定的某种规则,得到一个结果y。这里的x和规则需要我们用时给出。

list.stream().map(Ren::getName).forEach(System.out::println);//特定类型

这句代码中“list.stream()”作用是把集合数据转为流数据(此时,流中放着一个个的Ren类的对象了),以便后续操作处理,而后面的这个".map(Ren::getName)"是把每一个Ren类对象映射为这个人的名字,这时流中的数据就是一个个人名了,然后就是循环打印每一个人名。重点看看“.map(Ren::getName)”这句,这是一个典型的特定类型任意对象实例方法引用,map方法参数Function函数式接口中的方法接收一个参数(这里就是Ren的一个对象,特定类型任意对象),然后返回一个值(这里就是人的名字,通过对象的实例方法获得),满足非静态方法调用(Ren类的一个对象调用自己的getName方法)。再一次表明:方法引用本质就是方法调用

构造方法引用

构造方法引用顾名思义就是对类构造方法的引用,大致可分为一般类和数组类的构造方法引用

  1. 一般类的构造方法引用
list.stream().map(Ren::getName).forEach(Ren::new);//构造方法引用
		//list.stream().map(n->n.getName()).forEach(n->new Ren(n));//lambda表达式写法
		//list.stream().map(new Function<Ren, String>() {//传统写法
		//	@Override
		//	public String apply(Ren t) {
		//			return t.getName();
		//	}	
		//}).forEach(new Consumer<String>() {
		//	@Override
		//	public void accept(String t) {
		//		new Ren(t);
		//	}});

Ren类中有三个构造方法,为什么“.forEach(Ren::new)”会自动调用带有一个参数的构造方法,还是那句:方法引用本质就是方法调用,Consumer函数式接口的方法是一个单一参数无返回值的,而在构造方法中只有这个带一个参数的构造方法满足这个条件,所以自动就识别出要调用这个构造方法。
由于构造方法的特殊性(可以代表一个对象),构造方法引用本身可以作为一种返回值类型,所以下面的写法就成了:

String s = "dd";
	//transform方法就是把当前字符串转为别的形式
	Ren r = s.transform(Ren::new);//相当于创建了一个名字叫dd的Ren的对象
  1. 数组类的构造方法引用

其实在构造中加一句打印语句,主要是为了举例说明构造方法引用,显然这种用法是不合适的(也可能实际情况需要,就是合适的了),其实构造方法引用更适用于数组类。我们知道声明一个数组,一般有以下几种形式:

String[] s = new String[1];
		String[] s1 = new String[]{"1"};
		String[] s2 = {"1"};
		String s3[] = new String[]{"1"};
		String s4[] = {"1"};

但往往实际工作中最常用的是下面这种形式:

String[] s = new String[1];

这种形式的数组定义,我们可以抽象为这样一个模型:接收一个int类型的参数,得到一个数组类型。是不是和jdk中的IntFunction函数式接口的方法形式一样了。

@FunctionalInterface
public interface IntFunction<R> {
    R apply(int value);
}

下面的例子中,Stream接口中的toArray方法就是把IntFunction函数式接口作为参数的。在Stream流中,数据的个数就是数组的长度,数据的类型就是数组的类型。

list.stream().map(Ren::getName).toArray(String[]::new);//构造方法引用
		//list.stream().map(n->n.getName()).toArray(n->new String[n]);//lambda表达式写法
		//list.stream().map(new Function<Ren, String>() {//传统写法
		//	@Override
		//	public String apply(Ren t) {
		//		return t.getName();
		//	}
		//}).toArray(new IntFunction<String[]>() {
		//	@Override
		//	public String[] apply(int value) {
		//		return new String[value];
		//	}
		//});

List集合种的toArray虽然也是接收一个IntFunction,但是其底层实现和Stream不同,List转数组(最终调用的是这个“T[] toArray(T[] a);”),不要求数组的长度,只要有这个数组的形式就行了,所以以后遇到List转数组,就还是用之前的老方法最好,省了一步调用。

Ren[] a = new Ren[0];//或a={};
		Ren[] arr = list.toArray(a);//这种才是List转数组的推荐做法
		//这个方法最终还是调用的上面的那个转数组的方法
		list.toArray(Ren[]::new);//构造方法引用
		//list.toArray(n -> new Ren[n]);//lambda表达式写法
		//list.toArray(n -> new Ren[0]);//指定一个长度为0的数组也是可以的,其实底层就是这么做的
		//list.toArray(new IntFunction<Ren[]>() {//传统写法
		//	@Override
		//	public Ren[] apply(int value) {
		//		return new Ren[value];
		//	}});

总结

举一个形象的例子来说明方法引用和方法调用,fun1是一个已经存在的方法,fun2这种方法调用就可以看成方法引用(当然方法引用有它的语法形式,这里是为了加深理解),fun3和fun4都是方法调用。

public void fun1(String s) {
		System.out.println(s);
	}
	public void fun2(String s) {
		fun1(s);
	}
	public void fun3(String s) {
		System.out.println("开始");
		fun1(s);
		System.out.println("结束");
	}
	public void fun4(String s, String s1, String s2) {
		System.out.println("开始"+s1);
		fun1(s);
		System.out.println("结束"+s2);
	}

方法引用,是基于lambda表达式,基于函数式接口来的,和方法调用本质一样,它只是更纯粹的方法调用,除了调用方法,别的什么都不干。我们一般在调用一个方法后,还要加上一些其他操作,但是方法引用就是仅仅调用一个方法。