浅谈Java 8中的方法引用(Method References)

Posted yuhouchuqing

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了浅谈Java 8中的方法引用(Method References)相关的知识,希望对你有一定的参考价值。

  本人接触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中类型的方法引用:

KindExample
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 String::compareToIgnoreCase would have the formal parameter list (String a, String b), where a and bare arbitrary names used to better describe this example. The method reference would invoke the method a.compareToIgnoreCase(b).

  这里需要先知道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 的编程因此变得更为灵活。

 







以上是关于浅谈Java 8中的方法引用(Method References)的主要内容,如果未能解决你的问题,请参考以下文章

java 8 方法引用(method references)

浅谈Java中的栈和堆

浅谈Java对象的equals方法

浅析Java 8新特性Method Reference

浅谈Java中的对象和引用

vue 父组件调用子组件方法ref