转载

Java8函数式编程之Stream编程详解(1)中间操作

我始终认为通过案例学习是最好的掌握一个技能的方式,因此我们还是通过之前文章中的案例,对Stream编程中涉及到的具体的方法进行详细的学习。

初始化数据

首先初始化一个List集合,用于后续讲解所用。

List<Sku> list;

@Before
public void init() {
    list = CartService.getCartSkuList();
}

我们仍旧使用之前的购物车数据

中间操作实例

首先讲解中间操作,它分为有状态和无状态两种形式

filter使用:过滤不符合断言判断的数据

@Test
public void filterTest() {
    list.stream()
            // 判断是否符合一个断言(商品类型是否为书籍),不符合则过滤元素
            .filter(sku -> SkuCategoryEnum.BOOKS.equals(sku.getSkuCategory()))
            // 打印终端操作结果
            .forEach(item -> System.out.println(JSON.toJSONString(item, true)));
}

这个操作的目的是:从商品列表中选择所有的书籍类型,其余类型过滤

运行结果:

{"skuCategory":"BOOKS","skuId":121,"skuName":"Java编程思想","skuPrice":100.0,"totalNum":10,"totalPrice":100.0}
{"skuCategory":"BOOKS","skuId":3,"skuName":"程序化广告","skuPrice":80.0,"totalNum":10,"totalPrice":80.0}

总结:filter用于判断集合数据是否符合一个断言,不符合则过滤掉元素

filter结果为TRUE,表示通过测试,数据不过滤;结果为FALSE,表示未通过测试,数据被过滤

map使用:将一个元素转换为另一个元素

@Test
public void mapTest() {
    list.stream()
            // 将一个元素转换为另一个数据类型的元素
            .map(sku -> sku.getSkuName())
            // 打印终端操作结果
            .forEach(item -> System.out.println(JSON.toJSONString(item, true)));
}

这个操作的目的是:从商品列表中获取每个商品的名称,并打印

运行结果:

"无人机"
"VR一体机"
"牛仔裤"
"衬衫"
"Java编程思想"
"程序化广告"

总结:map用于将一个元素转换为另一个(类型的)元素

flatMap(扁平化)使用:将一个对象转换为一个流的操作

@Test
public void flatMap() {
    list.stream()
            // 这里将商品名称切分为一个字符流,最终输出
            .flatMap(sku -> Arrays.stream(sku.getSkuName().split("")))
            // 打印终端操作结果
            .forEach(item -> System.out.println(JSON.toJSONString(item, true)));
}

这个操作的目的是:将商品名称逐字进行拆分

运行结果:

"无"
"人"
"机"
"V"
"R"
"一"
"体"
"机"
"牛"
"仔"
"裤"
"衬"
"衫"
"J"
"a"
"v"
"a"
"编"
"程"
"思"
"想"
"程"
"序"
"化"
"广"
"告"

从运行结果可以直观看出:flatMap接收一个流 并返回一个新的流 ,且这个新的流能够与其他的流进行合并

我们看下flatMap方法的声明

<R> Stream<R> flatMap(Function<? super T, ? extends Stream<? extends R>> mapper);

我们可以清楚的看到,flatMap接受一个流,并返回一个新流

总结一下,flatMap 是获取了对象中集合类属性,组成一个新的流供我们使用。可以用在Map类数据的去重场景。

比如:key=商户 value=商户分类,我们要统计所有商户的类别,就可以将商户分类扁平化为一个流,进行distinct操作。

peek: 对流中元素进行遍历操作,与forEach类似,但不会销毁流元素

@Test
public void peekTest() {
    list.stream()
            // 对一个流中的所有元素进行处理,但与forEach不同之处在于,peek是一个中间操作,
            // 操作完成还能被后续使用。forEach是一个终端操作,处理完之后流就不能被操作了。
            .peek(sku -> System.out.println(sku.getSkuName()))
            // 打印终端操作结果
            .forEach(item -> System.out.println(JSON.toJSONString(item, true)));
}

这个操作的目的为:迭代商品列表,逐个输出商品名,并最终通过forEach迭代一次。

运行结果:

无人机
{"skuCategory":"ELECTRONICS","skuId":2,"skuName":"无人机","skuPrice":1000.0,"totalNum":10,"totalPrice":1000.0}
VR一体机
{"skuCategory":"ELECTRONICS","skuId":1,"skuName":"VR一体机","skuPrice":2100.0,"totalNum":10,"totalPrice":2100.0}
牛仔裤
{"skuCategory":"CLOTHING","skuId":4,"skuName":"牛仔裤","skuPrice":60.0,"totalNum":10,"totalPrice":60.0}
衬衫
{"skuCategory":"CLOTHING","skuId":13,"skuName":"衬衫","skuPrice":120.0,"totalNum":10,"totalPrice":120.0}
Java编程思想
{"skuCategory":"BOOKS","skuId":121,"skuName":"Java编程思想","skuPrice":100.0,"totalNum":10,"totalPrice":100.0}
程序化广告
{"skuCategory":"BOOKS","skuId":3,"skuName":"程序化广告","skuPrice":80.0,"totalNum":10,"totalPrice":80.0}

看输出效果,peek与forEach是交替执行的,并不是先执行完peek之后才执行forEach

可以看到,流是惰性执行的 先执行中间操作,再执行终端操作,交替打印(peek是无状态的)

sort:对流中元素进行排序,可选择自然排序或指定排序规则

@Test
public void sortTest() {
    list.stream()
            .peek(sku -> System.out.println(sku.getSkuName()))
            // 根据Sku的价格进行升序排列
            .sorted(Comparator.comparing(Sku::getTotalPrice))
            // 打印终端操作结果
            .forEach(item -> System.out.println(JSON.toJSONString(item, true)));
}

运行结果:

无人机
VR一体机
牛仔裤
衬衫
Java编程思想
程序化广告
{"skuCategory":"CLOTHING","skuId":4,"skuName":"牛仔裤","skuPrice":60.0,"totalNum":10,"totalPrice":60.0}
{"skuCategory":"BOOKS","skuId":3,"skuName":"程序化广告","skuPrice":80.0,"totalNum":10,"totalPrice":80.0}
{"skuCategory":"BOOKS","skuId":121,"skuName":"Java编程思想","skuPrice":100.0,"totalNum":10,"totalPrice":100.0}
{"skuCategory":"CLOTHING","skuId":13,"skuName":"衬衫","skuPrice":120.0,"totalNum":10,"totalPrice":120.0}
{"skuCategory":"ELECTRONICS","skuId":2,"skuName":"无人机","skuPrice":1000.0,"totalNum":10,"totalPrice":1000.0}
{"skuCategory":"ELECTRONICS","skuId":1,"skuName":"VR一体机","skuPrice":2100.0,"totalNum":10,"totalPrice":2100.0}

和上述案例对比,这个案例在于增加了sorted排序操作。

执行完成,可以看到,peek与forEach并不是交替打印的,这是因为增加了sorted操作(sorted是有状态的)之后,必须要sort完成,才进行后续操作

补充:看一个较为复杂的排序案例

public void sortTrade() {
    List<Sku> collect = list.stream()
            .sorted(// 逆序
                    Comparator
                            // 排序条件1:总价逆序
                            .comparing(Sku::getTotalPrice,
                                    Comparator.reverseOrder())
                            // 排序条件2:skuId升序(自然排序)
                            .thenComparing(Sku::getSkuId)
                            // 排序条件3:商品单价逆序
                            .thenComparing(Sku::getSkuPrice, Comparator.reverseOrder())
                            // 自定义排序条件:根据类别 
                            .thenComparing(
                                    // 排序字段值
                                    Sku::getSkuCategory,
                                    // 排序规则
                                    (type1, type2) -> {
                                        if (SkuCategoryEnum.BOOKS.equals(type1)) {
                                            // 标识type1在先,type2在后
                                            return -1;
                                        }
                                        if (SkuCategoryEnum.CLOTHING.equals(type2)) {
                                            // 标识type1在后,type2在先
                                            return 1;
                                        }
                                        // 两者相等
                                        return 0;
                                    })).collect(Collectors.toList());

    ;
}

我们分析一下这段代码,可以看到它是根据一定的排序规则,链式地进行编码。如果掌握了Stream编程,那么这段代码写起来会很顺手,读起来也赏心悦目。

但是另一方面,这点代码由于采用了链式编程方式,每个操作都依赖之前操作的结果,对于不了解Stream编程的同学讲就比较痛苦了。

我的建议是,stream编程我们要学,对于复杂的流程如果要采用stream实现,那么请把注释写清楚,方便后续维护。

毕竟Stream被人诟病较多的地方就是:复杂逻辑下不好理解,不易测试。

我个人的建议是,注释在精不在多,关键逻辑,复杂逻辑要有注释,对自己对别人都有好处。

distinct:对流元素进行去重,有状态操作(针对所有元素进行排序)

@Test
public void distinctTest() {
    list.stream()
            .map(sku -> sku.getSkuCategory())
            .distinct()
            // 打印终端操作结果
            .forEach(item -> System.out.println(JSON.toJSONString(item, true)));
}

运行结果:

"ELECTRONICS"
"CLOTHING"
"BOOKS"

distinct()操作的目的为对流元素进行去重操作。原集合共6个元素,通过distinct()操作将重复的类别进行去除,最终保留不重复的类别结果。

skip:跳过前N条记录,有状态操作

@Test
public void skipTest() {
    list.stream()
            .sorted(Comparator.comparing(Sku::getTotalPrice))
            // 对价格排序之后跳过前三条
            .skip(3)
            // 打印终端操作结果
            .forEach(item -> System.out.println(JSON.toJSONString(item, true)));
}

这段代码的意图为:对商品的价格进行排序,跳过前三个(从小到大排序,跳过最小的三个)

运行结果:

{"skuCategory":"CLOTHING","skuId":13,"skuName":"衬衫","skuPrice":120.0,"totalNum":10,"totalPrice":120.0}
{"skuCategory":"ELECTRONICS","skuId":2,"skuName":"无人机","skuPrice":1000.0,"totalNum":10,"totalPrice":1000.0}
{"skuCategory":"ELECTRONICS","skuId":1,"skuName":"VR一体机","skuPrice":2100.0,"totalNum":10,"totalPrice":2100.0}

limit:截断前N条记录,有状态操作

@Test
public void limitTests() {
    list.stream()
            .sorted(Comparator.comparing(Sku::getTotalPrice))
            // 对价格排序之后取前三条
            .limit(3)
            // 打印终端操作结果
            .forEach(item -> System.out.println(JSON.toJSONString(item, true)));
}

这段代码的意图与skip刚好相反:对商品价格进行排序,取前三个(从小到大,取top3)

运行结果:

{"skuCategory":"CLOTHING","skuId":4,"skuName":"牛仔裤","skuPrice":60.0,"totalNum":10,"totalPrice":60.0}
{"skuCategory":"BOOKS","skuId":3,"skuName":"程序化广告","skuPrice":80.0,"totalNum":10,"totalPrice":80.0}
{"skuCategory":"BOOKS","skuId":121,"skuName":"Java编程思想","skuPrice":100.0,"totalNum":10,"totalPrice":100.0}

结果和上面的运行结果刚好互为补集。由此我们可以猜测是否可以通过skip + limit实现内存分页?

答案是肯定的,具体的方法为

通过skip 和 limit 实现一个假分页如:3条一页

第一页 skip(0 * 3).limit(3)
第二页 skip(1 * 3).limit(3)
...
第N页 skip((n - 1) * 3).limit(3)

总结为公式就是:

skip((pageNum - 1) * pageSize).limit(pageSize)

版权声明:

原创不易,洗文可耻。除非注明,本博文章均为原创,转载请以链接形式标明本文地址。

原文  http://wuwenliang.net/2020/05/17/Java8函数式编程之Stream编程详解-1-中间操作/
正文到此结束
Loading...