Java8 Stream
概述
Stream 是 Java8 中处理集合的关键抽象概念,它可以指定你希望对集合进行的操作,可以执行非常复杂的查找、过滤和映射数据等操作。使用Stream API 对集合数据进行操作,就类似于使用 SQL 执行的数据库查询。也可以使用 Stream API 来并行执行操作。简而言之,Stream API 提供了一种高效且易于使用的处理数据的方式。
Stream 不是集合元素,它不是数据结构并不保存数据,它是有关算法和计算的,它更像一个高级版本的 Iterator。原始版本的 Iterator,用户只能显式地一个一个遍历元素并对其执行某些操作;高级版本的 Stream,用户只要给出需要对其包含的元素执行什么操作,比如 “过滤掉长度大于 10 的字符串”、“获取每个字符串的首字母”等,Stream 会隐式地在内部进行遍历,做出相应的数据转换。
Stream 就如同一个迭代器(Iterator),单向,不可往复,数据只能遍历一次,遍历过一次后即用尽了,就好比流水从面前流过,一去不复返。
而和迭代器又不同的是,Stream 可以并行化操作,迭代器只能命令式地、串行化操作。顾名思义,当使用串行方式去遍历时,每个 item 读完后再读下一个 item。而使用并行去遍历时,数据会被分成多个段,其中每一个都在不同的线程中处理,然后将结果一起输出。Stream 的并行操作依赖于 Java7 中引入的 Fork/Join 框架(JSR166y)来拆分任务和加速处理过程。
Stream 的另外一大特点是,数据源本身可以是无限的。
特点:
-
不是数据结构,不会保存数据。
-
不会修改原来的数据源,它会将操作后的数据保存到另外一个对象中。(保留意见:毕竟peek方法可以修改流中元素)
-
惰性求值,流在中间处理过程中,只是对操作进行了记录,并不会立即执行,需要等到执行终止操作的时候才会进行实际的计算。
-
流的操作并不是一个一个链式的执行的。而是先拿出来一个元素,执行所有的操作。执行完毕之后,再拿出来一个元素进行下一次操作。
使用步骤:
当我们使用一个流的时候,通常包括三个基本步骤:
获取一个数据源(source)→ 数据转换 → 执行操作获取想要的结果,每次转换原有 Stream 对象不改变,返回一个新的 Stream 对象(可以有多次转换),这就允许对其操作可以像链条一样排列,变成一个管道。
- 创建Stream。
- 中间操作:通过一系列中间(Intermediate)方法,对数据集进行过滤、检索等数据集的再次处理。
- 终端操作:通过最终(terminal)方法完成对数据集中元素的处理。
流的操作类型:
对流的操作分为两种:
-
Intermediate(中间操作):一个流可以后面跟随零个或多个 intermediate 操作。其目的主要是打开流,做出某种程度的数据映射/过滤,然后返回一个新的流,交给下一个操作使用。这类操作都是惰性化的(lazy),就是说,仅仅调用到这类方法,并没有真正开始流的遍历。
-
Terminal(终端操作、终止操作):一个流只能有一个 terminal 操作,当这个操作执行后,流就被使用“光”了,无法再被操作。所以这必定是流的最后一个操作。Terminal 操作的执行,才会真正开始流的遍历,并且会生成一个结果(新的集合或值),或者一个副作用(side effect)。
在对于一个 Stream 进行多次转换操作 (Intermediate 操作),每次都对 Stream 的每个元素进行转换,而且是执行多次,这样时间复杂度就是 N(转换次数)个 for 循环里把所有操作都做掉的总和吗?其实不是这样的,转换操作都是 lazy 的,多个转换操作只会在 Terminal 操作的时候融合起来,一次循环完成。我们可以这样简单的理解,Stream 里有个操作函数的集合,每次转换操作就是把转换函数放入这个集合中,在 Terminal 操作的时候循环 Stream 对应的集合,然后对每个元素执行所有的函数。
还有一种操作被称为 short-circuiting 。用以指:
- 对于一个 intermediate 操作,如果它接受的是一个无限大(infinite/unbounded)的 Stream,但返回一个有限的新 Stream。
- 对于一个 terminal 操作,如果它接受的是一个无限大的 Stream,但能在有限的时间计算出结果。
当操作一个无限大的 Stream,而又希望在有限时间内完成操作,则在管道内拥有一个 short-circuiting 操作是必要非充分条件。
常见的操作可以归类如下。
- Intermediate:
map (mapToInt, flatMap 等)、 filter、 distinct、 sorted、 peek、 limit、 skip、 parallel、 sequential、 unordered
- Terminal:
forEach、 forEachOrdered、 toArray、 reduce、 collect、 min、 max、 count、 anyMatch、 allMatch、 noneMatch、 findFirst、 findAny、 iterator
- Short-circuiting:
anyMatch、 allMatch、 noneMatch、 findFirst、 findAny、 limit
创建 Stream
从 Collection 和数组
Collection.stream()
List<String> list = Arrays.asList("a", "b", "c");
// 创建一个顺序流
Stream<String> stream = list.stream();
Collection.parallelStream()
List<String> list = Arrays.asList("a", "b", "c");
// 创建一个并行流
Stream<String> parallelStream = list.parallelStream();
如果流中的数据量足够大,并行流可以加快处速度。
除了直接创建并行流,还可以通过parallel()
把顺序流转换成并行流。
Optional<Integer> findFirst = list.stream().parallel().filter(x->x>6).findFirst();
Arrays.stream(T array)
int[] array = {1,3,5,6,8};
IntStream stream = Arrays.stream(array);
从Stream接口的静态工厂方法
Stream.of()
of方法,其生成的Stream是有限长度的,Stream的长度为其内的元素个数。
public static<T> Stream<T> of(T... values) {
return Arrays.stream(values);
}
public static<T> Stream<T> of(T t) {
return StreamSupport.stream(new Streams.StreamBuilderImpl<>(t), false);
}
Stream<Integer> integerStream = Stream.of(1, 2, 3);
Stream<String> stringStream = Stream.of("A");
Stream.generate()
generator方法,返回一个无限长度的Stream,其元素由Supplier接口的提供。在Supplier是一个函数接口,只封装了一个get()方法,其用来返回任何泛型的值,该结果在不同的时间内,返回的可能相同也可能不相同,没有特殊的要求。
public static<T> Stream<T> generate(Supplier<T> s) {
Objects.requireNonNull(s);
return StreamSupport.stream(
new StreamSpliterators.InfiniteSupplyingSpliterator.OfRef<>(Long.MAX_VALUE, s), false);
}
- 这种情形通常用于随机数、常量的 Stream,或者需要前后元素间维持着某种状态信息的 Stream。
- 把 Supplier 实例传递给 Stream.generate() 生成的 Stream,默认是串行(相对 parallel 而言)但无序的(相对 ordered 而言)。
Stream<Double> generateA = Stream.generate(new Supplier<Double>() {
@Override
public Double get() {
return java.lang.Math.random();
}
});
Stream<Double> generateB = Stream.generate(()-> java.lang.Math.random());
Stream<Double> generateC = Stream.generate(java.lang.Math::random);
以上三种形式达到的效果是一样的,只不过是下面的两个采用了Lambda表达式,简化了代码,其实际效果就是返回一个随机值。一般无限长度的Stream会与filter、limit等配合使用,否则Stream会无限制的执行下去,后果可想而知,如果你有兴趣,不妨试一下。
Stream.iterate()
iterate方法,其返回的也是一个无限长度的Stream,与generate方法不同的是,其是通过函数f迭代对给指定的元素种子而产生无限连续有序Stream,其中包含的元素可以认为是:seed,f(seed), f(f(seed))无限循环。
public static<T> Stream<T> iterate(final T seed, final UnaryOperator<T> f) {
Objects.requireNonNull(f);
final Iterator<T> iterator = new Iterator<T>() {
@SuppressWarnings("unchecked")
T t = (T) Streams.NONE;
@Override
public boolean hasNext() {
return true;
}
@Override
public T next() {
return t = (t == Streams.NONE) ? seed : f.apply(t);
}
};
return StreamSupport.stream(Spliterators.spliteratorUnknownSize(
iterator,
Spliterator.ORDERED | Spliterator.IMMUTABLE), false);
}
Stream.iterate(1, item -> item + 1)
.limit(10)
.forEach(System.out::println);
// 打印结果:1,2,3,4,5,6,7,8,9,10
上面示例,种子为1,也可认为该Stream的第一个元素,通过f函数来产生第二个元素。接着,第二个元素,作为产生第三个元素的种子,从而产生了第三个元素,以此类推下去。需要主要的是,该Stream也是无限长度的,应该使用filter、limit等来截取Stream,否则会一直循环下去。
Stream.empty()
empty方法返回一个空的顺序Stream,该Stream里面不包含元素项。
public static<T> Stream<T> empty() {
return StreamSupport.stream(Spliterators.<T>emptySpliterator(), false);
}
从 BufferedReader
BufferedReader.lines()
使用 BufferedReader.lines() 方法,将每行内容转成流。
BufferedReader reader = new BufferedReader(new FileReader("F:\\test_stream.txt"));
Stream<String> lineStream = reader.lines();
lineStream.forEach(System.out::println);
其他
- Random.ints()
- BitSet.stream()
- Pattern.splitAsStream(java.lang.CharSequence)
- JarFile.stream()
Pattern.splitAsStream()
使用 Pattern.splitAsStream() 方法,将字符串分隔成流。
Pattern pattern = Pattern.compile(",");
Stream<String> stringStream = pattern.splitAsStream("a,b,c,d");
stringStream.forEach(System.out::println)
// 打印结果:a b c d
中间操作
筛选与切片
filter
filter:过滤流中的某些元素。filter 对原始 Stream 进行某项测试,通过测试的元素被留下来生成一个新 Stream。
Stream<T> filter(Predicate<? super T> predicate);
eg: 留下偶数
Integer[] sixNums = {1, 2, 3, 4, 5, 6};
Integer[] evens = Stream.of(sixNums).filter(n -> n%2 == 0).toArray(Integer[]::new);
// 经过条件“被 2 整除”的 filter,剩下的数字为 {2, 4, 6}。
limit
limit(n):获取n个元素。limit 返回 Stream 的前面 n 个元素。
Stream<T> limit(long maxSize);
Stream<Integer> stream = Stream.of(6, 4, 6, 7, 3, 9, 8, 10, 12, 14, 14);
Stream<Integer> newStream = stream.limit(4);
newStream.forEach(System.out::println);
// 打印结果:6 4 6 7
skip
skip(n):跳过n元素,skip 则是扔掉前 n 个元素,配合limit(n)可实现分页。
skip方法将过滤掉原Stream中的前N个元素,返回剩下的元素所组成的新Stream。如果原Stream的元素个数大于N,将返回原Stream的后(原Stream长度-N)个元素所组成的新Stream;如果原Stream的元素个数小于或等于N,将返回一个空Stream。
Stream<T> skip(long n);
Stream<Integer> stream = Stream.of(6, 4, 6, 7, 3, 9, 8, 10, 12, 14, 14);
Stream<Integer> newStream = stream.skip(4);
newStream.forEach(System.out::println);
// 打印结果:3 9 8 10 12 14 14
skip 和 limit 结合使用
Stream<Integer> stream = Stream.of(6, 4, 6, 7, 3, 9, 8, 10, 12, 14, 14);
Stream<Integer> newStream = stream.limit(7).skip(4);
newStream.forEach(System.out::println);
// 打印结果:3 9 8
Stream<Integer> stream = Stream.of(6, 4, 6, 7, 3, 9, 8, 10, 12, 14, 14);
Stream<Integer> newStream = stream.skip(4).limit(2);
newStream.forEach(System.out::println);
// 打印结果:3 9
skip 和 limit 实现分页
list = list.stream().skip((pageNo - 1) * pageSize).limit(pageSize).collect(Collectors.toList())
distinct
distinct:通过流中元素的 hashCode() 和 equals() 去除重复元素。
Stream<T> distinct();
Stream<Integer> stream = Stream.of(6, 4, 6, 7, 3, 9, 8, 10, 12, 14, 14);
Stream<Integer> newStream = stream.distinct();
newStream.forEach(System.out::println);
// 打印结果:6 4 7 3 9 8 10 12 14
映射
映射,可以将一个流的元素按照一定的映射规则映射到另一个流中。分为map
和flatMap
:
map
map:接收一个函数作为参数,该函数会被应用到每个元素上,并将其映射成一个新的元素。
map方法将对于Stream中包含的元素使用给定的转换函数进行转换操作,新生成的Stream只包含转换生成的元素。为了提高处理效率,官方已封装好了,三种变形:mapToDouble,mapToInt,mapToLong。其实很好理解,如果想将原Stream中的数据类型,转换为double, int或者是long是可以调用相对应的方法。
<R> Stream<R> map(Function<? super T, ? extends R> mapper);
DoubleStream mapToDouble(ToDoubleFunction<? super T> mapper);
IntStream mapToInt(ToIntFunction<? super T> mapper);
LongStream mapToLong(ToLongFunction<? super T> mapper);
转换大写
String[] strArr = { "abcd", "bcdd", "defde", "fTr" };
List<String> strList = Arrays.stream(strArr).map(String::toUpperCase).collect(Collectors.toList());
// 打印结果:[ABCD, BCDD, DEFDE, FTR]
平方数
List<Integer> nums = Arrays.asList(1, 2, 3, 4);
List<Integer> squareNums = nums.stream().map(n -> n * n).collect(Collectors.toList());
// 打印结果:[1, 4, 9, 16]
flatMap
flatMap:接收一个函数作为参数,将流中的每个值都换成另一个流,然后把所有流连接成一个流。
map 生成的是个 1:1 映射,每个输入元素,都按照规则转换成为另外一个元素。还有一些场景,是一对多映射关系的,这时需要 flatMap。
flatMap方法与map方法类似,都是将原Stream中的每一个元素通过转换函数转换,不同的是,该换转函数的对象是一个Stream,也不会再创建一个新的Stream,而是将原Stream的元素取代为转换的Stream。如果转换函数生产的Stream为null,应由空Stream取代。flatMap有三个对于原始类型的变种方法,分别是:flatMapToInt,flatMapToLong和flatMapToDouble。
<R> Stream<R> flatMap(Function<? super T, ? extends Stream<? extends R>> mapper);
DoubleStream flatMapToDouble(Function<? super T, ? extends DoubleStream> mapper);
IntStream flatMapToInt(Function<? super T, ? extends IntStream> mapper);
LongStream flatMapToLong(Function<? super T, ? extends LongStream> mapper);
eg1:
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());
outputStream.forEach(System.out::println);
// 打印结果:1 2 3 4 5 6
flatMap 把 input Stream 中的层级结构扁平化,就是将最底层元素抽出来放到一起,最终 output 的新 Stream 里面已经没有 List 了,都是直接的数字。
eg2:
List<Integer> nums = Arrays.asList(1, 2, 3, 4);
Stream<Integer> outputStream = nums.stream().flatMap(n -> Stream.of(n, 6*n));
outputStream.forEach(System.out::println);
// 打印结果:1 6 2 12 3 18 4 24
排序
sorted
对 Stream 的排序通过 sorted 进行,它比数组的排序更强之处在于你可以首先对 Stream 进行各类 map、filter、limit、skip 甚至 distinct 来减少元素数量后,再排序,这能帮助程序明显缩短执行时间。
sorted():自然排序,流中元素需实现Comparable接口
Stream<T> sorted();
// 自然正序
Stream.of(5, 4, 3, 2, 1).sorted().forEach(System.out::println);
// 打印结果:1 2 3 4 5
Arrays.asList("aa", "ff", "dd").stream().sorted().forEach(System.out::println);
// 打印结果:aa dd ff
// 自然逆序
Stream.of(5, 4, 3, 2, 1).sorted(Comparator.reverseOrder()).forEach(System.out::println);
// 打印结果:5 4 3 2 1
Arrays.asList("aa", "ff", "dd").stream().sorted(Comparator.reverseOrder()).forEach(System.out::println);
// 打印结果:ff dd aa
sorted(Comparator com):定制排序,自定义Comparator排序器
Stream<T> sorted(Comparator<? super T> comparator);
// 定制排序,正序
list.stream().sorted(Comparator.comparing(User::getAge)).forEach(System.out::println);
// 定制排序,逆序(注意两种写法对比)
list.stream().sorted(Comparator.comparing(User::getAge).reversed()).forEach(System.out::println); //先以属性升序, 结果进行属性降序
list.stream().sorted(Comparator.comparing(User::getAge, Comparator.reverseOrder())).forEach(System.out::println); //以属性降序
按多字段排序
注意以下写法对比
list.stream().sorted(Comparator.comparing(User::getAge).reversed().thenComparing(User::getId).reversed()).forEach(System.out::println);//先以属性一升序,升序结果进行属性一降序,再进行属性二升序,结果进行属性一降序属性二降序,最终结果是属性一升序、属性二降序,并非想要的属性一降序、属性二降序。
list.stream().sorted(Comparator.comparing(User::getAge).reversed().thenComparing(User::getId, Comparator.reverseOrder())).forEach(System.out::println);//先以属性一升序,升序结果进行属性一降序,再进行属性二降序
list.stream().sorted(Comparator.comparing(User::getAge, Comparator.reverseOrder()).thenComparing(User::getId, Comparator.reverseOrder())).forEach(System.out::println); //先以属性一降序,再进行属性二降序
通过以上例子我们可以发现
-
Comparator.comparing(类::属性一).reversed();
-
Comparator.comparing(类::属性一, Comparator.reverseOrder());
两种排序是完全不一样的,一定要区分开来。 ① 是得到排序结果后再排序,② 是直接进行排序
很多人会混淆导致理解出错,② 更好理解,建议使用 ②
消费
peek
peek:如同于map,能得到流中的每一个元素。但map接收的是一个Function表达式,有返回值;而peek接收的是Consumer表达式,没有返回值。
Stream<T> peek(Consumer<? super T> action);
peek主要被用在debug用途。
Stream.of("one", "two", "three","four").filter(e -> e.length() > 3)
.peek(e -> System.out.println("Filtered value: " + e))
.map(String::toUpperCase)
.peek(e -> System.out.println("Mapped value: " + e))
.collect(Collectors.toList());
上面的例子输出:
Filtered value: three
Mapped value: THREE
Filtered value: four
Mapped value: FOUR
上面的例子我们输出了stream的中间值,方便我们的调试。
终端操作
终端操作会从流的流水线生成结果。其结果可以是任何不是流的值,例如:List、Integer,甚至是 void 。
查找与匹配
allMatch
检查是否匹配所有元素,接收一个 Predicate 函数,当流中每个元素都符合该断言时才返回true,否则返回false。
boolean allMatch(Predicate<? super T> predicate);
List<Integer> nums = Arrays.asList(1, 2, 3, 4);
boolean b1 = nums.stream().allMatch(e -> e < 5); //true
boolean b2 = nums.stream().allMatch(e -> e < 3); //false
noneMatch
检查是否没有匹配所有元素,接收一个 Predicate 函数,当流中每个元素都不符合该断言时才返回true,否则返回false。
boolean noneMatch(Predicate<? super T> predicate);
List<Integer> nums = Arrays.asList(1, 2, 3, 4);
boolean b1 = nums.stream().noneMatch(e -> e < 5); //false
boolean b2 = nums.stream().noneMatch(e -> e < 3); //false
anyMatch
检查是否至少匹配一个元素,接收一个 Predicate 函数,只要流中有一个元素满足该断言则返回true,否则返回false。
boolean anyMatch(Predicate<? super T> predicate);
List<Integer> nums = Arrays.asList(1, 2, 3, 4);
boolean b1 = nums.stream().anyMatch(e -> e < 5); //true
boolean b2 = nums.stream().anyMatch(e -> e < 3); //true
findFirst
返回流中第一个元素。
Optional<T> findFirst();
List<Integer> nums = Arrays.asList(1, 2, 3, 4);
Optional<Integer> first = nums.stream().findFirst();
System.out.println(first.orElse(null)); // 1
Object obj = Arrays.asList().stream().findFirst().orElse(null);
System.out.println(obj); // null
findAny
返回流中的任意元素。
该方法可以获取Stream中的任意一个元素。大多数时候看起来像是findAny()
总是返回第一个元素?但是它并不保证每次返回的都是第一个元素,尤其是在使用并发流的情况下,总之,findAny()
方法可能返回Stream中的任意一个元素。
Optional<T> findAny();
List<Integer> nums = Arrays.asList(1, 2, 3, 4);
Optional<Integer> first = nums.stream().findAny();
System.out.println(first.orElse(null));
Object obj = Arrays.asList().stream().findAny().orElse(null);
System.out.println(obj); // null
count
返回流中元素的总个数。
long count();
List<Integer> nums = Arrays.asList(1, 2, 3, 4);
long count = nums.stream().count();
System.out.println(count); // 4
max
返回流中元素最大值。
Optional<T> max(Comparator<? super T> comparator);
// 自然排序
List<Integer> nums = Arrays.asList(1, 2, 3, 4);
Optional<Integer> max = nums.stream().max(Integer::compareTo);
System.out.println(max.orElse(null)); // 4
// 定制排序
List<Integer> nums = Arrays.asList(1, 2, 3, 4);
Optional<Integer> max = nums.stream().max(new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
return o1.compareTo(o2);
}
});
System.out.println(max.orElse(null)); // 4
List<Integer> nums = Arrays.asList(1, 2, 3, 4);
Optional<Integer> max = nums.stream().max((n1, n2) -> n1.compareTo(n2));
System.out.println(max.orElse(null)); // 4
min
返回流中元素最小值。
Optional<T> min(Comparator<? super T> comparator);
// 自然排序
List<Integer> nums = Arrays.asList(1, 2, 3, 4);
Optional<Integer> max = nums.stream().min(Integer::compareTo);
System.out.println(max.orElse(null)); // 1
// 定制排序
List<Integer> nums = Arrays.asList(1, 2, 3, 4);
Optional<Integer> max = nums.stream().min(new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
return o1.compareTo(o2);
}
});
System.out.println(max.orElse(null)); // 1
List<Integer> nums = Arrays.asList(1, 2, 3, 4);
Optional<Integer> max = nums.stream().min((n1, n2) -> n1.compareTo(n2));
System.out.println(max.orElse(null)); // 1
forEach
内部迭代(使用 Collection 接口需要用户去做迭代,称为外部迭代。相反,Stream API 使用内部迭代——它帮你把迭代做了)
void forEach(Consumer<? super T> action);
void forEachOrdered(Consumer<? super T> action);
forEachOrdered 将按流源指定的顺序处理流元素,而不管流是顺序的还是并行的。
List<Integer> nums = Arrays.asList(1, 2, 3, 4);
nums.stream().forEach(System.out::println);
外部迭代
最传统的方法是用Iterator,当然还以用for i、增强for循环等等。这一类方法叫做外部迭代,意为显式地进行迭代操作,即集合中的元素访问是由一个处于集合外部的东西来控制的,在这里控制着循环的东西就是迭代器。
简单理解外部迭代就是由用户来决定 “做什么”和“怎么做”的操作。比如 “做什么”(把大写转成小写)与“怎么做”(通过 Iterator
遍历)是由用户来决定的。
内部迭代
顾名思义,这种方式的遍历将在集合内部进行,我们不会显式地去控制这个循环。无需关心遍历元素的顺序,我们只需要定义对其中每一个元素进行什么样的操作。注意在这种设定下可能无法直接获取到当前元素的下标。
内部迭代我们只需要提供 “做什么”,把“怎么做”的任务交给了 JVM
。
外部循环的代码非常直接,但它有如下问题:
-
Java 的 for 循环是串行的,而且必须按照集合中元素的顺序进行依次处理;
-
集合框架无法对控制流进行优化,例如通过排序、并行、短路(short-circuiting)求值以及惰性求值改善性能。
尽管有时 for-each 循环的这些特性(串行,依次)是我们所期待的,但它对改善性能造成了阻碍。实际上,我们可以使用内部迭替代外部迭代,用户把对迭代的控制权交给类库,并向类库传递迭代时所需执行的代码。用户把对操作的控制权交还给类库,从而允许类库进行各种各样的优化(例如乱序执行、惰性求值和并行等等)。总的来说,内部迭代使得外部迭代中不可能实现的优化成为可能。
归约
reduce
归约,也称缩减,顾名思义,是把一个流缩减成一个值,能实现对集合求和、求乘积和求最值操作。
这个方法的主要作用是把 Stream 元素组合起来。它提供一个起始值(种子),然后依照运算规则(BinaryOperator),和前面 Stream 的第一个、第二个、第 n 个元素组合。从这个意义上说,字符串拼接、数值的 sum、min、max、average 都是特殊的 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<T> reduce(BinaryOperator<T> accumulator);
其接受一个函数接口 BinaryOperator
List<Integer> nums = Arrays.asList(1, 2, 3, 4);
Optional accResult = nums.stream().reduce((acc, item) -> {
System.out.println("acc : " + acc);
acc += item;
System.out.println("item: " + item);
System.out.println("acc+ : " + acc);
System.out.println("--------");
return acc;
});
System.out.println("accResult: " + accResult.orElse(null));
上述例子将输出:
acc : 1
item: 2
acc+ : 3
--------
acc : 3
item: 3
acc+ : 6
--------
acc : 6
item: 4
acc+ : 10
--------
accResult: 10
第二个形式,T reduce(T identity, BinaryOperator<T> accumulator);
与第一种形式相同的是都会接受一个BinaryOperator函数接口,不同的是其会接受一个identity参数,用来指定Stream循环的初始值。如果Stream为空,就直接返回该值。另一方面,该方法不会返回Optional,因为该方法不会出现null。
List<Integer> nums = Arrays.asList(1, 2, 3, 4);
int accResult = nums.stream().reduce(0, (acc, item) -> {
System.out.println("acc : " + acc);
acc += item;
System.out.println("item: " + item);
System.out.println("acc+ : " + acc);
System.out.println("--------");
return acc;
});
System.out.println("accResult: " + accResult);
上述例子将输出:
acc : 0
item: 1
acc+ : 1
--------
acc : 1
item: 2
acc+ : 3
--------
acc : 3
item: 3
acc+ : 6
--------
acc : 6
item: 4
acc+ : 10
--------
accResult: 10
第三个形式, <U> U reduce(U identity, BiFunction<U, ? super T, U> accumulator, BinaryOperator<U> combiner);
在串行流(stream)中,该方法跟第二个方法一样,即第三个参数combiner不会起作用。在并行流(parallelStream)中,我们知道流被fork join出多个线程进行执行,此时每个线程的执行流程就跟第二个方法reduce(identity,accumulator)一样,而第三个参数combiner函数,则是将每个线程的执行结果当成一个新的流,然后使用第一个方法reduce(accumulator)流程进行归约。
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24);
Integer v2 = list.stream().reduce(0,
(x1, x2) -> {
System.out.println("stream accumulator: x1:" + x1 + " x2:" + x2);
return x1 - x2;
},
(x1, x2) -> {
System.out.println("stream combiner: x1:" + x1 + " x2:" + x2);
return x1 * x2;
});
System.out.println(v2); // -300
Integer v3 = list.parallelStream().reduce(0,
(x1, x2) -> {
System.out.println("parallelStream accumulator: x1:" + x1 + " x2:" + x2);
return x1 - x2;
},
(x1, x2) -> {
System.out.println("parallelStream combiner: x1:" + x1 + " x2:" + x2);
return x1 * x2;
});
收集
collect
将流转换为其他形式。接收一个 Collector接口的 实现,用于给Stream中元素做汇总的方法。
<R, A> R collect(Collector<? super T, A, R> collector);
Collector 接口中方法的实现决定了如何对流执行收集操作(如收 集到 List、Set、Map)。但是 Collectors 实用类提供了很多静态方法,可以方便地创建常见收集器实例。具体方法如下:
归集(toList、toSet、toMap)
因为流不存储数据,那么在流中的数据完成处理后,需要将流中的数据重新归集到新的集合里。toList
、toSet
和toMap
比较常用,另外还有toCollection
、toConcurrentMap
等复杂一些的用法。
下面用一个案例演示toList
、toSet
和toMap
:
List<Integer> list = Arrays.asList(1, 6, 3, 4, 6, 7, 9, 6, 20);
List<Integer> listNew = list.stream().filter(x -> x % 2 == 0).collect(Collectors.toList());
Set<Integer> set = list.stream().filter(x -> x % 2 == 0).collect(Collectors.toSet());
List<Person> personList = new ArrayList<Person>();
personList.add(new Person("Tom", 8900, 23, "male", "New York"));
personList.add(new Person("Jack", 7000, 25, "male", "Washington"));
personList.add(new Person("Lily", 7800, 21, "female", "Washington"));
personList.add(new Person("Anni", 8200, 24, "female", "New York"));
Map<?, Person> map = personList.stream().filter(p -> p.getSalary() > 8000)
.collect(Collectors.toMap(Person::getName, p -> p));
System.out.println("toList:" + listNew);
System.out.println("toSet:" + set);
System.out.println("toMap:" + map);
运行结果:
toList:[6, 4, 6, 6, 20]
toSet:[4, 20, 6]
toMap:{Tom=mutest.Person@5fd0d5ae, Anni=mutest.Person@2d98a335}
统计
Collectors 提供了一系列用于数据统计的静态方法:
- 计数:counting
- 平均值:averagingInt、averagingLong、averagingDouble
- 最值:maxBy、minBy
- 求和:summingInt、summingLong、summingDouble
- 统计以上所有:summarizingInt、summarizingLong、summarizingDouble
List<Person> personList = new ArrayList<Person>();
personList.add(new Person("Tom", 8900, 23, "male", "New York"));
personList.add(new Person("Jack", 7000, 25, "male", "Washington"));
personList.add(new Person("Lily", 7800, 21, "female", "Washington"));
// 求总数
Long count = personList.stream().collect(Collectors.counting());
// 求平均工资
Double average = personList.stream().collect(Collectors.averagingDouble(Person::getSalary));
// 求最高工资
Optional<Integer> max = personList.stream().map(Person::getSalary).collect(Collectors.maxBy(Integer::compare));
// 求工资之和
Integer sum = personList.stream().collect(Collectors.summingInt(Person::getSalary));
// 一次性统计所有信息
DoubleSummaryStatistics collect = personList.stream().collect(Collectors.summarizingDouble(Person::getSalary));
System.out.println("员工总数:" + count);
System.out.println("员工平均工资:" + average);
System.out.println("员工工资总和:" + sum);
System.out.println("员工工资所有统计:" + collect);
运行结果
员工总数:3
员工平均工资:7900.0
员工工资总和:23700
员工工资所有统计:DoubleSummaryStatistics{count=3, sum=23700.000000, min=7000.000000, average=7900.000000, max=8900.000000}
分组(partitioningBy、groupingBy)
分区 partitioningBy
collect的一个常用操作将Stream分解成两个集合。假如一个数字的Stream,我们可能希望将其分割成两个集合,一个是偶数集合,另外一个是奇数集合。我们首先想到的就是过滤操作,通过两次过滤操作,很简单的就完成了我们的需求。
但是这样操作起来有问题。首先,为了执行两次过滤操作,需要有两个流。其次,如果过滤操作复杂,每个流上都要执行这样的操作, 代码也会变得冗余。
这里我们就不得不说Collectors库中的partitioningBy方法,它接受一个流,并将其分成两部分:使用Predicate对象,指定条件并判断一个元素应该属于哪个部分,并根据布尔值返回一个Map到列表。因此对于key为true所对应的List中的元素,满足Predicate对象中指定的条件;同样,key为false所对应的List中的元素,不满足Predicate对象中指定的条件。
public static <T> Collector<T, ?, Map<Boolean, List<T>>> partitioningBy(Predicate<? super T> predicate) {
return partitioningBy(predicate, toList());
}
eg: 使用partitioningBy,我们就可以将数字的Stream分解成奇数集合和偶数集合了。
Map<Boolean, List<Integer>> collectParti = Stream.of(1, 2, 3, 4, 5, 6)
.collect(Collectors.partitioningBy(it -> it % 2 == 0));
System.out.println("collectParti : " + collectParti);
// 结果: collectParti : {false=[1, 3, 5], true=[2, 4, 6]}
分组 groupingBy
数据分组是一种更自然的分割数据操作, 与将数据分成true和false两部分不同,可以使用任意值对数据分组。
调用Stream的collect方法,传入一个收集器,groupingBy接受一个分类函数,用来对数据分组,就像partitioningBy一样,接受一个
Predicate对象将数据分成true和false两部分。我们使用的分类器是一个Function对象,和map操作用到的一样。
public static <T, K> Collector<T, ?, Map<K, List<T>>> groupingBy(Function<? super T, ? extends K> classifier) {
return groupingBy(classifier, toList());
}
public static <T, K, A, D> Collector<T, ?, Map<K, D>> groupingBy(Function<? super T, ? extends K> classifier, Collector<? super T, A, D> downstream) {
return groupingBy(classifier, HashMap::new, downstream);
}
List<Person> personList = new ArrayList<Person>();
personList.add(new Person("Tom", 8900, "male", "New York"));
personList.add(new Person("Jack", 7000, "male", "Washington"));
personList.add(new Person("Lily", 7800, "female", "Washington"));
personList.add(new Person("Anni", 8200, "female", "New York"));
personList.add(new Person("Owen", 9500, "male", "New York"));
personList.add(new Person("Alisa", 7900, "female", "New York"));
// 将员工按薪资是否高于8000分组
Map<Boolean, List<Person>> part = personList.stream().collect(Collectors.partitioningBy(x -> x.getSalary() > 8000));
Map<Boolean, List<Person>> group = personList.stream().collect(Collectors.groupingBy(x -> x.getSalary() > 8000));
// 将员工按性别分组
Map<String, List<Person>> group2 = personList.stream().collect(Collectors.groupingBy(Person::getSex));
// 将员工先按性别分组,再按地区分组
Map<String, Map<String, List<Person>>> group3 = personList.stream().collect(Collectors.groupingBy(Person::getSex, Collectors.groupingBy(Person::getArea)));
System.out.println("员工按薪资是否大于8000分区情况:" + part);
System.out.println("员工按薪资是否大于8000分组情况:" + group);
System.out.println("员工按性别分组情况:" + group2);
System.out.println("员工按性别、地区:" + group3);
员工按薪资是否大于8000分区情况:{
false=[Person(name=Jack, salary=7000, age=null, sex=male, area=Washington), Person(name=Lily, salary=7800, age=null, sex=female, area=Washington), Person(name=Alisa, salary=7900, age=null, sex=female, area=New York)],
true=[Person(name=Tom, salary=8900, age=null, sex=male, area=New York), Person(name=Anni, salary=8200, age=null, sex=female, area=New York), Person(name=Owen, salary=9500, age=null, sex=male, area=New York)]
}
员工按薪资是否大于8000分组情况:{
false=[Person(name=Jack, salary=7000, age=null, sex=male, area=Washington), Person(name=Lily, salary=7800, age=null, sex=female, area=Washington), Person(name=Alisa, salary=7900, age=null, sex=female, area=New York)],
true=[Person(name=Tom, salary=8900, age=null, sex=male, area=New York), Person(name=Anni, salary=8200, age=null, sex=female, area=New York), Person(name=Owen, salary=9500, age=null, sex=male, area=New York)]
}
员工按性别分组情况:{
female=[Person(name=Lily, salary=7800, age=null, sex=female, area=Washington), Person(name=Anni, salary=8200, age=null, sex=female, area=New York), Person(name=Alisa, salary=7900, age=null, sex=female, area=New York)],
male=[Person(name=Tom, salary=8900, age=null, sex=male, area=New York), Person(name=Jack, salary=7000, age=null, sex=male, area=Washington), Person(name=Owen, salary=9500, age=null, sex=male, area=New York)]
}
员工按性别、地区:{
female={
New York=[Person(name=Anni, salary=8200, age=null, sex=female, area=New York), Person(name=Alisa, salary=7900, age=null, sex=female, area=New York)],
Washington=[Person(name=Lily, salary=7800, age=null, sex=female, area=Washington)]
},
male={
New York=[Person(name=Tom, salary=8900, age=null, sex=male, area=New York), Person(name=Owen, salary=9500, age=null, sex=male, area=New York)],
Washington=[Person(name=Jack, salary=7000, age=null, sex=male, area=Washington)]
}
}
接合(joining)
joining
可以将stream中的元素用特定的连接符(没有的话,则直接连接)连接成一个字符串。
public static Collector<CharSequence, ?, String> joining(CharSequence delimiter) {
return joining(delimiter, "", "");
}
public static Collector<CharSequence, ?, String> joining(CharSequence delimiter,
CharSequence prefix, CharSequence suffix) {
return new CollectorImpl<>(
() -> new StringJoiner(delimiter, prefix, suffix),
StringJoiner::add, StringJoiner::merge,
StringJoiner::toString, CH_NOID);
}
List<String> list = Arrays.asList("A", "B", "C");
String string1 = list.stream().collect(Collectors.joining("-")); // A-B-C
String string2 = list.stream().collect(Collectors.joining("-", "[", "]")); // [A-B-C]
归约
和 reduce 类似。
public static <T> Collector<T, ?, Optional<T>> reducing(BinaryOperator<T> op);
public static <T> Collector<T, ?, T> reducing(T identity, BinaryOperator<T> op);
public static <T, U> Collector<T, ?, U> reducing(U identity, Function<? super T, ? extends U> mapper, BinaryOperator<U> op);
形式一 public static <T> Collector<T, ?, Optional<T>> reducing(BinaryOperator<T> op)
:
List<Person> personList = new ArrayList<Person>();
personList.add(new Person("Tom", 8900, "male", "New York"));
personList.add(new Person("Jack", 7000, "male", "Washington"));
personList.add(new Person("Lily", 7800, "female", "Washington"));
personList.add(new Person("Anni", 8200, "female", "New York"));
personList.add(new Person("Owen", 9500, "male", "New York"));
personList.add(new Person("Alisa", 7900, "female", "New York"));
// 查询每个地区工资最高的人
Comparator<Person> bySalary = Comparator.comparing(Person::getSalary);
Map<String, Optional<Person>> salaryByArea = personList.stream()
.collect(Collectors.groupingBy(Person::getArea, Collectors.reducing(BinaryOperator.maxBy(bySalary))));
System.out.println(salaryByArea);
// 查询工资最高的人
Optional<Person> collect = personList.stream().collect(Collectors.reducing(BinaryOperator.maxBy(bySalary)));
System.out.println(collect);
输出结果
{New York=Optional[Person(name=Owen, salary=9500, age=null, sex=male, area=New York)], Washington=Optional[Person(name=Lily, salary=7800, age=null, sex=female, area=Washington)]}
Optional[Person(name=Owen, salary=9500, age=null, sex=male, area=New York)]
形式二 public static <T> Collector<T, ?, T> reducing(T identity, BinaryOperator<T> op)
:
和 reduce 类似。会接受一个identity参数,用来指定循环的初始值。
List<Person> personList = new ArrayList<Person>();
personList.add(new Person("Tom", 8900, "male", "New York"));
personList.add(new Person("Jack", 7000, "male", "Washington"));
personList.add(new Person("Lily", 7800, "female", "Washington"));
personList.add(new Person("Anni", 8200, "female", "New York"));
personList.add(new Person("Owen", 9500, "male", "New York"));
personList.add(new Person("Alisa", 7900, "female", "New York"));
// 查询每个地区工资最高的人
Comparator<Person> bySalary = Comparator.comparing(Person::getSalary);
Person identity = new Person("identity", 8500, null, null);
Map<String, Person> salaryByArea = personList.stream()
.collect(Collectors.groupingBy(Person::getArea, Collectors.reducing(identity, BinaryOperator.maxBy(bySalary))));
System.out.println(salaryByArea);
// 查询工资最高的人
Person collect = personList.stream().collect(Collectors.reducing(identity, BinaryOperator.maxBy(bySalary)));
System.out.println(collect);
输出结果
{New York=Person(name=Owen, salary=9500, age=null, sex=male, area=New York), Washington=Person(name=identity, salary=8500, age=null, sex=null, area=null)}
Person(name=Owen, salary=9500, age=null, sex=male, area=New York)
形式三 public static <T, U> Collector<T, ?, U> reducing(U identity, Function<? super T, ? extends U> mapper, BinaryOperator<U> op)
:
有的时候,我们想在 reducing
的时候把 Person
的工资先处理一下(比如四舍五入)。这就需要我们做一个映射处理。定义一个 Function<? super T, ? extends U> mapper
来干这个活。
List<Person> personList = new ArrayList<Person>();
personList.add(new Person("Tom", 8900, "male", "New York"));
personList.add(new Person("Jack", 7000, "male", "Washington"));
personList.add(new Person("Lily", 7800, "female", "Washington"));
personList.add(new Person("Anni", 8200, "female", "New York"));
personList.add(new Person("Owen", 9500, "male", "New York"));
personList.add(new Person("Alisa", 7900, "female", "New York"));
// 定义映射 处理
Function<Person, Person> mapper = ps -> {
Integer salary = ps.getSalary();
ps.setSalary(salary/100);
return ps;
};
// 查询每个地区工资最高的人
Comparator<Person> bySalary = Comparator.comparing(Person::getSalary);
Person identity = new Person("identity", 85, null, null);
Map<String, Person> salaryByArea = personList.stream()
.collect(Collectors.groupingBy(Person::getArea, Collectors.reducing(identity, mapper, BinaryOperator.maxBy(bySalary))));
System.out.println(salaryByArea);
// 查询工资最高的人
Person collect = personList.stream().collect(Collectors.reducing(identity, mapper, BinaryOperator.maxBy(bySalary)));
System.out.println(collect);
输出结果
{New York=Person(name=Owen, salary=95, age=null, sex=male, area=New York), Washington=Person(name=identity, salary=85, age=null, sex=null, area=null)}
Person(name=identity, salary=85, age=null, sex=null, area=null)
参考连接