【jdk8新特性】Stream流

【jdk8新特性】Stream流

00.前言

如果有任何问题请指出,感谢。

01.作用

对集合进行处理操作的方法

stream流的特性如下

Stream流不是一种数据结构,不保存数据,它只是在原数据集上定义了一组操作。
这些操作是惰性的,即每当访问到流中的一个元素,才会在此元素上执行这一系列操作。
Stream不保存数据,故每个Stream流只能使用一次。
stream流的方法有很多中,大致分为延迟方法和终结方法。

Stream流属于管道流,只能被消费(使用)一次
第一个stream流调用完毕方法,数据就会流转到下一个Stream上(如果没有下一个流则直接关闭)
而这时第一个stream流已经使用完毕,就会关闭了
所以第一个Stream流就不能再调用方法

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

02.传统处理集合的方法进行对比

传统方法

public static void main(String[] args) throws InterruptedException {
        // 一个ArrayList集合中存储有以下数据:张无忌,周芷若,赵敏,张强,张三丰
        // 需求:1.拿到所有姓张的 2.拿到名字长度为3个字的 3.打印这些数据
        ArrayList<String> list = new ArrayList<>();
        Collections.addAll(list, "张无忌", "周芷若", "赵敏", "张强", "张三丰");
        // 1.拿到所有姓张的
        ArrayList<String> zhangList = new ArrayList<>(); // {"张无忌", "张强", "张三丰"}
        for (String name : list) {
            if (name.startsWith("张")) {
                zhangList.add(name);
            }
        }

        // 2.拿到名字长度为3个字的
        ArrayList<String> threeList = new ArrayList<>(); // {"张无忌", "张三丰"}
        for (String name : zhangList) {
            if (name.length() == 3) {
                threeList.add(name);
            }
        }

        // 3.打印这些数据
        for (String name : threeList) {
            System.out.println(name);
        }

    }

结果

张无忌
张三丰

Stream流及其简化

public static void main(String[] args) throws InterruptedException {
    // 一个ArrayList集合中存储有以下数据:张无忌,周芷若,赵敏,张强,张三丰
    // 需求:1.拿到所有姓张的 2.拿到名字长度为3个字的 3.打印这些数据
    ArrayList<String> list = new ArrayList<>();
    Collections.addAll(list, "张无忌", "周芷若", "赵敏", "张强", "张三丰");

    // 1.拿到所有姓张的 2.拿到名字长度为3个字的 3.打印这些数据
    list.stream()
            .filter((s) -> {
                return s.startsWith("张");
            })
            .filter((s) -> {
                return s.length() == 3;
            })
            .forEach((s) -> {
                System.out.println(s);
            });

    System.out.println("-----------");

    list.stream()
            .filter(s -> s.startsWith("张"))
            .filter(s -> s.length() == 3)
            .forEach(s -> System.out.println(s));

    System.out.println("-----------");

    list.stream()
            .filter(s -> s.startsWith("张"))
            .filter(s -> s.length() == 3)
            .forEach(System.out::println);
    System.out.println("-----------");

}

结果

张无忌

张三丰

张无忌

张三丰

张无忌

张三丰

可以看出相同情况下,Stream流对集合的处理更加的简单并且便于理解,Stream流通常与lambda表达式配合使用。

03.获取Stream流的几种方式

例子

public static void main(String[] args) {
    // 方式1 : 根据Collection获取流
    // Collection接口中有一个默认的方法: default Stream stream()
    List<String> list = new ArrayList<>();
    Stream<String> stream1 = list.stream();

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

    Map<String, String> map = new HashMap<>();
    Stream<String> stream3 = map.keySet().stream();
    Stream<String> stream4 = map.values().stream();
    Stream<Map.Entry<String, String>> stream5 = map.entrySet().stream();

    // 方式2 : Stream中的静态方法of获取流
    // static Stream of(T... values)
    Stream<String> stream6 = Stream.of("aa", "bb", "cc");

    String[] strs = {"aa", "bb", "cc"};
    Stream<String> stream7 = Stream.of(strs);

    // 基本数据类型的数组行不行?不行的,会将整个数组看做一个元素进行操作.
    int[] arr = {11, 22, 33};
    Stream<int[]> stream8 = Stream.of(arr);
    
    // 装箱后的数据类型的数组行不行?可以,操作的元素就是数组里面的每个元素
    Integer[] ar = {11, 22, 33};
    Stream<Integer> stream9 = Stream.of(ar);
}

几个注意点:

  1. 创建方式有 集合.stream(),Stream.of(元素集合)

  2. 基本数据类型的集合 如果创建流操作的是这个集合本身 也就是集合是一个元素。

  3. 装箱后的数据类型的集合 创建流操作的是这个集合里面的元素。

  4. 因为map不是集合,所有操作map只能先取出map的key或value集合 然后再进行流操作

04.操作Stream流的注意点

例子

public static void main(String[] args) {
    Stream<String> stream = Stream.of("aa", "bb", "cc");
    // 1. Stream只能操作一次
    // long count = stream.count();
    // long count2 = stream.count();

    // 2. Stream方法返回的是新的流
    // Stream limit = stream.limit(1);
    // System.out.println("stream" + stream);
    // System.out.println("limit" + limit);

    // 3. Stream不调用终结方法,中间的操作不会执行
    stream.filter((s) -> {
        System.out.println(s);
        return true;
    }).count();
}

注意点

上述例子中有三个注意点

  1. Stream只能操作一次。这个的意思是指同一个集合的流只能被操作一次,一个流只能被连续操作 也就是链式编程,而不能再次调用这个流

  2. Stream方法返回的是新的流。也就是Stream流处理后返回的流是新的流可以再被操作

  3. Stream不调用终结方法,中间的操作不会执行。

补充终结方法

count(),用于记录集合元素个数

forEach(),遍历集合

05.【重点】Stream流常用方法:

forEach方法:终结方法

**作用:**遍历集合元素

例子

@Test
public void testForEach() {
    List<String> one = new ArrayList<>();
    Collections.addAll(one, "迪丽热巴", "宋远桥", "苏星河", "老子", "庄子", "孙子");

    /*// 得到流
    // 调用流中的方法
    one.stream().forEach((String str) -> {
        System.out.println(str);
    });

    // Lambda可以省略
    one.stream().forEach(str -> System.out.println(str));*/

    // Lambda可以转成方法引用
    one.stream().forEach(System.out::println);

}

可以看出利用lambda表达式可以把遍历操作大幅度简化

count方法:终结方法

**作用:**返回集合元素个数

例子

@Test
public void testCount() {
    List<String> one = new ArrayList<>();
    Collections.addAll(one, "迪丽热巴", "宋远桥", "苏星河", "老子", "庄子", "孙子");

    long count = one.stream().count();
    System.out.println(count);
}

filter方法:

**作用:**过滤集合中不符合要求的元素

例子

@Test
public void testFilter() {
    List<String> one = new ArrayList<>();
    Collections.addAll(one, "迪丽热巴", "宋远桥", "苏星河", "老子", "庄子", "孙子");

    // 得到名字长度为3个字的人(过滤)
    // filter(Predicate predicate)
    /*one.stream().filter((String s) -> {
        return s.length() == 3;
    }).forEach(System.out::println);*/

    // one.stream().filter(s -> s.length() == 3).forEach(System.out::println);// 简化
}

limit方法:

**作用:**获取集合前n个元素

例子

@Test
public void testLimit() {
    List<String> one = new ArrayList<>();
    Collections.addAll(one, "迪丽热巴", "宋远桥", "苏星河", "老子", "庄子", "孙子");

    // 获取前3个数据
    one.stream()
            .limit(3)
            .forEach(System.out::println);
}

skip方法:

**作用:**跳过前n个元素

例子

@Test
public void testSkip() {
    List<String> one = new ArrayList<>();
    Collections.addAll(one, "迪丽热巴", "宋远桥", "苏星河", "老子", "庄子", "孙子");

    // 跳过前两个数据
    one.stream()
            .skip(2)
            .forEach(System.out::println);
}

map方法

**作用:**把一种数据类型映射为另一种数据类型

**例子:**把String类型映射为int类型

@Test
public void testMap() {
    Stream<String> original = Stream.of("11", "22", "33");

    // Map可以将一种类型的流转换成另一种类型的流
    // 将Stream流中的字符串转成Integer
    /*Stream stream = original.map((String s) -> {
        return Integer.parseInt(s);
    });*/
    // original.map(s -> Integer.parseInt(s)).forEach(System.out::println);

    original.map(Integer::parseInt).forEach(System.out::println);
}

sorted方法

**作用:**集合元素排序

例子

public void testSorted() {
    // sorted(): 根据元素的自然顺序排序
    // sorted(Comparator comparator): 根据比较器指定的规则排序
    Stream<Integer> stream = Stream.of(33, 22, 11, 55);

    // stream.sorted().forEach(System.out::println);// 直接排序默认升序
    
    /*stream.sorted((Integer i1, Integer i2) -> {
        return i2 - i1;
    }).forEach(System.out::println);*/

    stream.sorted((i1, i2) -> i2 - i1).forEach(System.out::println);// 自定义排序
}

distinct方法

**作用:**去重

例子1

@Test
public void testDistinct() {
    Stream<Integer> stream = Stream.of(22, 33, 22, 11, 33);

    stream.distinct().forEach(System.out::println);

    Stream<String> stream1 = Stream.of("aa", "bb", "aa", "bb", "cc");
    stream1.distinct().forEach(System.out::println);
}

例子2:自定义类型的去重

@Test
public void testDistinct2() {
    Stream<Person> stream = Stream.of(
            new Person("貂蝉", 18),
            new Person("杨玉环", 20),
            new Person("杨玉环", 20),
            new Person("西施", 16),
            new Person("西施", 16),
            new Person("王昭君", 25)
    );

    stream.distinct().forEach(System.out::println);
}

这里我们可以看到有一个Person类,如果我们不重写Person类的hashCode方法与equals方法的话是去重不了的

Person类代码

@Data
public class Person {
    private String name;
    private int age;

    public Person() {
    }

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;

        Person person = (Person) o;

        if (age != person.age) return false;
        return name != null ? name.equals(person.name) : person.name == null;
    }

    @Override
    public int hashCode() {
        int result = name != null ? name.hashCode() : 0;
        result = 31 * result + age;
        return result;
    }

    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}

Match方法

**作用:**匹配是否有符合的元素

例子

@Test
public void testMatch() {
    Stream<Integer> stream = Stream.of(5, 3, 6, 1);

    // boolean b = stream.allMatch(i -> i > 0); // allMatch: 匹配所有元素,所有元素都需要满足条件
    // boolean b = stream.anyMatch(i -> i > 5); // anyMatch: 匹配某个元素,只要有其中一个元素满足条件即可
    boolean b = stream.noneMatch(i -> i < 0); // noneMatch: 匹配所有元素,所有元素都不满足条件
    System.out.println(b);
}

补充

  1. allMatch: 匹配所有元素,所有元素都需要满足条件
  2. anyMatch: 匹配某个元素,只要有其中一个元素满足条件即可
  3. noneMatch: 匹配所有元素,所有元素都不满足条件

find方法

**作用:**查询指定元素

例子

@Test
public void testFind() {
    Stream<Integer> stream = Stream.of(33, 11, 22, 5);
    Optional<Integer> first = stream.findFirst();
    // Optional first = stream.findAny();
    System.out.println(first.get());
}

补充

  1. findFirst方法的作用是返回集合中的第一个元素
  2. findAny方法的作用是返回集合中的任意一个元素,不一定是第一个

max,min方法

**作用:**查询最大、最小元素

例子

@Test
public void testMax_Min() {
    // 获取最大值
    // 1, 3, 5, 6
    Optional<Integer> max = Stream.of(5, 3, 6, 1).max((o1, o2) -> o1 - o2);
    System.out.println("最大值: " + max.get());

    // 获取最小值
    // 1, 3, 5, 6
    Optional<Integer> min = Stream.of(5, 3, 6, 1).min((o1, o2) -> o1 - o2);
    System.out.println("最小值: " + min.get());

}

reduce方法

**作用:**将所有数据归纳得到一个数据

例子

@Test
public void testReduce() {
    // T reduce(T identity, BinaryOperator accumulator);
    // T identity: 默认值
    // BinaryOperator accumulator: 对数据进行处理的方式
    // reduce如何执行?
    // 第一次, 将默认值赋值给x, 取出集合第一元素赋值给y
    // 第二次, 将上一次返回的结果赋值x, 取出集合第二元素赋值给y
    // 第三次, 将上一次返回的结果赋值x, 取出集合第三元素赋值给y
    // 第四次, 将上一次返回的结果赋值x, 取出集合第四元素赋值给y
    int reduce = Stream.of(4, 5, 3, 9).reduce(0, (x, y) -> {
        System.out.println("x = " + x + ", y = " + y);
        return x + y;
    });
    System.out.println("reduce = " + reduce); // 21

    // 获取最大值
    Integer max = Stream.of(4, 5, 3, 9).reduce(0, (x, y) -> {
        return x > y ? x : y;
    });
    System.out.println("max = " + max);
}

结果

reduce = 21
x = 0, y = 4
x = 4, y = 5
x = 9, y = 3
x = 12, y = 9
reduce = 21
max = 9

可以看出第一个求和的例子是 把返回值赋值给第一个参数 当前值赋值给第二个参数,完成累加

第二个例子是 把每次的返回值与当前值进行比较 返回值赋值给第一个参数 当前值赋值给第二个参数,完成求最大值

concat方法

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

例子

@Test
    public void testContact() {
        Stream<String> streamA = Stream.of("张三");
        Stream<String> streamB = Stream.of("李四");

        // 合并成一个流
        Stream<String> newStream = Stream.concat(streamA, streamB);
        // 注意:合并流之后,不能操作之前的流啦.
//         streamA.forEach(System.out::println);

        newStream.forEach(System.out::println);
    }

结果

张三
李四

**补充:**需要注意 两个流合并后 原先的流就不能再用了 因为原先的流已经关闭,流转到新的流了。

collect、toArray方法

**作用:**把流转化为指定集合或者数组中

例子1:流转化为集合

@Test
public void testStreamToCollection() {
    Stream<String> stream = Stream.of("aa", "bb", "cc", "bb");

    // 将流中数据收集到集合中
    // collect收集流中的数据到集合中
    // List list = stream.collect(Collectors.toList());
    // System.out.println("list = " + list);

    // Set set = stream.collect(Collectors.toSet());
    // System.out.println("set = " + set);

    // 收集到指定的集合中ArrayList
    // ArrayList arrayList = stream.collect(Collectors.toCollection(ArrayList::new));
    // System.out.println("arrayList = " + arrayList);
    HashSet<String> hashSet = stream.collect(Collectors.toCollection(HashSet::new));
    System.out.println("hashSet = " + hashSet);

}

**补充:**前两个例子没有指定具体的集合类型,后两个例子用toCollection方法 指定了具体的集合类型。

例子2:流转化为数组

@Test
public void testStreamToArray() {
    Stream<String> stream = Stream.of("aa", "bb", "cc");

    // 转成Object数组不方便
    // Object[] objects = stream.toArray();
    // for (Object o : objects) {
    //     System.out.println("o = " + o);
    // }
    // String[]
    String[] strings = stream.toArray(String[]::new);
    for (String string : strings) {
        System.out.println("string = " + string + ", 长度: " + string.length());
    }
}

**补充:**使用toArray方法可以把流转化为数组,但默认不指定具体类型转换为Object类。

例子3:其他收集流中数据的方式

@Test
public void testStreamToOther() {
    Stream<Student> studentStream = Stream.of(
            new Student("赵丽颖", 58, 95),
            new Student("杨颖", 56, 88),
            new Student("迪丽热巴", 56, 99),
            new Student("柳岩", 52, 77));

    // 获取最大值
    // Optional max = studentStream.collect(Collectors.maxBy((s1, s2) -> s1.getSocre() - s2.getSocre()));
    // System.out.println("最大值: " + max.get());

    // 获取最小值
    // Optional min = studentStream.collect(Collectors.minBy((s1, s2) -> s1.getSocre() - s2.getSocre()));
    // System.out.println("最小值: " + min.get());

    // 求总和
    // Integer sum = studentStream.collect(Collectors.summingInt(s -> s.getAge()));
    // System.out.println("总和: " + sum);

    // 平均值
    // Double avg = studentStream.collect(Collectors.averagingInt(s -> s.getSocre()));
    // Double avg = studentStream.collect(Collectors.averagingInt(Student::getSocre));
    // System.out.println("平均值: " + avg);

    // 统计数量
    Long count = studentStream.collect(Collectors.counting());
    System.out.println("统计数量: " + count);

}

**补充:**可以看出collect可以实现很多常见的功能,如:最大值、最小值、求和、平均值、数量等

例子4:分组

@Test
public void testGroup() {
    Stream<Student> studentStream = Stream.of(
            new Student("赵丽颖", 52, 95),
            new Student("杨颖", 56, 88),
            new Student("迪丽热巴", 56, 55),
            new Student("柳岩", 52, 33));

    // Map> map = studentStream.collect(Collectors.groupingBy(Student::getAge));

    // 将分数大于60的分为一组,小于60分成另一组
    Map<String, List<Student>> map = studentStream.collect(Collectors.groupingBy((s) -> {
        if (s.getSocre() > 60) {
            return "及格";
        } else {
            return "不及格";
        }
    }));

    map.forEach((k, v) -> {
        System.out.println(k + "::" + v);
    });
}

结果:

不及格::[Student{name=‘迪丽热巴’, age=56, socre=55}, Student{name=‘柳岩’, age=52, socre=33}]
及格::[Student{name=‘赵丽颖’, age=52, socre=95}, Student{name=‘杨颖’, age=56, socre=88}]

可以看出通过groupingBy方法可以实现根据规则进行分组

例子5:多级分组

@Test
public void testCustomGroup() {
    Stream<Student> studentStream = Stream.of(
            new Student("赵丽颖", 52, 95),
            new Student("杨颖", 56, 88),
            new Student("迪丽热巴", 56, 55),
            new Student("柳岩", 52, 33));

    // 先根据年龄分组,每组中在根据成绩分组
    // groupingBy(Function classifier, Collector downstream)
    Map<Integer, Map<String, List<Student>>> map = studentStream.collect(Collectors.groupingBy(Student::getAge, Collectors.groupingBy((s) -> {
        if (s.getSocre() > 60) {
            return "及格";
        } else {
            return "不及格";
        }
    })));

    // 遍历
    map.forEach((k, v) -> {
        System.out.println(k);
        // v还是一个map,再次遍历
        v.forEach((k2, v2) -> {
            System.out.println("\t" + k2 + " == " + v2);
        });
    });
}

结果:

52
不及格 == [Student{name=‘柳岩’, age=52, socre=33}]
及格 == [Student{name=‘赵丽颖’, age=52, socre=95}]
56
不及格 == [Student{name=‘迪丽热巴’, age=56, socre=55}]
及格 == [Student{name=‘杨颖’, age=56, socre=88}]

可以看出这个例子是先根据年龄分组 再根据分数分组

例子6:分区

@Test
public void testPartition() {
    Stream<Student> studentStream = Stream.of(
            new Student("赵丽颖", 52, 95),
            new Student("杨颖", 56, 88),
            new Student("迪丽热巴", 56, 55),
            new Student("柳岩", 52, 33));

    Map<Boolean, List<Student>> map = studentStream.collect(Collectors.partitioningBy(s -> {
        return s.getSocre() > 60;
    }));

    map.forEach((k , v) -> {
        System.out.println(k + " :: " + v);
    });
}

结果

false :: [Student{name=‘迪丽热巴’, age=56, socre=55}, Student{name=‘柳岩’, age=52, socre=33}]
true :: [Student{name=‘赵丽颖’, age=52, socre=95}, Student{name=‘杨颖’, age=56, socre=88}]

分区就是特别的分组,分区的key只有 false与true

例子7:拼接

@Test
public void testJoining() {
    Stream<Student> studentStream = Stream.of(
            new Student("赵丽颖", 52, 95),
            new Student("杨颖", 56, 88),
            new Student("迪丽热巴", 56, 99),
            new Student("柳岩", 52, 77));

    // 根据一个字符串拼接: 赵丽颖__杨颖__迪丽热巴__柳岩
    // String names = studentStream.map(Student::getName).collect(Collectors.joining("__"));

    // 根据三个字符串拼接
    String names = studentStream.map(Student::getName).collect(Collectors.joining("__", "^_^", "V_V"));
    System.out.println("names = " + names);
}

结果

names = ^ _ ^赵丽颖__ 杨颖 __ 迪丽热巴__柳岩V_V

可以看出这个方法的作用是在每一个字符串之前(也可以是前后) 拼接字符串

06.parallelStream 并行流

parallelStream的优缺点:

**优点:**parallelStream其实就是一个并行执行的流,效率更高

**缺点:**有线程安全问题

获取并行Stream流的两种方式:

  1. 直接获取并行的流
  2. 将串行流转成并行流

代码

@Test
public void testgetParallelStream() {
    // 掌握获取并行Stream流的两种方式
    // 方式一:直接获取并行的Stream流
    List<String> list = new ArrayList<>();
    Stream<String> stream = list.parallelStream();

    // 方式二:将串行流转成并行流
    Stream<String> parallel = list.stream().parallel();
}

并行流的例子

@Test
public void testParallel() {
    Stream.of(4, 5, 3, 9, 1, 2, 6)
            .parallel() // 转成并行流
            .filter(s -> {
                System.out.println(Thread.currentThread() + "::" + s);
                return s > 3;
            })
            .count();
}

结果

Thread[main,5,main]::1
Thread[ForkJoinPool.commonPool-worker-3,5,main]::5
Thread[ForkJoinPool.commonPool-worker-15,5,main]::2
Thread[ForkJoinPool.commonPool-worker-9,5,main]::9
Thread[ForkJoinPool.commonPool-worker-11,5,main]::3
Thread[ForkJoinPool.commonPool-worker-7,5,main]::4
Thread[ForkJoinPool.commonPool-worker-5,5,main]::6
消耗时间:46

可以看出不同线程再操作这个流

并行和串行Stream流的效率对比

// 并行的Stream : 消耗时间:137
@Test
public void testParallelStream() {
    LongStream.rangeClosed(0, times).parallel().reduce(0, Long::sum);
}

// 串行的Stream : 消耗时间:260
@Test
public void testStream() {
    // 得到5亿个数字,并求和
    LongStream.rangeClosed(0, times).reduce(0, Long::sum);
}

// 使用for循环 : 消耗时间:162
@Test
public void testFor() {
    int sum = 0;
    for (int i = 0; i < times; i++) {
        sum += i;
    }
}

结果

可以看出并行流的效率更加优秀

parallelStream线程安全问题

// parallelStream线程安全问题
@Test
public void parallelStreamNotice() {
    ArrayList<Integer> list = new ArrayList<>();
    /*IntStream.rangeClosed(1, 1000)
            .parallel()
            .forEach(i -> {
                list.add(i);
            });
    System.out.println("list = " + list.size());*/

    // 解决parallelStream线程安全问题方案一: 使用同步代码块
    /*Object obj = new Object();
    IntStream.rangeClosed(1, 1000)
            .parallel()
            .forEach(i -> {
                synchronized (obj) {
                    list.add(i);
                }
            });*/

    // 解决parallelStream线程安全问题方案二: 使用线程安全的集合
    // Vector v = new Vector();
    /*List synchronizedList = Collections.synchronizedList(list);
    IntStream.rangeClosed(1, 1000)
            .parallel()
            .forEach(i -> {
                synchronizedList.add(i);
            });
    System.out.println("list = " + synchronizedList.size());*/

    // 解决parallelStream线程安全问题方案三: 调用Stream流的collect/toArray
    List<Integer> collect = IntStream.rangeClosed(1, 1000)
            .parallel()
            .boxed()
            .collect(Collectors.toList());
    System.out.println("collect.size = " + collect.size());
}

结果

如果不考虑线程安全问题 第一个例子的三次结果如下

list = 920
消耗时间:22

list = 949
消耗时间:21

list = 916
消耗时间:23

可以看出因为ArrayList与parallelStream是线程不安全的 所以不能保证每次都能成功的add,所以导致list的长度每次都不同

解决办法:

  1. 使用同步代码块,这个是最容易想到的方法
  2. 使用线程安全的集合,因为刚才出问题的原因是ArrayList是线程不安全的 所以我们只要换一个线程安全的集合就好
  3. 调用Stream流的collect/toArray

你可能感兴趣的:(java,开发语言)