您的位置:首页 > 编程语言 > Java开发

java函数式编程 (原书 richard warburton 简要提取笔记)

2020-07-20 04:22 246 查看

一、概述

随着CPU频率遇到瓶颈,多核CPU 兴起,旧的Java 还欠缺高效的并行操作。java8强化函数式编程是java发展的必然结果。在java8之前,java通过内部匿名类来实现函数式的操作,但受限于代码模板,操作冗余,可读性差。在

回调函数,事假处理程序上尤为明显。

函数式编程,即函数至上的编程方法,是对行为进行抽象的一种方法,它使用不可变值和函数,函数对一个值进行处理,映射成另一个值。中间不需要用到其它变量,避免了命令式编程对共用数据尤其是对临界区数据加减锁的操作,同时因为命令式中并行操作在临界区实际上也是串行操作。函数式编程将显著提高并行操作的性能和安全性。

 

二、 函数式接口和lambda表达式

2.1函数式接口(Functional Interface)

函数式接口是一个有且仅有一个抽象方法,但是可以有多个非抽象方法的接口。通常用@FunctionalInterface标记。

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

函数式接口可以被隐式转换为 lambda 表达式和方法引用。

如定义了一个函数式接口如下:

@FunctionalInterface

interface GreetingService

{

    void sayMessage(String message);

}

那么就可以使用Lambda表达式来表示该接口的一个实现

GreetingService greetService1 = message -> System.out.println("Hello " + message);

 

JDK 1.8 之前已有的函数式接口:

java.lang.Runnable

java.util.concurrent.Callable

java.security.PrivilegedAction

java.util.Comparator

java.io.FileFilter

java.nio.file.PathMatcher

java.lang.reflect.InvocationHandler

java.beans.PropertyChangeListener

java.awt.event.ActionListener

javax.swing.event.ChangeListener

2.2 Lambda表达式(静态类型的函数接口)

几个lambda表达式的例子

Runnable noArguments = () -> System.out.println("Hello World");

ActionListener oneArgument = event -> System.out.println("button clicked");

Runnable multiStatement = () -> { System.out.print("Hello");

System.out.println(" World"); };

BinaryOperator<Long> add = (x, y) -> x + y;

BinaryOperator<Long> addExplicit = (Long x, Long y) -> x + y;

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

格式:参数->表达式主体

当参数为void 空参数时格式为 ()。

当个参数 (参数1)  括号可以省略。

当为多个参数时格式 (参数1,参数2,参数3)。

当然以上都是建立在编译器可以推断出参数类型的基础上的,也可以显示声明参数类型。

表达式主体可以是单个表达式,也可以是用{}括起来的多行表达式,可以return或throw exception。

一个lambda表达式表示一个函数行为,可以赋值给函数或作为函数参数。

Lambda 表达式作为参数时,其类型由它的目标类型推导得出,

如果只有一个可能的目标类型,由相应函数接口里的参数类型推导得出;

如果有多个可能的目标类型,由最具体的类型推导得出;

如果有多个可能的目标类型且最具体的类型不明确,则需人为指定类型。

注:使用匿名内部类时需要显式 地声明参数类型 ActionEvent event

三、Stream 函数式编程风格的类库

3.1从外部迭代到内部迭代

用原书中的例子:计算从伦敦来的艺术家的人数

可以用for循环或外部迭代即用者将集合生成集合的迭代器,遍历iterator.next。

迭代器从本质上来讲是一种串行化操作。for循环会将行为和方法混为一谈。

对于内部迭代可以写成:

long count = allArtists.stream() .filter(artist -> artist.isFrom("London")) .count();

这里用到了stream接口,过滤和接口都是sream的一种方法,虽然分为过滤和计数,但是艺术家结合只迭代了一次。

filter 只刻画出了 Stream,但没有产生新的集合。像 filter 这样只描述 Stream,最终不产生新集合的方法叫作惰性求值方法。如果操作的返回值是 Stream, 那么操作就是惰性求值。而像 count 这样 最终会从 Stream 产生值的方法叫作及早求值方法。如果一个操作的返回值是另一个值或为空,那么它就是及早求值。

惰性编程是一种将对函数或请求的处理延迟到真正需要结果时进行的通用概念。在本例中,filter()内lambda表达主体中加入输出信息操作,需要等到拥有终止操作的流,count 打印信息才会输出,如果仅仅filter,信息是不会输出的。

3.2常用的流操作:

1. collect(toList()),collect(toSet()),....由 Stream 里的值生成一个列表,集合等是一个及早求值操作。

2. map,使用类型转换函数将一个流中的值转换成一个新的流。

flatmap,可以生成新Stream对象

将字符串转大写

List<String> collected = Stream.of("a", "b", "hello")

.map(string -> string.toUpperCase())

//Lambda 表达式接受一个 String 类型的参数,返回 //一个新的 String。

.collect(toList());

List<Integer> together = Stream.of(asList(1, 2),asList(3, 4))

.flatMap(numbers->numbers.stream())

.collect(toList());

map也可以转换为stream三种基本类型,如.mapToInt 返回IntSream对象,包含summaryStatistics()方法,summaryStatistics对象.getMax(),.getSum,.getAverage得到具体内容。

3. filter 遍历数据并检查其中的元素时

List<String> beginningWithNumbers = Stream.of("a", "1abc", "abc1")

.filter(value -> isDigit(value.charAt(0)))

.collect(toList());

4.max min  主要用到函数式接口Comparator

minxxx=examplelist.stream()

.min(CompComparator.comparing(examplelist元素类型->examplelist 元素类型的可以比较的属性))

.get();

comparing方法接受一个函数并返回另一个函数。

Stream的max方法返回Optional对象。调用get方法可以取出Optional对象中的值。

5. forEach类似增强型for功能,只不过用于stream.forEach,并行流

forEachOrdered  有序

6. reduce 操作可以实现从一组值中生成一个值

int count = Stream.of(1, 2, 3)

.reduce(0, (acc,element) -> acc+element);//count=6

reduce 方法有两种形式,一种如上例需要有一 个初始值,另一种变式则不需要有初始值。没有初始值的情况下,reduce 的第一步使用 Stream 中的前两个元素。有时,reduce 操作不存在有意义的初始值,这样做就是有意义 的,此时,reduce 方法返回一个 Optional 对象。

7. .sorted

...

3.3 一个重构命令式代码到函数式的例子

 

上图代码功能是找出长度大鱼1min的曲目,结果存到String集合中

public Set<String> findLongTracks(List<Album> albums) {

return albums.stream()

.flatMap(album -> album.getTracks())

//getTracks 返回 一个 Stream 对象

//多个 Stream 合并成一个 Stream 并返回

.filter(track -> track.getLength() > 60)

.map(track -> track.getName())

//track流转化为name流

.collect(toSet());

//name 流生成set 返回

}

 

如果函数的参数列表里包含函数接口,或该函数返回一个函数接口,那么该函数就是高阶函数。Stream接口中几乎所有的函数都是高阶函数。comparing 函数,它接受一个函数作为参数,获取相应的值,同时返回一个 Comparator。Comparator 可能会被误认为是一个对象或一个匿名类,但它有且只有一个抽象方法,所以实际上是一个函数接口。

将 Lambda 表达式传给 Stream 上的高阶函数,应该尽量避免副作用(Lambda 表达式应该获取值,给变量赋值,指向控制台等违背函数式设计原理)。唯一的例外是forEach方法,它是一个终结方法。

3.4 并行化

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

如计算专辑曲目长度

public int parallelArraySum() {

return albums

.parallelStream()

// 或者 .stream().parallel()

  .flatMap(Album::getTracks)

 .mapToInt(Track::getLength)

.sum(); }

Java 8 还引入了一些针对数组的并行操作,脱离流框架也可以使用 Lambda 表达式。数组上的并行化操作有:

 

初始化数组:

public static double[] parallelInitialize(int size) {

double[] values = new double[size];

Arrays.parallelSetAll(values, i -> i);

//for(int i = 0; i < values.length;i++) { values[i] = i; }  //命令式操作

return values;

 }

3.5 Optional 数据类型

Optional 数据类型表示 null或某类型数据,由来替换可能为null的数值

使用工厂方法of可以从某个值创建出一个 Optional 对象。Optional 对象相当于值的容器,而该值可以通过get方法提取。

Optional<String>a=Optional.of("a");//创建optional对象,内容为String类型a

assertTrue(a.isPresent());//检查a是否是空

Optional emptyOptional = Optional.empty();//创建空值对象

Optional alsoEmpty = Optional.ofNullable(null);//将一个空值转为空值对象

在调用get方法前需要调用ispresent判断是否空,或者采用orElse或者oreLesget方法。

assertEquals("b", emptyOptional.orElse("b")); //如果为空 b和b判断相等

 

四、集合类和收集器

4.1 方法引用 Classname::methodName

前面例子中用lambda调用参数:

artist -> artist.getName()

用方法引用重写:

Artist::getName

构造函数,用lambda创建一个Artist对象:

(name, nationality) -> new Artist(name, nationality)

方法引用重写:

Artist::new

方法引用创建数组

String[]::new

在一个有序集合中创建一个流时,流中的元素就按出现顺序排列,当集合无序时(如HashSet)hashset.stream()也是无序的。流可以用sorted()排序,unordered方法消除顺序。大多数操作都是在有序流上效率更高,如 filter、map 和 reduce。

4.2收集器( reduce 方法的模拟)

有如下常用收集器:

集合转化:

流上的链式操作很多时候需要最终生成一个集合收集元素

使用toCollection,它接受一个函数作为参数,来创建指定类型集合: stream.collect(toCollection(TreeSet::new));

转换成值:

通常啊利用 maxBy minBy averagInt

如下

publicOptional<Artist>biggestGroup(Stream<Artist>artists){

Function<Artist,Long> getCount = artist -> artist.getMembers().count();

return artists.collect(maxBy(comparing(getCount)));

//找到成员最多的乐队

 }

数据分两半:

partitioningBy,它接受一个流,根据predicate是为true,分成两部分。

artists.collect(partitioningBy(artist -> artist.isSolo()));

//predicate 为 artist是否是独唱。

数组分多组:

groupingBy,它接受一个流,根据predicate的值,分成多部分。predicate类似sql中的having操作。

n albums.collect(groupingBy(album -> album.getMainMusician()));

//将不同主唱的专辑分开

字符串连接Collectors.joining(分隔符,开始符,结束符):

 artists.stream() .map(Artist::getName) .collect(Collectors.joining(", ", "[", "]"));

逗号分隔,方括号起始结束 得到   [艺术家1,艺术家2,艺术家3]

其它:

收集器功能组合可以得到组合收集器,也可以重构和定制收集器。

 

五、小结

初看java8中的函数式比较难于理解,学习函数式编程原理后复看,理解了很多,本文仅仅对java中函数式编程的写法做了总结介绍,其中有体现的函数式中的类型推断,数据结构操作,高阶函数等。文中没有具体讲述原理,对函数式给java带来的设计模式的转变因自己缺乏理解也没有介绍。

 

六、参考资料

1.java8函数式编程 richard warburton (主要参考资料)

2.函数式编程思维

3.CSDN

 

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: