CONTENTS
1. 新方式与旧方式的对比
通常情况下,方法会根据所传递的数据产生不同的结果。如果想让一个方法在每次调用时都有不同的表现呢?如果将代码传递给方法,就可以控制其行为。
以前的做法是,创建一个对象,让它的一个方法包含所需行为,然后将这个对象传递给我们想控制的方法。下面的示例演示了这一点,然后增加了 Java 8 的实现方式:方法引用和 Lambda 表达式:
package funcprog;
interface Strategy {
String approach(String msg);
}
class DefaultStrategy implements Strategy {
@Override
public String approach(String msg) {
return msg.toLowerCase() + "?";
}
}
class Unrelated {
static String twice(String msg) {
return msg + " " + msg;
}
}
public class Strategize {
Strategy strategy;
String msg;
Strategize(String msg) {
strategy = new DefaultStrategy();
this.msg = msg;
}
void f() {
System.out.println(strategy.approach(msg));
}
void changeStrategy(Strategy strategy) {
this.strategy = strategy;
}
public static void main(String[] args) {
Strategize s = new Strategize("Hello world");
s.f(); // hello world?
Strategy[] strategies = {
new Strategy() {
@Override
public String approach(String msg) {
return msg.toUpperCase() + "!";
}
}, // 匿名内部类
msg -> msg.substring(0, 5), // Lambda表达式
Unrelated::twice // 方法引用
};
for (Strategy newStrategy: strategies) {
s.changeStrategy(newStrategy);
s.f();
}
/*
* HELLO WORLD!
* Hello
* Hello world Hello world
*/
}
}
Strategy
提供了接口,功能是通过其中唯一的 approach()
方法来承载的,通过创建不同的 Strategy
对象,我们可以创建不同的行为。
传统上,我们通过定义一个实现了 Strategy
接口的类来完成这种行为,比如 DefaultStrategy
。更简洁、自然的方式是创建一个匿名内部类,不过这样仍然会存在一定数量的重复代码,而且我们总是要花点功夫才能明白这里是在使用匿名内部类。
Java 8 的 Lambda 表达式突出的特点是用箭头 ->
将参数和函数体分隔开来,箭头右边是从 Lambda 返回的表达式,这和类定义以及匿名内部类实现了同样的效果,但是代码要少得多。
Java 8 的方法引用是用 ::
,左边是类名或对象名,右边是方法名,但是没有参数列表。
2. Lambda表达式
Lambda 表达式是使用尽可能少的语法编写的函数定义。Lambda 表达式产生的是函数,而不是类,在 Java 虚拟机(JVM)上,一切都是类,所以幕后会有各种各样的操作让 Lambda 看起来像函数。
任何 Lambda 表达式的基本语法如下:
- 参数;
- 之后跟一个
->
,可以读作“产生”; ->
后面跟一个方法体。
需要注意以下几个方面:
- 如果只有一个参数,可以只写这个参数,不写括号,也可以使用括号,尽管这种方式更不常见。
- 如果有多个参数,将它们放在使用括号包裹起来的参数列表内。
- 如果没有参数,必须使用括号来指示空的参数列表。
- 如果方法体只有一行,那么方法体中表达式的结果会自动成为 Lambda 表达式的返回值,不能使用
return
。 - 如果 Lambda 表达式需要多行代码,则必须将这些代玛行放到
{}
中,这种情况下需要使用return
从 Lambda 表达式生成一个值。
package funcprog;
interface Description {
String f();
}
interface Body {
String f(String str);
}
interface Multi {
String f(String str, int x);
}
public class LambdaExpressions {
static Description d = () -> "Hello World!";
static Body b1 = s -> "Hello " + s;
static Body b2 = (s) -> "Hello " + s;
static Multi m = (s, x) -> {
System.out.println("Multi");
return "Hello " + s + " " + x;
};
public static void main(String[] args) {
System.out.println(d.f());
System.out.println(b1.f("AsanoSaki"));
System.out.println(b2.f("AsanoSaki"));
System.out.println(m.f("AsanoSaki", 666));
/*
* Hello World!
* Hello AsanoSaki
* Hello AsanoSaki
* Multi
* Hello AsanoSaki 666
*/
}
}
递归意味着一个函数调用了自身。在 Java 中也可以编写递归的 Lambda 表达式,但是要注意这个 Lambda 表达式必须被赋值给一个静态变量或一个实例变星,否则会出现编译错误:
package funcprog;
interface Factorial {
int f(int n);
}
public class RecursiveFactorial {
static Factorial fact; // 需要将Lambda表达式赋值给一个静态变量
public static void main(String[] args) {
fact = n -> n == 0 ? 1 : n * fact.f(n - 1);
for (int i = 0; i < 5; i++)
System.out.print(fact.f(i) + " "); // 1 1 2 6 24
}
}
请注意,不能在定义的时候像这样来初始化 fact
:
static Factorial fact = n -> n == 0 ? 1 : n * fact.f(n - 1);
尽管这样的期望非常合理,但是对于 Java 编译器而言处理起来太复杂了,所以会产生编译错误。
现在我们再用递归的 Lambda 表达式实现斐波那契数列,这次使用实例变量,用构造器来初始化:
package funcprog;
interface Fibonacci {
int f(int n);
}
public class RecursiveFibonacci {
Fibonacci fib;
RecursiveFibonacci() {
// 构造器内初始化Fibonacci
fib = n -> n == 0 ? 0 : n == 1 ? 1 : fib.f(n - 2) + fib.f(n - 1);
}
public static void main(String[] args) {
RecursiveFibonacci rf = new RecursiveFibonacci();
for (int i = 0; i < 10; i++)
System.out.print(rf.fib.f(i) + " "); // 0 1 1 2 3 5 8 13 21 34
}
}
3. 方法引用
Java 8 方法引用指向的是方法,没有之前 Java 版本的历史包袱,方法引用是用类名或对象名,后面跟 ::
,然后跟方法名:
package funcprog;
interface Callable {
void call(String s);
}
class Print {
// 非静态类
void print(String s) {
// 签名和call()一致
System.out.println(s);
}
}
public class MethodReferences {
static void hello(String s) {
// 静态方法引用
System.out.println("Hello " + s);
}
static class GoodMorning {
void goodMorning(String s) {
// 静态内部类的非静态方法
System.out.println("Good morning " + s);
}
}
static class GoodEvening {
static void goodEvening(String s) {
// 静态内部类的静态方法
System.out.println("Good evening " + s);
}
}
public static void main(String[] args) {
Print p = new Print();
Callable c = p::print;
c.call("AsanoSaki");
c = MethodReferences::hello;
c.call("AsanoSaki");
c = new GoodMorning()::goodMorning;
c.call("AsanoSaki");
c = GoodEvening::goodEvening;
c.call("AsanoSaki");
/*
* AsanoSaki
* Hello AsanoSaki
* Good morning AsanoSaki
* Good evening AsanoSaki
*/
}
}
3.1 Runnable
java.lang
包中的 Runnable
接口也遵从特殊的单方法接口格式,其 run()
方法没有参数,也没有返回值,所以我们可以将 Lambda 表达式或方法引用用作 Runnable
:
package funcprog;
class RunnableReference {
static void f() {
System.out.println("RunnableReference::f()");
}
}
public class RunnableMethodReference {
public static void main(String[] args) {
// Thread对象接受一个Runnable作为其构造器参数
new Thread(new Runnable() {
// 匿名内部类
@Override
public void run() {
System.out.println("Anonymous");
}
}).start(); // start()方法会调用run()
new Thread( // Lambda表达式
() -> System.out.println("Lambda")
).start();
new Thread(RunnableReference::f).start(); // 方法引用
/*
* Anonymous
* Lambda
* RunnableReference::f()
*/
}
}
3.2 未绑定方法引用
未绑定方法引用(unbound method reference)指的是尚未关联到某个对象的普通(非静态)方法,对于未绑定引用,必须先提供对象,然后才能使用:
package funcprog;
class A {
String f() {
return "A::f()"; }
}
interface GetStringUnbound {
String get();
}
interface GetStringBoundA {
String get(A a);
}
public class UnboundMethodReference {
public static void main(String[] args) {
GetStringBoundA g = A::f;
A a = new A();
System.out.println(a.f()); // A::f()
System.out.println(g.get(a)); // A::f()
}
}
如果我们按以下方式将 A::f
赋值给 GetStringUnbound
编译器会报错,即使 get()
的签名和 f()
相同。问题在于,这里事实上还涉及另一个(隐藏的)参数:我们的老朋友 this
。如果没有一个可供附着的 A
对象,就无法调用 f()
。因此,A::f
代表的是一个未绑定方法引用,因为它没有绑定到某个对象。
为解决这个冋题,我们需要一个 A
对象,所以我们的接口事实上还需要一个额外的参数,如 GetStringBoundA
中所示,如果将 A::f
赋值给一个 GetStringBoundA
,Java 则会幵心地接受。在未绑定引用的情况下,函数式方法(接口中的单一方法)的签名与方法引用的签名不再完全匹配,这样做有一个很好的理由,那就是我们需要一个对象,让方法在其上调用。
在 g.get(a)
中我们接受了未绑定引用,然后以 A
为参数在其上调用了 get()
,最终以某种方式调用了 a.f()
。Java 知道它必须接受第一个参数,事实上就是 this
,并在它的上面调用该方法。
如果方法有更多参数,只要遵循第一个参数取的是 this
这种模式即可,即对于本例来说第一个参数为 A
即可。
3.3 构造器方法引用
我们也可以捕获对某个构造器的引用,之后通过该引用来调用那个构造器:
package funcprog;
class Cat {
String name;
Cat() {
name = "Kitty"; } // 无参构造器
Cat(String name) {
this.name = name; } // 有参构造器
}
interface MakeCatNoArgs {
Cat makeCat();
}
interface MakeCatWithArgs {
Cat makeCat(String name);
}
public class ConstructorReference {
public static void main(String[] args) {
MakeCatNoArgs m1 = Cat::new; // 所有构造器名字都是new,编译器可以从接口来推断使用哪个构造器
MakeCatWithArgs m2 = Cat::new;
Cat c1 = m1.makeCat(); // 调用此处的函数式接口方法makeCat()意味着调用构造器Cat::new
Cat c2 = m2.makeCat("Lucy");
}
}
4. 函数式接口
方法引用和 Lambda 表达式都必须赋值,而这些赋值都需要类型信息,让编译器确保类型的正确性,尤其是 Lambda 表达式,又引入了新的要求,考虑如下代码:
x -> x.toString()
我们看到返回类型必须是 String
,但是 x
是什么类型呢?因为 Lambda 表达式包含了某种形式的类型推断(编译器推断出类型的某些信息,而不需要程序员显式指定),所以编译器必须能够以某种方式推断出 x
的类型。
下面是第二个示例:
(x, y) -> x + y
现在 x
和 y
可以是支持 +
操作符的任何类型,包括两种不同的数值类型,或者是一个 String
和某个能够自动转换为 String
的其他类型。
Java 8 引入了包含一组接口的 java.util.function
,这些接口是 Lambda 表达式和方法引用的目标类型,每个接口都只包含一个抽象方法,叫作函数式方法。当编写接口时,这种函数式方法模式可以使用 @FunctionalInterface
注解来强制实施:
package funcprog;
@FunctionalInterface
interface FunctionalPrint {
void print(String s);
}
@FunctionalInterface
interface NotFunctional {
void f1();
void f2();
} // 编译器会报错
public class FunctionalAnnotation {
public void hello(String s) {
System.out.println("Hello " + s);
}
public static void main(String[] args) {
FunctionalAnnotation f = new FunctionalAnnotation();
FunctionalPrint fp = f::hello;
fp.print("AsanoSaki"); // Hello AsanoSaki
fp = s -> System.out.println("Hi " + s);
fp.print("AsanoSaki"); // Hi AsanoSaki
}
}
@FunctionalInterface
注解是可选的,Java 会将 main()
中的 FunctionalPrint
看作函数式接口。在 NotFunctional
接口的定义中我们可以看到 @FunctionalInterface
的作用:如果接口中的方法多于一个,则会产生一条编译错误信息。
现在我们仔细看一下 fp
的定义中发生了什么,FunctionalPrint
定义了接口,然而被赋值给它们的只是方法 hello()
,而不是类,它甚至不是实现了这里定义的某个接口的类中的方法。这是 Java 8 增加的一个小魔法:如果我们将一个方法引用或 Lambda 表达式赋值给某个函数式接口(而且类型可以匹配),那么 Java 会调整这个赋值,使其匹配目标接口。而在底层,Java 编译器会创建一个实现了目标接口的类的实例,并将我们的方法引用或 Lambda 表达式包裹在其中。
使用了 @FunctionalInterface
注解的接口也叫作单一抽象方法(Single Abstract Method,SAM)类型。
4.1 默认目标接口
java.util.function
旨在创建一套足够完备的目标接口,这样一般情况下我们就不需要定义自己的接口了。这一套接口的命名遵循一定的规律,一般来说通过名字就可以了解特定的接口是做什么的,部分接口示例如下:
package funcprog;
import java.util.function.BiConsumer;
import java.util.function.DoubleFunction;
import java.util.function.Function;
import java.util.function.IntToDoubleFunction;
public class FunctionVariants {
static Function<Integer, String> f = x -> "Function " + x;
static DoubleFunction<String> df = x -> "DoubleFunction " + x;
static IntToDoubleFunction itdf = x -> x / 2.0;
static BiConsumer<Integer, String> bc = (x, s) -> System.out.println("BiConsumer " + x + " " + s);
public static void main(String[] args) {
System.out.println(f.apply(6)); // Function 6
System.out.println(df.apply(6)); // DoubleFunction 6.0
System.out.println(itdf.applyAsDouble(6)); // 3.0
bc.accept(6, "AsanoSaki"); // BiConsumer 6 AsanoSaki
}
}
4.2 带有更多参数的函数式接口
java.util.function
中的接口毕竟是有限的,如果我们需要有3个参数的函数接口呢?因为那些接口相当直观,所以看一下 Java 库的源代码,然后编写我们自己的接口也很容易:
package funcprog;
@FunctionalInterface
interface TriFunction<T, U, V, R> {
// R为泛型返回类型
R apply(T t, U u, V v);
}
public class TriFunctionTest {
public static void main(String[] args) {
TriFunction<Integer, Long, Double, Double> tf = (i, j, k) -> i + j + k;
System.out.println(tf.apply(1, 2L, 3D)); // 6.0
}
}
5. 高阶函数
高阶函数是一个能接受函数作为参数或能把函数当返回值的函数,有了 Lambda 表达式,在方法中创建并返回一个函数简直不费吹灰之力,要接受并使用函数,方法必须在其参数列表中正确地描述函数类型:
package funcprog;
import java.util.function.Function;
public class ProduceFunction {
static Function<String, String> produce() {
// 函数作为返回值
return String::toLowerCase; // 或者用Lambda表达式s -> s.toLowerCase()
}
static String consume(Function<Integer, String> toStr, int x) {
// 函数作为参数
return toStr.apply(x);
}
public static void main(String[] args) {
Function<String, String> toLower = produce();
System.out.println(toLower.apply("Hello World")); // hello world
System.out.println(consume(x -> "To String " + x, 6)); // To String 6
}
}
6. 函数组合
函数组合是指将多个函数结合使用,以创建新的函数,这通常被认为是函数式编程的一部分。java.util.function
中的一些接口也包含了支持函数组合的方法,我们以 andThen()
和 compose()
方法为例:
andThen()
:先执行原始操作,再执行方法参数中的操作。compose()
:先执行方法参数中的操作,再执行原始操作。
package funcprog;
import java.util.function.Function;
public class FunctionComposition {
// hello world
static Function<String, String> f1 = s -> s.substring(0, 5),
f2 = s -> new StringBuilder(s).reverse().toString();
public static void main(String[] args) {
System.out.println(f1.andThen(f2).apply("Hello World")); // olleH
System.out.println(f1.compose(f2).apply("Hello World")); // dlroW
}
}