java8系列-04 Stream

Published on in java with 321 views and 2 comments

Stream英文直译为“流”,其实这里也是对文件流、集合流等各种流的操作。(虽然我们最常用在Collection集合中进行操作,但它的应用可是很广泛的)它是java8中最亮眼的特性之一,能极大的方便我们对于数据的操作,让程序员们能写出非常高效率又干净的代码。下面我们先看一个例子,从例子中进行初步认识,然后再带大家慢慢的来揭露它的神秘面纱。

Stream基础

先来感受一下Stream的神奇吧~

public class StreamDemo {
    /**
     * 这里有一个很简单的需求:
     * 有一组用户数据(List<User>),
     * 我们要从这组数据中筛选出年龄在18-22,并且性别为男的所有用户
     */

    // java8以下的方法
    public static List<User> filterUser(List<User> users){
        Iterator<User> it = users.iterator();
        List<User> us = new ArrayList<>();
        while (it.hasNext()){
            User u = it.next();
            if(u.getAge()>=18 && u.getAge()<=22 && u.getSex().contentEquals("男")){
                us.add(new User(u.getName(),u.getAge(),u.getSex()));
            }
        }
        return us;
    }

    // 使用stream方式
    public static List<User> filterByStream(List<User> users){
        List<User> us = new ArrayList<>();
        Stream<User> stream  = users.stream();
        stream.filter(user -> user.getAge()>=18 && user.getAge()<=22 && user.getSex().contentEquals("男"))
                .forEach(user -> us.add(new User(user.getName(),user.getAge(),user.getSex())));
        stream.close(); // 记得关流,否则将耗尽你的内存,造成内存泄漏程序崩溃。
        return us;
    }

    public static void main(String[] args) {
        List<User> data = getData();
        // java8前的用法:
        StreamDemo.filterUser(data).forEach((User u)-> System.out.println(u.getName())); // 输出张三 肖二
        // Stream的用法:
        StreamDemo.filterByStream(data).forEach((User u)-> System.out.println(u.getName()));// 输出张三 肖二
    }

    public static List<User> getData(){
        List<User> users = new ArrayList<>();
        users.add(new User("张三",22,"男"));
        users.add(new User("李四",18,"女"));
        users.add(new User("王五",28,"男"));
        users.add(new User("肖二",20,"男"));
        return users;
    }
}

由上可以看出使用Stream是非常简便的。那么它有什么书写规则可以遵循呢?

其实书写Stream分为三步走。第一步,先创建一个Stream()对象,然后执行一些中间的聚合操作(如上面例子中的filter),最后是执行最终的操作(如上面例子中的forEach)。其中中间方法是可以有多个的。其实它就是一个元素流管道,先经过中间方法的处理,最后由最终操作(terminal operation)得到前面处理的结果。

下面我们来分步认识一下

1.创建Stream(源)

其实,创建Stream是有许多种方式的,毕竟也有各种各样的流嘛。下面列出了九种创建方式,请客官慢慢品一下吧。

- 空的Stream

Stream<String> streamEmpty  = Stream.empty;

创建一个空Stream通常被用来避免空指针或者零元素对象的Stream返回为null的现象。

- 集合Steram

Collections<String> co = Arrays.asList("a", "b", "c");
Stream<String> streamCollection = co.stream();

可以创建Collection家族下面的List、Set、Queue。

- 数组Stream

Stream<String> streamArray = Stream.of("a", "b", "c");

上面是一种简便方式,当然也可以先创建一个数组(arr),再给数组创建Stream(Arrays.stream(arr))。

- 通过构建器来创建

Stream<String> streamBuilder = Stream.<String>builder().add("a").add("b").add("c").build();

当builder被用于指定参数类型时,应被额外标识在声明右侧,否则方法build()将创建一个Stream(Object)实例:

- 通过生成器来生成一个Stream对象

Stream<String> streamGenerated = Stream.generate( () -> "element").limit(10); // limit也是一个terminal

方法generator()接受一个供应器Supplier用于元素生成。Supplier函数式接口在第二篇中有讲到,不接收参数返回一个T类型结果,有点像工厂直接给你创建一个对象一样。这里代码的意思是一直创建String字符串“element”,然后用limit来限制只有10个。否则会一直创建到jvm内存达到顶值。

- 通过迭代器

Stream<Integer> streamItreated = Stream.iterate(20, n -> n + 2).limit(10);

这里也是会永远迭代直到达到jvm内存的顶值,所以也要使用limit来限制一下。生成流中的元素为:20、22、24、…。

- 基元流

怎么理解这个基佬流呢?说错了,是基元流。其实它就是对于八大基础数据类型的流,不过这里只提供了三个:int、long、double。分别为IntStream、LongStream、DoubleStream。

IntStream intStream = IntStream.range(1, 10);
LongStream longStream = LongStream.rangeClosed(1, 10);

方法range(int start, int end)创建了一个有序流。它使后面的值每个增加1,但却不包括最后一个参数。所以上面的range方法最终会创建一个1到9的IntStream。方法rangeClosed(int start, int end)与range()大致相同,但它却包含了最后一个值。所以最终会创建一个1到10的LongStream

这两个方法用于生成三大基本数据类型的stream。

另外,以前我们用来创建随机数的类Random也扩展了用于生成基元流

Random random = new Random();
DoubleStream doubleStream = random.doubles(2);

- 字符串流

字符串流主要得益于char。char和int的私密关系我想你是知道的…

IntStream streamOfChars = "abc".chars();

- 文件流

Path path = Paths.get("C:\\file.txt");
Stream<String> streamString = Files.lines(path);
Stream<String> streamCharset = Files.lines(path, Charset.forName("utf-8"));

Java NIO类文件允许通过方法lines()生成文本文件的Stream。文本的每一行都会变成stream的一个元素

2.中间(Intermediate)方法

我这里第二步的中间方法讲的是 - 惰性求值

什么是惰性求值呢?

答: 惰性求值就是像上面例子中filter的这类函数,它最终不会产生出一个结果,而只是对数据进行进一步的操作。相反,还有一个叫做及早求值,如上面例子中的forEach,最终会从Stream中产生一个新的值。

及早求值 我在这里把它归到第三步中,也就是最后一步求出值的操作中(当然,别忘了关流)

那像这样惰性求值的Intermediate有哪些呢?

答:主要有这些:map (mapToInt, mapToLong, mapToDouble)、flatMap 、 filter、 distinct、 sorted、 peek、 skip、 parallel、 sequential、 unordered。

这些惰性求值的函数具体都有什么用呢?

答: 这里卖个关子,请滑到下面的Stream继续探索,我会在那里根据源码来讲解。

3.最终(terminal)方法完成对数据的处理

最终方法基本就是那些及早求值的函数了:

不过它也分为两大类,一类是Terminal(终结符、末尾),还有一类是short-circuiting(短路)

  • Terminal:forEach、 forEachOrdered、 toArray、 reduce、 collect、 min、 max、 count、iterator
  • short-circuiting:anyMatch、 allMatch、 noneMatch、 findFirst、 findAny

下面会通过源码来学习几个中间方法和最终方法


注意:

  1. JAVA8 是不允许重复使用stream的。如果对于同一个stream有相同的引用,则会造成IllegalStateException异常
  2. Stream的执行顺序是垂直调用,如例:
stream.filter(user -> user.getAge()>=18 && user.getAge()<=22 && user.getSex().contentEquals("男"))
                .skip(1)
                .forEach(user -> us.add(new User(user.getName(),user.getAge(),user.getSex())));
// 中间方法skip的作用是返回一个跳过第几元素(上面为跳过第一元素)的流。所以上面的方法执行会得到第二个元素开始的集合(如果值够的话)

上面的例子会执行filter方法,当filter方法满足条件执行完成后就会再去执行skip,最后到达最终方法forEach。这就是Stream的垂直调用。

Stream继续探索(源码解读)

既然是探索,那肯定得带着疑问走啊。现在的问题就是我已经明白了怎么去创建一个Stream了,但是还不懂中间方法和最终方法的作用和用法。所以下面的操作就是一起来跟着源码来操作一番吧~(为了篇幅着想,这里只举了几个例子)

中间方法的几个例子

1. map

map在Stream中算是一个非常常用的方法了。

  • 源码:
// 接收一个Function功能型函数式接口的参数(第二篇中有讲到即接收一个T类型的参数,返回一个R类型的结果)
// 从下面定义的泛型可以看出参数函数式接口的类型应该与返回值的类型关系应该是继承或是一致的
<R> Stream<R> map(Function<? super T, ? extends R> mapper);
  • 用法:
/**
 * 如下例子:从集合中的每个数值都拿出来乘二后再组成一个新的集合
 */
List<Integer> data = Arrays.asList(1,2,4,8,16);
List<Integer> res = data.stream().map((param)->param*2).collect(Collectors.toList());
System.out.println(res); // [2, 4, 8, 16, 32]
  • 作用: 可以来映射每个元素到对应的结果

2. flatMap

看完上面一个map,这里又来了一个flatMap。flat? 平的? 平面? A?

  • 源码
<R> Stream<R> flatMap(Function<? super T, ? extends Stream<? extends R>> mapper);
  • 用法
List<String> hello = Arrays.asList("hello","hi","你好","||");
List<String> straightMan = Arrays.asList("随便","你忙","多喝热水","||"); //直男三连
List<String> goodBoy = Arrays.asList("你是好人","you are good guy","very good");

List<List<String>> data = new ArrayList<>();
data.add(hello);
data.add(straightMan);
data.add(goodBoy);

List<String> res = data.stream().flatMap(flat->flat.stream()).collect(Collectors.toList());
System.out.println(res);// 输出[hello, hi, 你好, ||, 随便, 你忙, 多喝热水, ||, 你是好人, you are good guy, very good]

// 不多说,这个例子希望你是看得懂的^_^..
  • 作用: 和map类似,不同的是其每个元素转换得到的是Stream对象,它其实是对流进行一个扁平化的操作。

3. peek

刚刚看完平的,现在又来一个偷看(peek)?Stream可真好玩啊~

  • 源码
// 这里又用到了一个Consumer消费型函数式接口(接收一个T类型的参数,不返回结果),果然是看在眼里记在心里。
Stream<T> peek(Consumer<? super T> action);
  • 用法
List<String> heart = Arrays.asList("I","Love","U");
Stream stream = heart.stream().peek(System.out::print); // 这里会输出  ILoveU
Object say = stream.collect(Collectors.toList());
stream.close();
System.out.println(say); // 这里会输出  ILoveU[I, Love, U]
  • 作用:从上面例子可以看出peek会返回Stream接口,它是一个Intermediate,所以它返回的Stream可以供后续继续使用。

最终方法的几个例子

1. reduce

  • 源码
//BinaryOperator接口继承于BiFunction函数式方法,在第一篇最后部分有讲到,接收两参数返回一结果
//BinaryOperator<T>接口用于执行lambda表达式并返回一个T类型的返回值
T reduce(T identity, BinaryOperator<T> accumulator);
  • 用法
int reducedTwoParams =
                IntStream.range(1, 3).reduce(10, (a, b) -> a + b);
        System.out.println("reducedTwoParams ="+reducedTwoParams);  // reducedTwoParams = 13
  • 作用:reduce主要被用来计算值,结果也是一个值,如上reducedTwoParams = 10+1+2+3

2. findAny

  • 源码
// Optional也是java8的一个新特性,在下一章会开始探索。主要被用来防止空指针异常的
Optional<T> findAny();
  • 用法
/**
* 源码里面关于它的注释:
* Returns an {@link Optional} describing some element of the stream,
* or an  empty {@code Optional} if the stream is empty.
*/
// 简要:如果stream是空的情况下就返回一个empty,否则按照条件来返回相应的值
Stream<String> stream = Stream.of("a", "o", "e","i","w").filter(element -> element.contains("e"));
Optional<String> anyElement = stream.findAny();
System.out.println(anyElement); // Optional[e]

// 如上,如果把filter操作改成filter(element -> element.contains("q"));,则会返回Optional.empty
  • 作用:可以用来查找相关数值.

关于Stream的知识我上面讲的这些还远远不达标,所以探索下去的心请希望能一直在跳动着。

Responses