Java8: Stream

逆秩枚举
• 阅读 5397

写在前面: Java8的Stream用起来真的不是一般的爽。当你看到Stream的操作后,相信你再也不会去写各种for循环、嵌套for循环,特别是做报表,体会更深.

What is Stream?

流(Stream)是Java API的新成员,它允许以声明性的方式处理数据集合(类似于数据库查询语句).暂且理解为遍历数据集的高级迭代器.

先举个例子尝尝鲜:

/*
需求: 获取菜单中热量小于400卡路里的菜肴名称,并按照卡路里排序.
*/
@Data
@Accessor(chain = true)
public class Dish {
    // 该类将会在本文中多次用到
    // omit getter,setter and constructor
    private String name;
    private boolean vegetarian;
    private int calories;
    private Type type;

    public enum Type {MEAT, FISH, OTHER}
}

// 普通写法
public static List<String> getLowCaloricDishesNamesInJava7(List<Dish> dishes){
    List<Dish> lowCaloricDishes = new ArrayList<>();
    for(Dish d: dishes){
        if(d.getCalories() < 400){
            lowCaloricDishes.add(d);
        }
    }
    List<String> lowCaloricDishesName = new ArrayList<>();
    Collections.sort(lowCaloricDishes, new Comparator<Dish>() {
        public int compare(Dish d1, Dish d2){
            return Integer.compare(d1.getCalories(), d2.getCalories());
        }
    });
    for(Dish d: lowCaloricDishes){
        lowCaloricDishesName.add(d.getName());
    }
    return lowCaloricDishesName;
}
// Stream 写法
public static List<String> getLowCaloricDishesNamesInJava8(List<Dish> dishes){
    return dishes.stream()
        .filter(d -> d.getCalories() < 400)
        .sorted(comparing(Dish::getCalories))
        .map(Dish::getName)
        .collect(toList());
}

从上面的代码可以明显看出区别,Stream写法更加简短、优美,并且可读性很强,我看到这段代码我就知道是干什么的.这就引申出Stream的优点:

  • 声明性 -- 更简洁,更易读(表达能力很强)
  • 可复合 -- 更灵活(可过滤,可映射,可排序...)
  • 可并行 -- 性能更好(使用parallelStream)

Introduce of Stream

流就是从支持数据处理操作的源生成的元素序列

  • 元素序列: 我理解的存储流数据的数据结构(?)
  • 源: 提供数据的源,如集合,数组,输入/输出资源等.从有序集合生成流时会保留原有的顺序.
  • 数据处理操作: filter,sort,map,find...流操作可以顺序执行,也可以并行执行.

Stream and Collection

集合是数据结构,所以它的主要目的是存储和访问集合元素,但流的目的是在于计算.

集合讲的是数据,集合可以遍历无数次,而流只能遍历一次,遍历完之后我们就说这个流被消费掉了.准确的说,流只能被消费一次,那些终端操作都是消费流.

Stream<Integer> s = Arrays.asList(1,2,3,4).stream();
s.forEach(System.out.print);  // 打印:1 2 3 4
s.forEach(System.out.print);  // 无打印 

Inner Iteration and Outer Iteration

使用集合需要我们自己去做迭代(比如for-each),这就叫外部迭代.相反,Stream库使用内部迭代,也就是不需要我们去做迭代.比如:

// 还是上面那个Dish类,假设有个对象List<Dish> menu, 要打印menu中所有菜肴的名称
// 外部迭代
for(Dish d : menu) {
    System.out.println(d.getName());
}
// 内部迭代
menu.stream().map(Dish::getName).forEach(System.out::println);

Operations of Stream

先看一个例子:

menu.stream().filter(d -> d.getCalories() > 300).map(Dish::getName).forEach(System.out::println);
  • 生成流: menu.steam(),生成流的方式有好多,如Arrays.stream(),可自己去查看Java API
  • 中间操作(流的延迟性质): 处理流并返回流,比如filter,map,distinct等,类似fluent API.
  • 终端操作: 消费流数据,比如forEach,collect等.

流的延迟性质

如果流水线上没有触发一个终端操作,那么中间操作是不会对流数据进行处理的.这是因为中间操作一般可以合并起来,在终端操作时一次性处理.比如:

List<String> names = menu.stream().filter(d -> {
        System.out.println("filtering");
        return d.getCalories() > 300;
    }).map(d -> {
        System.out.println("mapping");
        return d.getName();
    }).limit(3).collect(toList());
/*
上述的代码输出:
    filtering
    mapping
    filtering
    mapping
    filtering
    mapping
*/

从上述的打印结果明显可以看出来,流会对中间操作进行合并,尽管filter和map是两个独立的操作,但它们合并到同一次遍历中了(循环合并).

Common Operations

很多流操作的方法参数类型都是函数式接口,这些函数式接口都是JDK自带的,本文将不会解释这些函数式接口,可以自己看接口的定义.

操作 类型 参数类型 函数描述符 描述
filter 中间 Predicate<T> T -> Boolean 过滤
distinct 中间 去重
skip 中间 long 跳过前几项
limit 中间 long 只取前几项
map 中间 Function<T,R> T -> R 映射
flatMap 中间 Function<T,Stream<R>> T -> Stream<R> 扁平化流
sorted 中间 Comparator<T> (T,T) -> int 排序
anyMatch 终端 Predicate<T> T -> Boolean 任意项匹配
noneMatch 终端 Predicate<T> T -> Boolean 无匹配
allMatch 终端 Predicate<T> T -> Boolean 所有匹配
findAny 终端 返回任意项
findFirst 终端 返回第一项
forEach 终端 Consumer<T> T -> void 遍历流
collect 终端 Collector<T,A,R> 收集流数据
reduce 终端 BinaryOperator<T> (T,T) -> T 归约
count 终端 long 数量

上面这些都是常用的流操作,顺便提一下,使用skip和limit还可以做分页操作.下面讲解一下map,flatMap,reduce

映射: map

映射,也就是我从一个数据经过某些操作变成了另一个数据,也就是x --> y


x --f(x)--> y

举个栗子:

// 获取List<Dish> menu中所有菜肴的名称
Stream<Dish> ds = menu.stream();  //菜单流
Stream<String> ns = menu.stream().map(e -> e.getName());  //菜肴名称流

扁平化: flatMap

扁平化流,这是<<Java8实战>>中这么翻译的,按我个人理解的话,我觉得flatMap就是合并流:

Stream<T> + Stream<T> + Stream<T> ==> Stream<R>

举个栗子:

// 有一个String集合,需要将每个String切分成字符,并去重
List<String> ss = Arrays.asList("Hello", "World")
                        .stream()                   // Stream<String>
                        .map(e -> e.split(""))      // Stream<String[]>
                        .flatMap(Arrays::stream)    // Stream<String>
                        .distinct()
                        .collect(toList());
ss.forEach(System.out::print);
/*
输出:
Helowrd
*/

对于上面这个栗子,执行map操作后返回Stream<String[]>(String[]指的是流元素的类型),接下来执行flatMap,将String[] -> Stream<String>, 然后将多个Stream<String>合并成一个Stream<String>.
下面这张图可以很形象地解释flatMap.
Java8: Stream

归约: reduce

归约这个说法不太好理解,查看词典reduce还有个解释是"归纳为".wiki上对于归约的解释:

所谓的归约是将某个计算问题转换为另一个问题的过程。

也就是说reduce是描述如何从一个计算问题转换为另一个问题.大概是这么理解的吧.嗯,应该就是这样理解的(有木有大佬帮忙解释一下...〒︿〒).还是举几个栗子吧.

/*
计算一个数值集合的总和
*/
Integer sum = Arrays.asList(4, 5, 3, 9).stream().reduce(0, (a, b) -> a + b);
// 计算问题: 计算List<Integer>的总和
// 过程: reduce, 转换为另一个问题"可以设置一个初值为0, 然后每次累加, 也就是(a, b) -> a + b"
// (可能理解有误)

/*
求一个数值集合的最大值
*/
Integer max = Arrays.asList(1, 2, 3, 4, 5).stream().reduce(Integer::max).orElse(0);

至于reduce方法具体是如何实现的,一步一步累加的过程,可以看下图:Java8: Stream

reduce有三个重载方法,可根据需要使用对应的方法.

    T reduce(T identity, BinaryOperator<T> accumulator);
    Optional<T> reduce(BinaryOperator<T> accumulator);
    <U> U reduce(U identity,BiFunction<U, ? super T, U> accumulator,BinaryOperator<U> cobiner);

使用上面三个方法的时候要注意函数式接口的类型,以及泛型,下面我举个例子,计算菜单中所有的菜肴的热量总和,声明一点,下面的写法是非常不好的写法(shit code),只是单纯用来比较reduce三个重载方法的用法,以及写的时候要注意函数式接口的类型

Dish sum1 = dishes.stream().reduce(new Dish(), (a, b) -> new Dish().setCalories(a.getCalories() + b.getCalories()));
System.out.println(sum1.getCalories());

Dish sum2 = dishes.stream().reduce((a, b) -> new Dish().setCalories(a.getCalories() + b.getCalories())).get();
System.out.println(sum2.getCalories());

// 第三个参数暂时还不知道什么用处
Integer sum3 = dishes.stream().reduce(0, (c, d) -> c + d.getCalories(), (a, b) -> a - b);
System.out.println(sum3);

当然上面那个求和也可以这么写: Arrays.asList(4, 5, 3, 9).stream().mapToInt(e -> e).sum(),其实sum的实现也是调的reduce方法.这里的mapToInt是转化成一个IntStream(数值流).

Numerical Stream

举个例子:

int calories = menu.stream().map(Dish::getCalories).reduce(0, Integer::sum);

这段代码的的问题是,它有一个暗含装箱的成本(为什么是装箱,而不是拆箱的成本?).

Java8引入了三个原始类型特化流接口来解决这个问题: IntStream,LongStream,DoubleStream,分别将流中的元素特化为int,long,double,从而避免了暗含的装箱成本.映射到数值流,可以使用mapToInt,mapToLong,mapToDouble,而转换为对象流直接调用boxed()方法即可.

Java8为数值流提供了很多的方法,比如sum,min,max,count,average等等.现在咱们就可以使用数值流来计算菜单中所有菜肴的热量总和:

int calories = menu.stream().mapToInt(Dish::getCalories).sum();

Summary

  • 在Stream中使用了大量的函数式接口,结合上一篇博客来说的话,流是函数式接口的最佳实践,那么使用流是Lambda表达式的最佳实践
  • Stream的专注点是处理数据,只能被消费一次(终端操作),而集合的重点是在于存取
  • Stream的写法简短易读,一目了然
  • Stream的常用操作,除了flatMap和reduce不太好理解,其他的都是顾名思义
  • 数值流,减少了装箱的成本
点赞
收藏
评论区
推荐文章
blmius blmius
3年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
Wesley13 Wesley13
3年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
美凌格栋栋酱 美凌格栋栋酱
6个月前
Oracle 分组与拼接字符串同时使用
SELECTT.,ROWNUMIDFROM(SELECTT.EMPLID,T.NAME,T.BU,T.REALDEPART,T.FORMATDATE,SUM(T.S0)S0,MAX(UPDATETIME)CREATETIME,LISTAGG(TOCHAR(
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
Wesley13 Wesley13
3年前
Java8系列之Stream总结
流的简介  官方解释,Stream是Java8的一大亮点,它与java.io包里的InputStream和OutputStream是完全不同的概念。它也不同于StAX对XML的解析的Stream,也不是AmazonKinesis对大数据实时处理的Stream。它是对集合对象功能的增强,她专注于对集合对象进行各种非常便利、高效的聚合操作(ag
Wesley13 Wesley13
3年前
FLV文件格式
1.        FLV文件对齐方式FLV文件以大端对齐方式存放多字节整型。如存放数字无符号16位的数字300(0x012C),那么在FLV文件中存放的顺序是:|0x01|0x2C|。如果是无符号32位数字300(0x0000012C),那么在FLV文件中的存放顺序是:|0x00|0x00|0x00|0x01|0x2C。2.  
Wesley13 Wesley13
3年前
mysql设置时区
mysql设置时区mysql\_query("SETtime\_zone'8:00'")ordie('时区设置失败,请联系管理员!');中国在东8区所以加8方法二:selectcount(user\_id)asdevice,CONVERT\_TZ(FROM\_UNIXTIME(reg\_time),'08:00','0
Wesley13 Wesley13
3年前
Java8 新特性之集合操作Stream
Java8新特性之集合操作StreamStream简介Java8引入了全新的StreamAPI。这里的Stream和I/O流不同,它更像具有Iterable的集合类,但行为和集合类又有所不同。stream是对集合对象功能的增强,它专注于对集合对象进行各种非常便利、高效的聚合操作,或者大批量数据操作。
Wesley13 Wesley13
3年前
PHP创建多级树型结构
<!lang:php<?php$areaarray(array('id'1,'pid'0,'name''中国'),array('id'5,'pid'0,'name''美国'),array('id'2,'pid'1,'name''吉林'),array('id'4,'pid'2,'n
Wesley13 Wesley13
3年前
Java8 Stream 的一些操作和介绍
原创:转载需注明原创地址https://www.cnblogs.com/fanerwei222/p/11858186.htmlJava8Stream是一个新的东西,就是能够将常见的数据结构转化成Stream,再直接用这个Stream来做各种操作,比如过滤,大小写转换,提取某个实体的某个属性,都可以直接通过Stream的方法来操作
Wesley13 Wesley13
3年前
Java 8 – Filter a null value from a Stream
Java8–FilteranullvaluefromaStreampackagecom.mkyong.java8;importjava.util.List;importjava.util.stream.Collectors;importjava.util.stream.Stream;publiccla