Java8新特性之Stream流

什么是Stream

Stream是Java8中处理集合的关键抽象概念,他可以指定你希望对集合进行非常复杂的查找,过滤和映射数据等操作。Steam是一种高效的处理数据的一种方式。
备注: Java中的集合讲的是数据,而Stream流讲究的是对数据的处理
注意:1. Stream自己不会存储元素。 
     2. Streatm不会改变原对象。相反,他们会返回一个持有结果的新Stream。
     3. Stream操作时延迟执行,也就是他们会等到需要结果的时候才会执行。

Stream流的构成

当我们使用Stream流的时候,通常包括三个基本步骤:

获取一个数据源(source)—-> 数据转换 —–> 执行操作获取想要的结果, 每次转换原有 Stream 对象不改变,返回一个新的 Stream 对象(可以有多次转换),这就允许对其操作可以像链条一样排列,变成一个管道,如下图所示。

stream流的流程图

第一步:创建Stream 一个数据源(如:集合或数组),获取一个流。

第二步: 中间操作 一个中间操作链,对数据源中的数据进行处理操作。

第三步:终止操作 执行中间操作链,并返回产生的最终结果。

创建Stream流的方式

第一种:通过Collection和数组

1. Collection.stream();
2. Collection.parallelStream();
3. Arrays.stream(T array) or Stream.of()

第二种:从BufferedReader中获取

1. Java.io.BufferedReader.lines();

第三种:静态工厂

1. java.util.stream.IntStream.range();
2. java.nio.file.Files.walk();

第四种: 自己构建

1. java.util.Spliterator

第五种: strem创建无限流

1. public static< T> Stream< T> iterate(final T seed, final UnaryOperator< T> f);
2. public static< T> Stream< T> generate(Supplier< T> s);

第六种: 其他

1. Random.ints();
2. BitSet.stream();
3. Pattern.splitAsStream(java.lang.CharSequence);
4. JarFile.stream();

Stream流的中间操作和终止操作

流的操作类型总共可以分为两种:

  • Intermediate(中间操作):一个流可以后台跟随零个或多个intermediate操作。其目的主要是打开流,做出对应的数据映射/过滤,而后返回一个新的流,交给下一个操作使用。这类操作都是惰性化的(Lazy),也就是说调用到这类方法,并没有真正开始流的遍历。
  • Terminal(终止操作): 一个流只能有一个terminal操作,当操作执行这个操作后,此时的流就被使用“光”了,无法再被操作。也就是流的最后一部操作。Terminal操作的执行,才会真正开始流的遍历,并且会生成一个结果,或者一个side effect.
  • short-circuiting(短循环)

​ 还有一个操作被称为short-circuiting,用以指:

  • 对于一个intermediate操作,如果他接受的是一个无限大(infinite/unbounded)的Stream,但返回一个有限的新Stream
  • 对于一个Terminal操作,如果他接受的是要给无限大的Stream,单能在有限的时间计算出结果。
  • 总结:当操作一个无限大的Streamm,此时有希望再有限的时间内进行完成,此时可以再管道内拥有一个short-circuiting操作可以增加处理的效率。

在对于一个 Stream 进行多次转换操作 (Intermediate 操作),每次都对 Stream 的每个元素进行转换,而且是执行多次,这样时间复杂度就是 N(转换次数)个 for 循环里把所有操作都做掉的总和吗?其实不是这样的,转换操作都是 lazy 的,多个转换操作只会在 Terminal 操作的时候融合起来,一次循环完成。我们可以这样简单的理解,Stream 里有个操作函数的集合,每次转换操作就是把转换函数放入这个集合中,在 Terminal 操作的时候循环 Stream 对应的集合,然后对每个元素执行所有的函数。

当把一个数据包装成Stream后,就要开始对里卖弄的元素进行各种操作了。常见的中间操作和终止操纵的方法以及短循环可以归类如下。

  • Intermediate: map(mapToInt,flatMap等),filter(),distinct(),sorted(),peek(),limit(),skip(),paraller(),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流的操作方法

map/flatMap : 把input stream中的每一个元素,映射成output stream的另外一个元素
  • map : 一对一操作 , 每个输入元素都按照规则转换为另外一个元素。
1. 讲单词转换为大写
List<String> list = Arrays.asList("hello", "world");
 List<String> collect = list.stream().map(String::toUpperCase).collect(Collectors.toList());
 // 输出结果: HELLO WORLD

2.  求平方数
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 把 input Stream 中的层级结构扁平化,就是将最底层元素抽出来放到一起,最终 output 的新 Stream 里面已经没有 List 了,都是直接的数字。
1. 一对多
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());
//输出结果: 1  2  3  4  5  6 
filter: filter对原始stream进行某项测试,通过测试的元素都被留下来生成一个新的Stream.
1. 留下偶数操作
Integer[] sixNums = {1, 2, 3, 4, 5, 6};
Integer[] evens =
Stream.of(sixNums).filter(n -> n%2 == 0).toArray(Integer[]::new);
//输出结果: 2  4  6

2. 把单词挑出来
BufferedReader reader = new BufferedReader(new StringReader("haha"));
        List<String> output = reader.lines().
                flatMap(line -> Stream.of(line.split("h"))).
                filter(word -> word.length() > 0).
                collect(Collectors.toList());
        Stream.of(output).forEach(System.out::println);
 //输出结果: [a,a]       
备注: 首先该方法会把每行的单词用flatMap整理到新的Stream,然后保留长度不为0的,就是当前文章中的所有单词。
forEach : forEach方法是一个Terminal操作,接受一个Lambda表达式,然后再Stream的每一个元素上执行该表达式。
1.打印姓名(forEach 和 pre-java8 的对比)
// Java 8
roster.stream()
 .filter(p -> p.getGender() == Person.Sex.MALE)
 .forEach(p -> System.out.println(p.getName()));
// Pre-Java 8
for (Person p : roster) {
 if (p.getGender() == Person.Sex.MALE) {
 System.out.println(p.getName());
 }
}

对一个人员集合遍历,找出男性并打印姓名。可以看出来,forEach 是为 Lambda 而设计的,保持了最紧凑的风格。而且 Lambda 表达式本身是可以重用的,非常方便。当需要为多核系统优化时,可以 parallelStream().forEach(),只是此时原有元素的次序没法保证,并行的情况下将改变串行时操作的行为,此时 forEach 本身的实现不需要调整,而 Java8 以前的 for 循环 code 可能需要加入额外的多线程逻辑。

但一般认为,forEach 和常规 for 循环的差异不涉及到性能,它们仅仅是函数式风格与传统 Java 风格的差别。

另外一点需要注意,forEach 是 terminal 操作,因此它执行后,Stream 的元素就被“消费”掉了,你无法对一个 Stream 进行两次 terminal 运算。下面的代码是错误的:

ream.forEach(element -> doOneThing(element));
stream.forEach(element -> doAnotherThing(element));

相反,具有相似功能的 intermediate 操作 peek 可以达到上述目的。如下是出现在该 api javadoc 上的一个示例。

1. peek 对每个元素执行操作并返回一个新的 Stream
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());

forEach 不能修改自己包含的本地变量值,也不能用 break/return 之类的关键字提前结束循环。

findFirst : 这是一个 termimal 兼 short-circuiting 操作,它总是返回 Stream 的第一个元素,或者空。

注意点: 这里的返回值类型是Optional.这也是模仿Scala语言中的概念,作为一个容器,他可能含有某值,或者不包含。使用他的目的是尽可能避免NullpointerException.

Optional的两个用例展示

String strA = " abcd ", strB = null;
print(strA);
print("");
print(strB);
getLength(strA);
getLength("");
getLength(strB);
public static void print(String text) {
 // Java 8的函数式编程Optional
 Optional.ofNullable(text).ifPresent(System.out::println);
 // Pre-Java 8
 if (text != null) {
 System.out.println(text);
 }
 }
public static int getLength(String text) {
 // Java 8
return Optional.ofNullable(text).map(String::length).orElse(-1);
 // Pre-Java 8
// return if (text != null) ? text.length() : -1;
 };

在更复杂的 if (xx != null) 的情况中,使用 Optional 代码的可读性更好,而且它提供的是编译时检查,能极大的降低 NPE 这种 Runtime Exception 对程序的影响,或者迫使程序员更早的在编码阶段处理空值问题,而不是留到运行时再发现和调试。

Stream 中的 findAny、max/min、reduce 等方法等返回 Optional 值。还有例如 IntStream.average() 返回 OptionalDouble 等等。

reduce: 把stream元素组合起来,他提供一个起始值(种子),然后依照运算规则(BinaryOperator),和前面stream的第一个,第二个,第n个元素组合。例如字符串拼接,数值的sum,min,max,average都是特殊的reduce

Integer sum = integers.reduce(0,(a,b)->a+b);或

Integer sum = integers.reduce(0,Integer::sum);

也有没有起始值的情况,这是会把stream的前面两个元素组合起来,返回的是optional

reduce案例展示

// 字符串连接,concat = "ABCD"
String concat = Stream.of("A", "B", "C", "D").reduce("", String::concat); 
// 求最小值,minValue = -3.0
double minValue = Stream.of(-1.5, 1.0, -3.0, -2.0).reduce(Double.MAX_VALUE, Double::min); 
// 求和,sumValue = 10, 有起始值
int sumValue = Stream.of(1, 2, 3, 4).reduce(0, Integer::sum);
// 求和,sumValue = 10, 无起始值
sumValue = Stream.of(1, 2, 3, 4).reduce(Integer::sum).get();
// 过滤,字符串连接,concat = "ace"
concat = Stream.of("a", "B", "c", "D", "e", "F").
 filter(x -> x.compareTo("Z") > 0).
 reduce("", String::concat);
limit/skip: limit返回Stream的前面几个元素;Skip则是扔掉前n个元素(它是一个叫subStream的方法名改而来)

limit和skip案例展示

public void testLimitAndSkip() {
 List<Person> persons = new ArrayList();
 for (int i = 1; i <= 10000; i++) {
 Person person = new Person(i, "name" + i);
 persons.add(person);
 }
List<String> personList2 = persons.stream().
map(Person::getName).limit(10).skip(3).collect(Collectors.toList());
 System.out.println(personList2);
}
private class Person {
 public int no;
 private String name;
 public Person (int no, String name) {
 this.no = no;
 this.name = name;
 }
 public String getName() {
 System.out.println(name);
 return name;
 }
}
//打印结果: [name4, name5, name6, name7, name8, name9, name10]  备注:skip把前面的三个都跳过

注意点: limit和skip对sorted后的运行测试无影响,因为他们两个方法都属于terminal方法

List<Person> persons = new ArrayList();
 for (int i = 1; i <= 5; i++) {
 Person person = new Person(i, "name" + i);
 persons.add(person);
 }
List<Person> personList2 = persons.stream().sorted((p1, p2) -> 
p1.getName().compareTo(p2.getName())).limit(2).collect(Collectors.toList());
System.out.println(personList2);
// 打印结果:
name2
name1
name3
name2
name4
name3
name5
name4
[Person{no=1, name='name1'}, Person{no=2, name='name2'}]

即虽然最后的返回元素数量是 2,但整个管道中的 sorted 表达式执行次数没有像前面例子相应减少。

注意点: 对一个parallel的Stream流来说,如果其元素是有序的,那么limit操作的成本会比较大,应为他的返回对象必须是前n个也有一个次序的元素。取而代之的策略是取元素间的次序,或者不要使用parallel stream。

sorted :对Stream中的元素进行排序操作,他比数组的排序更强之处在于可以首先对Stream进行各类map,filter,limit,skip甚至是distctl来减少元素数量后,再进行排序,从而提高效率

sorted案例:再排序前进行limit或skip操作

List<Person> persons = new ArrayList();
 for (int i = 1; i <= 5; i++) {
 Person person = new Person(i, "name" + i);
 persons.add(person);
 }
List<Person> personList2 = persons.stream().limit(2).sorted((p1, p2) -> p1.getName().compareTo(p2.getName())).collect(Collectors.toList());
System.out.println(personList2);
//打印结果: 
name2
name1
[Person{no=1, name='name1'}, Person{no=2, name='name2'}]
min/max/distinct :min 和 max 的功能也可以通过对 Stream 元素先排序,再 findFirst 来实现,但前者的性能会更好,为 O(n),而 sorted 的成本是 O(n log n)。同时它们作为特殊的 reduce 方法被独立出来也是因为求最大最小值是很常见的操作。

案例: 找出最长一行的长度

BufferedReader br = new BufferedReader(new FileReader("c:\\SUService.log"));
int longest = br.lines().mapToInt(String::length).max().getAsInt();
br.close();
System.out.println(longest);
 ```

> 案例: 找出全文的单词,转小写并排序

List words = br.lines().flatMap(line -> Stream.of(line.split(” “))).filter(word -> word.length() > 0).map(String::toLowerCase).distinct().sorted().collect(Collectors.toList());
br.close();
System.out.println(words);




###### **Match** : 匹配操作 

> Stream有三个match方法:
>
> * allMatch: Stream中全部元素符合传入的predicate,返回true
> * anyMatch:Stream中只要有一个元素符合传入的predicate,返回true
> * noneMatch:Stream中没有一个元素符合传入的predicate,返回true

> ##### 使用 Match案例:

List persons = new ArrayList();
persons.add(new Person(1, “name” + 1, 10));
persons.add(new Person(2, “name” + 2, 21));
persons.add(new Person(3, “name” + 3, 34));
persons.add(new Person(4, “name” + 4, 6));
persons.add(new Person(5, “name” + 5, 55));
boolean isAllAdult = persons.stream().
allMatch(p -> p.getAge() > 18);
System.out.println(“All are adult? ” + isAllAdult);
boolean isThereAnyChild = persons.stream().
anyMatch(p -> p.getAge() < 12);
System.out.println(“Any child? ” + isThereAnyChild);
//打印结果:
All are adult? false
Any child? true


#### Stream进阶: 自己生成流

###### Stream.generate: 通过实现Supplier接口,你可以自己来控制流的生成。

> 应用场景: 通常用于随机数,常量的stream.或者需要前后元素见维持着某种状态的Stream.把Supplier实例传递给Stream.generate()生成的Stream.默认是串行(相对于parallel而言)但是无序的(相对于ordered)而言。由于他是无限的,再管道中,必须利用limit之类的操作显示Stream大小。

> 案例: 生成10个随机整数

Random seed = new Random();
Supplier random = seed::nextInt;
Stream.generate(random).limit(10).forEach(System.out::println);
//Another way
IntStream.generate(() -> (int) (System.nanoTime() % 100)).
limit(10).forEach(System.out::println);


> Stream.generator()还接受自己实现的Supplier.例如在构造海量测试数据的时候,用某种自动的规则给每一个变量赋值;或者依据公式计算Stream的每个元素值。这些都是维持状态。
>
> 案例: 自实现Supplier

Stream.generate(new PersonSupplier()).
limit(10).
forEach(p -> System.out.println(p.getName() + “, ” + p.getAge()));
private class PersonSupplier implements Supplier {
private int index = 0;
private Random random = new Random();
@Override
public Person get() {
return new Person(index++, “StormTestUser” + index, random.nextInt(100));
}
}
//打印结果:
StormTestUser1, 9
StormTestUser2, 12
StormTestUser3, 88
StormTestUser4, 51
StormTestUser5, 22
StormTestUser6, 28
StormTestUser7, 81
StormTestUser8, 51
StormTestUser9, 4
StormTestUser10, 76






###### Stream.iterator :  iterator和reduce操作很像,接受一个种子值,和一个UnaryOperator(例如f).然后种子值成为Stream的第一个元素,f(seed)为第二个,f(f(seed))第三个,以次内推。

> 案例: 生成一个等差数列

Stream.iterator(0,n->n+3).limit(10).forEach(x-> System.out.println(x+” “));
//打印结果:
0 3 6 9 12 15 18 21 24 27


> 与 Stream.generate 相仿,在 iterate 时候管道必须有 limit 这样的操作来限制 Stream 大小。 



#### Stream进阶: 用Collectors进行reduction操作

> java.util.stream.Collectors主要作用就是辅助进行各类有用的reduction操作,例如转变输出为Collectior,,把Stream元素进行归组。
>
> Collectors.groupingBy()         Collections.partitioningBy()
>
> 案例: 按照年龄归组  Collectors.groupingBy方法

// 首先生成100人的信息,然后按照年龄归组,相同年龄的人放在同一个list中:
Map


> 案例: 按照未成年人和成年人归组    Collectors.partitioningBy方法

Map

Stream流的特性总结:

  • 不是数据结构
  • 他没有内部存储,他只是用操作管道从soruce(数据结构,数组,generator function,IO channel)抓取数据。
  • 所有 Stream 的操作必须以 lambda 表达式为参数
  • 不支持索引访问
  • 惰性化
  • 很多 Stream 操作是向后延迟的,一直到它弄清楚了最后需要多少数据才会开始。
  • Intermediate 操作永远是惰性化的。
  • 并行能力
  • 一个 Stream 是并行化的,就不需要再写多线程代码,所有对它的操作会自动并行进行的。

猜你喜欢

转载自blog.csdn.net/m0_37779570/article/details/81872972