流允许以声明性方式处理数据集合,并且流可以透明的并行处理。
List<String> lowCaloricDishsName = menu.stream().filter(d -> d.getCalories() < 400).sorted(comparing(Dish :: getCalories)).map(Dish :: getName).collect(toList()); //串行
List<String> lowCaloricDishesName = menu.paralleStream().filter(d -> d.getCalories() < 400).sorted(comparing(Dish :: getCalories)).map(Dish::getName).collect(toList()); //并行
代码是以声明性方式写的,并且可以把几个基本操作链接起来来表达复杂的数据处理流水线,同时保持代码清晰可读。filter,sorted,map和collect等操作是与具体线程模型无关的高层次构件,所以它们的内部实现可以是单线程的,可以透明地充分利用你的多核架构。
流是从支持数据处理操作的原生成的元素序列。元素序列是指流提供了一个接口可以访问特定元素类型的一组有序值。集合是数据结构,它的目的主要是以特定的时间/空间复杂度存储和访问元素。但流的目的在于表达计算。流会使用一个提供数据的源,如集合,数组或输入/输出资源。从有序集合生成流时会保存原有的顺序,由列表生成的流,其元素顺序与列表一致。流的数据处理功能支持类似于数据库的操作,以及函数式编程语言中的常用操作,如filter,map,reduce,find,match,sort等。流操作可以顺序执行也可以并行执行。流操作由两个特点:流水线和内部迭代。很多流操作本身会返回一个流,这样多个操作可以链接起来,形成一个大的流水线。流水线的操作可以看作对数据源进行数据库式查询。
流和集合之间的差异在于什么时候进行计算。集合是一个内存中的数据结构,它包含数据结构中目前所有的值,集合中的每个元素都是先计算出来后才能添加到集合中。流则是概念上固定的数据结构,其元素则是按需计算。集合使用外部迭代(for-each),流使用内部迭代。内部迭代可以自动选择一种适合硬件的数据表示和并行实现。
流和迭代器一样只能遍历一次,遍历完之后,这个流就被消费掉了。
简单的流操作
可以连接起来的流操作称为中间操作,关闭流的操作称为终端操作。filter,map和sort可以连成一条流水线,它们是中间操作。collect出发流水线的执行并
关闭它,是终端操作。除非流水线傻姑娘出发一个终端操作,否则中间操作不会执行任何处理。因为中间操作一般都可以合并起来,在终端操作时一次性全部处理。
流的使用一般包括三件事:
1.一个数据源来执行一个查询
2.一个中间操作链,形成一条流的流水线
3.一个终端操作,执行流水线,并能生成结果
filter Stream<T> Predicate<T> T -> boolean
map Stream<R> Function<T, R> T -> R
limit Stream<T>
sorted Stream<T> Comparator<T> (T, T) -> T
distinct Stream<T>
foreach:消费流中的每个元素并对其应用Lambda,返回void
count:返回流中元素的个数,返回long
collect:将流中的元素归约成一个集合
filter:该操作接受一个谓词作为参数,并返回一个包括所有符合谓词的元素的流。
menu.stream().filter(Dish::isVegetarian).collect(toList());
distinct:它会返回一个元素各异的流。
numbers.stream().filter(i -> i%2 == 0).distinct().forEach(System.out :: println);
limit:返回一个不超过给定长度的流。所需的长度作为参数传递给limit。
menu.stream().filter(d -> d.getCalories() > 300).limit(3).collect(toList());
skip:返回一个扔掉前n个元素的流。如果流中元素不足n个,则返回一个空流。
menu.stream().filter(d -> d.getCalories() > 300).skip(2).collect(toList());
map:接受一个函数作为参数,这个函数会被应用到每个元素上,并将其映射成一个新的元素。
menu.strean.map(Dish::getName).collect(toList());
Arrays.stream()的方法可以接受一个数组并产生一个流。
Stream<String> streamOfWords = Arrays.stream(arrayOfWords);
flatMap:各个数组并不是分别映射成一个流,而是映射成流的内容。所有使用map(Arrays::stream)时生成的单个流都被合并起来,即扁平化为一个流。
words.stream().map(w -> w.split("")).flapMap(Arrays::stream).distinct().collect(Collectors.toList());
numbers.stream().flatMap(i -> numbers2.stream().map(j -> new int[](i, j)));
anyMatch:流中是否有一个元素能匹配给定的谓词。
allMatch:流中的元素是否都能匹配给定的谓词。
noneMatch:确保流中没有任何元素与给定的谓词匹配。
findAny:返回当前流中的任意元素。
findFirst:找到流中的第一个符合谓词的元素
Optional<T>类是一个容器类,代表一个值存在或不存在。isPresent()将在Optional包含值的时候返回true,否则返回false。ifPresent(Consumer<T> block)会在值存在的时候执行给定的代码块。T get()会在值存在时返回值,否则返回一个默认值。T orElse(T other)会在值存在时返回值,否则返回一个默认值。
归约:需要将流中所有的元素反复结合起来,得到一个值。用函数式编程语言的术语来说是折叠。
reduce接受两个参数:一个初始值(可选),一个BinaryOperator<T>来将两个元素结合起来产生一个新值。
numbers.stream().reduce(0, (a,b) -> a + b); ==> numbers.stream().reduce(0, Integer :: sum);
numbers.stream().reduce(0, Integer :: max);
numbers.stream().reduce(0, Integer :: min);
流操作:
从输入流中获取每一个元素并在输出中得到0或1个结果没这些操作一般是无状态的,它们一般没有内部状态。如filter,map,sum等。但是像sort,distinct等操作,从流中排序和删除重复项时都需要知道先前的历史,我们把这些操作称为有状态操作。
原始类型流转化
Java8引入了三个原始类型(IntStream,DoubleStream和LongStream)特化流接口来解决暗含的装箱成本。
将流转化为特化版本的常用方式是mapToInt,mapToDouble和mapToLong。
menu.stream().mapToInt(Dish::getCalories).sum(); //IntStream支持sum,max,min,average等
menu.stream().mapToInt(Dish::getCalories).boxed(); //将instream转换为正常流,也可以使用mapToObj方法返回一额对象值流。
Optional原始类型特化版本:OptionalInt,OptionalDouble和OptionalLong。比如要找到IntStream中的最大元素,调用max返回一个OptionalInt,若没有最大值,则可以用OptionalInt的orElse方法来显式定义一个默认值。
Java8引入了两个可以用于IntStream和LongStream的静态方法来生成范围:range和rangeClosed。这两个方法第一个参数接收起始值,第二个参数接收结束值。range是不包含结束值,但rangeClosed则包含结束值。
IntStream.rangeClosed(1, 100).filter(n -> n % 2 == 0);
创建流
由值创建:Stream.of接受任意数量的参数创建一个字符串流
Stream.of("java8", "lambda", "in", "action");
由数组创建:Arrays.Stream接受一个数组作为参数创建一个流
Arrays.stream(new int[]{1, 2, 3});
由文件生成流:java.nio.file中的很多静态方法都会返回一个流。File.lines返回一个由指定文件中的各行构成的字符串流。
File.lines(Path.get("data.txt"), Charset.defaultCharset()).flapMap(line -> Arrays.stream(line.split(" "))).distinct.count();
有函数生成流:创建无限流。Stream.iterate和Stream.generate会根据给定函数按需创建值。一般需要用limit(n)来制定,以避免打印无穷多个值。
iterate接受一个初始值,还有一个依次应用在每个产生的新值上的Lambda。iterate基本上是顺序的,无界的。在需要一次生成一系列值的时候应使用iterate。
Stream.iterate(new int[]{0, 1}, t -> new int[]{t[1], t[0] + t[1]}).limit(20).forEach(System.out::println);
generate接受一个Supplier<T>类型的Lambda提供新的值。
Stream.generate(Math::random).limit(5).forEach(System.out::println);
Collectior会对元素应用过一个转换函数,并将结果累积在一个数据结构中,从而产生这一过程的最终输出。Collectors类提供的工厂方法主要提供了三个功能:将流元素归约和汇总为一个值;元素分组和元素分区。
Collectors.groupingby生成一个map。
Map<Currency, List<Transaction>> transactionByCurrencies = transactions.stream().collect(groupingBy(Transaction::getCurrency));
Collectors.counting工厂方法返回的收集器返回流元素总数
long howManyDishes = menu.stream().collect(Collectors.counting); ==> menu.stream().count();
Collectors.maxBy和Collectors.minBy计算流中的最大值和最小值。这两个收集器接收一个Comparator参数来比较流中的元素。
Comparator<Dish> dishCaloriesComparator = Comparator.comparingInt(Dish::getCalaries);
Optional<Dish> mostCalorieDish = menu.stream().collect(maxBy(dishCaloriesComparator));
Collectors.summingInt接受一个把对象映射为求和所需int函数,并返回一个收集器。该收集器在传递给普通的collect方法后即执行我们需要的汇总操作。对应的还有summingLong和summingDouble。
int totalCalories = menu.stream().collect(summingInt(Dish::getCalories));
Collectors.averagingInt计算值的平均数。还有averagingDouble和averagingLong
double avgCalories = menu.stream().collect(averaging(Dish::getCalories));
Collectors.summaringInt工厂方法返回收集器。它会返回一个IntSummaryStatistics类里,提供了getter方法来访问结果。相应的有LongSummaryStatistics和DoubleSummaryStatistics。
IntSummaryStatistics menuStatistics = menu.stream().collect(summarizingInt(Dish::getCalories));
Collectors.joining工厂方法返回的收集器会把对流中每个对象应用toString方法得到所有名字字符串连接成一个字符串。joining在内部使用了StringBuilder来吧生成的字符串逐个追加起来。若原先类中有toString方法,则无需映射。
menu.stream().map(Dish::getName).collect(joining()); ==> menu.stream().collect(joining());
joining工厂方法可以接受元素之间的分界符。
menu.stream().map(Dish::getName).collect(joining(","));
Collectors.reduce工厂方法是所有这些特殊方法的一般化。它接受三个参数:归约操作的起始值,归约中所用的函数以及BinaryOperator。reduce工厂方法返回Optional<T>,以便在空留的情况下安全地执行归约操作。可以使用orElse或orElseGet方法设置Optional的默认值。
?意味着收集器的累加器类型未知,是一个通配符。
分组
groupingBy方法传递了一个Function,它提取了流中每个元素的一个属性。通常把这个Function称作分类函数,因为它用来把流中的元素分成不同的组。分组的结果是一个Map,把分组函数返回的值作为映射的key,把流中所有具有这个分类值的项目的列表作为对应的映射值。
Map<CaloricLevel, List<Dish>> dishedByCaloricLevel = menu.stream().collect(groupingBy(dish -> {if(dish.getCalories() <= 400) return CaloricLevel.DIET; else if(dish.getCalories() <= 700) return CaloricLevel.NORMAL; else return CaloricLevel.FAT;}));
groupingBy还可以接受collector类型的第二个参数。若要进行二级分组的话,可以把一个内层groupingBy传递给外层groupingBy,并定义一个为流中项目分类的二级标准。
Map<Dish.Type, Map<CaloricLevel, List<Dish>>> dishesByTypeCaloricLevel = menu.stream().collect(groupingBy(Dish.type, groupingBy(dish ->{ if(dish.getCalories() <= 400) return CaloricLevel.DIET; else if(dish.getCalories() <= 700) return CaloricLevel.NORMAL; else return CaloricLevel.FAT })));
groupingBy收集器只有在应用分组条件后,第一次在流中找到某个键对应的元素时才会把键加入分组Map中,
在分组过程中,可以通过Collections.collectingAndThen方法将返回的结果转换成另一种类型
Map<Dish.Type, Dish> mostCaloricType = menu.stream().collect(groupingBy(Dish::getType, collectingAndThen(maxBy(comparingInt(Dish::getCalories)), Optional::get)));
通过groupingBy的第二个参数传递的收集器将会对分到同一组中的中的所有元素执行进一步归约。
Map<Dish.Type, Integer> totalCaloriesByType = menu.stream().collect(groupingBy(Dish::getType, summingInt(Dish::getCalories)));
常常和groupingBy连用的是mapping方法。mapping接收两个参数:一个函数对流中的元素做变换,另一个则将变换的结果对象收集起来。其目的是在累加之前对每个输入元素应用一个映射函数。
Map<Dish.Type, Set<CaloricLevel>> caloricsLevelsByType = menu.stream().collect().groupingBy(Dish::getType, mapping(dish -> {if(dish.getCalories() <= 400) return CaloricLevel.DIET; else if(dish.getCalories() <= 700) return CaloricsLevel.NORMAL; else return CaloricsLevel.FAT;}, toSet()));
分区
分区是分组的特殊情况,由一个谓词作为分类函数,它称分区函数。分区函数返回一个布尔值,这意味着得到分组Map的键的类型是Boolean。
Map<Boolean, List<Dish>> partitionedMenu = menu.stream().collect(partitioningBy(Dish::isVegetarian));
分区的好处在于保留了分区函数返回true或false的两套流元素列表。
Map<Boolean, Map<Dish.Type, List<Dish>>> vegetarianDishesByType = menu.stream().collect(partitioningBy(Dish::isVegetarian, groupingBy(Dish::getType)));
Collectors类的静态工厂方法
toList List<T> 把流中的所有项目收集到一个List
toSet Set<T> 把流中的所有项目收集到一个Set。
toCollection Collection<T> 把流中的所有项目收集到给定的供应源创建的集合
counting Long 计算流中国元素的个数
summingInt Integer 对流中项目的一个整数属性求和
avaragingINt Double 计算流中项目Integer属性的平均值
summarizingInt IntSummaryStatistics 收集关于流中项目Integer属性的统计值。最大,最小和平均值
joining String 连接对流中每个项目调用toString方法所生成的字符串
maxBy Optional<T> 一个包裹了流中按照给定比较器选出的最大元素Optional。若流为空则为Optional.empty()
minBy Optional<T> 一个包裹了流中按照给定比较器选择的最小元素Optional。若流为空则为Optional.empty()
reducing 归约操作产生的类型 从一个座位累加器的初始值开始,利用BinaryOperator与流中的元素逐个结合,从而将流归约为单个值
collectingAndThen 转换函数返回的类型 包裹另一个收集器,对其结果应用转换函数
groupingBy Map<K, List<T>> 根据项目的一个属性的值对流中的项目作为组,并将属性址作为结果Map的键
partitioningBy Map<Boolean, List<T>> 根据对流中每个项目应用谓词的结果来对项目进行分区
Collector接口的定义
public interface Collector<T, A, R>{
Supplier<A> supplier();
BiConsumer<A, T> accumulator();
Function<A, T> finisher();
BinaryOperator<A> combiner();
Set<Characteristics> characteristics();
}
T是流中要收集的项目的范型;A是累加器的类型,累加器是在收集过程中用于累积部分结果的对象;R是收集操作得到的对象的类型
spplier方法返回一个结果为空的Supplier,在调用时创建一个空的累加器实例,供数据收集过程使用。
accumulator方法会返回执行归约操作的函数。当遍历到流中第n个元素时,这个函数执行时会有两个参数:保存归约结果的累加器(已收集了流中的前n-1个项目)和第n个元素本身。该函数返回void。
finisher方法返回在累积过程中的最后要调用的一个函数,以便将累加器对象转换为整个集合在操作的最终结果。
combiner方法返回一个供归约操作使用的函数,定义了对流的个个字部分进行并行处理,各个字部分归约所得的累加器要如何合并。
characteristics会返回一个不可变的Characteristics集合,它定义了收集器的行为。Characteristics是一个包含三个项目的枚举:
UNORDERED:归于结果不受流中项目的便利和累积顺序的影响
CONCURRENT:accumulator函数可以从多个线程同时调用,且该收集器可以并行归约,若收集器没有标为UNORDERED,那它仅在用语无需数据源时才可以并行归约
IDENTITY_FINISH:表明完成器方法返回的函数是一个恒等函数,可以跳过。累加器对象将会直接用作归约过程的最终结果
public class ToListCollector<T> implements Collectort<T, List<T>, List<T>>{
public Supplier<List<T>> supplier(){
return ArrayList::new;
}
public BiConsumer<List<T>, T> accumulator(){
return List::add;
}
public Function<List<T>, List<T>> finisher(){
return Function.identity();
}
public BinaryOperator<List<T>> combiner(){
return (list1, list2) -> {list1.addAll(list2); return list1};
}
public Set<Characteristics> characteristics(){
return Collections.unmodifiableSet(EnumSet.of(IDENTITY_FINISH, CONCURRENT));
}
}
对于IDENTITY_FINISH的收集操作,可以使用Stream的collect方法。
List<Dish> dishes = menuStream.collect(ArrayList::new, List::add, List::addAll);
Stream接口通过对收集源调用parallelStream方法来把集合转换为并行流。并行流就是一个把内容分成多个数据块,并用不同的线程分别处理每个数据块的流。
public static long parallelSum(long n){
return Stream.iterate(1L, i -> i + 1).limit(n).parallel().reduce(0L, Long::sum);
}
对顺序流调用parallel方法并不意味着流本身有任何实际的变化。它在内部实际上就是设了一个boolean标志,表示调用parallel之后进行的所有操作都并行执行。你需要对并行流调用sequential方法就可以把它变成顺序流。可以将parallel和sequential方法结合起来,就可以更细化地控制在便利流时那些操作需要并行,那些操作需要顺序执行。
stream().parallel().filter().sequential().map().parallel().reduce();
并行流内部使用了默认的ForkJoinPool,它默认的线程数量就是处理器数量,这个值是由Runtime.getRunTime().availableProcessors()得到的。可以通过系统属性java.util.concurrent.ForkJoinPool.common.parallelism来改变线程池大小。
System.setProperty("java.util.concurrent.ForkJoinPool.common.parallelism", "12");
iterate生成的是装箱对象,必须拆箱成数字才能求和;而且我们很难把iterate分成多个独立块来并行执行。因为每次应用这个函数都要依赖前一次应用的结果。因此通常用LongStream.rangeClosed的方法,因此LongStream.rangeClosed直接产生原始类型的long数字,没有装箱拆箱的开销。LongStream.rangeClosed会生成数字范围,很容拆成为独立的小块。
并行化过程需要对流中做递归划分,把每个子流的归纳操作分配到不同的线程,然后把这些操作的结果合并成一个值。但在多个内核之间移动数据的代价也可能比想象的大,所以重要的一点是要保证在内核中并行执行工作的时间比在内核之间传输数据的时间长。
错用并行流而产生错误的首要原因是使用的算法改变了某些共享状态。
应参考以下情况来考虑是否 应该使用并行流
1.测量。把顺序流转成并行流不一定是好事,并行流不一定总是比顺序流快。应该用过适当的基准来检查其性能
2.留意装箱。自动装箱和拆箱操作会大大降低性能。应尽量使用原始类型流来避免这种操作。
3.有些操作在并行流上本身就比顺序流的性能差。如limit和findFirst等依赖于元素顺序的操作。
4.需要考虑操作流束线的总计算成本。设N是要处理的元素的总数,Q是一个元素通过流水线的大致处理成本,则N*Q就是这个对成本的一个粗略的定性估计。Q值较高意味着并行流时行性能好的可能性较大
5.对于较小的数据量,选择并行流几乎从来都不是一个好的决定
6.要考虑流背后的数据结构是否容易分解
7.流自身的特点以及流水线中的中间操作修改流的方式,都可能会改变分解过程的性能
8.考虑终端操作中国年合并步骤的代价是大是小
流的数据源和可分解性
ArrayList 极佳
LinkedList 差
IntStream.range 极佳
Stream.iterate 差
HashSet 好
TreeSet 好
分枝/合并框架的目的是以递归方式将可以并行的任务拆分成更小的任务,然后将每个字任务的结果合并起来生成整体结果。它是整体ExecutorService接口的一个实现,它把子任务分配给线程池中的工作线程。
要把任务提交到ForkJoinPool,必须创建RecursiveTask<R>的一个子类,其中R是并行化任务产生的结果,若任务不返回结果,则是RecursiveAction类型。要定义RecursiveTask,只需要实现它唯一的抽象方法compute。
protected abstract R compute();
这个方法同时定义了将任务拆分成子任务的逻辑,以及无法再拆分或不方便再拆分时,生成单个字任务结果的逻辑。 一般来说把ForkJoinPool实例化一次,然后把实例保存在静态字段中,使之成为单例。使用默认的无参构造器是想让线程池使用Runtime.availableProcessors的返回值来决定线程池使用的线程数。availableProcessors方法实际上返回的事可用内核的数量,包括超线程生成的虚拟内核。
使用fork/join需要注意的事项:
对一个任务调用join方法会阻塞调用方,直到该任务作出结果
不应该在RecursiveTask内部使用ForkJoinPool的invoke方法。应该直接调用fork或compute方法,只有顺序代码才应该用invoke方法来启动并行计算
对子任务调用fork方法可以把它排进ForkJoinPool
调试使用分支/合并框架的并行计算不是很方便
和并行流一样,不应理所当然的认为在多核处理器上使用分支/合并框架就比顺序计算快。当子任务的运行时间比分出新任务所花的时间长时才考虑并行。分支/合并框架需要“预热”或要执行几遍才会被JIT编译优化。
理想情况下,划分并行任务时,应该让每个任务都用完全相同的时间完成,让所有的CPU内核都同样繁忙。但在实际中每个字任务花的时间可能天差地别。要么因为划分策略效率低,要么是有不可预知的原因,比如磁盘访问慢,或是需要和外部服务协调执行。分支/合并框架工程用一种称为工作窃取(work stealing)的技术来解决这个问题。在实际应用中,这些任务差不多被平均分配到ForkJoinPool中的所有线程上。每个线程都被为分配给他的任务保存一个双向链式队列,每完成一个任务,就会从队列头上取出下一任务开始执行。若某个线程已早早完成了分配给它的所有任务,它会从队列尾巴上偷走一个一个任务。这个过程会一直继续下去,直到所有的任务都执行完毕,所有的队列都清空。这种工作窃取算法用于在池中的工作线程之间重新分配和平衡任务。
Java8提供了Spliterator接口,意为可分迭代器(splitable iterator)。Spliterator接口提供了一个spliterator方法。
public interface Spliterator<T>{
boolean tryAdvance(Consumer<? super T> action);
Spliterator<T> trySplit();
long estimateSize();
int characteristics();
}
tryAdcance方法会按顺序一个一个使用Spliterator中的元素,并且若还有其他元素要遍历就返回true。
trySplit方法可以把一些元素划出去分给第二个Spliterator,让他们两个并行处理。
estimateSize方法估计还剩下多少个元素需要遍历
characteristics方法代表Spliterator本身特性集的编码
Spliterator的特性
ORDER 元素有既定的顺序,因此Spliterator在遍历和划分时也会遵循这一顺序
DISTINCT 对任意一对遍历过的元素x和y,x.equals(y)返回false
SORTED 遍历的元素按照一个预定义的顺序排序
SIZED 该Spliterator由一个已知大小的源建立,因此estimatedSize返回的是准确值保证遍历的元素不是null
NONNULL 保证遍历的元素不会为null
IMMUTABL Spliterator的数据源不能修改。意味着在遍历时不能添加,删除或修改任何元素
CONCURRENT 该Spliterator的数据源可以被其他线程同时修改而无需同步
SUBSIZED 该Spliterator和所有从它拆分出来的Spliterator都是SIZED
将Stream拆分成多个部分的算法是一个递归过程。Spliterator可以在第一次遍历,第一次拆分或第一次查询估计大小时绑定元素的数据源,而不是在创建时就绑定,这种情况称为延迟绑定(late-binding)的Spliterator。