方法一定要有返回值 \ o /

在编程中,方法或函数的设计是至关重要的。一个好的方法设计不仅能提高代码的可读性和可维护性,还能提升系统的整体性能和可靠性。在这篇博客中,我们将探讨一个看似简单却蕴含深刻思想的原则:"方法要有返回值"。这个原则不仅要求开发者关注方法的签名和返回值,还要求在API设计时考虑方法能返回哪些内容,有时候,即使方法只是执行副作用,也可以有返回值。

前言

在软件开发中,尤其是面对复杂系统时,理解和改变系统的能力是衡量开发者水平的重要标准。最近,我在处理一些历史代码时,发现许多开发者忽略了方法可以有返回值这一事实。这种忽视不仅影响了代码的质量,也限制了系统的可扩展性和可维护性。

为什么方法要有返回值?

受到函数式编程思想的影响,我逐渐意识到纯函数的优势。纯函数只有输入和输出,没有副作用,引用透明。这使得纯函数在使用和复用时更加可靠,因为其正确性在很大程度上已经得到确认。然而,面向对象编程(OOP)和领域驱动设计(DDD)思想常常使开发者过分关注副作用,忽略了方法返回值的重要性。

副作用的存在使得代码复用变得困难,并容易引发意想不到的问题。例如,频繁修改对象的字段值会导致对象存在多种状态,开发者需要维护这些状态。这种情况下,开发者往往会深入到代码逻辑中去关注副作用,而忽略了方法的返回结果。

实际开发中的误用

以下是一些常见的误用例子:

  1. Map#computeIfAbsent:在使用此方法时,返回结果常常被忽略。实际上,其返回值可以用于链式操作,从而提高代码的可读性和性能。

    java 复制代码
    // 普通实现
    Map<Integer, List<User>> usersByState = new HashMap<>();
    usersByState.computeIfAbsent(1, new ArrayList<>());
    usersByState.get(1).add(user1);
    
    // 修改后
    Map<UserState, List<User>> usersByState = new HashMap<>();
    usersByState.computeIfAbsent(UserState.INACTIVE, new ArrayList<>()).add(user1);
  2. Bean Setter:JavaBean的setter方法迫使我们关注对象状态,而忽略了返回值的使用。相比之下,builder模式通过返回this来提醒我们正在创建一个复杂对象。

    java 复制代码
    User user = new User();
    user.setName("abc");
    user.setAge(2);
    user.setAddress("xxx");
  3. 异步任务:异步任务的返回结果应该显式处理,即使不需要结果,也至少需要处理异常。

    java 复制代码
    // bad case
    EXECUTOR.submit(task);
    
    // good case: using guava
    public class TimeoutDemo {
        public static void main(String[] args) {
            ListeningExecutorService pool = MoreExecutors.listeningDecorator(Executors.newCachedThreadPool());
            ScheduledExecutorService timer = Executors.newSingleThreadScheduledExecutor();
            ListenableFuture<Integer> future = pool.submit(() -> {
                try {
                    Thread.sleep(5000);
                    return 1;
                } catch (InterruptedException e) {
                    return -1;
                }
            });
            FluentFuture.from(future)
                .withTimeout(3, TimeUnit.SECONDS, timer)
                .catching(Exception.class, TimeoutDemo::logError, timer);
            System.out.println("result = " + Futures.getUnchecked(future));
            pool.shutdown();
            timer.shutdown();
        }
    
        static <T> T logError(Throwable e) {
            System.out.println(e.getClass());
            return null;
        }
    }
  4. 数据库访问:例如,MyBatis的insert方法返回插入的行数,这一返回值在实际开发中常常被忽略。

  5. 回调机制:不应忽略回调的返回值,例如Kafka发送消息成功后的回调钩子方法。

  6. 深入理解返回值:开发者需要理解返回值的特点,例如是否可变,输入和输出之间的关系等。

    java 复制代码
    // ok case
    Splitter.on(',').split(" foo,,,  bar ,");
    
    // good case
    // 返回值不可变,可复用,可以当成常量使用
    private static final Splitter MY_SPLITTER = Splitter.on(',')
       .trimResults()
       .omitEmptyStrings();
    // use MY_SPLITTER.split(xxx)

有趣的例子

  1. 在API设计中,返回值的设计同样重要。例如,Map#put方法返回之前放入的值,这一设计在多线程情况下可以防止检查-修改问题。
Java 复制代码
// Caffeine 类库实现多线程阶乘计算
AsyncCache<Integer, Integer> cache = Caffeine.newBuilder().buildAsync();

int factorial(int x) {
  var future = new CompletableFuture<Integer>();
  var prior = cache.asMap().putIfAbsent(x, future);
  if (prior != null) {
    return prior.join();
  }
  int result = (x == 1) ? 1 : (x * factorial(x - 1));
  future.complete(result);
  return result;
}
  1. Collection#removeIf方法的返回值为boolean,表示是否有对象从列表中删除。然而,从设计角度来看,返回值最好是int,表示有多少个对象移除了。
java 复制代码
users.removeIf(u -> {
    if (blackList.contains(u)) {
        log.info("user blocked userId={}", u.id());
        return true;
    }
    return false;
});

有时候需要同时获得移除的对象和剩下的对象集合,可以使用stream改进实现如下:

java 复制代码
Map<Boolean, List<User>> parts = users.stream().collect(partitioningBy(blackList::contains));
List<User> blockedUsers = parts.get(true);
List<User> passedUsers = parts.get(false);
  1. Java8 引入了一些函数式编程特性,不过函数式接口感觉做的一般,比如 Runnable 实际上就是 Callable,Consumer 就是 Function<A, Void>,对于Java开发者来说,这些类型之间的转换比较麻烦。这些接口设计不利于开发者从函数的角度考虑问题,以至于很多API的设计需要支持多种实际上等价的方法,在此点名批评 CompleatableFuture 😂。

  2. Java 官方不喜欢Tuple,也没有相关语法糖,导致用户需要手动定义一些临时Result类型。特别是在Stream Collector 中,定义了一些奇怪的结果,summarizingInt 返回统计信息类 IntSummaryStatistics,partitioningBy 返回类型为Map<Boolean, T>。

总结

方法的返回值不仅是一个简单的设计原则,更是提高代码质量和系统可靠性的重要手段。在设计和使用API时,开发者应充分利用返回值,以减少副作用带来的不良影响。通过关注方法的返回值,我们可以更好地理解和管理我们的项目,提高代码的可读性和可维护性。

相关推荐
Java小白程序员15 分钟前
Spring Framework :IoC 容器的原理与实践
java·后端·spring
小小愿望33 分钟前
前端无法获取响应头(如 Content-Disposition)的原因与解决方案
前端·后端
xuTao6671 小时前
Easy Rules 规则引擎详解
java·easy rules
追逐时光者1 小时前
C#/.NET/.NET Core技术前沿周刊 | 第 50 期(2025年8.11-8.17)
后端·.net
m0_480502642 小时前
Rust 入门 KV存储HashMap (十七)
java·开发语言·rust
杨DaB2 小时前
【SpringBoot】Swagger 接口工具
java·spring boot·后端·restful·swagger
YA3332 小时前
java基础(九)sql基础及索引
java·开发语言·sql
why技术2 小时前
也是震惊到我了!家里有密码锁的注意了,这真不是 BUG,是 feature。
后端·面试
小李是个程序3 小时前
登录与登录校验:Web安全核心解析
java·spring·web安全·jwt·cookie