浅析Java中函数式编程、匿名函数和泛型

分享示例说明函数式编程如何提高代码的重用性和可读性。

微信搜索关注《Java学研大本营》

使用函数式编程可以减少代码重复,使代码更易于理解。

Java编程语言以其面向对象的特性而闻名,但也因其冗长和繁琐的异常处理机制而而广受批评。当Java语言在1.8版本引入函数式编程能力时,人们并没有马上理解到这如何给程序员提供帮助。

图片

本文给大家讲解一个示例,以说明函数式编程如何提高代码的重用性和可读性。

1 问题

为了实现功能,第三方编写的通信客户端使用了依赖注入和注释。然而,该客户端存在一些问题,例如抛出已检查异常、缺乏日志记录和重试能力。因此,我们需要对该客户端的功能进行封装,以添加重试功能、日志记录和良好的异常处理。

如果没有函数式编程,那么程序需要创建一个外观,将每个客户端功能都包装在委托中,并添加日志记录、异常处理和通信重试的逻辑。客户端共有50个需要封装的函数,这将导致几乎完全相同的50个副本的代码,仅有调用的客户端函数和传递的数据类型有所不同。这样会带来大量的重复代码问题。

为了解决这个问题,要使用函数式编程的方式对该客户端功能进行封装,以实现重试能力、日志记录和良好的异常处理。

2 使用函数式编程解决问题

解决方案是添加1或2个专门用于处理异常、执行日志记录和实现重试循环的方法。但是,它们需要调用通信客户端中的特定函数。

使用函数式编程,方法可以将函数作为其参数接收。该方法不需要在设计时知道哪个函数。Java(因为这与其他编程语言不同)所要求的是函数具有预期的签名。

在我们的情况下,我们需要2种变体:BiFunction签名和BiConsumer签名。区别在于BiFunction返回一个值,而BiConsumer则不返回。

2.1 示例代码:委托

public final ActionResult<List<Order>>
    downloadOrders() {
    return get(
      ".downloadOrders()",
      (
        client,
        apiKey
      ) -> client.downloadOrders(apiKey.toString())
    );
  }

上面的示例没有显示错误处理、日志记录和重试循环。这是由get(String, BiFunction)方法执行的。因此,我们不需要50个重复的错误处理、日志记录和重试循环,而是有50个易于理解的委托。

get(String, BiFunction)方法回调传入的BiFunction,我们在上面看到了这一点:

(    
  client,
  apiKey
) -> client.downloadOrders(apiKey.toString())

我们可能会从JavaScript中认识到这一点。语法是一个BiFunction的lambda表示法:它接受2个参数,可能做一些事情,并返回一些东西。它是一个回调函数,根据需要即时创建,并且因为它没有名称,所以它保持匿名(并由编译器分配标识)。它的参数client和apiKey在方法get(String, BiFunction)内生成,并在运行时传回回调函数中。它看起来像这样:

2.2 示例代码:接受函数作为参数的包装器

private final <T>
    ActionResult<T>
    get(
      final String caller,
      final BiFunction<
        Client,
        ApiKey,
        T
      > callback
    ) {
    final Client client = Client.newInstance(caller);
    final ApiKey key = this.getKey();
    final MutableList<Exception> errors = Lists.mutable.empty();
    boolean mustRetry = true;
    for (
      int retryCount = 0;
      mustRetry && retryCount < MAX_RETRIES;
      retryCount += 1
    ) {
      try {
        return new ActionResult<T>(
          callback.apply(
            client,
            apiKey
          )
        ).with(errors);
      } catch (Exception ex) {
        mustRetry = mustRetry(ex);
        if (mustRetry) {
          try {
            TimeUnit.SECONDS.wait(1 << (retryCount + 1));
          } catch (InterruptedException ie) {
            errors.add(ie);
          }
        } else {
          errors.add(ex);
        }
      }
    }
    return ActionResult.<T>empty()
      .with(errors);
  }

在那段代码中,BiFunction通过声明进行回调:

callback.apply(
  client,
  apiKey
)

请注意,get(String, BiFunction)不知道回调返回的数据类型。在像Java这样的强类型和显式类型编程语言中,通常不可能。直到泛型引入Java编程语言之前,这是不可能的。这就是为什么代码中存在:它是回调返回的数据类型的占位符。

请注意,调用站点也没有指定返回值的数据类型。相反,它由客户端函数的返回值和委托上指定的返回数据类型隐含。如果它们不匹配,编译器将发出警告,保持数据类型良好且检查过。

3 总结

  • 坏的抽象是面向对象编程中许多问题的根源。我们希望将目前的两个异常处理和重试循环包装器减少到一个包装器,但是由于一个接受BiFunction回调,另一个接受BiConsumer回调,还没有找到合适的方法。因为从概念上讲,它们执行不同的操作:发送和接收。使用相同的包装器可能会破坏这种概念上的区别。总之,对于现在来说值得高兴的是避免了重复使用50个包装器的情况。

  • 当然,有些优秀的编译器可能会检测到代码重复,并采取一些技巧将它们减少为具有不同回调的单个包装器。然而,这并非我们当前关注的重点。我们的目标是消除重复代码并提高代码的可读性。

  • 从理论上讲,使用这个包装器和函数委托可能会导致程序变慢。然而,根据经验,由于与远程通信伙伴进行通信时遇到的网络延迟,潜在的几毫秒延迟相对微不足道。尽管如此,如果在您的环境中这导致了明显的性能下降,请测量吞吐量并进行相应的调整。

  • 静态代码分析工具若能提供有关消除重复代码的建议,将对代码优化非常有益。这样的工具可以帮助开发人员识别和消除重复的代码段,从而提高代码的可读性、可维护性和性能。

推荐书单

《Java从入门到精通(第7版)(软件开发视频大讲堂)》

《Java从入门到精通(第7版)》从初学者角度出发,通过通俗易懂的语言、丰富多彩的实例,详细讲解了使用Java语言进行程序开发需要掌握的知识。全书分为4篇共24章,内容包括初识Java,开发工具(IDEA、Eclipse),Java语言基础,流程控制,数组,类和对象,继承、多态、抽象类与接口,包和内部类,异常处理,字符串,常用类库,集合类,枚举类型与泛型,lambda表达式与流处理,I/O(输入/输出),反射与注解,数据库操作,Swing程序设计,Java绘图,多线程,并发,网络通信,飞机大战游戏,MR人脸识别打卡系统。书中所有知识都结合具体实例进行讲解,涉及的程序代码都给出了详细的注释,这可以帮助读者轻松领会Java程序开发的精髓,并快速提高开发技能。

《Java从入门到精通(第7版)(软件开发视频大讲堂)》icon-default.png?t=N7T8https://item.jd.com/14067396.html

图片

精彩回顾

10条建议,写出简洁专业的Java代码

使用Java和IntelliJ IDEA,介绍Serverless和Azure编程范式

使用IntelliJ IDEA重构Java代码的最佳实践

10个必知必会的Kubernetes快捷方式

10分钟掌握SQL连接:inner、outer、left、right

微信搜索关注《Java学研大本营》

访问【IT今日热榜】,发现每日技术热点

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值