Java8 函数式编程、Lambda、Stream流

1. 概述

1.1 函数式编程简介

我们最常用的面向对象编程(Java)属于命令式编程(Imperative Programming)这种编程范式。常见的编程范式还有逻辑式编程(Logic Programming)函数式编程(Functional Programming)

函数式编程作为一种编程范式,在科学领域,是一种编写计算机程序数据结构和元素的方式,它把计算过程当作是数学函数的求值,而避免更改状态和可变数据。

函数式编程并非近几年的新技术或新思维,距离它诞生已有大概50多年的时间了。他一直不是主流的编程思维,但在众多所谓顶级编程高手的科学工作者间,函数式编程是十分盛行的。

什么是函数式编程?简单的回答:一切都是数学函数。函数式编程语言里也可以有对象,但通常这些对象都是恒定不变的——要么是函数参数,要么是函数返回值。函数式编程语言里没有for/next循环,因为这些逻辑意味着现有状态的改变。相替代的是,这种循环逻辑在函数式编程语言里是通过递归把函数当成参数传递的方式实现的。

举个例子:

a = a + 1;

这段代码在普通成员看来并没有什么问题,但在数学家看来却是不成立的,因为它意味着变量值得改变。

1.2 Lambda 表达式简介

Java 8的最大变化就是引入了Lambda表达式——一种紧凑的、传递行为的方式。

先来看个例子:

button.addActionListener(new ActionListener(){
    public void actionPerformed(ActionEvent event){
        System.out.println("button clicked");
    }
});

这段代码使用了匿名类。ActionListener是一个接口,这里new了一个类实现了ActionListener接口,然后重写了actionPerformed方法。actionPerformed方法接收ActionEvent类型参数,返回空。

这段代码我们其实只关心中间打印的语句,其他都是多余的。所以使用Lambda表达式,我们就可以简写为:

button.addActionListener(event -> System.out.println("button clicked"));

2. Lambda表达式

2.1 Lambda表达式的形式

Java中Lambda表达式一共有5种基本形式,具体如下:

  1. Runnable noArguments = () -> System.out.println("Hello World");
    
  2. ActionListener oneArgument = event -> System.out.println("button clicked");
    
  3. Runnable multiStatement = () -> {
        System.out.print("hello");
        System.out.print(" world");
    }
    
  4. BinaryOperator<Long> add = (x, y) -> x + y;
    
  5. BinaryOperator<Long> addExplicit = (Long x, Long y) -> x + y;
    

2.2 闭包

如果你以前使用过匿名内部类,也许遇到过这样的问题。当你需要在匿名内部类所在方法里的变量,就必须把该变量声明为final。如下例子所示:

final String name = getUserName();
button.addActionListener(new ActionListener(){
    public void actionPerformed(ActionEvent event){
        System.out.println("hi " + name);
    }
});

Java 8放松了这一限制,可以不必再把变量声明为final, 但其实该变量实际上仍然是final的。虽然无需将变量声明为final,但在Lambda表达式中,也无法用作非终态变量。如果坚持用作非终态变量(即改变变量的值),编译器就会报错。

2.3 函数接口

上面例子里提到了ActionListener接口,我们看一下它的代码:

public interface ActionListener extends EventListener{
    /**
     *Invoked when an action occurs
     */
    public void actionPerformed(ActionEvent e);
}

ActionListener 只有一个抽象方法:actionPerformed, 被用来表示行为:接受一个参数,返回空。由于actionPerformed定义在一个接口里,因此abstract关键字不是必需的。该接口也继承自一个不具有任何方法的父接口:EventListener

我们把这种接口就叫做函数接口。

JDK 8 中提供了一组常用的核心函数接口:

接口 参数 返回类型 描述
Predicate<T> T boolean 用于判别一个对象。比如求一个人是否为男性
Consumer<T> T void 用于接收一个对象进行处理但没有返回值,比如接收一个人并打印他的名字
Function<T, R> T R 转换一个对象为不用类型的对象
Supplier<T> None T 提供一个对象
UnaryOperator<T> T T 接收对象并返回同类型的对象
BinaryOperator<T> (T,T) T 接收两个同类型的对象,并返回一个原类型对象

其中ConsumerSupplier对应,一个是消费者,一个是提供者。

Predicate用于判断对象是否符合某个条件,经常被用来过滤对象。

Function是将一个对象转换为另一个对象,比如要装箱或者拆箱某个对象。

UnaryOperator接收和返回同类型对象,一般用于对对象修改属性。BinaryOperator则可以理解为合并对象。

3. 集合处理

3.1 Stream简介

在程序编写的过程中,集合的处理应该是很普遍的。Java 8 对于Collection的处理花了很大的功夫。Java 8 中,引入了流(Stream)的概念,这个流和以前我们使用的IO中的流并不太相同。所有继承自Collection的接口都可以转换为Stream。还是看个例子。

假设我们有一个List包含的一系列的PersonPerson有姓名name和年龄age两个字段,现要求这个列表中年龄大于20的人数。

通常我们可能会这么写:

long count = 0;
for(Person p : persons){
    if(p.getAge() > 20){
        count++;
    }
}

但如果使用Stream的话,则会简单很多:

long count = persons.stream().filter(person -> person.getAge() > 20).count();

这只是stream的很简单的一个用法。现在链式调用方法算是一个主流,这样写也更利于阅读和理解编写者的意图,一步方法做一件事。

3.2 Stream常用操作

Stream的方法分为两类,一类叫惰性求值,一类叫及早求值。

判断一个操作是惰性求值还是及早求值很简单:只需看它的返回值。如果返回值是Stream,那么是惰性求值。其实可以这么理解,如果调用惰性求值方法,Stream只是记录下了这个惰性求值方法的过程,并没有去计算,等到调用及早求值方法后,就连同前面的一系列惰性求职方法顺序进行计算,返回结果。

通用形式为:

Steam.惰性求值.惰性求值. ... .惰性求值.及早求值

整个过程和建造者模式有共通之处。建造者模式使用一系列操作设置属性和配置,最后调用一个build方法,这时,对象才被真正创建。

3.2.1 collect(toList())

collect(toList())方法由Stream里的值生成一个列表,是一个及早求值操作。可以理解为StreamCollection的转换。

注意,这里的toList()其实是Collectors.toList(),因为采用了静态导入,看起来更简洁。

List<String> collected = Stream.of("a", "b", "c").collect(Collectors.toList());
assertEquals(Arrays.asList("a", "b", "c"), collected);

3.2.2 map

如果有一个函数可以将一种类型的值转换为另外一种类型,map操作就可以使用该函数,将一个流中的值转换成一个新的流。

image-20200323221248551.png
List<String> collected = Stream.of("a", "b", "hello").map(string -> string.toUpperCase()).collect(toList());
assertEquals(asList("A", "B", "HELLO"), collected);

map方法就是接收一个Function的匿名函数类进行的转换。

image-20200323222158357.png

3.2.3 filter

遍历数据并检查其中的元素时,可尝试使用Stream中提供的新方法filter

image-20200323224750819.png
List<String> beginningWithNumbers = Stream.of("a", "1abc", "abc1").filter(value -> isDigit(value.charAt(0))).collect(toList());
assertEquals(asList("1abc"), beginningWithNumbers);

filter方法就是接受一个Predicate的匿名函数类,判断对象是否符合条件,符合条件的才保留下来。

image-20200323225043393.png

3.2.4 flatMap

flatMap 方法可用Stream替换值,然后将多个Stream连接成一个Stream

image-20200323225221314.png
List<Integer> together = Stream.of(asList(1,2), asList(3, 4)).flatMap(numbers -> numbers.stream()).collect(toList());
assertEquals(asList(1,2,3,4), together);

flatMap最常用的操作就是合并多个Collection

3.2.5 max 和 min

Stream上常用的操作之一是求最大值和最小值。Stream API 中的maxmin操作足以解决这一问题。

List<Integer> list = Lists.newArrayList(3, 5, 2, 9, 1);
int maxInt = list.stream().max(Integer::compareTo).get();
int minInt = list.stream().min(Integer::compareTo).get();

assertEquals(maxInt, 9);
assertEquals(minInt, 1);

这里有两个要点需要注意:

  1. maxmin方法返回的是一个Optional对象。Optional对象封装的就是实际的值,可能为空,所以保险起见,可以先用isPresent()方法判断一下。Optional的引入就是为了解决方法返回null的问题
  2. Integer::compareTo也是属于Java 8 引入的新特性,叫做方法引用(Method References)

3.2.6 reduce

reduce操作可以实现从一组值中生成一个值。在上述例子中用到的count,minmax方法,因为常用而被纳入标准库中。事实上,这些方法都是reduce操作。

image-20200323230457432.png

上图展示了reduce进行累加的一个过程。具体代码如下:

int result = Stream.of(1, 2, 3, 4).reduce(0, (acc, element) -> acc + element);
assertEquals(10, result);

注意reduce的第一个参数,这是一个初始值。0 + 1 + 2 + 3 + 4 = 10

如果是累乘,则为:

int result = Stream.of(1, 2, 3, 4).reduce(1, (acc, element) -> acc * element);
assertEquals(24, result);

因为任何数乘以1都为自身。1 * 1 * 2 * 3 * 4 = 24

Stream还有很多通用方法,具体可以查阅Java 8 的API文档。

3.3 数据并行化操作

Stream的并行化也是Java 8 的一大亮点。数据并行话是指将数据分成块,为每块数据分配单独的处理单元。这样可以充分利用多喝CPU的优势。

并行化操作流只需改变一个方法调用。如果已经有一个Stream,调用它的parallel()方法就能让其拥有并行操作的能力。如果想从一个集合类创建一个流,调用parallelStream()就能立即获得一个拥有并行能力的流。

int sumSize = Stream.of("Apple", "Banana", "Orange", "Pear").parallel().map(s -> s.length()).reduce(Integer::sum).get();
assertEquals(sumSize, 21);

这里求的是一个字符串列表中各个字符串长度总和。

image-20200323231728428.png

3.4 其他

3.4.1 收集器

Stream转换为List是很常用的操作, 其他Collectors还有很多方法,可以将Stream转换为Set,或者将数据分组转换为Map,并对数据进行处理。也可以指定转换为具体类型,如ArrayListLinkedList或者HashMap。甚至可以自定义Collectors,编写自己的收集器。

3.4.2 元素顺序

一些集合类型中的元素是按顺序排列的,比如List;而另一些则是无序的,比如HashSet。增加了流操作后,顺序问题变得更加复杂。

总之记住,如果集合本身就是无序的,由此生成的流也是无序的。一些中间操纵会产生顺序,比如对值做映射时,映射后的值是有序的,这种顺序就会保留下来。如果进来的流是无序的,出去的流也是无序的。如果需要对流中的数据进行排序,可以调用sorted方法:

List<Integer> list = Lists.newArrayList(3, 5, 1, 10, 8);
List<Integer> sortedList = list.stream().sorted(Integer::compareTo).collect(Collectors.toList());
assertEquals(sortedList, Lists.newArrayList(1, 3, 5, 8, 10));

3.4.3 @FunctionalInterface

我们讨论过函数接口定义的标准,但未提及 @FunctionalInterface 注释。事实上,每个用作函数接口的接口都应该添加这个注释。

但 Java 中有一些接口,虽然只含一个方法,但并不是为了使用 Lambda 表达式来实现的。比如,有些对象内部可能保存着某种状态,使用带有一个方法的接口可能纯属巧合。

该注释会强制 javac 检查一个接口是否符合函数接口的标准。如果该注释添加给一个枚举类型、类或另一个注释,或者接口包含不止一个抽象方法,javac 就会报错。重构代码时,使用它能很容易发现问题。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,185评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,445评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,684评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,564评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,681评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,874评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,025评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,761评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,217评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,545评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,694评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,351评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,988评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,778评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,007评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,427评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,580评论 2 349

推荐阅读更多精彩内容