Java函数式编程(Lambda表达式、方法引用)详解

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 表达式的基本语法如下:

  1. 参数;
  2. 之后跟一个 ->,可以读作“产生”;
  3. -> 后面跟一个方法体。

需要注意以下几个方面:

  • 如果只有一个参数,可以只写这个参数,不写括号,也可以使用括号,尽管这种方式更不常见。
  • 如果有多个参数,将它们放在使用括号包裹起来的参数列表内。
  • 如果没有参数,必须使用括号来指示空的参数列表。
  • 如果方法体只有一行,那么方法体中表达式的结果会自动成为 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

现在 xy 可以是支持 + 操作符的任何类型,包括两种不同的数值类型,或者是一个 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
    }
}

猜你喜欢

转载自blog.csdn.net/m0_51755720/article/details/134057388