JDK8特性——Stream API

集合处理数据的弊端

当我们在需要对集合中的元素进行操作的时候,除了必需的添加,删除,获取外,最典型的操作就是集合遍历,

public class StreamTest01 {
    
    

    public static void main(String[] args) {
    
    
        // 定义一个List集合
        List<String> list = Arrays.asList("张三","张三丰","成龙","周星驰");
        // 1.获取所有 姓张的信息
        List<String> list1 = new ArrayList<>();
        for (String s : list) {
    
    
            if(s.startsWith("张")){
    
    
                list1.add(s);
            }
        }

        // 2.获取名称长度为3的用户
        List<String> list2 = new ArrayList<>();
        for (String s : list1) {
    
    
            if(s.length() == 3){
    
    
                list2.add(s);
            }
        }

        // 3. 输出所有的用户信息
        for (String s : list2) {
    
    
            System.out.println(s);
        }
    }
}

上面的代码针对与我们不同的需求总是一次次的循环循环循环,我们希望有更加高效的处理方式,这时我们就可以通过JDK8中提供的Stream API来解决这个问题了。

Stream更加优雅的解决方案:

public class StreamTest02 {
    
    

    public static void main(String[] args) {
    
    
        // 定义一个List集合
        List<String> list = Arrays.asList("张三","张三丰","成龙","周星驰");
        System.out.println("----------");
        list.stream()
                .filter(s->s.startsWith("张"))
                .filter(s->s.length() == 3)
                .forEach(System.out::println);
    }
}

上面的SteamAPI代码的含义:获取流,过滤张,过滤长度,逐一打印。相比于上面的案例更加的简洁直观。

Steam流式思想概述

注意:Stream和IO流(InputStream/OutputStream)没有任何关系,请暂时忘记对传统IO流的固有印象!
Stream流式思想类似于工厂车间的“生产流水线”,Stream流不是一种数据结构,不保存数据,而是对数据进行加工
处理。Stream可以看作是流水线上的一个工序。在流水线上,通过多个工序让一个原材料加工成一个商品。
在这里插入图片描述
对应到集合数据就是这样:
在这里插入图片描述

Stream API能快速完成许多复杂的操作,如筛选、切片、映射、查找、去除重复,统计,匹配和归约。

Stream流的获取方式

根据Collection获取

java.util.Collection 接口中加入了default方法 stream,也就是说Collection接口下的所有的实现都可以通过steam方法来获取Stream流。

    public static void main(String[] args) {
    
    
        List<String> list = new ArrayList<>();
        list.stream();
        Set<String> set = new HashSet<>();
        set.stream();
        Vector vector = new Vector();
        vector.stream();
    }

但是Map接口别没有实现Collection接口,那这时怎么办呢?这时我们可以根据Map获取对应的key value的集合。

    public static void main(String[] args) {
    
    
        Map<String,Object> map = new HashMap<>();
        Stream<String> stream = map.keySet().stream(); // key
        Stream<Object> stream1 = map.values().stream(); // value
        Stream<Map.Entry<String, Object>> stream2 = map.entrySet().stream(); // entry
    }

通过Stream的of方法

在实际开发中我们不可避免的还是会操作到数组中的数据,由于数组对象不可能添加默认方法,所有Stream接口中提供了静态方法of

扫描二维码关注公众号,回复: 16662693 查看本文章
public class StreamTest05 {
    
    

    public static void main(String[] args) {
    
    
        Stream<String> a1 = Stream.of("a1", "a2", "a3");
        String[] arr1 = {
    
    "aa","bb","cc"};
        Stream<String> arr11 = Stream.of(arr1);
        Integer[] arr2 = {
    
    1,2,3,4};
        Stream<Integer> arr21 = Stream.of(arr2);
        arr21.forEach(System.out::println);
        // 注意:基本数据类型的数组是不行的
        int[] arr3 = {
    
    1,2,3,4};
        Stream.of(arr3).forEach(System.out::println);
    }
}

Stream常用方法介绍

Stream流模型的操作很丰富,这里介绍一些常用的API。

方法名 方法作用 返回值类型 方法种类
count 统计个数 long 终结
forEach 逐一处理 void 终结
filter 过滤 Stream 函数拼接
limit 取用前几个 Stream 函数拼接
skip 跳过前几个 Stream 函数拼接
map 映射 Stream 函数拼接
concat 组合 Stream 函数拼接

这些方法可以被分成两种:

终结方法:返回值类型不再是 Stream 类型的方法,不再支持链式调用。本小节中,终结方法包括 count 和 forEach 方法。

非终结方法:返回值类型仍然是 Stream 类型的方法,支持链式调用。

Stream注意事项(重要):

  1. Stream只能操作一次
  2. Stream方法返回的是新的流
  3. Stream不调用终结方法,中间的操作不会执行

forEach

forEach用来遍历流中的数据

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

该方法接受一个Consumer接口,会将每一个流元素交给函数处理

    public static void main(String[] args) {
    
    
        Stream.of("a1", "a2", "a3").forEach(System.out::println);;
    }

count

Stream流中的count方法用来统计其中的元素个数的

long count();

该方法返回一个long值,代表元素的个数。

    public static void main(String[] args) {
    
    
        long count = Stream.of("a1", "a2", "a3").count();
        System.out.println(count);
    }

filter

filter方法的作用是用来过滤数据的。返回符合条件的数据

在这里插入图片描述

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

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

该接口接收一个Predicate函数式接口参数作为筛选条件

    public static void main(String[] args) {
    
    
         Stream.of("a1", "a2", "a3","bb","cc","aa","dd")
                 .filter((s)->s.contains("a"))
                 .forEach(System.out::println);

    }

limit

limit方法可以对流进行截取处理,支取前n个数据,
在这里插入图片描述

Stream<T> limit(long maxSize);

参数是一个long类型的数值,如果集合当前长度大于参数就进行截取,否则不操作:

    public static void main(String[] args) {
    
    
         Stream.of("a1", "a2", "a3","bb","cc","aa","dd")
                 .limit(3)
                 .forEach(System.out::println);

    }

skip

如果希望跳过前面几个元素,可以使用skip方法获取一个截取之后的新流
在这里插入图片描述

   Stream<T> skip(long n);
    public static void main(String[] args) {
    
    
         Stream.of("a1", "a2", "a3","bb","cc","aa","dd")
                 .skip(3)
                 .forEach(System.out::println);

    }

map

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

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

在这里插入图片描述

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

    public static void main(String[] args) {
    
    
         Stream.of("1", "2", "3","4","5","6","7")
                 .map(Integer::parseInt)
                 .forEach(System.out::println);

    }

sorted

如果需要将数据排序,可以使用sorted方法:

    Stream<T> sorted();

在使用的时候可以根据自然规则排序,也可以通过比较强来指定对应的排序规则

    public static void main(String[] args) {
    
    
         Stream.of("1", "3", "2","4","0","9","7")
                 .map(Integer::parseInt)
                 //.sorted() // 根据数据的自然顺序排序
                 .sorted((o1,o2)->o2-o1) // 根据比较强指定排序规则
                 .forEach(System.out::println);

    }

distinct

如果要去掉重复数据,可以使用distinct方法:

    Stream<T> distinct();

在这里插入图片描述

    public static void main(String[] args) {
    
    
         Stream.of("1", "3", "3","4","0","1","7")
                 //.map(msg->Integer.parseInt(msg))
                 .map(Integer::parseInt)
                 //.sorted() // 根据数据的自然顺序排序
                 .sorted((o1,o2)->o2-o1) // 根据比较强指定排序规则
                 .distinct() // 去掉重复的记录
                 .forEach(System.out::println);
    }

Stream流中的distinct方法对于基本数据类型是可以直接出重的,但是对于自定义类型,我们是需要重写hashCode和equals方法来移除重复元素。

match

如果需要判断数据是否匹配指定的条件,可以使用match相关的方法

boolean anyMatch(Predicate<? super T> predicate); // 元素是否有任意一个满足条件
boolean allMatch(Predicate<? super T> predicate); // 元素是否都满足条件
boolean noneMatch(Predicate<? super T> predicate); // 元素是否都不满足条件
    public static void main(String[] args) {
    
    
        boolean b = Stream.of("1", "3", "3", "4", "5", "1", "7")
                .map(Integer::parseInt)
                //.allMatch(s -> s > 0)
                //.anyMatch(s -> s >4)
                .noneMatch(s -> s > 4)
                ;
        System.out.println(b);
    }

match是一个终结方法

find

如果我们需要找到某些数据,可以使用find方法来实现

    Optional<T> findFirst();
    Optional<T> findAny();

在这里插入图片描述

    public static void main(String[] args) {
    
    

        Optional<String> first = Stream.of("1", "3", "3", "4", "5", "1", "7").findFirst();
        System.out.println(first.get());

        Optional<String> any = Stream.of("1", "3", "3", "4", "5", "1", "7").findAny();
        System.out.println(any.get());
    }

max和min

在这里插入图片描述
如果我们想要获取最大值和最小值,那么可以使用max和min方法

Optional<T> min(Comparator<? super T> comparator);
Optional<T> max(Comparator<? super T> comparator);

使用

    public static void main(String[] args) {
    
    

        Optional<Integer> max = Stream.of("1", "3", "3", "4", "5", "1", "7")
                .map(Integer::parseInt)
                .max((o1,o2)->o1-o2);
        System.out.println(max.get());
    }

reduce

在这里插入图片描述

如果需要将所有数据归纳得到一个数据,可以使用reduce方法

T reduce(T identity, BinaryOperator<T> accumulator);
    public static void main(String[] args) {
    
    
        Integer sum = Stream.of(4, 5, 3, 9)
                // identity默认值
                // 第一次的时候会将默认值赋值给x
                // 之后每次会将 上一次的操作结果赋值给x y就是每次从数据中获取的元素
                .reduce(0, (x, y) -> {
    
    
                    System.out.println("x="+x+",y="+y);
                    return x + y;
                });
    }

map和reduce的组合

在实际开发中我们经常会将map和reduce一块来使用

    public static void main(String[] args) {
    
    
        // 1.求出所有年龄的总和
        Integer sumAge = Stream.of(
                new Person("张三", 18)
                , new Person("李四", 22)
                , new Person("张三", 13)
                , new Person("王五", 15)
                , new Person("张三", 19)
        ).map(Person::getAge) // 实现数据类型的转换
                .reduce(0, Integer::sum);
        System.out.println(sumAge);
    }

mapToInt

如果需要将Stream中的Integer类型转换成int类型,可以使用mapToInt方法来实现

    public static void main(String[] args) {
    
    
        // 为了提高程序代码的效率,我们可以先将流中Integer数据转换为int数据,然后再操作
        IntStream intStream = Stream.of(arr)
                .mapToInt(Integer::intValue);
        intStream.filter(i->i>3)
                .forEach(System.out::println);

    }

concat

如果有两个流,希望合并成为一个流,那么可以使用Stream接口的静态方法concat

    public static <T> Stream<T> concat(Stream<? extends T> a, Stream<? extends T> b) {
    
    
        Objects.requireNonNull(a);
        Objects.requireNonNull(b);

        @SuppressWarnings("unchecked")
        Spliterator<T> split = new Streams.ConcatSpliterator.OfRef<>(
                (Spliterator<T>) a.spliterator(), (Spliterator<T>) b.spliterator());
        Stream<T> stream = StreamSupport.stream(split, a.isParallel() || b.isParallel());
        return stream.onClose(Streams.composedClose(a, b));
    }
    public static void main(String[] args) {
    
    
        Stream<String> stream1 = Stream.of("a","b","c");
        Stream<String> stream2 = Stream.of("x", "y", "z");
        // 通过concat方法将两个流合并为一个新的流
        Stream.concat(stream1,stream2).forEach(System.out::println);
    }

Stream结果收集

结果收集到集合

    /**
     * Stream结果收集
     *    收集到集合中
     */
    @Test
    public void test01(){
    
    
        // Stream<String> stream = Stream.of("aa", "bb", "cc");
        List<String> list = Stream.of("aa", "bb", "cc","aa")
                .collect(Collectors.toList());
        System.out.println(list);
        // 收集到 Set集合中
        Set<String> set = Stream.of("aa", "bb", "cc", "aa")
                .collect(Collectors.toSet());
        System.out.println(set);
    }

结果收集到数组

Stream中提供了toArray方法来将结果放到一个数组中,返回值类型是Object[],如果我们要指定返回的类型,那么可以使用另一个重载的toArray(IntFunction f)方法

    /**
     * Stream结果收集到数组中
     */
    @Test
    public void test02(){
    
    
        Object[] objects = Stream.of("aa", "bb", "cc", "aa")
                .toArray(); // 返回的数组中的元素是 Object类型
        System.out.println(Arrays.toString(objects));
        // 如果我们需要指定返回的数组中的元素类型
        String[] strings = Stream.of("aa", "bb", "cc", "aa")
                .toArray(String[]::new);
        System.out.println(Arrays.toString(strings));

    }

结果聚合计算

当我们使用Stream流处理数据后,可以像数据库的聚合函数一样对某个字段进行操作,比如获得最大值,最小值,求和,平均值,统计数量。

    /**
     * Stream流中数据的聚合计算
     */
    @Test
    public void test03(){
    
    
        // 获取年龄的最大值
        Optional<Person> maxAge = Stream.of(
                new Person("张三", 18)
                , new Person("李四", 22)
        ).collect(Collectors.maxBy((p1, p2) -> p1.getAge() - p2.getAge()));
        System.out.println("最大年龄:" + maxAge.get());
        // 获取年龄的最小值
        Optional<Person> minAge = Stream.of(
                new Person("张三", 18)
                , new Person("李四", 22)
        ).collect(Collectors.minBy((p1, p2) -> p1.getAge() - p2.getAge()));
        System.out.println("最新年龄:" + minAge.get());
        // 求所有人的年龄之和
        Integer sumAge = Stream.of(
                new Person("张三", 18)
                , new Person("李四", 22)
        )
                //.collect(Collectors.summingInt(s -> s.getAge()))
                .collect(Collectors.summingInt(Person::getAge))
                ;
        System.out.println("年龄总和:" + sumAge);
        // 年龄的平均值
        Double avgAge = Stream.of(
                new Person("张三", 18)
                , new Person("李四", 22)
        ).collect(Collectors.averagingInt(Person::getAge));
        System.out.println("年龄的平均值:" + avgAge);
        // 统计数量
        Long count = Stream.of(
                new Person("张三", 18)
                , new Person("李四", 22)
        ).filter(p->p.getAge() > 18)
                .collect(Collectors.counting());
        System.out.println("满足条件的记录数:" + count);

    }

结果分组

当我们使用Stream流处理数据后,可以根据某个属性将数据分组

    /**
     * 分组计算
     */
    @Test
    public void test04(){
    
    
        // 根据账号对数据进行分组
        Map<String, List<Person>> map1 = Stream.of(
                new Person("张三", 18, 175)
                , new Person("李四", 22, 177)
        ).collect(Collectors.groupingBy(Person::getName));
        map1.forEach((k,v)-> System.out.println("k=" + k +"\t"+ "v=" + v));
    }

多级分组: 先根据name分组然后根据年龄分组

    /**
     * 分组计算--多级分组
     */
    @Test
    public void test05(){
    
    
        // 先根据name分组,然后根据age(成年和未成年)分组
        Map<String,Map<Object,List<Person>>> map =  Stream.of(
                new Person("张三", 18, 175)
                , new Person("李四", 22, 177)
        ).collect(Collectors.groupingBy(
                Person::getName
                ,Collectors.groupingBy(p->p.getAge()>=18?"成年":"未成年"
                )
        ));
        map.forEach((k,v)->{
    
    
            System.out.println(k);
            v.forEach((k1,v1)->{
    
    
                System.out.println("\t"+k1 + "=" + v1);
            });
        });
    }

结果分区

Collectors.partitioningBy会根据值是否为true,把集合中的数据分割为两个列表,一个true列表,一个false列表
在这里插入图片描述

    /**
     * 分区操作
     */
    @Test
    public void test06(){
    
    
        Map<Boolean, List<Person>> map = Stream.of(
                new Person("张三", 18, 175)
                , new Person("李四", 22, 177)
        ).collect(Collectors.partitioningBy(p -> p.getAge() > 18));
        map.forEach((k,v)-> System.out.println(k+"\t" + v));
    }

结果拼接

Collectors.joining会根据指定的连接符,将所有的元素连接成一个字符串

    /**
     * 对流中的数据做拼接操作
     */
    @Test
    public void test07(){
    
    
        String s1 = Stream.of(
                new Person("张三", 18, 175)
                , new Person("李四", 22, 177)
                , new Person("张三", 14, 165)
                , new Person("李四", 15, 166)
                , new Person("张三", 19, 182)
        ).map(Person::getName)
                .collect(Collectors.joining());
        // 张三李四张三李四张三
        System.out.println(s1);
    

并行Stream流

前面使用的Stream流都是串行,也就是在一个线程上面执行。

    /**
     * 串行流
     */
    @Test
    public void test01(){
    
    
        Stream.of(5,6,8,3,1,6)
                .filter(s->{
    
    
                    System.out.println(Thread.currentThread() + "" + s);
                    return s > 3;
                }).count();
    }

parallelStream就是一个并行执行的流,它通过默认的ForkJoinPool,可以提高多线程任务的速度。

获取并行流

我们可以通过两种方式来获取并行流。

  1. 通过List接口中的parallelStream方法来获取
  2. 通过已有的串行流转换为并行流(parallel)
    /**
     * 获取并行流的两种方式
     */
    @Test
    public void test02(){
    
    
        List<Integer> list = new ArrayList<>();
        // 通过List 接口 直接获取并行流
        Stream<Integer> integerStream = list.parallelStream();
        // 将已有的串行流转换为并行流
        Stream<Integer> parallel = Stream.of(1, 2, 3).parallel();
    }

并行流操作

    /**
     * 并行流操作
     */
    @Test
    public void test03(){
    
    

        Stream.of(1,4,2,6,1,5,9)
                .parallel() // 将流转换为并发流,Stream处理的时候就会通过多线程处理
                .filter(s->{
    
    
                    System.out.println(Thread.currentThread() + " s=" +s);
                    return s > 2;
                }).count();
    }

并行流和串行流对比

通过for循环串行Stream流和并行Stream流来对500000000亿个数字求和,看消耗时间


public class Test03 {
    
    

    private static long times = 500000000;

    private  long start;

    @Before
    public void befor(){
    
    
        start = System.currentTimeMillis();
    }

    @After
    public void end(){
    
    
        long end = System.currentTimeMillis();
        System.out.println("消耗时间:" + (end - start));
    }

    /**
     * 普通for循环 消耗时间:138
     */
    @Test
    public void test01(){
    
    
        System.out.println("普通for循环:");
        long res = 0;
        for (int i = 0; i < times; i++) {
    
    
            res += i;
        }
    }

    /**
     * 串行流处理
     *   消耗时间:203
     */
    @Test
    public void test02(){
    
    
        System.out.println("串行流:serialStream");
        LongStream.rangeClosed(0,times)
                .reduce(0,Long::sum);
    }

    /**
     * 并行流处理 消耗时间:84
     */
    @Test
    public void test03(){
    
    
        LongStream.rangeClosed(0,times)
                .parallel()
                .reduce(0,Long::sum);
    }
}

通过案例可以看到parallelStream的效率是最高的。

Stream并行处理的过程会分而治之,也就是将一个大的任务切分成了多个小任务,这表示每个任务都是一个线程操作。

线程安全问题

在多线程的处理下,肯定会出现数据安全问题。如下:

    @Test
    public void test01(){
    
    
        List<Integer> list = new ArrayList<>();
        for (int i = 0; i < 1000; i++) {
    
    
            list.add(i);
        }
        System.out.println(list.size());
        List<Integer> listNew = new ArrayList<>();
        // 使用并行流来向集合中添加数据
        list.parallelStream()
                //.forEach(s->listNew.add(s));
                .forEach(listNew::add);
        // 839
        System.out.println(listNew.size());
    }

输出或者直接抛异常

java.lang.ArrayIndexOutOfBoundsException
	at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
	at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
	at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
	at java.lang.reflect.Constructor.newInstance(Constructor.java:423)
	at java.util.concurrent.ForkJoinTask.getThrowableException(ForkJoinTask.java:598)
....
Caused by: java.lang.ArrayIndexOutOfBoundsException: 366
	at java.util.ArrayList.add(ArrayList.java:463)

针对这个问题,我们的解决方案有哪些呢?

  1. 加同步锁
  2. 使用线程安全的容器
  3. 通过Stream中的toArray/collect操作

加同步锁

    /**
     * 
     */
    @Test
    public void test02(){
    
    
        List<Integer> listNew = new ArrayList<>();
        Object obj = new Object();
        IntStream.rangeClosed(1,1000)
                .parallel()
                .forEach(i->{
    
    
                    synchronized (obj){
    
    
                        listNew.add(i);
                    }

                });
        System.out.println(listNew.size());
    }

使用线程安全的容器

     /**
     * 将线程不安全的容器转换为线程安全的容器
     */
    @Test
    public void test04(){
    
    
        List<Integer> listNew = new ArrayList<>();
        // 将线程不安全的容器包装为线程安全的容器
        List<Integer> synchronizedList = Collections.synchronizedList(listNew);
        Object obj = new Object();
        IntStream.rangeClosed(1,1000)
                .parallel()
                .forEach(i->{
    
    
                        synchronizedList.add(i);
                });
        System.out.println(synchronizedList.size());
    }

通过Stream中的toArray/collect操作

    /**
     * 我们还可以通过Stream中的 toArray方法或者 collect方法来操作
     * 就是满足线程安全的要求
     */
    @Test
    public void test05(){
    
    
        List<Integer> listNew = new ArrayList<>();
        Object obj = new Object();
        List<Integer> list = IntStream.rangeClosed(1, 1000)
                .parallel()
                .boxed()
                .collect(Collectors.toList());
        System.out.println(list.size());
    }

猜你喜欢

转载自blog.csdn.net/qq_28314431/article/details/132908119