一句话介绍:
方法引用(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
类有两个成员变量 name
和 weight
,它有多个方法,其中包括两个静态方法 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 表达式一样,它还是一种语法糖,为我们的开发工作提效。为达到同样的目标,相比传统实现方式,这种语法糖减轻了代码量,使用更轻便,不再拘泥于特定场景下囿于面向对象语言规则而产生的笨重表达,是对它们的一种轻量级替代。