Stream流
完整的文章往这跳👉:
从基础到精通,一遍文章读懂 JDK8 Stream流 的使用!
关于Collectors工具类详解往这跳👉:
Collectors 工具类怎样用?看完这篇文章,或许你就懂了~
2.4 细说 Stream 中的用法
大概了解了 Stream流 的一个过程后,让我们来一起当回厂长,去 “流水线” 中的每一个生产车间去检验检验,瞅瞅这个 “流水线” 都能干些什么?
说实话,当这个图弄上来的时候,我都不知道该从哪个先开始…
既然前面 Stream流 初体验 中用到了去重,那么就先从 distinct()
这个先开始吧!
先来搭一些 “前期准备” 吧…
-
基础类
@Data @AllArgsConstructor public class Book { // 书籍名称 private String bookName; // 作者 private String author; // 作者的年龄 private Integer age; // 书的价格 private Integer price; }
我这里用的是 Lombok 中的
@Data
里面包含了:Getter、Setter、RequiredArgsConstructor、ToString、EqualsAndHashCode -
公用的集合 (内容自己定义吧,不重要… 整几个相同的就行)
@Test public List<Book> getBooks(){ List<Book> books = new ArrayList<>(); Collections.addAll(books, new Book("笑傲江湖", "金庸", 99, 365), new Book("笑傲江湖", "金庸", 99, 365), new Book("神雕侠侣", "金庸", 99, 365), new Book("雪中悍刀行","烽火戏诸侯",35,320), new Book("雪中悍刀行","烽火戏诸侯",35,320), new Book("剑来","烽火戏诸侯",35,320), new Book("西游记","吴承恩",56,198), new Book("三国演义","罗贯中",58,230), new Book("水浒传","施耐庵",55,200), new Book("红楼梦","曹雪芹",66,280) ); return books; }
2.4.1 distinct()
先看源码上的解释:
返回不同元素组成的流 (即去重嘛~),规则就是:Object 中的 equals 方法。
所以需要注意的是:当我们实际要去用的时候,是需要重写 hashCode 与 equals 的,它会先比较 hashCode,hashCode 相同时则使用 equals 方法比较 (在 Lombok 就一个 @Data 啥都不用管了)
-
怎么用呢?代码如下:
@Test public void test13(){ List<Book> books = getBooks(); books.stream() .distinct() .forEach(System.out::println); }
-
注意!!一定要重写 hashCode 和 equals ,不然就会失效了。如果使用 Lombok 的小伙伴可以试试把 Book 类中的
@Data
给注释了,加上个@ToString
去看看没有重写的结果就知道了//@Data @ToString @AllArgsConstructor public class Book { private String bookName; private String author; private Integer age; private Integer price; }
再跑一遍,就会看到这个集合原来怎么样,现在也还是怎么样 (
distinct()
失效了)。
2.4.2 sorted()
这个就是排序。在 Stream 中 sorted() 有两个重载方法,一个无参的,默认按自然排序 (即正序排序);另一个需要实现 Comparator 这个函数式接口,自己定义排序规则
Stream<T> sorted();
Stream<T> sorted(Comparator<? super T> comparator);
那么我们就整个场景:按作者的年龄来排序
-
首先来瞅瞅无参的这个,先浅看一波源码
可以看到
this.isNaturalSort = true
是否是自然排序设置了为 true,即下面那里调用个的这个方法:Comparator.naturalOrder()
这个不扯那么远,大概的意思就是无参的这个排序 Stream 中默认使用了自然排序 (也就是正序了)
-
那既然他默认帮咱们做了排序了,那我们直接调来瞅瞅是啥样子的效果?
@Test public void test14(){ List<Book> books = getBooks(); books.stream() .sorted() .forEach(System.out::println); }
运行!映入眼前的是一片红…
不要慌,源码上有一句注释:Will throw CCE when we try to sort if T is not Comparable ,即如果T不可比较,当我们尝试排序时将抛出CCE (也就是当前的ClassCastException)。
确实咱把一个对象 Book 都扔过去了,谁知道你要比较啥?所以我们可以这样做:让 Book 这个类实现的 Comparable 接口 ,通过接口提供的 compareTo 方法来进行排序。
-
所以这个 Book 对象可以这样:
@Data @AllArgsConstructor public class Book implements Comparable<Book>{ private String bookName; private String author; private Integer age; private Integer price; @Override public int compareTo(@NotNull Book o) { return age - o.age; } }
-
再次运行,你会发现运行成功,且效果也与预期一样
或许有些好兄弟已经发现了!咱们这样做,说到底还是通过实现了 Comparable接口 从而自定义了排序的规则,似乎和
sorted()
没啥关系。说白了,就是一顿 “脱裤子放屁” 的 XX 操作…确实,当前的场景下用这个无参的去排序本身就是不太合理的,它感觉更适合在某些较为单一的数据中去使用。所以在不一样的场景下,使用不一样的方法是很有必要的
-
在 Stream 中还给我们提供了另一个方法,那么此时我们就可以这样做了:
@Test public void test14(){ List<Book> books = getBooks(); books.stream() .sorted(Comparator.comparingInt(Book::getAge)) .forEach(System.out::println); }
即在
sorted(Comparator<? super T> comparator)
这个方法中里面是要一个 Comparator 的实现,而这个又是一个函数式接口,所以可以在里面使用 Lambda表达式,具体可以看我上一篇文章运行的结果也是一样的,就不粘出来了
2.4.3 filter()
条件过滤,根据具体的条件去过滤数据,而不满足的则会被剔除掉。
Stream<T> filter(Predicate<? super T> predicate);
直接看一个 Demo 吧:最近资金紧缺,过滤掉大于300元的书,买不起
@Test
public void test15(){
List<Book> books = getBooks();
books.stream()
.filter(book -> book.getPrice() <= 300)
.forEach(System.out::println);
}
很简单的一个方法,想要实现怎样的效果,只需在方法中去定义自己的放回就可以了
2.4.4 forEach()
这个就不用多说啦~老熟客了,这个方法要传入一个 Consumer 函数接口对象,没有返回值
void forEach(Consumer<? super T> action);
当然,这个需要说明一下的是,该方法的在执行操作的时候,行为是不确定的,即在并行操作里面,该方法不能保证按顺序执行
感兴趣的朋友可以自己写个 Demo 用并行流去试一下,当数据量大的时候应该可以看出变化
2.4.5 forEachOrdered()
这个其实与 forEach()
是一样的,只是不同的是,该方法保证在串行流或者是并行流中都能够保证元素按顺序执行
void forEachOrdered(Consumer<? super T> action);
2.4.6 count()
这个也是在 Stream 流中非常简单的一个,它的作用:用于统计流中的元素数量,返回的 long 类型
long count();
2.4.7 max() & min()
这两个就跟孪生兄弟一样,相差不大。只是一个 获取流中最大的元素 ,一个则 获取流中最小的元素
Optional<T> max(Comparator<? super T> comparator);
Optional<T> min(Comparator<? super T> comparator);
这两个方法都需要传入一个 Comparator 函数式接口对象,该接口中有一个compare方法,用于自定义大小比较规则。
那么要怎样去用呢?这个与前面的 sorted 那里的也是一样的,大同小异。
-
找出哪本书价格最高
@Test public void test16(){ List<Book> books = getBooks(); Book book = books.stream() .max(Comparator.comparing(Book::getPrice)) .get(); System.out.println("book = " + book); }
-
找出哪本书价格最低也是一样的,max 变成了 min 而已
@Test public void test16(){ List<Book> books = getBooks(); Book book = books.stream() .min(Comparator.comparing(Book::getPrice)) .get(); System.out.println("book = " + book); }
-
或者说你有一串数字,要找出最大值或者最小值
@Test public void test17() { Integer num = Stream.of(1, 2, 3, 4, 5, 6, 7, 18, 9, 10) .max(Integer::compareTo) .get(); System.out.println("num = " + num); // num = 18 }
或许你会有疑问,
Integer::compareTo
这是啥?在 Integer 源码中可以看到 Integer 是实现了
Comparable<>
接口的,当然也重写了其compareTo()
方法public int compareTo(Integer anotherInteger) { return compare(this.value, anotherInteger.value); } public static int compare(int x, int y) { return (x < y) ? -1 : ((x == y) ? 0 : 1); }
2.4.8 reduce()
reduce()
是一种聚合操作,即将多个值经过特定的计算后获取到的单个值。像上面已经介绍了的 count()
、max()
、min()
等都是聚合操作。
就像刚说的,你会发现其都是将流中多个值经过特定的计算后得到的单个值
reduce()
有三个重载方法
Optional<T> reduce(BinaryOperator<T> accumulator);
T reduce(T identity, BinaryOperator<T> accumulator);
<U> U reduce(U identity,
BiFunction<U, ? super T, U> accumulator,
BinaryOperator<U> combiner);
Optional reduce(BinaryOperator accumulator);
这个与其他两个相比,应该是最为常用的一个了。和上面几个聚合函数一样,该函数的返回值也是 Optional 对象,因为结果存在空指针的情况。
-
求集合中元素的和
@Test public void test18(){ int i = Stream.of(1, 2, 3, 4, 5, 6, 7, 18, 9, 10) .reduce((x, y) -> x + y) .get(); System.out.println("i = " + i); // i = 65 }
即大概是这个意思,当然下面的相乘也是这个道理。最终的目的都还是将多个值经过特定的计算后获取到的单个值
-
求集合中元素的积
@Test public void test18(){ int i = Stream.of(1, 2, 3, 4, 5) .reduce((x, y) -> x * y) .get(); System.out.println("i = " + i); // i = 120 }
如果说想要更清晰的知道它是怎样执行的,也可以 DEBUG 去瞅瞅,或者像这个打印一下出来
@Test public void test18(){ int i = Stream.of(1, 2, 3, 4, 5, 6, 7, 18, 9, 10) .reduce((x, y) -> { System.out.println("x = " + x + " " + "y =" + y); return x + y; }).get(); System.out.println("i = " + i); // i = 65 }
T reduce(T identity, BinaryOperator accumulator);
T identity
类似于是一个默认值,即当集合为空时,就返回这个默认值;当然如果集合不为空时,这个值也会参与到计算当中
-
还是上面那个例子,就集合中元素的和
@Test public void test19(){ int i = Stream.of(1, 2, 3, 4, 5, 6, 7, 18, 9, 10) .reduce(0, (x, y) -> x + y); System.out.println("i = " + i); // i = 65 }
但是,但集合为空时,那么
T identity
这个就是充当默认值的角色了@Test public void test19(){ List<Integer> list = new ArrayList<>(); int i = list .stream() .reduce(0, (x, y) -> x + y); System.out.println("i = " + i); // i = 0 }
那么集合不为空时,这个值也会参与到计算当中又当如何理解呢?试一下就知道了
@Test public void test19(){ int i = Stream.of(1, 2, 3, 4, 5, 6, 7, 18, 9, 10) .reduce(5, (x, y) -> x + y); System.out.println("i = " + i); // i = 70 int j = Stream.of(1, 2, 3, 4, 5, 6, 7, 18, 9, 10) .reduce(3, (x, y) -> x + y); System.out.println("j = " + j); // j = 68 }
U reduce(U identity, BiFunction<U, ? super T, U> accumulator, BinaryOperator combiner);
这个就稍微复杂很多了。
在方法中我们可以看到有两个泛型:T 和 U ,其中 T 是集合中的元素类型;而 U 是计算之后返回结果的类型,即 U 的类型是由第一个参数 identity 决定的
即该方法可以返回与集合中元素不同类型的值,而前两个方法则只能返回与集合中元素相同的值
像这样:
@Test
public void test21(){
List<Book> books = Stream.of(
new Book("剑来", "烽火", 38, 100),
new Book("斗破", "土豆", 34, 60),
new Book("完美", "辰东", 37, 70)
).collect(Collectors.toList());
List<Integer> reduce = books.stream().reduce(new ArrayList<Integer>(), new BiFunction<ArrayList<Integer>, Book, ArrayList<Integer>>() {
@Override
public ArrayList<Integer> apply(ArrayList<Integer> integers, Book book) {
integers.add(book.getPrice());
System.out.println("list = " + integers);
System.out.println("bookPrice = " + book.getPrice());
return integers;
}
}, new BinaryOperator<ArrayList<Integer>>() {
@Override
public ArrayList<Integer> apply(ArrayList<Integer> integers1, ArrayList<Integer> integers2) {
integers1.addAll(integers2);
System.out.println("integers1 = " + integers1);
System.out.println("integers2 = " + integers2);
return integers1;
}
});
System.out.println("finalList = " + reduce);
}
运行之后的输出结果:
list = [100]
bookPrice = 100
list = [100, 60]
bookPrice = 60
list = [100, 60, 70]
bookPrice = 70
finalList = [100, 60, 70]
可以看到,经过一顿操作之后类型发生了转变,这个是没问题的;但有没有发现,似乎 BinaryOperator
中的方法没有打印出来?那让我们再看一个:
@Test
public void test20(){
List<Book> books = Stream.of(
new Book("剑来", "烽火", 38, 100),
new Book("斗破", "土豆", 34, 60),
new Book("完美", "辰东", 37, 70)
).collect(Collectors.toList());
Integer i = books.parallelStream().reduce(0, new BiFunction<Integer, Book, Integer>() {
@Override
public Integer apply(Integer integer, Book book) {
System.out.println("线程 " + Thread.currentThread().getId() + " ===> " + "integer = " + integer);
System.out.println("线程 " + Thread.currentThread().getId() + " ===> " + "bookPrice = " + book.getPrice());
return integer + book.getPrice();
}
}, new BinaryOperator<Integer>() {
@Override
public Integer apply(Integer integer1, Integer integer2) {
System.out.println("线程 " + Thread.currentThread().getId() + " ===> " + "integer1 = " + integer1);
System.out.println("线程 " + Thread.currentThread().getId() + " ===> " + "integer2 = " + integer2);
return integer1 + integer2;
}
});
System.out.println("i = " + i);
}
把 stream()
换成了 parallelStream()
,即从串行流换成并行流,并且在打印前面加上当前线程的 ID ,运行后如下:
线程 1 ===> integer = 0
线程 17 ===> integer = 0
线程 16 ===> integer = 0
线程 16 ===> bookPrice = 100
线程 17 ===> bookPrice = 70
线程 1 ===> bookPrice = 60
线程 1 ===> integer1 = 60
线程 1 ===> integer2 = 70
线程 1 ===> integer1 = 100
线程 1 ===> integer2 = 130
i = 230
那么为什么 BinaryOperator
没有执行呢?这是因为 Stream 是支持并发操作的,为了避免竞争,对于 reduce 这个方法,线程都会有独立的 result,即 BinaryOperator combiner
的作用在于汇总所有线程的计算结果,从而得到一个最终的 result
上述可以多运行几次或者多加一些数据量去观察规律,进程在多线程中都会争夺时间片,因此每次运行的结果不一定相同
2.4.9 limit() & skip()
- limit :限制,截取流中指定数量的元素
- skip :跳过,跳过流中指定数量的元素
-
获取最便宜的3本书
@Test public void test22(){ List<Book> books = getBooks(); books.stream() .sorted(Comparator.comparing(Book::getPrice)) // 排序 .limit(3) // 截取 .forEach(System.out::println); // 打印输出 }
-
跳过也是一样的,跳过最贵的5本书
@Test public void test22(){ List<Book> books = getBooks(); books.stream() .sorted(Comparator.comparing(Book::getPrice,Comparator.reverseOrder())) // 排序 .skip(5) // 截取 .forEach(System.out::println); // 打印输出 }
这两个比较简单,直接对流中的元素使用即可
2.4.10 allMatch() & anyMatch() & noneMatch()
这三个算是一组的,返回的都是布尔类型,用于对一些逻辑的判断。
boolean allMatch(Predicate<? super T> predicate);
boolean anyMatch(Predicate<? super T> predicate);
boolean noneMatch(Predicate<? super T> predicate);
- allMatch() 当流中的元素都按指定的规则匹配上,才会返回 true (即全部对才对)
- anyMatch() 当流中有任意元素满足指定的规则时,返回 true (即对一个就对了)
- noneMatch() 当流中所有元素都没有与指定的规则匹配上,才会返回 true (即全部错了才是对的)
-
是否所有的书都大于60元
@Test public void test23(){ List<Book> books = Stream.of( new Book("剑来", "烽火", 38, 100), new Book("斗破", "土豆", 34, 60), new Book("完美", "辰东", 37, 70), new Book("斗罗", "三少", 36, 80) ).collect(Collectors.toList()); boolean b = books.stream().allMatch(book -> book.getPrice() > 60); System.out.println("是否所有的书都大于60元 = " + b); // false }
allMatch() 所有都满足,返回 true,否之则 false
-
是否有高于60元的书
@Test public void test23(){ List<Book> books = Stream.of( new Book("剑来", "烽火", 38, 100), new Book("斗破", "土豆", 34, 60), new Book("完美", "辰东", 37, 70), new Book("斗罗", "三少", 36, 80) ).collect(Collectors.toList()); boolean b = books.stream().anyMatch(book -> book.getPrice() > 60); System.out.println("是否有高于60元的书 = " + b); // true }
anyMatch() 有一个满足,返回 true,否之则 false
-
第三个的例子其实有点难举,因为个人感觉有点变扭。
@Test public void test23(){ List<Book> books = Stream.of( new Book("剑来", "烽火", 38, 100), new Book("斗破", "土豆", 34, 60), new Book("完美", "辰东", 37, 70), new Book("斗罗", "三少", 36, 80) ).collect(Collectors.toList()); boolean b = books.stream().noneMatch(book -> book.getPrice() > 110); System.out.println("b = " + b); // true }
这个有点像是与
allMatch()
反着来,只有当流中的所有元素,都不满足指定的规则时,才会返回 true,否之则 false 。这个多调整几次数据,运行几遍看看效果便能琢磨出规律。
2.4.11 map()
<R> Stream<R> map(Function<? super T, ? extends R> mapper);
这个方法提供了一个映射规则,对流中的数据进行映射,从而用新的数据替换旧的数据
这个方法的使用频率很高,而且也非常的实用
-
获取书的名称
@Test public void test25(){ List<Book> books = Stream.of( new Book("剑来", "烽火", 38, 100), new Book("斗破", "土豆", 34, 60), new Book("完美", "辰东", 37, 70), new Book("斗罗", "三少", 36, 80) ).collect(Collectors.toList()); books.stream() .map(Book::getBookName) .forEach(System.out::println); }
-
当然你也可以这样,不过这里需要把你想要的数据 return 出去
@Test public void test25(){ List<Book> books = Stream.of( new Book("剑来", "烽火", 38, 100), new Book("斗破", "土豆", 34, 60), new Book("完美", "辰东", 37, 70), new Book("斗罗", "三少", 36, 80) ).collect(Collectors.toList()); books.stream() .map(book -> { String bookName = book.getBookName(); String author = book.getAuthor(); return bookName + "---" + author; }) .forEach(System.out::println); }
2.4.12 flatMap()
扁平化映射,其实本质上与 map() 是一样的,都是对流中的元素进行一定的处理然后再返回出来
<R> Stream<R> flatMap(Function<? super T, ? extends Stream<? extends R>> mapper);
那么什么时候使用 flatMap() 什么时候又使用 map() 呢?
网上大佬一句话总结:多层数据结构转单层时用 flatmap(),单层转单层或者多层转多层用 map()
-
那么我们试一下在集合里面放数组是个怎样的效果
@Test public void test26() { List<String[]> collect = Stream.of( new String[]{"a,b,c"}, new String[]{"d,e,f"}, new String[]{"g,h,j"} ).collect(Collectors.toList()); collect.stream() .flatMap(new Function<String[], Stream<?>>() { @Override public Stream<?> apply(String[] strings) { // 可以看到,流中的元素是数组。那么我们可以将数组装换成 Stream 类型来处理 return Arrays.stream(strings); } }) .forEach(System.out::println); }
我们以往处理的时候,在集合中只有一种数据结构;而此时集合里面放的是数组,那么此时这个 扁平化 这个操作就用的上了
先对集合中的数据结构作处理,然后再进行数据处理
-
也还可以这样玩一下:把里面的字母变成大写
@Test public void test27() { List<String[]> collect = Stream.of( new String[]{"a,b,c"}, new String[]{"d,e,f"}, new String[]{"g,h,j"} ).collect(Collectors.toList()); collect.stream() .flatMap(Arrays::stream) .map(String::toUpperCase) .forEach(System.out::println); }
这里用的是 Lambda 表达式的形式
2.4.13 mapToInt() & mapToDouble() & mapToLong()
IntStream mapToInt(ToIntFunction<? super T> mapper);
DoubleStream mapToDouble(ToDoubleFunction<? super T> mapper);
LongStream mapToLong(ToLongFunction<? super T> mapper);
这几个方法比较好玩,将流中的元素转换成了 IntStream/DoubleStream/LongStream 从而可以进行一些运算,比如:获取数据最大值、最小值、数据量、平均值等
-
获取数据最大值
@Test public void test28() { int i = Stream.of(1, 2, 3, 4, 5, 6, 7, 18, 9, 10) .mapToInt(value -> value) .max() .getAsInt(); System.out.println("i = " + i); // 18 }
-
获取数据的和
@Test public void test28() { int i = Stream.of(1, 2, 3, 4, 5, 6, 7, 18, 9, 10) .mapToInt(value -> value) .sum(); System.out.println("i = " + i); // 65 }
-
获取数据的平均值
@Test public void test28() { double i = Stream.of(1, 2, 3, 4, 5, 6, 7, 18, 9, 10) .mapToInt(value -> value) .average() .getAsDouble(); System.out.println("i = " + i); // 6.5 }
-
也可以对这组数据进行分析
@Test public void test28() { IntSummaryStatistics statistics = Stream.of(1, 2, 3, 4, 5, 6, 7, 18, 9, 10) .mapToInt(value -> value) .summaryStatistics(); System.out.println("数据分析 = " + statistics); // 数据分析 : IntSummaryStatistics{count=10, sum=65, min=1, average=6.500000, max=18} }
这个一组与之对应的还有一些 扁平化 的操作:
IntStream flatMapToInt(Function<? super T, ? extends IntStream> mapper);
DoubleStream flatMapToDouble(Function<? super T, ? extends DoubleStream> mapper);
LongStream flatMapToLong(Function<? super T, ? extends LongStream> mapper);
本质上是与上面是一样的,什么时候用哪个,围绕这句话即可: 多层数据结构转单层时用 flat 的,单层转单层或者多层转多层用普通的
2.4.14 collect()
这个可能是用的最多的几个方法之一了吧
<R> R collect(Supplier<R> supplier,
BiConsumer<R, ? super T> accumulator,
BiConsumer<R, R> combiner);
<R, A> R collect(Collector<? super T, A, R> collector);
collect()
多参的方法,我似乎没见人用过。不过在源码上写了一两个例子可以瞅瞅
// 将字符串累积到ArrayList中 stringStream 假设该流是存在的
List<String> asList = stringStream.collect(ArrayList::new, ArrayList::add, ArrayList::addAll);
// 将获取一个字符串流并将它们连接成一个字符串 stringStream 假设该流是存在的
String concat = stringStream.collect(StringBuilder::new, StringBuilder::append, StringBuilder::append).toString();
用的最多的还是这个 collect()
单参的方法 ,那么就不得不说一下 Collectors