Java函数式编程的方法引用(2)

90 阅读12分钟

方法引用

方法引用可以被看作仅仅调用特定方法的Lambda的一种快捷 写法。它的基本思想是,如果一个Lambda代表的只是“直接调用这个方法”,那最好还是用名称来调用它,而不是去描述如何调用它。

事实上,方法引用就是根据已有的方法实现来创建 Lambda表达式。但是,显式地指明方法的名称让代码的可读性会更好。

如何使用方法引用

方法引用可以重复使用现有的方法定义,并像Lambda一样传递它们。在一些情况下, 比起使用Lambda表达式,方法引用似乎更易读。

下面就是借助更新的Java 8 API,用方法引用写的一个排序的例子。

使用Lambda表达式实现排序:

list.sort((Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight()));

使用方法引用:

list.sort(Comparator.comparing(Apple::getWeight));

事实上,方法引用就是根据已有的方法实现来创建 Lambda表达式。显式地指明方法的名称让代码的可读性会更好。

当需要使用方法引用时,目标引用放在分隔符::前,方法的名称放在后面。例如, Apple::getWeight就是引用了Apple类中定义的方法getWeight。

PS: 请记住,不需要括号,因为现在还没有实际调用这个方法。

如何构建方法引用?

方法引用主要有三类:

  • 指向静态方法的方法引用(例如Integer的parseInt方法,写作Integer::parseInt)
  • 指向任意类型实例方法的方法引用(例如String的length方法,写作 String::length)
  • 指向现有对象的实例方法的方法引用(假设有一个局部变量expensiveTransaction 用于存放Transaction类型的对象,它支持实例方法getValue,那么就可以写expensiveTransaction::getValue)

List的sort方法需要一个Comparator作为参数。

Comparator描述了 一个具有(T,T)->int签名的函数描述符。

利用String类中的compareToIgnoreCase方法来定义一个Lambda表达式(注意compareToIgnoreCase是String类中预先定义的)。

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

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

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

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

构造函数引用

对于一个现有构造函数,你可以利用它的名称和关键字new来创建它的一个引用: ClassName::new。它的功能与指向静态方法的引用类似。

无参构造函数

如果构造函数没有参数,那么适合使用Supplier的签名:() → T

Supplier<Apple> c1 = Apple::new;
Apple a1 = c1.get();

等价于:

Supplier<Apple> c1 = () -> new Apple();
Apple a1 = c1.get();

一个参数的构造函数

如果构造函数的签名是Apple(Integer weight),只有一个参数,那么它就适合Function接口的签名:

Function<Integer, Apple> c2 = Apple::new;
Apple a2 = c2.apply(110);

等价于:

Function<Integer, Apple> c2 = (weight) -> new Apple(weight);
Apple a2 = c2.apply(110);

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

List<Integer> weights = Arrays.asList(7, 3, 4, 10);
List<Apple> apples = map(weights, Apple::new);

public static List<Apple> map(List<Integer> list, Function<Integer, Apple> f){
    List<Apple> result = new ArrayList<>();
    for(Integer e: list){
        result.add(f.apply(e));
    }
    return result;
}

两个参数的构造函数

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

BiFunction<String, Integer, Apple> c3 = Apple::new;
Apple c3 = c3.apply("green", 110);

等价于:

BiFunction<String, Integer, Apple> c3 = (color, weight) -> new Apple(color, weight);
Apple c3 = c3.apply("green", 110);

复合 Lambda 表达式的有用方法

Java8 的好几个函数式接口都有为方便而设计的方法。具体而言,许多函数式接口,比如用于传递Lambda表达式的Comparator、Function和Predicate都提供了允许进行复合的方法。 这是什么意思呢? 在实践中,这意味着可以把多个简单的Lambda复合成复杂的表达式。比如可以让两个谓词之间做一个or操作,组合成一个更大的谓词。而且还可以让一个函数的结果成为另一个函数的输入。

Java8的函数式接口提供了三种复合方式:

  • 比较器复合
  • 谓词复合
  • 函数复合

比较器复合

假设现在使用静态方法Comparator.comparing比较两个苹果的重量,如下所示:

package org.xqd.learning.analyzemybatissourcecode.controller;

import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;

public class LearnConsumer {
    public static void main(String[] args) {
        List<Apple> list = new ArrayList<>();
        
        Apple a1 = new Apple();
        a1.setWeight(1);
        
        Apple a2 = new Apple();
        a2.setWeight(3);
        
        Apple a3 = new Apple();
        a3.setWeight(2);
        
        list.add(a1);
        list.add(a2);
        list.add(a3);
        
        Comparator<Apple> c = Comparator.comparing(Apple::getWeight);
        list.sort(c);
        list.forEach(System.out::println);
    }
}

输出的结果是一个升序。

逆序

如果你想要对苹果按重量递减排序怎么办?再建一个Comparator接口吗?

package org.xqd.learning.analyzemybatissourcecode.controller;

import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;

public class LearnConsumer {
    public static void main(String[] args) {
        List<Apple> list = new ArrayList<>();
        
        Apple a1 = new Apple();
        a1.setWeight(1);

        Apple a2 = new Apple();
        a2.setWeight(3);
        
        Apple a3 = new Apple();
        a3.setWeight(2);

        list.add(a1);
        list.add(a2);
        list.add(a3);

        Comparator<Apple> c = Comparator.comparing(Apple::getWeight).reversed();
        Comparator<Apple> inverseWeightComparator = (Apple a, Apple b) -> Integer.compare(b.getWeight(), a.getWeight());

        list.sort(c);
        list.sort(inverseWeightComparator);
        list.forEach(System.out::println);
    }
}

inverseWeightComparator是一个降序的比较器。

然后再将这个比较器应用到list上。

但远远不用这么复杂,接口已经提供一个默认方法reversed可以使给定的比较器逆序。因此仍然用开始的那个比较器,只要修改一下前一个例子就可以对苹果按重量递减排序:

Comparator<Apple> c = Comparator.comparing(Apple::getWeight).reversed();

reversed()方法直接替代了下面的代码:

Comparator<Apple> inverseWeightComparator = (Apple a, Apple b) -> Integer.compare(b.getWeight(), a.getWeight());

比较器链

如果两个对象用第一个Comparator比较之后是一样的,需要再提供第二个 Comparator进行比较,那么此时就需要:thenComparing() 方法,可以让所有的比较器形成比较器链。

Comparator<Apple> c = Comparator.comparing(Apple::getWeight).reversed().thenComparing(Apple::getColor);
list.sort(c);

谓词复合

谓词接口包括三个方法:negate、and和or,可以重用已有的Predicate来创建更复杂的谓词。

函数复合

Java 8 提供了把Function接口所代表的Lambda表达式复合起来的方法。

  • andThen
  • compose

这两个方法是默认方法,它们都会返回Function的一个实例。

andThen方法会返回一个函数,它先对输入应用一个给定函数,再对输出应用另一个函数。

假设有一个函数f给数字加1 (x -> x + 1),另一个函数g给数字乘2,你可以将它们组 合成一个函数h,先给数字加1,再给结果乘2:

Function<Integer, Integer> f = x -> x + 1;
Function<Integer, Integer> g = x -> x * 2;
Function<Integer, Integer> h = f.andThen(g);
int result = h.apply(1);

h.apply(1) 会先执行f函数,再执行g函数。

result是4。

看一下Function接口的andThen方法定义:

default <V> Function<T, V> andThen(Function<? super R, ? extends V> after) {
    Objects.requireNonNull(after);
    return (T t) -> after.apply(apply(t));
}

入参字段是after,顾名思义就是后执行。

整个方法先执行apply(t),得到返回结果,再执行after.apply()方法。

类似地还有compose方法,和andThen方法相反,会先执行入参函数,再执行调用compose方法的函数。

default <V> Function<V, R> compose(Function<? super V, ? extends T> before) {
    Objects.requireNonNull(before);
    return (V v) -> apply(before.apply(v));
}

入参字段是before,顾名思义是先执行。

整个方法先执行before.apply(y),然后得到的结果作为apply()方法的入参。

比如在上一个例子里用compose的话,它将意味着f(g(x)), 而andThen则意味着g(f(x)):

Function<Integer, Integer> f = x -> x + 1;
Function<Integer, Integer> g = x -> x * 2;
Function<Integer, Integer> h = f.compose(g);
int result = h.apply(1);

result是3。

Java8的函数式接口

函数式接口定义且只定义了一个抽象方法(除了抽象方法,其他方法不限制,比如默认方法,使用default定义)。

函数式接口的抽象方法的签名称为函数描述符,为了应用不同的Lambda表达式,需要一套能够描述常见函数描述符的函数式接口。

Java 8在java.util.function包中引入了几个新的函数式接口,下面依次进行介绍。

Predicate

java.util.function.Predicate接口定义了一个名叫test的抽象方法,它接受泛型T对象,并返回一个boolean。

@FunctionalInterface
public interface Predicate<T> {
    boolean test(T t);
}

如果想用Predicated的test方法,入参可以是任意类型,但是出参必须的结果必须是Boolean类型。

所以,这个函数式接口的函数描述符是(T t) → boolean.

示例:返回集合中非空的字符串

import java.util.ArrayList;
import java.util.List;
import java.util.function.Predicate;

public class LearnFunction {
    public static <T> List<T> filter(List<T> list, Predicate<T> p) {
        List<T> results = new ArrayList<>();
        for (T s : list) {
            if (p.test(s)) {
                results.add(s);
            }
        }
        return results;
    }

    public static void main(String[] args) {
        //定义集合
        List<String> list = List.of("a", "b", "c", "", "e");
        //定义Predicate的Lambda表达式
        Predicate<String> isEmpty = (String s) -> !s.isEmpty();
        List<String> filter = filter(list, isEmpty);
        //输出最后的结果
        filter.forEach(System.out::println);
    }
}

Predicate的其他方法:

and方法: andPredicate的默认方法,它返回一个组成的谓词,表示这个谓词和其他谓词的短路逻辑AND。在评估组成的谓词时,如果这个谓词是假的,那么其他谓词将不会被评估。在错误的情况下,如果此谓词抛出错误,那么其他谓词将不会被评估。

default Predicate<T> and(Predicate<? super T> other) {
    Objects.requireNonNull(other);
    return (t) -> test(t) && other.test(t);
}

在指定泛型T时,必须保证泛型T的类型一致。

使用示例:

import java.util.function.Predicate;

public class PredicateAndDemo {
    public static void main(String[] args) {
        Predicate<Student> isMaleStudent = s -> s.getAge() >= 20 && "male".equals(s.getGender());
				Predicate<Student> isFemaleStudent = s -> s.getAge() > 18 && "female".equals(s.getGender());
				Predicate<Student> isStudentPassed = s -> s.getMarks() >= 33;

				// Testing if male student passed.
				Student student1 = new Student("Mahesh", 22, "male", 30);
				Boolean result = isMaleStudent.and(isStudentPassed).test(student1);
				System.out.println(result); //false

				// Testing if female student passed.
				Student student2 = new Student("Gauri", 19, "female", 40);
				result = isFemaleStudent.and(isStudentPassed).test(student2);
				System.out.println(result); //true
    }
} 

negate方法: negatePredicate的默认方法,它返回一个表示该谓词的逻辑否定的谓词。如果评估的结果是真的,negate将使它变成假的,如果评估的结果是假的,negate将使它变成真的。

default Predicate<T> negate() {
    return (t) -> !test(t);
}

使用示例:

import java.util.function.Predicate;
public class PredicateNegateDemo {
	  public static void main(String[] args) {
				Predicate<Integer> isNumberMatched = n -> n > 10 && n < 20;
        //With negate()
        Boolean result = isNumberMatched.negate().test(15);
        System.out.println(result); //false
       
        //Without negate()
        result = isNumberMatched.test(15);
        System.out.println(result); //true
		}
} 

or方法: orPredicate的默认方法,它返回一个组成的谓词,表示此谓词和其他谓词的短路逻辑OR。在评估组成的谓词时,如果此谓词为真,那么其他谓词将不会被评估。在错误的情况下,如果此谓词抛出错误,那么其他谓词将不会被评估。

default Predicate<T> or(Predicate<? super T> other) {
    Objects.requireNonNull(other);
    return (t) -> test(t) || other.test(t);
}

在指定泛型T时,必须保证泛型T的类型一致。

使用示例:

import java.util.function.Predicate;
public class PredicateOrDemo {
		public static void main(String[] args) {
				Predicate<Student> isMaleStudent = s -> s.getAge() >= 20 && "male".equals(s.getGender());
				Predicate<Student> isFemaleStudent = s -> s.getAge() > 18 && "female".equals(s.getGender());
				Predicate<Student> isStudentPassed = s -> s.getMarks() >= 33;

				Student student1 = new Student("Mahesh", 22, "male", 35);
				
				//Test either male or female student
				Boolean result = isMaleStudent.or(isFemaleStudent).test(student1);
				System.out.println(result); //true
				
				//Is student passed, too
				result = isMaleStudent.or(isFemaleStudent).and(isStudentPassed).test(student1);
				System.out.println(result); //true
		}
} 

isEqual方法:isEqualPredicate的静态方法,它返回测试两个参数是否等于Objects.equals(Object, Object)的谓词。

static <T> Predicate<T> isEqual(Object targetRef) {
    return (null == targetRef)
            ? Objects::isNull
            : object -> targetRef.equals(object);
}

注意这是一个静态方法。

使用示例:

import java.util.function.Predicate;
public class PredicateIsEqualDemo {
		public static void main(String[] args) {
        System.out.println("---Testing Hello message---");	
        Predicate<String> isHelloMsg = Predicate.isEqual("Hello");
        System.out.println(isHelloMsg.test("Hello")); //true
        System.out.println(isHelloMsg.test("Hi"));  //false
  }
}

not 方法: not返回的predicate是对所提供的predicate的否定,通过返回调用target.negate()的结果实现。


@SuppressWarnings("unchecked")
static <T> Predicate<T> not(Predicate<? super T> target) {
    Objects.requireNonNull(target);
    return (Predicate<T>)target.negate();
}

注意:notJava 11中引入的Predicate的静态方法。

使用示例:

import java.util.function.Predicate;
public class PredicateNotDemo {
		public static void main(String[] args) {
		    Predicate<Integer> isOdd = n -> n % 2 == 1;
		    Predicate<Integer> isEven = Predicate.not(isOdd);
		    System.out.println(isEven.test(10)); //true
    
		    Predicate<String> isNotHelloMsg = Predicate.not(Predicate.isEqual("Hello"));
		    System.out.println(isNotHelloMsg.test("Hi")); //true
		    System.out.println(isNotHelloMsg.test("Hello")); //false
	  }
} 

Consumer

java.util.function.Consumer定义了一个名叫accept的抽象方法,它接受泛型T的对象,没有返回(void)。

@FunctionalInterface
public interface Consumer<T>{
    void accept(T t);
}

如果需要访问类型T的对象,并对其执行某些操作,且不想返回任何结果,那么就可以使用这个接口。

Consumer其他方法:

@FunctionalInterface
public interface Consumer<T> {
    default Consumer<T> andThen(Consumer<? super T> after) {
        Objects.requireNonNull(after);
        return (T t) -> { accept(t); after.accept(t); };
    }
}

Supplier

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

Function

java.util.function.Function<T, R>接口定义了一个叫作apply的方法,它接受一个 泛型T的对象,并返回一个泛型R的对象。

@FunctionalInterface
public interface Function<T, R>{
    R apply(T t);
}

如果需要定义一个Lambda,将输入对象的信息映射到输出,就可以使用这个接口(比如提取苹果的重量,或把字符串映射为它的长度)。

Function其他方法:

@FunctionalInterface
public interface Function<T, R> {
    default <V> Function<V, R> compose(Function<? super V, ? extends T> before) {
        Objects.requireNonNull(before);
        return (V v) -> apply(before.apply(v));
    }

    default <V> Function<T, V> andThen(Function<? super R, ? extends V> after) {
        Objects.requireNonNull(after);
        return (T t) -> after.apply(apply(t));
    }

    static <T> Function<T, T> identity() {
        return t -> t;
    }
}

最后

方法引用是Lambda的进一步简化。

无论是方法引用,还是lambda表达式,原则就是它们的签名要和想要使用的函数式接口中抽象方法的签名保持一致

以Comparator为例,list.sort方法需要一个Comparator的实例,实现方式有很多种。

一、实现Comparator接口

static class MyComparator implements Comparator<Apple> {
		@Override
    public int compare(Apple a1, Apple a2) {
				return a1.getWeight() - a2.getWeight();
    }
}

二、使用lambda表达式

Comparator<Apple> inverseWeightComparator = (Apple a, Apple b) -> a.getWeight() - b.getWeight() < 0 ? -1 : (a.getWeight() == b.getWeight() ? 0 : 1);
//或
Comparator<Apple> inverseWeightComparator = (Apple a, Apple b) -> b.compare(, a.getWeight());

三、使用方法引用

Comparator<Apple> c = Comparator.comparing(Apple::getWeight);

最后三种方式返回的都是一个Comparator接口的实例。

lambda表达式的签名是:(Apple, Apple) → int

方法引用的签名是:(Apple, Apple) → int

为什么方法引用的签名和lambda表达式一致呢?先看一下comparing方法:

public static <T, U extends Comparable<? super U>> Comparator<T> comparing(Function<? super T, ? extends U> keyExtractor){
    Objects.requireNonNull(keyExtractor);
    return (Comparator<T> & Serializable)
        (c1, c2) -> keyExtractor.apply(c1).compareTo(keyExtractor.apply(c2));
}

comporting的入参是一个Function函数式接口。

Function函数式接口的抽象方法方法签名是: T → U

也就是入参的类型是T,返回一个U。

只要符合这个条件,那么就可以传给comparing。

Apple的getWeight方法入参是Apple,出参是int,正好符合Function的签名。