Stream流中的方法
Stream提供了大量的方法进行聚集操作,这些方法既可以是“中间的”,也可以是“末端的”。
中间方法:中间操作允许流保持打开状态,并允许直接调用后续方法。上面程序中的map()方法就是中间方法。中间方法的返回值是另外一个流。
末端方法:末端方法是对流的最终操作。当对某个Stream执行末端方法后,该流将会被“消耗”且不再可用。上面程序中的sum()、count()、average()等方法都是末端方法。
除此之外,关于流的方法还有如下两个特征:
有状态的方法:这种方法会给流增加一些新的属性,比如元素的唯一性、元素的最大数量、保证元素以排序的方式被处理等。有状态的方法往往需要更大的性能开销。
短路方法:短路方法可以尽早结束对流的操作,不必检查所有的元素。
下面简单介绍一下Stream常用的中间方法:
filter(Predicate predicate):过滤Stream中所有不符合predicate的元素。
mapToXxx(ToXxxFunction mapper):使用ToXxxFunction对流中的元素执行一对一的转换,该方法返回的新流中包含了ToXxxFunction转换生成的所有元素。
peek(Consumer action):依次对每个元素执行一些操作,该方法返回的流与原有流包含相同的元素。该方法主要用于调试。
distinct():该方法用于排序流中所有重复的元素(判断元素重复的标准是使用equals()比较返回true)。这是一个有状态的方法。
sorted():该方法用于保证流中的元素在后续的访问中处于有序状态。这是一个有状态的方法。
limit(long maxSize):该方法用于保证对该流的后续访问中最大允许访问的元素个数。这是一个有状态的、短路方法。
下面简单介绍一下Stream常用的末端方法:
forEach(Consumer action):遍历流中所有元素,对每个元素执行action。
toArray():将流中所有元素转换为一个数组。
reduce():该方法有三个重载的版本,都用于通过某种操作来合并流中的元素。
min():返回流中所有元素的最小值。
max():返回流中所有元素的最大值。
count():返回流中所有元素的数量。
anyMatch(Predicate predicate):判断流中是否至少包含一个元素符合Predicate条件。
noneMatch(Predicate predicate):判断流中是否所有元素都不符合Predicate条件。
findFirst():返回流中的第一个元素。
findAny():返回流中的任意一个元素。
除此之外,Java 8允许使用流式API来操作集合,Collection接口提供了一个stream()默认方法,该方法可返回该集合对应的流,接下来即可通过流式API来操作集合元素。由于Stream可以对集合元素进行整体的聚集操作,因此Stream极大地丰富了集合的功能。
Java 8新增了Stream、IntStream、LongStream、DoubleStream等流式API,这些API代表多个支持串行和并行聚集操作的元素。上面4个接口中,Stream是一个通用的流接口,而IntStream、LongStream、DoubleStream则代表元素类型为int、long、double的流。
Java 8还为上面每个流式API提供了对应的Builder,例如Stream.Builder、IntStream.Builder、LongStream.Builder、DoubleStream.Builder,开发者可以通过这些Builder来创建对应的流。
独立使用Stream的步骤如下:
使用Stream或XxxStream的builder()类方法创建该Stream对应的Builder。
重复调用Builder的add()方法向该流中添加多个元素。
调用Builder的build()方法获取对应的Stream。
调用Stream的聚集方法。
在上面4个步骤中,第4步可以根据具体需求来调用不同的方法,Stream提供了大量的聚集方法供用户调用,具体可参考Stream或XxxStream的API文档。对于大部分聚集方法而言,每个Stream只能执行一次。
特别注意:
- 流的使用是遵循以下的原则的。
- 惰性求值(如果没有终结操作,没有中间操作是不会得到执行的)
流是一次性的(一旦一个流对象经过一个终结操作后。这个流就不能再被使用) - 不会影响原数据(我们在流中可以多数据做很多处理。但是正常情况下是不会影响原来集合中的元素的。这往往也是我们期望的)
串行Stream
Stream 接口
java.util.Stream 表示能应用在一组元素上一次执行的操作序列。Stream 操作分为中间操作或者最终操作两种,最终操作返回一特定类型的计算结果,而中间操作返回Stream本身,这样你就可以将多个操作依次串起来。Stream 的创建需要指定一个数据源,比如 java.util.Collection的子类,List或者Set,Map不支持。Stream的操作可以串行执行或者并行执行。首先看看Stream是怎么用,首先创建实例代码的用到的数据List:
代码如下:
List stringCollection = new ArrayList<>(); stringCollection.add("ddd2"); stringCollection.add("aaa2"); stringCollection.add("bbb1"); stringCollection.add("aaa1"); stringCollection.add("bbb3"); stringCollection.add("ccc"); stringCollection.add("bbb2"); stringCollection.add("ddd1");
Java 8扩展了集合类,可以通过 Collection.stream() 或者 Collection.parallelStream() 来创建一个Stream。下面几节将详细解释常用的Stream操作:
Filter 过滤
过滤通过一个predicate接口来过滤并只保留符合条件的元素,该操作属于中间操作,所以我们可以在过滤后的结果来应用其他Stream操作(比如forEach)。forEach需要一个函数来对过滤后的元素依次执行。forEach是一个最终操作,所以我们不能在forEach之后来执行其他Stream操作。
代码如下:
stringCollection .stream() .filter((s) -> s.startsWith("a")) .forEach(System.out::println); // "aaa2", "aaa1"
Sort 排序
排序是一个中间操作,返回的是排序好后的Stream。如果你不指定一个自定义的Comparator则会使用默认排序。
代码如下:
stringCollection .stream() .sorted() .filter((s) -> s.startsWith("a")) .forEach(System.out::println); // "aaa1", "aaa2"
需要注意的是,排序只创建了一个排列好后的Stream,而不会影响原有的数据源,排序之后原数据stringCollection是不会被修改的:
代码如下:
System.out.println(stringCollection); // ddd2, aaa2, bbb1, aaa1, bbb3, ccc, bbb2, ddd1
Map 映射
中间操作map会将元素根据指定的Function接口来依次将元素转成另外的对象,下面的示例展示了将字符串转换为大写字符串。你也可以通过map来将对象转换成其他类型,map返回的Stream类型是根据你map传递进去的函数的返回值决定的。
代码如下:
stringCollection .stream() .map(String::toUpperCase) .sorted((a, b) -> b.compareTo(a)) .forEach(System.out::println); // "DDD2", "DDD1", "CCC", "BBB3", "BBB2", "AAA2", "AAA1"
flatMap映射
map只能把一个对象转换成另一个对象来作为流中的元素,而flatMap可以把一个对象转换成多个对象作为流中的元素。
这里的意思是,如果我们的一个类中的一个对象数据我们取出来,他是一个List类型,那么如果我们使用的是map,那么我们再次放入到流中的就是这个List类型,也许(当然,需要看业务场景),这并不方便我们对这个List类型中的数据进行处理操作,所以,有没有一种方法,可以我们得到一个集合之后,我们得到的流是这个集合中的所有数据呢?从而变为对集合中所有数据的操作?
有,就是flatMap,我们假设我们的业务需求是需要取出所有Author类中的books属性,books属性是一个List集合,假设我们需要取出所有的List集合并且去重后得到book的name,那么使用map和使用flatMap的差距就很明显了。
package com.stream; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.stream.Collectors; public class Main { public static void main(String[] args) { List<Author> authors = getAuthors(); //会发现其实输出的是一个又一个的List集合 List<Book> books = new ArrayList<>(); authors.stream().map(x->{ return x.getBooks(); }).forEach(x->{ for (Book book : x) { books.add(book); } }); //System.out.println(books); books.stream().distinct().forEach(x->{ System.out.println(x.getName()); }); System.out.println("----------------"); authors.stream().flatMap(x->{ return x.getBooks().stream(); }).distinct().forEach(x-> System.out.println(x.getName())); System.out.println("----------------"); //得到书的分类,并且书的分类不止一种可能 authors.stream() .flatMap(x->{ return x.getBooks().stream(); }).distinct() .flatMap( book->Arrays.stream(book.getCategory().split(","))) .distinct().forEach(System.out::println); } private static List<Author> getAuthors() { //数据初始化 Author author = new Author(1L, "蒙多", 33, "一个从菜刀中明悟哲理的祖安人", null); Author author2 = new Author(2L, "亚拉索", 15, "狂风也追逐不上他的思考速度", null); Author author3 = new Author(3L, "易", 14, "是这个世界在限制他的思维", null); Author author4 = new Author(3L, "易", 14, "是这个世界在限制他的思维", null); //书籍列表 List<Book> books1 = new ArrayList<>(); List<Book> books2 = new ArrayList<>(); List<Book> books3 = new ArrayList<>(); books1.add(new Book(1L, "刀的两侧是光明与黑暗", "哲学,爱情", 88, "用一把刀划分了爱恨")); books1.add(new Book(2L, "一个人不能死在同一把刀下", "个人成长,爱情", 99, "讲述如何从失败中明悟真理")); books2.add(new Book(3L, "那风吹不到的地方", "哲学", 85, "带你用思维去领略世界的尽头")); books2.add(new Book(3L, "那风吹不到的地方", "哲学", 85, "带你用思维去领略世界的尽头")); books2.add(new Book(4L, "吹或不吹", "爱情,个人传记", 56, "一个哲学家的恋爱观注定很难把他所在的时代理解")); books3.add(new Book(5L, "你的剑就是我的剑", "爱情", 56, "无法想象一个武者能对他的伴侣这么的宽容")); books3.add(new Book(6L, "风与剑", "个人传记", 100, "两个哲学家灵魂和肉体的碰撞会激起怎么样的火花呢?")); books3.add(new Book(6L, "风T剑", "个人传记", 100, "两个哲学家灵魂和肉体的碰撞会激起怎么样的火花呢?")); author.setBooks(books1); author2.setBooks(books2); author3.setBooks(books3); author4.setBooks(books3); List<Author> authorList = new ArrayList<>(Arrays.asList(author, author2, author3, author4)); return authorList; } } @Data @NoArgsConstructor @AllArgsConstructor @EqualsAndHashCode public class Author { private Long id; private String name; private Integer age; private String intro; private List<Book> books; } @Data @NoArgsConstructor @AllArgsConstructor @EqualsAndHashCode public class Book { private Long id; private String name; private String category; private Integer score; private String intro; }
Match 匹配
Stream提供了多种匹配操作,允许检测指定的Predicate是否匹配整个Stream。所有的匹配操作都是最终操作,并返回一个boolean类型的值。
代码如下:
boolean anyStartsWithA = stringCollection .stream() .anyMatch((s) -> s.startsWith("a")); System.out.println(anyStartsWithA); // true boolean allStartsWithA = stringCollection .stream() .allMatch((s) -> s.startsWith("a")); System.out.println(allStartsWithA); // false boolean noneStartsWithZ = stringCollection .stream() .noneMatch((s) -> s.startsWith("z")); System.out.println(noneStartsWithZ); // true
Count 计数
计数是一个最终操作,返回Stream中元素的个数,返回值类型是long。
代码如下:
long startsWithB = stringCollection .stream() .filter((s) -> s.startsWith("b")) .count(); System.out.println(startsWithB); // 3
Reduce 规约
这是一个最终操作,允许通过指定的函数来将stream中的多个元素规约为一个元素,规越后的结果是通过Optional接口表示的:
reduce的作用是把stream中的元素组合起来,我们可以传入一个初始值,他会按照我们的计算方式依次拿流中的元素和在初始值的基础上进行计算,计算结果在和后面的元素计算。
代码如下:
Optional reduced = stringCollection .stream() .sorted() .reduce((s1, s2) -> s1 + "#" + s2); reduced.ifPresent(System.out::println); // "aaa1#aaa2#bbb1#bbb2#bbb3#ccc#ddd1#ddd2"
并行Streams
前面提到过Stream有串行和并行两种,串行Stream上的操作是在一个线程中依次完成,而并行Stream则是在多个线程上同时执行。
下面的例子展示了是如何通过并行Stream来提升性能:
首先我们创建一个没有重复元素的大表:
代码如下:
int max = 1000000; List values = new ArrayList<>(max); for (int i = 0; i < max; i++) { UUID uuid = UUID.randomUUID(); values.add(uuid.toString()); }
然后我们计算一下排序这个Stream要耗时多久,
串行排序:
代码如下:
long t0 = System.nanoTime(); long count = values.stream().sorted().count(); System.out.println(count); long t1 = System.nanoTime(); long millis = TimeUnit.NANOSECONDS.toMillis(t1 - t0); System.out.println(String.format("sequential sort took: %d ms", millis)); // 串行耗时: 899 ms
并行排序:
代码如下:
long t0 = System.nanoTime(); long count = values.parallelStream().sorted().count(); System.out.println(count); long t1 = System.nanoTime(); long millis = TimeUnit.NANOSECONDS.toMillis(t1 - t0); System.out.println(String.format("parallel sort took: %d ms", millis)); // 并行排序耗时: 472 ms
上面两个代码几乎是一样的,但是并行版的快了50%之多,唯一需要做的改动就是将stream()改为parallelStream()。
Map
前面提到过,Map类型不支持stream,不过Map提供了一些新的有用的方法来处理一些日常任务。
代码如下:
Map<Integer, String> map = new HashMap<>(); for (int i = 0; i < 10; i++) { map.putIfAbsent(i, "val" + i); } map.forEach((id, val) -> System.out.println(val));
以上代码很容易理解, putIfAbsent 不需要我们做额外的存在性检查,而forEach则接收一个Consumer接口来对map里的每一个键值对进行操作。
下面的例子展示了map上的其他有用的函数:
代码如下:
map.computeIfPresent(3, (num, val) -> val + num); map.get(3); // val33 map.computeIfPresent(9, (num, val) -> null); map.containsKey(9); // false map.computeIfAbsent(23, num -> "val" + num); map.containsKey(23); // true map.computeIfAbsent(3, num -> "bam"); map.get(3); // val33
接下来展示如何在Map里删除一个键值全都匹配的项:
代码如下:
map.remove(3, "val3"); map.get(3); // val33 map.remove(3, "val33"); map.get(3); // null
另外一个有用的方法:
getOrDefault(Object key, V defaultValue)
意思就是当Map集合中有这个key时,就使用这个key对应的value值,如果没有就使用默认值defaultValue
代码如下:
map.getOrDefault(42, "not found"); // not found
对Map的元素做合并也变得很容易了:
代码如下:
map.merge(9, "val9", (value, newValue) -> value.concat(newValue)); map.get(9); // val9 map.merge(9, "concat", (value, newValue) -> value.concat(newValue)); map.get(9); // val9concat
Merge做的事情是如果键名不存在则插入,否则则对原键对应的值做合并操作并重新插入到map中。