OO(object oriented,面向对象)是抽象数据,FP(functional programming,函数式编程)是抽象行为。
通常,传递给方法的数据不同,结果不同。如果我们希望方法在调用时行为不同,该怎么做呢?
结论是:只要能将代码传递给方法,我们就可以控制它的行为。此前,我们通过在方法中创建包含所需行为的对象,然后将该对象传递给我们想要控制的方法来完成此操作。
Java中,方法引用和 Lambda 表达式的出现让我们可以在需要时传递功能,而不是仅在必要时才这么做。
Lambda表达式
Lambda 表达式产生函数,而不是类。 虽然在 JVM(Java Virtual Machine,Java 虚拟机)上,一切都是类,但是幕后有各种操作执行让 Lambda 看起来像函数 ,作为程序员,你可以高兴地假装它们“就是函数”。《On Java8》
- 个人理解:
Java中通过Lambda表达式来简化实现函数式接口(可以看作是函数式接口的匿名内部类的简化写法)。
使用前提:lambda表达式用于函数式接口的实现。
How:
- 标准语法:(parameters)->{statements};
- 简化语法:
- 省略类型声明:不需要声明参数类型,编译器可以统一识别参数值。
- 省略参数圆括号:一个参数无需定义圆括号,但多个参数需要定义圆括号。
- 省略大括号:如果主体包含了一个语句,就不需要使用大括号。
- 省略return__关键字:如果主体只有一个表达式返回值则编译器会自动返回值,大括号需要指定明表达式返回了一个数值。
方法引用
如果说,Java中Lambda表达式是函数式接口匿名实现的简化形式,那么,方法引用就是Lambda表达式的简化形式。
- 使用前提:
- 若Lambda体中的内容有方法已经实现了,我们可以使用“方法引用”;
- Lambda体中调用方法的签名,必须要与函 数式接口中抽象方法的签名一致(注意:类::实例方法名 形式例外,需要在形参数列表的第一个参数传递方法调用对象)。
- 主要语法形式: | 对象::实例方法名 | | | —- | —- | | 类::静态方法名 | | | 类::实例方法名 | Lambda参数列表中的第一参数是实例方法的调用者,且第二个参数是实例方法的参数 | | 类::new | 构造器引用 | | 注意:这里的方法名可以只写方法名,不用写方法形参,因为在JVM可以通过Context推断出来 | |
- 示例一:类::静态方法名 ```java class Go { static void go() { System.out.println(“method_reference”); }
}
public class RunnableMethodReference { public static void main(String[] args) {
new Thread(new Runnable() {
public void run() {
System.out.println("Anonymous");
}
}).start(); //[1]
new Thread(
() -> System.out.println("lambda")
).start(); //[2]
new Thread(Go::go).start(); //[3]
} }
==========控制台============= Anonymous lambda method_reference
仔细观察[3]处,原本 Thread(Runnable target) 接受的是一个Runnable接口的实现类,而此处却可以使用方法go()。首先,这只是一个方法而不是类;其次,它甚至都不是实现了该接口的类中的方法。
- 这是添加到Java 8中的一点小魔法:如果将方法引用或 Lambda 表达式赋值给函数式接口(类型需要匹配),Java 会适配你的赋值到目标接口。 编译器会在后台把方法引用或 Lambda 表达式包装进实现目标接口的类的实例中。
- **示例二:类::实例方法名**
```java
class X {
String f() { return "方法引用!"; }
}
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] 中,即使 make() 与 f() 具有相同的签名,编译也会报“invalid method reference”(无效方法引用)错误。因为,这里其实还需要另一个隐藏参数参与:我们的老朋友 this。 你不能在没有 X 对象的前提下调用 f()。 因此,X :: f 表示未绑定的方法引用,因为它尚未“绑定”到对象。
要解决这个问题,我们需要一个 X 对象,因此我们的接口实际上需要一个额外的参数,正如在 TransformX 中看到的那样。 如果将 X :: f 赋值给 TransformX,在 Java 中是允许的。我们必须做第二个心理调整——使用未绑定的引用时,函数式方法的签名(接口中的单个方法)不再与方法引用的签名完全匹配。 原因是:你需要一个对象来调用方法。
[2] 的结果有点像脑筋急转弯。我拿到未绑定的方法引用,并且调用它的transform()方法,将一个X类的对象传递给它,最后使得 x.f() 以某种方式被调用。Java知道它必须拿第一个参数,该参数实际就是this 对象,然后对此调用方法。
如果你的方法有更多个参数,就以第一个参数接受this的模式来处理。
- 示例三:类::new ```java 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() 方法名称可以不同)。<br />注意:我们如何对 [1],[2] 和 [3] 中的每一个使用 Dog :: new。 这三个构造函数只有一个相同名称::: new,但在每种情况下赋值给不同的接口,编译器可以从中知道具体使用哪个构造函数。<br />编译器知道调用函数式方法(本例中为 make())就相当于调用构造函数。
<a name="1wXOS"></a>
# 函数式接口
只有一个抽象方法的接口,Java8允许我们将函数直接赋值给接口,这样的语法更加简单漂亮,不过本质上是编译器在后台把该函数包装进实现目标接口的类的实例中。同时可以用注解@FunctionalInterface修饰来保证该接口只能是函数式接口。
- **Java8内置的四大核心函数式接口**:
| **接口** | **类型** |
| --- | --- |
| Cosumer<T> {void accept(T t)}; | 消费型 |
| Supplier<T> {T get()}; | 供给型 |
| Function<T,R> {R apply(T t)}; | 函数型 |
| Predicate<T> {boolean test(T t)}; | 断言型 |
<a name="cgb1K"></a>
## 基本命名准则
java.util.function 包旨在创建一组完整的目标接口,使得我们一般情况下不需再定义自己的接口。主要因为基本类型的存在,导致预定义的接口数量有少许增加。 如果你了解命名模式,顾名思义就能知道特定接口的作用。
| **feature** | **rule** |
| --- | --- |
| 仅处理对象而非基本类型 | 名称为Function、Consumer、Predicate 等,参数类型通过泛型添加。 |
| 接收的参数是基本类型 | 则由名称的第一部分表示,如 LongConsumer、DoubleFunction、IntPredicate 等,但返回基本类型的 Supplier 接口例外。 |
| 返回值为基本类型 | 用 To 表示,如 ToLongFunction <T> 和 IntToLongFunction; |
| 返回值类型与参数类型相同 | 是一个 Operator :单个参数使用 UnaryOperator,两个参数使用 BinaryOperator; |
| 接收参数并返回一个布尔值 | 是一个 谓词 (Predicate); |
| 接收的两个参数类型不同 | 名称中有一个 Bi |
下表描述了 java.util.function 中的目标类型(包括例外情况):
| **特征** | | **描述** | **函数式接口** |
| --- | --- | --- | --- |
| **无参** | **无返** | | Runnable |
| | **有返** | | Callable<V>、Supplier<T>、BooleanSupplier、IntSupplier、LongSupplier、DoubleSupplier |
| **有参** | **无返** | 1参数 | Consumer<T>、IntConsumer、LongConsumer、DoubleConsumer |
| | | 2参数,参数类型不同 | BiConsumer<T,U> |
| | | 2参数,参数2为基本类型 | ObjIntConsumer<T>、ObjLongConsumer<T>、ObjDoubleConsumer<T> |
| | **有返** | 1参数 | Function<T,R>、IntFunction<R>、LongFunction<R>、DoubleFunction<R>、ToIntFunction<T>、ToLongFunction<T>、ToDoubleFunction<T>、IntToLongFunction、IntToDoubleFunction、LongToIntFunction、LongToDoubleFunction、DoubleToIntFunction、DoubleToLongFunction |
| | | 1参数,返回值类型相同 | UnaryOperator<T>、IntUnaryOperator、LongUnaryOperator、DoubleUnaryOperator |
| | | 2参数,类型相同,返回值类型相同 | BinaryOperator<T>、IntBinaryOperator、LongBinaryOperator、DoubleBinaryOperator |
| | | 2参数,类型相同,返回整型 | Comparator<T> |
| | | 2 参数,类型相同; 返回布尔 | Predicate<T>,BiPredicate<T,U>、IntPredicate、LongPredicate 、DoublePredicate |
| | | 参数基本类型,返回基本类型 | IntToLongFunction、IntToDoubleFunction、LongToIntFunction、LongToDoubleFunction 、DoubleToIntFunction 、DoubleToLongFunction |
| | | 2不同类型参数 | BiFunction<T,U,R>、BiPredicate<T,U>、ToIntBiFunction<T,U> <br />ToLongBiFunction<T,U>、ToDoubleBiFunction<T> |
<a name="c63466b3"></a>
## 函数组合
函数组合(Function Composition)意为“多个函数组合成新函数”。它通常是函数式编程的基本组成部分。
<a name="TtbI6"></a>
## 柯里化和部分求和
柯里化:将一个多参数的函数,转换为一系列单参数函数。
```java
import java.util.function.*;
public class CurryingAndPartials {
// 未柯里化:
static String uncurried(String a, String b) {
return a + b;
}
public static void main(String[] args) {
// 柯里化的函数:
Function<String, Function<String, String>> sum = a -> b -> a + b; // [1]
System.out.println(uncurried("Hi ", "Ho"));
Function<String, String> hi = sum.apply("Hi "); // [2]
System.out.println(hi.apply("Ho"));
// 部分应用:
Function<String, String> sumHi = sum.apply("Hup ");
System.out.println(sumHi.apply("Ho"));
System.out.println(sumHi.apply("Hey"));
}
}
======== console========
Hi Ho
Hi Ho
Hup Ho
Hup Hey
[2] 柯里化的目的是能够通过提供单个参数来创建一个新函数,所以现在有了一个“带参函数”和剩下的 “自由函数”(free argument) 。实际上,你从一个双参数函数开始,最后得到一个单参数函数。