Java8 Stream的简易实现

前言

Java8新增的Stream API是一个强大的特性,它可以简化集合中的常用操作,包括过滤、映射、分组等。下面就来实现一个简易版的Stream。

从表面上看,流似乎和列表很接近,但实际上它们有着本质的区别。

  • 列表是多个元素的容器,当列表被创建出来时,它里面的每个元素也已经被创建出来了。

  • 流是一种计算结构,它封装了内部元素如何产生的计算过程,但是并没有包含实际的元素数据。换句话说,当一个流被创建出来时,它内部的元素并没有被创建,但是我们可以通过调用流的方法来按顺序生成每个元素。

所以,流具有惰性计算的特性,它可以表示普通列表无法表示的一些结构,如无限流。

流的定义

流的定义看起来很像链表,一个流由两部分组成:第一个元素(first)和剩余元素组成的流(remain)。定义如下:

public interface Stream<T> {
    /**
     * 流中第一个元素
     */
    T first();

    /**
     * 剩余元素组成的流
     */
    Stream<T> remain();

    /**
     * 创建流
     * @param firstSupplier 第一个元素的工厂
     * @param remainSupplier 剩余元素组成的流的工厂
     * @param <T> 元素类型
     * @return 流
     */
    static <T> Stream<T> create(Supplier<T> firstSupplier, Supplier<Stream<T>> remainSupplier) {
        return new Stream<>() {
            @Override
            public T first() {
                return firstSupplier.get();
            }

            @Override
            public Stream<T> remain() {
                return remainSupplier.get();
            }
        };
    }
}

这种递归的定义非常有利于使用递归算法来操作流。下面可以看到,流的大多数相关操作都是用递归算法实现的。

假设我们已经有了一个流,那么如何获取流中的元素呢?首先调用first来获取第一个元素,然后调用remain().first()来获取第二个元素,依此类推:

Stream<Integer> stream = ...
Integer first  = stream.first(); // 第一个元素
Integer second = stream.remain().first(); // 第二个元素
Integer third  = stream.remain().remain().first(); // 第三个元素

// 依此类推...

当然,我们不会用这种方法来访问流中的元素。具体如何访问,请继续往下看。

空流

空流是最简单的流,无法从空流中获取任何元素。空流也标志着一个流的结束。下面是空流的实现:

Stream<?> EMPTY = create(
        () -> {throw new IllegalStateException("当前流已结束");},
        () -> {throw new IllegalStateException("当前流已结束");}
);


/**
 * 获取空流
 */
@SuppressWarnings("unchecked")
static <T> Stream<T> empty() {
    return (Stream<T>) EMPTY;
}

/**
 * 判断当前流是否结束
 */
default boolean end() {
    return this == EMPTY;
}

有限流的生成

有限流可以通过多种方式生成,包括从数组生成、从迭代器生成、从集合生成。

从数组生成流

/**
 * 从数组生成流
 * @param arr 数组
 * @param <T> 元素类型
 * @return 流
 */
@SafeVarargs
static <T> Stream<T> of(T... arr) {
    return fromArray(0, arr);
}

/**
 * 从数组和起始索引生成流
 * @param startIndex 起始索引
 * @param arr 数组
 * @param <T> 元素类型
 * @return 流
 */
static <T> Stream<T> fromArray(int startIndex, T[] arr) {
    return startIndex == arr.length
            ? empty()
            : create(() -> arr[startIndex], () -> fromArray(startIndex + 1, arr));
}

从迭代器生成流

/**
 * 从迭代器生成流
 * @param iterator 迭代器
 * @param <T> 元素类型
 * @return 流
 */
static <T> Stream<T> fromIterator(Iterator<T> iterator) {
    return iterator.hasNext()
            ? create(iterator::next, () -> fromIterator(iterator))
            : empty();
}

从集合生成流

/**
 * 从集合生成流
 * @param collection 集合
 * @param <T> 元素类型
 * @return 流
 */
static <T> Stream<T> fromCollection(Collection<T> collection) {
    return fromIterator(collection.iterator());
}

示例

Stream<Integer> s1 = Stream.of(1, 2, 3); // 从数组生成
Stream<Integer> s2 = Stream.fromIterator(List.of(1, 2, 3).iterator()); // 从迭代器生成
Stream<Integer> s3 = Stream.fromCollection(Set.of(1, 2, 3)); // 从集合生成

无限流的生成

无限流意味着流中的元素个数没有限制,也就是永远都不会结束,所以end方法调用永远为false。有以下两种方法生成无限流。

从工厂方法生成流

/**
 * 从工厂方法生成流
 * @param supplier 生成流中元素的工厂方法
 * @param <T> 元素类型
 * @return 流
 */
static <T> Stream<T> fromSupplier(Supplier<T> supplier) {
    return create(supplier, () -> fromSupplier(supplier));
}

从生成器生成流

/**
 * 迭代生成流
 * @param initial 初始值
 * @param generator 生成器
 * @param <T> 元素类型
 * @return 流
 */
static <T> Stream<T> fromGenerator(T initial, UnaryOperator<T> generator) {
    return create(() -> initial, () -> generate(generator.apply(initial), generator));
}

示例

Stream<Integer> s1 = Stream.fromSupplier(() -> 1); // 无限个1组成的流
Stream<Integer> s2 = Stream.fromGenerator(1, n -> n + 1); // 全体自然数组成的流

遍历流中的元素

知道了如何创建流,那么如何遍历或输出流中的元素呢?可以实现下面的forEach方法:

/**
 * 遍历流中所有元素
 * @param consumer 遍历操作
 */
default void forEach(Consumer<T> consumer) {
    Stream<T> s = this;
    while (!s.end()) {
        consumer.accept(s.first());
        s = s.remain();
    }
}

然后就可以像下面这样输出流中的元素:

Stream<Integer> s = Stream.of(1, 2, 3, 4, 5);
s.forEach(System.out::println); // 输出1 2 3 4 5

流的截断和偏移

上面的forEach方法只适用于有限流,如果在无限流上调用forEach方法,会导致死循环。所以,我们需要对无限流进行截取操作,这样就能做到遍历无限流的一部分。

/**
 * 截取流中前n个元素
 * @param n 要截取的元素个数
 * @return 流
 */
default Stream<T> limit(int n) {
    return n <= 0 || end()
            ? empty()
            : create(this::first, () -> remain().limit(n - 1));
}

/**
 * 跳过流中的元素
 * @param n 跳过的个数
 * @return 流
 */
default Stream<T> skip(int n) {
    return end() || n <= 0
            ? this
            : remain().skip(n - 1);
}

limit用于提取流的前n个元素,skip用于忽略流的前n个元素,有了这两个方法,我们就能随心所欲地截取任何流中的任意一段。

流的变换操作

熟悉Java8 Stream API的读者一定用过mapfilter这两个常用的流操作,下面我们就来实现它们。

map

map用于对流中的所有元素进行转换操作。

/**
 * 映射流中的元素
 * @param mapper 映射器
 * @param <U> 映射后的元素类型
 * @return 流
*/
default <U> Stream<U> map(Function<T, U> mapper) {
    return end()
            ? empty()
            : create(() -> mapper.apply(first()), () -> remain().map(mapper));
}

filter

filter用于过滤流中的元素。

/**
 * 过滤流中的元素
 * @param predicate 断言
 * @return 流
 */
default Stream<T> filter(Predicate<T> predicate) {
    if (end()) {
        return empty();
    }
    T e = first();
    if (predicate.test(e)) {
        return Stream.create(() -> e, () -> remain().filter(predicate));
    } else {
        return remain().filter(predicate);
    }
}

示例

Stream<String> s = Stream.of(1, 2, 3, 4, 5, 6)
        .filter(n -> n % 2 == 0) // 2, 4, 6
        .map(n -> "hello " + n); // hello 2, hello 4, hello 6

流的聚合操作

有时候我们像将整个流聚合成某种数据结构,如列表、集合等,这就需要用到流的聚合操作。

collect

对流进行自定义聚合操作。

/**
 * 流的聚合操作
 * @param initial 初始值
 * @param accumulator 聚合操作
 * @param <U> 聚合后的类型
 * @return 流
 */
default <U> U collect(U initial, BiFunction<U, T, U> accumulator) {
    U result = initial;
    Stream<T> s = this;
    while (!s.end()) {
        result = accumulator.apply(result, s.first());
        s = s.remain();
    }
    return result;
}

toList

将流转换成列表。

/**
 * 将流转换成列表
 * @return 列表
 */
default List<T> toList() {
    return collect(new ArrayList<>(), (list, e) -> {
        list.add(e);
        return list;
    });
}

toSet

将流转换成集合。

/**
 * 将流转换成集合
 * @return 集合
 */
default Set<T> toSet() {
    return collect(new HashSet<>(), (set, e) -> {
        set.add(e);
        return set;
    });
}

toMap

将流转换成Map

/**
 * 将流转换成map
 * @param keyGenerator key生成器
 * @param valueGenerator value生成器
 * @param <K> key的类型
 * @param <V> value的类型
 * @return map
 */
default <K, V> Map<K, V> toMap(Function<T, K> keyGenerator, Function<T, V> valueGenerator) {
    return collect(new HashMap<>(), (map, e) -> {
        map.put(keyGenerator.apply(e), valueGenerator.apply(e));
        return map;
    });
}

count

对流中的元素进行计数。

/**
 * 获取流中元素个数
 * @return 元素个数
 */
default int count() {
    return collect(0, (cnt, e) -> cnt + 1);
}

流的高级操作

下面是流的一些高级操作。

concat

concat用于将两个流首尾连接在一起。

/**
 * 首尾连接两个流
 * s1[0] -> s1[1] -> s2[2] ->  ... -> s2[0] -> s2[1] -> s2[2] -> ...
 * @param s1 s1
 * @param s2 s2
 * @param <T> 元素类型
 * @return 流
 */
static <T> Stream<T> concat(Stream<T> s1, Stream<T> s2) {
    return s1.end()
            ? s2
            : create(s1::first, () -> concat(s1.remain(), s2));
}

/**
 * 首尾连接两个流
 * @param s 要连接的流
 * @return 流
 */
default Stream<T> concat(Stream<T> s) {
    return concat(this, s);
}

示例:

Stream<Integer> s1 = Stream.of(1, 2, 3, 4);
Stream<Integer> s2 = Stream.of(5, 6, 7);
Stream<Integer> s = s1.concat(s2); // 1, 2, 3, 4, 5, 6, 7

interleave

interleave用于将两个流交错连接在一起。

/**
 * 交错连接两个流
 * s1[0] -> s2[0] -> s1[1] -> s2[1] -> ...
 * @param s1 s1
 * @param s2 s2
 * @param <T> 元素类型
 * @return 流
 */
static <T> Stream<T> interleave(Stream<T> s1, Stream<T> s2) {
    return s1.end()
            ? s2
            : create(s1::first, () -> interleave(s2, s1.remain()));
}

/**
 * 交错连接两个流
 * @param s 要连接的流
 * @return 流
 */
default Stream<T> interleave(Stream<T> s) {
    return interleave(this, s);
}

示例:

Stream<Integer> s1 = Stream.of(1, 3, 5, 7);
Stream<Integer> s2 = Stream.of(2, 4, 6);
Stream<Integer> s = s1.interleave(s2); // 1, 2, 3, 4, 5, 6, 7

flatMap

flatMap用于将流中的每个元素都映射成一个流,然后将所有流连接起来。

/**
 * 扁平化流
 * @param mapper 元素到流的映射器
 * @param <U> 扁平化后的元素类型
 * @return 流
 */
default <U> Stream<U> flatMap(Function<T, Stream<U>> mapper) {
    return collect(empty(), (s, e) -> s.concat(mapper.apply(e)));
}

示例:

Stream<Integer> s = Stream.of(10, 20)
        .flatMap(n -> Stream.of(n + 1, n + 2, n + 3)); // 11, 12, 13, 21, 22, 23

后记

完整代码:https://github.com/byx2000/simple-stream

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

推荐阅读更多精彩内容

  • stream描述 用于支持元素流上的功能样式操作的类,例如集合上的map-reduce转换。例如: 这里我们使用w...
    yyg阅读 339评论 0 0
  • 流是Java API的新成员,它允许你以声明性方式处理数据集合,还可以透明地并行处理;你可以把它们看成遍历数据集的...
    夏与清风阅读 870评论 0 0
  • 筛选和切片 filter 方法 distinct 方法 limit 方法 skip 方法 谓词筛选 Stream ...
    刘涤生阅读 2,997评论 2 4
  • Java 8 中的 Stream 是对集合(Collection)对象功能的增强,它专注于对集合对象进行各种非常便...
    Gojo99阅读 457评论 0 0
  • 使用流操作来表达复杂的数据处理查询。 集合是Java中使用多的API。要是没有集合,还能做什么呢?几乎每个Java...
    bern85阅读 866评论 0 3