JDK8 特性(二)

2022/5/24 JDK8

# 1 Stream流

# 1.1 集合处理数据的弊端

  • 案例引入:

    1. 首先筛选所有姓张的人;
    2. 然后筛选名字有三个字的人;
    3. 最后进行对结果进行打印输出。
  • 传统实现

public class StreamDemo {
    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        Collections.addAll(list, "张三丰", "周芷若", "张无忌", "赵敏", "张强");

        //  1. 首先筛选所有姓张的人;
        List<String> zhangList = new ArrayList<>();
        for (String name : list) {
            if(name.contains("张")){
                zhangList.add(name);
            }
        }
        //  2. 然后筛选名字有三个字的人;
        List<String> threeList = new ArrayList<>();
        for (String name : zhangList) {
            if(name.length() == 3){
                threeList.add(name);
            }
        }
        //  3. 最后进行对结果进行打印输出。
        for (String name : threeList) {
            System.out.println(name);
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
  • Stream流式实现
public class StreamDemo {
    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        Collections.addAll(list, "张三丰", "周芷若", "张无忌", "赵敏", "张强");

        // 1. 首先筛选所有姓张的人; 2. 然后筛选名字有三个字的人; 3. 最后进行对结果进行打印输出
        list.stream()
                .filter(str -> str.contains("张"))
                .filter(str -> str.length() == 3)
                .forEach(System.out::println);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12

# 1.2 Stream流式思想概述

注意:Stream和IO流(InputStream/OutputStream)没有任何关系,请暂时忘记对传统IO流的固有印象

Stream流式思想类似于工厂车间的“生产流水线”,Stream流不是一种数据结构,不保存数据,而是对数据进行加工处理。Stream可以看作是流水线上的一个工序。在流水线上,通过多个工序让一个原材料加工成一个商品。

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

# 1.3 获取Stream流的两种方式

  • 方式一
    • Collection集合接口中有默认方法default Stream<E> stream(){...}
    • 也就是说实现该接口的集合类都可以直接调用stream()方法获取流对象
  • 方式二
    • Stream中的静态方法static<T> Stream<T> of(T t){...}
    • 可以调用该方法获取流对象

# 1.4 了解Stream流的常用方法和注意事项

  • Stream常用方法
方法名 方法作用 返回值类型 方法种类
count 统计个数 long 终结
forEach 逐一处理 void 终结
filter 过滤 Stream 函数拼接
limit 取用前几个 Stream 函数拼接
skip 跳过前几个 Stream 函数拼接
map 映射 Stream 函数拼接
concat 组合 Stream 函数拼接
  • 终结方法:返回值类型不再是 Stream 类型的方法,不再支持链式调用。

  • 非终结方法:返回值类型仍然是 Stream 类型的方法,支持链式调用。(除了终结方法外,其余方法均为非终结方法。)

  • Stream注意事项

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

# forEach

  • forEach用来遍历流中的数据 -> 终结方法
void forEach(Consumer&lt;? super T> action);
1
  • 该方法接收一个 Consumer 接口函数,会将每一个流元素交给该函数进行处理。
public class StreamTest {
    private List<String> list;

    @Before
    public void before(){
        list = new ArrayList<>();
        Collections.addAll(list, "张三丰", "周芷若", "张无忌", "赵敏", "张强");
    }

    @Test
    public void forEach(){
        list.stream().forEach(System.out::println);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# count

  • count方法来统计其中的元素个数 -> 终结方法
long count();
1
  • 该方法返回一个long值代表元素个数。基本使用
public class StreamTest {
    private List<String> list;

    @Before
    public void before(){
        list = new ArrayList<>();
        Collections.addAll(list, "张三丰", "周芷若", "张无忌", "赵敏", "张强");
    }

    @Test
    public void count(){
        System.out.println(list.stream().count());
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# filter

  • filter用于过滤数据,返回符合过滤条件的数据 -> 非终结方法
Stream&lt;T> filter(Predicate&lt;? super T> predicate);
1
  • 该接口接收一个 Predicate 函数式接口参数(可以是一个Lambda或方法引用)作为筛选条件。

public class StreamTest {
    private List<String> list;

    @Before
    public void before(){
        list = new ArrayList<>();
        Collections.addAll(list, "迪丽热巴", "张三丰", "周芷若", "张无忌", "赵敏", "张强");
    }
    @Test
    public void filter(){
        // 计算名字长度为3的人数
        System.out.println(list.stream().filter(name -> name.length() == 3).count());
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# limit

  • limit 方法可以对流进行截取,只取用前n个 -> 非终结方法
Stream&lt;T> limit(long maxSize);
1
  • 参数是一个long型,如果集合当前长度大于参数则进行截取。否则不进行操作
public class StreamTest {
    private List<String> list;

    @Before
    public void before(){
        list = new ArrayList<>();
        Collections.addAll(list, "迪丽热巴", "张三丰", "周芷若", "张无忌", "赵敏", "张强");
    }

    @Test
    public void limit(){
        list.stream().limit(3).forEach(System.out::println);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# skip

  • 如果希望跳过前几个元素,可以使用 skip 方法获取一个截取之后的新流 -> 非终结方法
Stream&lt;T> skip(long n);
1
  • 如果流的当前长度大于n,则跳过前n个; 否则将会得到一个长度为0的空流。
public class StreamTest {
    private List<String> list;

    @Before
    public void before(){
        list = new ArrayList<>();
        Collections.addAll(list, "迪丽热巴", "张三丰", "周芷若", "张无忌", "赵敏", "张强");
    }
    
    @Test
    public void skip(){
        list.stream().skip(3).forEach(System.out::println);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# map

  • 如果需要将流中的元素映射到另一个流中,可以使用 map 方法 -> 非终结方法
&lt;R> Stream&lt;R> map(Function&lt;? super T, ? extends R> mapper);
1
  • 该接口需要一个 Function 函数式接口参数,可以将当前流中的T类型数据转换为另一种R类型的流
public class StreamTest {
    private List<String> list;

    @Before
    public void before(){
        list = new ArrayList<>();
        Collections.addAll(list, "迪丽热巴", "张三丰", "周芷若", "张无忌", "赵敏", "张强");
    }

    @Test
    public void map(){
         list.stream()
                 // 类型增强
                .map(name -> "name: " + name)
                .forEach(System.out::println);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

# sorted

  • 如果需要将数据排序,可以使用 sorted 方法。 -> 非终结方法
Stream&lt;T> sorted(); // 根据元素的自然顺序排序
Stream&lt;T> sorted(Comparator&lt;? super T> comparator); // 自定义比较器排序
1
2
  • 该方法可以有两种不同地排序实现
public class StreamTest {
    private List<String> list;

    @Before
    public void before(){
        list = new ArrayList<>();
        Collections.addAll(list, "迪丽热巴", "张三丰", "周芷若", "张无忌", "赵敏", "张强");
    }

    @Test
    public void sorted(){
        list.stream()
                // 默认自然序
                .sorted()
                // 执行比较器进行排序
                .sorted(((o1, o2) -> o1.length() - o2.length()))
                .forEach(System.out::println);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

# distinct

  • 如果需要去除重复数据,可以使用 distinct 方法 -> 非终结方法
    • 自定义类型是根据对象的 hashCode 和 equals 来去除重复元素的
Stream&lt;T> distinct();
1
  • 基本使用
public class StreamTest {
    private List<String> list;

    @Before
    public void before(){
        list = new ArrayList<>();
        Collections.addAll(list, "迪丽热巴", "张三丰", "周芷若", "张无忌", "赵敏", "张强");
    }

    @Test
    public void distinct(){
        list.add("迪丽热巴");
        list.add("张无忌");
        list.stream()
                  .distinct()
                  .forEach(System.out::println);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

# match

  • 如果需要判断数据是否匹配指定的条件,可以使用 Match 相关方法 -> 终结方法
boolean anyMatch(Predicate&lt;? super T> predicate);
boolean allMatch(Predicate&lt;? super T> predicate);
boolean noneMatch(Predicate&lt;? super T> predicate);
1
2
3
  • 基本使用
public class StreamTest {
    private List<String> list;

    @Before
    public void before(){
        list = new ArrayList<>();
        Collections.addAll(list, "迪丽热巴", "张三丰", "周芷若", "张无忌", "赵敏", "张强");
    }

    @Test
    public void match(){
        // allMatch 所有元素都需要满足
        System.out.println(list.stream()
                .allMatch(name -> name.contains("张")));
    
        // anyMatch 如果有元素满足即可
        System.out.println(list.stream()
                .anyMatch(name -> name.contains("张")));
    
        // noneMatch 所有元素都不满足
        System.out.println(list.stream()
                .noneMatch(name -> name.contains("男")));
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

# find

  • 如果需要找到某些数据,可以使用 find 相关方法 -> 终结方法
Optional&lt;T> findFirst();
Optional&lt;T> findAny();
1
2
  • 基本使用
public class StreamTest {
    private List<String> list;

    @Before
    public void before(){
        list = new ArrayList<>();
        Collections.addAll(list, "迪丽热巴", "张三丰", "周芷若", "张无忌", "赵敏", "张强");
    }

    @Test
    public void find(){
        // 都是获取第一个元素
        Optional<String> optional = list.stream().findAny();
        Optional<String> optional1 = list.stream().findFirst();
        System.out.println(optional.get());
        System.out.println(optional1.get());
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

# max && min

  • 如果需要获取最大和最小值,可以使用 max 和 min 方法 -> 终结方法
Optional&lt;T> max(Comparator&lt;? super T> comparator);
Optional&lt;T> min(Comparator&lt;? super T> comparator);
1
2
  • 基本使用
public class StreamTest {
    @Test
    public void min_max(){
        // 传入一个比较器, 排序完后获取最后一个
        Optional<Integer> max = Stream.of(4, 2, 7, 1).max((o1, o2) -> o1 - o2);
        System.out.println(max.get());
    
        // 传入一个比较器, 排序完后获取第一个
        Optional<Integer> min = Stream.of(4, 2, 7, 1).min((o1, o2) -> o1 - o2);
        System.out.println(min.get());
    }
}
1
2
3
4
5
6
7
8
9
10
11
12

# reduce

  • 如果需要将所有数据归纳得到一个数据,可以使用 reduce 方法 -> 终结方法
T reduce(T identity, BinaryOperator&lt;T> accumulator);
1
  • 基本使用
public class StreamTest {
    @Test
    public void reduce(){
        Integer reduce = Stream.of(4, 5, 3, 9)
                // T identity: 默认值
                // BinaryOperator<T> accumulator: 对数据的处理方式
                // 获取最大值, 初始值为0
                .reduce(0, Integer::max);
        System.out.println(reduce);
        
        reduce = Stream.of(4, 5, 3, 9)
                // T identity: 默认值
                // BinaryOperator<T> accumulator: 对数据的处理方式
                // 获取加和结果, 初始值为0
                .reduce(0, Integer::sum);
        System.out.println(reduce);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

# map && reduce结合

  • 使用案例
public class StreamTest {
    @Test
    public void reduceAndMap(){
        // 获取最大年龄
        Optional<Integer> maxAge = Stream.of(
                        new Person("张三", 18),
                        new Person("李四", 20),
                        new Person("王五", 17),
                        new Person("小红", 19),
                        new Person("小明", 18))
                .map(Person::getAge)
                .reduce(Integer::max);
        System.out.println(maxAge.get());
    
        // 获取年龄总和
        Optional<Integer> totalAge = Stream.of(
                        new Person("张三", 18),
                        new Person("李四", 22),
                        new Person("王五", 17),
                        new Person("小红", 19),
                        new Person("小明", 21))
                .map(Person::getAge)
                .reduce(Integer::sum);
        System.out.println(totalAge.get());
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

# mapToInt

  • 如果需要将Stream中的Integer类型数据转成int类型,可以使用 mapToInt 方法 -> 终结方法
IntStream mapToInt(ToIntFunction&lt;? super T> mapper);
1
  • 该接口需要一个 Function 函数式接口参数,可以将当前流中的T类型数据转换为另一种R类型的流

public class StreamTest {
    @Test
    public void mapToInt(){
        // Integer占用的内存比int多, 在Stream流中会自动拆箱装箱
        Stream<Integer> stream = Stream.of(1, 2, 3, 4);
        // 将Integer类型转换为int类型, 可以节省空间
        IntStream intStream = stream
                .mapToInt(Integer::intValue);
    }
}
1
2
3
4
5
6
7
8
9
10

# concat

  • 如果有两个流,希望合并成为一个流,那么可以使用 Stream 接口的静态方法 concat -> 终结方法
    • 这是一个静态方法,与 java.lang.String 当中的 concat 方法是不同的
static &lt;T> Stream&lt;T> concat(Stream&lt;? extends T> a, Stream&lt;? extends T> b){...}
1
  • 基本使用
public class StreamTest {
    @Test
    public void concat(){
        Stream<String> streamA = Stream.of("张三");
        Stream<String> streamB = Stream.of("李四");
        // 将以上流合并, 合并的流就不能够继续操作了
        Stream.concat(streamA, streamB).forEach(System.out::println);
    }
}
1
2
3
4
5
6
7
8
9

# 1.5 Stream综合案例

  • 现在有两个 ArrayList 集合存储队伍当中的多个成员姓名,要求使用Stream实现若干操作步骤:
    1. 第一个队伍只要名字为3个字的成员姓名;
    2. 第一个队伍筛选之后只要前3个人;
    3. 第二个队伍只要姓张的成员姓名;
    4. 第二个队伍筛选之后不要前2个人;
    5. 将两个队伍合并为一个队伍;
    6. 根据姓名创建 Person 对象;
    7. 打印整个队伍的Person对象信息。
public class StreamCaseDemo {
    public static void main(String[] args) {
        List<String> one = new ArrayList<>();
        List<String> two = new ArrayList<>();
        Collections.addAll(one, "迪丽热巴", "宋远桥", "苏星河", "老子", "庄子", "孙子", "洪七公");
        Collections.addAll(two, "古力娜扎", "张无忌", "张三丰", "赵丽颖", "张二狗", "张天爱", "张三");
    
        Stream<String> streamOne = one.stream()
                // 1. 第一个队伍只要名字为3个字的成员姓名
                .filter(name -> name.length() == 3)
                // 2. 第一个队伍筛选之后只要前3个人
                .limit(3);
    
        Stream<String> streamTwo = two.stream()
                // 3. 第二个队伍只要姓张的成员姓名
                .filter(name -> name.startsWith("张"))
                // 4. 第二个队伍筛选之后不要前2个人
                .skip(2);
        // 5. 将两个队伍合并为一个队伍
        Stream<String> stream = Stream.concat(streamOne, streamTwo);
        // 6. 根据姓名创建 Person 对象
        stream.map(Person::new)
                // 7. 打印整个队伍的Person对象信息
                .forEach(System.out::println);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

# 1.6 收集Stream流中的结果

  • 对流操作完成之后,如果需要将流的结果保存到数组或集合中,可以收集流中的数据

# 收集结果到集合中

  • Stream流提供 collect 方法,其参数需要一个java.util.stream.Collector<T,A, R>接口对象来指定收集到哪种集合中。java.util.stream.Collectors类提供一些方法,可以作为Collector`接口的实例:
    • public static <T> Collector<T, ?, List<T>> toList(): 转换为 List 集合
    • public static <T> Collector<T, ?, Set<T>> toSet(): 转换为 Set 集合
    • public static <T, C extends Collection<T>> Collector<T, ?, C> toCollection(Supplier<C> collectionFactory): 收集到指定的集合中

# 收集结果到数组中

  • Stream提供 toArray 方法来将结果放到一个数组中
Object[] toArray(); // 返回Object数组
&lt;A> A[] toArray(IntFunction&lt;A[]> generator); // 返回指定数组
1
2

# 对流中数据进行聚合计算

  • 当我们使用Stream流处理数据后,可以像数据库的聚合函数一样对某个字段进行操作。比如获取最大值,获取最小值,求总和,平均值,统计数量。
public class StreamCollectTest {
    @Test
    public void aggregation(){
        List<Person> personList = Stream.of(
                        new Person("张三", 18),
                        new Person("李四", 22),
                        new Person("王五", 17),
                        new Person("小红", 19),
                        new Person("小明", 21))
                .collect(Collectors.toList());

        // 获取最大值
        Optional<Person> optional = personList.stream()
                .collect(Collectors.maxBy((o1, o2) -> o1.getAge() - o2.getAge()));
        System.out.println("最大值: " + optional.get());

        // 获取最小值
        Optional<Person> optionalMin = personList.stream()
                .collect(Collectors.minBy((o1, o2) -> o1.getAge() - o2.getAge()));
        System.out.println("最小值: " + optionalMin.get());

        // 求总和
        Integer totalAge = personList.stream()
                .collect(Collectors.summingInt(Person::getAge));
        System.out.println("总和: " + totalAge);

        // 平均值
        Double avg = personList.stream()
                .collect(Collectors.averagingInt(Person::getAge));
        System.out.println("平均值: " + avg);

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

        // 以上方法聚合
        IntSummaryStatistics summaryStatistics = personList.stream()
                .collect(Collectors.summarizingInt(Person::getAge));
        System.out.println("上面所有方法的聚合: " + summaryStatistics);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42

# 对流中数据进行分组

  • 当我们使用Stream流处理数据后,可以根据某个属性将数据分组
public class StreamCollectTest {
    @Test
    public void group(){
        List<Person> personList = Stream.of(
                    new Person("张三", 18),
                    new Person("李四", 22),
                    new Person("王五", 18),
                    new Person("小红", 18),
                    new Person("小明", 21))
                .collect(Collectors.toList());
        // 通过年龄来进行分组
        personList.stream()
                .collect(Collectors.groupingBy(Person::getAge))
                // 分组结果为键值对
                .forEach((key, value) -> System.out.println(key + "::" + value));

        // 将年龄大于19的分为一组, 小于19分为一组
        personList.stream()
                .collect(Collectors.groupingBy(s -> {
                    if(s.getAge() > 19){
                        return ">=19";
                    }else{
                        return "<19";
                    }
                }))
                // 分组结果为键值对, 键为groupingBy返回的数据
                .forEach((k, v) -> System.out.println(k + "::" + v));
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

# 对流中数据进行多级分组

  • 还可以对数据进行多级分组:
public class StreamCollectTest {
    @Test
    public void multiGroup(){
        List<Person> personList = Stream.of(
                        new Person("张三丰", 18),
                        new Person("迪丽热巴", 22),
                        new Person("古力娜扎", 18),
                        new Person("迪迦奥特曼", 18),
                        new Person("宇宙无敌法外狂徒张三", 21))
                .collect(Collectors.toList());

        // 先根据年龄分组, 每组中再根据名字的长度分组
        personList.stream()
                .collect(
                    // 根据年龄分组
                    Collectors.groupingBy(p -> {
                        if(p.getAge() > 19){
                            return "大于19";
                        }else{
                            return "小于等于19";
                        }
                    },
                    // 根据名字长度分组
                    Collectors.groupingBy(p -> {
                        if(p.getName().length() > 4){
                            return "较长的名字";
                        }else{
                            return "较短的名字";
                        }
                    }))
                )
                // 结果的类型为: Map<String, Map<String, Person>>
                .forEach((oneK, oneV) -> {
                    System.out.println("年龄" + oneK);
                    oneV.forEach((twoK, twoV) -> {
                        System.out.println("\t" + twoK + "::" + twoV);
                    });
                });
        /*
         result -> {
            年龄小于等于19
                较长的名字::[Person{name='迪迦奥特曼', age=18}]
                较短的名字::[Person{name='张三丰', age=18}, Person{name='古力娜扎', age=18}]
            年龄大于19
                较长的名字::[Person{name='宇宙无敌法外狂徒张三', age=21}]
                较短的名字::[Person{name='迪丽热巴', age=22}]
         }
        */
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50

# 对流中数据进行分区

  • Collectors.partitioningBy会根据值是否为true,把集合分割为两个列表,一个true列表,一个false列表
public class StreamCollectTest {
    @Test
    public void partition(){
        List<Person> personList = Stream.of(
                        new Person("张三", 18),
                        new Person("李四", 22),
                        new Person("王五", 18),
                        new Person("小红", 18),
                        new Person("小明", 21))
                .collect(Collectors.toList());

        personList.stream()
                // 将结果分为, true和false两个分区
                .collect(Collectors.partitioningBy(p -> p.getAge() > 19))
                .forEach((k, v) -> System.out.println(k + "::" + v));
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

# 对流中数据进行拼接

  • Collectors.joining会根据指定的连接符,将所有元素连接成一个字符串
public class StreamCollectTest {
    @Test
    public void join(){
        List<Person> personList = Stream.of(
                        new Person("张三", 18),
                        new Person("李四", 22),
                        new Person("王五", 18),
                        new Person("小红", 18),
                        new Person("小明", 21))
                .collect(Collectors.toList());

        // 根据一个字符拼接
        String names = personList.stream()
                .map(Person::getName)
                .collect(Collectors.joining("-"));
        System.out.println(names);

        // 根据三个字符拼接
        names = personList.stream()
                .map(Person::getName)
                // 参数说明: 分隔符, 前缀, 后缀
                .collect(Collectors.joining(",", "{", "}"));
        System.out.println(names);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25