函数式编程
在介绍 Lambda 表达式之前, 首先需要引入另一个概念, 函数式编程.
函数式编程是一种编程范式, 也就是如何编写程序的方法论. 它的核心思想是将运算过程尽量写成一系列嵌套的函数调用, 关注的是做什么而不是怎么做, 因而被称为声明式编程. 以 Stateless(无状态)和 Immutable(不可变)为主要特点, 代码简洁, 易于理解, 能便于进行并行执行, 易于做代码重构, 函数执行没有顺序上的问题, 支持惰性求值, 具有函数的确定性 -- 无论在什么场景下都会得到同样的结果
我们把以前的过程式编程范式叫做 Imperative Programming - 指令式编程, 而把函数式编程范式叫做 Declarative Programming - 声明式编程. 下面通过一个简单的示例介绍两者的区别.
- // 指令式编程
- int a = 1;
- int b = 2;
- int c = a+b;
- int d = c - 10;
- // 声明式编程
- minus(plus(a, b), 10);
函数式接口
在 Java8 中, 引入了函数式接口这个新的概念, 函数式接口就是一个有且仅有一个抽象方法, 但是可以有多个非抽象方法 (静态方法和 default 关键字修饰的默认方法) 的接口.
如果接口中声明的是 java.lang.Object 类中的 public 方法, 那么这些方法就不算做是函数式接口的抽象方法. 因为任何一个实现该接口的类都会有 Object 类中公共方法的默认实现.
@FunctionalInterface 注解用于标注接口会被设计成一个函数式接口, 虽然他不是必须的, 但是推荐使用, 这样会在编译期检查使用 @FunctionalInterface 的接口是否是一个函数式接口.
Runnable 线程任务类, Comparator 比较器都只有一个抽象方法, 所以他们都是函数式接口, 另外 Java8 新引入了几个常用的泛型函数式接口 Predicate,Consumer,Function,Supplier, 以及在此基础之上扩展的一些函数式接口, 如 BiFunction,BinaryOperator 等等.
为了避免自动装箱操作, Java8 对 Predicate,Function,Supplier,Consumer 等一些通用的函数式接口的原始类型进行了特化, 例如: IntFunction.
- @Test
- public void test6() {
- IntPredicate intPredicate = (int i) -> i % 2 == 1;
- intPredicate.test(1000);
- Predicate<Integer> predicate = (Integer i) -> i % 2 == 1;
- predicate.test(1000);
- }
上面的示例中, Predicate<Integer> 每次调用它的方法时都要进行一次装箱和拆箱, 而 IntPredicate 避免了这个问题, 当处理的数据比较多时, 使用 IntPredicate 可以提高你的程序运行效率.
你可以像下面这样自定义一个函数式接口:
- @Test
- public void test3() {
- FunctionInterface1<String, Integer, List, Map<String, Object>> f1 = (str, num, list) -> new HashMap<>(16);
- }
- @FunctionalInterface
- public interface FunctionInterface1<O, T, K, R> {
- R apply(O o, T t, K k);
- }
Lambda 表达式
Lambda 表达式的基本语法是: (参数列表) -> 函数主体:
- (parameters) -> expression
- (parameters) -> {statements;}
- Runnable r1 = () -> System.out.println("test");
- Runnable r2 = () -> {
- System.out.println("test");
- };
Lambda 表达式允许你直接以内联的形式为函数式接口的抽象方法提供实现, 并把整个表达式作为函数式接口的实例(具体的说, 是函数式接口的一个具体实现的实例).
Lambda 表达式可以被赋给一个变量, 也可以作为参数传递给一个接受函数式接口作为入参的方法, 还可以作为一个返回值类型为函数式接口的方法返回值.
- public Callable<String> fetch() {
- return () -> "测试 Lambda 表达式";
- }
上面的示例中, Callable<String> 的抽象方法签名是 () -> String , 和 Lambda 表达式 () -> "测试 Lambda 表达式" 的签名是一致的, 所以可以将其作为方法返回值.
只要 Lambda 表达式和函数式接口的抽象方法签名 (及函数描述符) 相同, 则同一个 Lambda 表达式可以与多个不同的函数式接口联系起来.
- @Test
- public void test7() {
- Comparator<Apple> c1 = (a1, a2) -> a1.getWeight().compareTo(a2.getWeight());
- ToIntBiFunction<Apple, Apple> c2 = (a1, a2) -> a1.getWeight().compareTo(a2.getWeight());
- BiFunction<Apple, Apple, Integer> c3 = (a1, a2) -> a1.getWeight().compareTo(a2.getWeight());
- }
如果一个 Lambda 的主体是一个表达式, 它就和一个返回 void 的函数描述符 (即函数式接口的抽象方法签名, 例如 (T, U) -> R) 兼容. 下面这个语句是合法的, 虽然 Lambda 主体返回的是 List, 而不是 Consumer 上下文要求的 void.
Consumer<String> c = s -> Arrays.asList(s);
Lambda 表达式可以没有限制的在其主体中引用实例变量和静态变量, 但如果是局部变量, 则必须显式的声明为 final 或只能被赋值一次 https://www.zhihu.com/question/21395848 , 才能在 Lambda 主体中被引用.
- public class ChapterTest3 {
- String s1 = "";
- static String s2 = "";
- @Test
- public void test8() {
- String str = "局部变量";
- str = "局部变量";
- new Thread(() -> System.out.println(str)).start();// 局部变量 str 重新赋值了, 这一行就无法通过编译
- new Thread(() -> System.out.println(s1)).start();
- new Thread(() -> System.out.println(s2)).start();
- s1 = "实例变量";
- s2 = "静态变量";
- }
- }
方法引用主要有三类
指向静态方法的方法引用, 例如 s -> String.valueOf(s) 可简写成 String::valueOf
指向任意类型的实例方法的方法引用, 例如 (String s) -> s.length() 可简写成 String::length (简单的说, 就是你在引用一个对象的方法, 而这个对象本身是 Lambda 的一个入参)
指向 Lambda 表达式外部的已经存在的对象的实例方法的方法引用, 下面的示例很好的展示了如何将 Lambda 重构成对应的方法引用
- @Test
- public void test10() {
- Consumer<String> c1 = i -> this.run(i);
- // 上面的 Lambda 表达式可以简写成下面的方法引用, 符合方法引用的第三类方式, this 引用即所谓的外部对象
- Consumer<String> c2 = this::run;
- }
- public void run(String s) { }
- @Test
- public void test9() {
- // 指向静态方法的方法引用
- Function<Integer, String> f1 = s -> String.valueOf(s);
- Function<Integer, String> f2 = String::valueOf;
- // 指向实例方法的方法引用
- List<String> list = Arrays.asList("a", "b", "A", "B");
- list.sort((s1, s2) -> s1.compareToIgnoreCase(s2));
- // 上面这个 Lambda 表达式转变成更简洁的方法引用
- list.sort(String::compareToIgnoreCase);
- }
下面的转换模板图, 通俗易懂的总结了如何将 Lambda 表达式重构为等价的方法引用.
关于构造函数引用, 下面展示了一个简单易懂的栗子
- @Test
- public void test11() {
- // 无参构造
- Supplier<Apple> c1 = () -> new Apple();
- Supplier<Apple> c2 = Apple::new;
- Apple a1 = c2.get();
- // 有参构造
- BiFunction<String, Integer, Apple> f1 = (color, weight) -> new Apple(color, weight);//Lambda 表达式
- BiFunction<String, Integer, Apple> f2 = Apple::new;// 构造函数引用
- Apple a2 = f2.apply("red", 10);
- }
最后我们总结一下 Lambda 表达式的使用, 假设我们需要对一个 List 集合进行不同规则的排序, 这个不同规则对应的就是一个比较器 Comparator, 我们可以有多种实现方式.
最原始的方式就是定义一个 Comparator 接口的实现类作为入参, 其次就是使用匿名类的方式提供一个 Comparator 接口的实现作为入参.
在 Java8 中, 我们可以不必像上面这么啰嗦, Lambda 表达式很好地简化了这个实现过程, 比如我们这里需要按苹果的重量排序, 那么可以这样写
- @Test
- public void test12() {
- List<Apple> inventory = new ArrayList<>();
- inventory.add(new Apple("red", 94));
- inventory.add(new Apple("green", 100));
- inventory.sort((a1, a2) -> a1.getWeight().compareTo(a2.getWeight()));
- }
再想想, 还能不能更简化一下, 使用方法引用的方式进一步简化呢? 在 Comparator 接口中, 提供了静态方法 Comparator<T> comparing(Function<? super T, ? extends U> keyExtractor) , 就是为了简化 Lambda 表达式准备的, 让我们重新将上面的代码重构成方法引用
- @Test
- public void test12() {
- List<Apple> inventory = new ArrayList<>();
- inventory.add(new Apple("red", 94));
- inventory.add(new Apple("green", 100));
- inventory.sort(Comparator.comparing(Apple::getWeight));
- }
关于 Comparator 比较器, Predicate 谓词, Function 函数的组合用法
- /**
- * 函数的组合用法
- */
- @Test
- public void test15() {
- Function<String, Integer> f = i -> Integer.valueOf(i);// 方法引用写法: Integer::valueOf
- Function<Integer, Apple> g = weight -> new Apple(weight); // 构造函数引用写法: Apple::new
- Function<String, Apple> h = f.andThen(g); // andThen()相当于数学上的 g(f(x)) 函数
- Apple apple = h.apply("99"); //result: Apple(color=null, weight=99)
- Function<Apple, String> y = Apple::getColor;
- Function<Apple, Integer> z = f.compose(y); // compose()相当于数学上的 f(y(x)) 函数
- Integer result = z.apply(new Apple("red", 78));// 会报 java.lang.NumberFormatException: For input string: "red" 异常
- }
- /**
- * 谓词的组合用法
- * and 和 or 方法是按照在表达式链中的位置, 从左到右确定优先级的, 如 a.or(b).and(c).or(d) 可以看成 ((a || b) && c) || d
- */
- @Test
- public void test14() {
- Predicate<Apple> p1 = apple -> "green".equals(apple.getColor());
- final Predicate<Apple> negate = p1.negate(); // 非
- System.out.println(negate.test(new Apple("green", 98)));// result: false
- final Predicate<Apple> and = p1.and(apple -> apple.getWeight()> 150);// 与
- System.out.println(and.test(new Apple("green", 140)));//result: false
- final Predicate<Apple> or = p1.or(apple -> apple.getWeight()> 150);// 或
- System.out.println(or.test(new Apple("blue", 170)));//result: true
- }
- /**
- * 比较器组合的用法
- */
- @Test
- public void test13() {
- inventory.sort(Comparator.comparing(Apple::getWeight).reversed());// 苹果按重量倒序排序
- System.out.println(inventory);
- // 苹果按重量倒序排序, 当苹果重量相同时, 按颜色升序排序
- inventory.sort(Comparator.comparing(Apple::getWeight).reversed().thenComparing(Apple::getColor));
- System.out.println(inventory);
- }
参考资料
函数式编程初探
Java 8 实战 https://book.douban.com/subject/26772632//
来源: https://www.cnblogs.com/qingshanli/p/11743439.html