阿里巴巴Java开发规范学习——编程规约(2)

编程规约

(三) 代码格式

1.【强制】单行字符数限制不超过 120 个,超出需要换行,换行时遵循如下原则:

1) 第二行相对第一行缩进 4 个空格,从第三行开始,不再继续缩进,参考示例。
2) 运算符与下文一起换行。
3) 方法调用的点符号与下文一起换行。
4) 方法调用中的多个参数需要换行时,在逗号后进行。
5) 在括号前不要换行,见反例。

正例:

StringBuffer sb = new StringBuffer();
// 超过 120 个字符的情况下,换行缩进 4 个空格,点号和方法名称一起换行
sb.append("zi").append("xin")...
.append("huang")...
.append("huang")...
.append("huang");

反例:

StringBuffer sb = new StringBuffer();
// 超过 120 个字符的情况下,不要在括号前换行
sb.append("zi").append("xin")...append
("huang");
// 参数很多的方法调用可能超过 120 个字符,不要在逗号前换行
method(args1, args2, args3, ...
, argsX)
2. 【推荐】单个方法的总行数不超过 80 行。

说明: 包括方法签名、结束右大括号、方法内代码、注释、空行、回车及任何不可见字符的总
行数不超过80 行。
正例: 代码逻辑分清红花和绿叶,个性和共性,绿叶逻辑单独出来成为额外方法,使主干代码
更加清晰;共性逻辑抽取成为共性方法,便于复用和维护。

(四)OOP 规约

OOP(面向对象编程)规约

1. 【强制】避免通过一个类的对象引用访问此类的静态变量或静态方法,无谓增加编译器解析成本,直接用类名来访问即可。
2. 【强制】所有的覆写方法,必须加@Override 注解。

说明: getObject()与get0bject()的问题。一个是字母的 O,一个是数字的 0,加@Override可以准确判断是否覆盖成功。另外,如果在抽象类中对方法签名进行修改,其实现类会马上编
译报错。

3. 【强制】相同参数类型,相同业务含义,才可以使用 Java 的可变参数,避免使用 Object。

说明: 可变参数必须放置在参数列表的最后。(倡同学们尽量不用可变参数编程)
正例: public List listUsers(String type, Long… ids) {…}

4. 【强制】外部正在调用或者二方库依赖的接口,不允许修改方法签名,避免对接口调用方产生影响。接口过时必须加@Deprecated 注解,并清晰地说明采用的新接口或者新服务是什么。
5. 【强制】Object 的equals 方法容易抛空指针异常,应使用常量或确定有值的对象来调用equals。

正例: “test”.equals(object);
反例: object.equals(“test”);
说明:推荐使用java.util.Objects#equals(JDK7 引入的工具类)

6. 【强制】所有的相同类型的包装类对象之间值的比较,全部使用equals 方法比较。

说明:包装类中,equals()方法被设计为基于它们所封装的基本类型值进行比较,从而提供正确的逻辑结果。

相比之下,如果使用==运算符直接比较两个包装类对象,实际上是在比较它们的引用是否指向内存中的同一位置,而非比较它们所封装的值。

只有在以下两种特定情况下,使用==比较包装类对象会得到与equals()方法相同的值比较结果:

比较的是同一个对象实例:即两个引用指向内存中的同一块区域,此时和equals()都会返回true。
比较的是两个缓存范围内的相同小整数值的包装对象(仅对Integer、Byte、Short、Character、Long五种整数型包装类适用):例如,-128至127之间的Integer对象会被自动装箱为常量池中的单例对象。因此,对于这个范围内的值,使用
比较也会得到正确的结果。但超出此范围的整数值或非整数型包装类对象不适用此规则。
然而,由于上述第二种情况依赖于JVM的具体实现且并不适用于所有包装类,为了代码的通用性、可读性和避免潜在的错误,强烈建议在比较所有相同类型的包装类对象的值时,一律使用equals()方法。这样可以确保无论对象的创建方式如何,都能准确地按照其封装的基本类型值进行比较。

Java中的包装类共有以下八种,它们分别对应于Java的八种基本数据类型:

  1. Byte - 对应于基本类型 byte
  2. Short - 对应于基本类型 short
  3. Integer - 对应于基本类型 int
  4. Long - 对应于基本类型 long
  5. Float - 对应于基本类型 float
  6. Double - 对应于基本类型 double
  7. Boolean - 对应于基本类型 boolean
  8. Character - 对应于基本类型 char

⚠️补充:BigDecimal不是包装类。
BigDecimal类提供了equals()方法来比较两个BigDecimal对象是否相等,但是这种比较方法是严格的,它要求两个对象的值和精度都必须完全相同才会返回true。

对于需要考虑精度而不是严格相等的比较,最好使用compareTo()方法。

7. 关于基本数据类型与包装数据类型的使用标准如下:
  • 1) 【强制】所有的 POJO 类属性必须使用包装数据类型。
  • 2) 【强制】RPC 方法的返回值和参数必须使用包装数据类型。
  • 3) 【推荐】所有的局部变量使用基本数据类型。
    说明:POJO 类属性没有初值是醒使用者在需要使用时,必须自己显式地进行赋值,任何
    NPE 问题 (NullPointerException空指针异常),或者入库检查,都由使用者来保证。
    正例:数据库的查询结果可能是 null,因为自动拆箱,用基本数据类型接收有 NPE 风险。
    反例:比如显示成交总额涨跌情况,即正负 x%,x 为基本数据类型,调用的 RPC 服务,调用
    不成功时,返回的是默认值,页面显示为0%,这是不合理的,应该显示成中划线。所以包装
    数据类型的null 值,能够表示额外的信息,如:远程调用失败,异常退出。

推荐在Java编程中所有的局部变量使用基本数据类型,主要是出于以下几个原因:

  1. 内存效率
  • 基本数据类型(如 int, boolean, double 等)直接在栈内存中存储其实际值,占用的空间较小且固定。相比而言,包装类对象(如 Integer, Boolean, Double 等)不仅需要在栈内存中存储对象引用,还在堆内存中分配空间存储实际值,这导致了额外的内存开销。对于局部变量,特别是在循环、大规模计算等场景下,大量使用包装类可能会显著增加内存消耗。
  1. 性能优化
  • 直接操作基本数据类型通常比操作包装类更快。这是因为基本类型的操作通常涉及简单的CPU指令,而对包装类对象的操作往往涉及到方法调用(如访问器方法、equals()、hashCode()等),这些方法调用会产生额外的开销。尤其是在频繁进行数值计算或比较的代码段中,使用基本数据类型可以减少方法调用和对象交互的延迟,提升程序运行速度。
  1. 避免自动装箱/拆箱
  • Java提供了自动装箱(将基本类型转换为对应的包装类对象)和自动拆箱(将包装类对象转换回基本类型)功能,以方便在需要对象的地方使用基本类型。然而,这种隐式转换并非无成本,它会在运行时产生额外的字节码指令。特别是当装箱/拆箱操作频繁发生时,会引入不必要的性能损耗。直接使用基本数据类型可以避免这些无谓的性能损失。
  1. 简洁性和可读性
  • 使用基本数据类型可以使代码更简洁,减少对包装类对象创建、初始化和管理的复杂性。此外,对于熟悉基本类型的程序员来说,直接看到和操作基本类型更容易理解和维护代码。尤其是在局部变量作用域较小、生命周期较短的情况下,使用基本数据类型有助于保持代码的清晰度。
  1. 避免空指针异常
  • 当使用包装类对象时,如果不小心处理,可能会遇到NullPointerException。例如,如果一个局部变量被初始化为null,后续在未检查其是否为null的情况下直接访问其值,就会抛出异常。而基本数据类型不存在null值,因此避免了这类问题。

综上所述,推荐所有局部变量使用基本数据类型主要是为了提高内存效率、优化性能、避免自动装箱/拆箱带来的开销、保持代码简洁易读以及避免空指针异常。当然,在某些特定场景下,如需要利用包装类提供的特有方法(如Integer.toHexString())、需要放入容器类中(如List<Integer>)或者进行泛型编程时,使用包装类仍然是必要的。但在一般情况下,特别是在局部变量的使用中,优先考虑基本数据类型是一个良好的编程习惯。

8. 【强制】序列化类新增属性时,请不要修改 serialVersionUID 字段,避免反序列失败;如果完全不兼容升级,避免反序列化混乱,那么请修改 serialVersionUID 值。

说明:注意serialVersionUID不一致会抛出序列化运行时异常。

在Java中,serialVersionUID 是一个用来标识类版本的固定不变的 long 类型值,主要用于Java对象的序列化和反序列化过程。它的重要性体现在以下几个方面:

  1. 版本控制
  • 当一个类被序列化时,它的 serialVersionUID 值会被写入到序列化的字节流中。在反序列化时,Java运行环境会检查传入字节流中的 serialVersionUID 是否与当前类定义的 serialVersionUID 相匹配。如果两者不一致,反序列化将会失败,抛出 InvalidClassException
  1. 兼容性管理
  • 如果在不改变 serialVersionUID 的情况下对类进行了修改(如新增、删除或修改属性),只要这些修改不影响已序列化对象的字段布局(即不影响已有字段的顺序、类型和名称),反序列化仍能成功。这是因为Java序列化机制允许在保持字段布局不变的前提下,对类进行向后兼容的修改。

基于以上原理,关于 serialVersionUID 的使用有以下建议:

新增属性时,请不要修改 serialVersionUID 字段

  • 当你对一个已经序列化的类进行扩展,如新增属性,且这些改动不会影响已序列化对象的字段布局时,不应修改 serialVersionUID。保持原有 serialVersionUID 不变,可以让新版本类与旧版本序列化数据兼容,确保反序列化过程顺利进行。

如果完全不兼容升级,避免反序列化混乱,那么请修改 serialVersionUID

  • 当对类的改动较大,如删除或修改已有字段,或者新增字段改变了已有字段的顺序,使得新版本类无法与旧版本序列化数据兼容时,应该修改 serialVersionUID。通过改变 serialVersionUID,可以防止旧版本的序列化数据被误用在新版本类上进行反序列化,从而避免因字段不匹配导致的反序列化混乱和潜在运行时错误。

总结来说,serialVersionUID 是管理类版本和保证序列化兼容性的重要工具。在对序列化类进行修改时,应根据改动的性质和对兼容性的影响,决定是否需要更新 serialVersionUID。新增属性时通常不需要修改,除非这些改动导致了类的不兼容升级。在进行不兼容升级时,主动修改 serialVersionUID 可以防止反序列化错误和潜在的数据混乱。