Java8函数式编程真有这么神奇?- 案例展示lambda表达式对编码效率提升到底有多大

共 7605字,需浏览 16分钟

 ·

2022-06-08 07:31


这是一个新的系列,主要讲Java8的lambda编程以及Stream流式编程相关的用法和案例。

这个系列脱胎于一个内部的分享,由于篇幅较长,内容较多,因此拆分成多篇文章进行发布,方便自己后续参考,也希望能够帮到读者朋友。

本文是Java8函数式编程系列的第一篇,我们一起学习一下Java8函数式编程的基本概念及操作。

1.概述Lambda表达式

Lambda 表达式,也可称为闭包,它是推动 Java 8 发布的最重要新特性。

Lambda 允许把函数作为一个方法的参数(函数作为参数传递进方法中)。

使用 Lambda 表达式可以使代码变的更加简洁紧凑。

1.1 lambda表达式语法

    (parameters) -> expression
    或
    (parameters) -> {statement};
  1. 我们可以将lambda表达式理解为一种代替原先匿名函数的新的编程方式

  2. 通过使用lambda表达式替换匿名函数的形式,将lambda表达式作为方法参数,实现判断逻辑参数化传递的目的。

1.2 lambda表达式形式

无参数

    () -> System.out.println("code");

只有一个参数

    name -> System.out.println("hello:" + name + "!");

没有参数,且逻辑复杂,需要通过大括号将多个语句括起来

    () -> {    
        System.out.println("hello");    
        System.out.println("lambda");
    }

包含两个参数的方法

    BinaryOperator functionAdd = (x, y) -> x + y;
    Long result = functionAdd.apply(1L, 2L);

包含两个参数且对参数显式声明

    BinaryOperator functionAdd = (Long x, Long y) -> x + y;
    Long result = functionAdd.apply(1L, 2L);

1.3 函数式接口

定义:

一个接口有且只有一个抽象方法;

函数式接口的实例可以通过 lambda 表达式、方法引用或者构造方法引用来创建;

「注意」

如果一个接口只有一个抽象方法,那么该接口就是一个函数式接口

如果我们在某个接口上声明了 「@FunctionalInterface」 注解,那么编译器就会按照函数式接口的定义来要求该接口

@FunctionInterface是Java8函数式接口注解,属于声明式注解,帮助编译器校验被标注的接口是否符合函数式接口定义

1.4 方法引用

我们通过Lambda表达式来实现匿名方法。

有些情况下,使用Lambda表达式仅仅是调用一些已经存在的方法;除了调用动作外,没有其他任何多余的动作,在这种情况下我们倾向于通过方法名来调用它,而Lambda表达式可以帮助我们实现这一要求。它使得Lambda在调用那些已经拥有方法名的方法的代码更简洁、更容易理解。

方法引用可以理解为Lambda表达式的另外一种表现形式。

方法引用是调用特定方法的lambda表达式的一种快捷写法,可以让你重复使用现有的方法定义,并像lambda表达式一样传递他们。

注意:

使用方法引用时,只写方法名,不写括号

1.4.1 方法引用格式:

    格式: 目标引用    双冒号分隔符   方法名

    eg:  String       ::        valueOf

1.4.2 方法引用分类:

1.指向静态方法的方法引用:当Lambda表达式的结构体是一个对象,并且调用其静态方法时,使用如下方式

    表达式:
        (args) -> ClassName::staticMethod(args);
    格式:    ClassName :: staticMethodName
    eg:     Integer   :: valueOf

2.指向任意类型实例方法的方法引用:当直接调用对象的实例方法,则使用如下方式进行调用

    表达式:
            (args) -> args.instanceMethod();
    格式:  ClassName::instanceMethod;
    eg:   String::indexOf  
                String::toString

3.指向现有对象的实例方法的方法引用:通过对象实例,方法引用实例方法

    表达式:
        (args) -> object.instanceMethod(args);
        改写为
        (args) ->  object::instanceMethod;
    eg:
        StringBuilder sb = new StringBuilder();
        Consumer consumer = (String str) -> stringBuilder.append(str);

就可以改写为

    Consumer consumer = (String str) -> stringBuilder::append;

2.从一个案例入手

我们先看一个例子,宏观感受一下Java8 Lambda编程带来的便利(后续讲解Stream同样使用该案例)

2.1 案例:直观体验Java8Stream操作

Sku实体类:  标识一个电商下单商品信息对象

    public class Sku {
        // 商品编号
        private Integer skuId;
        // 商品名称
        private String skuName;
        // 单价
        private Double skuPrice;
        // 购买个数
        private Integer totalNum;
        // 总价
        private Double totalPrice;
        // 商品类型
        private Enum skuCategory;

        public Sku() {
        }

        public Sku(Integer skuId, String skuName, Double skuPrice, Integer totalNum, Double totalPrice, Enum skuCategory) {
            this.skuId = skuId;
            this.skuName = skuName;
            this.skuPrice = skuPrice;
            this.totalNum = totalNum;
            this.totalPrice = totalPrice;
            this.skuCategory = skuCategory;
        }
            ...省略getter  setter...
    }

SkuCategoryEnum枚举类: 商品类型枚举

    public enum SkuCategoryEnum {
        CLOTHING(10, "服务类"),
        ELECTRONICS(20, "数码产品类"),
        SPORTS(30, "运动类"),
        BOOKS(40, "图书类")
        ;

        // 商品类型编号
        private Integer code;
        // 商品名称
        private String name;

        SkuCategoryEnum(Integer code, String name) {
            this.code = code;
            this.name = name;
        }
        ...省略getter...
    }

CartService类: 初始化一批数据,模拟购物车

    public class CartService {
        // 初始化购物车
        private static List cartSkuList = new ArrayList<>();

        static {
            cartSkuList.add(new Sku(2, "无人机", 1000.00, 10, 1000.00, SkuCategoryEnum.ELECTRONICS));
            cartSkuList.add(new Sku(1, "VR一体机", 2100.00, 10, 2100.00, SkuCategoryEnum.ELECTRONICS));
            cartSkuList.add(new Sku(4, "牛仔裤", 60.00, 10, 60.00, SkuCategoryEnum.CLOTHING));
            cartSkuList.add(new Sku(13, "衬衫", 120.00, 10, 120.00, SkuCategoryEnum.CLOTHING));
            cartSkuList.add(new Sku(121, "Java编程思想", 100.00, 10, 100.00, SkuCategoryEnum.BOOKS));
            cartSkuList.add(new Sku(3, "程序化广告", 80.00, 10, 80.00, SkuCategoryEnum.BOOKS));
        }

        public static List getCartSkuList() {
            return cartSkuList;
        }
    }

我们直接看这个案例

    private static List cartSkuList = CartService.getCartSkuList();

    @Test
    public void show() {
        List collect = cartSkuList.stream()
                // 方法引用
                .map(Sku::getSkuId)
                .distinct()
                .sorted()
                .collect(Collectors.toList());
        collect.stream().forEach(skuId -> {
            System.out.println(skuId.toString());
        });
    }

简单解释下这段代码的意图:

首先获取购物车中商品列表,将该列表转换为流;收集商品列表中的所有商品编号(skuId),对商品编号进行去重,并进行自然排序(升序排列),最后收集为一个商品编号集合,并对该集合进行遍历打印。

我并没有加注释,但是相信聪明的你也一定能读懂上面这段代码,这正是Stream编程的特点:方法名见名知意,流式编程方式符合人类思考逻辑

运行该用例,打印如下:

    1
    2
    3
    4
    13
    121

打印结果符合我们的预期意图。

想象一下,如果不使用lambda+Stream方式,而是使用java7及之前的传统集合操作,实现上述操作我们的代码量有多少?保守估计至少是上述代码段的1.5倍。

这个案例可能还不具备说服力,接下来的文章中,我将通过一个对比案例来比较lambda编程与传统方式对集合操作的效率提升。

那么,使用了lambda函数式编程之后,对我们的开发真有显著的提升么?

在接下来的章节中,我们通过一个实战案例对比原始集合操作与Stream集合操作具体有哪些不同,直观地展示Stream集合操作对编程效率的提升。

案例:对比原始集合操作与Stream集合操作

需求场景:

针对上面的购物车,我们想要

  1. 全局查看购物车中都有哪些商品
  2. 将购物车中的图书类商品进行过滤(删除图书类商品)
  3. 在其余商品中挑选两件最贵的
  4. 打印出上述两件商品的名称和总价

原始集合操作:

    @Test
    public void traditionalWay() {
        // 1. 打印所有商品
        List skus = CartService.getCartSkuList();
        for (Sku sku : skus) {
            System.out.println(JSON.toJSONString(sku, true));
        }
        // 2. 过滤图书类商品
        List notIncludeBooksList = new ArrayList<>();
        for (Sku sku : skus) {
            if (!sku.getSkuCategory().equals(SkuCategoryEnum.BOOKS)) {
                notIncludeBooksList.add(sku);
            }
        }
        // 3. 其余商品中挑选两件最贵的 价格倒排序,取top2
        // 3.1 先排序
        notIncludeBooksList.sort(new Comparator() {
            @Override
            public int compare(Sku sku0, Sku sku1) {
                if (sku0.getTotalPrice() > sku1.getTotalPrice()) {
                    return -1;
                }
                if (sku0.getTotalPrice() < sku1.getTotalPrice()) {
                    return 1;
                }
                return 0;
            }
        });

        // 3.2 取top2
        List top2SkuList = new ArrayList<>();
        for (int i = 0; i < 2; i++) {
            top2SkuList.add(notIncludeBooksList.get(i));
        }
        // 4. 打印出上述两件商品的名称和总价
        // 4.1 求两件商品总价
        double totalMoney = 0.0;
        for (Sku sku : top2SkuList) {
            totalMoney += sku.getTotalPrice();
        }
        // 4.2 获取两件商品名称
        List resultSkuNameList = new ArrayList<>();
        for (Sku sku : top2SkuList) {
            resultSkuNameList.add(sku.getSkuName());
        }
        // 打印输出结果
        System.out.println("结果商品名称: " + JSON.toJSONString(resultSkuNameList, true));
        System.out.println("商品总价:" + totalMoney);
    }

运行结果:

    {"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}
    {"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}
    {"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}
    结果商品名称: [
        "VR一体机",
        "无人机"
    ]
    商品总价:3100.0

我们可以看到传统的集合操作还是写了比较多的代码,而且在编码过程中为了满足各种要求,我们通过声明新的容器来接受过程中的操作结果,这带来了内存使用量的增加。

接下来看一下Stream方式下如何编码实现我们的需求:

Stream集合操作:

    @Test
    public void streamWay() {
        AtomicReference money = new AtomicReference<>(Double.valueOf(0.0));
        List resultSkuNameList = CartService.getCartSkuList()
            // 获取集合流
                .stream()
                /**1. 打印商品信息*/
                .peek(sku -> System.out.println(JSON.toJSONString(sku)))
                /**2. 过滤掉所有的图书类商品*/
                .filter(sku -> !SkuCategoryEnum.BOOKS.equals(sku.getSkuCategory()))
                /**3. 价格进行排序,默认是从小到大,调用reversed进行翻转排序即从大到小*/
                .sorted(Comparator.comparing(Sku::getTotalPrice).reversed())
                /**4. 取top2*/
                .limit(2)
                /**累加金额*/
                .peek(sku -> money.set(money.get() + sku.getTotalPrice()))
                /**获取商品名称*/
                .map(sku -> sku.getSkuName())
                .collect(Collectors.toList());
        System.out.println("商品总价:" + money.get());
        System.out.println("商品名列表:" + JSON.toJSONString(resultSkuNameList));
    }

运行结果:

    {"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}
    {"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}
    {"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}
    商品总价:3100.0
    商品名列表:["VR一体机","无人机"]

我们可以看到,通过Stream集合操作,运行结果与传统集合操作完全一致。但是编码量却能够显著减少。

「辩证的分析一下」,如果对Stream操作没有一个较为明确的了解,阅读这段代码确实有些难度,但是只要有一点了解,Stream集合操作代码带来的无论是编码量显著降低还是可读性提升,亦或是内存空间的节约都是可观的。

阶段小结

可见,学习并运用Lambda及Stream编程,对于提升我们的编码效率以及提升代码可读性都有着明显的收益。

参考资料

「《告别996,开启Java高效编程之门》慕课网」



浏览 43
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报