💎一站式轻松地调用各大LLM模型接口,支持GPT4、智谱、星火、月之暗面及文生图 广告
## 函数式接口 方法引用和 Lambda 表达式都必须被赋值,同时赋值需要类型信息才能使编译器保证类型的正确性。尤其是Lambda 表达式,它引入了新的要求。 代码示例: ```java x -> x.toString() ``` 我们清楚这里返回类型必须是 **String**,但 `x` 是什么类型呢? Lambda 表达式包含类型推导(编译器会自动推导出类型信息,避免了程序员显式地声明)。编译器必须能够以某种方式推导出 `x` 的类型。 下面是第二个代码示例: ```java (x, y) -> x + y ``` 现在 `x` 和 `y` 可以是任何支持 `+` 运算符连接的数据类型,可以是两个不同的数值类型或者是 一个 **String** 加任意一种可自动转换为 **String** 的数据类型(这包括了大多数类型)。 但是,当 Lambda 表达式被赋值时,编译器必须确定 `x` 和 `y` 的确切类型以生成正确的代码。 该问题也适用于方法引用。 假设你要传递 `System.out :: println` 到你正在编写的方法 ,你怎么知道传递给方法的参数的类型? 为了解决这个问题,Java 8 引入了 `java.util.function` 包。它包含一组接口,这些接口是 Lambda 表达式和方法引用的目标类型。 每个接口只包含一个抽象方法,称为函数式方法。 在编写接口时,可以使用 `@FunctionalInterface` 注解强制执行此“函数式方法”模式: ```java // functional/FunctionalAnnotation.java @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` 包旨在创建一组完整的目标接口,使得我们一般情况下不需再定义自己的接口。主要因为基本类型的存在,导致预定义的接口数量有少许增加。 如果你了解命名模式,顾名思义就能知道特定接口的作用。 以下是基本命名准则: 1. 如果只处理对象而非基本类型,名称则为 `Function`,`Consumer`,`Predicate` 等。参数类型通过泛型添加。 2. 如果接收的参数是基本类型,则由名称的第一部分表示,如 `LongConsumer`,`DoubleFunction`,`IntPredicate` 等,但返回基本类型的 `Supplier` 接口例外。 3. 如果返回值为基本类型,则用 `To` 表示,如 `ToLongFunction <T>` 和 `IntToLongFunction`。 4. 如果返回值类型与参数类型一致,则是一个运算符:单个参数使用 `UnaryOperator`,两个参数使用 `BinaryOperator`。 5. 如果接收两个参数且返回值为布尔值,则是一个谓词(Predicate)。 6. 如果接收的两个参数类型不同,则名称中有一个 `Bi`。 下表描述了 `java.util.function` 中的目标类型(包括例外情况): | **特征** |**函数式方法名**|**示例**| | :---- | :----: | :----: | |无参数; <br> 无返回值|**Runnable** <br> (java.lang) <br> `run()`|**Runnable**| |无参数; <br> 返回类型任意|**Supplier** <br> `get()` <br> `getAs类型()`| **Supplier`<T>` <br> BooleanSupplier <br> IntSupplier <br> LongSupplier <br> DoubleSupplier**| |无参数; <br> 返回类型任意|**Callable** <br> (java.util.concurrent) <br> `call()`|**Callable`<V>`**| |1 参数; <br> 无返回值|**Consumer** <br> `accept()`|**`Consumer<T>` <br> IntConsumer <br> LongConsumer <br> DoubleConsumer**| |2 参数 **Consumer**|**BiConsumer** <br> `accept()`|**`BiConsumer<T,U>`**| |2 参数 **Consumer**; <br> 1 引用; <br> 1 基本类型|**Obj类型Consumer** <br> `accept()`|**`ObjIntConsumer<T>` <br> `ObjLongConsumer<T>` <br> `ObjDoubleConsumer<T>`**| |1 参数; <br> 返回类型不同|**Function** <br> `apply()` <br> **To类型** 和 **类型To类型** <br> `applyAs类型()`|**Function`<T,R>` <br> IntFunction`<R>` <br> `LongFunction<R>` <br> DoubleFunction`<R>` <br> ToIntFunction`<T>` <br> `ToLongFunction<T>` <br> `ToDoubleFunction<T>` <br> IntToLongFunction <br> IntToDoubleFunction <br> LongToIntFunction <br> LongToDoubleFunction <br> DoubleToIntFunction <br> DoubleToLongFunction**| |1 参数; <br> 返回类型相同|**UnaryOperator** <br> `apply()`|**`UnaryOperator<T>` <br> IntUnaryOperator <br> LongUnaryOperator <br> DoubleUnaryOperator**| |2 参数类型相同; <br> 返回类型相同|**BinaryOperator** <br> `apply()`|**`BinaryOperator<T>` <br> IntBinaryOperator <br> LongBinaryOperator <br> DoubleBinaryOperator**| |2 参数类型相同; <br> 返回整型|Comparator <br> (java.util) <br> `compare()`|**`Comparator<T>`**| |2 参数; <br> 返回布尔型|**Predicate** <br> `test()`|**`Predicate<T>` <br> `BiPredicate<T,U>` <br> IntPredicate <br> LongPredicate <br> DoublePredicate**| |参数基本类型; <br> 返回基本类型|**类型To类型Function** <br> `applyAs类型()`|**IntToLongFunction <br> IntToDoubleFunction <br> LongToIntFunction <br> LongToDoubleFunction <br> DoubleToIntFunction <br> DoubleToLongFunction**| |2 参数类型不同|**Bi操作** <br> (不同方法名)|**`BiFunction<T,U,R>` <br> `BiConsumer<T,U>` <br> `BiPredicate<T,U>` <br> `ToIntBiFunction<T,U>` <br> `ToLongBiFunction<T,U>` <br> `ToDoubleBiFunction<T>`**| 此表仅提供些常规方案。通过上表,你应该或多或少能自行推导出你所需要的函数式接口。 可以看出,在创建 `java.util.function` 时,设计者们做出了一些选择。 例如,为什么没有 `IntComparator`,`LongComparator` 和 `DoubleComparator` 呢?有 `BooleanSupplier` 却没有其他表示 **Boolean** 的接口;有通用的 `BiConsumer` 却没有用于 **int**,**long** 和 **double** 的 `BiConsumers` 变体(我理解他们为什么放弃这些接口)。这到底是疏忽还是有人认为其他组合使用得很少呢(他们是如何得出这个结论的)? 你还可以看到基本类型给 Java 添加了多少复杂性。基于效率方面的考虑(问题之后有所缓解),该语言的第一版中就包含了基本类型。现在,在语言的生命周期中,我们仍然会受到语言设计选择不佳的影响。 下面枚举了基于 Lambda 表达式的所有不同 **Function** 变体的示例: ```java // functional/FunctionVariants.java 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 表达式尝试生成适合函数签名的最简代码。 在某些情况下,有必要进行强制类型转换,否则编译器会报截断错误。 主方法中的每个测试都显示了 `Function` 接口中不同类型的 `apply()` 方法。 每个都产生一个与其关联的 Lambda 表达式的调用。 方法引用有自己的小魔法: ```java / functional/MethodConversion.java 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()); } } ``` 输出结果: ``` accept() someOtherName() ``` 查看 `BiConsumer` 的文档,你会看到 `accept()` 方法。 实际上,如果我们将方法命名为 `accept()`,它就可以作为方法引用。 但是我们也可用不同的名称,比如 `someOtherName()`。只要参数类型、返回类型与 `BiConsumer` 的 `accept()` 相同即可。 因此,在使用函数接口时,名称无关紧要——只要参数类型和返回类型相同。 Java 会将你的方法映射到接口方法。 要调用方法,可以调用接口的函数式方法名(在本例中为 `accept()`),而不是你的方法名。 现在我们来看看,将方法引用应用于基于类的函数式接口(即那些不包含基本类型的函数式接口)。下面的例子中,我创建了适合函数式方法签名的最简单的方法: ```java // functional/ClassFunctionals.java 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()`。