函数式编程

在介绍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或只能被赋值一次,才能在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);
}

匿名类和Lambda的区别

在匿名类中,this代表的是类自身,但是在Lambda中,它代表的是包含类。其次,匿名类可以屏蔽包含类的变量,而Lambda表达式则不能(即Lambbda表达式内外不能出现同一名称的变量)。

public class ChapterTest8 {
int a = 1; @Test
public void test1() {
int a = 2;
Runnable r1 = () -> {
//int a = 3;//编译不通过
System.out.println(this.a);//result: 1
};
Runnable r2 = new Runnable() {
int a = 4;
@Override
public void run() {
int a = 5;
System.out.println(this.a);//result: 4
}
};
new Thread(r1).start();
new Thread(r2).start();
}
}

上面的代码中,Lambda表达式中的 this.a 指向的是 ChapterTest8 类中的实例变量,所以输出是 ,而匿名类中的 this.a 指向的是匿名类自身的实例变量,所以输出是 。

另外上面代码Lambda表达式中的  int a = 3; 编译是无法通过的,因为在Lambda表达式外面已经有两个同名的局部变量和实例变量。匿名类则不会有这个问题。

重载方法的Lambda匹配问题

在涉及重载的上下文里, 将匿名类转换为Lambda表达式可能导致最终的代码更加晦涩(比如重载的方法入参具有相同的函数描述符), 可以使用显式的类型转换来解决这个问题。

    @Test
public void test2() {
doSomething((Task) () -> System.out.println());//此处重载的方法入参具有相同的函数描述符() -> void, 可以使用显式的类型转换来解决这个问题
} private void doSomething(Runnable r) {
r.run();
} private void doSomething(Task t) {
t.execute();
} @FunctionalInterface
interface Task {
void execute();
}

上面的代码示例中,重载方法 doSomething() 的入参都是一个函数式接口,他们具有相同的函数描述符 () -> void ,因此这里使用Lambda表达式作为入参,编译器会无法根据上下文推断出你要调用的是哪个方法,你可以对传入的Lambda表达式做一个显式的类型转换,即可解决这个问题。

使用Lambda表达式重构常用的设计模式

策略模式

通过Lambda表达式来直接传递不同的策略, 不需要像Java8之前那样针对每个策略提供具体的实现

    @Test
public void test4() throws IOException {
//这里 i -> i.length() > 8 就是一个策略
boolean r1 = new strategy(i -> i.length() > 8).test("djdjdsjdj");
} class strategy {
//假定这是一个自定义的策略
private Predicate<String> predicate; public strategy(Predicate<String> predicate) {
this.predicate = predicate;
} public boolean test(String s) {
return predicate.test(s);
}
}

模板模式

在Java8之前, 模板模式通常这样写

    abstract class OnlineBanking {
/**
* 模板方法: 封装不变部分,扩展可变部分
*/
public final void processCustomer(int id) {
Customer c = DataBase.getCustomerById(id);
this.makeCustomerHappy(c);
} /**
* 可变部分由子类去实现
*/
abstract void makeCustomerHappy(Customer c);
}

如上, OnlineBanking抽象类将一些通用算法抽象出来, 封装到 processCustomer() 方法中。而其他需要扩展的可变部分, 定义一个抽象类 makeCustomerHappy() , 交由不同的子类去实现。

现在你可以用Lambda表达式来实现同样的效果, 而且不再需要针对每个不同的算法去创建一个具体的实现类。

    @Test
public void test5() {
new OnlineBankingLambda().processCustomer(9527, customer -> System.out.println("不同的行为参数化传递给模板方法"));
} class OnlineBankingLambda {
/**
* 模板方法: 封装不变部分,扩展可变部分
*/
public final void processCustomer(int id, Consumer<Customer> consumer) {
Customer c = DataBase.getCustomerById(id);
//在Java8, 扩展的可变部分可以直接通过不同的行为参数化传递给模板方法, 不再需要创建一个子类去具体的实现.
consumer.accept(c);
}
}

上面模板模式的原始写法和Lambda写法仅是作为一个对比, 在实际业务中, 两者没有绝对的优劣之分。比如当需要扩展的可变算法种类比较多时, 如果使用Lambda表达式的写法, 那么每个可变算法就对应一个函数式接口, 这样反而会让代码的结构变得更加混乱, 可阅读性也大大降低。

参考资料

函数式编程初探

Java 8实战

作者:张小凡
出处:https://www.cnblogs.com/qingshanli/

本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。如果觉得还有帮助的话,可以点一下右下角的【推荐】。

Java8系列 (一) Lambda表达式的更多相关文章

  1. 怒学Java8系列一:Lambda表达式

    PDF文档已上传Github  Github:https://github.com/zwjlpeng/Angrily_Learn_Java_8 第一章 Lambda 1.1 引言 课本上说编程有两种模 ...

  2. Java8学习笔记----Lambda表达式 (转)

    Java8学习笔记----Lambda表达式 天锦 2014-03-24 16:43:30 发表于:ATA之家       本文主要记录自己学习Java8的历程,方便大家一起探讨和自己的备忘.因为本人 ...

  3. Java8新特性-Lambda表达式是什么?

    目录 前言 匿名内部类 函数式接口 和 Lambda表达式语法 实现函数式接口并使用Lambda表达式: 所以Lambda表达式是什么? 实战应用 总结 前言 Java8新特性-Lambda表达式,好 ...

  4. 乐字节-Java8新特性-Lambda表达式

    上一篇文章我们了解了Java8新特性-接口默认方法,接下来我们聊一聊Java8新特性之Lambda表达式. Lambda表达式(也称为闭包),它允许我们将函数当成参数传递给某个方法,或者把代码本身当作 ...

  5. Java8新特性 - Lambda表达式 - 基本知识

    A lambda expression is an unnamed block of code (or an unnamed function) with a list of formal param ...

  6. JAVA8学习——深入浅出Lambda表达式(学习过程)

    JAVA8学习--深入浅出Lambda表达式(学习过程) lambda表达式: 我们为什么要用lambda表达式 在JAVA中,我们无法将函数作为参数传递给一个方法,也无法声明返回一个函数的方法. 在 ...

  7. Java8学习(3)- Lambda 表达式

    猪脚:以下内容参考<Java 8 in Action> 本次学习内容: Lambda 基本模式 环绕执行模式 函数式接口,类型推断 方法引用 Lambda 复合 上一篇Java8学习(2) ...

  8. Java8一:Lambda表达式教程

    1. 什么是λ表达式 λ表达式本质上是一个匿名方法.让我们来看下面这个例子: public int add(int x, int y) {         return x + y;     } 转成 ...

  9. Java8(1)之Lambda表达式初步与函数式接口

    Lambda表达式初步 介绍 什么是Lambda表达式? 在如 Lisp.Python.Ruby 编程语言中,Lambda 是一个用于表示匿名函数或闭包的运算符 为何需要lambda表达式? 在 Ja ...

随机推荐

  1. Winform去掉标题栏后移动窗体

    第一步:声明全局变量->  private Point _HoverTreePosition; 第二步: #region 隐藏标题栏后移动窗口 private void Form_HoverTr ...

  2. Android静态注册广播无法接收的问题(8.0+版本)

    如果你静态注册的广播无法接收到消息,请先检查下:你的安卓版本是不是8.0+ * 前言** Google官方声明:Beginning with Android 8.0 (API level 26), t ...

  3. [Advanced Python] 10 - Transfer parameters

    动态库调用 一.Python调用 .so From: Python调用Linux下的动态库(.so) (1) 生成.so:.c to .so lolo@-id:workme$ gcc -Wall -g ...

  4. Oracle中RMAN基本命令教程

    一.target--连接数据库 1.本地: [oracle@oracle ~]$ rman target / 2.远程: [oracle@oracle ~]$ rman target sys/orac ...

  5. Splitting into digits CodeForce#1104A

    题目链接:Splitting into digits 题目原文 Vasya has his favourite number 

  6. Android Studio [WebView]

    WebViewActivity.java package com.xdw.a122; import android.graphics.Bitmap; import android.support.v7 ...

  7. Flutter免费(视频)教程汇总

    Flutter学习导航 Flutter简介: Flutter可以轻松快速地构建漂亮的移动应用程序. Flutter是谷歌的移动应用SDK,用于短时间内在iOS和Android上制作高质量的原生界面应用 ...

  8. Centos7 快速安装Docker

    写在前面 Docker是一个开源的引擎,可以轻松的为任何应用创建一个轻量级的.可移植的.自给自足的容器.开发者在笔记本上编译测试通过的容器可以轻松批量地在生产环境中部署. 网上的安装教程也很多这里我推 ...

  9. SPSS学习笔记参数检验—两配对样本t检验

    目的:检验两个有联系的正态总体的均值是否存在显著差异. 适用条件:有联系,正态总体,样本量要一样.一般可以分为一下四种: ①同一受试对象处理前后的对比:如对于糖尿病人,对同一组病人在使用新治疗方法前测 ...

  10. c#学习路线应该靠谱

    因为学c/c++,找不到工作.想转c#,搜索得到的学习路线 C#入门经典 C#数据库入门经典 C#高级编程 ADO.net高级编程 基础的东西搞明白之后,可以学习设计模式,C#设计模式 你是说深入的书 ...