【Java8】 方法引用 讲解

一、概述

在学习 lambda 表达式之后,我们通常使用 lambda 表达式来创建匿名方法。然而,有时候我们仅仅是调用了一个已存在的方法。如下:

Arrays.sort(stringsArray,(s1,s2)->s1.compareToIgnoreCase(s2));

 在Java8中,我们可以直接通过方法引用简写 lambda 表达式中已经存在的方法。

Arrays.sort(stringsArray, String::compareToIgnoreCase);

这种特性就叫做方法引用(Method Reference)。

二、什么是方法引用

     方法引用是用来直接访问类或者实例的已经存在的方法或者构造方法。方法引用提供了一种引用而不执行方法的方式,它需要由兼容的函数式接口构成的目标类型上下文。计算时,方法引用会创建函数式接口的一个实例

当Lambda表达式中只是执行一个方法调用时,不用Lambda表达式,直接通过方法引用的形式可读性更高一些。方法引用是一种更简洁易懂的Lambda表达式。

注意方法引用是一个Lambda表达式,其中方法引用的操作符是双冒号"::"。

简单地说,就是一个Lambda表达式。在Java 8中,我们会使用Lambda表达式创建匿名方法,但是有时候,我们的Lambda表达式可能仅仅调用一个已存在的方法,而不做任何其它事,对于这种情况,通过一个方法名字来引用这个已存在的方法会更加清晰,Java 8的方法引用允许我们这样做。方法引用是一个更加紧凑,易读的Lambda表达式,注意方法引用是一个Lambda表达式,其中方法引用的操作符是双冒号"::"

三、方法引用例子

先看一个例子,首先定义一个Person类,如下:

package mytest;

import java.time.LocalDate;
import java.util.Arrays;
import java.util.Comparator;



class Person
{

	public Person(String name, LocalDate birthday)
	{
		this.name = name;
		this.birthday = birthday;
	}

	String name;
	LocalDate birthday;

	public LocalDate getBirthday()
	{
		return birthday;
	}

	//  字符串的对比用如下方法
	public static int compareByAge(Person a, Person b)
	{
		return a.birthday.compareTo(b.birthday);
	}

	@Override
	public String toString()
	{
		return this.name;
	}
}

/**
 * 假设我们有一个Person数组,并且想对它进行排序,这时候,我们可能会这样写:原始写法,使用匿名类:
 */

public class CRacer
{
	public static void main(String[] args)
	{
		Person[] pArr = new Person[]{
				new Person("003", LocalDate.of(2016, 9, 1)),
				new Person("001", LocalDate.of(2016, 2, 1)),
				new Person("002", LocalDate.of(2016, 3, 1)),
				new Person("004", LocalDate.of(2016, 12, 1))};

		// 使用匿名类 实现 对比函数
		Arrays.sort(pArr, new Comparator<Person>()
		{
			@Override
			public int compare(Person a, Person b)
			{
				return a.getBirthday().compareTo(b.getBirthday());
			}
		});

		System.out.println(Arrays.asList(pArr));

	}


}

其中,Arrays类的sort方法定义如下:

public static <T> void sort(T[] a, Comparator<? super T> c)

这里,我们首先要注意Comparator接口是一个函数式接口,因此我们可以使用Lambda表达式,而不需要定义一个实现Comparator接口的类,并创建它的实例对象,传给sort方法。

package mytest;

import java.time.LocalDate;
import java.util.Arrays;
import java.util.Comparator;


class Person
{

	public Person(String name, LocalDate birthday)
	{
		this.name = name;
		this.birthday = birthday;
	}

	String name;
	LocalDate birthday;

	public LocalDate getBirthday()
	{
		return birthday;
	}

	//  字符串的对比用如下方法
	public static int compareByAge(Person a, Person b)
	{
		return a.birthday.compareTo(b.birthday);
	}

	@Override
	public String toString()
	{
		return this.name;
	}
}

/**
 * 假设我们有一个Person数组,并且想对它进行排序,这时候,我们可能会这样写:原始写法,使用匿名类:
 */

public class CRacer
{
	public static void main(String[] args)
	{
		Person[] pArr = new Person[]{
				new Person("003", LocalDate.of(2016, 9, 1)),
				new Person("001", LocalDate.of(2016, 2, 1)),
				new Person("002", LocalDate.of(2016, 3, 1)),
				new Person("004", LocalDate.of(2016, 12, 1))};

		// 使用匿名类 实现 对比函数
		Arrays.sort(pArr, new Comparator<Person>()
		{
			@Override
			public int compare(Person a, Person b)
			{
				return a.getBirthday().compareTo(b.getBirthday());
			}
		});

		System.out.println(Arrays.asList(pArr));


		//使用lambda表达式
		Arrays.sort(pArr, (Person a, Person b) -> a.getBirthday().compareTo(b.getBirthday()) );

		System.out.println(Arrays.asList(pArr));

		// 使用已经有的对比条件
		//使用lambda表达式和类的静态方法
		Arrays.sort(pArr, (a ,b) -> Person.compareByAge(a, b));

		System.out.println(Arrays.asList(pArr));

		// 使用方法引用 实现
		//使用方法引用,引用的是类的静态方法  方法不需要加括号
		Arrays.sort(pArr, Person::compareByAge);

		System.out.println(Arrays.asList(pArr));

	}
}

在以上代码中,方法引用 Person::compareByAge 在语义上与Lambda表达式 (a, b) -> Person.compareByAge(a, b) 是等同的,都有如下特性:

  • 真实的参数是拷贝自Comparator<Person>.compare方法,即(Person, Person);
  • 表达式体调用Person.compareByAge方法。

四、四种方法引用类型

方法引用的标准形式是:类名::方法名。(注意:只需要写方法名,不需要写括号

有以下四种形式的方法引用:

类型 示例
引用静态方法 ContainingClass::staticMethodName
引用某个对象的实例方法 containingObject::instanceMethodName
引用某个类型的任意对象的实例方法 ContainingType::methodName
引用构造方法 ClassName::new

使用方法引用的主要步骤

  1. 定义一个函数式接口
  2. 定义一个满足函数式接口抽象方法要求的方法
  3. 使用对步骤2中定义的(x::y)方法引用实例化函数式接口的实例(注:静态方法的方法引用格式为 类名::方法名;实例方法的方法引用格式为 对象实例名::方法名)
  4. 使用函数式接口实例调用方法:Instance.AbstractMethod();

1、静态方法引用

组成语法格式:ClassName::staticMethodName

我们前面举的例子Person::compareByAge就是一个静态方法引用

注意:

  • 静态方法引用比较容易理解,和静态方法调用相比,只是把 换为 ::
  • 在目标类型兼容的任何地方,都可以使用静态方法引用。

例子:

  String::valueOf   等价于lambda表达式 (s) -> String.valueOf(s)

  Math::pow          等价于lambda表达式  (x, y) -> Math.pow(x, y);

字符串翻转例子:

package mytest;

interface StringFunc
{
	String func(String n);
}

class MyStringOps
{
	public static String strReverse(String str)
	{
		StringBuilder result = new StringBuilder();
		for (int i = str.length() - 1; i >= 0; i--)
		{
			result.append(str.charAt(i));
		}
		return result + "";
	}
}

public class CRacer
{
	public static String stringOp(StringFunc sf, String s)
	{
		return sf.func(s);
	}

	public static void main(String[] args)
	{
		String inStr = "labmda add power to Java";
		//MyStringOps::strReverse 相当于实现了接口方法func()
		// 并在接口方法func()中作了MyStringOps.strReverse()操作
		//表达式MyStringOps::strReverse的计算结果为对象引用,其中,strReverse提供了StringFunc的func()方法的实现。
		String outStr = stringOp(MyStringOps::strReverse, inStr);
		System.out.println("Original string: " + inStr);
		System.out.println("String reserved: " + outStr);

	}
}

查找最大值demo

package mytest;

import java.util.ArrayList;
import java.util.Collections;

class MyClass
{

	private int val;

	MyClass(int v)
	{
		this.val = v;
	}

	public int getValue()
	{
		return this.val;
	}

}

public class CRacer
{
	public static int compareMC(MyClass a, MyClass b)
	{
		return a.getValue() - b.getValue();
	}

	public static void main(String[] args)
	{
		ArrayList<MyClass> a1 = new ArrayList<MyClass>();
		a1.add(new MyClass(1));
		a1.add(new MyClass(4));
		a1.add(new MyClass(2));
		a1.add(new MyClass(9));
		a1.add(new MyClass(3));
		a1.add(new MyClass(7));
		//UseMethodRef::compareMC 生成了抽象接口Comparator定义的compare()方法的实例。
       // UseMethodRef定义了静态方法compareMC(),它与Comparator定义的compare()方法兼容。
       //因此,没有必要显式的实现Comparator接口并创建其实例。
		MyClass maxValObj = Collections.max(a1, CRacer::compareMC);
		System.out.println("Maximum value is: " + maxValObj.getValue());
	}

}

2、特定实例对象的方法引用

这种语法与用于静态方法的语法类似,只不过这里使用对象引用而不是类名。实例方法引用又分以下三种类型:

2.1.实例上的实例方法引用

组成语法格式:instanceReference::methodName

如下示例,引用的方法是myComparisonProvider 对象的compareByName方法。

class ComparisonProvider{
    
    public int compareByName(Person a, Person b){
        return a.getName().compareTo(b.getName());
    }

    public int compareByAge(Person a, Person b){
        return a.getBirthday().compareTo(b.getBirthday());
    }
}

ComparisonProvider myComparisonProvider = new ComparisonProvider();
Arrays.sort(rosterAsArray, myComparisonProvider::compareByName);
      public static void main(String[] args) {
        String inStr = "lambda add power to Java";
        MyStringOps strOps = new MyStringOps();//实例对象
        //strOps::strReverse1 相当于实现了接口方法func() 
        String outStr = stringOp(strOps::strReverse1, inStr);

        System.out.println("Original string: " + inStr);
        System.out.println("String reserved: " + outStr);
    }
唯一区别就是一个传入的是 class:StaticFun  object:PublicFun

2.2超类上的实例方法引用

组成语法格式:super::methodName

方法的名称由methodName指定,通过使用super,可以引用方法的超类版本。

例子:

还可以捕获this 指针,this :: equals  等价于lambda表达式  x -> this.equals(x);

2.3.类型上的实例方法引用

组成语法格式:ClassName::methodName

注意:

若类型的实例方法是泛型的,就需要在::分隔符前提供类型参数,或者(多数情况下)利用目标类型推导出其类型。

静态方法引用和类型上的实例方法引用拥有一样的语法。编译器会根据实际情况做出决定。一般我们不需要指定方法引用中的参数类型,因为编译器往往可以推导出结果,但如果需要我们也可以显式在::分隔符之前提供参数类型信息。

例子:

String::toString 等价于lambda表达式 (s) -> s.toString()

这里不太容易理解,实例方法要通过对象来调用,方法引用对应Lambda,Lambda的第一个参数会成为调用实例方法的对象。

在泛型类或泛型方法中,也可以使用方法引用。

package mytest;

interface MyFunc<T>
{

	int func(T[] als, T v);
}

class MyArrayOps
{

	public static <T> int countMatching(T[] vals, T v)
	{
		int count = 0;
		for (int i = 0; i < vals.length; i++)
		{
			if (vals[i] == v)
			{
				count++;
			}
		}
		return count;
	}

}

public class CRacer
{
	public static <T> int myOp(MyFunc<T> f, T[] vals, T v)
	{
		return f.func(vals, v);
	}

	public static void main(String[] args)
	{
		Integer[] vals = {1, 2, 3, 4, 2, 3, 4, 4, 5};
		String[] strs = {"One", "Two", "Three", "Two"};
		int count;
		//count = myOp(MyArrayOps::<Integer>countMatching, vals, 4); // 泛型 实现统计
		count = myOp(MyArrayOps::countMatching, vals, 4); // 泛型 实现统计
		System.out.println("vals contains " + count + " 4s");
		// count = myOp(MyArrayOps::<String>countMatching, strs, "Two");
		count = myOp(MyArrayOps::countMatching, strs, "Two");
		System.out.println("strs contains " + count + " Twos");
	}

}

分析:

      在程序中,MyArrayOps是非泛型类,包含泛型方法countMatching()。该方法返回数组中与指定值匹配的元素的个数。注意这里如何指定泛型类型参数。例如,在main()方法中,对countMatching()方法的第一次调用如下所示:count = myOp(MyArrayOps::<Integer>countMatching,vals,4); 这里传递了类型参数Integer。

      注意,参数传递发生在::的后面。这种语法可以推广。当把泛型方法指定为方法引用时,类型参数出现在::之后、方法名之前。但是,需要指出的是,在这种情况(和其它许多情况)下,并非必须显示指定类型参数,因为类型参数会被自动推断得出。对于指定泛型类的情况,类型参数位于类名的后面::的前面。

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

在Java 8 In Action是这样介绍的,指向任意类型实例方法的方法引用(我觉得叫类的任意对象的实例方法引用更直观)。我开始一直想当然的就认为是类::实例方法这样就可以了,结果写了几个发现都用不了,看了官网给出的示例发现又可以,于是,作为当代优秀青年,怎么可能不解决这个问题呢。

官网给出的示例:

String[] stringArray = { "Barbara", "James", "Mary", "John", "Patricia", "Robert", "Michael", "Linda" };
Arrays.sort(stringArray, String::compareToIgnoreCase);

 方法引用的等效lambda表达式 String::compareToIgnoreCase 将具有形式参数列表(String a, String b),其中a和b是用于更好地描述此示例的任意名称。方法引用将调用该方法 a.compareToIgnoreCase(b)。我反正是没看懂这是讲的啥。之后再查了下,原文是这样的 “ reference to an instance method of an arbitrary object of a particular type ”  arbitrary 任意的, particular 特定的,翻译过来就是引用特定类型的任意对象的实例方法。于是乎,知道了类的实例方法调用是有讲究的。那么,有什么样的条件呢?

public class test1 {
    public static void main(String[] args) {
 
        List<Student> list = new ArrayList<>();
        list.add(new Student("Jack1", 88));
        list.add(new Student("Jack2", 81));
        list.add(new Student("Jack3", 82));
        list.add(new Student("Jack4", 83));
        list.add(new Student("Jack5", 84));
        list.add(new Student("Jack6", 85));
        list.sort(Student::compareByScore);
        System.out.println(list);
    }
}
 
class Student {
    private String name;
    private int score;
 
    public Student(){
 
    }
 
    public Student(String name,int score){
        this.name = name;
        this.score = score;
    }
 
    public String getName() {
        return name;
    }
 
    public void setName(String name) {
        this.name = name;
    }
 
    public int getScore() {
        return score;
    }
 
    public void setScore(int score) {
        this.score = score;
    }
 
    public int compareByScore(Student student){
        return this.getScore() - student.getScore();
    }
 
    @Override
    public String toString(){
        return this.name + "  " + this.score + "  ";
    }
}

list.sort接受一个Comparator,Comparator需要实现compare方法,lambda形式 (v1,v2)-> {dosth;return intval;}。这里的compareByScore就是这种lambda形式的方法引用。

如果将上面的compareByScore方法改成如下形式,为什么又要报错了,我不就多传了一个参数进去么,其他的啥也没做啊。
 

 public int compareByScore(Student student1, Student student2){
        return this.getScore() - student1.getScore();
    }

    不好意思,它要求接口方法的参数必须比引用方法的参数多一个。而且第一个参数要是该引用方法的所在类型的或其父类,除接口方法的第一个参数以外, 其余参数的类型要求一样。这一段话,有些难以理解,看下面的代码就会明白了(也就是Interface参数个数要比我们引用方法参数个数多一个)。

demo1

package mytest;

class Test1
{
	public void a()
	{
		System.out.println("just a test " + this.hashCode());
	}

	public static void main(String[] args)
	{
		MyInter m = Test1::a;
		System.out.println(m.hashCode());
		Test1  object = new Test1();
		System.out.println(object.hashCode());
		m.d(object);
	}
}

@FunctionalInterface
interface MyInter
{
	// 入参参数比Test1的a方法多一个,接口方法的第一个参数恰巧是调用引用方法的对象
	void d(Test1 b);
	//void d(int b); 这个会报错
}

demo2

class Test1
{
	public void a(int param1, int param2)
	{
		System.out.println("just a test");
	}

	public static void main(String[] args)
	{

		MyInter m = Test1::a;
	}
}

@FunctionalInterface
interface MyInter
{
	//该接口参数比上述的a方法参数数量多一个,除去第一个,其它类型一致(可兼容,如可以一个int,一个Integer)
	//且 interface 中 虚函数的一个参数一定是 引用方法所在类 类型
	void d(Test1 d, int param1, int param2);
}

demo3

class Father
{
	public void a(Integer param1, int param2)
	{
	}
}
==========================================
class Son extends Father
{

	public static void main(String[] args)
	{
		MyInter m = Son::a;  //  这个是不对的!
		m.fun(new Son(), 12, 13); // 实参  需要是 形参的同类或者子类
	}
}

@FunctionalInterface
interface MyInter
{
	//该接口参数比上述的a方法参数数量多一个,除去第一个,其它类型一致(可兼容,如可以一个int,一个Integer)
	// interface 虚函数第一个参数 类型
	void fun(Father d, int param1, int param2);
}

==========================================
class Son extends Father
{
	public static void main(String[] args)
	{
		MyInter m = Father::a; // 该类要是 interface 中的同类或父类
		m.fun(new Son(), 12, 13); // 实参  需要是 形参的同类或者子类
	}
}

@FunctionalInterface
interface MyInter
{
	//该接口参数比上述的a方法参数数量多一个,除去第一个,其它类型一致(可兼容,如可以一个int,一个Integer)
	/*
		interface第一个参数	引用函数类型	状态
		Father			Father		OK
		Son			Son		OK
		Son 			Father 		OK
		Father			Son		NO
	* */
	void fun(Son d, int param1, int param2);
}

demo4

class Test1
{
	public void a(Integer param1, int param2)
	{
	}

	public static void main(String[] args)
	{
		MyInter m = (j, k, l) -> j.a(k, l);
       //第一个参数为方法目标,其余参数为参数
	}
}

@FunctionalInterface
interface MyInter
{
	void d(Test1 d, int param1, int param2);
}

 这个指向任意类型实例方法的方法引用有两个要求
       第一点:接口方法(interface)的参数比引用方法的参数多一个
       第二点:接口方法的第一个参数恰巧是调用引用方法的对象,或者是引用方法类型的子类。

interface第一个参数 引用函数类型 状态是否正确
Father Father OK
Son Son OK
Son Father OK
Father Son NO

4、构造方法引用

构造方法引用又分构造方法引用和数组构造方法引用。

构造函数的方法引用,它们是对象初始化方法。
使用构造函数的方法引用的主要步骤有:

  1. 定义一个只有抽象方法的函数式接口,该interface 中抽象方法的返回类型与使用该对象进行构造函数引用的对象相同。
  2. 创建一个类,该类的构造函数与函数式接口的抽象方法匹配。
  3. 使用对步骤#2中定义的构造函数的方法引用,实例化函数式接口的实例。(构造函数的方法引用格式为 类名::new)
  4. 在步骤#2中使用构造函数引用实例化类的实例。

构造函数引用与泛型一起使用时会变得更有用。通过使用泛型工厂方法,可以创建各种类型的对象。可以结合下面例子重点理解下思想。

4.1.构造方法引用(也可以称作构造器引用)

组成语法格式:Class::new

构造函数本质上是静态方法,只是方法名字比较特殊,使用的是new 关键字

例子:

String::new, 等价于lambda表达式 () -> new String() 

package mytest;

interface MyFunc1
{

	MyClass func(int n);
	// 会有个类的构造方法来实现该函数

}

class MyClass
{

	private int val;

	MyClass(int v)
	{
		val = v;
	}

	MyClass()
	{
		val = 0;
	}

	public int getValue()
	{
		return val;
	}

}

public class CRacer
{
	public static void main(String[] args)
	{

		MyFunc1 myClassCons1 = MyClass -> new MyClass();
		// 跟下面同样效果  相当于用构造函数 实现了interface 的 虚函数 切记
		MyFunc1 myClassCons = MyClass::new;
		// 等价于 MyFunc1 myClassCons = new MyClass();
		MyClass mc = myClassCons.func(100);
		System.out.println("val in mc is: " + mc.getValue());
	}

}

4.2.数组构造方法引用

组成语法格式:TypeName[]::new

例子:

int[]::new 是一个含有一个参数的构造器引用,这个参数就是数组的长度。等价于lambda表达式  x -> new int[x]。

假想存在一个接收int参数的数组构造方法

IntFunction<int[]> arrayMaker = int[]::new;
int[] array = arrayMaker.apply(10) // 创建数组 int[10]
package mytest;

import java.util.function.Function;

interface MyFunc1
{
	MyClass func(int n);
	// 会有个类的构造方法来实现该函数
}

class MyClass
{

	private int val;

	MyClass(int v)
	{
		val = v;
	}

	MyClass()
	{
		val = 0;
	}

	public int getValue()
	{
		return val;
	}

}

public class CRacer
{
	public static void main(String[] args)
	{

		MyFunc1 myClassCons1 = MyClass -> new MyClass();
		// 跟下面同样效果  相当于用构造函数 实现了interface 的 虚函数 切记
		MyFunc1 myClassCons = MyClass::new;
		// 等价于 MyFunc1 myClassCons = new MyClass();
		MyClass mc = myClassCons.func(100);
		System.out.println("val in mc is: " + mc.getValue());
		
		/*
		* 数组引用的创建 第一个参数是传入数组长度,第二个参数是传入数组类型, 真正实例化的时候跟  Scala 中 apply 类似来构造
		* 多看源码即可理解思路
		* */
		Function<Integer, String[]> function = (x) -> new String[x];
		Function<Integer, String[]> functionNew = String[]::new;
		String[] abc = function.apply(12);
		System.out.println(function.apply(12).length + functionNew.apply(33).length);
	}

}

挺好的一个例子参考:

package mytest;

import java.util.Arrays;
import java.util.List;

@FunctionalInterface
interface Supplier<T>
{
	T get();
}

class Car
{
	public static Car create(final Supplier<Car> supplier)
	{
		return supplier.get();
	}

	public static void collide(final Car car)
	{
		System.out.println("静态方法形式调用 " + car.toString());
	}

	public void follow(final Car another)
	{
		System.out.println("对象方法形式调用 " + another.toString());
	}

	public void repair()
	{
		System.out.println("任意对象方法引用 " + this.toString());
	}

	@Override
	public String toString()
	{
		return "just a Car " + this.hashCode();
	}
}

public class CRacer
{
	public static void main(String[] args)
	{
		//构造器引用:它的语法是Class::new,或者更一般的Class< T >::new实例如下:
		final Car car = Car.create(Car::new);
		System.out.println(car.hashCode());
		final List<Car> cars = Arrays.asList(car);

		//静态方法引用:它的语法是Class::static_method,实例如下:
		cars.forEach(Car::collide);

		//特定类的任意对象的方法引用:它的语法是Class::method实例如下:
		cars.forEach(Car::repair);
        		cars.forEach(System.out::println); 
		//  forEach 应该接受的要是cars的父类,而我们传入的函数 
          //   println 是 Object 是所以类型的父类 因此可以执行。 再理解下 多态
		 
		//特定对象的方法引用:它的语法是instance::method实例如下:
		final Car police = Car.create(Car::new);
		System.out.println(police.hashCode());
		cars.forEach(police::follow);
	}
}

参考:

  1. https://blog.csdn.net/qq_32403063/article/details/86002379
  2. https://www.jb51.net/article/162534.htm
  3. https://www.cnblogs.com/sunyubin/p/9751412.html
  4. https://docs.oracle.com/javase/tutorial/java/javaOO/methodreferences.html
  5. https://stackoverflow.com/questions/32855138/how-does-a-method-reference-to-an-instance-method-of-an-arbitrary-object-of-a-p
  6. https://stackoverflow.com/questions/25512532/instance-method-reference-and-lambda-parameters
  7. https://segmentfault.com/a/1190000012269548
  8. https://blog.csdn.net/qwe125698420/article/details/53415746
  9. https://blog.csdn.net/learningcoding/article/details/72539918
  10. https://blog.idrsolutions.com/2015/02/java-8-method-references-explained-5-minutes/
  11. java8 默认方法
 
关注公众号 海量干货等你
原文地址:https://www.cnblogs.com/sowhat1412/p/12734138.html