Java编程思想——函数式编程Lambda

(一)函数式编程

函数式编程(FP)的意义所在:通过合并现有代码来生成新功能而不是从头开始编写所有内容,我们可以更快地获得更可靠的代码.至少在某些情况下,这套理论似乎很有用.在这一过程中,一些非函数式语言已经习惯了使用函数式编程产生的优雅的语法

OO(object oriented,面向对象)是抽象数据,FP(functional programming,函数式编程)是抽象行为.

Java 8 的 Lambda 表达式.由箭头 -> 分隔开参数和函数体,箭头左边是参数,箭头右侧是从 Lambda 返回的表达式,即函数体.这实现了与定义类、匿名内部类相同的效果,但代码少得多.

Java 8 的方法引用,由 :: 区分.在 :: 的左边是类或对象的名称,在 :: 的右边是方法的名称,但没有参数列表

(二)Lambda表达式

(1)Lambda使用实例
Lambda 表达式是使用最小可能语法编写的函数定义:

Lambda 表达式产生函数,而不是类. 在 JVM(Java Virtual Machine,Java 虚拟机)上,一切都是一个类,因此在幕后执行各种操作使 Lambda 看起来像函数 —— 但作为程序员,你可以高兴地假装它们“只是函数”.

Lambda 语法尽可能少,这正是为了使 Lambda 易于编写和使用

interface Book {
    String getBookName(String name);
}

interface Person {
    String description(String name, int age);
}

interface Multi {
    String twoArg(String head, Double d);
}

public class LambdaExpressions {

    static Book book = b -> "书籍名称:" + b;

    static Person person = (String a, int b) -> {
        return "姓名:" + a + "--年龄:" + b;
    };

    static Multi mult = (h, n) -> h + n;

    public static void main(String[] args) {
        System.out.println(book.getBookName("三国演义"));
        System.out.println(person.description("张三", 18));
        System.out.println(mult.twoArg("Pi! ", 3.14159));
    }
}

我们从三个接口开始,每个接口都有一个单独的方法(功能性接口).但是,每个方法都有不同数量的参数,以便演示 Lambda 表达式语法

(2)任何 Lambda 表达式的基本语法

1:参数
2:接着 ->,可视为“产出”
3:-> 之后的内容都是方法体

(3)Lambda 表达式的结构
1:Lambda 表达式可以具有零个,一个或多个参数.
2:可以显式声明参数的类型,也可以由编译器自动从上下文推断参数的类型.例如(int a) 与(a)相同
3:参数用小括号括起来,用逗号分隔.例如 (a, b) 或 (int a, int b) 或 (String a, int b, float c)
4:空括号用于表示一组空的参数.例如 () -> 42.
5::当有且仅有一个参数时,如果不显式指明类型,则不必使用小括号.例如 a -> return a*a.
6:Lambda 表达式的正文可以包含零条,一条或多条语句.
7:如果 Lambda 表达式的正文只有一条语句,则大括号可不用写,且表达式的返回值类型要与匿名函数的返回类型相同,同时可以不写return关键字
static Person person = (String a, int b) -> “姓名:”+a +"–年龄:"+b;

8:如果 Lambda 表达式的正文有一条以上的语句必须包含在大括号(代码块)中,且表达式的返回值类型要与匿名函数的返回类型相同

9:如果在 Lambda 表达式中确实需要多行,则必须将这些行放在花括号中. 在这种情况下,就需要使用 return

(4)递归

递归函数是一个自我调用的函数.可以编写递归的 Lambda 表达式,但需要注意:递归方法必须是实例变量或静态变量,否则会出现编译时错误. 我们将为每个案例创建一个示例.

public class RecursiveFactorial {
    static IntCall fact;

    public static void main(String[] args) {
        fact = n -> n == 0 ? 1 : n * fact.call(n - 1);
        for (int i = 0; i <= 10; i++) {
            System.out.println(fact.call(i));
        }
    }
}

interface IntCall {
    int call(int arg);
}

上述递归代码中中间Lambda部分完整代码就是下面这样子:

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

IntCall 接口中有唯一的单个参数的方法,这里,fact 是一个静态变量. 注意使用三元 if-else. 递归函数将一直调用自己,直到 i == 0.所有递归函数都有“停止条件”,否则将无限递归并产生异常.

我们可以将 Fibonacci 序列改为使用递归 Lambda 表达式来实现,这次使用实例变量:

public class RecursiveFibonacci {
    IntCall fib;

    RecursiveFibonacci() {
        fib = n -> n == 0 ? 0 :
                n == 1 ? 1 :
                        fib.call(n - 1) + fib.call(n - 2);
    }

    int fibonacci(int n) {
        return fib.call(n);
    }

    public static void main(String[] args) {
        RecursiveFibonacci rf = new RecursiveFibonacci();
        for (int i = 0; i <= 10; i++) {
            System.out.println(rf.fibonacci(i));
        }
    }
}

下面来解释一下上面的代码,其中call方法内部实现对象通过构造函数创建而成,这里可以理解为在构造函数中构建了接口的匿名函数,然后返回了一个匿名对象实例,即子类实现了接口的方法,然后拿到该实例调用接口方法

构造函数内代码完成格式如下:

 IntCall fib = (n) -> {
        return n == 0 ? 0 : (n == 1 ? 1 : this.fib.call(n - 1) + this.fib.call(n - 2));
    };

俩个三元运算,判断n是否0,是否1,否则继续调用

(三)方法引用

(1)方法引用
类名::方法名

如果Lambda表达式需要做的事情,在另外一个类当中已经做过了,那么就可以使用方法引用的写法

静态方法: 类名称::静态方法名
成员方法: 对象实例名称::成员方法名

下面看下《Java编程思想》里面提供实例

import java.util.*;

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!");
  }
}
call()
Hello, Bob
valuable information
Help!

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

[2] show() 的签名(参数类型和返回类型)符合 Callable 的 call() 的签名.

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

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

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

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

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

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

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

[10] 最后,获取静态内部类的方法引用的操作与 [8] 中外部类方式一样.

备注:
通过上面实例可以得出一个很重要的结论,那就是通过单一方法接口可以将具有相同方法签名的方法与其他类方法映射,且使用该接口接收,通过这种方式去实现该接口具体功能

(2)Runnable接口

Runnable 接口自 1.0 版以来一直在 Java 中,因此不需要导入.它也符合特殊的单方法接口格式:它的方法 run() 不带参数,也没有返回值.因此,我们可以使用 Lambda 表达式和方法引用作为 Runnable:

@FunctionalInterface
public interface Runnable {
   
    public abstract void run();
}
 public static void main(String[] args) {
        //传统方式创建线程
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("线程运行2");
            }
        }).start();
        //Lambda方式创建线程
        new Thread(() -> System.out.println("线程运行1")).start();
    }

在这里插入图片描述

(四)函数式接口

(1)函数式接口
Java中使用Lambda表达式的前提是:必须有“函数式接口”
函数式接口:有且仅有一个抽象方法的接口;

如何才能万无一失地检测的那个接口是不是函数式接口?
用一个固定的格式写在public interface之前接口
@FunctionalInterface

@FunctionalInterface 注解是可选的; 接口中如果有多个方法则会产生编译时错误消息.

Lambda表达式要想使用,一定要有函数式接口的推断环境
1:要么通过方法的参数类型来确定那个函数式接口
2:要么通过赋值操作来确定是哪个函数式接口

Lambda的格式就是为了将抽象方法,翻译成以下三点:
1:一些参数(方法参数)
2:一个箭头
3:一些代码(方法体)

例如抽象方法
public abstract int sum(int a ,int b)
翻译成Lambda标准格式:
(int a ,int b) -> {return a+b ;}

(2)上下文推断

1:如果作为传参时是根据调用方法的参数类型来判断的
method(int a,int b)-> {return a+b;});
2:根据赋值语句左侧的类型来进行Lambda上下文推断
Calculator param=(int a,int b)-> {return a+b;};
method(param);
//(int a,int b)-> {return a+b;}; 错误写法,没有上下文环境,无法推断是那个函数式接口

@FunctionalInterface
interface Functional {
    String goodbye(String arg);
}

interface FunctionalNoAnn {
    String goodbye(String arg);
}


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 fl = a -> "Goodbye, " + a;
        FunctionalNoAnn fnal = a -> "Goodbye, " + a;
    }
}

上面代码中, Functional 和 FunctionalNoAnn 自定义接口,然而被赋值的只是方法 goodbye().首先,这只是一个方法而不是类;其次,它甚至都不是实现了该接口的类中的方法.Java 8 在这里添加了一点小魔法:如果将方法引用或 Lambda 表达式赋值给函数式接口(类型需要匹配),Java 会适配你的赋值到目标接口. 编译器会自动包装方法引用或 Lambda 表达式到实现目标接口的类的实例中.

尽管 FunctionalAnnotation 确实适合 Functional 模型,但 Java 不允许我们将 FunctionalAnnotation 像 fac 定义一样直接赋值给 Functional,因为它没有明确地实现 Functional 接口. 令人惊奇的是 ,Java 8 允许我们以简便的语法为接口赋值函数.

java.util.function 包旨在创建一组完整的目标接口,使得我们一般情况下不需再定义自己的接口.这主要是因为基本类型会产生一小部分接口. 如果你了解命名模式,顾名思义就能知道特定接口的作用.

(3)函数式接口基本命名准则
1:如果只处理对象而非基本类型,名称则为 Function,Consumer,Predicate 等.参数类型通过泛型添加.

2:如果接收的参数是基本类型,则由名称的第一部分表示,如 LongConsumer,DoubleFunction,IntPredicate 等,但基本 Supplier 类型例外.

3:如果返回值为基本类型,则用 To 表示,如 ToLongFunction 和 IntToLongFunction.

4:如果返回值类型与参数类型一致,则是一个运算符:单个参数使用 UnaryOperator,两个参数使用 BinaryOperator.

5:如果接收两个参数且返回值为布尔值,则是一个谓词(Predicate).

6:如果接收的两个参数类型不同,则名称中有一个 Bi

(4)多参数函数式接口
java.util.functional 中的接口是有限的。比如有了 BiFunction,但它不能变化。 如果需要三参数函数的接口怎么办? 其实这些接口非常简单,很容易查看 Java 库源代码并自行创建。代码示例:

@FunctionalInterface
public interface TriFunction<T, U, V, R> {
    R apply(T t, U u, V v);
}
public class TriFunctionTest {
    static double f(int i, long l, double d) {
        return i + l + d;
    }

    public static void main(String[] args) {
        TriFunction<Integer, Long, Double, Double> tf =
                TriFunctionTest::f;
        //调用函数,传递各个参数
        Double apply = tf.apply(2, 30L, 33.1);
        System.out.println(apply);
    }
}

在这里插入图片描述
解释:
TriFunction接口定义了函数式接口,其中有三个不同的基本类型参数,有返回值
TriFunction<Integer, Long, Double, Double> tf =
TriFunctionTest::f;
通过方法引用,将对应参数和返回值装配到函数式接口中,前三个为参数,第四个参数为返回值类型
apply方法用于传递参数

原创文章 105 获赞 33 访问量 1万+

猜你喜欢

转载自blog.csdn.net/Octopus21/article/details/105254240