计算机科学中有不少带“流”的概念,例如字符流,字节流,比特流等等,不多有书籍在讲到这些概念的时候会详解介绍什么是流,因此有时候会致使读者感到迷惑,在这里,我大胆尝试简单解释一下“流”究竟是个什么东西。java
举个例子,水流你们都见过吧(不管是水管中的水流,仍是海流或者河流),从微观的角度看水流,它就是由一个一个的水分子和其余物质组合造成的(至于怎么流动的,这就是流体力学的事了,先无论),从一个或者多个源流动到一个或者多个目的地,例如你们的生活用水就是从水库流到各位的家中。在水流动的过程当中,能够采起一些处理,使得水变得更加洁净,安全。shell
从这个例子中,不难看到有几个关键词:分子,源,目的地,处理等。如今,再来看看计算机中的所谓的比特流,比特流里的“分子”就是一个一个的比特(0和1),源就是计算机自己(从宏观的角度看),而目的地则是其余的计算机,在源和目的地之间,咱们一样能够加入一些处理操做来处理比特,使得目的地收到的数据是符合需求的。编程
通过这么一个类比,各位应该大概知道什么是“流”了吧,如今给出一个比较简短的定义(来源是《Java8实战》):“从支持数据处理操做的源生成的元素序列”。看起来不像是人话对吧,不要惧怕,把这句话拆开来看就行了:数组
除此以外,流还有一些特色:安全
做为补充理解,能够看看下面这张图,来源也是《Java8 实战》一书,描述的是集合和流的区别(我的以为是一个很形象的比喻):数据结构
假设有一个需求:如今有一个Car对象的集合,咱们但愿从集合中找到并返回全部符合age <= 2条件的对象,而后根据age字段对对象集合进行排序,最后返回对象的brand字段的集合。框架
若是使用传统的方式编写代码,代码多是下面这样的:ide
List<Car> filteredCars = new ArrayList<>();
for (Car car : cars) {
if (car.getAge() <= 2) {
filteredCars.add(car);
}
}
Collections.sort(filteredCars, new Comparator<Car>() {
@Override
public int compare(Car o1, Car o2) {
return o1.compareTo(o2);
}
});
List<String> carNames = new ArrayList<>();
for (Car car : filteredCars) {
carNames.add(car.getBrand());
}
复制代码
注意到代码中使用了一个filteredCars集合,该集合既不是源集合,也不是目标集合,只是一个中间集合,若是咱们采用这种方式编程,这个中间集合是不得不使用的,但中间集合是有空间消耗的,若是集合数据不少,那么这个中间集合的影响就会很大。除此以外,这种方式编写的代码并不简洁,若是没有注释的话,想要彻底弄清楚这段代码是在干什么应该不是一件容易的事。那有没有办法简化代码,让人一看就知道代码的目的呢?答案是例如Java8的Stream API,下面来看看使用这种新的方式编写代码是怎样的:函数
List<String> carNames = cars.stream().filter((car) -> car.getAge() <= 2)
.sorted(Car::compareTo)
.map(Car::getBrand)
.collect(Collectors.toList());
复制代码
很是简单,就四行代码!(若是你愿意,写成一行也能够,可是不推荐)并且很是易读,看了第一行,就能发现:“哦,这里要作一个filter的操做”,看了第二行就能发现这是一个sorted排序操做,剩下的同理。oop
先不用管stream()是什么,sorted()是什么,后面会介绍到。
上述例子表现了Stream的一个优势,除此以外,Stream还能够用于应对数据是无限的状况,例如素数流,偶数流等等,更多的应用没法在这短短一篇文章中特性,还须要多多修炼!
Stream API的操做不少,例如filter,sorted,map,reduce,collect等等,大体能够分为两类操做:中间操做和终端操做。流通过中间操做后,其输出仍然能够做为下一个操做的输入,但通过终端操做以后,就没法继续进行操做了,因此通常终端操做都是一些具备“聚合”功能的操做,例如collect将流中的数据“收集”成集合或者其余什么用于保存数据的数据结构(用户能够自定义这个收集操做,也可使用内置的API)。而中间操做通常都是对数据进行“处理”,例如filter用于筛选数据,map用于对数据进行映射,即将数据转换成另外一种形式等。下图是经常使用的API:
下面我将选择几个最经常使用的操做进行介绍:
先给出共用的数据集合以及类:
public class Car implements Comparable<Car> {
private String brand;
private Color color;
private Integer age;
public Car(String brand, Color color, Integer age) {
this.brand = brand;
this.color = color;
this.age = age;
}
public String getBrand() {
return brand;
}
public void setBrand(String brand) {
this.brand = brand;
}
@Override
public String toString() {
return "Car{" +
"brand='" + brand + '\'' +
", color=" + color +
", age=" + age +
'}';
}
@Override
public int compareTo(Car o) {
return this.getAge() < o.getAge() ? 1 : this.getAge() == o.getAge() ? 0 : -1;
}
public enum Color {
RED,WHITE,PINK,BLACK,BLUE;
}
//getter and setter
}
复制代码
private static final List<Car> cars = Arrays.asList(
new Car("BWM",Car.Color.BLACK, 2),
new Car("Tesla", Car.Color.WHITE, 1),
new Car("BENZ", Car.Color.RED, 3),
new Car("Maserati", Car.Color.BLACK,1),
new Car("Audi", Car.Color.PINK, 5));
复制代码
filter的做用就是筛选数据,若是数据符合条件,就让他继续在流里流动,不然直接取出来,使其离开所在的流。假设如今咱们要筛选cars集合中全部颜色是黑色的car对象,该怎么作呢?很是简单,以下所示:
cars.stream().filter(car -> car.getColor().equals(Car.Color.BLACK))
.forEach(System.out::println);
复制代码
forEach先不用管,后面会讲到。
尝试一下,验证一下答案是否是BWM和Maserati呢?filter接受一个参数,参数类型是Predicate<? super T>,在上一篇文章中,我已经介绍了函数式接口以及lambda表达式,因此这里就再也不赘述了。
Google有一个很著名的大数据框架,即Map-Reduce,若是对Hadoop有一些了解的话,应该都知道。Map其实就是一个映射,即将原始数据转换成另外一种形式。如今咱们继续上面filter的例子,若是如今我想让返回的仅仅是筛选后的对象的brand字段集合,该如何作呢?可使用map,以下所示:
cars.stream().filter(car -> car.getColor().equals(Car.Color.BLACK))
.map(Car::getBrand)
.forEach(System.out::println);
复制代码
这里的map就是将Car对象实例转换成brand字符串,这个操做是很是有意义的,由于若是若是咱们仅仅须要一个brand字符串,对其余的根本不关系,又有什么必要还留着其余数据来影响后续的处理呢?
这是扁平化的map操做,和普通的map操做最大的不一样就是flatMap能把流中的某个元素都换成另外一个流,而后把全部的流链接起来造成一个新的流。仍是有些难以理解是吧,借用书上的一个例子来讲明一下:
String[] arrayOfWords = {"Hello", "World"};
Arrays.stream(arrayOfWords)
.map(word -> word.split(""))
.distinct()
.forEach(System.out::println);
复制代码
这段代码的目的是找出全部字母,这些字母是在数组中的单词里出现的,例如Helllo,World中出现了H,e,l,l,o,r,d,w这几个字母。但若是你运行一下上面这段代码,会发现返回的并非咱们所想的那样,而是相似这样的:
[Ljava.lang.String;@15aeb7ab
[Ljava.lang.String;@7b23ec81
复制代码
即返回是两个数组,怎么会这样呢?简单剖析一下,map操做的对象类型是String,即每一个单词,而后调用split()方法,该方法的返回值是Spring[]类型,因此map的返回类型是Stream<String[]>类型,然后面又没有太多的处理了,故最后forEach遍历的起始是String[]类型的对象,并非咱们想要的字符串。那么,怎么修改呢?答案是:使用flatMap使其扁平化:
Arrays.stream(arrayOfWords)
.map(word -> word.split(""))
.flatMap(Arrays::stream)
.distinct()
.forEach(System.out::println);
复制代码
只是在map后面多了一个flatMap操做就能解决问题了吗?刚刚说了,map的返回值类型是Stream<String[]>,即如今流中的元素类型是String[],flatMap尝试把String[]数组类的内容展开,即若是数组里的内容是"Hello",那么flatMap(Arrays::stream),就把H,e,l,l,o当作新的流,而后再组合成一个新的更大的流。一图胜千言,来看看具体的分析图:
即找到任何一个符合条件的元素就当即返回一个true,若是都没找到,那么就返回false。以下所示:
boolean IsExist = cars.stream()
.anyMatch(car -> car.getAge() <= 2);
复制代码
很是简单,很少做解释了。
即遍历,在Java8之前,咱们要么显式的使用迭代器或者用加强for循环的方式,要么就使用索引的方式(前提是集合支持索引)来遍历集合的元素,在Java8以后,遍历集合元素变得更加简单了,再也不须要显式的构造for循环,这种方式被称做“内部迭代”。关于其使用,在上面的例子已经屡次使用到了,这里就再也不写例子了,但我想提的是内部迭代的效率和外部显式迭代的效率对比,网上有不少文章有提到,内部迭代的效率比外部迭代更低,我在个人机器上稍微测试了一下(有预热),发现确实如此,但并不会低不少。我想表达的是,若是真的对性能特别敏感,那么用传统的外部显式迭代也许会是一个更好的选择,不然我更推荐基于Stream API的内部迭代,由于它更简洁,语义更明确(我的见解)。
其实这里提到的“内部迭代”的概念并不只仅存在于forEach操做,能够说整个流API的操做都是基于“内部迭代”的,这里特别说明一下。
最后就是collect操做了,这是一个“聚合”或者叫作“归约”操做。其参数是Collector<? super T, A, R> collector类型,但这并非一个函数式接口,在Collectors这个类里提供了一些经常使用的汇集成集合操做,例如toList就是汇集成一个List,toSet就是汇集成一个Set,同时,这是一个终端操做,一旦调用了该操做,就没法继续进行其余操做了。以下所示:
cars.stream().filter(car -> car.getColor().equals(Car.Color.BLACK))
.map(Car::getBrand)
.collect(Collectors.toList());
复制代码
其实咱们也能够自定义汇集成其余更多类型的集合或者一些自定义的数据结构,具体的实现方法能够参考Collectors里的几个方法,就很少说了。
本文简单介绍了什么是流,为何要使用流以及如何使用Java8提供的Stream API,但其实Stream API的功能远不止这样,还有一些更增强大的功能,例如count()操做等等等等.....Java8除了提供实现好的API,还能够自定义一些符合用户需求的功能,这也是Stream API强大的缘由之一。
Stream是一个很是庞大的体系,我这一篇短短几千词的文章远远不能囊括全部。若是本文有什么地方有错误或者不足,真诚的但愿您能指出,你们共同进步!