文章目录
引言
什么是Streams?
它不是集合元素,不是数据结构,并不保存数据
一个高级版本的迭代器(Iterator)
- Streams会隐式地在内部进行遍历,做出相应的数据转换
- 单向的,不可往复
即数据只能遍历一次,遍历过一次后即用尽了 - 可以并行化操作
parallelStream()方法,依赖于 Java7 中引入的 Fork/Join 框架来拆分任务和加速处理过程
数据源本身可以是无限的(集合是有限的)
Streams的用途?
作为一个高级版本的迭代器(Iterator),当然是用来对数据源进行遍历操作的。
疑问:遍历操作用for循环不就好了?
示例:打印与年龄超过 65 岁的买家进行交易的卖家姓名,并按姓名排序:
- 利用for循环处理:
//找到与年龄超过 65 岁的买家进行交易的卖家
Set<Seller> sellers = new HashSet<>();
for (Txn t : txns) {
if (t.getBuyer().getAge() >= 65)
sellers.add(t.getSeller());
}
// 按姓名排序
List<Seller> sorted = new ArrayList<>(sellers);
Collections.sort(sorted, new Comparator<Seller>() {
public int compare(Seller a, Seller b) {
return a.getName().compareTo(b.getName());
}
});
// 打印
for (Seller s : sorted)
System.out.println(s.getName());
- 利用Stream处理:
txns.stream()
.filter(t -> t.getBuyer().getAge() >= 65)
.map(Txn::getSeller)
.distinct()
.sorted(comparing(Seller::getName))
.map(Seller::getName)
.forEach(System.out::println);
由上示例可见:
Streams API方式的代码组织结构更清晰,可读性更强。我们只需指定“做什么”,而“如何做”交给Streams API就可以了。
从性能优化层面看:
for循环为外部迭代,其“串行”、“依次”特性使得集合框架无法对控制流进行优化。而Streams API为类库的内部迭代,类库可以进行各种优化操作(乱序执行、惰性求值、并行等),提升迭代性能。
1 Stream的组成
当我们使用一个流的时候,通常包括三个基本步骤:
- 获取一个数据源(source);
- 数据转换;
- 执行操作获取想要的结果。
每次转换原有 Stream 对象(数据源)不改变,返回一个新的 Stream 对象(可以有多次转换),这就允许对其操作可以像链条一样排列,变成一个管道,如下图所示:
流的操作类型包括:
编号 | 操作类型 | 说明 | 常见操作 |
---|---|---|---|
1 | Intermediate | a.其目的主要是打开流,做出某种程度的数据映射/过滤,然后返回一个新的流,交给下一个操作使用。b.一个流可以后面跟随0个或多个 intermediate 操作。c.该类操作都是惰性化的。就是说,仅仅调用到这类方法,并没有真正开始流的遍历 | map (mapToInt, flatMap等)、filter、distinct、sorted、peek、limit、skip、parallel、sequential、unordered |
2 | Terminal | a.一个流只能有一个 terminal 操作,当这个操作执行后,流就被使用“光”了,无法再被操作。b.这必定是流的最后一个操作。Terminal 操作的执行,才会真正开始流的遍历,并且会生成一个结果,或者一个 side effect。c.急性操作,流的操作步骤可以理解为:数据源—惰性操作—惰性操作—急性操作 | forEach、forEachOrdered、toArray、reduce、collect、min、max、count、anyMatch、allMatch、noneMatch、findFirst、findAny、iterator |
3 | short-circuiting | a.当操作一个无限大的 Stream,而又希望在有限时间内完成操作,则在管道内拥有一个 short-circuiting 操作是必要非充分条件。b.具体的:1)、对于一个 intermediate 操作,如果它接受的是一个无限大的 Stream,但返回一个有限的新 Stream。2)、对于一个 terminal 操作,如果它接受的是一个无限大的 Stream,但能在有限的时间计算出结果。 | anyMatch、allMatch、noneMatch、findFirst、findAny、limit |
这里放一个简单的流操作示例:
int sum = widgets.stream()
.filter(w -> w.getColor() == RED)
.mapToInt(w -> w.getWeight())
.sum();
2 Stream操作详解
2.1 流的构造与转换
2.1.1 流的构造
流的构造主要有三种方式。
1 通过Stream.of()方法
String[] nameArray = {"Tom", "Jerry"};
Stream<String> stream = Stream.of(nameArray);
2 [Collections].stream()方法
String[] nameArray = {"Tom", "Jerry"};
List<String> list = Arrays.asList(nameArray);
Stream<String> stream = list.stream();
3 数值流
对于基本数值型,目前有三种对应的包装类型 Stream:IntStream、LongStream、DoubleStream。
IntStream.of(new int[]{1, 2, 3}).forEach(System.out::println);
IntStream.range(1, 3).forEach(System.out::println);
IntStream.rangeClosed(1, 3).forEach(System.out::println);
2.1.2 流的转换
流既然能通过其它的数据类型进行构造,同样的也能转换成其它的数据结构,常见的有如下三种。
1 转换成数组
String[] toArray = stream.toArray(String[]::new);
2 转换成集合
// transform to List
List<String> stringList = stream.collect(Collectors.toList());
// transform to set
Set<String> stringSet = stream.collect(Collectors.toSet());
// transform to map
Map<String, Integer> stringMap = stream.collect(Collectors.toMap(String::toUpperCase, String::length));
注意:当通过collect(Collectors.toMap())转换成Map的时候,如果出现的Key重复的情况,会报异常。
3 转换成String
String toString = stream.collect(Collectors.joining()).toString();
2.2 流的详细操作
map
功能:生成 1:1 映射,每个输入元素,都按照规则转换成为另外一个元素。
//平方数
List<Integer> nums = Arrays.asList(1, 2, 3, 4);
List<Integer> squareNums = nums.stream().
map(n -> n * n).
collect(Collectors.toList());
//转换为大写
List<String> output = wordList.stream().
map(String::toUpperCase).
collect(Collectors.toList());
flatMap
功能:把 input Stream 中的层级结构扁平化,就是将最底层元素抽出来放到一起,最终 output 的新 Stream 里面已经没有 List 了,都是直接的数字。
Stream<List<Integer>> inputStream = Stream.of(
Arrays.asList(1),
Arrays.asList(2, 3),
Arrays.asList(4, 5, 6)
);
Stream<Integer> outputStream = inputStream.
flatMap((childList) -> childList.stream());
filter
根据条件进行元素的过滤。
// 获取文章中的全部单词
List<String> output = reader.lines().
flatMap(line -> Stream.of(line.split(REGEXP))).
filter(word -> word.length() > 0).
collect(Collectors.toList());
forEach
功能:接收一个 Lambda 表达式,然后在 Stream 的每一个元素上执行该表达式。
//找出男性并打印姓名
//Java 8
personlist.stream()
.filter(p -> p.getGender() == Person.Sex.MALE)
.forEach(p -> System.out.println(p.getName()));
//Pre-Java 8
for (Person p : personlist) {
if (p.getGender() == Person.Sex.MALE) {
System.out.println(p.getName());
}
}
注意:forEach 不能修改自己包含的本地变量值,也不能用 break/return 之类的关键字提前结束循环。
reduce
功能: 对Stream 元素进行组合运算(提供一个起始值,然后依照BinaryOperator运算规则,和前面 Stream 的第1个、第2个、第 n 个元素组合)。
// 字符串连接,concat = "ABCD"
String concat = Stream.of("A", "B", "C", "D").reduce("", String::concat);
// 求最小值,minValue = -3.0
double minValue = Stream.of(-1.5, 1.0, -3.0, -2.0).reduce(Double.MAX_VALUE, Double::min);
// 求和,sumValue = 10, 有起始值
int sumValue = Stream.of(1, 2, 3, 4).reduce(0, Integer::sum);
// 求和,sumValue = 10, 无起始值
sumValue = Stream.of(1, 2, 3, 4).reduce(Integer::sum).get();
// 过滤,字符串连接,concat = "ace"
concat = Stream.of("a", "B", "c", "D", "e", "F");
filter(x -> x.compareTo("Z") > 0).
reduce("", String::concat);
sum、min、max、average
List<Integer> integers = Arrays.asList(1, 2, 3, 4);
//sum操作
Integer result = integers.stream().mapToInt(w -> w).sum();
//等价于reduce操作
Integer result = integers.stream().reduce(0, (a, b) -> a + b);
注意:Stream 中的reduce、max/min、findFirst、findAny等操作返回的结果类型为:Optional.
optional
- Optional作为一个容器,它可能含有某值,或者不包含;
- 使用它的目的是尽可能避免 NullPointerException
在更复杂的 if (element != null) 的情况中,使用 Optional 代码的可读性更好;而且它提供的是编译时检查,能极大的降低 NPE 这种 Runtime Exception 对程序的影响,或者迫使程序员更早的在编码阶段处理空值问题,而不是留到运行时再发现和调试。
limit/skip
功能:limit 返回 Stream 的前面 n 个元素;skip 则是扔掉前 n 个元素。
public void testLimitAndSkip () {
List<Person> persons = new ArrayList();
for (int i = 1; i <= 10000; i++) {
Person person = new Person(i, "name" + i);
persons.add(person);
}
List<String> personList2 = persons.stream().
map(Person::getName).limit(10).skip(3).collect(Collectors.toList());
System.out.println(personList2);
}
private class Person {
public int no;
private String name;
public Person(int no, String name) {
this.no = no;
this.name = name;
}
public String getName() {
System.out.println(name);
return name;
}
}
输出:
name1
name2
name3
name4
name5
name6
name7
name8
name9
name10
[name4, name5, name6, name7, name8, name9, name10]
sorted
功能:排序。比数组的排序更强之处在于你可以首先对 Stream 进行各类 map、filter、limit、skip 甚至 distinct 来减少元素数量后,再排序,这能帮助程序明显缩短执行时间。
//找出全文的单词,转小写,并排序
List<String> words = br.lines().
flatMap(line -> Stream.of(line.split(" "))).
filter(word -> word.length() > 0).
map(String::toLowerCase).
distinct().
sorted().
collect(Collectors.toList());
br.close();
System.out.println(words);
Match
匹配元素,包括allMatch, anyMatch匹配等。
List<Person> persons = new ArrayList();
persons.add(new Person(1, "name" + 1, 10));
persons.add(new Person(2, "name" + 2, 21));
persons.add(new Person(3, "name" + 3, 34));
persons.add(new Person(4, "name" + 4, 6));
persons.add(new Person(5, "name" + 5, 55));
//allMatch
boolean isAllAdult = persons.stream().
allMatch(p -> p.getAge() > 18);
System.out.println(isAllAdult); // false
//anyMatch
boolean isThereAnyChild = persons.stream().
anyMatch(p -> p.getAge() < 12);
System.out.println(isThereAnyChild); // true
//noneMatch
boolean isNoChild = persons.stream().
anyMatch(p -> p.getAge() > 60);
System.out.println(isNoChild); // true
3 Guava的transform()
以Lists.transform()为例进行说明
3.1 Guava的Lists.transform()返回的结果具有“视图(View)”特性:
- view和它所依赖的数据结构共享底层数据,当底层数据结构修改时,view会变化
- 当view修改时(前提是view可以修改),底层数据结构也会修改
而Stream每次转换原有Stream对象不改变,返回一个新的 Stream 对象,不修改自己所封装的底层数据结构的数据,如果希望“transform后不要返回视图,而得到独立拷贝”,*上的建议这样:
//返回不可变列表(推荐)
ImmutableList.copyOf(Lists.transform(targetlist, function));
//返回可变列表
Lists.newArrayList(Lists.transform(targetlist, function));
3.2 Stream进行数据结构转换更加方便
4 项目中的实例
4.1 实例1
/**
* 远程加载所有区域信息,并转为不可变Map
*
* @return
* @throws GatewayException
*/
private Map<Integer, AreaInfoMsg> loadAreas() throws GatewayException {
Map<Integer, AreaInfoMsg> areaMap = groupGeoGateway.getAllAreas()
.values().stream().flatMap(List::stream)
.collect(Collectors.toMap(AreaInfoMsg::getId, e -> e));
return ImmutableMap.copyOf(areaMap);
}
/**
* 获取所有区域信息
* @return
*/
public Map<Integer,List<AreaInfoMsg>> getAllAreas() throws GatewayException {
logger.debug("load areas from groupgeo now");
try {
return groupGeoThriftClient.mapArea();
} catch (TException e) {
throw new GatewayException("Failed loading areas from groupgeo");
}
}
4.2 实例2
//分批异步请求
private ListenableFuture<Map<Integer, SinaiPoiModel>> innerBatchSubmit(List<Integer> manyIds) {
List<List<Integer>> lists = Lists.partition(manyIds, BATCH_LIMIT);
//分批提交,忽略单次提交异常
List<ListenableFuture<Map<Integer, SinaiPoiModel>>> futures = Lists.transform(lists,
ids -> {
try {
return innerSubmit(ids);
} catch (GatewayException e) {
return Futures.immediateFailedCheckedFuture(e);
}
});
//多次提交的结果合并为一个异步结果
return Futures.transform(
Futures.successfulAsList(futures),
mapList -> mapList.stream().filter(Objects::nonNull)
.collect(HashMap::new, Map::putAll, Map::putAll)
);
}
5 Stream的不足
-
遍历操作无法提前结束循环(break/return)
-
不支持对其元素进行直接操作或直接访问,而只支持通过声明式操作在其上进行运算然后得到结果
-
不推荐Web程序采用并行流(
parallelStream
)
List<String> names = people.parallelStream()
.map(Person::getName)
.collect(toList());
parallelStream
会调用JVM内全局共享的一个 Fork/Join 框架,里面默认会启动“当前机器CPU核数”个线程。也就是说,并行流最好仅用于“纯CPU计算”——因为一旦某个网络请求把其中某个线程阻塞了,那就是JVM全局共享的“CPU核数那么多的线程”就会被阻塞。