3.6 方法引用

方法引用让你可以重复使用现有的方法定义,并像Lambda一样传递它们。在一些情况下,比起使用Lambda表达式,它们似乎更易读,感觉也更自然。下面就是我们借助更新的Java 8 API(3.7节会详细讨论),用方法引用写的一个排序的例子:

先前:

  1. inventory.sort((Apple a1, Apple a2)
  2. a1.getWeight().compareTo(a2.getWeight()));

之后(使用方法引用和java.util.Comparator.comparing):

  1. inventory.sort(comparing(Apple::getWeight)); ←---- 你的第一个方法引用

不用担心新的语法及其工作原理,接下来的几节将会对此进行介绍。

3.6.1 管中窥豹

你为什么应该关注方法引用?方法引用可以被看作仅仅调用特定方法的Lambda的一种快捷写法。它的基本思想是,如果一个Lambda代表的只是“直接调用这个方法”,那最好还是用名称来调用它,而不是去描述如何调用它。事实上,方法引用就是让你根据已有的方法实现来创建Lambda表达式。但是,显式地指明方法的名称,你的代码的可读性会更好。它是如何工作的呢?当你需要使用方法引用时,目标引用放在分隔符::前,方法的名称放在后面。例如,Apple::getWeight就是引用了Apple类中定义的方法getWeight。请记住,getWeight后面不需要括号,因为你没有实际调用这个方法,只是引用了它的名称。方法引用就是Lambda表达式(Apple apple) -> apple.getWeight()的快捷写法。表3-4给出了Java 8中方法引用的其他一些例子。

表 3-4 Lambda及其等效方法引用的例子

Lambda 等效的方法引用
(Apple apple) -> apple.getWeight() Apple::getWeight
() -> Thread.currentThread().dumpStack() Thread.currentThread()::dumpStack
(str, i) -> str.substring(i) String::substring
(String s) -> System.out.println(s) (String s) -> this.isValidName(s) System.out::println this::isValidName

你可以把方法引用看作针对仅仅涉及单一方法的Lambda的语法糖,因为你表达同样的事情时要写的代码更少了。

如何构建方法引用

方法引用主要有三类。

(1) 指向静态方法的方法引用(例如IntegerparseInt方法,写作Integer::parseInt)。

(2) 指向任意类型实例方法的方法引用(例如Stringlength方法,写作String::length)。

(3) 指向现存对象或表达式实例方法的方法引用(假设你有一个局部变量expensive Transaction保存了Transaction类型的对象,它提供了实例方法getValue,那你就可以这么写expensiveTransaction::getValue)。

第二种和第三种方法引用可能乍看起来有点儿晕。第二种方法引用的思想是你在引用一个对象的方法,譬如String::length,而这个对象是Lambda表达式的一个参数。举个例子,Lambda表达式(String s) -> s.toUppeCase()可以重写成String::toUpperCase。而第三种方法引用主要用在你需要在Lambda中调用一个现存外部对象的方法时。例如,Lambda表达式()->expensiveTransaction.getValue()可以重写为expensiveTransaction::getValue。第三种方法引用在你需要传递一个私有辅助方法时特别有用。譬如,你定义了一个辅助方法isValidName

  1. private boolean isValidName(String string) {
  2. return Character.isUpperCase(string.charAt(0));
  3. }

你可以借助方法引用,在Predicate的上下文中传递该方法:

  1. filter(words, this::isValidName)

为了帮助你消化这些新知识,我们准备了一份将Lambda表达式重构为等价方法引用的简易速查表,如图3-5所示。

3.6 方法引用 - 图1

图 3-5 为三种不同类型的Lambda表达式构建方法引用的办法

请注意,构造函数、数组构造函数以及父类调用(super-call)的方法引用形式比较特殊。举一个方法引用的具体例子。假设你想要忽略大小写对一个由字符串组成的List排序。Listsort方法需要一个Comparator作为参数。前文介绍过,Comparator使用(T, T) -> int这样的签名作为函数描述符。你可以利用String类中的compareToIgnoreCase方法来定义一个Lambda表达式(注意compareToIgnoreCaseString类中预先定义的)。

  1. List<String> str = Arrays.asList("a","b","A","B");
  2. str.sort((s1, s2) -> s1.compareToIgnoreCase(s2));

Lambda表达式的签名与Comparator的函数描述符兼容。利用前面所述的方法,这个例子可以用方法引用改写成下面的样子,这样代码更加简洁了:

  1. List<String> str = Arrays.asList("a","b","A","B");
  2. str.sort(String::compareToIgnoreCase);

请注意,编译器会进行一种与Lambda表达式类似的类型检查过程,来确定对于给定的函数式接口,这个方法引用是否有效:方法引用的签名必须和上下文类型匹配。

为了检验你对方法引用的理解程度,试试测验3.6吧!

测验3.6:方法引用

下列Lambda表达式的等效方法引用是什么?

(1) ToIntFunction stringToInt = (String s) -> Integer.parseInt(s);

(2) BiPredicate, String> contains = (list, element) -> list.contains(element);

(3) Predicate startsWithNumber = (String string) -> this .startsWithNumber(string);

答案:(1) 这个Lambda表达式将其参数传给了Integer的静态方法parseInt。这种方法接受一个需要解析的String,并返回一个Integer。因此,可以使用图3-5中的办法➊(Lambda表达式调用静态方法)来重写Lambda表达式,如下所示:

  1. ToIntFunction<String> stringToInt = Integer::parseInt;

(2) 这个Lambda使用其第一个参数,调用其contains方法。由于第一个参数是List类型的,因此你可以使用图3-5中的办法➋,如下所示:

  1. BiPredicate<List<String>, String> contains = List::contains;

这是因为,目标类型描述的函数描述符是(List,String) -> boolean,而List::contains可以被解包成这个函数描述符。

(3) 这种“表达式–风格”的Lambda会调用一个私有方法。你可以使用图3-5中的办法❸,如下所示:

  1. Predicate<String> startsWithNumber = this::startsWithNumber

到目前为止,我们只展示了如何利用现有的方法实现和如何创建方法引用。但是你也可以对类的构造函数做类似的事情。

3.6.2 构造函数引用

对于一个现有构造函数,你可以利用它的名称和关键字new来创建它的一个引用:ClassName::new。它的功能与指向静态方法的引用类似。例如,假设有一个构造函数没有参数。它适合Supplier的签名() -> Apple。你可以这样做:

  1. Supplier<Apple> c1 = Apple::new; ←---- 构造函数引用指向默认的Apple()构造函数
  2. Apple a1 = c1.get(); ←---- 调用Supplierget方法将产生一个新的Apple

这就等价于:

  1. Supplier<Apple> c1 = () -> new Apple(); ←---- 利用默认构造函数创建AppleLambda 表达式
  2. Apple a1 = c1.get(); ←---- 调用Supplierget方法将产生一个新的Apple

如果你的构造函数的签名是Apple(Integer weight),那么它就适合Function接口的签名,于是你可以这样写:

  1. Function<Integer, Apple> c2 = Apple::new; ←---- 指向Apple(Integer weight)的构造函数引用
  2. Apple a2 = c2.apply(110); ←---- 调用该Function函数的apply方法,并给出要求的重量,将产生一个Apple

这就等价于:

  1. Function<Integer, Apple> c2 = (weight) -> new Apple(weight); ←---- 用要求的重量创建一个AppleLambda表达式
  2. Apple a2 = c2.apply(110); ←---- 调用该Function函数的apply方法,并给出要求的重量,将产生一个新的Apple对象

在下面的代码中,一个由Integer构成的List中的每个元素都通过前面定义的类似的map方法传递给了Apple的构造函数,得到了一个具有不同重量苹果的List

  1. List<Integer> weights = Arrays.asList(7, 3, 4, 10);
  2. List<Apple> apples = map(weights, Apple::new); ←---- 将构造函数引用传递给map方法
  3. public List<Apple> map(List<Integer> list, Function<Integer, Apple> f) {
  4. List<Apple> result = new ArrayList<>();
  5. for(Integer i: list) {
  6. result.add(f.apply(i));
  7. }
  8. return result;
  9. }

如果你有一个具有两个参数的构造函数Apple(String color, Integer weight),那么它就适合BiFunction接口的签名,于是你可以这样写:

  1. BiFunction<Color, Integer, Apple> c3 = Apple::new; ←---- 指向Apple(String color, Integer weight)的构造函数引用
  2. Apple a3 = c3.apply(GREEN, 110); ←---- 调用该BiFunction函数的apply方法,并给出要求的颜色和重量,将产生一个新的Apple对象

这就等价于:

  1. BiFunction<String, Integer, Apple> c3 = ←---- 用要求的颜色和重量创建一个AppleLambda表达式
  2. (color, weight) -> new Apple(color, weight);
  3. Apple a3 = c3.apply(GREEN, 110); ←---- 调用该BiFunction函数的apply方法,并给出要求的颜色和重量,将产生一个新的Apple对象

不将构造函数实例化却能够引用它,这个功能有一些有趣的应用。例如,你可以使用Map来将构造函数映射到字符串值。你可以创建一个giveMeFruit方法,给它一个String和一个Integer,它就可以创建出不同重量的各种水果:

  1. static Map<String, Function<Integer, Fruit>> map = new HashMap<>();
  2. static {
  3. map.put("apple", Apple::new);
  4. map.put("orange", Orange::new);
  5. // etc...
  6. }
  7. public static Fruit giveMeFruit(String fruit, Integer weight){
  8. return map.get(fruit.toLowerCase()) ←---- 你用map得到了一个Function<Integer, Fruit>
  9. .apply(weight); ←---- Integer类型的weight参数调用Functionapply()方法将提供所要求的Fruit
  10. }

为了检验你对方法和构造函数引用的理解程度,试试测验3.7吧!

测验3.7:构造函数引用

你已经看到了如何将有零个、一个、两个参数的构造函数转变为构造函数引用。那要怎么样才能对具有三个参数的构造函数,比如RGB(int, int, int),使用构造函数引用呢?

答案:你看,构造函数引用的语法是ClassName::new,那么在这个例子里面就是RGB::new。但是你需要与构造函数引用的签名匹配的函数式接口。由于语言本身并没有提供这样的函数式接口,因此你可以自己创建一个:

  1. public interface TriFunction<T, U, V, R> {
  2. R apply(T t, U u, V v);
  3. }

现在你可以像下面这样使用构造函数引用了:

  1. TriFunction<Integer, Integer, Integer, RGB> colorFactory = RGB::new;

我们讲了好多新内容:Lambda、函数式接口和方法引用。下一节会把这一切付诸实践!