02 Stream流

Java8的 Stream 使用的是函数式编程模式,它可以被用来对集合或数组进行链状流式的操作,可以更方便地让我们对集合或数组操作。

案例

1. 数据准备

@Data
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode
public class Author {
   private Long id;
   private String name;
   private Integer age;
   private String intro;
   private List<Book> books;
}
@Data
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode
public class Book {
    private Long id;
    private String name;
    private String category;
    private Integer score;
    private String intro;
}
private static List<Author> getAuthors() {
    Author author1 = new Author(1L, "雷蒙多", 33, "简介1", null);
    Author author2 = new Author(2L, "亚拉索", 15, "简介2", null);
    Author author3 = new Author(3L, "易", 14, "简介3", null);
    Author author4 = new Author(3L, "易", 14, "简介3", null);

    List<Book> books1 = new ArrayList<>();
    List<Book> books2 = new ArrayList<>();
    List<Book> books3 = new ArrayList<>();

    books1.add(new Book(1L, "刀的两侧是光明与黑暗", "哲学,爱情", 88, "用一把刀划分了爱恨"));
    books1.add(new Book(2L, "一个人不能死在同一把刀下", "个人成长,爱情", 99, "讲述如何从失败中明悟真理"));

    books2.add(new Book(3L, "那风吹不到的地方", "哲学", 85, "带你用思维去领略世界的尽头"));
    books2.add(new Book(3L, "那风吹不到的地方", "哲学", 85, "带你用思维去领略世界的尽头"));
    books2.add(new Book(4L, "吹或不吹", "爱情,个人传记", 56, "一个哲学家的恋爱观注定很难把他所在的时代理解"));

    books3.add(new Book(5L, "你的剑就是我的剑", "爱情", 56, "无法想象一个武者能对他的伴侣这么的宽容"));
    books3.add(new Book(6L, "风与剑", "个人传记", 100, "两个哲学家灵魂和肉体的碰撞会激起怎么样的火花呢?"));
    books3.add(new Book(6L, "风与剑", "个人传记", 100, "两个哲学家灵魂和肉体的碰撞会激起怎么样的火花呢?"));
    author1.setBooks(books1);
    author2.setBooks(books2);
    author3.setBooks(books3);
    author4.setBooks(books3);
    
    return new ArrayList<>(Arrays.asList(author1, author2, author3, author4));
}

2. 快速入门

需求

用 getAuthors() 方法获取到作家的集合,打印出所有年龄小于18的作家的名字,并且要注意去重。

实现

因为List并不是stream对象,为此使用集合对象时需要先将其转换为stream对象,拿到stream流,然后才可以调用stream对象的方法进行过滤处理。这里先使用匿名内部类的方式实现。

public static void main(String[] args) {
    List<Author> authors = getAuthors();
    authors.stream()    // 将List转为Stream流对象
            .distinct()    // 去重,靠的是Author类中的@EqualsAndHashCode注解,如果没有这个注解就需要自己编写这两个方法
            .filter(new Predicate<Author>() {   // 调用filter对年龄进行过滤,首先使用匿名内部类的方式实现
                @Override
                public boolean test(Author author) {
                    return author.getAge() < 18;
                }
            })
            .forEach(new Consumer<Author>() {   // forEach方法用来遍历剩余的每个元素进行消费,也使用匿名内部类的方式实现
                @Override
                public void accept(Author author) {
                    System.out.println(author.getName());
                }
            });
}

随后改为Lambda表达式实现

public static void main(String[] args) {
    List<Author> authors = getAuthors();
    
    authors.stream()    // 将List转为Stream流对象
            .distinct()    // 去重,靠的是Author类中的@EqualsAndHashCode注解,如果没有这个注解就需要自己编写这两个方法
            .filter(author -> author.getAge() < 18)   // 调用filter对年龄进行过滤
            .forEach(author -> System.out.println(author.getName()));  // forEach方法用来遍历剩余的每个元素进行消费
}

3. IDEA 快速查看 Stream 流程

首先将断点打在使用流的地方:

02Stream-outPut1.png 然后使用DEBUG运行程序,在DEBUG窗口查看【Current Stream Chain】 02Stream-outPut2.png 逐个方法对应的Tab就可以查看流的处理流程: 02Stream-outPut3.png

常用操作

1. 创建流

Java 中有两类集合,一类是单列集合,父接口为Collection,一类是双列集合,父接口为Map。根据集合对象的不同,有如下几种创建流的方式:

  1. 单列集合(List、Set):集合对象.stream()

    List<Author> authors = getAuthors();
    Stream<Author> stream = authors.stream();
    
  2. 数组([]):Arrays.stream(数组) 或者 Stream.of(数组)

    Integer[] arr = {1, 2, 3, 4, 5};
    Stream<Integer> stream1 = Arrays.stream(arr);
    Stream<Integer> stream2 = Stream.of(arr);
    
  3. 双列集合:转换为单列集合后再创建

    Map<String, Integer> map = new HashMap<>();
    map.put("xiaoxin", 19);
    map.put("ameng", 17);
    map.put("wukong", 16);
    
    Stream<Map.Entry<String, Integer>> stream = map.entrySet().stream();
    

2. 中间操作

filter

filter(Predicate<? super T> predicate):对流中的元素进行条件过滤,符合过滤条件的才能继续留在流中。

例如:打印所有姓名长度大于1的作家的姓名。

public static void main(String[] args) {
    List<Author> authors = getAuthors();

    authors.stream()
            .filter(author -> author.getName().length() > 1)
            .forEach(author -> System.out.println(author.getName()));
}

map

map(Function<? super T,? extends R> mapper):对流中的元素进行计算或类型转换。

例如:打印所有作家的姓名。

public static void main(String[] args) {
    List<Author> authors = getAuthors();

    authors.stream()
            .map(author -> author.getName())
            .forEach(name -> System.out.println(name));
}

当然,其实这个需求单独用forEach也可以实现。但是经过map操作之后,流中的数据类型会改变,一定程度上减轻了数据量级。

public static void main(String[] args) {
    List<Author> authors = getAuthors();

    authors.stream()
            .forEach(author -> System.out.println(author.getName()));
}

distinct

distinct():可以去除流中的重复元素。

注意:distinct方法是依赖类中的equals方法来判断是否是相同对象的,所以如果要对某个类型的对象进行去重,这个类中必须重写equals() 和 hashCode() 方法。

例如:打印所有作家的姓名,并且要求其中不能有重复元素。

public static void main(String[] args) {
    List<Author> authors = getAuthors();

    authors.stream()
            .distinct()
            .forEach(author -> System.out.println(author.getName()));
}

sorted

  • sorted():对数据流中的元素按自然顺序排序。

  • sorted(Comparator<? super T> comparator):根据提供的Comparator对流中的元素排序。

例如:对流中的元素按照年龄进行降序排序,并且要求不能有重复的元素。

在这个案例中,如果要使用第一种方法,就必须让 Author 继承 Comparable 接口,并实现 compareTo() 方法定义排序规则。

@Data
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode
public class Author implements Comparable<Author> {
    private Long id;
    private String name;
    private Integer age;
    private String intro;
    private List<Book> books;

    @Override
    public int compareTo(Author o) {
        // 升序排序
        // return this.getAge() - o.getAge();
        // 降序排序
        return o.getAge() - this.getAge();
    }
}

然后在main函数中调用:

public static void main(String[] args) {
    List<Author> authors = getAuthors();

    authors.stream()
            .distinct()
            .sorted()
            .forEach(author -> System.out.println(author.getAge()));
}

第二种方式就是保持Author不动,不让 Author 实现 Comparable 接口,而是在 sorted() 中定义排序规则。

public static void main(String[] args) {
    List<Author> authors = getAuthors();

    authors.stream()
            .distinct()
            .sorted((o1, o2) -> o2.getAge() - o1.getAge()) // 降序排列
            .forEach(author -> System.out.println(author.getAge()));
}

limit

limit(long maxSize):设置流的最大长度(元素数量),超出的部分将被舍弃。

例如,对流中的元素按照年龄进行降序排序,并且要求不能有重复的元素,然后打印其中年龄最大的两个作家的姓名。

public static void main(String[] args) {
    List<Author> authors = getAuthors();

    authors.stream()
            .distinct()
            .sorted((o1, o2) -> o2.getAge() - o1.getAge()) // 降序排列
            .limit(2)
            .forEach(author -> System.out.println(author.getAge()));
}

skip

skip(long n):跳过流中的前 n 个元素,返回剩下的元素。

例如,打印除了年龄最大的作家外的其他作家,要求不能有重复元素,并且按照年龄降序排序。

public static void main(String[] args) {
    List<Author> authors = getAuthors();

    authors.stream()
            .distinct()
            .sorted((o1, o2) -> o2.getAge() - o1.getAge()) // 降序排列
            .skip(1)
            .forEach(author -> System.out.println(author.getAge()));
}

flatMap

flatMap(Function<? super T,? extends Stream<? extends R>> mapper):map 只能把一个对象转换成另一个对象来作为流中的元素,而fatMap可以把一个对象转换成多个对象作为流中的元素。

例如,打印所有书籍的名字,并对重复的元素进行去重。

刚开始可能会想到使用map()方法,取出author中的books列表,然后进行去重:

public static void main(String[] args) {
    List<Author> authors = getAuthors();

    authors.stream()
            .map(author -> author.getBooks())
            .distinct()
            .forEach(System.out::println);
}

但是,map()返回的是author中的List对象,使用distinct进行去重时,流中的元素为List,而不是Book,因此去重的结果并不是我们预期的。

这时,就可以使用flatMap(),将流中列表类型的元素转换为新的流,新流中包含的就是列表中的元素,再使用distinct去重时,去重的对象就是Book对象了。

public static void main(String[] args) {
    List<Author> authors = getAuthors();

    // 匿名内部类形式
    authors.stream()
            .flatMap(new Function<Author, Stream<?>>() {
                @Override
                public Stream<?> apply(Author author) {
                    return author.getBooks().stream();
                }
            })
            .distinct()
            .forEach(System.out::println);

    // Lambda
    authors.stream()
            .flatMap(author -> author.getBooks().stream())
            .distinct()
            .forEach(System.out::println);
}

例二:打印现有书籍的所有分类,要求对分类进行去重,且不能出现多个分类(爱情,文艺)的格式

public static void main(String[] args) {
    List<Author> authors = getAuthors();

    authors.stream()
            .flatMap(author -> author.getBooks().stream())
            .distinct()
            .flatMap(book -> Arrays.stream(book.getCategory().split(",")))
            .distinct()
            .forEach(System.out::println);
}

3. 结尾操作

必须要有结尾操作,中间操作才会被调用到,进而生效,否则中间操作不会被执行

forEach

forEach(Consumer<? super T> action):对流中的元素进行遍历操作,可以通过传入的参数指定对遍历到的元素进行什么具体操作。

count

count():获取当前流中元素的个数。

max&min

  • max(Comparator<? super T> comparator):通过传入的Comparator对元素进行比较,得到最大值;

  • min(Comparator<? super T> comparator):通过传入的Comparator对元素进行比较,得到最小值。

Comparator的实现方法和 sorted() 中一致。

例如,获取这些作家的所出书籍的最高分并打印(最低分同理,改为min即可)。

此外,max和min返回的是一个Optional对象,需要通过 get() 获取到原始对象才可以使用。

public static void main(String[] args) {
    List<Author> authors = getAuthors();

    Optional<Book> max = authors.stream()
            .flatMap(author -> author.getBooks().stream())
            .distinct()
            .max((o1, o2) -> o1.getScore() - o2.getScore());
    if (max.isPresent()) {
        Book book = max.get();
        System.out.println(book.getScore());
    }
    // 上面对max进行判断与输出的代码,也可以简化为Lambda表达式如下
    max.ifPresent(book -> System.out.println(book.getScore()));

    // 因为这里最后只需要输出分数,因此当我们取到 Stream<Book> 流对象后
    // 可以将流转换为 Stream<Integer> 流,只包含分数就可以,降低数据量级
    Optional<Integer> maxed = authors.stream()
            .flatMap(author -> author.getBooks().stream())
            .distinct()
            .map(book -> book.getScore())
            .max((o1, o2) -> o1 - o2);
    maxed.ifPresent(score -> System.out.println(score));
}

collect

collect(Collector<? super T,A,R> collector):将当前流转换为一个集合。

在某些场景下,集合通过流处理之后,需要导出为一个新的集合进行使用,这时候就需要使用 collect() 方法。

例子:

获取一个存放所有作者名字的List集合。

public static void main(String[] args) {
    List<Author> authors = getAuthors();

    List<String> nameList = authors.stream()
            .map(author -> author.getName())
            .collect(Collectors.toList());

    System.out.println(nameList);
}

获取一个所有书名的Set集合。

public static void main(String[] args) {
    List<Author> authors = getAuthors();

    Set<String> bookSet = authors.stream()
            .flatMap(author -> author.getBooks().stream())
            .map(book -> book.getName())
            .collect(Collectors.toSet());
    System.out.println(bookSet);
}

获取一个Map集合,map的key为作者名,value为List

由于 toMap() 的匿名内部类比较复杂,先给出匿名内部类的方式,便于理解原理。

public static void main(String[] args) {
    List<Author> authors = getAuthors();

    Map<String, List<Book>> books = authors.stream()
            .collect(Collectors.toMap(new Function<Author, String>() {
                @Override
                public String apply(Author author) {
                    return author.getName();
                }
            }, new Function<Author, List<Book>>() {
                @Override
                public List<Book> apply(Author author) {
                    return author.getBooks();
                }
            }, new BinaryOperator<List<Book>>() {
                @Override
                public List<Book> apply(List<Book> books1, List<Book> books2) {
                    return books2;
                }
            }));
    System.out.println(books);
}

使用toMap()方法有一个注意事项,toMap()共有三种实现:

主要是前三个参数:

  • Function<? super T,? extends K> keyMapper:key的映射函数,将T类型映射为K类型
  • Function<? super T,? extends U> valueMapper:value的映射函数,将T类型映射为U类型
  • BinaryOperator mergeFunction:聚合函数,用于指定key重复时的操作 因为将流中元素的某个字段转化为key后是可能存在重复的,并且无法通过distinct()去重,distinct()只会去掉重复的对象,而不能仅对流中元素的某个字段去重。 BinaryOperator则是用来指导key重复时的聚合规则,实现BinaryOperator接口需要重写apply(o1, o2)方法,接收两个参数,如果 return o1 则表示保留先出现的key对应的value,return o2 则表示用后面出现的key对应的value覆盖前面的。

随后,用Lambda表达式简化:

public static void main(String[] args) {
    List<Author> authors = getAuthors();

    Map<String, List<Book>> books = authors.stream()
            .collect(Collectors.toMap(author -> author.getName(), author -> author.getBooks(), (books1, books2) -> books2));
    System.out.println(books);
}

查找与匹配

anyMatch

anyMatch(Predicate<? super T> predicate):判断流内是否有任意符合匹配条件的元素,结果为boolean类型。只要有一个元素满足条件就返回true。

例如,判断是否有年龄在29岁以上的作家。

public static void main(String[] args) {
    List<Author> authors = getAuthors();

    boolean b = authors.stream()
            .anyMatch(author -> author.getAge() > 29);
    System.out.println(b);  // true
}
allMatch

allMatch(Predicate<? super T> predicate):与anyMatch()类似,判断流内是否所有元素都满足匹配条件,结果为boolean类型。当所有元素都满足条件时才返回true。

noneMatch

noneMatch(Predicate<? super T> predicate):与上面两个类似,判断流内是否所有元素都不满足匹配条件,结果为boolean类型。当所有元素都不满足条件时才返回true。

findAny

findAny():获取流中的任意一个元素,返回的是一个 Optional 对象。该方法没有办法保证获取的一定是流中的第一个元素,因此用的更多的是下面的 findFirst() 方法。

例子:获取任意一个年龄大于18的作家,如果存在就输出他的名字。

findAny() 并不像他的字面意思一样,可以查找一个满足条件的元素,他只是在最后处理完的流中随机获取一个元素并返回。因此,如果要做筛选的话,还是要依赖 filter() 方法。 那么 findAny() findFirst() 存在的意义是什么呢? 因为流处理结束后,最终的流是可能为空的,比如说下面的代码中,如果作家年龄都小于18,那么最后的流将会是空的,如果直接使用很可能会报空指针异常。因此,findAny() 和 findFirst() 方法主要是用来避免空指针异常的。 当调用 findAny() 和 findFirst() 方法时,返回的是一个 Optional 对象,Optional 对象的 ifPresent() 方法便可以对流元素对象进行判空,不为空才执行相应逻辑。

public static void main(String[] args) {
    List<Author> authors = getAuthors();

    // 查找年龄大于18的作家
    Optional<Author> optionalAuthor = authors.stream()
            .filter(author -> author.getAge() > 18)
            .findAny();
    
    // 如果存在就输出他的名字
    optionalAuthor.ifPresent(author -> System.out.println(author));
}
findFirst

findFirst():获取流中的第一个元素,返回的是一个 Optional 对象。与findAny()的用法一样。

reduce

对流中的数据按照指定的计算方式计算出 一个结果。有三种实现:

两个参数

reduce(T identity, BinaryOperator<T> accumulator) 的作用是把 stream 中的元素给组合起来,我们可以传入一个初始值,它会按照传入的计算方式依次取出流中的元素和初始化值一起进行计算,计算结果后再和后面的元素计算。

他内部的计算方式如下:

// 定义初始值
T result = identity;
for (T element : this.stream)
    // 逐个取出流中的元素,按照apply中定义的逻辑与初始值进行计算
    result = accumulator.apply(result, element)
return result;

其中identity就是我们通过方法参数传入的初始值,accumulator的apply具体进行什么计算也是我们通过方法参数来确定的。

例子:

  1. 使用reduce求所有作者年龄的和。

先用匿名内部类的方式实现,第二个参数定义了一个 BinaryOperator() 并实现了 apply() 方法,定义聚合操作为 加法,注意传入的参数,第一个表示result,第二个表示流遍历到的元素。

public static void main(String[] args) {
    List<Author> authors = getAuthors();

    Integer ageSum = authors.stream()
            .distinct()
            .map(Author::getAge)
            .reduce(0, new BinaryOperator<Integer>() {
                @Override
                public Integer apply(Integer result, Integer element) {
                    return result + element;
                }
            });
}

再用Lambda表达式简化:

public static void main(String[] args) {
    List<Author> authors = getAuthors();

    Integer ageSum = authors.stream()
            .distinct()
            .map(Author::getAge)
            .reduce(0, (result, element) -> result + element);
}
  1. 使用reduce求所有作者中年龄的最大值。

实际上使用 max() 也可以实现这个操作,但是 max() 在底层其实也是调用的 reduce() 方法,并且在开发中有时候需要求一些其它类型的统计值,因此,还是看看如何使用 reduce() 来实现 max() 的功能。

public static void main(String[] args) {
    List<Author> authors = getAuthors();

    Integer maxAge = authors.stream()
            .map(Author::getAge)
            .reduce(Integer.MIN_VALUE, (result, element) -> result < element ? element : result);
}
单个参数

reduce(BinaryOperator<T> accumulator) 的作用和两参的 reduce() 作用一样,只是将流中的第一个元素作为初始值,而不是传入自定义的初始值,然后依然是按照 accumulator 中定义的操作进行计算。

其内部的调用原理如下所示:

boolean foundAny = false;
T result = null;  // 上面是将传入的identity作为result初始值
for (T element : this stream) {
    if (!foundAny) {  // 找出第一个元素,用来初始化result
        foundAny = true;
        result = element;
    } else {
        result = accumulator. apply(result, element);
    }
}
return foundAny ? Optional. of(result) : Optional. empty();

如果用单参数的 reduce() 实现求年龄最大值,代码如下:

public static void main(String[] args) {
    List<Author> authors = getAuthors();

    Optional<Integer> reduce = authors.stream()
            .map(Author::getAge)
            .reduce((result, element) -> result > element ? result : element);
}

accumulator 的参数中同样是,第一个表示result,第二个表示遍历到的流中的元素。

注意事项

  1. 惰性求值:

    • 在对流进行操作时,操作不会立即执行,而是等到需要结果时才进行计算,即没有结尾操作,中间操作是不会执行的。
    • 这种延迟计算的特性可以提高性能,因为它只计算流中实际需要的元素,而不是对整个流进行操作。
  2. 流是一次性的(One-Time Use):

    • Stream 流是一次性的,一旦对流进行了结尾操作(如收集结果、循环遍历等),流就会被消耗掉,无法再次使用。
    • 如果需要对同一组数据进行多个操作,可以创建一个新的流来进行操作。
public static void main(String[] args) {
    List<Author> authors = getAuthors();

    Stream<Author> stream = authors.stream();
    // 第一次对流进行处理,并执行了结尾操作
    stream.forEach(System.out::println);
    // 流终结后再次使用流,报错!!!
    stream.forEach(System.out::println);
}
  1. 不会影响原数据:

    • Stream 流的操作不会直接修改原始数据源中的元素,也不会影响原始数据源的结构。

    • 所有的流操作都是基于数据源的副本或视图进行的,保持了原始数据的不变性。

    • 除非在流中调用了流中元素对象的setter类似的方法,例如:

    public static void main(String[] args) {
        List<Author> authors = getAuthors();
    
        authors.stream()
                .map(new Function<Author, Object>() {
                    @Override
                    public Object apply(Author author) {
                        author.setAge(author.getAge() + 10);
                        return author;
                    }
                }).forEach(System.out::println);
    }