一、概述
Stream流操作是Java为了简化数据操作推出的新特性。虽然被称为Stream流,但是它和IO流是完全不同的概念。Stream流更多的是为了增强集合操作,在数据量较大时,我们可以通过Stream流直接在内存中操作数据。通过使用流,我们可以说明想要完成什么任务,而不是说明如何去实现它。
例如,我们想要统计某个整数集合中所有偶数的个数,传统方式会使用迭代的方式:
List<Integer> integerList = Arrays.asList(1,2,3,4,5,6);
int count = 0;
for (Integer integer : integerList) {
if (integer % 2 == 0){
count++;
}
}
而使用流式操作,只需指明要做的事情而无需关心实现。
long count1 = integerList.stream().filter(t -> t % 2 == 0).count();
其中integerList.stream()
用来创建流,filter()
是中间操作,count()
用来做终止流的操作,这里是统计作用。
流和集合是不同的概念,虽然看起来可能有些类似,但是它们有很多不同的地方:
流并不存储元素。这些元素可能存储在底层的集合中,或者按需生成。
流的操作不会修改其数据源。例如filter方法不会从新的流中移除元素,而是会生成一个新的流,其中包含被过滤掉的元素。
List<Integer> integerList = Arrays.asList(1,2,3,4,5,6); //流操作 long count1 = integerList.stream().filter(t -> t % 2 == 0).count(); //结果依然是[1,2,3,4,5,6] System.out.println(Arrays.toString(integerList.toArray())); //结果为3 System.out.println(count1);
流的操作是尽可能惰性执行的。这意味着直至需要其结果时,操作才会执行。例如,如果我们只想找前5个偶数,那么filter方法就会在匹配到第五个偶数后停止过滤。同时,我们还可以创建无限流。
二、流的创建方式
1.使用Collection接口的stream方法
Collection<Integer> collection = Arrays.asList(1,2,3,4);
Stream<Integer> stream = collection.stream();
2. 使用Stream的静态方法of
of方法具有可变长参数,因此我们可以构建具有任意数据引元的流:
Stream<Integer> integerStream = Stream.of(1, 2, 3, 4, 5, 6);
Stream<String> stringStream = Stream.of("1","2","3");
3. 使用Arrays中的stream方法
Stream<String> stream1 = Arrays.stream(new String[]{"1", "2", "3"}, 0, 3);
上述创建流的方式表示,从一个数组中截取从0到3的元素来创建流。
Stream<String> stream1 = Arrays.stream(new String[]{"1", "2", "3"});
4. 创建不包含任何元素的流
Stream<Object> empty = Stream.empty();
5. 创建无限序列
Stream流有两种创建无限序列的方式,一种是通过generate
方法,另一种是通过iterate
方法。
generate
方法接受一个实现了Supplier
接口的对象,Supplier
接口时供给型函数式接口,不需要传入参数,主要是通过反复调用Supplier
中的方法来实现
Stream.generate(() -> "1").forEach(System.out::println);
//产生一个无限随机数序列
Stream.generate(Math::random).forEach(System.out::println);
三、 操作流
1. filter
filter转换会产生一个流,它的元素与某种条件相匹配。例如我们可以从一个整数集合中找出所有的偶数。
Stream<Integer> stream = integerList.stream().filter(t -> t%2 ==0);
filter的参数是一个实现了Predicate
接口的对象,Predicate
接口是判定型接口,可以对传递的参数进行判断,返回值为Boolean。
2. map
map可以对参数进行映射,比如说我们希望将字符串集合中所有的小写字母映射(转换为大写字母),就可以使用map
List<String> stringList = Arrays.asList("a","b","c");
stringList.stream().map(String::toUpperCase).forEach(System.out::println);
它的底层使用了Function<T,R>
函数式接口来实现。
3. flatMap
应用场景分析
public void fun10(){
List<String> stringList = Arrays.asList("abc","bcd","cde");
Stream<Stream<String>> streamStream = stringList.stream().map(this::letters);
Stream<String> stringStream = stringList.stream().flatMap(this::letters);
}
private Stream<String> letters(String s){
List<String> result = new ArrayList<>();
for (int i = 0; i < s.length() ; i++) {
result.add(s.substring(i,i+1));
}
return result.stream();
}
如上所示,如果映射结果是Stream流,那么我们可以直接使用flatMap方法,这样它底层会帮助我们去转换Stream,从而更容易去使用。
4. limit
调用stream.limit(n)
会返回一个新的流,它在n个元素之后结束(如果原来的流更短,那么就会在流结束时结束)
stringList.stream().flatMap(this::letters).limit(3);
我们可以使用这个方法对无限流进行裁剪。
Stream.generate(() -> "1").limit(10).forEach(System.out::println);
5. skip
调用skip
方法会丢弃前n个元素
6. concat
调用concat
方法可以将两个流连接在一起
7. distinct
用来去重,调用distinct
方法可以将流中的重复元素剔除
8. sorted
用来对流中的元素进行排序,它有两种实现方式,一种是对实现Comparable
接口的元素直接进行排序,另一种是通过Compparator
接口自定义排序规则。
stringList.stream().sorted().forEach(System.out::println);
//自定义排序规则
stringList.stream().sorted(String::compareTo).forEach(System.out::println);
四、终止流
流在开启之后必须关闭,但是Stream
流的关闭方式不是通过close
方法而是通过终止操作来完成的,一般的终止操作如下
1. count
用来统计流中元素的个数
2. max、min
用来获取流中元素的最大值或最小值
3. findFirst
返回非空集合中的第一个元素。
4. findAny
查找任意匹配,这个方法在使用并行流进行处理时比较合适。
5. anyMatch
这个方法接受一个判定型函数式接口,不需要搭配filter使用。
上述操作返回值基本都是
Optional<T>
类型,Optional<T>
类型提供了规避空指针异常的有效方式,使用得当的情况下更安全。
五、Optional类型
Optional<T>
对象是一种包装器对象,要么包装了类型T的对象,要么没有包装任何对象。对于第一种情况,我们称为值存在。Optional<T>
类型被当作一种更安全的方式,用来替代类型T
的引用,这种引用要么引用某个对象,要么为null。但是,它只有在正确使用的情况下才更安全。
1. 使用Optional值
有效的使用Optional
的关键是要使用这样的方法:它在值不存在的情况下会产生一个可替代物,而如果值存在则使用该值。
首先,我们看第一条策略。通常,在没有任何匹配时,我们会希望使用某种默认值,可能是空字符串或者设置的其它值:
//构造一个空的集合
List<String> stringList = Arrays.asList();
//获取流并进行排序操作,最后查找第一个元素
Optional<String> first = stringList.stream().sorted().findFirst();
//如果值不存在,则默认为"hello"
String s = first.orElse("hello");
//因为没有找到结果,所以输出hello,如果结果不为空,则输出结果的值
System.out.println(s);
你和可以调用代码来设置默认值
first.orElseGet(() -> Locale.getDefault().getDisplayName());
或者可以在没有任何值的时候抛出异常
String s1 = first.orElseThrow(RuntimeException::new);
另一条使用可选值的策略是只有在其存在的情况下才消费该值。
ifPresent
方法会接受一个函数。如果该可选值存在,那么它会被传递到该函数。否则不会发生任何事。
first.ifPresent(stringList::add );
六、收集结果
针对将流中的元素收集到另一个目标中,有一个便捷的方法collect
可以使用,它会接受一个Collector
接口的实例。Collector
类提供了大量用于生成公共收集器的工厂方法。为了将流收集到列表或者集中,可以直接调用
1. 收集到集合中
stringList.stream().sorted().collect(Collectors.toList());
或者
stringList.stream().sorted().collect(Collectors.toSet());
如果想要控制集合的类型,例如需要将其收集到TreeSet
集合中,那么可以进行如下操作:
stringList.stream().collect(Collectors.toCollection(TreeSet::new));
2. 通过连接操作收集结果
//不指定分隔符
stringList.stream().collect(Collectors.joining());
//指定分隔符
stringList.stream().collect(Collectors.joining(","));
3. 将结果约简为总和、平均值、最大最小值
IntSummaryStatistics collect = stringList.stream().collect(Collectors.summarizingInt(String::length));
collect.getAverage();
collect.getCount();
collect.getMax();
collect.getMin();
collect.getSum();
4. 收集到映射表中
假设我们有一个Stream<Person>
,并且希望将其元素收集到一个映射表中,这样后续就可以通过它们的ID来查找人员了。Collectors.toMap
方法有两个函数引元,它们用来产生映射表的键和值。
list.stream().collect(Collectors.toMap(Person::getId,Function.identity()));
其中Function.identity()
方法获取到的是当前值。
如果有多个元素具有相同的键,那么就会存在冲突,收集器将会抛出一个异常。可以使用三个参数的
toMap
方法来规避这一问题。list.stream().collect(Collectors.toMap(Person::getId,Function.identity(),(oldValue,newValue)-> newValue));
第三个参数,映射过程中覆盖值的规则,第一个值表示的是旧值,第二个表示的是新值
注:如果希望使用并行的方式处理映射,可以使用toConcurrentMap
方法
5. 分组
假如说,我们有这样的一个需求:我们有一个List<Person>
集合,我们希望将它按照年龄分组,然后转换为Map<Integer,List<Person>>
的形式。在上一节中,我们提到可以使用toMap
方法来将List
转换为Map
,但是它并不适用于这种复杂场景。
5.1 groupingBy
Map<String, List<Person>> collect = list.stream().collect(Collectors.groupingBy(Person::getId));
5.2 partitioningBy
该函数返回true
的元素和其它的元素。在这种情况下,使用partitioningBy
比使用groupingBy
更高效。
5.3 注意
和toConcurrentMap
方法类似,我们可以使用groupingByConcurrent
方法以并行流的方式处理映射。
5.4 下游收集器
我们使用groupingBy
方法可以实现分组,但是默认会生成Map<T,List<U>>
格式的数据。有时候,我们可能并不需要这种形式的数据,我们需要获取分组后每组的数量,或者我们希望生成的集合是Set<U>
而不是List<U>
。对于这些需求,我们可以使用下游收集器来完成。
Map<String, Set<Person>> collect1 = list.stream().collect(Collectors.groupingBy(Person::getId, Collectors.toSet()));
Map<String, Long> collect2 = list.stream().collect(Collectors.groupingBy(Person::getId, Collectors.counting()));
从上图可以看出,我们可以为collect
方法传递两个参数,第二个参数就可以作为下游收集器,可以简单的理解下游收集器可以按照我们的想法返回指定的数据格式。
七、基本类型流
Java8的流相关库中具有专门的类型IntStream
、LongStrem
和DoubleStream
,用来直接存储基本类型值,而无需使用包装器。如果想要存储short
、boolean
、char
、byte
,可以使用IntStream
,而对于float
,可以使用DoubleStream
。
我们可以使用IntStream.of()
或者Arrays.stream(values,form,to)
来创建基本类型流。同时我们可以通过mapToInt
来将对象流转换为基本流,或者通过boxed()
方法将基本流转换为对象流。
基本流的使用方式和对象流基本上是一样的。
八、并行流
我们可以使用Collection.parallelStream()
来获得一个并行流。或者可以使用parallel()
将任意流转换为并行流:Stream.of(Array).parallel()
。
九、实际案例应用
1. 计算集合中元素的和
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5, 6);
//Integer reduce = list.parallelStream().reduce(0, Integer::sum);
Optional<Integer> reduce1 = list.parallelStream().reduce(Integer::sum);
reduce1.ifPresent(System.out::println);
reduce1.orElseThrow(Throwable::new);
//System.out.println(reduce);
2. 遍历某个文件夹下所有文件的名字
//使用Lambda表达式
List<String> files = Stream.of(Objects.requireNonNull(new File("C:\").
listFiles())).
filter(File::isDirectory).
map(File::getName).
collect(Collectors.toList());
//使用传统的匿名内部类格式
List<Object> collect = Stream.of(Objects.requireNonNull(new File("C:\").
listFiles())).filter(new Predicate<File>() {
@Override
public boolean test(File file) {
return file.isDirectory();
}
}).map(new Function<File, Object>() {
@Override
public Object apply(File file) {
return file.getName();
}
}).collect(Collectors.toList());
files.forEach(System.out::println);