十三、函数式编程(2)

本章概要

  • 方法引用
    • Runnable 接口
    • 未绑定的方法引用
    • 构造函数引用
  • 函数式接口
    • 多参数函数式接口
    • 缺少基本类型的函数

方法引用

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!");
    }
}

输出结果:

十三、函数式编程(2)_第1张图片

[1] 我们从单一方法接口开始(同样,你很快就会了解到这一点的重要性)。

[2] show() 的签名(参数类型和返回类型)符合 Callablecall() 的签名。

[3] hello() 也符合 call() 的签名。

[4] help() 也符合,它是静态内部类中的非静态方法。

[5] assist() 是静态内部类中的静态方法。

[6] 我们将 Describe 对象的方法引用赋值给 Callable ,它没有 show() 方法,而是 call() 方法。 但是,Java 似乎接受用这个看似奇怪的赋值,因为方法引用符合 Callablecall() 方法的签名。

[7] 我们现在可以通过调用 call() 来调用 show(),因为 Java 将 call() 映射到 show()

[8] 这是一个静态方法引用。

[9] 这是 [6] 的另一个版本:对已实例化对象的方法的引用,有时称为_绑定方法引用_。

[10] 最后,获取静态内部类中静态方法的引用与 [8] 中通过外部类引用相似。

上例只是简短的介绍,我们很快就能看到方法引用的所有不同形式。

Runnable接口

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();
    }
}

输出结果:

十三、函数式编程(2)_第2张图片

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()); // 同等效果
    }
}

输出结果:

十三、函数式编程(2)_第3张图片

到目前为止,我们已经见过了方法引用和对应接口的签名(参数类型和返回类型)一致的几个赋值例子。 在 [1] 中,我们尝试同样的做法,把 Xf() 方法引用赋值给 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

现在 xy 可以是任何支持 + 运算符连接的数据类型,可以是两个不同的数值类型或者是 一个 String 加任意一种可自动转换为 String 的数据类型(这包括了大多数类型)。 但是,当 Lambda 表达式被赋值时,编译器必须确定 xy 的确切类型以生成正确的代码。

该问题也适用于方法引用。 假设你要传递 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() 中把 FunctionalFunctionalNoAnn 都当作函数式接口来看待。 在 NotFunctional 的定义中可看出@FunctionalInterface 的作用:当接口中抽象方法多于一个时产生编译期错误。

仔细观察在定义 ffna 时发生了什么。 FunctionalFunctionalNoAnn 声明了是接口,然而被赋值的只是方法 goodbye()。首先,这只是一个方法而不是类;其次,它甚至都不是实现了该接口的类中的方法。这是添加到Java 8中的一点小魔法:如果将方法引用或 Lambda 表达式赋值给函数式接口(类型需要匹配),Java 会适配你的赋值到目标接口。 编译器会在后台把方法引用或 Lambda 表达式包装进实现目标接口的类的实例中。

虽然 FunctionalAnnotation 确实符合 Functional 模型,但是 Java不允许我们像fac定义的那样,将 FunctionalAnnotation 直接赋值给 Functional,因为 FunctionalAnnotation 并没有显式地去实现 Functional 接口。唯一的惊喜是,Java 8 允许我们将函数赋值给接口,这样的语法更加简单漂亮。

java.util.function 包旨在创建一组完整的目标接口,使得我们一般情况下不需再定义自己的接口。主要因为基本类型的存在,导致预定义的接口数量有少许增加。 如果你了解命名模式,顾名思义就能知道特定接口的作用。

以下是基本命名准则:

  1. 如果只处理对象而非基本类型,名称则为 FunctionConsumerPredicate 等。参数类型通过泛型添加。
  2. 如果接收的参数是基本类型,则由名称的第一部分表示,如 LongConsumerDoubleFunctionIntPredicate 等,但返回基本类型的 Supplier 接口例外。
  3. 如果返回值为基本类型,则用 To 表示,如 ToLongFunction IntToLongFunction
  4. 如果返回值类型与参数类型相同,则是一个 Operator :单个参数使用 UnaryOperator,两个参数使用 BinaryOperator
  5. 如果接收参数并返回一个布尔值,则是一个 谓词 (Predicate)。
  6. 如果接收的两个参数类型不同,则名称中有一个 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 时,设计者们做出了一些选择。

例如,为什么没有 IntComparatorLongComparatorDoubleComparator 呢?有 BooleanSupplier 却没有其他表示 Boolean 的接口;有通用的 BiConsumer 却没有用于 intlongdoubleBiConsumers 变体(我理解他们为什么放弃这些接口)。这到底是疏忽还是有人认为其他组合使用得很少呢(他们是如何得出这个结论的)?

你还可以看到基本类型给 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());
    }
}

输出结果:

十三、函数式编程(2)_第4张图片

查看 BiConsumer 的文档,你会看到它的函数式方法为 accept() 。 的确,如果我们将方法命名为 accept(),它就可以作为方法引用。 但是我们也可用不同的名称,比如 someOtherName()。只要参数类型、返回类型与 BiConsumeraccept() 相同即可。

因此,在使用函数接口时,名称无关紧要——只要参数类型和返回类型相同。 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,看看我们将如何创建各种缺失的预定义组合,涉及 intlongdouble (基本类型):

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);
    }
}

输出结果:

十三、函数式编程(2)_第5张图片

这里使用 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 源代码)——尽管这里出现性能瓶颈的可能性不大。

你可能感兴趣的:(#,On,Java,基础卷,Runnable,接口,未绑定的方法引用,构造函数引用,多参数函数式接口,缺少基本类型的函数)