java8学习笔记(2)-方法引用和构造器引用

2019/06/13

简介

方法引用有点像 C/C++ 中的函数指针,它能让我们更容易地以函数式风格编程。Java8 中引入了双冒号 :: 来表示方法引用。

使用方法引用时,如果存在方法重载,编译器会自动根据上下文选择参数列表匹配的方法。

在 JDK 的源码注释和 JavaDoc 中,会将方法引用和构造器引用区分表述,但本文认为构造器引用可以看作一种特殊的方法引用。

类名::静态方法名

public class Main {

    public static void main(String[] args) {
        List<Integer> list = Arrays.asList(0, 1, 2, 3, 4, 5, 6, 7);
        list.forEach(i -> System.out.println(i * i));
    }
}

上面这段代码遍历了一个整数列表,打印每个整数的平方。其中 Lambda 表达式 i -> System.out.println(i * i) 就是针对每个整数所执行的操作,我们可以把这一操作写成静态方法,然后用方法引用的形式传给 forEach 方法:

public class Main {

    public static void print(Integer i) {
        System.out.println(i * i);
    }

    public static void main(String[] args) {
        List<Integer> list = Arrays.asList(0, 1, 2, 3, 4, 5, 6, 7);
        // 类名::静态方法名
        list.forEach(Main::print);
    }
}

在语句 list.forEach(Main::print); 中,list 的实例方法 void forEach(Consumer<? super T> action) 本应接收一个 Consumer<? super Integer> 参数,用 Lambda 表达式可以表示为 (Integer i)->{},即一个 void function(Integer) 函数。通过方法引用 Main::print,实际上引用了 Main 类的静态方法 public static void print(Integer i),符合 forEach 方法的参数要求。

我们来看看 forEach(Consumer<? super T> action) 的源码:

default void forEach(Consumer<? super T> action) {
    Objects.requireNonNull(action);
    for (T t : this) {
        action.accept(t);
    }
}

所以,执行 list.forEach(Main::print); 最终就相当于执行了这样一段代码;

for (Integer i : list) {
    Main.print(i);
}

Consumer 接口的函数式方法是 void accept(T t),而在 T 取 Integer 时,Main 类的 void print(Integer i) 方法正好返回类型和参数列表都能与之对应,所以方法 Main::print 可以作为 forEach 方法的参数,实际上它是一个匿名的 Consumer 实例。

对象::实例方法名

直接上代码:

public class Student {
    private String name;

    public void hello(Student other) {
        System.out.println("我是" + name + "," + other.name + "你好");
    }

    public Student(String name) {
        this.name = name;
    }

    public static void main(String[] args) {
        List<Student> students = Arrays.asList(
                new Student("张三"),
                new Student("李四"),
                new Student("王五")
        );
        Student zhaoliu = new Student("赵六");
        students.forEach(zhaoliu::hello);
    }
}

运行结果:

我是赵六,张三你好
我是赵六,李四你好
我是赵六,王五你好

在上面这段代码中,students.forEach(zhaoliu::hello); 传入了形如 对象::实例方法名 的方法引用 zhaoliu::hello。这一句实际上相当于下面这段代码:

for (Student student : students) {
    zhaoliu.hello(student);
}

类名::实例方法名

前面两种方法引用都比较好理解,它们和我们通过类名调用静态方法、通过对象名调用实例方法是很相似的。

类名::实例方法名 看起来就有些奇怪.不过,我们最终调用的肯定还是对应方法名的实例方法,既然是调用实例方法,总是要对应到某个具体对象的。

直接上代码:

public class Student {
    private String name;

    public void hello() {
        System.out.println("我是" + name);
    }

    public Student(String name) {
        this.name = name;
    }

    public static void main(String[] args) {
        List<Student> students = Arrays.asList(
                new Student("张三"),
                new Student("李四"),
                new Student("王五")
        );
        students.forEach(Student::hello);
    }
}

运行结果:

我是张三
我是李四
我是王五

不难看出,在语句 students.forEach(Student::hello); 中,把方法引用 Student::hello 传给 forEach,最终实际上相当于执行了下面这段代码:

for (Student student : students) {
    student.hello();
}

再看另一个例子:

public class Student {
    private String name;

    public Student(String name) {
        this.name = name;
    }

    public int compare(Student other) {
        return name.compareTo(other.name);
    }

    @Override
    public String toString() {
        return "Student{" +
                "name='" + name + '\'' +
                '}';
    }

    public static void main(String[] args) {
        List<Student> students = Arrays.asList(
                new Student("2张三"),
                new Student("1李四"),
                new Student("3王五")
        );
        students.sort(Student::compare);
        students.forEach(System.out::println);
    }
}

运行结果:

Student{name='1李四'}
Student{name='2张三'}
Student{name='3王五'}

List 的 sort(Comparator<? super E> c) 方法以 Comparator 为参数,而 Comparator 接口的函数式方法是 int compare(T o1, T o2),它需要接收两个参数。

如果用 Lambda 表达式的形式来实现排序,应该写成 students.sort((s1, s2) -> s1.name.compareTo(s2.name));,这个 Lambda 表达式所表达的函数是要接收两个参数的(和Comparator的compare方法一致)。而在上面这个例子中,Student 类的 compare(Student other) 方法只接收一个参数。

那为什么 Student::compare 也能充当一个匿名的 Comparator 实例呢?首先,Student 类的 compare 方法返回值类型也是 int。然后,假设有两个 Student 对象 student1 和 student2,我们比较一下 student1.compare(student2)compare(student1,student2),再结合上面的第一个例子,不难推测,在使用类名::实例名这种方法引用时,其实就是把 this 对象作为 compare(T o1, T o2) 中的 o1,而把 Student 类的 compare(Student other) 方法的参数 other 作为 compare(T o1, T o2) 中的 o2

类名::new

还是直接上代码:

public class Main {

    public static void print(Supplier<String> supplier) {
        System.out.println("test:" + supplier.get());
    }

    public static void print(String string, Function<String, String> function) {
        System.out.println("test:" + function.apply(string));
    }

    public static void main(String[] args) {
        print(String::new);
        print("hello", String::new);
    }
}

运行结果:

test:
test:hello

语句 print(String::new); 中的 String::new 对应的是 String 类的无参构造方法,而 print("hello", String::new); 中的 String::new 对应的是 String 类的构造方法 String(String original)

调用那个构造方法,是由函数式方法的参数列表决定的。Supplier 接口的函数式方法 get() 不接收参数,而 Function 接口的 apply(T t) 方法接收一个参数,所以编译器会根据上下文自动选择合适的构造方法。

总结

和 Lambda 表达式一样,方法引用允许我们方便地传递行为,甚至它比 Lambda 表达式还要更加简洁。善用 Lambda 表达式和方法引用,是写好函数式风格 Java 代码的基础。


(转载本站文章请注明作者和出处)

Show Disqus Comments

Post Directory