Java 8 方法引用没有历史包袱。方法引用组成:类名或对象名,后面跟 ::
,然后跟方法名称。
interface Callable { // [1]
void call(String s);
}
class Describe {
void show(String msg) { // [2]
System.out.println(msg);
}
}
public class MethodReferences {
static void hello(String name) { // [3]
System.out.println("Hello, " + name);
}
static class Description {
String about;
Description(String desc) {
about = desc;
}
void help(String msg) { // [4]
System.out.println(about + " " + msg);
}
}
static class Helper {
static void assist(String msg) { // [5]
System.out.println(msg);
}
}
public static void main(String[] args) {
Describe d = new Describe();
Callable c = d::show; // [6]
c.call("call()"); // [7]
c = MethodReferences::hello; // [8]
c.call("Bob");
c = new Description("valuable")::help; // [9]
c.call("information");
c = Helper::assist; // [10]
c.call("Help!");
}
}
输出结果:
[1] 我们从单一方法接口开始(同样,你很快就会了解到这一点的重要性)。
[2] show()
的签名(参数类型和返回类型)符合 Callable 的 call()
的签名。
[3] hello()
也符合 call()
的签名。
[4] help()
也符合,它是静态内部类中的非静态方法。
[5] assist()
是静态内部类中的静态方法。
[6] 我们将 Describe 对象的方法引用赋值给 Callable ,它没有 show()
方法,而是 call()
方法。 但是,Java 似乎接受用这个看似奇怪的赋值,因为方法引用符合 Callable 的 call()
方法的签名。
[7] 我们现在可以通过调用 call()
来调用 show()
,因为 Java 将 call()
映射到 show()
。
[8] 这是一个静态方法引用。
[9] 这是 [6] 的另一个版本:对已实例化对象的方法的引用,有时称为_绑定方法引用_。
[10] 最后,获取静态内部类中静态方法的引用与 [8] 中通过外部类引用相似。
上例只是简短的介绍,我们很快就能看到方法引用的所有不同形式。
Runnable 接口自 1.0 版以来一直在 Java 中,因此不需要导入。它也符合特殊的单方法接口格式:它的方法 run()
不带参数,也没有返回值。因此,我们可以使用 Lambda 表达式和方法引用作为 Runnable:
// 方法引用与 Runnable 接口的结合使用
class Go {
static void go() {
System.out.println("Go::go()");
}
}
public class RunnableMethodReference {
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("Anonymous");
}
}).start();
new Thread(
() -> System.out.println("lambda")
).start();
new Thread(Go::go).start();
}
}
输出结果:
Thread 对象将 Runnable 作为其构造函数参数,并具有会调用 run()
的方法 start()
。 注意这里只有匿名内部类才要求显式声明 run()
方法。
未绑定的方法引用是指没有关联对象的普通(非静态)方法。 使用未绑定的引用时,我们必须先提供对象:
// 没有方法引用的对象
class X {
String f() {
return "X::f()";
}
}
interface MakeString {
String make();
}
interface TransformX {
String transform(X x);
}
public class UnboundMethodReference {
public static void main(String[] args) {
// MakeString ms = X::f; // [1]
TransformX sp = X::f;
X x = new X();
System.out.println(sp.transform(x)); // [2]
System.out.println(x.f()); // 同等效果
}
}
输出结果:
到目前为止,我们已经见过了方法引用和对应接口的签名(参数类型和返回类型)一致的几个赋值例子。 在 [1] 中,我们尝试同样的做法,把 X
的 f()
方法引用赋值给 MakeString。结果即使 make()
与 f()
具有相同的签名,编译也会报“invalid method reference”(无效方法引用)错误。 问题在于,这里其实还需要另一个隐藏参数参与:我们的老朋友 this
。 你不能在没有 X
对象的前提下调用 f()
。 因此,X :: f
表示未绑定的方法引用,因为它尚未“绑定”到对象。
要解决这个问题,我们需要一个 X
对象,因此我们的接口实际上需要一个额外的参数,正如在 TransformX 中看到的那样。 如果将 X :: f
赋值给 TransformX,在 Java 中是允许的。我们必须做第二个心理调整——使用未绑定的引用时,函数式方法的签名(接口中的单个方法)不再与方法引用的签名完全匹配。 原因是:你需要一个对象来调用方法。
[2] 的结果有点像脑筋急转弯。我拿到未绑定的方法引用,并且调用它的transform()
方法,将一个X类的对象传递给它,最后使得 x.f()
以某种方式被调用。Java知道它必须拿第一个参数,该参数实际就是this
对象,然后对此调用方法。
如果你的方法有更多个参数,就以第一个参数接受this
的模式来处理。
// 未绑定的方法与多参数的结合运用
class This {
void two(int i, double d) {
}
void three(int i, double d, String s) {
}
void four(int i, double d, String s, char c) {
}
}
interface TwoArgs {
void call2(This athis, int i, double d);
}
interface ThreeArgs {
void call3(This athis, int i, double d, String s);
}
interface FourArgs {
void call4(
This athis, int i, double d, String s, char c);
}
public class MultiUnbound {
public static void main(String[] args) {
TwoArgs twoargs = This::two;
ThreeArgs threeargs = This::three;
FourArgs fourargs = This::four;
This athis = new This();
twoargs.call2(athis, 11, 3.14);
threeargs.call3(athis, 11, 3.14, "Three");
fourargs.call4(athis, 11, 3.14, "Four", 'Z');
}
}
需要指出的是,我将类命名为 This,并将函数式方法的第一个参数命名为 athis,但你在生产级代码中应该使用其他名字,以防止混淆。
你还可以捕获构造函数的引用,然后通过引用调用该构造函数。
class Dog {
String name;
int age = -1; // For "unknown"
Dog() {
name = "stray";
}
Dog(String nm) {
name = nm;
}
Dog(String nm, int yrs) {
name = nm;
age = yrs;
}
}
interface MakeNoArgs {
Dog make();
}
interface Make1Arg {
Dog make(String nm);
}
interface Make2Args {
Dog make(String nm, int age);
}
public class CtorReference {
public static void main(String[] args) {
MakeNoArgs mna = Dog::new; // [1]
Make1Arg m1a = Dog::new; // [2]
Make2Args m2a = Dog::new; // [3]
Dog dn = mna.make();
Dog d1 = m1a.make("Comet");
Dog d2 = m2a.make("Ralph", 4);
}
}
Dog 有三个构造函数,函数式接口内的 make()
方法反映了构造函数参数列表( make()
方法名称可以不同)。
注意我们如何对 [1],[2] 和 [3] 中的每一个使用 Dog :: new
。 这三个构造函数只有一个相同名称::: new
,但在每种情况下赋值给不同的接口,编译器可以从中知道具体使用哪个构造函数。
编译器知道调用函数式方法(本例中为 make()
)就相当于调用构造函数。
方法引用和 Lambda 表达式都必须被赋值,同时赋值需要类型信息才能使编译器保证类型的正确性。尤其是Lambda 表达式,它引入了新的要求。 代码示例:
x -> x.toString()
我们清楚这里返回类型必须是 String,但 x
是什么类型呢?
Lambda 表达式包含 类型推导 (编译器会自动推导出类型信息,避免了程序员显式地声明)。编译器必须能够以某种方式推导出 x
的类型。
下面是第二个代码示例:
(x, y) -> x + y
现在 x
和 y
可以是任何支持 +
运算符连接的数据类型,可以是两个不同的数值类型或者是 一个 String 加任意一种可自动转换为 String 的数据类型(这包括了大多数类型)。 但是,当 Lambda 表达式被赋值时,编译器必须确定 x
和 y
的确切类型以生成正确的代码。
该问题也适用于方法引用。 假设你要传递 System.out :: println
到你正在编写的方法 ,你怎么知道传递给方法的参数的类型?
为了解决这个问题,Java 8 引入了 java.util.function
包。它包含一组接口,这些接口是 Lambda 表达式和方法引用的目标类型。 每个接口只包含一个抽象方法,称为 函数式方法 。
在编写接口时,可以使用 @FunctionalInterface
注解强制执行此“函数式方法”模式:
@FunctionalInterface
interface Functional {
String goodbye(String arg);
}
interface FunctionalNoAnn {
String goodbye(String arg);
}
/*
@FunctionalInterface
interface NotFunctional {
String goodbye(String arg);
String hello(String arg);
}
产生错误信息:
NotFunctional is not a functional interface
multiple non-overriding abstract methods
found in interface NotFunctional
*/
public class FunctionalAnnotation {
public String goodbye(String arg) {
return "Goodbye, " + arg;
}
public static void main(String[] args) {
FunctionalAnnotation fa =
new FunctionalAnnotation();
Functional f = fa::goodbye;
FunctionalNoAnn fna = fa::goodbye;
// Functional fac = fa; // Incompatible
Functional fl = a -> "Goodbye, " + a;
FunctionalNoAnn fnal = a -> "Goodbye, " + a;
}
}
@FunctionalInterface
注解是可选的; Java 会在 main()
中把 Functional 和 FunctionalNoAnn 都当作函数式接口来看待。 在 NotFunctional
的定义中可看出@FunctionalInterface
的作用:当接口中抽象方法多于一个时产生编译期错误。
仔细观察在定义 f
和 fna
时发生了什么。 Functional
和 FunctionalNoAnn
声明了是接口,然而被赋值的只是方法 goodbye()
。首先,这只是一个方法而不是类;其次,它甚至都不是实现了该接口的类中的方法。这是添加到Java 8中的一点小魔法:如果将方法引用或 Lambda 表达式赋值给函数式接口(类型需要匹配),Java 会适配你的赋值到目标接口。 编译器会在后台把方法引用或 Lambda 表达式包装进实现目标接口的类的实例中。
虽然 FunctionalAnnotation
确实符合 Functional
模型,但是 Java不允许我们像fac
定义的那样,将 FunctionalAnnotation
直接赋值给 Functional
,因为 FunctionalAnnotation
并没有显式地去实现 Functional
接口。唯一的惊喜是,Java 8 允许我们将函数赋值给接口,这样的语法更加简单漂亮。
java.util.function
包旨在创建一组完整的目标接口,使得我们一般情况下不需再定义自己的接口。主要因为基本类型的存在,导致预定义的接口数量有少许增加。 如果你了解命名模式,顾名思义就能知道特定接口的作用。
以下是基本命名准则:
Function
,Consumer
,Predicate
等。参数类型通过泛型添加。LongConsumer
,DoubleFunction
,IntPredicate
等,但返回基本类型的 Supplier
接口例外。To
表示,如 ToLongFunction
和 IntToLongFunction
。Operator
:单个参数使用 UnaryOperator
,两个参数使用 BinaryOperator
。Predicate
)。Bi
。下表描述了 java.util.function
中的目标类型(包括例外情况):
特征 | 函数式方法名 | 示例 |
---|---|---|
无参数; | ||
无返回值 | Runnable (java.lang) | |
run() |
Runnable | |
无参数; | ||
返回类型任意 | Supplierget() |
|
getAs类型() |
Supplier** |
|
** | ||
BooleanSupplier | ||
IntSupplier | ||
LongSupplier | ||
DoubleSupplier** | ||
无参数; | ||
返回类型任意 | Callable (java.util.concurrent) | |
call() |
Callable** |
|
1 参数; | ||
无返回值 | Consumeraccept() |
**Consumer |
** | ||
IntConsumer | ||
LongConsumer | ||
DoubleConsumer** | ||
2 参数 Consumer | BiConsumeraccept() |
**BiConsumer |
2 参数 Consumer; | ||
第一个参数是 引用; | ||
第二个参数是 基本类型 | Obj类型Consumeraccept() |
**ObjIntConsumer |
** | ||
****ObjLongConsumer |
||
** | ||
****ObjDoubleConsumer |
||
1 参数; | ||
返回类型不同 | Functionapply() |
|
To类型 和 类型To类型applyAs类型() |
Function** |
|
** | ||
IntFunction**** |
||
** | ||
****LongFunction |
||
** | ||
DoubleFunction**** |
||
** | ||
ToIntFunction**** |
||
** | ||
****ToLongFunction |
||
** | ||
****ToDoubleFunction |
||
** | ||
IntToLongFunction | ||
IntToDoubleFunction | ||
LongToIntFunction | ||
LongToDoubleFunction | ||
DoubleToIntFunction | ||
DoubleToLongFunction** | ||
1 参数; | ||
返回类型相同 | UnaryOperatorapply() |
**UnaryOperator |
** | ||
IntUnaryOperator | ||
LongUnaryOperator | ||
DoubleUnaryOperator** | ||
2 参数,类型相同; | ||
返回类型相同 | BinaryOperatorapply() |
**BinaryOperator |
** | ||
IntBinaryOperator | ||
LongBinaryOperator | ||
DoubleBinaryOperator** | ||
2 参数,类型相同; | ||
返回整型 | Comparator | |
(java.util) | ||
compare() |
**Comparator |
|
2 参数; | ||
返回布尔型 | Predicatetest() |
**Predicate |
** | ||
****BiPredicate |
||
** | ||
IntPredicate | ||
LongPredicate | ||
DoublePredicate** | ||
参数基本类型; | ||
返回基本类型 | 类型To类型FunctionapplyAs类型() |
**IntToLongFunction |
IntToDoubleFunction | ||
LongToIntFunction | ||
LongToDoubleFunction | ||
DoubleToIntFunction | ||
DoubleToLongFunction** | ||
2 参数; | ||
类型不同 | Bi操作 (不同方法名) | **BiFunction |
** | ||
****BiConsumer |
||
** | ||
****BiPredicate |
||
** | ||
****ToIntBiFunction |
||
** | ||
****ToLongBiFunction |
||
** | ||
****ToDoubleBiFunction |
此表仅提供些常规方案。通过上表,你应该或多或少能自行推导出你所需要的函数式接口。
可以看出,在创建 java.util.function
时,设计者们做出了一些选择。
例如,为什么没有 IntComparator
,LongComparator
和 DoubleComparator
呢?有 BooleanSupplier
却没有其他表示 Boolean 的接口;有通用的 BiConsumer
却没有用于 int,long 和 double 的 BiConsumers
变体(我理解他们为什么放弃这些接口)。这到底是疏忽还是有人认为其他组合使用得很少呢(他们是如何得出这个结论的)?
你还可以看到基本类型给 Java 添加了多少复杂性。该语言的第一版中就包含了基本类型,原因是考虑效率问题(该问题很快就缓解了)。现在,在语言的生命周期里,我们一直忍受语言设计的糟糕选择所带来的影响。
下面枚举了基于 Lambda 表达式的所有不同 Function 变体的示例:
import java.util.function.*;
class Foo {
}
class Bar {
Foo f;
Bar(Foo f) {
this.f = f;
}
}
class IBaz {
int i;
IBaz(int i) {
this.i = i;
}
}
class LBaz {
long l;
LBaz(long l) {
this.l = l;
}
}
class DBaz {
double d;
DBaz(double d) {
this.d = d;
}
}
public class FunctionVariants {
static Function<Foo, Bar> f1 = f -> new Bar(f);
static IntFunction<IBaz> f2 = i -> new IBaz(i);
static LongFunction<LBaz> f3 = l -> new LBaz(l);
static DoubleFunction<DBaz> f4 = d -> new DBaz(d);
static ToIntFunction<IBaz> f5 = ib -> ib.i;
static ToLongFunction<LBaz> f6 = lb -> lb.l;
static ToDoubleFunction<DBaz> f7 = db -> db.d;
static IntToLongFunction f8 = i -> i;
static IntToDoubleFunction f9 = i -> i;
static LongToIntFunction f10 = l -> (int) l;
static LongToDoubleFunction f11 = l -> l;
static DoubleToIntFunction f12 = d -> (int) d;
static DoubleToLongFunction f13 = d -> (long) d;
public static void main(String[] args) {
Bar b = f1.apply(new Foo());
IBaz ib = f2.apply(11);
LBaz lb = f3.apply(11);
DBaz db = f4.apply(11);
int i = f5.applyAsInt(ib);
long l = f6.applyAsLong(lb);
double d = f7.applyAsDouble(db);
l = f8.applyAsLong(12);
d = f9.applyAsDouble(12);
i = f10.applyAsInt(12);
d = f11.applyAsDouble(12);
i = f12.applyAsInt(13.0);
l = f13.applyAsLong(13.0);
}
}
这些 Lambda 表达式尝试生成适合函数签名的最简代码。 在某些情况下有必要进行强制类型转换,否则编译器会报截断错误。
main()
中的每个测试都显示了 Function
接口中不同类型的 apply()
方法。 每个都产生一个与其关联的 Lambda 表达式的调用。
方法引用有自己的小魔法:
import java.util.function.*;
class In1 {
}
class In2 {
}
public class MethodConversion {
static void accept(In1 i1, In2 i2) {
System.out.println("accept()");
}
static void someOtherName(In1 i1, In2 i2) {
System.out.println("someOtherName()");
}
public static void main(String[] args) {
BiConsumer<In1, In2> bic;
bic = MethodConversion::accept;
bic.accept(new In1(), new In2());
bic = MethodConversion::someOtherName;
// bic.someOtherName(new In1(), new In2()); // Nope
bic.accept(new In1(), new In2());
}
}
输出结果:
查看 BiConsumer
的文档,你会看到它的函数式方法为 accept()
。 的确,如果我们将方法命名为 accept()
,它就可以作为方法引用。 但是我们也可用不同的名称,比如 someOtherName()
。只要参数类型、返回类型与 BiConsumer
的 accept()
相同即可。
因此,在使用函数接口时,名称无关紧要——只要参数类型和返回类型相同。 Java 会将你的方法映射到接口方法。 要调用方法,可以调用接口的函数式方法名(在本例中为 accept()
),而不是你的方法名。
现在我们来看看,将方法引用应用于基于类的函数式接口(即那些不包含基本类型的函数式接口)。下面的例子中,我创建了适合函数式方法签名的最简单的方法:
import java.util.*;
import java.util.function.*;
class AA {
}
class BB {
}
class CC {
}
public class ClassFunctionals {
static AA f1() {
return new AA();
}
static int f2(AA aa1, AA aa2) {
return 1;
}
static void f3(AA aa) {
}
static void f4(AA aa, BB bb) {
}
static CC f5(AA aa) {
return new CC();
}
static CC f6(AA aa, BB bb) {
return new CC();
}
static boolean f7(AA aa) {
return true;
}
static boolean f8(AA aa, BB bb) {
return true;
}
static AA f9(AA aa) {
return new AA();
}
static AA f10(AA aa1, AA aa2) {
return new AA();
}
public static void main(String[] args) {
Supplier<AA> s = ClassFunctionals::f1;
s.get();
Comparator<AA> c = ClassFunctionals::f2;
c.compare(new AA(), new AA());
Consumer<AA> cons = ClassFunctionals::f3;
cons.accept(new AA());
BiConsumer<AA, BB> bicons = ClassFunctionals::f4;
bicons.accept(new AA(), new BB());
Function<AA, CC> f = ClassFunctionals::f5;
CC cc = f.apply(new AA());
BiFunction<AA, BB, CC> bif = ClassFunctionals::f6;
cc = bif.apply(new AA(), new BB());
Predicate<AA> p = ClassFunctionals::f7;
boolean result = p.test(new AA());
BiPredicate<AA, BB> bip = ClassFunctionals::f8;
result = bip.test(new AA(), new BB());
UnaryOperator<AA> uo = ClassFunctionals::f9;
AA aa = uo.apply(new AA());
BinaryOperator<AA> bo = ClassFunctionals::f10;
aa = bo.apply(new AA(), new AA());
}
}
请注意,每个方法名称都是随意的(如 f1()
,f2()
等)。正如你刚才看到的,一旦将方法引用赋值给函数接口,我们就可以调用与该接口关联的函数方法。 在此示例中为 get()
、compare()
、accept()
、apply()
和 test()
。
java.util.functional
中的接口是有限的。比如有 BiFunction
,但也仅此而已。 如果需要三参数函数的接口怎么办? 其实这些接口非常简单,很容易查看 Java 库源代码并自行创建。代码示例:
// functional/TriFunction.java
@FunctionalInterface
public interface TriFunction<T, U, V, R> {
R apply(T t, U u, V v);
}
简单测试,验证它是否有效:
public class TriFunctionTest {
static int f(int i, long l, double d) {
return 99;
}
public static void main(String[] args) {
TriFunction<Integer, Long, Double, Integer> tf =
TriFunctionTest::f;
tf = (i, l, d) -> 12;
}
}
这里我们同时测试了方法引用和 Lambda 表达式。
让我们重温一下 BiConsumer
,看看我们将如何创建各种缺失的预定义组合,涉及 int,long 和 double (基本类型):
import java.util.function.*;
public class BiConsumerPermutations {
static BiConsumer<Integer, Double> bicid = (i, d) ->
System.out.format("%d, %f%n", i, d);
static BiConsumer<Double, Integer> bicdi = (d, i) ->
System.out.format("%d, %f%n", i, d);
static BiConsumer<Integer, Long> bicil = (i, l) ->
System.out.format("%d, %d%n", i, l);
public static void main(String[] args) {
bicid.accept(47, 11.34);
bicdi.accept(22.45, 92);
bicil.accept(1, 11L);
}
}
输出结果:
这里使用 System.out.format()
来显示。它类似于 System.out.println()
但提供了更多的显示选项。 这里,%f
表示我将 n
作为浮点值给出,%d
表示 n
是一个整数值。 这其中可以包含空格,输入 %n
会换行 — 当然使用传统的 \n
也能换行,但 %n
是自动跨平台的,这是使用 format()
的另一个原因。
上例只是简单使用了合适的包装类型,而装箱和拆箱负责它与基本类型之间的来回转换。 又比如,我们可以将包装类型和Function
一起使用,而不去用各种针对基本类型的预定义接口。代码示例:
import java.util.function.*;
public class FunctionWithWrapped {
public static void main(String[] args) {
Function<Integer, Double> fid = i -> (double) i;
IntToDoubleFunction fid2 = i -> i;
}
}
如果没有强制转换,则会收到错误消息:“Integer cannot be converted to Double”(Integer 无法转换为 Double),而使用 IntToDoubleFunction 就没有此类问题。 IntToDoubleFunction 接口的源代码是这样的:
@FunctionalInterface
public interface IntToDoubleFunction {
double applyAsDouble(int value);
}
因为我们可以简单地写 Function
并产生正常的结果,所以用基本类型(IntToDoubleFunction
)的唯一理由是可以避免传递参数和返回结果过程中的自动拆装箱,进而提升性能。
似乎是考虑到使用频率,某些函数类型并没有预定义。
当然,如果因为缺少针对基本类型的函数式接口造成了性能问题,你可以轻松编写自己的接口( 参考 Java 源代码)——尽管这里出现性能瓶颈的可能性不大。