登录 |  注册
首页 >  编程语言 >  java8特性 >  Java 8 Stream

Java 8 Stream

Java 8 中的 Stream 是对集合(Collection)对象功能的增强,它专注于对集合对象进行各种非常便利、高效的聚合操作(aggregate operation),或者大批量数据操作 (bulk data operation)。Stream API 借助于同样新出现的Lambda表达式,极大的提高编程效率和程序可读性。同时它提供串行和并行两种模式进行汇聚操作,并发模式能够充分利用多核处理器的优势,使用fork/join并行方式来拆分任务和加速处理过程。通常编写并行代码很难而且容易出错, 但使用Stream API无需编写一行多线程的代码,就可以很方便地写出高性能的并发程序。所以说,Java 8 中首次出现的java.util.stream 是一个函数式语言+多核时代综合影响的产物。

java.png

@Test
public void test() {
    // 通过集合的stream方法获取流
    List<String> list = new ArrayList<>();
    Stream<String> stream = list.stream();
    // 通过Arrays工具类的stream方法获取流
    Stream<String> stream1 = Arrays.stream(new String[]{"1", "2", "3", "4", "5"});
    // 通过Stream类的of方法获取流
    Stream<Integer> stream2 = Stream.of(1, 2, 3, 4, 5);
    // 创建无限流
    Stream<Integer> stream3 = Stream.iterate(0, (x) -> x + 2);
}

以上是四种获取Stream的方式,最后一种创建的是无限流,也就是说,该流创建的是从0开始,每次加2的无限序列。

使用Stream我们能够很轻松地实现过滤操作,比如获取无限流中前5个元素,代码如下:

@Test
public void test() {
    Stream<Integer> stream = Stream.iterate(0, (x) -> x + 2);
    Stream<Integer> stream1 = stream.limit(5);
    stream1.forEach(System.out::println);
}

通过limit方法即可实现获取前5个元素,这里使用了forEach方法进行遍历,并使用了Lambda表达式,我们一起来复习一下,查看forEach方法的源码:

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

可以看到该方法的参数是一个消费型的函数式接口,其接口的抽象方法是带参而无返回值的,所以若是想输出元素,则可以如此做:

stream1.forEach((i) -> System.out.println(i));

又因为输出语句的参数和返回值与接口抽象方法的声明一致,所以可以使用方法引用,最终简化为:

stream1.forEach(System.out::println);

我们还可以通过Stream类的generate方法生成无限流:

@Test
public void test() {
    Stream.generate(Math::random)
        .limit(5)
        .forEach(System.out::println);
}

generate方法需要的是一个供给型接口,它的抽象方法是不带参而有返回值的,我们通过 Math.random() 方法生成随机数作为返回值,又因为random方法也是带一个参而有返回值的,所以可以使用类的静态方法引用,后续的Stream操作中将涉及大量的Lambda表达式,届时将不再过多介绍Lambda表达式的写法。

Stream的筛选操作

刚才我们通过limit方法简单地了解到Stream的筛选功能,当然了,Stream的筛选能力可远不止如此,这里介绍四种筛选方法:

  1. filter

  2. limit

  3. skip

  4. distinct

@Test
public void test() {
    List<User> userList = Arrays.asList(
        new User("张三", 20, 0),
        new User("李四", 30, 0),
        new User("王五", 25, 1),
        new User("赵六", 42, 1)
    );
    // 筛选出年龄大于25的用户
    Stream<User> stream = userList.stream()
        .filter((user) -> user.getAge() > 25);
    stream.forEach(System.out::println);
}

通过filter方法,我们能够实现很多的筛选功能,比如这里就可以筛选出年龄大于25的用户,filter方法的参数是一个断言型接口,接收一个参数,返回值为boolean类型,即:为true则满足筛选条件,为false则不满足。

limit方法我们已经使用过了,它用来截断Stream,使得Stream获取从开始位置到指定位置的元素内容。

第三个方法是skip:

@Test
public void test() {
    List<User> userList = Arrays.asList(
        new User("张三", 20, 0),
        new User("李四", 30, 0),
        new User("王五", 25, 1),
        new User("赵六", 42, 1)
    );
    // 筛选出年龄大于25的用户
    Stream<User> stream = userList.stream()
        .skip(2);
    stream.forEach(System.out::println);
}

它与limit方法正好相反,skip会丢弃掉从开始位置到指定位置的元素内容,比如上面这段程序便会将 张三 和 李四 用户的信息丢掉。

最后一个方法是distinct:

@Test
public void test() {
    List<User> userList = Arrays.asList(
        new User("张三", 20, 0),
        new User("李四", 30, 0),
        new User("王五", 25, 1),
        new User("王五", 25, 1),
        new User("王五", 25, 1),
        new User("赵六", 42, 1)
    );
    // 筛选出年龄大于25的用户
    Stream<User> stream = userList.stream()
        .distinct();
    stream.forEach(System.out::println);
}

该方法用于去除流中重复的元素,需要注意的是该方法依赖于equals和hashCode方法,所以User对象必须重写这两个方法:

public class User {
    private String name;
    private Integer age;
    private Integer sex;
 ......
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        User user = (User) o;
        return Objects.equals(name, user.name) && Objects.equals(age, user.age) && Objects.equals(sex, user.sex);
    }
    @Override
    public int hashCode() {
        return Objects.hash(name, age, sex);
    }
}

Stream的映射操作

Stream中的map方法用于映射操作,它能将元素转换成其它形式或提取信息,接收一个函数作为参数,该函数会被应用到Stream中的每个元素上,并将其映射成为一个新的元素。

@Test
public void test() {
    List<String> list = Arrays.asList("aa", "bb", "cc", "dd", "ee");
    list.stream()
        .map(String::toUpperCase)
        .forEach(System.out::println);
}

map方法的参数是一个函数型接口,Stream中的每个元素都会被应用到该接口方法的实现上,所以每个元素都会被转换成大写字母。

map方法也能用于提取Stream中每个元素的信息:

@Test
public void test() {
    List<User> userList = Arrays.asList(
        new User("张三", 20, 0),
        new User("李四", 30, 0),
        new User("王五", 25, 1),
        new User("赵六", 42, 1)
    );
    userList.stream()
        .map(User::getName)
        .forEach(System.out::println);
}

此时所有用户的名字就都被提取出来了。

Stream的排序操作

Stream中的排序也分为两种,自然排序和自定义排序,首先是自然排序,调用sorted方法即可实现:

@Test
public void test() {
    List<Integer> list = Arrays.asList(3, 1, 6, 7, 5, 9);
    list.stream()
        .sorted()
        .forEach(System.out::println);
}

自定义排序就需要传入自己实现的比较器:

@Test
public void test() {
    List<User> userList = Arrays.asList(
        new User("张三", 20, 0),
        new User("李四", 20, 0),
        new User("王五", 25, 1),
        new User("赵六", 42, 1)
    );
    userList.stream()
        .sorted((u1, u2) -> {
            if (u1.getAge().equals(u2.getAge())) {
                return u1.getName().compareTo(u2.getName());
            } else {
                return u1.getAge().compareTo(u2.getAge());
            }
        })
        .forEach(System.out::println);
}

该程序段实现了按照年龄对User信息进行排序,若年龄相同,则再按照姓名排序。

Stream的匹配操作

Stream中提供了丰富的匹配方法用于校验数据,分别如下:

  • allMatch:是否匹配所有元素

  • anyMatch:是否至少匹配一个元素

  • noneMatch:是否没有匹配所有元素

  • findFirst:返回第一个元素

  • findAny:返回流中的任意元素

  • count:返回元素个数

  • max:返回最大值

  • min:返回最小值

比如:

@Test
public void test() {
    List<User> userList = Arrays.asList(
        new User("张三", 20, 0),
        new User("李四", 20, 0),
        new User("王五", 25, 1),
        new User("赵六", 42, 1)
    );
    boolean flag = userList.stream()
        .allMatch((u) -> u.getSex().equals(1));
    System.out.println(flag);
}

这段程序中,它会判断Stream中的所有User对象的sex值是否为1,若满足,则返回true,否则返回false,这里的结果当然就是false了。

若是将匹配方法修改为 anyMatch ,则结果会变为true:

@Test
public void test() {
    List<User> userList = Arrays.asList(
        new User("张三", 20, 0),
        new User("李四", 20, 0),
        new User("王五", 25, 1),
        new User("赵六", 42, 1)
    );
    boolean flag = userList.stream()
        .anyMatch((u) -> u.getSex().equals(1));
    System.out.println(flag);
}

当所有User对象的sex为0时结果才为false。

noneMatch方法:

@Test
public void test() {
    List<User> userList = Arrays.asList(
        new User("张三", 20, 0),
        new User("李四", 20, 0),
        new User("王五", 25, 1),
        new User("赵六", 42, 1)
    );
    boolean flag = userList.stream()
        .noneMatch((u) -> u.getSex().equals(1));
    System.out.println(flag);
}

因为Stream中有User对象的sex值为1,所以没有匹配所有元素是不成立的,故结果为false,只有当所有User对象的sex值均为0,此时没有匹配所有元素成立,结果才为true。

findFirst方法:

@Test
public void test() {
    List<User> userList = Arrays.asList(
        new User("张三", 20, 0),
        new User("李四", 20, 0),
        new User("王五", 25, 1),
        new User("赵六", 42, 1)
    );
    Optional<User> optional = userList.stream()
        .sorted(Comparator.comparing(User::getAge))
        .findFirst();
    User user = optional.get();
    System.out.println(user);
}

该程序段对Stream中的User对象按年龄进行升序,并使用findFirst方法获取第一个User对象,注意这里的返回值是Optional,这也是JDK1.8的新特性,它是用来避免频繁出现的空指针异常的,这个我们后面会介绍。

findAny方法:

@Test
public void test() {
    List<User> userList = Arrays.asList(
        new User("张三", 20, 0),
        new User("李四", 20, 0),
        new User("王五", 25, 1),
        new User("赵六", 42, 1)
    );
    Optional<User> optional = userList.stream()
        .filter((u) -> u.getAge().equals(20))
        .findAny();
    User user = optional.get();
    System.out.println(user);
}

这里首先使用filter过滤出年龄为20的用户,此时张三和李四都符合条件,而findAny方法便会从这两个对象中随机选择一个返回,然而这种情况它只会一直返回姓名为张三的User对象,因为当前的Stream是串行流,我们需要获取并行流才能实现随机获取的效果:

// parallelStream()方法获取并行流
Optional<User> optional = userList.parallelStream()
    .filter((u) -> u.getAge().equals(20))
    .findAny();

count方法:

@Test
public void test() {
    List<User> userList = Arrays.asList(
        new User("张三", 20, 0),
        new User("李四", 20, 0),
        new User("王五", 25, 1),
        new User("赵六", 42, 1)
    );
    long count = userList.stream()
        .count();
    System.out.println(count);
}

获取当前Stream中的元素个数,结果为4。

max方法:

@Test
public void test() {
    List<User> userList = Arrays.asList(
        new User("张三", 20, 0),
        new User("李四", 20, 0),
        new User("王五", 25, 1),
        new User("赵六", 42, 1)
    );
    Optional<User> optional = userList.stream()
        .max(Comparator.comparing(User::getAge));
    User user = optional.get();
    System.out.println(user);
}

该程序段获取的是Stream中年龄最大的User对象。

min方法:

@Test
public void test() {
    List<User> userList = Arrays.asList(
        new User("张三", 20, 0),
        new User("李四", 20, 0),
        new User("王五", 25, 1),
        new User("赵六", 42, 1)
    );
    Optional<User> optional = userList.stream()
        .min(Comparator.comparing(User::getAge));
    User user = optional.get();
    System.out.println(user);
}

将调用方法换为min,则它将获取Stream中年龄最小的User对象。

Stream的收集操作

@Test
public void test() {
    List<Integer> list = Arrays.asList(3, 1, 6, 7, 5, 9);
    Integer result = list.stream()
        .reduce(0, Integer::sum);
    System.out.println(result);
}

该程序段中使用了一个新的方法:reduce ,它的作用是将流中的元素按规则反复地结合起来,得到一个值,比如这里的结果就是将流中的元素全部相加得到和,我们先将方法引用展开再说说其原理:

@Test
public void test() {
    List<Integer> list = Arrays.asList(3, 1, 6, 7, 5, 9);
    Integer result = list.stream()
        .reduce(0, (i, j) -> i + j);
    System.out.println(result);
}

reduce会将第一个参数值0作为起始值,其第一次迭代便是将0作为变量i的值,并从流中取出第一个元素作为变量j的值,所以第一次的执行结果为:0 + 1 = 1 ,并将该结果作为变量i的值,从流中取出第二个元素作为变量j的值进行下一次运算,结果为:1 + 3 = 4 ,以此类推,最终得到总和。

Stream提供了collect方法用于收集操作,它可以将那些经过过滤、映射、排序等操作后的Stream数据重新收集起来,成为一个新的集合数据,代码如下:

@Test
public void test() {
    List<User> userList = Arrays.asList(
        new User("张三", 20, 0),
        new User("李四", 20, 0),
        new User("王五", 25, 1),
        new User("赵六", 42, 1)
    );
    List<String> nameList = userList.stream()
        .map(User::getName)
        .collect(Collectors.toList());
    System.out.println(nameList);
}

此时我们便将所有User对象的姓名取出,并收集到了一个新的集合中。Collectors类提供了非常多的静态方法供我们更方便地进行收集,toList、toSet、toMap、toCollection等等,比如将数据收集成Set集合:

@Test
public void test() {
    List<User> userList = Arrays.asList(
        new User("张三", 20, 0),
        new User("李四", 20, 0),
        new User("王五", 25, 1),
        new User("王五", 25, 1),
        new User("赵六", 42, 1)
    );
    Set<String> nameSet = userList.stream()
        .map(User::getName)
        .collect(Collectors.toSet());
    System.out.println(nameSet);
}

则这样便取出了所有User对象的姓名且不重复。若是想将数据收集成HashSet呢?Collectors类中并未提供toHashSet方法,但我们可以通过toCollection方法实现:

@Test
public void test() {
    List<User> userList = Arrays.asList(
        new User("张三", 20, 0),
        new User("李四", 20, 0),
        new User("王五", 25, 1),
        new User("王五", 25, 1),
        new User("赵六", 42, 1)
    );
    Set<String> nameSet = userList.stream()
        .map(User::getName)
        .collect(Collectors.toCollection(HashSet::new));
    System.out.println(nameSet);
}

toCollection方法需要接收一个供给型接口,通过构造器引用创建HashSet对象即可。

Collectors还提供了一些类似SQL的聚合操作,比如求元素个数:

Long count = userList.stream()
    .map(User::getName).count();

求平均值:

Double avgAge = userList.stream()
    .collect(Collectors.averagingDouble(User::getAge));

求总和:

Integer sumAge = userList.stream()
    .collect(Collectors.summingInt(User::getAge));

最大值:

Optional<User> optional = userList.stream()
    .collect(Collectors.maxBy(Comparator.comparingInt(User::getAge)));
User user = optional.get();

最小值:

Optional<User> optional = userList.stream()
    .collect(Collectors.minBy(Comparator.comparingInt(User::getAge)));
User user = optional.get();

它甚至能够实现分组,比如按照年龄分组:

@Test
public void test() {
    List<User> userList = Arrays.asList(
        new User("张三", 20, 0),
        new User("李四", 20, 0),
        new User("王五", 25, 1),
        new User("王五", 25, 1),
        new User("赵六", 42, 1)
    );
    Map<Integer, List<User>> map = userList.stream()
        .collect(Collectors.groupingBy(User::getAge));
    System.out.println(map);
}

运行结果:

{20=[User{name='张三', age=20, sex=0}, User{name='李四', age=20, sex=0}], 25=[User{name='王五', age=25, sex=1}, User{name='王五', age=25, sex=1}], 42=[User{name='赵六', age=42, sex=1}]}

还能够进行多级分组,比如先按照年龄分组,再按照性别分组:

@Test
public void test() {
    List<User> userList = Arrays.asList(
        new User("张三", 20, 0),
        new User("李四", 20, 0),
        new User("王五", 25, 1),
        new User("王五", 25, 1),
        new User("赵六", 42, 1)
    );
    Map<Integer, Map<String, List<User>>> map = userList.stream()
        .collect(Collectors.groupingBy(User::getAge, Collectors.groupingBy((u) -> {
            if (u.getSex() == 0) {
                return "男";
            } else {
                return "女";
            }
        })));
    System.out.println(map);
}

运行结果:

{20={男=[User{name='张三', age=20, sex=0}, User{name='李四', age=20, sex=0}]}, 25={女=[User{name='王五', age=25, sex=1}, User{name='王五', age=25, sex=1}]}, 42={女=[User{name='赵六', age=42, sex=1}]}}

按年龄进行分区:

@Test
public void test() {
    List<User> userList = Arrays.asList(
        new User("张三", 20, 0),
        new User("李四", 20, 0),
        new User("王五", 25, 1),
        new User("王五", 25, 1),
        new User("赵六", 42, 1)
    );
    Map<Boolean, List<User>> map = userList.stream()
        .collect(Collectors.partitioningBy((u) -> u.getAge() > 20));
    System.out.println(map);
}

运行结果:

{false=[User{name='张三', age=20, sex=0}, User{name='李四', age=20, sex=0}], true=[User{name='王五', age=25, sex=1}, User{name='王五', age=25, sex=1}, User{name='赵六', age=42, sex=1}]}

它会按照年龄大于20和小于等于20的规则将User对象分为两组。

<<上一课程
推荐文章
  • 之前看过一篇文章介绍H标签的使用方法:H1必须有只能是一个,H2标签可以是多个。不同标签的作用h1和h2标签主要是提高关键词的密度更容易让蜘蛛抓取我们首先要明白HEADER标签是什么。HEADER标签就是HTML语言中的h1到h6定义标题头的六个不同文字大小的TAGES。本质是为了呈现内容结构,共有
  • 高并发下如何设计秒杀系统?秒杀系统是网络商家为了促销等目的进行的网上限时抢购活动。比如某宝某东某夕夕上的秒杀。用户在规定的时间内,定时定量的秒杀,无论商品是否秒杀完毕,该场次的秒杀活动都会结束。秒杀系统具有瞬时流量、高并发读、高并发写以及高可用等特点。秒杀时会有大量用户在同一时间进行抢购,瞬时并发访
  • 经常做仓储管理系统,很少关注电商系统,之前兼职做过一个电商系统,其对应的商品档案就很容易和仓储管理系统混淆,今天抽个时间整理下他们在前端展示的区别和联系。一个电商系统,SPU、SKU、快照等设计的好坏,影响到后面的开发进度,以及架构的调整等。而SPU、SKU、快照又是一个电商系统的核心。SPU,是s
  • Java8新特性中最为重要的便是Lambda表达式和StreamAPI了,先来了解一下Lambda表达式吧。Lambda表达式Lambda表达式是一个匿名函数,我们可以将Lambda表达式理解为一段可以作为参数传递的代码,通过Lambda表达式,我们可以将Java程序变得更加简洁和灵活。来看一段程序
  • Java8中的Stream是对集合(Collection)对象功能的增强,它专注于对集合对象进行各种非常便利、高效的聚合操作(aggregateoperation),或者大批量数据操作(bulkdataoperation)。StreamAPI借助于同样新出现的Lambda表达式,极大的提高编程效率和
  • 要养成一个好的编码习惯从自己编码开始,对自己代码的合理化命名,编码不仅对自己有好处,而且别人也容易读懂你的代码。所以下载阿里的代码规范插件来约束自己凌乱的代码。阿里规范插件GitHub地址:https://github.com/alibaba/p3cIDEA安装该插件步骤:1.打开IDEA,File
学习大纲
0.1 Java8新特性介绍
0.2 Java 8 Stream