掌握8条方法设计规则,设计优雅健壮的Java方法

简介: 掌握8条方法设计规则,设计优雅健壮的Java方法

掌握8条方法设计规则,设计优雅健壮的Java方法

一个良好的方法设计可以提高代码的可读性、可维护性和可扩展性,而糟糕的方法设计则可能导致代码难以理解和修改

本文基于 Effective Java 方法章节总结8条设计方法的规则,帮助开发者更好进行方法设计

image.png

检查参数的有效性

为了防止错误发生,方法中一般会对参数进行校验,比如ArrayList的构造和添加方法

传入容量为负数会抛出非法参数异常IllegalArgumentException

public ArrayList(int initialCapacity) {
    if (initialCapacity > 0) {
        this.elementData = new Object[initialCapacity];
    } else if (initialCapacity == 0) {
        this.elementData = EMPTY_ELEMENTDATA;
    } else {
        throw new IllegalArgumentException("Illegal Capacity: "+
                                           initialCapacity);
    }
}

还有其添加时会检查传入下标是否有效

private void rangeCheckForAdd(int index) {
    if (index > size || index < 0)
        throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}

为了增加方法的健壮性,必须在方法中对参数进行检查,同时也可以在文档中说明哪样的参数是有效的

如果不检查参数是否有效,可能在运行时抛出异常,也可能计算出错误结果导致排查时间久

必要时进行保护性拷贝

方法入参、响应的对象是可变对象时,如果方法中依赖这些对象,但在其他地方又对对象进行修改,那么可能会导致方法中计算错误

比如一个记录时间周期的类,使用可变对象Date记录起止时间

//可变周期
public final class Period {
    //起始时间使用可变对象Date
    private final Date start;
    private final Date end;
    
    //直接依赖可变对象
    public Period(Date start, Date end) {
        if (start.compareTo(end) > 0)
            throw new IllegalArgumentException(
                    start + " after " + end);
        this.start = start;
        this.end = end;
    }
    
    //获取起始时间(引用逃逸、不安全)
    public Date start() {
        return start;
    }
    public Date end() {
        return end;
    }
    
    public String toString() {
        return start + " - " + end;
    }
}

构造方法则是直接依赖可变对象Date,并且start、end方法也是直接返回字段引用(引用逃逸)

当外界通过修改入参 或者 通过方法获取到逃逸对象进行改变时,就会导致错误的结果

        Date start = new Date();
        Date end = new Date();
        Period p = new Period(start, end);
        //修改入参
        start.setYear(76);
        //引用逃逸 被修改
        p.end().setYear(78);
        //Thu Apr 01 11:10:31 CST 1976 - Sat Apr 01 11:10:31 CST 1978
        System.out.println(p);

为了不发生这样错误的情况,可以将依赖的对象改变为不可变对象,也就是将Date替换为不可变对象如LocalDateTime

如果依赖的对象必须是不可变对象时,就要使用保护性拷贝

在入参依赖、方法返回时使用拷贝,防止外界对可变对象进行修改

public class CopyPeriod {
    private final Date start;
    private final Date end;
    //传入参数时进行拷贝
    public CopyPeriod(Date start, Date end) {
        this.start = new Date(start.getTime());
        this.end = new Date(end.getTime());
        if (this.start.compareTo(this.end) > 0)
            throw new IllegalArgumentException(
                    this.start + " after " + this.end);
    }
    //返回字段时进行拷贝
    public Date start() {
        return new Date(start.getTime());
    }
    public Date end() {
        return new Date(end.getTime());
    }

    public String toString() {
        return start + " - " + end;
    }
}

这样在其他地方修改可变对象时也不会对其进行影响

        start = new Date();
        end = new Date();
        CopyPeriod cp = new CopyPeriod(start, end);
        //保护性拷贝 引用不会逃逸 修改无效
        start.setYear(76);
        cp.end().setYear(78);
        //Mon Apr 01 11:10:31 CST 2024 - Mon Apr 01 11:10:31 CST 2024
        System.out.println(cp);

方法返回通过拷贝的方式防止引用逃逸,但是这样可能存在频繁创建对象的性能问题

如果调用方是可信任的(不会修改逃逸的可变对象),那么也可以不进行保护性拷贝(在文档中说明)

谨慎设计方法

1.谨慎选择方法的名称

见名知意

2.不要过于追求提供便利的方法

设计API时方法太多导致不好维护,有必要提供便利的方法可以放在工具类中

3.避免过长的参数列表

太长导致使用不方便

可以通过拆分方法,每个方法使用子集的参数减少参数列表过长

也可以使用类包含所有参数

或者结合前两种情况使用建造者builder

4.定义参数类型为接口而不是类

接口范围更广、通用性更好,比如能定义Map就不要定义HashMap,如果参数为TreeMap是转换为HashMap需要开销

5.对于boolean参数,可以考虑使用两个元素的枚举类型

boolean类型参数往往不够明确,仅仅能表示两种状态,却不能提供清晰的语义含义

通过枚举类型,我们可以更具体地定义和区分这些状态,从而提高代码的可读性和可维护性

假设有一个处理订单的方法,它原本可能这样设计,接受一个布尔参数来决定是否需要立即执行发货操作

immediateShipping 参数的含义不是非常直观,使用者需要查阅文档或者根据上下文理解true和false的具体含义

        public void processOrder(boolean immediateShipping) {
            //立即发货
            if (immediateShipping) {
                shipNow();
            } else {
                //后续再发货
                scheduleForLater();
            }
        }

调用者必须传递一个 ShippingPolicy 枚举实例,这使得意图更加明显,同时也减少了误解的可能性

        public enum ShippingPolicy {
            IMMEDIATE_SHIPPING,
            SCHEDULE_FOR_LATER
        }

        public void processOrder(ShippingPolicy policy) {
            switch (policy) {
                case IMMEDIATE_SHIPPING:
                    shipNow();
                    break;
                case SCHEDULE_FOR_LATER:
                    scheduleForLater();
                    break;
            }
        }

如果未来业务需求扩展,比如增加了新的发货策略,只需在枚举中添加新值即可,无需更改方法签名,体现更好的扩展性

如果不确定/需要扩展可以考虑使用这种方式代替boolean类型

慎用重载

方法的重载是编译(静态)就确定的,而重写(覆写)才是动态运行时确定的

通过类型Set、List、Collection重载三个方法

        public static String classify(Set<?> s) {
            return "Set";
        }

        public static String classify(List<?> lst) {
            return "List";
        }

        public static String classify(Collection<?> c) {
            return "Unknown Collection";
        }

在编译时都申明为Collection,那么在执行时都调用的是classify(Collection<?> c)方法

    Collection<?>[] collections = {
                new HashSet<String>(),
                new ArrayList<BigInteger>(),
                new HashMap<String, String>().values()
        };

        for (Collection<?> c : collections)
            //Unknown Collection
            System.out.println(classify(c));

如果使用重载时发生类型转换从而调用成其他重载方法会导致结果错误并且难以排查

为了避免这种情况发生,最好不使用重载,并使用命名模式描述对应类型

比如ObjectOutputStream中的writeInt(int val)writeChar(int val) writeLong(long val)

如果非要使用重载,那么重载方法中实现需要一样

比如String中的contentEquals(StringBuffer sb)contentEquals(CharSequence cs)

        public boolean contentEquals(StringBuffer sb) {
            return contentEquals((CharSequence)sb);
        }

        public boolean contentEquals(CharSequence cs) {
            //...
        }

慎用可变长参数

使用可变长参数时会将变量初始化为数组,如果能够确定参数数量在某个范围中(1-5),可以使用重载代替

如果无法预估参数数量才使用可变长参数,使用时携带必要的参数和注意性能

实现时要考虑不传可变长参数的情况或传必要参数

        //如果不传参数 手动抛出异常报错
        static int min(int... args) {
             if (args.length == 0)
                 throw new IllegalArgumentException("Too few arguments");
             int min = args[0];
             for (int i = 1; i < args.length; i++)
                 if (args[i] < min)
                     min = args[i];
             return min;
        }


        //优雅处理 有默认值 必要参数 firstArg
        static int min(int firstArg, int... remainingArgs) {
            int min = firstArg;
            for (int arg : remainingArgs)
                if (arg < min)
                    min = arg;
            return min;
        }

返回空容器而不是null

当返回没数据的容器、数组时,应该返回空集合而不是null

使用Collections.emptyList()Collections.emptyMap()高效返回空容器

使用空容器可以与调用方一致,大不了就是没数据不能继续操作,而返回null会导致调用方未判空从而出现空指针异常

当然调用方也可以规范使用空集合判空工具类如CollectionUtils.isNotEmpty()

谨慎使用Optional

Optional作为JDK8中提供处理非空判断的“容器”会存储一个对象

        // 不使用Optional的情况
        public User findUserByUsername(String username) {
            // 假设db.findUser是一个可能返回null的操作
            return db.findUser(username);
        }

        // 调用方在不使用Optional时的处理
        User user = findUserByUsername("nonexistent_user");
        if (user != null) {
            // 处理用户信息
        } else {
            // 用户不存在时的处理逻辑
        }

使用Optional能够考虑处理更多种情况:非空情况、为空情况抛异常或默认值

        // 使用Optional的情况
        public Optional<User> findUserByUsername(String username) {
            return Optional.ofNullable(db.findUser(username));
        }

        // 调用方在使用Optional时的处理
        Optional<User> optionalUser = findUserByUsername("nonexistent_user");
        optionalUser.ifPresent(user -> {
            // 处理用户信息
        });
        //为空抛出异常或用默认值
        User user = optionalUser.orElseThrow(() -> new UserNotFoundException("User not found"));
        User defaultUser = optionalUser.orElse(new GuestUser());
        // 使用Optional的情况
        public Optional<User> findUserByUsername(String username) {
            return Optional.ofNullable(db.findUser(username));
        }

        // 调用方在使用Optional时的处理
        Optional<User> optionalUser = findUserByUsername("nonexistent_user");
        optionalUser.ifPresent(user -> {
            // 处理用户信息
        });
        //为空抛出异常或用默认值
        User user = optionalUser.orElseThrow(() -> new UserNotFoundException("User not found"));
        User defaultUser = optionalUser.orElse(new GuestUser());

如果注重性能、基本包装类型(冗余)、键值时(Map<String, Optional>)则不要使用Optional

为所有导出的API编写文档注释

为重要的API编写文档注释

包括方法作用、入参(@param)、返回(@return)、抛出异常(@throws)

总结

方法中不检查入参会导致运行时异常或错误结果,考虑在方法中检查入参,增加代码健壮性

依赖的可变对象逃逸被修改会导致错误结果,可使用不可变对象或保护性拷贝(入参、响应)解决

设计方法时需要见名知意、避免参数过长、定义参数类型为接口而不是类、boolean类型考虑泛型,并且API中不要追求大量便利的方法,这种方法应该在工具类中

重载编译时就能够确定,为了避免转换类型调用错重载方法,可以使用具体类型命名的方法代替重载,如果一定要使用重载可以让实现一致

无法预估参数长度才使用可变长参数,初始化数组有性能消耗,考虑方法不传可变长参数的情况

返回容器的方法不要返回null而是使用工具类返回空容器,调用时使用容器工具类判空

使用Optional判空可以考虑非空、为空默认值、为空异常等情况,但注重性能、使用基本包装类、键值对与集合的泛型中不要使用

为重要的API编写详细的文档注释

最后(不要白嫖,一键三连求求拉~)

本篇文章被收入专栏 Effective Java,感兴趣的同学可以持续关注喔

本篇文章笔记以及案例被收入 Gitee-CaiCaiJavaGithub-CaiCaiJava,除此之外还有更多Java进阶相关知识,感兴趣的同学可以starred持续关注喔~

有什么问题可以在评论区交流,如果觉得菜菜写的不错,可以点赞、关注、收藏支持一下~

关注菜菜,分享更多技术干货,公众号:菜菜的后端私房菜

相关文章
|
7天前
|
Java API
Java 对象释放与 finalize 方法
关于 Java 对象释放的疑惑解答,以及 finalize 方法的相关知识。
32 17
|
21天前
|
算法 Java Linux
java制作海报二:java使用Graphics2D 在图片上合成另一个照片,并将照片切割成头像,头像切割成圆形方法详解
这篇文章介绍了如何使用Java的Graphics2D类在图片上合成另一个照片,并将照片切割成圆形头像的方法。
34 1
java制作海报二:java使用Graphics2D 在图片上合成另一个照片,并将照片切割成头像,头像切割成圆形方法详解
|
8天前
|
Java 开发者
在Java多线程编程中,创建线程的方法有两种:继承Thread类和实现Runnable接口
【10月更文挑战第20天】在Java多线程编程中,创建线程的方法有两种:继承Thread类和实现Runnable接口。本文揭示了这两种方式的微妙差异和潜在陷阱,帮助你更好地理解和选择适合项目需求的线程创建方式。
11 3
|
10天前
|
Java 大数据 API
别死脑筋,赶紧学起来!Java之Steam() API 常用方法使用,让开发简单起来!
分享Java Stream API的常用方法,让开发更简单。涵盖filter、map、sorted等操作,提高代码效率与可读性。关注公众号,了解更多技术内容。
|
8天前
|
Java 开发者
在Java多线程编程中,选择合适的线程创建方法至关重要
【10月更文挑战第20天】在Java多线程编程中,选择合适的线程创建方法至关重要。本文通过案例分析,探讨了继承Thread类和实现Runnable接口两种方法的优缺点及适用场景,帮助开发者做出明智的选择。
9 2
|
8天前
|
安全 Java
Java多线程通信新解:本文通过生产者-消费者模型案例,深入解析wait()、notify()、notifyAll()方法的实用技巧
【10月更文挑战第20天】Java多线程通信新解:本文通过生产者-消费者模型案例,深入解析wait()、notify()、notifyAll()方法的实用技巧,包括避免在循环外调用wait()、优先使用notifyAll()、确保线程安全及处理InterruptedException等,帮助读者更好地掌握这些方法的应用。
10 1
|
8天前
|
Java 开发者
Java多线程初学者指南:介绍通过继承Thread类与实现Runnable接口两种方式创建线程的方法及其优缺点
【10月更文挑战第20天】Java多线程初学者指南:介绍通过继承Thread类与实现Runnable接口两种方式创建线程的方法及其优缺点,重点解析为何实现Runnable接口更具灵活性、资源共享及易于管理的优势。
19 1
|
8天前
|
Java
在Java多线程编程中,`wait()`和`notify()`方法的相遇如同一场奇妙的邂逅
在Java多线程编程中,`wait()`和`notify()`方法的相遇如同一场奇妙的邂逅。它们用于线程间通信,使线程能够协作完成任务。通过这些方法,生产者和消费者线程可以高效地管理共享资源,确保程序的有序运行。正确使用这些方法需要遵循同步规则,避免虚假唤醒等问题。示例代码展示了如何在生产者-消费者模型中使用`wait()`和`notify()`。
15 1
|
8天前
|
安全 Java 开发者
Java多线程中的`wait()`、`notify()`和`notifyAll()`方法,探讨了它们在实现线程间通信和同步中的关键作用
本文深入解析了Java多线程中的`wait()`、`notify()`和`notifyAll()`方法,探讨了它们在实现线程间通信和同步中的关键作用。通过示例代码展示了如何正确使用这些方法,并分享了最佳实践,帮助开发者避免常见陷阱,提高多线程程序的稳定性和效率。
19 1
|
8天前
|
Java
在Java多线程编程中,`wait()` 和 `notify()/notifyAll()` 方法是线程间通信的核心机制。
在Java多线程编程中,`wait()` 和 `notify()/notifyAll()` 方法是线程间通信的核心机制。它们通过基于锁的方式,使线程在条件不满足时进入休眠状态,并在条件成立时被唤醒,从而有效解决数据一致性和同步问题。本文通过对比其他通信机制,展示了 `wait()` 和 `notify()` 的优势,并通过生产者-消费者模型的示例代码,详细说明了其使用方法和重要性。
15 1