本人接触Java 8的时间不长,对Java 8的一些新特性略有所知。Java 8引入了一些新的编程概念,比如经常用到的 lambda表达式、Stream、Optional以及Function等,让人耳目一新。这些功能其实上手并不是很难,根据别人的代码抄过来改一下,并不要知道内部的实现原理,也可以很熟练地用好这些功能。但是当我深究其中一些细节时,会发现有一些知识的盲区。下面我就来谈一下Java 8中的Method References这个概念。
首先我给出官方对于这一概念的详细解释,https://docs.oracle.com/javase/tutorial/java/javaOO/methodreferences.html。本文虽不是简单的翻译官方文档,但是还是有必要简要的介绍一下这一概念。
1. 什么是方法引用(Method References)
方法引用(Method References)是一个与Lambda表达式、函数式接口(Functional Inferface)紧密关联的概念。如我们所知,函数式接口(Functional Inferface)是一种有且仅有一个抽象方法的接口。而Lambda表达式是Java 8引入的对于函数式接口更加简洁的实现方式。方法引用(Method References)则是另一种对函数式接口的实现方式。下面是Java 8中的一个函数式接口(Functional Inferface)的一般性定义,它可以拥有一个或多个default 方法和 static方法,但只能拥有一个抽象方法(这里abstract关键字可以被省略)。
/** *这就是一个Functional Interface,无论加不加注解@FunctionalInterface,这都是一个Functional Interface。 */ public interface A { public abstract void method1(int a); public default void method2(int b) { //Do something }; public static void method3(int c) { //Do something }; }
如果有其他的方法需要以 Interface A的实例作为入参时,在Lambda表达式出现之前,我们一般会使用匿名内部类的方式来处理。如下所示:
public class B {
//类B的某一个方法的入参需要传入接口A的一个实例 public void method1(A a) { a.method1(1); a.method2(2); } public static void main(String args[]){ B b = new B(); //使用匿名内部类实现函数式接口A的唯一抽象方法,并传入实例 b.method1(new A() { @Override public void method1(int a) { //Do something } }); } }
Lambda表达式出现以后,我们开始使用下面这种方式:
public class B {
//类B的某一个方法的入参需要传入接口A的一个实例 public void method1(A a) { a.method1(1); a.method2(2); } public static void main(String args[]){ B b = new B(); //使用Lambda表达式实现函数式接口的唯一抽象方法 b.method1( (a)->{ /*Do something*/ }); } }
使用方法引用(Method References),可以将上面的代码转换为如下代码:
public class B { //类B的某一个方法的入参需要传入接口A的一个实例 public void method1(A a) { a.method1(1); a.method2(2); } //类B的两个静态方法 public static void method2(int a) { //Do something } public static int method3(int a) { //Do something return 1; } public static void main(String args[]){ B b = new B(); //使用匿名内部类实现函数式接口的唯一抽象方法 b.method1((a)->{/*Do something*/}); b.method1(B::method2); b.method1(B::method3);//由于接口中的方法返回类型是void,此处会丢弃麽B::method3的返回值
}
}
这种用类名加两个冒号的写法,就是方法引用(Method References)。有点类似于C语言的函数指针,将一个方法作为另一个方法的入参。在面向对象语言中,这是一种将方法对象化的方式。在我们已经有现成的方法时,不需要再去实现一遍这个方法,只需要把现有的方法视为一个对象,将它的引用作为入参,比Lambda表达式还要方便快捷。
2. 方法引用的种类
官方的文档给出了4中类型的方法引用:
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 |
Reference to a static method就是我们上面代码中所示,将一个static method作为方法引用。
Reference to an instance method of a particular object也很好理解,当我们需要使用某个方法时,发现它不是static method,这时我们需要先生成一个拥有这个方法的对象实例,然后通过实例的引用标识符去调用这个方法:
public class B { //类B的某一个方法的入参需要传入接口A的一个实例 public void method1(A a) { a.method1(1); a.method2(2); } //类B的两个静态方法 public static void method2(int a) { //Do something } public static int method3(int a) { //Do something return 1; }
//类B的两个成员方法 public void method4(int a) { //Do something } public int method5(int a) { //Do something return 1; } public static void main(String args[]){ B b = new B(); //使用匿名内部类实现函数式接口的唯一抽象方法 b.method1((a)->{/*Do something*/}); b.method1(B::method2); b.method1(B::method3);//由于接口中的方法返回类型是void,此处会丢弃B::method3的int返回值 B anotherB=new B(); b.method1(anotherB::method4); b.method1(anotherB::method5);//同上,由于接口中的方法返回类型是void,此处会丢弃anotherB::method5的int返回值
}
}
大家应该注意到了,这里有一个比较特殊的处理,虽然接口A中的方法method1的返回类型为void,但仍然可以传入一个返回类型为int的方法引用。如果接口A的返回类型为int,方法引用的返回参数可以是byte,但不能是long。这里大家如果感兴趣可以继续深入的研究。
Reference to an instance method of an arbitrary object of a particular type,这个相对复杂一点,官方文档也一笔带过了,这里我们再深入一点。首先我们先分析官方文档中的例子:
The following is an example of a reference to an instance method of an arbitrary object of a particular type: String[] stringArray = { "Barbara", "James", "Mary", "John", "Patricia", "Robert", "Michael", "Linda" }; Arrays.sort(stringArray, String::compareToIgnoreCase); The equivalent lambda expression for the method reference |
这里需要先知道Arrays.sort和Comparator的代码大概做了什么:
public class Arrays { //.......... public static <T> void sort(T[] a, Comparator<? super T> c) { if (c == null) { sort(a); } else { if (LegacyMergeSort.userRequested) legacyMergeSort(a, c); else TimSort.sort(a, 0, a.length, c, null, 0, 0); } } //............. }
这里相当于将String::compareToIgnoreCase作为方法引用去实现Comparator接口,那么Comparator接口中有什么呢?其实就是下面代码中描述的:
@FunctionalInterface public interface Comparator<T> { int compare(T o1, T o2); }
而String中的compareToIgnoreCase却只有一个入参,与Comparator中的compare方法参数列表不一致:
Class String{ //...... public int compareToIgnoreCase(String str) { return CASE_INSENSITIVE_ORDER.compare(this, str); } //....... }
这时在使用Arrays.sort(stringArray, String::compareToIgnoreCase);时 sort方法中是不知道传过来的是String::compareToIgnoreCase的,依然会使用类似c.compare(stringArray[i],stringArray[j])的语句去比较字符串。但这时实际执行的是stringArray[i].compareToIgnoreCase(stringArray[j])。这是官方的一个例子,下面我们来自己写一个更为普遍一些的例子。
如下所示,接口A中的method1有两个入参,第一个入参为class B的实例,在main方法中我们向B.method1中传递了一个方法引用B::method2。我们已经知道,如果method2是B中的静态方法,我们可以使用B::method2,否则我们只能先new一个B的实例,比如 B b=new B(); 然后使用b::method2。这里method2不是一个静态方法,但是我们仍然使用了B::method2,为什么呢?这就是方法引用的Reference to an instance method of an arbitrary object of a particular type。这时在B.method1中,并不知道参数a具体是通过什么方式实现的,有可能是用匿名内部类,有可能是lambda表达式,有可能是其他类的静态方法等等。所以在B.method1中只能使用接口A声明的方式去调用。但是实际上,Java 8在这里进行了处理,对方法引用进行了转换,接口方法中的第一个参数,映射为实例的引用,第二个参数才是这个实例的方法中的参数。方法引用--这一概念最好的体现就在于此,将B::method2赋值给接口A的一个引用,即使参数个数不同也可以赋值。
@FunctionalInterface public interface A { public abstract int method1(B b,int a); } public class B { public static void method1(A a) { a.method1(new B(),1); } public int method2(int a) { return 1;} public static void main(String args[]){ B.method1(B::method2); } }
Reference to a constructor,这种方法引用的特殊之处在于使用ClassName::new来表示构造函数。当然,官方文档中已经解释的很好了,我这里仅做一下概括。如下所示,一般的文章会把B.method1(B::new)等同于lambda表达式B.method1( ()->{return new B()} ), 但在下面的例子中,上述转换无法编译。因为接口A中的抽象方法method1的返回值为void。但这时仍可以使用B::new,也可以正常打印出1024。
@FunctionalInterface public interface A { public abstract void method1(int a); } public class B { int value; public static void method1(A a) { a.method1(1024); } public B(int value) { this.value=value; System.out.println(value); } public static void main(String args[]){ B.method1(B::new);
B.method1( ()->{return new B()} ) //compile error } }
结语
方法引用(Method References)的上述4种使用场景十分灵活,与lambda表达式、函数式接口(Functional Inferface)共同组成了Java对于方法对象化的实现。在此基础上又扩展出了Java 8的Function包中的众多类,而Function包又对Stream包等一系列包提供了强大的支持。Java8 的编程因此变得更为灵活。