06_fp_02_method_reference

第六十一章 Method Reference表达式

Java 8 引入了一个新的概念:Method Reference表达式(Method Reference Expression)。在以前的版本中,Java语言无法抽象一个方法。例如:将方法赋值给一个变量,或者将方法作为参数传入另一个方法。在C语言中,这些可以通过函数指针实现。然而,Java语言采用了不同的实现方式和策略。在本章中,我们先介绍Method Reference。Method Reference为我们划定了哪些方法是可以作为一等公民(First-Class Citizen)的。在Method Reference和Functional Interface的协助下,Java编译器能够将一个函数转化为一个Functional Interface的对象,从而,在Java语言上,开发人员能像使用其他类对象那样传递函数了。

1 简介

就像在Java标准文档(Java SE Specification)中介绍的那样,Method Reference抽象的是一个方法调用,但是,这个方法调用并没有触发真正的方法调用。这句话有点难于理解,不过,简单地说,Method Reference提供了一个方法调用的环境(或者上下文)。Method Reference与真实的方法调用一模一样,唯一的区别是,Method Reference并没有发生方法调用。但是,当Method Reference被方法时,就像它在原处被方法的一样。

A method reference expression is used to refer to the invocation of a method without actually performing the invocation. -- The Java Language Specification, Java SE 13 Edition

2 用法

举例是一个很好的帮助理解Method Reference表达式的办法。从用法上看,Method Reference可分为四类。

  1. 指向静态函数的Method Reference (Reference to a static method) 在下面的例子中定义了一个车辆类Vehicle。comparePrice是Vehicle类的一个静态函数。那么,Vehicle::comparePrice是一个指向Vehicle类中comparePrice这个静态函数的Method Reference。因此,它可以传入Arrays.sort函数中,将vehicles这个数组按照车辆的价格排序。注:Java编译器会将Method Reference转换为Functional Interface的一个对象,并传入Arrays.sort函数中。我们会在后续章节介绍这个转换过程。
import java.util.Arrays;

public class Vehicle {
    protected Integer price;

    public static int comparePrice(Vehicle v1, Vehicle v2) {
        return v1.price.compareTo(v2.price);
    }

    public static void main(String[] args) {
        Vehicle[] vehicles = {};
        Arrays.sort(vehicles, Vehicle::comparePrice);
    }
}
  1. 指向对象的成员方法的Method Reference (Reference to an instance method of a particular object) 在这个例子中,我们创建了一个VehicleComparisonProvider类,专门用于比较Vehicle对象。在该类中定义了两个比较方法compareByPrice和compareByAge。这两个方法都是成员方法。在main函数中,myProvider::compareByPrice作为一个Method Reference传入Arrays.sort。需要注意的是,本例将VehicleComparisonProvider的一个对象myProvider绑定在这个Method Reference中。在此处,myProvider::compareByPrice并未触发函数调用,而是将对象myProvider和成员函数compareByPrice一起绑定在了这个Method Reference中。当需要调用这个Method Reference的时候,Java虚拟机能够找到这个成员方法对应的对象。因此,我们又称这种情况为Instance-bound Method Reference。
import java.util.Arrays;

public class VehicleComparisonProvider {
    public int compareByPrice(Vehicle v1, Vehicle v2) {
        return v1.getPrice().compareTo(v2.getPrice());
    }

    public int compareByAge(Vehicle v1, Vehicle v2) {
        return v1.getAge().compareTo(v2.getAge());
    }

    public static void main(String[] args) {
        Vehicle[] vehicles = {};
        VehicleComparisonProvider myProvider = new VehicleComparisonProvider();
        Arrays.sort(vehicles, myProvider::compareByPrice);
    }
}

当在成员方法中将另一个成员方法或者父类的成员方法作为Method Reference传递时,可使用关键字this和super指定所绑定的对象。如下例所示。ChildVehicleComparisonProvider类继承自VehicleComparisonProvider。在其成员方法sortByBrand中,如果使用当前类的compareByBrand成员方法作为比较方法,可将this::compareByBrand传递给Arrays.sort函数。这个用法与上例中的myProvider::compareByPrice类似。唯一不同的是,本例中的Method Reference绑定的是this指向的对象。

同理,当需要传递在父类中定义的成员方法时,可使用super::compareByPrice或者ChildVehicleComparisonProvider.super::compareByPrice作为Method Reference传递。super指向的是当前的父类对象。Java语言要求当使用 类名.super::函数名 格式时,类名必须是当前的类名,即包含super::函数名这个表达式的类名。

此类Method Reference的使用方法(或者格式)不能用于静态函数。

import java.util.Arrays;

public class ChildVehicleComparisonProvider extends VehicleComparisonProvider {
    public void sortByBrand(Vehicle[] vehicles) {
        Arrays.sort(vehicles, this::compareByBrand);
    }

    public int compareByBrand(Vehicle v1, Vehicle v2) {
        return v1.getBrand().compareTo(v2.getBrand());
    }

    public void sortByPrice(Vehicle[] vehicles) {
        Arrays.sort(vehicles, super::compareByPrice);
        Arrays.sort(vehicles, ChildVehicleComparisonProvider.super::compareByPrice);
    }
}
  1. 指向任意对象的成员方法的Method Reference (Reference to an instance method of an arbitrary object of a particular type) 与前一种情况不同的是,此例中的Method Reference并未绑定一个具体的对象,因为Vehicle::comparePrice仅指出了是类Vehicle中的comparePrice函数。在运行Arrays.sort函数时,给定两个车辆对象v1和v2,该Reference Method的调用实际上类似于v1.comparePrice(v2)。值得注意的是,车辆数组vehicles包含的元素必须都是Vehicle类的对象。
import java.util.Arrays;

public class Vehicle {
    protected Integer price;
    
    public Vehicle(Integer price) {
        this.price = price;
    }

    public int comparePrice (Vehicle v) {
        return this.price.compareTo(v.price);
    }

    public static void main(String[] args) {
        Vehicle[] vehicles = {new Vehicle(100), new Vehicle(200)};
        Arrays.sort(vehicles, Vehicle::comparePrice);
    }
}
  1. 指向构造函数 的Method Reference(Reference to a constructor) 指向构造函数的Method Reference的用法与指向静态函数的用法类似,只不过,因为构造函数没有名字,所以,关键字"new"特指构造函数。如下例所示,这是一个Vehicle工厂类的例子。在VehicleFactory中,Vehicle::new作为指向Vehicle构造函数的Method Reference传入了newVehicle函数。

    Method Reference还可用于数组的构造函数。例如,newVehicleArray函数接受一个创建Vehicle数组的工厂函数或者构造函数,size参数指明数组的长度。Vehicle[]::new是这个数组构造函数的Method Reference。Java编译器将其转换成一个functional interface对象,传入newVehicleArray函数。

import java.util.function.Function;

public class VehicleFactory {
    public static Vehicle newVehicle(Function<Integer, Vehicle> factory, Integer price) {
        return factory.apply(price);
    }

    public static Vehicle[] newVehicleArray(Function<Integer, Vehicle[]> factory, Integer size) {
        return factory.apply(size);
    }

    public static void main(String[] args) {
        Vehicle i = VehicleFactory.newVehicle(Vehicle::new, 100);
        Vehicle[] vehicles = VehicleFactory.newVehicleArray(Vehicle[]::new, 10);
    }
}

3 Method Reference的处理过程

Method Reference的处理过程非常复杂,在这里,我们仅仅大致介绍一下思路。Method Reference的处理过程可分为三个步骤。

  1. 第一个步骤是分析Method Reference的格式。如上述所列出的四类Method Reference,它们均有其各自的格式。Java编译器会根据格式,提取出相对应的类名(或者对象名)和方法名。然后,利用函数的参数列表在类中查找符合入参数量和类型的候选函数。
  2. 第二个步骤是分析Method Reference与Functional Interface是否兼容匹配。Method Reference可出现在赋值语句(Assignment Context),函数调用语句(Invocation Context),和类型转换语句(Type Casting Context)。这些语句的语义均是将Method Reference转换成一个Functional Interface的对象。因此,Java编译器需要检查这些语句是否合法,类型是否匹配。
  3. 第三个步骤是调用Method Reference。因为在第二个步骤中,Java编译器会将Method Reference转换为一个Functional Interface类的实例。不同的编译器实现方法不同。但是,一个常见的实现方法是编译器会创建一个匿名类,这个匿名类实现了目标Functional Interface的抽象函数。在函数的实现中,间接地调用了这个Method Reference。所以,调用一个Method Reference就和调用一个成员方法一样。成员方法的名字就是Functional Interface中新增的抽象方法的名字。

4 结语

Java 8为了更好的支持函数式编程,推出了Method Reference表达式、Lambda表达式Functional Interface的概念。Method Reference的设计目的是为了将类继承框架下的函数/方法引入函数式编程。Method Reference表达式支持静态函数、成员函数、和构造函数。因此,在函数式编程思想下,开发人员也能复用这些函数,为打通面向对象编程和函数式编程打下了基础。

 

上一章
下一章

注册用户登陆后可留言

Copyright  2019 Little Waterdrop, LLC. All Rights Reserved.