• 初识Java 11-1 函数式编程


    目录

    旧方式与新方式

    lambda表达式

    方法引用

    Runnable

    未绑定方法引用

    构造器方法引用

    函数式接口

    带有更多参数的函数式接口

    解决缺乏基本类型函数式接口的问题


    本笔记参考自: 《On Java 中文版》


            函数式编程语言的一个特点就是其处理代码片段的简易性,就像处理数据一样简单。Java 8加入的lambda表达式方法引用为函数式风格编程做出了一定的支持。

            在计算机的早期时代,为了让程序能够适应有限的内存,程序员往往需要在程序执行时修改内存中的代码,让程序做出不同的行为,依此节省空间。这就是自修改代码技术。因为彼时的程序大都足够小,因此维护起来并不会太麻烦。

            但随着内存的增大,自修改代码被认为是一个糟糕的想法,它极大地增加了程序的维护成本。尽管如此,这种使用代码以某种方式操纵其他代码的想法依旧十分吸引人:通过组合已经经过良好测试的代码,我们可以生产出更有效率、更加安全的代码。

            函数式编程的意义就在于此:通过整合现有代码来产生新的功能,而不是从零开始编写所有内容,由此可以得到更加可靠、实现起来更快的代码。

        面向对象编程抽象数据,而函数式编程抽象行为。

            纯函数式语言在安全方面规定了额外的约束条件,所有的数据必须是不可变的:设置一次,永不改变。此时函数绝对不会修改现有值,而是只生成新值。(纯函数式语言在面对一些问题时能够提出一个好的解决,但这不代表纯函数式语言就是最好的解决方式)

            Python等非函数式编程语言已经将函数式编程的概念纳入其中,并且受益匪浅。Java也加入了类似的特性。

    旧方式与新方式

            通过将代码传递给方法,我们可以控制方法,使其产生出不同的行为。

            旧的方式是创建一个对象(在下例中是Strategy),让其的某个方法包含所需行为,在将这个对象传递给我们想要控制的方法:

    1. package functional;
    2. import java.util.Locale;
    3. interface Strategy {
    4. String approch(String msg);
    5. }
    6. class Soft implements Strategy {
    7. @Override
    8. public String approch(String msg) {
    9. return msg.toLowerCase() + "?";
    10. }
    11. }
    12. class Unrelated {
    13. static String twice(String msg) {
    14. return msg + " " + msg;
    15. }
    16. }
    17. public class Strategize {
    18. Strategy strategy;
    19. String msg;
    20. Strategize(String msg) {
    21. strategy = new Soft(); // 将Soft()作为一个默认的决策
    22. this.msg = msg;
    23. }
    24. void communicate() {
    25. System.out.println(strategy.approch(msg));
    26. }
    27. void changeStrategy(Strategy strategy) {
    28. this.strategy = strategy;
    29. }
    30. public static void main(String[] args) {
    31. Strategy[] strategies = {
    32. new Strategy() { // 创建一个匿名内部类来改变行为,虽然依旧会有重复的代码
    33. @Override
    34. public String approch(String msg) {
    35. return msg.toUpperCase() + "!";
    36. }
    37. },
    38. msg -> msg.substring(0, 5), // 这就是Java 8开始提供的lambda表达式
    39. Unrelated::twice // 这也是Java 8中出现的方法引用
    40. };
    41. Strategize s = new Strategize("Hello there");
    42. s.communicate();
    43. for (Strategy newStrategy : strategies) {
    44. s.changeStrategy(newStrategy); // 遍历数组strategies中的每一个决策,并将其放入s中进行决策更换
    45. s.communicate(); // 更换决策后,每一次输出都会产生不同的结果:我们传递了行为,而被仅仅是数据
    46. }
    47. }
    48. }

            程序执行的结果是:

            Strategy提供的接口包含了唯一的approach()方法。通过创建不同的Strategy对象,就可以创建不同的行为。

            在上述程序中,包含了默认的决策Soft()和一个匿名内部类。除此之外,还出现两个了Java 8添加的新内容:

    1. lambda表达式:
      msg -> msg.substring(0, 5)

      这种表达式的特点是使用箭头->分隔参数和函数体。

    2. 方法引用:
      Unrelated::twice

      特点是::。其中,::左边是类名或对象名,右边是方法名,但没有参数列表。

            在Java 8之前,使用普通的类或者匿名内部类来传递功能,但这种语法的并不方便。lambda表达式和方法引用改变了这种情况,使得传递功能变得更加便捷。

    lambda表达式

    ||| lambda表达式是使用尽可能少的语法编写的函数定义

            可以说,lambda表达式产生的是函数,而不是方法。当然,Java中的一切都是类,之所以lambda表达式会让人产生这种“错觉”,是因为幕后进行了各种各样的操作。作为程序员,我们可以将lambda表达式视为函数。

            lambda表达式的语法宽松,且易于编写。例如:

    1. package functional;
    2. interface Description {
    3. String brief();
    4. }
    5. interface Body {
    6. String detailed(String head);
    7. }
    8. interface Multi {
    9. String twoArg(String head, Double d);
    10. }
    11. public class LambdaExpressions {
    12. static Body bod = h -> h + "No Parens!"; // 本条语句并不需要使用括号(仅限只有一个参数时)
    13. static Body bod2 = (h) -> h + "More details"; // 使用了括号。处于一致性的考虑,在只有一个参数时也使用括号
    14. static Description desc = () -> "Short info"; // 若没有参数,必须使用括号来指示空的参数列表
    15. static Multi mult = (h, n) -> h + n; // 有多个参数,此时必须将它们放在使用括号包裹的参数列表中
    16. static Description moreLines = () -> {
    17. System.out.println("moreLines()");
    18. return "from moreLines()";
    19. };
    20. public static void main(String[] args) {
    21. System.out.println(bod.detailed("Oh!"));
    22. System.out.println(bod2.detailed("Hi!"));
    23. System.out.println(desc.brief());
    24. System.out.println(mult.twoArg("Pi: ", 3.14159));
    25. System.out.println(moreLines.brief());
    26. }
    27. }

            程序执行的结果是:

            在上述的3个接口中,每个接口都有一个方法(这是后续会提到的函数式接口)。任何lambda表达式的基本语法如下:

        注释中提到过,若没有参数,就必须使用括号来指示空的参数列表。

            对一行的lambda表达式而言,方法体中表达式的结果会自动成为lambda表达式的返回值,所以这里使用return关键字是不和法的。另外,若lambda表达式需要多行代码,如上文中的moreLines,就需要将表达式的代码放入到花括号中。此时又会需要使用return从lambda表达式中生成一个值了。

        可以看到,lambda表达式可以通过接口更方便地生成行为不同的对象。

            最后,若仔细观察代码可以发现,lambda表达式返回的是一个方法引用。可以将这个引用打印出来:

    1. Body bod_other = new Body() { // 实现一个匿名内部类
    2. @Override
    3. public String detailed(String head) {
    4. return "";
    5. }
    6. };
    7. System.out.println(bod_other); // 打印普通类的信息
    8. System.out.println(bod); // 打印lambda表达式的引用

            可以发现结果有所不同:

    lambda表达式的末尾会有一串uuid(通用唯一识别码),用以区分方法。

    递归

            递归,即函数调用了自身。Java也允许编写递归的lambda表达式,但需要注意一点:这个lambda表达式必须被赋值给一个静态变量或一个实例变量。通过两个示例说明这些情况:

            两个示例会使用一个相同的接口:

    1. interface IntCall {
    2. int call(int arg);
    3. }

    【示例:静态变量】实现一个阶乘函数,递归计算小于等于n的正整数的乘积:

    1. public class RecursiveFactorial {
    2. static IntCall fact;
    3. public static void main(String[] args) {
    4. fact = n -> n == 0 ? 1 : n * fact.call(n - 1);
    5. for (int i = 0; i <= 10; i++)
    6. System.out.println(fact.call(i));
    7. }
    8. }

            程序执行的结果是:

            在这个例子中,fact就是一个静态变量。递归函数会不断调用其自身,因此必须有某种停止条件(在上述例子中是n == 0),否则就会陷入无限递归,直到栈空间被耗尽。

            下方这种初始化fact的方式是不被允许的:

    static IntCall fact = n -> n == 0 ? 1 : n * fact.call(n - 1);

    这种处理对Java编译器而言还是太过复杂了,会导致编译错误。

    ---

    【示例:示例变量】实现斐波那契数列:

    1. public class RecursiveFibonacci {
    2. IntCall fib;
    3. RecursiveFibonacci() {
    4. fib = n -> n == 0 ? 0 :
    5. n == 1 ? 1 :
    6. fib.call(n - 1) + fib.call(n - 2);
    7. }
    8. int fibonacci(int n) {
    9. return fib.call(n);
    10. }
    11. public static void main(String[] args) {
    12. RecursiveFibonacci rf = new RecursiveFibonacci();
    13. for (int i = 0; i <= 10; i++)
    14. System.out.println(rf.fibonacci(i));
    15. }
    16. }

            程序执行的结果是:

    方法引用

            Java 8提供的方法引用,其指向的是方法。方法引用的格式如下:

    1. interface Callable {
    2. void call(String s); // 与hello()和show()的签名了保持一致
    3. }
    4. class Describe {
    5. void show(String msg) {
    6. System.out.println(msg);
    7. }
    8. }
    9. public class MethodReferences {
    10. static void hello(String name) {
    11. System.out.println("Hello, " + name);
    12. }
    13. static class Description { // 定义一个内部类
    14. String about;
    15. Description(String desc) {
    16. about = desc;
    17. }
    18. void help(String msg) {
    19. System.out.println(about + " " + msg);
    20. }
    21. }
    22. static class Helper {
    23. static void assist(String msg) { // assist()是静态内部类中的一个静态方法
    24. System.out.println(msg);
    25. }
    26. }
    27. public static void main(String[] args) {
    28. Describe d = new Describe();
    29. Callable c = d::show; // 将Describe对象的show方法赋给了Callable
    30. c.call("call()"); // 通过call(),调用了show()
    31. c = MethodReferences::hello; // 等号右边是一个静态方法引用
    32. c.call("Bob");
    33. c = new Description("valuable")::help; // 对某个活跃对象上的方法的方法引用(“绑定方法引用”)
    34. c.call("information");
    35. c = Helper::assist; // 获得静态内部类中的静态方法的方法引用
    36. c.call("Help!");
    37. }
    38. }

            程序执行的结果是:

            在上述程序中,Callable.call()Describe.show()MethodReferences.hello(),这三者的签名保持了一致。这解释了为什么语句 Callable c = d::show; 及其之后的语句能够顺利编译。

    Runnable

            Runnable是一个java.lang包提供的接口。这个包遵循特殊的单方法接口格式:它的run()方法没有参数,也没有返回值。

    因此,可以将lambda表达式或方法引用用作Runnable

    1. class Go {
    2. static void go() {
    3. System.out.println("方法引用Go::go()");
    4. }
    5. }
    6. public class RunnableMethodReference {
    7. public static void main(String[] args) {
    8. new Thread(new Runnable() {
    9. @Override
    10. public void run() {
    11. System.out.println("定义一个run()方法");
    12. }
    13. }).start();
    14. new Thread(() -> System.out.println("这是一个lambda表达式")).start();
    15. new Thread(Go::go).start();
    16. }
    17. }

            程序执行的结果是:

            Thread类在官方文档中的描述如下:

            Thread会接受一个Runnable作为其构造器参数,它的start()方法会调用run()

        只有匿名内部类需要提供run()方法。


    绑定方法引用

            未绑定方法引用:指的是尚未关联到某个对象的普通(非静态)方法。对于未绑定引用,必须先提供对象,然后才能使用:

    1. class X {
    2. String f() {
    3. return "X::f()";
    4. }
    5. }
    6. interface MakeString {
    7. String make();
    8. }
    9. interface TransformX {
    10. String transform(X x);
    11. }
    12. public class UnboundMethodReference {
    13. public static void main(String[] args) {
    14. // MakeString ms = X::f; // 无效的方法引用
    15. TransformX sp = X::f;
    16. X x = new X();
    17. // 下列两条语句的效果是相同的
    18. System.out.println(sp.transform(x));
    19. System.out.println(x.f());
    20. }
    21. }

            程序执行的结果是:

            在上述例子之前,示例中对方法的引用,方法与其关联接口的签名是相同的。但这里出现了特例:

    MakeString ms = X::f;

    编译器不允许上述语句的编译,若强制执行,会引发报错(此为IDEA的报错信息)

    这个报错指出X::f是一个未绑定方法引用,因为这里涉及到了一个隐藏的参数:this。若把这条有问题的语句换成下列语句,则没有问题:

    1. X x = new X();
    2. MakeString ms = x::f; // 无效的方法引用

    上下两种语句的区别就在于,下方的语句提供了一个可供附着的X的对象x,这使得调动f()变为可能。X::f本身是无法“绑定到”一个对象上的。

            显而易见,除了自己生成一个对象外,我们还有另一个方式能解决这个问题。关键在于,我们还需要一个额外的参数,如TransformX中所示:

    String transform(X x);

    这种做法告诉我们:函数式方法(接口中的单一方法)的签名与方法引用的签名不必完全匹配。

            最后再看看这条语句:

    System.out.println(sp.transform(x));

    在前述知识的基础上,可以推断这条语句执行的过程:println()接受了一个未绑定引用,x作为参数在这个引用中调用了transform(),最终调用了x.f()

            若一个方法具有多个参数,则只需要让第一个参数使用这种this的模式即可:

    1. class This {
    2. void two(int i, double d) {
    3. }
    4. void three(int i, double d, String s) {
    5. }
    6. void four(int i, double d, String s, char c) {
    7. }
    8. }
    9. interface TwoArgs {
    10. void call2(This athis, int i, double d);
    11. }
    12. interface ThreeArgs {
    13. void call3(This athis, int i, double d, String s);
    14. }
    15. interface FourArgs {
    16. void call4(This athis, int i, double d, String s, char c);
    17. }
    18. public class MultiUnbound {
    19. public static void main(String[] args) {
    20. TwoArgs twoargs = This::two;
    21. ThreeArgs threeargs = This::three;
    22. FourArgs fourargs = This::four;
    23. This athis = new This();
    24. twoargs.call2(athis, 11, 2.14);
    25. threeargs.call3(athis, 11, 3.14, "Three");
    26. fourargs.call4(athis, 11, 3.14, "Four", 'Z');
    27. }
    28. }

    构造器方法引用

            同样的,也可以对构造器的引用进行捕获,此后通过这个引用来调用构造器:

    1. class Dog {
    2. String name;
    3. int age = -1;
    4. Dog() {
    5. name = "流浪狗";
    6. }
    7. Dog(String nm) {
    8. name = nm;
    9. }
    10. Dog(String nm, int yrs) {
    11. name = nm;
    12. age = yrs;
    13. }
    14. }
    15. interface MakeNoArgs {
    16. Dog make();
    17. }
    18. interface Make1Arg {
    19. Dog make(String nm);
    20. }
    21. interface Make2Args {
    22. Dog make(String nm, int age);
    23. }
    24. public class CtorReference {
    25. public static void main(String[] args) {
    26. // 所有这3个构造器都只有一个名字 ::new
    27. MakeNoArgs mna = Dog::new;
    28. Make1Arg m1a = Dog::new;
    29. Make2Args m2a = Dog::new;
    30. Dog dn = mna.make();
    31. Dog d1 = m1a.make("卡卡");
    32. Dog d2 = m2a.make("拉尔夫", 4);
    33. }
    34. }

            注意语句Dog::new。3条相同的语句告诉我们,这些构造器都有(且只有)一个名字 —— ::new。并且每一个引用都被赋予了不同的接口,编译器可以从接口来推断所需使用的构造器。

            在这里,调用函数式接口方法(make())意味着调用构造器。

    函数式接口

            方法引用和lambda表达式都需要先赋值,然后才能进行使用。而这些赋值都需要类型信息,让编译器确保类型的正确性。尤其是lambda表达式。例如:

    x -> x.toString()

            toString()方法会返回String,但上述语句并没有表示x的类型。这时候就需要进行类型推断了。因此,编译器必须要能够通过某种方式推断出x的类型。

            还有其他例子:

    1. (x, y) -> x + y // 需要考虑String类型存在与否
    2. System.out::println

    为了解决这种类型推断的问题,Java 8引入了包含一组接口的java.util.function,这些接口是lambda表达式和方法引用的目标类型。其中的每个接口都只包含了一个抽象方法(非抽象方法可以有多个),被称为函数式方法

            使用了这种”函数式方法“模式的接口,可以通过@FunctionalInterface注解来强制执行:

    1. @FunctionalInterface
    2. interface Functional { // 使用了注解
    3. String goodbye(String arg);
    4. }
    5. interface FunctionNoAnn { // 没有使用注解
    6. String goodbye(String arg);
    7. }
    8. //@FunctionalInterface
    9. //interface NoFunctional{ // 内置了两个方法,不符合函数式方法定义
    10. // String goodbye(String arg);
    11. // String hello(String arg);
    12. //}
    13. public class FunctionalAnnotation {
    14. public String goodbye(String arg) {
    15. return "Goodbye, " + arg;
    16. }
    17. public static void main(String[] args) {
    18. FunctionalAnnotation fa = new FunctionalAnnotation();
    19. Functional f = fa::goodbye;
    20. FunctionNoAnn fna = fa::goodbye;
    21. // Functional fac = fa; // 类型不兼容
    22. Functional f1 = arg -> "Goodbye, " + arg;
    23. FunctionNoAnn fnal = arg -> "Goodbye, " + arg;
    24. }
    25. }

            @FunctionalInterface注解是可选的。当只有一个方法时,Java把main()中的FunctionalFunctionalNoAnn都视为了函数式接口。

            现在看向两条赋值语句:

    1. Functional f = fa::goodbye;
    2. FunctionNoAnn fna = fa::goodbye;

    这两条赋值语句均把一个方法(这个方法甚至不是接口方法的实现)赋值给了一个接口引用。这是Java 8增加的功能:若把一个方法引用或lambda表达式赋值给某个函数式接口(且类型匹配),匿名Java会调整这次赋值,使其能够匹配目标接口。

        在底层的实现中,Java编译器会创建一个实现了目标接口的类的示例,并将我们进行赋值的方法引用或lambda表达式包裹在其中。

            使用了@FunctionalInterface注解的接口也叫做单一抽象方法

    命名规则

            java.util.function旨在创建一套足够完备的接口。一般来说,可以通过接口的名字了解接口的作用。以下是基本的命名规则(也可以去官方文档进行查看):

    • 只处理对象(而不是基本类型):名字较为直接,如FunctionConsumerPredicate等。
    • 接受一个基本类型的参数:使用名字的第一部分表示,如LongConsumerDoubleFunctionInPredicate(例外:基本的Supplier类型)
    • 返回的是基本类型的结果:To表示,例如ToLongFunctionIntToLongFunction
    • 返回类型和参数类型相同:被命名为OperatorUnaryOperator表示一个参数,BinaryOperator表示两个参数。
    • 接受一个参数并返回boolean被命名为Predicate
    • 接受两个不同类型的参数:名字中会有一个Bi(比如BiPredicate)。

        因为基本类型的存在,Java在设计这些接口时不得不考虑众多的类型,这无疑增加了Java的复杂性。

            例如:

    1. import java.util.function.*;
    2. class Foo {
    3. }
    4. class Bar {
    5. Foo f;
    6. Bar(Foo f) {
    7. this.f = f;
    8. }
    9. }
    10. class IBaz {
    11. int i;
    12. IBaz(int i) {
    13. this.i = i;
    14. }
    15. }
    16. class LBaz {
    17. long l;
    18. LBaz(long l) {
    19. this.l = l;
    20. }
    21. }
    22. class DBaz {
    23. double d;
    24. DBaz(double d) {
    25. this.d = d;
    26. }
    27. }
    28. public class FunctionVariants {
    29. static Function f1 = f -> new Bar(f);
    30. static IntFunction f2 = i -> new IBaz(i);
    31. static LongFunction f3 = l -> new LBaz(l);
    32. static ToLongFunction f4 = lb -> lb.l;
    33. static DoubleToIntFunction f5 = d -> (int) d;
    34. public static void main(String[] args) {
    35. Bar b = f1.apply(new Foo());
    36. IBaz ib = f2.apply(11);
    37. LBaz lb = f3.apply(12);
    38. long l = f4.applyAsLong(lb);
    39. int i = f5.applyAsInt(14);
    40. }
    41. }

            在一些情况下,需要使用类型转换,否则编译器会报出截断错误。上述程序中的每个方法都会调用其关联的lambda表达式。

            方法引用还有一些特别的用法:

    1. import java.util.function.BiConsumer;
    2. class In1 {
    3. }
    4. class In2 {
    5. }
    6. public class MethodConversion {
    7. static void accept(In1 i1, In2 i2) {
    8. System.out.println("accept()");
    9. }
    10. static void someOtherName(In1 i1, In2 i2) {
    11. System.out.println("somwOtherName()");
    12. }
    13. public static void main(String[] args) {
    14. BiConsumer bic;
    15. bic = MethodConversion::accept;
    16. bic.accept(new In1(), new In2());
    17. bic = MethodConversion::someOtherName;
    18. // bic.someOtherName(new In1(), new In2); //行不通
    19. bic.accept(new In1(), new In2());
    20. }
    21. }

            程序执行的结果是:

            以下是BitConSumer的文档说明:

    这个接口有一个accept()方法可以被用作方法引用。并且,即使名字并不相同,如someOtherName(),只要参数类型和返回类型能够与BiConsumeraccept()相同,也没有问题。

        使用函数式接口时,名字不重要,重要的是参数类型和返回类型。

            Java会负责将我们起的名字映射到函数式方法上。若要调用我们的方法,就需要调用这个函数式方法的名字。

    带有更多参数的函数式接口

            java.util.function中的接口是有限的,同时也是直观易懂的。因此,当我们需要的接口并没有在java.util.function中被提供时,我们也可以轻松地编写自己的接口:

    1. @FunctionalInterface
    2. public interface TriFunction {
    3. R apply(T t, U u, V v);
    4. }

            现在这个接口就可以被使用了:

    1. public class TriFunctionTest {
    2. static int f(int i, long l, double d) {
    3. return 99;
    4. }
    5. public static void main(String[] args) {
    6. TriFunction tf = TriFunctionTest::f;
    7. tf = (i, l, d) -> (i + l.intValue() + d.intValue());
    8. System.out.println(tf.apply(12, 12l, 12d));
    9. }
    10. }

            程序执行成功,输出36


    解决缺乏基本类型函数式接口的问题

            可以通过使用BiConsumer这种面向对象的接口,开创建java.util.function中没有提供的,涉及int等基本类型的函数式接口:

    1. import java.util.function.BiConsumer;
    2. public class BiConsumerPermutations {
    3. static BiConsumer bicid = (i, d) -> System.out.format("%d, %f%n", i, d);
    4. static BiConsumer bicdi = (d, i) -> System.out.format("%f, %d%n", d, i);
    5. static BiConsumer bicil = (i, l) -> System.out.format("%d, %d%n", i, l);
    6. public static void main(String[] args) {
    7. bicid.accept(11, 45.14);
    8. bicdi.accept(11.45, 14);
    9. bicil.accept(1, 14L);
    10. }
    11. }

            程序执行的结果是:

            上述程序使用了System.out.format(),这个方法支持%n这种跨平台的字符。

            这个例子中发生了自动装箱和自动拆箱,通过这种方式,我们可以获得处理基本类型的接口。同样的,可以在其他函数式接口中使用包装类:

    1. import java.util.function.Function;
    2. import java.util.function.IntToDoubleFunction;
    3. public class FunctionWithWrapped {
    4. public static void main(String[] args) {
    5. Function fid = i -> (double) i;
    6. IntToDoubleFunction fid2 = i -> i;
    7. }
    8. }

            需要注意的是使用强制类型转换的时机,否则会出现报错。

            可以发现,只需要通过包装类就可以获得一个用来处理基本类型的函数式接口。因此,若存在函数式接口的基本类型变种,其唯一的原因就是防止自动装箱/拆箱过程带来的性能损耗。

  • 相关阅读:
    Java设计模式(二)创建型设计模式
    logging.level的含义及设置 【java 日志 (logback、log4j)】
    win11该文件没有与之关联的应用怎么办
    逆向案例二:关键字密文解密,自定义的加密解密。基于企名片科技的爬取。
    免费开源圈子社交交友社区系统 可打包小程序 支持二开 源码交付!
    Linux4.4内核构建脚本分析(一)- vmlinux的构建
    Linux学习之进程三
    阿里云无影升级2.0 云电脑解决方案时代到来
    JDBC的工作原理
    linux安装Chrome跑web自动化
  • 原文地址:https://blog.csdn.net/w_pab/article/details/133266095