# 阿里巴巴Java开发规范——编程规约(3)
编程规约
(四) OOP规约
1.【强制】构造方法里面禁止加入任何业务逻辑,如果有初始化逻辑,请放在 init 方法中
这条编程规范的目的是为了保持代码的清晰性、可读性和维护性。具体解释如下:
-
清晰性:构造函数的主要职责是初始化对象的基本属性,使其达到可用状态。如果在构造函数中混入复杂的业务逻辑,会使构造过程变得模糊不清,阅读者需要花费更多精力去理解这个构造函数究竟做了什么。将业务逻辑移至单独的
init
方法中,可以使构造过程专注于对象的初始化,业务逻辑则集中在一个明确命名的方法中,提高了代码的清晰度。 -
可读性:构造函数通常在创建对象时被调用,其内部逻辑应当一目了然。如果包含业务逻辑,尤其是较为复杂的逻辑,会增加代码阅读难度,影响开发人员对类功能和行为的理解。将业务逻辑封装在
init
方法中,并通过方法名清晰地表明其作用,有助于提高代码的可读性。 -
维护性:当业务需求发生变化时,如果业务逻辑嵌套在构造函数中,修改或扩展这些逻辑可能会牵涉到构造函数本身,增加修改风险。将业务逻辑分离到独立的
init
方法,使得改动更加局部化,降低了对构造过程的影响,提升了代码的维护性。同时,这样的设计也便于进行单元测试,可以针对init
方法单独编写测试用例,确保业务逻辑的正确性。 -
灵活性:有时候,对象可能需要在不同的场景下进行不同程度的初始化。如果业务逻辑在构造函数中,那么每次创建对象时都会执行这些逻辑,无法根据实际需求进行灵活调整。而将业务逻辑放在
init
方法中,可以根据需要选择是否调用、何时调用以及如何调用该方法,增加了代码的灵活性。
综上所述,遵循“构造方法里面禁止加入任何业务逻辑,如果有初始化逻辑,请放在 init
方法中”这一规范,有利于提升代码的清晰性、可读性、维护性和灵活性。在实际编程中,应尽量保持构造函数简洁明了,仅用于设置对象的基本属性,将复杂的业务逻辑封装在专门的方法(如 init
)中处理。
2. 【强制】POJO 类必须写 toString 方法。使用 IDE 中的工具:source> generate toString时,如果继承了另一个 POJO 类,注意在前面加一下 super.toString。
说明:在方法执行抛出异常时,可以直接调用 POJO 的toString()方法打印其属性值,便于排查问题。
3. 【推荐】循环体内,字符串的连接方式,使用 StringBuilder 的 append 方法进行扩展。
说明: 下例中,反编译出的字节码文件显示每次循环都会 new 出一个 StringBuilder 对象,
然后进行append 操作,最后通过 toString 方法返回String 对象,造成内存资源浪费。
反例:
String str = "start";
for (int i = 0; i < 100; i++) {
str = str + "hello";
}
(五) 集合处理
1. 【强制】关于hashCode 和equals 的处理,遵循如下规则:
1) 只要重写 equals,就必须重写hashCode。
2) 因为 Set 存储的是不重复的对象,依据 hashCode 和equals 进行判断,所以 Set 存储的
对象必须重写这两个方法。
3) 如果自定义对象作为Map 的键,那么必须重写 hashCode 和equals。
说明:String 重写了hashCode 和equals 方法,所以我们可以非常愉快地使用 String 对象
作为key 来使用。
在 Java 中,如果一个类重写了 equals()
方法,那么通常也应该同时重写 hashCode()
方法。原因在于,这两个方法在 Java 的集合框架(如 HashMap
、HashSet
等)以及对象相等性的语义中扮演着重要且相互关联的角色。具体解释如下:
-
一致性要求:根据
Object
类中equals()
和hashCode()
方法的约定,对于任何非空对象x
,多次调用x.hashCode()
应该返回相同的哈希码。同时,如果x.equals(y)
为true
,那么x.hashCode()
和y.hashCode()
必须相等。这就是所谓的“相等的对象必须具有相等的哈希码”。如果不重写hashCode()
,默认实现可能无法满足这一要求,导致集合类的行为不符合预期。 -
集合类性能:
HashMap
、HashSet
等基于散列的集合类依赖于hashCode()
方法来快速定位元素。它们首先计算对象的哈希码,然后将其映射到一个桶(bucket)中。如果两个相等的对象具有不同的哈希码,它们会被分配到不同的桶中,导致在查找、添加、删除等操作时效率降低,甚至可能导致无法找到已存在的对象。因此,当重写equals()
改变了对象相等性的判断规则时,也需要相应地重写hashCode()
,确保相等对象的哈希码一致,以维持集合类的高效运作。 -
API 合约:Java 官方文档明确指出,如果一个类覆盖了
equals()
方法,那么它必须也要覆盖hashCode()
方法。这是为了保持 Java 集合框架的一致性和正确性。违反这一约定可能会导致程序在使用集合类时出现难以预料的问题。
示例:
假设有一个 Person
类,包含 name
和 age
属性,并且我们希望两个 Person
对象在 name
相同且 age
相同时被视为相等。此时,我们需要同时重写 equals()
和 hashCode()
方法:
public class Person {private String name;private int age;// 构造函数、getter/setter...@Overridepublic boolean equals(Object obj) {if (this == obj) return true;if (!(obj instanceof Person)) return false;Person other = (Person) obj;return Objects.equals(name, other.name) && age == other.age;}@Overridepublic int hashCode() {return Objects.hash(name, age);}
}
在这个例子中,我们不仅重写了 equals()
方法,使其基于 name
和 age
判断对象相等性,还同时重写了 hashCode()
方法,使用 Objects.hash()
工具方法生成一个基于 name
和 age
的哈希码。这样,当两个 Person
对象相等时,它们的哈希码也必然相等,符合集合类的要求。
总之,只要重写 equals()
方法,就必须同时重写 hashCode()
方法,以确保对象相等性的一致性、维持基于散列的集合类的性能,并遵守 Java API 的约定。
2. 【【强制】不要在foreach 循环里进行元素的remove/add 操作。remove 元素请使用Iterator方式,如果并发操作,需要对 Iterator 对象加锁。
正例:
List<String> list = new ArrayList<>();
list.add("1");
list.add("2");
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
String item = iterator.next();
if (删除元素的条件) {
iterator.remove();
}
}
反例::
for (String item : list) {
if ("1".equals(item)) {
list.remove(item);
}
}
3. 【推荐】集合初始化时,指定集合初始值大小。
说明: HashMap 使用 HashMap(int initialCapacity) 初始化。
正例: initialCapacity = (需要存储的元素个数 / 负载因子) + 1。 注意负载因子 (即loader
factor)默认为0.75,如果暂时无法确定初始值大小,请设置为 16(即默认值)。
反例:HashMap 需要放置1024 个元素,由于没有设置容量初始大小,随着元素不断增加,容
量7 次被迫扩大,resize 需要重建hash 表,严重影响性能。
4. 【推荐】使用entrySet 遍历Map 类集合KV,而不是keySet 方式进行遍历。
说明: keySet 其实是遍历了 2 次,一次是转为 Iterator 对象,另一次是从 hashMap 中取出
key 所对应的value。而entrySet 只是遍历了一次就把 key 和value 都放到了entry 中,效
率更高。如果是JDK8,使用Map.foreach 方法。
正例: values()返回的是 V 值集合,是一个list 集合对象;keySet()返回的是K 值集合,是
一个Set 集合对象;entrySet()返回的是K-V 值组合集合。
5. 【推荐】集合泛型定义时,在JDK7 及以上,使用diamond 语法或全省略。
说明:菱形泛型,即 diamond,直接使用<>来指代前边已经指定的类型。
正例:
// <> diamond 方式
HashMap<String, String> userCache = new HashMap<>(16);
// 全省略方式
ArrayList<User> users = new ArrayList(10);
6. 【推荐】高度注意 Map 类集合K/V 能不能存储 null 值的情况,如下表格:
反例: 由于HashMap 的干扰,很多人认为ConcurrentHashMap 是可以置入 null 值,而事实上,
存储null 值时会抛出NPE 异常。