Java~探究JDK1.8新特性Stream的使用

何为Stream

  • Stream是流的概念, 我们只知道IO流是程序与计算机交互的方式, 而Stream就是得益于Lambda所带来的函数式编程的思想, 将集合中的数据看成一个流, 方便我们对其进行遍历筛选操作
  • 比如传统的List或者Map的遍历筛选操作如下
    /**
     * 传统List遍历筛选操作
     */
    public static void everyList() {
        List<String> list = new ArrayList<>();
        list.add("张无忌");
        list.add("周芷若");
        list.add("赵敏");
        list.add("张强");
        list.add("张三丰");

        //得到名字长度为3个汉字的集合
        List<String> list1 = new ArrayList<>();
        for (String name: list
             ) {
            if(name.length() >= 3) {
                list1.add(name);
            }
        }
        //得到姓为张的集合
        List<String> list2 = new ArrayList<>();
        for (String name: list1
        ) {
            if(name.startsWith("张")) {
                list2.add(name);
            }
        }

        System.out.println(list2);
    }
  • 通过上面代码我们就可以发现虽然可读性很好, 但是本来很简单的操作现在变得复杂化, 代码非常冗余
  • 传统的遍历筛选操作着重于怎么做和做什么俩点, 但是你看Stream的写法, 他就是主要注重于做什么
    /**
     * 新特性的遍历筛选操作
     */
    public static void streamList() {
        List<String> list = new ArrayList<>();
        list.add("张无忌");
        list.add("周芷若");
        list.add("赵敏");
        list.add("张强");
        list.add("张三丰");

        list.stream().filter(name -> name.length() >= 3)
                .filter(name -> name.startsWith("张")).forEach(System.out :: println);
    }
  • 直接阅读代码的字面意思即可完美展示无关逻辑方式的语义:获取流、过滤长度为3、过滤姓张、逐一打印。
  • 代码中并没有体现使用线性循环或是其他任何算法进行遍历,我们真正要做的事情内容被更好地体现在代码中。

好处

  • Stream API可以极大提高Java程序员的生产力,让程序员写出高效率、干净、简洁的代码。
  • 这种风格将要处理的元素集合看作一种流, 流在管道中传输, 并且可以在管道的条件节点上进行处理, 比如筛选, 排序,聚合等。
  • 元素流在管道中经过中间操作的处理,最后由最终操作得到前面处理的结果。整体来看,流式思想类似于工厂车间的“生产流水线”。
    -
  • 这里的 filter 、 map 、 skip 都是在对函数模型进行操作是延迟方法,集合元素并没有真正被处理。只有当终结方法 count执行的时候,整个模型才会按照指定策略执行操作。而这得益于Lambda的延迟执行特性。
  • 也就是说Stream你不管对数据进行几次筛选, 他只会在遍历一遍源数据流从而得到我们想要的数据结果.

深入Stream

  1. 一个流可以后面跟随零个或多个 intermediate 操作。其目的主要是打开流,做出某种程度的数据映射/过滤,然后返回一个新的流,交给下一个操作使用。这类操作都是惰性化的(lazy),就是说,仅仅调用到这类方法,并没有真正开始流的遍历。其中最主要的中间操作有:map (mapToInt, flatMap 等)、 filter、 distinct、 sorted、 peek、 limit、 skip、 parallel、 sequential、 unordered
  2. 一个流只能有一个 terminal 操作,当这个操作执行后,流就被使用“光”了,无法再被操作。所以这必定是流的最后一个操作。Terminal 操作的执行,才会真正开始流的遍历,并且会生成一个结果,或者一个 side effect。常用的最终操作有:forEach、 forEachOrdered、 toArray、 reduce、 collect、 min、 max、 count、 anyMatch、 allMatch、 noneMatch、 findFirst、 findAny、 iterator

对比Collection

和以前的Collection操作不同, Stream操作还有两个基础的特征:

  1. Pipelining: 中间操作都会返回流对象本身。 这样多个操作可以串联成一个管道。 这样做可以对操作进行优化, 比如延迟执行.
  2. 内部迭代: 以前对集合遍历都是通过Iterator或者For-Each的方式, 显式的在集合外部进行迭代, 这叫做外部迭代。 Stream提供了内部迭代的方式, 通过访问者模式(Visitor)实现。
  • 当使用一个流的时候,通常包括三个基本步骤:获取一个数据源(source)→ 数据转换→执行操作获取想要的结果,每次转换原有 Stream 对象不改变,返回一个新的 Stream 对象(可以有多次转换),这就允许对其操作可以像链条一样排列,变成一个管道。

获取数据源

  1. 所有的 Collection 集合都可以通过 stream 默认方法获取流;
  2. Stream 接口的静态方法 of 可以获取数组对应的流。

Collection获取流

首先, java.util.Collection 接口中加入了default方法 stream 用来获取流,所以其所有实现类均可获取流.

    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        Stream<String> stream1 = list.stream();

        Set<String> set = new HashSet<>();
        Stream<String> stream2 = set.stream();

        Vector<String> vector = new Vector<>();
        Stream<String> stream3 = vector.stream();
    }

Map获取流

java.util.Map 接口不是 Collection 的子接口,且其K-V数据结构不符合流元素的单一特征,所以获取对应的流需要分key、value或entry等情况:

    public static void main(String[] args){
        Map<String,String> map = new HashMap<>();
        
        Stream<String> keyStream = map.keySet().stream();
        
        Stream<String> valueStream = map.values().stream();
        
        Stream<Map.Entry<String,String>> entrys = map.entrySet().stream();
    }

数组获取流

如果使用的不是集合或映射而是数组,由于数组对象不可能添加默认方法,所以 Stream 接口中提供了静态方法of ,还可以使用使用工具类Arrays, 很简单:

    public static void main(String[] args){
        String[] array = {"私","忆","一","秒","钟"};

        Stream<String> stream = Stream.of(array);

        Arrays.stream(array).forEach(System.out :: println);
    }

PS: of 方法的参数其实是一个可变参数,支持的集合种类很多, 自然数组也就支持

常用方法的使用(重点)

流模型的操作很丰富,这里介绍一些常用的API。这些方法可以被分成两种:

  1. 延迟方法:返回值类型仍然是 Stream 接口自身类型的方法,因此支持链式调用。(除了终结方法外,其余方法均为延迟方法。)
  2. 终结方法:返回值类型不再是 Stream 接口自身类型的方法,因此不再支持类似 StringBuilder 那样的链式调用。终结方法包括 count 和 forEach 方法。

延时方法

过滤:filter

可以通过 filter 方法将一个流转换成另一个子集流。

Stream<T> filter(Predicate<? super T> predicate);

该接口接收一个 Predicate 函数式接口参数(可以是一个Lambda或方法引用)作为筛选条件。

Predicate接口:
java.util.stream.Predicate 函数式接口,其中唯一的抽象方法为:

boolean test(T t);

  • 该方法将会产生一个boolean值结果,代表指定的条件是否满足。如果结果为true,那么Stream流的 filter 方法将会留用元素;如果结果为false,那么 filter 方法将会舍弃元素。
    public static void main(String[] args){
        Stream<String> original = Stream.of("张无忌","张三丰","周芷若");
        Stream<String> result = original.filter(s->s.startsWith("张"));
    }
映射:map

如果需要将流中的元素映射到另一个流中,可以使用 map 方法。

<R> Stream<R> map(Function<? super T, ? extends R> mapper);

该接口需要一个 Function 函数式接口参数,可以将当前流中的T类型数据转换为另一种R类型的流。

Function接口:
java.util.stream.Function 函数式接口,其中唯一的抽象方法为:

R apply(T t);

这可以将一种T类型转换成为R类型,而这种转换的动作,就称为“映射”。

    public static void main(String[] args){
        Stream<String> original = Stream.of("1","2","3");
        Stream<Integer> result = original.map(s->Integer.parseInt(s));
    }
取前几个:limit

limit 方法可以对流进行截取,只取用前n个

Stream<T> limit(long maxSize);

参数是一个long型,如果集合当前长度大于参数则进行截取;否则不进行操作。

    public static void main(String[] args){
        Stream<String> stream = Stream.of("私","忆","一","秒","钟");
        Stream<String> stream2 = stream.limit(2);
        System.out.println(stream2.count());
    }
跳过前几个:skip

如果希望跳过前几个元素,可以使用 skip 方法获取一个截取之后的新流:

Stream<T> skip(long n);

如果流的当前长度大于n,则跳过前n个;否则将会得到一个长度为0的空流。

    public static void main(String[] args){
        Stream<String> stream = Stream.of("私","忆","一","秒","钟");
        Stream<String> stream2 = stream.skip(2);
        System.out.println(stream2.count());
    }

终结方法

统计个数:count

正如旧集合 Collection 当中的 size 方法一样,流提供 count 方法来数一数其中的元素个数:

long count();

该方法返回一个long值代表元素个数(不再像旧集合那样是int值)。

   public static void main(String[] args){
        Stream<String> original = Stream.of("张无忌", "张三丰", "周芷若");
        Stream<String> result = original.filter(s->s.startsWith("张"));
        System.out.println(result.count());
    }
消费元素forEach

虽然方法名字叫 forEach ,但是与for循环中的“for-each”昵称不同。

void forEach(Consumer<? super T> action);

该方法接收一个 Consumer 接口函数,会将流中的每一个元素交给该函数进行处理。
Consumer接口:
java.util.function.Consumer接口是一个消费型接口。接口中包含抽象方法void accept(T t),意为消费一个指定泛型的数据。

    public static void main(String[] args){
        Stream<String> stream = Stream.of("私","忆","一","秒","钟");
        stream.forEach(s->System.out.println(s));
    }
  • 总结Stream
  1. 不是数据结构
  2. 它没有内部存储,它只是用操作管道从 source(数据结构、数组、generator function、IO channel)抓取数据。
  3. 它也绝不修改自己所封装的底层数据结构的数据。例如 Stream 的 filter 操作会产生一个不包含被过滤元素的新 Stream,而不是从 source 删除那些元素。
  4. 所有 Stream 的操作必须以 lambda 表达式为参数
  5. 不支持索引访问
  6. 你可以请求第一个元素,但无法请求第二个,第三个,或最后一个。
  7. 很容易生成数组或者 List
  8. 惰性化, 延时方法不是立即执行
  9. 很多 Stream 操作是向后延迟的,一直到它弄清楚了最后需要多少数据才会开始。
  10. Intermediate 操作永远是惰性化的。
  11. 并行能力
  12. 当一个 Stream 是并行化的,就不需要再写多线程代码,所有对它的操作会自动并行进行的。
  13. 集合有固定大小,Stream 则不必。limit(n) 和 findFirst() 这类的 short-circuiting 操作可以对无限的 Stream 进行运算并很快完成。
  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值