Java核心教程5: 流式编程

本次课程的标题不像之前那样易懂,是一个陌生的概念,“流式编程”是个什么东西?

在了解流式编程之前先思考一下“流”,水流、电流、人流,这些都是流。而流式编程则是让集合中的一个一个对象像水流一样流动,分别进行去重、过滤、映射等操作,就和批量化生产线一样。利用流,我们无需迭代集合中的元素,就可以提取和操作它们,这些操作通常被组合在一起,在流上形成一条操作管道。

流的一个核心好处是,它使得程序更加短小并且更易理解,让我们来看看上次作业中的“生成 50 个 1 到 100 之间的不重复的随机数并输出”,如果用流来解决的话代码是怎样的:

new Random().ints(0,100)   //生成0到100的随机数
    .distinct()   //去除重复的值
    .limit(50)    //只取前50个数
    .forEach(System.out::println);   //对每个元素调用println函数

这显然非常简单和直观,而且你甚至都不需要写任何一句循环!让我们赶紧进入流式编程的乐园吧。


一、创建流

下面用一组代码来展示各种创建流的方法:

//产生从0到200的随机浮点数的流
DoubleStream randomStream = new Random().doubles(0, 200);

//将数组转换成流,可以产生基本数据类型的流,
//如IntStream、FloatStream、DoubleStream等,运行效率比较高
DoubleStream arrayStream= Arrays.stream(new double[]{1,3,4,5,2,11});

//根据一组对象产生流,但不能产生基本数据类型的流,只能产生对应包装类的流
Stream<String> stream = Stream.of("happy", "sad", "bad", "yes", "no");

//产生从0到9的整数流
IntStream rangeStream=IntStream.range(0,10);

//以第一个参数为种子,迭代产生后面对象的流
Stream<Integer> iterateStream = Stream.iterate(0, i->2*i);

//大部分集合都有stream()方法用来产对应的流
Stream<String> collectionStream = new ArrayList<String>().stream();
Stream<String> parallelStream = new ArrayList<String>().parallelStream();
//通过parallelStream()方法可以产生一个并行流,Java会将操作在多个核心上运行



二、中间操作

中间操作具体包括去重、过滤、映射等操作,值得说明的是,在执行中间操作的代码的时候并不会执行这些操作,而只会把这些操作保存在流里面,每次中间操作都会产生一个新的流对象,保存从开始到现在要进行的所有操作序列,在执行结束操作的时候才会真正执行这些操作,这叫做懒加载

跟踪和调试

peek() 操作的目的是帮助调试。它允许你无修改地查看流中的元素。代码示例:

// streams/Peeking.java
class Peeking {
    public static void main(String[] args) throws Exception {
        Stream.of("Will I eat the apple?".split(" "))
        .map(w -> w + " ")
        .peek(System.out::print)
        .map(String::toUpperCase)
        .peek(System.out::print)
        .map(String::toLowerCase)
        .forEach(System.out::print);
    }
}

输出结果:

Will WILL will I I i eat EAT eat the THE the apple APPLE apple

可以看出流是对每个元素都分别进行同样的操作,就和流水线一样。

流元素排序

在最开始的代码中,我们熟识了 sorted() 的无参数方法。其实它还有另一种形式的实现:传入一个 Comparator 参数。代码示例:

// streams/SortedComparator.java
import java.util.*;
public class SortedComparator {
    public static void main(String[] args) throws Exception {
        //PS:这里的FileToWords是一个可以将文本文件转换成单词流的类
        FileToWords.stream("Cheese.dat")
        .skip(10)
        .limit(10)
        .sorted(Comparator.reverseOrder())
        .map(w -> w + " ")
        .forEach(System.out::print);
    }
}

输出结果:

you what to the that sir leads in district And

sorted() 预设了一些默认的比较器。这里我们使用的是反转“自然排序”。你当然也可以把 Lambda 函数作为参数传递给 sorted()

移除元素

  • distinct():最开始的代码中的 distinct() 可用于消除流中的重复元素。相比创建一个 Set 集合,该方法的效率要高很多。
  • filter(Predicate):过滤操作则会留下使过滤器方法返回值为 true的元素。

在下例中,isPrime() 作为过滤器函数,用于检测质数。

import java.util.stream.*;

public class Prime {
    public static Boolean isPrime(long n) {
        return LongStream.rangeClosed(2, (long)Math.sqrt(n))
            .noneMatch(i -> n % i == 0);
            //如果流中所有元素调用上述方法都返回false,则noneMatch()返回true
    }
    
    public LongStream numbers() {
        return LongStream.iterate(2, i -> i + 1)
            .filter(Prime::isPrime);
    }
    
    public static void main(String[] args) {
        new Prime().numbers()
            .limit(10)
            .forEach(n -> System.out.format("%d ", n));
        System.out.println();
        new Prime().numbers()
            .skip(90)
            .limit(10)
            .forEach(n -> System.out.format("%d ", n));
    }
}

输出结果:

2 3 5 7 11 13 17 19 23 29
467 479 487 491 499 503 509 521 523 541

range()是左闭右开区间不同,rangeClosed() 是闭区间,左右的值都包括。如果不能整除,即余数不等于 0,则 noneMatch() 操作返回 true,如果出现任何等于 0 的结果则返回 false

应用函数到元素

  • map(Function):将原来流中的每个元素都调用参数里的方法,其返回值汇总起来产生一个新的流。
  • mapToInt(ToIntFunction):操作同上,但结果是IntStream
  • mapToLong(ToLongFunction):操作同上,但结果是 LongStream
  • mapToDouble(ToDoubleFunction):操作同上,但结果是 DoubleStream

之前的代码中就多次用到了map方法,只需要知道它可以将流里的所有元素都变成与其对应的新元素就可以了,这里就不进行代码展示了。

最后需要注意的一点是,同一个流只能进行一次操作,例如下面的代码就会报错:

//以第一个参数为种子,迭代产生后面对象的流
Stream<Integer> iterateStream= Stream.iterate(0, i->2*i).limit(10);
iterateStream.map(Integer::doubleValue);
iterateStream.map(Integer::byteValue);  //这句语句会报错



三、结束操作

这些操作接收一个流并产生一个最终结果;它们不会向后面的流提供任何东西。因此,结束操作总是你在管道中做的最后一个操作。

转化为数组

  • toArray():将流转换成适当类型的数组。
  • toArray(generator):在特殊情况下,生成器用于分配自定义的数组存储。

遍历元素

  • forEach(Consumer):你已经看到过很多次 System.out::println 作为 Consumer 函数。
  • forEachOrdered(Consumer): 确保按照原始流的顺序执行。

看着这两种形式,似乎forEach方法并不会按顺序输出,但其实在没有调用parallel()方法之前这两个方法的输出结果都是一样的。

这里稍微简单介绍下 parallel():可实现多处理器并行操作。实现原理是将流分割为多个(通常数目为 CPU 核心数)并在不同处理器上分别执行操作。而进行并行操作的时候,forEach操作无法保证元素按原来的顺序输出,而forEachOrdered则可以确保按原来的顺序输出。

收集

  • collect(Collector):使用 Collector 收集流元素到结果集合中。
  • collect(Supplier, BiConsumer, BiConsumer):收集流元素到结果集合中,第一个参数用于创建一个新的结果集合,第二个参数用于将下一个元素加入到现有结果合集中,第三个参数用于将两个结果合集合并

第一种形式中的的Collector参数,Java核心库为我们提供了很多Collector实现类,都在Collectors这个工具类里面,例如Colletors.toListCollectos.toMapCollections.toCollection等等,基本上都是故名思义,就不介绍了。当然也可以自己写写一个类来继承自Collector来实现自定义的收集要求

但对于自定义的元素收集要求,最好的办法还是采用第二种形式,让我们来看一个示例代码:

import java.util.*;
import java.util.stream.*;
public class SpecialCollector {
    public static void main(String[] args) throws Exception {
        ArrayList<String> words = FileToWords.stream("Cheese.dat")
            .collect(ArrayList::new, ArrayList::add, ArrayList::addAll);
        words.stream()
            .filter(s -> s.equals("cheese"))
            .forEach(System.out::println);
    }
}

匹配

  • allMatch(Predicate) :如果流的每个元素根据提供的 Predicate 都返回 true 时,最终结果返回为 true。这个操作将会在第一个 false 之后短路,也就是不会在发生 false 之后继续执行计算。
  • anyMatch(Predicate):如果流中的任意一个元素根据提供的 Predicate 返回 true 时,最终结果返回为 true。这个操作将会在第一个 true 之后短路,也就是不会在发生 true 之后继续执行计算。
  • noneMatch(Predicate):如果流的每个元素根据提供的 Predicate 都返回 false 时,最终结果返回为 true。这个操作将会在第一个 true 之后短路,也就是不会在发生 true 之后继续执行计算。

元素查找

  • findFirst():返回一个含有第一个流元素的 Optional类型的对象,如果流为空返回 Optional.empty
  • findAny():返回含有任意流元素的 Optional类型的对象,如果流为空返回 Optional.empty

findFirst() 无论流是否为并行化的,总是会选择流中的第一个元素。对于非并行流,findAny()会选择流中的第一个元素(但从定义上来看是选择任意元素)。

统计信息

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