Java 8 Stream 使用总结

我一直以为,Stream 我接触的算晚了,可在工作中渐渐发现,尽管很多同事手握 Java8,但仍然遵循着传统的编程模式,并未充分利用 Java 8 新的特性。所以,这篇文章将谈谈 Stream 实战,并在实战中引出少部分概念。

文章通过两个用例,一个是如何从容器对象构造 Stream 的用例,另一个则是如何使用 Stream 的用例,通过这两个用例,你可以收获 Stream 的使用姿势。

容器对象构造 Stream 用例

// Construct Stream
// 1
Integer[] arrays = {1, 2, 3};
Stream<Integer> integerStream1 = Stream.of(arrays);
// 2
Stream<Integer> integerStream2 = Stream.of(1, 2, 3);
// 3
Stream<Integer> integerStream3 = Stream.<Integer>builder()
        .add(1).add(2).add(3).build();
// 4
IntStream intStream = IntStream.range(1, 4);

Stream 的构造非常简单,不过需要注意的是,除了 Stream 外,还有 IntStream, LongStream, DoubleStream 这类基本类型对应的流,这些流中增加了一些求和,求平均值等操作,并做了一些优化。

// Stream constructed by collection class
List<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
list.add(3);
// 1
Stream<Integer> listStream = list.stream();
// 2
Stream<Integer> parallelStream =list.parallelStream();

集合接口中新增了 stream 默认方法,调用该方法即可返回 Stream 对象。parallelStream 方法返回的是支持并行的 Stream 对象。

在前文中,我们也介绍过接口的默认方法,并提到它是为了方便这些新增的方法加入原有设计接口,并保持兼容的。

使用 Stream 用例

遍历

list.stream().forEach(System.out::println);
list.stream().forEachOrdered(System.out::println);
list.stream().peek(System.out::println).count();

上述代码是对流中的元素进行遍历。注意,创建的流对象不能重复使用,再次使用需要重新创建

peek 方法为什么需要再调用一个 count 操作呢?这是因为 peek 方法是一个中间操作,并不会立马执行。forEach 和 forEachOrdered 都是终结操作,会立马执行。所以 peek 方法需要再调用一个终结操作的方法来触发代码执行。

peek 方法这样使用并不推荐,这种使用方式在文档中被描述为 “副作用”,也就是并未合理地使用方法。

流的方法是否为终结操作可以通过文档查看。不过在日常使用中,我们按照方法的正常的调用逻辑来思考即可,比如,使用流对元素进行多种操作,包括后续介绍的过滤等,并不会多次的遍历流,因为多次遍历带来的性能损耗是不能接受的。

计算

统计元素个数

// 统计元素个数
println(list.stream().count()); //计数

匹配元素,返回 true 或 false

// 流中的所有元素都小于 4 ,则返回 true
println(list.stream().allMatch(e -> e < 4));
// 流中的任意一个元素等于 1 ,则返回 true
println(list.stream().anyMatch(e -> e == 1));
// 流中没有一个元素等于 1,则返回 true
println(list.stream().noneMatch(e -> e == 1));

查找元素

// 随机返回一个元素,如果没有的话,则返回 -1
println(list.stream().findAny().orElse(-1));
// 返回第一个元素,如果没有的话,返回 -1
println(list.stream().findFirst().orElse(-1));
// 返回最大值,没有的话,返回 -1
println(list.stream().max(Comparator
  .comparingInt(Integer::intValue)).orElse(-1));
// 返回最小值,没有的话,返回 -1
println(list.stream().min(Comparator
  .comparingInt(Intger::intValue)).orElse(-1));

findAny 方法的行为是不确定的,所以,利用它的随机性获取这一特性不太可取,它主要是为了最大化实现并行流操作的性能而设计的。

这类方法返回的都是 Optional 类,将该类用作调用获取实体对象方法的返回值时,可以非常有效的避免 NPE 问题,这是一个非常值得学习的编码习惯。

注意:不建议将任何的 Optional 类型作为字段或参数,optional 设计为:有限的机制让类库方法返回值清晰的表达 “没有值”。 optional 是不可被序列化的,如果类是可序列化的就会出问题。
也不建议将其用作获取集合对象的方法返回,获取集合对象的方法为了避免 NPE ,建议返回空集合。

数值计算:

这里的 Student 类以及 studentList 在紧接着的下文给出,不过有着丰富经验的你,应该能猜到它们指的是什么。

//数值计算
// 求和
int ageSum1 = studentList.stream().mapToInt(Student::getAge).sum();
int ageSum2 = studentList.stream()
  .map(Student::getAge).mapToInt(Integer::intValue).sum();
int ageSum3 = studentList.stream()
  .map(Student::getAge).flatMapToInt(IntStream::of).sum();
// 平均值
double averageAge =studentList.stream()
  .mapToInt(Student::getAge).averae().orElse(0.0);

sum 以及 average 是在基本类型的流中才有的方法。这里是将对象流转换为基本类型的流,即 Stream 转换为 IntStream。

转换

普通实体类 Studeng.java:

public static class Student{

    private String name;

    private Integer age;

    public Student(String name, Integer age){
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Integer getAge() {
        return age;
    }

    public void setAge(Integer age) {
        this.age = age;
    }
}

多个 Student 对象通过 Stream 组装为 List:

// 将多个对象组装为 list
List<Student> studentList = Stream.of(new Student("老大", 20)
        , new Student("老二", 18), new Student("老三", 16))
        .collect(Collectors.toList());

过滤并转换为 List, Set, map:

// 返回 (studentList 中 年龄小于 18) 的 list
List<Student> studentList1 = studentList.stream().filter(student ->
        student.getAge() < 18
).collect(Collectors.toList());

// 返回 (studentList 中 年龄小于 18) 的 set
Set<Student> studentSet = studentList.stream().filter(student ->
        student.getAge() < 18
).collect(Collectors.toSet());

// 返回 (studentList 中 年龄小于 18 ,且 name 到 age) 的 map
Map<String, Integer> nameAgeMap = studentList.stream().filter(student ->
        student.getAge() < 18
).collect(Collectors.toMap(Student::getName, Student::getAge, (u1, u2) -> u2));

// 返回 (studentList 中 年龄小于 18 ,且 name 到 age) 的有序的 map
Map<String, Integer> nameAgeSortedMap = studentList.stream().filter(student ->
        student.getAge() < 18
).collect(Collectors.toMap(Student::getName,Student::getAge, (u1, u2) -> u2,LinkedHashMap::new));

转换为 Map 的重载方法比较多,这是因为它需要考虑在 key 冲突后,如何存储值,即 (u1, u2) -> u2。自定义该 Lambda 表达式,可以决定当 key 重复时,如何选择 value 值。

LinkedHashMap::new 方法引用产生的 Map,将决定最后生成的 Map 的实现类。

不过再多的重载方法,都无法逃脱一个限制,那就是 key 或者 value 不能为 null。可在 HashMap 容器中,两者是可以为 null 的。所以,为了做到这个,我们需要选择使用原生的 collect 方法,而不是类库提供给我们的便捷的 Collectors:

Map<String, Integer> nameAgeMap = studentList.stream()
.collect(HashMap::new, (map, ele) -> map.put(ele.getName(), ele.getAge()), HashMap::putAll);

这里可以得到一个 Map<String, Integer> 对象,可能是和提升的类型推断有关,后续会有相关文章介绍这个特性。

那么,如果返回的 Set 接口 也想使用不同的实现类呢?(Collectors.toSet() 最终返回的是 HashSet )我们可以使用下面的方法:

// 返回 (studentList 中 年龄小于 18) 的 LinkedHashSet
 Set<Student> studentLinkedHashSet = studentList.stream().filter(student ->
         student.getAge() < 18
 ).collect(LinkedHashSet::new, Set::add, Set::addAll);

collect 方法除了接收 Colltors 提供的已经封装好的对象外,还支持自定义。LinkedHashSet::new 代表生成的容器对象,Set::add 代表如何往容器中添加元素,Set::addAll 代表在并行流中,如何合并两个容器。前文为了解决生成的 HashMap key 不能为 null 的问题,我们已自定义实现过。

转换为持有不同元素类型的 List:


// 从 Student 集合中返回一个去重且有序的 age 集合
List<Integer> ageSorted = studentList.stream()
 .map(Student::getAge).distinct().sorted()
 .collect(Colectors.toList());

map 方法接收一个 Lambda 表达式,表达式最终产生的值的类型将更改当前 Stream 的参数化类型。例如,在这里 studentList.stream() 返回的是一个 Stream ,但经过 map(Student::getAge) 方法后,产生的是一个 Stream ,所以,最终产生的 List 的参数化类型是 Integer。

实现分页

// 实现分页: 第一页开始,每页 2 条,这里返回第二页的数据
List<Student> pagingStudentList = studentList.stream()
  .skip(2).limit(2)
  .collect(Collectors.toList());

分组

// 相同年龄的 Student,即 age -> Students 的 Map
Map<Integer, List<Student>> ageStudentMap =studentList.stream()
  .collect(Collectors.groupingBy(Sudent::getAge, Collectors.toList()));

根据 age 进行分组,返回的 Map 中 value 为 List<Student>Collectors.toList() 也可以替换为 Collectors.mapping(Student::getName, Collectors.toList())

// 相同年龄的 Student,即 age -> names 的 Map
Map<Integer, List<String>> ageNamesMap = studentList.stream()
n.collect(Collectors.groupingBy(Student::getAge,
Collectors.mapping(Student::getName, Collectors.toList())));

这时,返回的 value 由 List<Student> 转换为了 List<String>

写在最后

其实,在日常中,Stream 使用的多了,也就熟练了。不过文章提到的很多小的点,也还是需要多了解一二,这样在使用的时候就完全能够游刃有余啦。


这是 Java 8 系列的第二篇文章,这篇注重实战,介绍了很多 Stream 的使用方式,我也尝试着按自己的方式分了类,但难免有纰漏之处,还请见谅。如果你觉得我的文章还不错,并对后续文章感兴趣的话,或者说我们有什么能够交流分享的,可以通过扫描下方二维码来关注我的公众号!我与风来

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值