Java8 in action
- 没有共享的可变数据,将方法和函数即代码传递给其他方法的能力就是我们平常所说的函数式编程范式的基石。
- Collection主要是为了存储和访问数据,而Stream则主要用于描述对数据的计算。这里的关键点在于,Stream允许并提倡并行处理一个Stream中的元素。
通过行为参数化传递代码
- 行为参数化就是可以帮助你处理频繁变更的需求的一种软件开发模式。它意味着拿出一个代码块,把它准备好却不去执行它。这个代码块以后作为参数传递给另一个方法,稍后再去执行它。
- 行为参数化,就是一个方法接受多个不同的地为作为参数,并在内部使用它们,完成不同行为的能力。行为参数化可以让代码更好地适应不断变化的要求,减轻未来的工作量。
- 传递代码,就是将新行为作为参数传递给方法。但在Java8之前这实现起来很啰嗦。为接口声明许多只用一次的实体类而造成的啰嗦代码,在Java8之前可以用匿名类来减少。
Lambda表达式
- 可以把Lambda表达式理解为简洁地表示可传递的匿名函数的一种方式:
- 匿名-----它不像普通方法那样有一个明确的名称,写的少而想的多
- 函数-----我们说它是函数,因为Lambda函数不像方法那样属于某个特定的类。但和方法一样,Lambda有参数列表,函数主体,返回类型,还可能有可以抛出的异常列表。
- 传递------Lambda表达式可以作为参数传递给方法或存储在变量中。
- 简洁------无需像匿名类那样写很多模板代码。
- 在哪里使用Lambda表达式,在函数式接口上使用Lambda表达式
- 函数式接口:就是只定义一个抽象方法的接口。Lambda表达式允许你直接以内联的形式为函数式接口的抽象方法提供实现,并把整个表达式作为函数式接口的实例。你用匿名内部类也可以完成同样的事情,只不过比较笨拙:需要提供一个实现,然后再直接内联将它实例化。
- 函数式接口的抽象方法的签名基本上就是Lambda表达式的签名。我们将这种抽象方法叫作函数描述符。
- 把Lambda付诸实践:环绕执行模式
- 第一步:记得行为参数化,传递行为正是Lambda的拿手好戏
- 第二步,使用函数式接口来传递行为
- 第三步,执行一个行为 Lambda表达式允许你直接内联,为函数式接口的抽象方法提供实现,并且将整个表达式作为函数式接口的一个实例。
- 第四步:传递Lambda
- 使用函数式接口。 Java8的设计师在java.util.function包中引入了几个新的函数式接口。
- Predicate java.util.function.Predict<T> 接口定义了一个名为test的抽象方法,它接受泛型T对象,并返回一个boolean。在你需要表示一个涉及类型T的布尔表达式时,就可以使用这个接口。
- Consumer java.util.function.Consumer<T> 定义了一个名叫accept的抽象方法,它接受泛型T的对象,没有返回(void).你如果需要访问类型T对象,并对其执行某些操作,就可以使用这个接口。
- Function java.util.function.Function<T,R>接口定义了一个叫作apply的方法,它接受一个泛型T的对象,并返回一个泛型R的对象。如果你需要定义一个Lambda,将输入对象的信息映射到输出,就可以使用这个接口。
- 原始类型特化 上面的三个泛型函数式接口:Predicate<T>, Consumer<T>和Function<T,R>。还有些函数式接口专为某些类型而设计。 Java类型要么是引用类型(如,Byte,Integer,Object,List),要么是原始类型(比如:int,double,byte,char).但是泛型只能绑定到引用类型。 Java8为我们前面所说的函数式接口带来了一个专门的版本,以便在输入和输出都是原始类型时避免自动装箱的操作。 一般来说,针对专门的输入参数类型的函数式接口的名称都要加上对应的原始类型前缀,如DoublePredicate,IntConsumer,LongBinaryOperator,IntFunction等。Function接口还有针对输出参数类型的变种:ToIntFunction<T>,IntToDoubleFunction等
- 类型检查,类型推断以及限制
- 类型检查 Lambda的类型是从使用Lambda的上下文推断出来的。上下文(比如,接受它传递的方法的参数,或接受它的值的局部变量)中Lambda表达式需要的类型称为目标类型。
- 同样的Lambda,不同的函数式接口。有了目标类型的概念,同一个Lambda表达式就可以与不同的函数式接口联系起来,只要它们的抽象方法签名能够兼容。 -------特殊的void兼容规则 如果一个Lambda的主体是一个语句表达式,它就和一个返回void的函数描述符兼容(当然需要参数列表也兼容)。如,以下两行都是合法的,尽管List的add方法返回一个boolean,而不是Consumer上下文(T->void)所要求的void:
//Predicate返回一个booean
Predicate<String> p = s->list.add(s)
//Consumer返回一个void
Consumer<String> b = s->list.add(s);
- 类型推断 Java编译器会从上下文(目标类型)推断出用什么函数式接口来配合Lambda表达式,这意味着它也可以推断出适合Lambda的签名,因为函数描述符可以通过目标类型来得到。这样做的好处在于,编译器可以了解Lambda表达式的参数类型,这样就可以在Lambda语法中省去标注参数类型。
- 使用局部变量。 Lambda表达式也允许使用自由变量(不是参数,而是在外层作用域中定义的变量),就像匿名类一样。它们被称作捕获Lambda. 关于能做这些变量做什么有一些限制。Lambda可以没有限制地捕获(也就是在其主体中引用)实例变量和静态变量。但局部变量必须声明为final,或事实上是final.换句话说,Lambda表达式只能捕获指派给它们的局部变量一次。(注:捕获实例变量可以被看作捕获最终局部变量this)
- 方法引用 方法引用让你可以重复使用现有的方法定义,并像Lambda一样传递它们。方法引用就是让你根据已有的方法实现来创建Lambda表达式。可以把方法引用 看作针对仅仅涉及单一方法的Lambda语法糖,因为你表达同样的事情时要写的代码更少了。方法引用主要有三类
- 指向静态方法的方法引用
- 指向任意类型实例方法的方法引用(如String的length方法)
- 指向现有对象的实例方法的引用
- 构造函数引用 对于一个现有的构造函数,你可以利用它的名称和关键字new来创建它的一个引用:ClassName:new。 它的功能与指向静态方法的引用类似。
- 在需要函数式接口的地方可以使用Lambda表达式。函数式接口就是仅仅定义一个抽象方法的接口。抽象方法的签名(称为函数描述符)描述了Lambda表达式的签名。
- 复合Lamda表达式的有用方法。 Java8的好几个函数式接口都有为方便而设计的方法。具体而言,许多函数式接口,比如用于传递Lambda表达式的Comparator,Function和Predicate都提供了允许你进行复合的方法。 在实践中,这意味着你可以把简单的Lambda复合成复杂的表达式。如,你可以让两个谓词之间做一个or操作,组合成一个更大的谓词。而且,你可以让一个函数的结果成为另一个函数的输入。你可能会想,函数式接口中怎么可能有更多的方法呢?窍门在于,我们即将介绍的方法都是默认方法,也就是说它们不是抽象方法。
- 谓词复合 谓词接口包括三个方法:negate,and和or,让重用已有的Predicate来创建更复杂的谓词。如,你可使用negate方法返回一个Predicate的非。
- 函数复合 你还可以把Function接口所代表的Lambda表达式复合起来。Funcation接口为此配了andThen和compose两个默认方法,它们都返回Function的一个实例。andThen方法会返回一个函数,它先对输入应用一个给定函数,再对输出应用另一个函数。你也可以类似地使用compose方法,先把给定的函数用作compose的参数里面给的那个函数,然后再把函数本身用于结果。
- 判断一个操作是惰性求值还是及早求值很简单:只需看它的返回值。如果返回值是Stream,那么是惰性求值,如果返回值是另一个值或为空,那么就是及早求值。使用这些操作的理想方式就是形成一个惰性求值的链,最后用一个及早求值的操作返回想要的结果。
小结:
- Lambda表达式可以理解为一种匿名函数,它没有名称,但有参数列表,函数主体,返回类型,可能还有一个可以抛出的异常列表
- 只有在接受函数式接口的地方才可以使用Lambda表达式
- Lambda表达式允许你直接内联,为函数式接口的抽象方法提供实现,并且将整个表达式作为函数式接口的一个实例。
- 为了避免装箱操作,对Predicate<T>和Function<T,R>等通用函数式接口的原始类型特化:IntPredicate,IntToLongFunction等。
- 环绕执行模式(即在方法所必须的代码中间,你需要执行点什么操作,如资源分配 和清理)可以配合Lambda提高灵活性和可重用性。
- Lambda表达式所需要代表的类型称为目标类型
函数式数据处理
引入流
- 流是什么 流是Java API的新成员,它允许你以声明方式处理数据集合
- Java8中的Stream API可以让你写出这样的代码:声明性--更简洁,更易读; 可复合--更灵活; 可并行--性能更好
- 流简介 流到底是什么呢?简短的定义就是“从支持数据处理操作的源生成的元素序列”。让我们一步步剖析这个定义。
- 元素序列 像集合一样,流也提供了一个接口,可以访问特定元素类型的一组有序值。因为集合是数据结构,主要目的是以特定的时间/空间复杂度存储和访问元素(如ArrayList).但流的目的在于表达计算,如filter,sorted和map.集合讲的是数据,流讲的是计算
- 源: 流会使用一个提供数据的源,如集合,数组或输入/输出资源
- 数据处理操作 流的数据处理功能支持类似于数据库的操作,以及函数式编程语言中的常用操作,如filter,map,reduce,find,match,sort等。流操作可以顺序执行,也可并行执行。
- 流与集合
- 粗糙地说,集合与流之间的差异就在于什么时候进行计算。集合是一个内存中的数据结构,它包含数据结构中目前所有的值---集合中的每个元素都得先算出来才能添加到集合中。相比之下,流则是在概念上固定的数据结构(你不能添加或删除元素),其元素则是按需计算的。
- 只能遍历一次 和迭代器一样,流只能遍历一次。遍历完之后,我们就说这个流已经被消费掉了。
- 外部迭代与内部迭代 Streams库使用内部迭代---它帮你把迭代做了,还把得到的流值存在了某个地方
- 流操作 java.util.stream.Stream中的Stream接口定义了许多操作。它们可以分为两大类。
- 中间操作 如filter或sorted等中间操作会返回另一个流。这让多个操作可以连接起来形成一个查询。
- 终端操作 终端操作会从流的流水线生成结果。其结果是任何不是流的值,如List,Integer,甚至void
- 使用流 一般三件事:1--一个数据源来执行一个查询。 2--一个中间操作链,形成一条流的流水线 3--一个终端操作,执行流水线,并能生成结果。
使用流
- 筛选和切片
- 用谓词筛选 Stream接口支持filter方法。该操作会接受一个谓词(一个返回boolean的函数)作为参数,并返回一个包括所有符合谓词的元素的流。
- 筛选各异的元素 流还支持一个叫作distinct的方法,它会返回一个元素各异(根据元素的上hashcode和equels方法)的流。
- 截断流 流支持limit(n)方法,会返回一个不超过给定长度的流。
- 跳过元素 流还支持skip(n)方法,返回一个扔掉了前n个元素的流。如果流中元素不足n个,则返回一个空流。
- 映射 一个非常常见的数据处理套路就是从某些对象中选择信息。比如在SQL里,你可以从表里选择一列。Stream API也通过map和flatMap方法提供了类似的工具。
- 对流中每一个元素应用函数 流支持map方法,它会接受一个函数作为参数。这个函数会被应用到每个元素上,并将映射成一个新的元素
- 流的扁平化 flatmap方法让你把一个流中的每个值都换成另一个流,然后把所有的流连接起来成为一个流。
- 查找和匹配 Stream API通过allMatch,anyMatch,noneMatch,findFirst和findAny方法提供了这样的工具。
检查谓词是否至少匹配一个元素 anyMatch方法
检查谓词是否匹配所有元素 allMatch
noneMatch 流中没有任何元素与给定的谓词匹配
查找元素 findAny方法将返回当前流中的任意元素。
-
Optional<T>类是一个容器类,代表一个值存在或不存在。Java8的库设计人员引入了Optional<T>,这样就不用返回众所周知容易出问题的null了。看它的几个方法:
--ifPresent() 将在Optional包含值的时候返回ture,否则false.
--ifPresent(Consumer<T> block) 会在值存在的时候执行给定的代码块。
--T get() 会在值存在时返回值,否则抛出一个NoSuchElement异常
--T orElse(T other)会在值存在时返回值,否则返回一个默认值。 查找第一个元素 findFirst
何时使用findFirst和findAny ,为什么会同时有findFirst和findAny?答案是并行。找到第一个元素在并行上限制更多。如果你不关心返回的元素是哪个,请使用findAny()
- 归约 如何把一个流中的元素组合起来,使用reduce操作来表达更复杂的查询,如“计算菜单中的总卡路里”或“菜单中卡路里最高的菜是哪一个”。此类查询需要将流中所有元素反复结合起来,得到一个值,比如一个Integer.这样的查询可以被归类为归约操作(将流归约成一个值)。用函数式编程语言的术语来说,这称为折叠(fold)
- 数值流
- 原始类型流特化。 Java8引入了三个原始类型特化流接口来解决一般装箱拆箱问题:intStream,DoubleStream和LongStream,分别将流中的元素特化为int,long和double,从而避免了暗含的装箱成本。每个接口都带来了进行常用数值归约的新方法,如sum,max
---1. 映射到数值流,将流转化为特化版本的常用方法是mapToInt,mapToDouble和mapToLong。这些方法和前面说的map方法的工作方式一样,只是它们返回的是一个特化流,而不是Stream<T>
---2. 转换回对象流 有了数值流,你可能会想把它转换回非特化流。可以用boxed方法
---3. 默认值OptionlInt。 Optional可以用Integer,String等参考类型来参数化。对于三种原始流特化,也分别有一个Optional原始类型特化版本:OptionalInt,OptionalDouble和OptionalLong.
- 数值范围 和数字打交道时,有一个常用的东西就是数值范围,如,假设你想要生成1和100之间的所有数字。Java8引入了两个可以用于IntStream和LongStream的静态方法,帮助生成这种范围:rang和的rangeClosed.这两个方法都是第一参数接受起始值,第二个参数接受结束值。但range是不包含结束值的,而rangeClosed则包含结束值。
- 构建流
-
由值创建流
可以使用静态方法Stream.of 通过显式值创建一个流。它可以接受任意数量的参数。
-
由数组创建流
你可以使用静态方法Arrays.stream从数组创建一个流。它接受一个数组作为参数。
-
由文件生成流
java中用于处理文件等I/O操作的NIO API已更新,以便利用Stream API. java.nio.file.Files中很多静态方法都会返回一个流。 如Files.lines,它会返回一上由指定文件中的各行构成的字符串流。
-
由函数生成的流:创建无限流
Stream API提供了两个静态方法来从函数生成流:Stream.iterate和Stream.generate. 这两个操作可以创建所谓的无限流:不像从固定集合创建的流那样有固定大小的流。由iterate和generate产生的流会用给定的函数按需创建值,因此可以无穷地计算下去!一般来说,应该使用limit(n)来对这种流加以限制。
---迭代 如:Stream.iterate(0,n->n+2)。 生成偶数流。一般来说,在需要依次生成一系列值的时候应该使用iterate.
---生成 与iterate方法类似,generate方法也可让你按需生成一个无限流,但generate不是依次每个新生成的值应用函数的。它接受一个Supplier<T>类型的Lambda提供新的值。如 Stream.generate(Math::random)
用流收集数据
- 收集器简介
- 收集器用作高级归约
收集器非常有用,用它可以简洁而灵活地定义collect用来生成的集合的标准。一般来说,Collector会元素应用一个转换函数(很多时候是不体现任何效果的恒等转换,如toList),并将结果累积到一个数据结构中,从而产生这一过程的最终输出。 - 预定义收集器
预定义收集器也就是那些从Collectors类提供的工厂方法(如groupBy)创建的收集器。它们主要提供三大功能:--将流元素归约和汇总为一个值 --元素分组 --元素分区
- 归约和汇总
查找流中最大值和最小值 Collectors.maxBy和Colectors.minBy,来计算流中的最大和最小值。这两个收集器接收一个Comparator参数来比较流中的元素。
汇总 Collecgors类专门为汇总提供了一个工厂方法:Collectors.summingInt。它可接受一个把对象映射为求和所需要int的函数,并返回一个收集器。该收集器传递给普通的collect方法后即可执行我们需要的汇总操作。 Collectors.summingLong和Collectors.summingDouble方法的作用完全一样,可用于求和字段为long和double的情况。 但汇总不仅仅是求和:还有Collectors.averagingInt,连同对应的averagingLong和averaingDouble可以计算数值的平均数。 目前为止,你已经看到了如何使用收集器来给流中的元素计数,找到这些元素数值属性的最大值和最小值,以及计算其总和和平均值。不过很多时候,你可能想要得到两个或更多这样的的结果,而且你希望只需要一次操作就可以完成。在这种情况下,可以使用summarizingInt工厂方法返回的收集器。如:IntSummaryStatistics menuStatistics = menu.stream().collect(summarizingIng(Dish::getCalories));
这个收集器会把所有这些信息收集到一个叫作IntSummaryStatistics类里,它提供了方便的取值(getter)方法来访问结果。连接字符串 joining工厂方法返回的收集器会把流中每一个对象应用toString方法得到所有字符串连接成一个字符串。 joining内部使用了StringBuilder来把生成的字符串逐个追加起来。但该字符串的可读性并不好,joining工厂方法有一个重载版本可以接受元素分界符joining(",").
到目前为止,我们已经探讨了各种将流归约互一个值的收集器。下一节,我们会展示为什么所有这种形式的归约过程,其实都是Collectors.reducing工厂方法提供的更广义归约收集器的特殊情况。广义的归约汇总
事实上,我们已经讨论的所有收集器,都是一个可以用reducing工厂方法定义的归纳过程的特殊情况而已。Collectors.reducing工厂方法是所有这些特殊情况的一般化。 它需要三个参数
第一个参数是归约操作的起始值,也中流中没有元素时的返回值,所以很显然对于数值和而言0是一个合适的值
第二个参数就是一个转化函数,如将菜肴转化成一个表示其所含热量的int
第三个参数是一个BinaryOperator,将两个项目累加成一个同类型的值。如两个int的和
同样,你可以使用下面的单参数形式的reducing来找到热量最高的菜。如下所示:
Optional<Dish> mostCalorieDish =menu.stream().collect(reducing((d1,d2)->d1.getCalories()>d2.getCalories()?d1:d2));
可以把单参数reducing工厂方法创建的收集器看作三参数方法的特殊情况,它把流中的第一个项目作为起点,把恒等函数(即一个函数仅仅是返回其输入参数)作为一个转换函数。这也意味着,要是把单参数reducing收集器传递给空流的collect方法,收集器就没有起点。它将因此返回一个Optional<Dish>对象
从逻辑上说,归约操作的工作原理:利用累积函数,把一个初始化为起始值的累加器,和转换函数应用到流中每个元素上得到的结果不断迭代合并起来。
- 分组
一个常见的数据为操作是根据一个或多个属性对集合中的项目进行分组。用Collectors.groupingBy工厂方法返回的收集器就可以轻松地完成这项任务。groupBy接收一个分类函数,用它来把流中的元素分成不同的组。把分组函数返回的值作为映射的键,把流中所有具有这个分类值的项目的列表作为对应的映射值。值就是包含所有对应类型的列表。
- 多级分组
要实现多级分组,我们可以使用一个由双参数版本的Collectors.groupingBy工厂方法创建的收集器,它除了普通的分类函数之外,还可以接受collector类型的第二个参数,那么要时行二级分组的话,我们可以把内层groupBy传递给外层groupingBy,并定义一个为流中项目分类的二级标准。二级分组的结果是两级map.
- 按子组收集数据
上面的小节,可以把第二个groupingBy收集器传递给外层收集器来实现多级分组。但进一步说,传递给第一个groupingBy的第二个收集器可以是任何类型,而不一定是另一个groupingBy. 如,要数一数菜单中每类菜有多少个,可以传递counting收集作为groupingBy收集器的第二个参数。
Map<Dish.Type,Long> typesCount = menu.stream().collect(groupingBy(Dish::getType,counting()));
注意普通的单参数groupingBy(f)(其中f是分类函数)实际上是groupingBy(f,toList())的简便写法。
---1. 把收集器的结果转换为另一种类型。 因为分组操作的Map结果中的每个值上包装的Optional没什么用,所 你可能想把它们去掉。要做到这一点,或者更一般地来说,把收集器返回的结果转换为另一种类型,你可以使用Collectors.collectingAndThen工厂方法返回的收集器。
Map<Dish.Type, Dish> mostCaloricByType =
menu.stream()
.collect(groupingBy(Dish::getType,
collectingAndThen(
maxBy(comparingInt(Dish::getCalories)),
Optional::get)));
这个工厂方法接受两个参数---要转换的收集器以及转换函数,并返回另一个收集器。这个收集器相当于旧收集器的一个包装,collect操作的最后一步就是将返回值用转换函数做一个映射。在这里,被包起来的收集器就是用maxBy建立的那个,而转换函数Optional::get则把返回的Optional中的值提取出来。这个操作放在这里是安全的,因为reducing收集器永远不会返回Optional.empty().
把收集器嵌套起来很常见,它们从外层逐层向里有以下几点:
1. groupingBy是最外层,根据菜肴的类型把菜单流分组,得到 三个子流
2. groupingBy收集器包裹着collectingAndThen收集器,因此分组操作得到的每个子流都用这第二个收集器做进一步归约。
3. collectingAndThen收集器又包裹着第三个收集器maxBy
4. 随后由归约收集器进行子流的归约操作,然后包含它的collectingAndThen收集器会对其结果应用Optional:get转换函数
5. 对三个子流分别执行这一过程并转换而得到的三个值,也就是各个类型中热量最高的Dish,将成为groupingBy收集器返回的Map中与各个分类键(Dish的类型)相关联的值。
---2. 与groupingBy联合使用的其他收集器的例子
通过groupingBy工厂方法的第二个参数传递的收集器将会对分到同一组中的所有流元素执行进一步归约操作。如:你还重用求出所有菜肴热量总和的收集器,不过这次是对每一组Dish求和:
Map<Dish.Type,Integer> totalCaloriesByType = menu.stream().collect(groupingBy(Dish::getType,summingInt(Dish::getCalories)));
然而常常和groupingBy联合使用的另一个收集器是mapping方法生成的。这个方法接受两个参数:一个函数对流中的元素做变换,另一个则将变换的结果对象收集起来。其目的是在累加之前对每个输入元素应用一个映射函数,这样就要可以让接受特定类型元素的收集器适应不同类型的对象。 例子:比方说你想要知道,对于每种类型的Dish,菜单中都有哪些CaloricLevel.我们可以把groupingBy和mapping收集器结合起来,如下:
Map<Dish.Type,Set<CaloricLevel>> caloricLevelsByType =
menu.stream().collect(
goupingBy(Dish::getType,mapping( dish->{if (dish.getCalories<=400) return CaloricLevel.DIET; else if(dish.getCalories()<=700) return CaloricLevel.NORMAL; else return CaloricLevel.FAT;},
toSet())
)
);
- 分区
分区是分组的特殊情况:由一个谓词(返回一个布尔值的函数)作为分类函数,它称分区函数。分区函数返回一个布尔值,这意味着得到的分组Map的键类型是Boolean
- 分区的优势
分区的好处在于保留了分区函数返回true或false的两套流元素列表。partitioningBy工厂方法还有一个重载版本,可以传递第第二个收集器
- 收集器接口
Collector接口包含了一系列方法,为实现具体的归约操作(即收集器)提供了范本。
public interface Collector<T,A,R>{
Supplier<A> suppler();
BiConsumer<A,T> accumulator();
Function<A,R> finisher();
Set<Characteristics> characteristics();
}
本列表适用以下定义。
T是流中要收集的项目的泛型。
A是累加器的类型,累加器是在收集过程中用于累加部分结果的对象。
R是收集操作得到的对象(通常但并不一定是集合)的类型。
- 理解Collector接口声明的方法
-
建立新的结果容器:supplier方法
supplier方法必须返回一个结果为空的Supplier,也就是一个无参数函数,在调用时它会创建一个空的累加器实例,供数据收集过程使用。
-
将元素添加到结果容器:accumulator方法
accumulator方法会返回执行归约操作的函数。当遍历到流中第n个元素时,这个函数执行时会有两个参数:保存归约结果的累加器(已收集了流中前n-1个项目),还有第n个元素本身。该函数将返回void,因为累加器是原位更新,即函数的执行改变了它的内部状态以体现遍历的元素的效果。
-
对结果容器应用最终转换:finisher方法
在遍历完流后,finisher方法必须返回在累加过程的最后要调用的一个函数,以便将累加器对象转换为整个集合操作的最终结果。
这三个方法已经足以对流进行顺序归约。实践中的实现细节可能还要复杂一点,一方面是因为流的延迟性质,可能在collect操作之前还需要完成其他中间操作的流水线,另一方面则是理论上可能要进行并行归约。
-
合并两个结果容器:combiner方法
四个方法中的最后一个-----combiner方法会返回一个供归约操作使用的函数,它定义了对流的各个子部分进行并行处理时,各个子部分归约所得的累加器要如何合并。
有了这个第四个方法,就可以对流进行并行归约了。它会用到Java7中引入的分支/合并框架和Spliterator抽象。
-
characteristics方法
characteristics会返回一个不可变的Characteristics集合,它定义了收集器的行为----特别是关于流是否可以并行归约,以及可以使用哪些优化的提示。Characteristics是一个包含三个项目的枚举:
---UNORDRED--归约结果不受流中项目的遍历和累积顺序的影响
---CONCURRENT--accumulator函数可以从多个线程同时调用,且该收集器可以并行归约流。如果收集器没有标为UNORDERED,那它仅在用于无序数据源时才可以并行归约。
---IDENTITY_FINISH--这表明完成器方法返回的函数是一个恒等函数,可以跳过。这种情况下,累加器对象将会直接用作归约过程的最终结果。这也意味着,将累加器A不加检查地转换为结果R是安全的。
并行数据处理与性能
-
并行流
Stream接口可以通过收集源调用parallelStream方法来把集合转换为并行流。并行流就是把一个内容分成多个数据块,并用不同的线程分别处理每个数据块的流。
- 并行流内部使用了默认的ForkJoinPool,它默认的线程数量就是你的处理器数量,这个值是由Runtime.getRunTime().availableProcessors()得到的。但是你可以通过系统属性 java.util.concurrent.ForkJoinPool.common.parallelism来改变线程池大小。如下:
System.setProperty("java.util.concurrent.ForkJoinPool.common.parallelism","12");
使用正确的数据结构然后使其并行化工作能保证最佳的性能。特别注意原始类型的装箱和拆箱操作。
-
高效使用并行流
一些帮你决定某个特定情况下是否有必要使用并行流的建议:
- 有疑问,测量。把顺序流转化成并行流轻而易举,但却不一定是好事。并行流并不总是比顺序流快。
- 留意装箱。自动装箱和拆箱操作会大大降低性能。Java8中有原始类型流(IntStream,LongStream,DoubleStream)来避免这种操作,但凡有可能应该使用这些流。
- 有些操作本身在并行流上的性能就比顺序流差。特别是limit和findFirst等依赖于元素顺序的操作。
- 还要考虑流的操作流水线的总计算成本。
- 对于较小的数据量,选择并行几乎从来都不是一个好的决定。并行处理少数几个元素的好处还抵不上并行化造成的额外开销。
- 要考虑流背后的数据结构是否易于分解。如ArrayList的拆分效率比LinkList高得多,因为前者用不着遍历就可以平均拆分,而后者则必须遍历。另外,用range工厂方法创建的原始类型流也可以快速分解。
- 分支合并框架
分支合并框架的目的是以递归方式将可以并行的任务拆分成更小的任务,然后将每个子任务的结果合并起来生成整体结果。这是ExecutorService接口的一个实现,它把子任务分配给线程池(称为ForkJoinPool)中的工作线程。
-
使用RecurisveTask
要把任务提交到这个池,必须创建RecursiveTask<R>的一个子类,其中R是并行化任务(以及所有子任务)产生的结果类型,或者如果任务不返回结果,则是RecursiveAction类型(当然它可能会更新其他非局部机构)。要定义RecursiveTask,只需要实现它唯一的抽象方法compute;
protected abstract R compute();
这个方法同时定义了将任务拆分成子任务的逻辑,以及无法再拆分或不方便再拆分时,生成单个子任务结果的逻辑。伪代码:if(任务足够小或不可分){ 顺序计算该任务 }else{ 将任务分成两个子任务 递归调用本方法,拆分每个子任务,等待所有子任务完成 合并每个子任务的结果 }
选个例子为基础,让我们试着用这人框架为一个数字范围(这里用一个long[]数组表示)求和。你需要先为RecursiveTask类做一个实现:
package com.tim.test;
public class ForkJoinSumCalculator
extends java.util.concurrent.RecursiveTask<Long> {
private final long[] numbers;
private final int start;
private final int end;
public static final long THRESHOLD = 10_000;
public ForkJoinSumCalculator(long[] numbers) {
this(numbers, 0, numbers.length);
}
private ForkJoinSumCalculator(long[] numbers, int start, int end) {
this.numbers = numbers;
this.start = start;
this.end = end;
}
@Override
protected Long compute() {
int length = end - start;
if (length <= THRESHOLD) {
return computeSequentially();
}
ForkJoinSumCalculator leftTask =
new ForkJoinSumCalculator(numbers, start, start + length / 2);
leftTask.fork();
ForkJoinSumCalculator rightTask =
new ForkJoinSumCalculator(numbers, start + length / 2, end);
Long rightResult = rightTask.compute();
Long leftResult = leftTask.join();
return leftResult + rightResult;
}
private long computeSequentially() {
long sum = 0;
for (int i = start; i < end; i++) {
{
sum += numbers[i];
}
return sum;
}
}
测试方法:
public static long forkJoinSum(long n) {
long[] numbers = LongStream.rangeClosed(1, n).toArray();
ForkJoinTask<Long> task = new ForkJoinSumCalculator(numbers);
return new ForkJoinPool().invoke(task);
}
运行ForkJoinSumCalculator 当把ForkJoinSumCalculator任务传给ForkJoinPool时,这个任务就由池中的一个线程执行,这个线程会调用任务的compute方法。该方法会检查任务是否小到足以顺序执行,如果不够小则会把要求和的数组分成两半,分给两个新的ForkJoinSumCalculator,而它们也由ForkJoinPool安排执行。因此,这一过程可以递归重复,把原任务分为更小的任务,直到满足不方便或不可能再进一步拆分的条件。这时会顺序计算每个任务的结果,然后由分支过程创建的(隐含的)任务二叉树遍历回到它的根。接下来会合并每个子任务的部分结果,从而得到总任务的结果。
-
使用分支合并框架的最佳做法
虽然分支/合并框架还算简单易用,但它容易被误用。以下是几个有效使用它的最佳做法:
- 对一个任务调用join方法会阻塞调用方,直到该任务做出结果。因此,有必要在两个子任务的计算开始之后再调用它。否则,你得到的版本会比原始的顺序算法更慢更复杂,因为每个子任务都必须等待另一个子任务完成才能启动。
- 不应该在RecursieTask内部使用ForkJoinPool的invoke方法。相反,应该始终直接调用compute或fork方法,只有顺序代码才应该用invoke来启动并行计算。
- 对子任务调用fork方法可以把它排进ForkJoinPool。同时对左边和右边的子任务调用它似乎很自然,但这样做的效率要比直接对其中一个调用compute低。这样做你可以为其中一个子任务重用同一线程,从而避免在线程池中多分配一个任务造成的开销。
- 调试使用分支/合并框架的并行计算可能有点棘手。特别是你平常都在喜欢的IDE里看栈跟踪来找问题,但放在分支、合并计算上就不行了,因为调用compute的线程并不是概念上的调用方,后者是调用fork的那个。
- 和并行流一样,你不应理所当然地认为在多核处理器上使用分支合并计算比顺序计算快。一个任务可以分解成多个独立的子任务,才能让性能在并行化时有所提升。所有这些子任务的运行时间都应该比分出新任务所花的时间长;一个惯用方法是把输入/输出放在一个子任务里,计算放在另一个里,这样计算就可以和输入/输出同时进行。此外,在比较同一算法的顺序和并行版本的性能时还有别的因素要考虑。就像任何其他Java代码一样,分支/合并框架需要“预热”或者说要执行几遍才会被JIT编译器优化。这就是为什么在测量性能之前跑几遍程序很重要,我们的测试框架就是这么
做的。同时还要知道,编译器内置的优化可能会为顺序版本带来一些优势(例如执行死码分析——删去从未被使用的计算)。
-
工作窃取
分支/合并框架工程用一种称为工作窃取的技术解决这个问题。在实际应用中,这意味着这些任务差不多被平均分配到ForkJoinPool中的所有线程上。每个线程都为分配给它的任务保存一个双向链式队列,每完成一个任务,就会从队列头上取出下一个任务开始执行。基于一些原因,某个线程可能早早完成了分配给它的任务,也就是它的队列已经空了,而其它的线程还很忙。这时,这个线程并没有闲下来,而是随机选了一个别的线程从队列的尾巴上‘偷走’一个任务。这个过程一直继续下去,直到所有的任务都执行完毕,所有的队列都清空。这就是为什么要划成许多小任务而不是少数几个大任务,这有助于更好地工作线程之间平衡负载。
-
Spliterator
Spliterator是Java 8中加入的另一个新接口;这个名字代表“可分迭代器”(splitable
iterator)。和Iterator一样,Spliterator也用于遍历数据源中的元素,但它是为了并行执行而设计的。虽然在实践中可能用不着自己开发Spliterator,但了解一下它的实现方式会让你对并行流的工作原理有更深入的了解。Java8已经为集合框架中包含的所有数据结构提供了一个默认的Spliterator实现。集合实现了Spliterator接口,接口提供了一个spliterator方法。这个接口定义了若干方法,如下面的代码清单所示。
public interface Spliterator<T> {
boolean tryAdvance(Consumer<? super T> action);
Spliterator<T> trySplit();
long estimateSize();
int characteristics();
}
与往常一样,T是Spliterator遍历的元素的类型。tryAdvance方法的行为类似于普通的因为它会按顺序一个一个使用Spliterator中的元素,并且如果还有其他元素要遍历就返回true。但trySplit是专为Spliterator接口设计的,因为它可以把一些元素划出去分给第二个Spliterator(由该方法返回),让它们两个并行处理。Spliterator还可通过estimateSize方法估计还剩下多少元素要遍历,因为即使不那么确切,能快速算出来是一个值也有助于让拆分均匀一点。
高效Java8编程
重构,测试和调试
- 为改善可读性和灵活性重构代码
- 改善代码的可读性
利用Lambda表达式,方法引用以及Stream改善代码的可读性:
--重构代码,用Lambda表达式取代匿名类
--用方法引用重构Lambda表达式
--用Stream API重构命令式的数据处理。
默认方法
Java8允许在接口内声明静态方法。Java8引入了一个新功能,叫默认方法,通过默认方法可以指定接口方法的默认实现。换句话说,接口能提供方法的具体实现。因此,实现接口的类如果不显式地提供该方法的具体实现,就会自动继承默认的实现。这种机制可以使你平滑地进行接口的优化和演进。
解决默认方法冲突的三条原则:
- 类中的方法优先级最高。类或父类中声明的方法的优先级高于任何声明为默认方法的优先级
- 如果无法依据第一条进行判断,那么子接口的优先级更高:函数签名相同时,优先选择拥有最具体实现的默认方法的接口,即如果B继承了A,那么B就比A更具体。
- 最后,如果还是无法判断,继承了多个接口的类必须通过显式覆盖和调用期望的方法,显式地选择使用哪一个默认方法的实现 。Java8 引入了一种新的语法X.super.m(...),其中x是你希望调用的m方法的父接口
用Optional取代null
- 创建Optional对象
- 声明一个空的Optional
可以通过静态工厂方法Optional.empty,创建一个空的Optional对象:Optional<Car> optCar = Optional.empty()
- 依据一个非空值创建Optional
使得静态工厂方法Optional.of,依据一个非空值创建一个Optional对象:
Optional<Car> optcar=Optional.of(car)
- 可接受null的Optional
使用静态工厂方法Optional.ofNullable,你可以创建一个允许null值的Optional
对象:
Optional<Car> optCar = Optional.ofNullable(car);
如果car是null,那么得到的Optional对象就是个空对象。
completablerFuture组合式异步编程
-
future接口
它建模了一种异步计算,返回一个执行运算结果的引用,当运算结束后,这个引用被返回给调用方。要使用Future,通常你只需要将耗时的操作封装在一个Callable对象中,再将它提交给ExecutorService,就万事大吉了
使用CompletableFuture构建异步应用。
使用supplyAsync创建CompletableFuture对象。
Java 8的 CompletableFuture API提供了名为thenCompose的方法,它就是专门为这一目的而设计的,thenCompose方法允许你对两个异步操作进行流水线,第一个操作完成时,将其结果作为参数传递给第二个操作。换句话说,你可以创建两个CompletableFutures对象,对第一个CompletableFuture对象调用thenCompose,并向其传递一个函数。当第一个CompletableFuture执行完毕后,它的结果将作为该函数的参数,这个函数的返回值是以第一个CompletableFuture的返回做输入计算出的第二个CompletableFuture对象。
CompletableFuture利用Lambda表达式以声明式的API提供了一种机制,能够用最有效的方式,
非常容易地将多个以同步或异步方式执行复杂操作的任务结合到一起
一等函数是可以作为参数传递,可以作为结果返回,同时还能存储在数据结构中的函数。
高阶函数接受至少一个或者多个函数作为输入参数,或者返回另一个函数的函数。Java
中典型的高阶函数包括comparing、andThen和compose。
科里化是一种帮助你模块化函数和重用代码的技术。