Stream是什么
书上说Stream是对JAVA中对集合处理的抽象,在我看来Stream更像是对java集合的一次扩展,因为Stream中的API都是我们对集合操作中可能遇到的问题。那为什么要用Stream呢?可以从两个方面去考虑,一方面,使得集合处理可以更加高效,Stream可以并行执行。另一方面,代码更佳优雅,更加简短,因为Stream中也用到了函数式编程接口。
举一个例子来看使用Stream的优势。对一个String集合中的所有字符串做统计,找出长度大于5的字符串。
//集合内容
List<String> list = new ArrayList<>();
list.add("a");
list.add("bc");
list.add("def");
list.add("ghij");
list.add("kmlno");
list.add("pqrstu");
使用传统的集合操作
int count =0;
for(String s : list){
if (s.length()>5){
count++;
}
}
使用Stream API进行处理
//串行执行
long count = list.stream().filter(s->s.length()>5).count()
//并行执行
long count = list.parallelStream().filter(s->s.length()>5).count()
相比于传统的迭代的模式,使用Stream对集合进行操作会更加简洁、方便、高效。
怎么使用Stream
使用Stream只需要三个步骤:
- 创建一个Stream
- 指定这个Stream要做的操作
- 使用一个终止动作来使得Stream产生结果
上面的字符串长度统计的例子中,list.stream()
用来建立了一个Stream,filter(s->s.length()>5)
告诉了这个流要做哪些事情,最后使用count
操作来强制它之前的延迟操作立即执行。Stream执行的一个特点就是延迟执行,count方法被调用的时候才会执行Stream之前定义的操作。另外,Stream执行的过程中不会对源对象产生改变,它们会返回一个新的Stream。
Stream的建立
JAVA8在Collection接口中添加了stream方法,所以我们可以将任何集合转换为一个Stream。另外我们也可以通过Stream.of
方法将一个数组转换为Stream。
//集合到Stream
Stream<String> stream = list.stream();
//数组到Stream
String contents = "a,b,c,d,e";
Stream<String> stream = Stream.of(contents.split(","));
//构造含有任意个数的Stream,Stream.of方法接受可变长度的参数
Stream<String> stream = Stream.of("q","w","e","r");
另外还可以建立无限序列,如建立一个行如0 1 2 3 4 5 …的无限序列
Stream<BigInteger> stream = Stream.iterate(BigInteger.ZERO,n->n.add(BigInteger.ONE));
另外一种创建无限Stream的方法是generate方法,当需要一个Stream值的时候就会调用该方法来产生一个包含随机数的无限流。
Stream<Double> stream = Stream.generate(Math::random);
Stream中的元素操作
Stream的过滤操作可以使用filter方法,该方法会返回一个新的流,这个流中的元素满足一定的条件。接口方法的定义如下所示
Stream<T> filter(Predicate<? super T> predicate);
该方法的参数是一个Predicate<T>
对象,是一个从T->boolean的函数。
如果需要对一个流中的每个元素都做一定的转换操作而不是进行条件过滤,如将一个字符串流中每个元素变为小写,那么可以使用map方法。
Stream<String> stream = list.stream().map(String::toLowerCase);
在使用迭代方式对集合进行处理的过程中,我们可以通过控制台打印的方式查看元素的执行情况。
for(集合){
...
打印元素,观察状态
...
}
在使用Stream流的过程中,可以使用peek方法完成相同的事情。该方法会产生一个与原始流具有相同元素的流,但是在每次获取一个元素时,都会调用一个函数,这样利于调试。
String[] stream =list.stream().peek(e->System.out.println("Fetching:"+e)).map(String::toLowerCase).toArray(String[]::new);
执行结果:
Fetching:a
Fetching:bc
Fetching:def
Fetching:ghij
Fetching:kmlno
Fetching:pqrstu
如果只是简单的打印流里面的元素,可以使用
list.stream().forEach(System.out::println)
Stream聚合操作
前面说过流的执行过程是延迟执行,当一个流遇到了终止操作后,它前面的操作才会执行。聚合方法都是终止操作。它会将流聚合为一个值。常用的聚合方法有返回流中的元素总数count方法。返回流中的最大值和最小值的max和min方法。findFirst方法返回非空集合中的第一个值。findAny方法返回所有匹配的元素。anyMatch方法返回流中是否有匹配的元素。
例子:
Optional<String> optional = list.stream().max(String::compareToIgnoreCase);
if (optional.isPresent()){
System.out.println(optional.get());
}
在执行这些方法时,返回值有可能为空,如果对返回值不加以判断就直接执行之后的代码,很有可能会引起空指针异常。JAVA8中推出了一个Optional<T>
类型,这是一种更好表示缺少返回值的方式。它是对一个T类型对象的封装或者表示不是任何对象,它比一般的指向T类型的引用更加安全。但是从上面的例子可以看出,使用Optional跟以前的直接对返回值进行判断并没有什么简化。如果没有设计Optional,那么上面的代码调用可能是下面的方式。
T value = list.stream().max(String::compareToIgnoreCase);
if (value != null){
System.out.println(value);
}
所以高效使用Optional的关键是把它当作一个中间结果,我们可以把这个中间结果传递给其它函数做处理。基本形式:
聚合值->Optional->函数处理,如我们要把上面比较的结果添加到一个result集合中去,那么可以使用
optional.ifPresent(value -> result.add(value));
有关Optional的详细信息可以参考这篇博文: Java 8 Optional类深度解析
Stream结果处理
在流处理的过程中,通常是要把处理的结果转换为集合对象,而不是要聚合为一个值。
流->数组
String[] result = list.stream().toArray(String[]::new)
流->set
Set<String> result = list.stream().collect(Collectors.toSet());
流->list
List<String> result = list.stream().collect(Collectors.toList());
流->map
由于map是k-v形式,所以要指明KV。如我们有一个Stream<Person>
对象,将它的id和name存入map中。
Map<Integer,String> idToName = stream.collect(Collectors.toMap(Person::getId,Person::getName))
如果key为id,值为person对象可以用以下方法获取map,如果key重复,会抛出异常。
Map<Integer, Person> idToPerson = stream.collect(Collectors.toMap(Person::getId, Function.identity()));
Stream分组操作
分组操作实际上是对map操作的一种简化与补充。如我们现在需要得到一个map,key为国家名,value为改国家所使用的语言。可以用以下代码实现:
Stream<Locale> locales = Stream.of(Locale.getAvailableLocales());
Map<String, Set<String>> countryLanguageSets = locales.collect(
Collectors.toMap(Locale::getDisplayCountry,
l -> Collections.singleton(l.getDisplayLanguage()),
(a, b) -> {
Set<String> r = new HashSet<>(a);
r.addAll(b);
return r; }
));
与上面生成map的方法相比,该方法多了一个参数,第三个参数的作用是在发现了一个国家的一种新语言时,我们将已有值和新值组成一个新的集合。上面实际上是对locale流按照国家来分组。所以也可以用以下代码实现
Map<String, List<Locale>> countryToLocales = locales.collect(
Collectors.groupingBy(Locale::getCountry));
Locale::getCountry为分组的依据。如果我们需要对分组后的结果在做一些处理,如转换为set或统计数量
locales = Stream.of(Locale.getAvailableLocales());
Map<String, Set<Locale>> countryToLocaleSet = locales.collect(
groupingBy(Locale::getCountry, Collectors.toSet()));
System.out.println("countryToLocaleSet: " + countryToLocaleSet);
locales = Stream.of(Locale.getAvailableLocales());
Map<String, Long> countryToLocaleCounts = locales.collect(
groupingBy(Locale::getCountry, counting()));
System.out.println("countryToLocaleCounts: " + countryToLocaleCounts);
Stream注意事项
当执行一个流操作时,并不会对底层的集合进行修改。