方法引用的那些事儿

一句话介绍:

方法引用Method Reference)是在 Lambda 表达式的基础上引申出来的一个功能。

先不铺展概念,从一个示例开始说起。

一、小示例

List<Integer> list = Arrays.asList(1, 2, 3);
list.forEach(num -> System.out.println(num));

上面是一个很普通的 Lambda 表达式:遍历打印列表的元素。

相比 JDK8 版本以前的 for  循环或 Iterator 迭代器方式,这种 Lambda 表达式的写法已经是一种很精简且易读的改进。但有没有更精简的改进?

答案是有!下面就有请方法引用出场:

list.forEach(System.out::println);

没用过这种方式的小伙伴,可能会纳闷:这是什么鬼?为什么编译器竟然不报错?该怎么理解?

这其实就是一种方法引用。中间的两个冒号“::”,就是 Java 语言中方法引用的特有标志,出现它,就说明使用到了方法引用。

因为 Foreach() 方法的形参是 Consume<T> 对象,所以,上面方法引用的方式等同于如下表达:

Consumer<Integer> consumer = System.out::print;
list.forEach(consumer);

有木有很神奇?System.out::print  语句的左值可以是一个 Consumer 对象。从编译器的角度来理解,等号右侧的语句是一种方法引用,那么编译器会认为该语句引用的是 Consumer 接口的 accept(T t) 抽象方法。

下面来细细拆分一下输出语句:System.out.println();

System 是一个可不变类,包含了多个域变量和静态方法,之所以能使用 System.out 这种形式,就因为 out 是它的一个静态变量,且是一个  PrintStream  对象:

/**
 * The "standard" output stream. This stream is already
 * open and ready to accept output data. Typically this stream
 * corresponds to display output or another output destination
 * specified by the host environment or user.
 * <p>
 * For simple stand-alone Java applications, a typical way to write
 * a line of output data is:
 * <blockquote><pre>
 *     System.out.println(data)
 * </pre></blockquote>
 * <p>
 * See the <code>println</code> methods in class <code>PrintStream</code>.
 *
 * @see     java.io.PrintStream#println()
 * @see     java.io.PrintStream#println(boolean)
 * @see     java.io.PrintStream#println(char)
 * @see     java.io.PrintStream#println(char[])
 * @see     java.io.PrintStream#println(double)
 * @see     java.io.PrintStream#println(float)
 * @see     java.io.PrintStream#println(int)
 * @see     java.io.PrintStream#println(long)
 * @see     java.io.PrintStream#println(java.lang.Object)
 * @see     java.io.PrintStream#println(java.lang.String)
*/
public final static PrintStream out = null;

println(xxx)PrintStream 类里一个普通方法。println(xxx) 方法有多个重载,不同点在入参的类型,可以使 int、float、double、char、char[]、boolean、long 等。

public void println(T x) {
    synchronized (this) {
        print(x);
        newLine();
    }
}

前面啰嗦那么多,重点来了!

println(xxx)  方法的特点是只有一个入参,没有出参。这个和 Consumer<T>  函数式接口的 accept(T t)  是不是很像?这也是方法引用的精髓:

只要一个已存在的方法,其入参类型、入参个数和函数式接口的抽象方法相同(不考虑两者的返回值),就可以使用该方法(如本例中的 println(xxx)),来指代函数式接口的抽象方法(如本例中的  accept(T t) 方法),等于是该抽象方法的一种实现,也不需要继承该函数式接口。

直接用已存的类名 + 两个冒号 + 方法名即可:类名::方法名。注意,这里的方法名是不带括号的。

这个比 Lambda 表达式还省事,Lambda 表达式是在不继承接口的基础上,直接用形如  () -> {} 的方式变相实现了抽象方法,方法引用是直接用已存的方法来指代该抽象方法!

总结一下,方法引用解决了什么问题?

它解决了代码功能复用的问题,使得表达式更为紧凑,可读性更强,借助已有方法来达到传统方式下需多行代码才能达到的目的。

二、方法引用的语法

方法引用的语法很简单。

使用一对冒号 :: 来完成,分为左右两个部分,左侧为类名或对象名,右侧为方法名或 new 关键字。有以下四种类型:

## 方法引用的几种类型:
1、构造器引用,形式为 类名::new
2、静态方法引用,形式为 类名::方法名
3、类特定对象的方法引用,形式为 类对象::方法名
4、类的任意对象引用,形式为 类名::方法名

看个非常简单的示例,对应了上面的四种引用类型。

public class Animal {
    private String name;

    public Animal() {
    }

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

    public String getName() {
        return name;
    }

    public static Animal getInstance(Supplier<Animal> supplier) {
        return supplier.get();
    }

    public void guard(Animal animal) {
        System.out.println(this.getName() + " guard " + animal.getName());
    }

    public void sleep() {
        System.out.println(this.getName() + " sleep.");
    }

    public static void bodyCheck(Animal animal) {
        System.out.println("body check " + animal.getName());
    }
}

定义了一个简单的 Animal 类,包含了静态方法、普通方法、有参构造函数等。

接下来,我们看下基于这个  Animal 类,四种方法引用类型的使用:

public static void main(String[] args) {
    List<Animal> animalList = new ArrayList<Animal>() {{
        add(new Animal("sheep"));
        add(new Animal("cow"));
    }};

    System.out.println("---- 构造器引用 ----");
    Animal pig = Animal.getInstance(Animal::new);
    pig.sleep();

    System.out.println("\n---- 静态对象的引用 ----");
    animalList.forEach(Animal::bodyCheck);

    System.out.println("\n---- 类特定对象的引用 ----");
    Animal dog = new Animal("dog");
    animalList.forEach(dog::guard);

    System.out.println("\n---- 类的任意对象的引用 ----");
    animalList.forEach(Animal::sleep);
}

如果上面的代码你都理解了,那方法引用你也已经基本掌握了。

下面,针对方法引用的这几种类型,各自再详细解释。

三、方法引用的几种类型

3.1 构造器引用

语法很简单:类名::方法名,使用方式如下:

// 示例 1
Supplier<List<Integer>> supplier1 = ArrayList::new;
List<Integer> list = supplier1.get();

// 示例 2
Supplier<Animal> supplier2 = Animal::new;
Animal animal = supplier2.get();

之所以能赋值给 Supplier 接口,是因为其抽象方法 get() 没有入参,与类的无参构造函数一致。

@FunctionalInterface
public interface Supplier<T> {

    /**
     * Gets a result.
     *
     * @return a result
     */
    T get();
}

这里还需要注意一点,自定义的类必须有“无参构造函数”,否则编译器会报错

我们都知道,当创建一个类后,如果不显式声明构造函数,编译器会默认加一个无参构造函数。但如果有显式声明一个或多个有参构造函数,则编译器不再默认追加无参构造函数。如下:

public class Animal {
    private String name;

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

上面代码中的 Animal 类只有一个构造函数  Animal(String name),不再有无参构造函数。这种方式下使用构造器引用就会报错:

Supplier<Animal> supplier = Animal::new; // 编译报错:Cannot resolve constructor 'Animal'

3.2 静态方法引用

语法为 类名::静态方法名

还是以上面的 Animal 类为例,为了更好展示静态方法引用,相比上面的示例,我们适当做一下调整:

public class Animal {
    private String name;
    private Integer weight;

    public Animal() {
    }

    public Animal(String name, Integer weight) {
        this.name = name;
        this.weight = weight;
    }

    public String getName() {
        return name;
    }

    public Integer getWeight() {
        return weight;
    }

    ...

    public static void bodyCheck(Animal animal) {
        System.out.println("body check " + animal.getName());
    }

    public static Integer compareByName(Animal one, Animal another) {
        return one.getName().compareTo(another.getName());
    }

    public Integer compareByWeight(Animal one, Animal another) {
        return one.getWeight() - another.getWeight();
    }
}

Animal 类有两个成员变量 nameweight,它有多个方法,其中包括两个静态方法  compareByName() 和  compareByWeight()

给定一个 Animal  对象列表,如果我们想根据名称排序,可以怎么做?你想到了几种方式?

  • 第一种:利用 Collections.sort(List<T> list) 方法

这种方式,需要 Animal 类实现 Coparable<T> 接口,给出  compareTo(T t)  抽象方法的具体实现,如下所示:

public class Animal implements Comparable {

    ...

    @Override
    public int compareTo(Object o) {
        Animal another = (Animal)o;
        return this.getName().compareTo(another.toString());
    }
}

// 调用
Collections.sort(animalList);

这种方式在 JDK7 版本及以前使用的比较多。

  • 第二种:利用  Collections.sort(List<T> list, Comparator<? super T> c) 方法

在集合类  Collections<T>  中,还有一个 sort(List<T> list) 的重载方法  sort(List list, Comparator<? super T> c)

使用该方法,Animal 类就无需再实现 Comparable<T> 接口,在 JDK7 版本及以前,使用匿名内部类来调用此方法即可。

相比第一种方式,结构上轻便了很多,代码实现如下:

Collections.sort(animalList, new Comparator<Animal>() {
    @Override
    public int compare(Animal o1, Animal o2) {
        return o1.getName().compareTo(o2.getName());
    }
});
  • 第三种:利用 Lambda 表达式

和第二种类似,只不过随着  JDK8 版本中 Lambda 表达式的出现,可替换以往的匿名内部类,代码实现上做到更简洁:

// Lambda 表达式的实现
Collections.sort(animalList, (a, b) -> a.getName().compareTo(b.getName()));
  • 第四种:借助方法引用

在第一种方式中,Animal 类还要实现 Comparable<T> 接口,然后做  compare()  抽象方法的具体实现。

整个实现上是过于笨重的,太形式化。

有了方法引用,就可以大大减轻这种不必要的形式化。因为 Animal 类中已经有了类似的比较方法,即静态方法  compareByName()

直接用这个方法代替 compare() 方法不就行啦,如下:

Collections.sort(animalList, Animal::compareByName);

是不是很简单!没有接口实现,也没有匿名内部类,以一种优雅的方式达到了相同的目的,这也是方法引用的魅力之处。

我个人理解,方法引用的出现,就是为了去优化冗余且过于形式化的代码,直接用短平快的方式解决。

  • 第五种:利用 List 接口的 sort() 默认方法

除了 Collections 集合类,List 接口中,也提供了列表的排序方法。

// 匿名内部类实现
animalList.sort(new Comparator<Animal>() {
    @Override
    public int compare(Animal o1, Animal o2) {
        return o1.getName().compareTo(o2.getName());
    }
});

// Lambda 表达式实现
animalList.sort((a, b) -> a.getName().compareTo(b.getName()));

// 静态方法引用的实现
animalList.sort(Animal::compareByName);
  • 第六种:Stream() 流排序

Stream() 流是 JDK8 中新引入的功能,排序代码如下:

// 方式 1:Lambda 表达式实现
animalList = animalList
    .stream()
    .sorted((a, b) -> a.getName().compareTo(b.getName()))
    .collect(Collectors.toList());

// 方式 2:静态方法引用
animalList = animalList
    .stream()
    .sorted(Animal::compareByName)
    .collect(Collectors.toList());

3.3 类特定对象的引用

在前一章节的第五种方式中,我们可以替换为类特定对象的引用。

语法:类对象::普通方法名

在上面的 Animal 类中,有一个普通方法:

public Integer compareByWeight(Animal one, Animal another) {
    return one.getWeight() - another.getWeight();
}

compareByWeight() 就是一个普通的实例方法,但它的定义依然与 Comparable 接口的 compare()  抽象方法定义是一致的。所以也可以使用在方法引用中。

怎么使用呢?方式如下:

Animal dog = new Animal("dog", 40);
animalList.sort(dog::compareByWeight);

类特定对象的引用、静态方法引用,两者在使用上没有区别,都达到一样的目的,只是方式不同,一个是类 + 静态方法名,一个是类对象 + 普通方法名。

3.4 类的任意对象的引用

语法:类名::普通方法名

从语法上看,与前面  2.3.2 小节的静态方法引用类似,都是类名 + 方法名的方式,只不过一个是普通方法,一个是静态方法,但这是不是意味着两者在含义上也是类似的呢?

答案是否定的。

对于  2.3.2 章节的静态方法引用,以及 2.3.3 章节的类特定对象的引用,它们的重点都是在引出方法,只不过引出的方式不同。

public class Animal {
    private String name;
    private Integer weight;

    public Animal(String name, Integer weight) {
        this.name = name;
        this.weight = weight;
    }

    ...

    public static Integer compareByName(Animal one, Animal another) {
        return one.getName().compareTo(another.getName());
    }

    public Integer compareByWeight(Animal one, Animal another) {
        return one.getWeight() - another.getWeight();
    }
}


// 静态方法引用
animalList.sort(Animal::compareByName);

// 类的特定对象的引用
Animal dog = new Animal("dog", 40);
animalList.sort(dog::compareByWeight);

就像上面的代码中,“类的特定对象的引用”示例中,换个 Animal 对象,依然能达到同样的效果:

Animal cat = new Animal("cat", 15);
animalList.sort(cat::compareByWeight);

好了,现在回到本小节的主题:类的任意对象的引用

我们可以怎么用呢?

在继续讲之前,我们先回头再观察下前面面代码中的 compareByWeight(xx, xxx) 方法。有没有发现它的两个参数有点儿冗余?另外,如果是两个参数,这个方法放在任何一个类中都可以使用,完全可以把它抽到一个工具类中使用,没必要放在这个类中。如果要放在该类中,可以换一种方式,传递一个参数即可:

public Integer compareByWeight(Animal another) {
    return this.getWeight() - another.getWeight();
}

调用代码如下:

animalList.sort(Animal::compareByWeight);

这里很多人都会疑惑,方法引用的前提,不都是入参个数都要一样吗?但 compareByWeight(Animal another) 方法只有一个参数,而 sort()  方法的形参  Comparator<T> 对应的抽象方法  compare(T o1, T o2) 是两个参数:

default void sort(Comparator<? super E> c) {
    Object[] a = this.toArray();
    Arrays.sort(a, (Comparator) c);
    ListIterator<E> i = this.listIterator();
    for (Object e : a) {
        i.next();
        i.set((E) e);
    }
}

@FunctionalInterface
public interface Comparator<T> {
    int compare(T o1, T o2);
}

这就是“类的任意对象的引用”这种类型的特殊之处。

方法引用会默认将第一个入参作为当前类的一个调用对象,其余参数继续作为方法的入参。

在本例中,compare(T o1, T o2) 方法是需要接入两个 Animal 对象的,但第一个对象 o1 可以作为当前 Animal 类的一个对象,剩下的 o2 继续作为引用方法  compareByWeight()  的参数,即:

o1.compareByWeight(o2)

这也是为何称为“类的任意对象的引用”。

为加深理解,我们再举一个例子。

前面的 Animal 类中,有一个 sleep()  普通方法和 bodyCheck(xx)  静态方法:

public class Animal {
    private String name;
    ...
    public void sleep() {
        System.out.println(this.getName() + " sleep.");
    }
    public static void bodyCheck(Animal animal) {
        System.out.println("body check " + animal.getName());
    }
}

Animal::sleep  构成了“类的任意对象的引用”,Animal::bodyCheck  构成了“静态方法引用”,它们都可以用在如下表达式中:

animalList.forEach(Animal::sleep);
animalList.forEach(Animal::bodyCheck);

sleep() 方法虽然没有入参,但依然可以用在 forEach() 方法中,因为  Consumer<T>  接口的 accept(T t) 抽象方法有一个入参,而该入参就可以作为 Animal 类的一个对象,来调用 sleep() 方法。

四、总结

如上所述,方法引用有多种类型,在实际使用过程中,可灵活运用。

说到底,跟 Lambda 表达式一样,它还是一种语法糖,为我们的开发工作提效。为达到同样的目标,相比传统实现方式,这种语法糖减轻了代码量,使用更轻便,不再拘泥于特定场景下囿于面向对象语言规则而产生的笨重表达,是对它们的一种轻量级替代。

上一篇:axios----instance


下一篇:深入探讨 Lambda 表达式(上)