java8系列-04 Stream

image

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<T>用于元素生成。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<String>。文本的每一行都会变成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的知识我上面讲的这些还远远不达标,所以探索下去的心请希望能一直在跳动着。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 151,688评论 1 330
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 64,559评论 1 273
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 101,749评论 0 226
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 42,581评论 0 191
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 50,741评论 3 271
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 39,684评论 1 192
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,122评论 2 292
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 29,847评论 0 182
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 33,441评论 0 228
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 29,939评论 2 232
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,333评论 1 242
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 27,783评论 2 236
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,275评论 3 220
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 25,830评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,444评论 0 180
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 34,553评论 2 249
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 34,618评论 2 249

推荐阅读更多精彩内容