序言
优秀的 java 代码不仅是正确,还应该是简洁、可维护、可靠、可测试、高效、可移植的。
目标:风格一致、易阅读、高质量
代码风格
- 【命名】标识符
标识符由不超过64字符的字母、数字、下划线组成
命名风格:驼峰命名, 如newCustomerId, supports Ipv6 ()
》接口、类、注解、枚举类型:大驼峰,测试类加Test后缀,文件名为顶层类名. Java
》属性、局部变量、方法、方法参数:小驼峰
String File Name;
String xmlData;
》 静态变量 、枚举值:全大写, 下划线分割
》 泛型 类型变量: 如E, T, T2, E_IN
》异常:后缀 Exception 或Error, 如AccessException
- 【命名】包
包名的字母应小写,以点号分隔,如:
package com.xxx.service1.v2
- 【命名】类、枚举、接口
名词、名词短语,形容词、形容词短语, 采用大驼峰,如:
class MacroPolo {}
interface HisPromotion {}
- 【命名】方法
》get + 非布尔属性名(), 如:public String getName()
》is/can/should + 布尔 属性名() ,如:public boolean isFinished()
》set + 属性名(),如:public void setVisible(boolean)
》has + 名词/形容词(),如: boolean hasNext()
》动词,如:public void drive()
》动词 + 宾语() , 如:public void addListeners(Listener)
》 CallBack 回调方法: 介词+动词,如 onCreate(), toString ()
- 【命名】常量
》不可修改为其它的值/对象
》对象类型, 对象在初始化完成后其属性不能被修改
应由合大字字母与下划线组成 ,单词间以下划线分隔, 不要使用魔鬼数字。
public static final int MAX_TIME_INTERVAL=360;
public static final String DISCOUNT_RATE_NAME=“half”
public static final int MAX_DESIGN_CAPACITY=3000;
enum SIZE {SMALL, MEDIUM, LARGE};
- 【命名】变量、方法参数
通常是名词或名词短语,采用小驼峰命名。
有集合意义的,采用复数形式。
String customeName;
List<String> users = new ArrayList<>(MAX_DESIGN_CAPACITY);
【反例】boolean isNotError; boolean isNotFound;
布尔型变量以表达是非意义的动词开头, 如: is, has, can , should等,
boolean isGranted;
boolean hasLicense;
boolean canSwim;
boolean shouldAbort = false;
- 【注释】和代码一样重要, 按需注释
Java有3种标记注释的方式:
- // 注释单行内容
- /* */ 注释连续多行内容
- /** */ Javadoc注释,可以使用Javadoc工具生成一个HTML文档
》为每个public, protected 修饰的类、接口、枚举、类方法和属性添加注释, 采用Javadoc注释格式(/** */)
》顶层public类的Javadoc应该包含功能说明和创建日期、版本信息
》方法的Javadoc中包含功能说明 ,根据实际需要按顺序使用@param, @return, @throws标签对参数、返回值、异常进行注释
》不写空有格式的方法头注释
- 【注释】文件头
》文件头注释中应该包含版权许可信息
》不写空有格式的文件头注释
/*
*版权所有 (C) XXXX公司 2001~2022
*/
- 【注释】代码
》注释与代码之间应该有空行或空格, 注释符与注释内容之间应该有空隔
》正式交付给客户的代码不应该包含TODO/FIXME注释
public interface InterfaceExample {
// 类成员变量,与前面的代码之间保留一个空行
String bField = ...;
}
- 【格式】源文件
》源文件编码格式(包括注释)使用UTF-8编码
》一个源文件按顺序包含版权、package、import、顶层类,且用空行分隔
》import包顺序:安卓>本公司>其它商业组织、开源第三方、net/org开源组织>Java
》一个类或接口的声明部分应该按类变量、静态初始化块、实例变量、实例初始化块、构造器、方法的顺序出现,且用空行分隔
- 【格式】大括号
》在if, else, switch, for, do, while等语句中, 即使程序体是空的或只有一条语句,也应该使用 大括号 。对switch的case, default, 大括号可选。
》对于非空块状结构,左大括号应该放在行尾,右大括号另起一行
》应避免空块,必须使用空块时,采用统一的大括号换行风格 , 例如:
void doNoghingElse() { //这样统一可以
}
- 【缩进】
使用空格进行缩进, 每次缩进4个空格;不允许使用tab、换页符等
- 【行内容】每行不超过一条语句
》每行只写一条语句
》行宽不超过120个窄字符, 注:1个宽字符占用2个窄字符的宽度
》较长的package可以不换行
》建议换行起点在操作符之前, 例如:
Student student = Student.builder()
.setName("Tom")
.setAge(19)
.setGrander("F")
.setMajor("软件工程")
.build();
- 【水平空格】
》用空格突出关键字和重要信息,必须加空格的场景:
(包括复合)赋值运算符前后, 如=, *=
逗号 、非for-in的 冒号 、for循环等分隔的;符号之后加空格
二元运算符、类型并交的|和&符号、for-in的冒号的前后两侧,例如 base + offset;
Lambda表达式 中的箭头前后,如 str -> str.length();
方法声明、条件判断语句、循环语句等场景下的)与{之间加空格 , 如 void func() { …}
》不应插入多余空格使代码垂直对齐, 例如:
private int size; //不需要在变量名之前加空格使得与下一行对齐
private String name; //不必与上行对齐注释
- 【枚举】
》枚举常量间以逗号,换行可选。例如:
private enum Encoding {
UTF8 {
@Override
public String toString() {
return " UTF -8";
}
},
UTF16,
US_ASCII
}
》枚举的使用场景:
- 布尔型的两元素值,如: public enum TemperatureScale { CELSIUS, FAHRENHEIT } //表示温度用摄氏|华氏
- 变量值仅在一下固定范围内变化用枚举来定义
- 整数或 字符串 的枚举模式,蕴含有某种命名空间的,如:
public enum ComparisonResult { ORDERED_BY_ASCENDING, ORDERED_BY_SAME, ORDERED_BY_DESCENDING }
- 【switch语句】
》当switch括号内的变量类型为String时,确保变量非空
》case语句块结束时,如果不加break,需要有注释说明(fall-through)
switch (addresslabel) {
case 0:
case 1:
system.out.println("11111");
//$fall-through$
case 2:
system.out.println("22222");
//$fall-through$
case 3:
system.out.println("33333");
break;
default:
system.out.println("default!");
}
- 【注解】
》应用于类、方法、类属性的每个注解独占一行,例如:
@Override
public int hashcode () {
......
}
@Partial
@Mock
DataLoader dataloader;
- 【修饰符】
》类和成员修饰符(如果有),按Java语言建议的顺序显示:
public>protected>private>abstraact>default>static>final>transient>volatile>synchronized>native>strictfp
》对于long/float/double类型的数字使用后缀指定数值的类型,例如:
long sum = 0L;
var isReady = true;
float flt=1.0f;
var dbl = 3.14d;
编程实践
- 【声明和初始化】
》每行只声明一个变量
》 局部变量 被声明在接近它们首次使用的行
boolean is lock ed = lock.tryLock(); //isLocked变量在使用时进行声明,并赋初始值
》禁止C风格的数组声明, 应该如数据元素类型紧跟中括号[]组成 ,例如:
【应该】String[ ] nonEmptyArray = {"GREEN", "RED", "BLUE" };
【不应该】String nonEmptyArray[ ] = {"GREEN", "RED", "BLUE" };
》禁止将mutable对象定义为常量, 使用public static final的意图是定义一个常量,如果用它修饰一个mutable(可变)对象,易造成功能异常。
【反例】public static final List<String> EMPTY_RESULT_LIST = new ArrayList<>(); // 这个List集合是mutable, 不应该定义为常量
- 【数据类型】
》进行数值运算时, 避免整数溢出
【反例】return num1 * num2; //当两个乘数的值较大,乘积大于 Integer .MAX_VALUE时会产生溢出
【正例】return Math.multiplyExact(num1, num2);
》确保除法运算、模运算中的除数不为0
if (divisorNum != 0) {
long result1 = dividendNum / divisorNum ;
}
》禁止使用 浮点数 作为循环计数器,由于浮点数存在精度问题,会导致非预期的结果
【反例】for (float flt = (float) 200000000; flt < 20000000005; flt++) { ... }
》需要精确计算时使用 BigDecimal , 不要使用float和double ,例如:
BigDecimal income = new BigDecimal("1.03");
》字符串,不要在代码中硬编码用于表示换行、文件路径分隔的字符
【正例】String filePath = path + File.separator + "temp.txt"
》字符串大小写转换、数字格式化为西方数字时,必须加上Locale.ROOT或Locale.ENGLISH
【正例】String testString2 = String.format(Locale.ROOT, "%d", 2);
》字符与字节的相互转换操作,要指明正确的编码方式
【正例】String result = new String(buf, StandardCharsets.UTF_8); //可跨平台转换字符
》明确地进行类型转换,避免依赖隐式类型转换
》在引用类型向下转换前,用 instanceof 进行判断
- 【表达式】
》不要在单个表达式中对相同的变量赋值超过一次
》用括号明确表达式的操作顺序, 避免过分依赖默认优先级
》表达式的比较, 遵循左侧倾向于变化, 右侧倾向于不变的原则
【正例】Objects.equals(var1, CONST_COMPANY_NAME)
【正例】if (obj != NULL) { ... }
》代码中不应使用断言assert
- 【控制语句】
》不要在控制性表达式中执行赋值操作,或执行复杂的条件判断
【反例】 if (isFoo = false) { ... } //在条件判断中赋值,不易理解
【例外】 while ((line = reader.readLine()) != null ) { ... }
》含 else if分支的条件判断应在最后加一个else分支
》禁止使用空的无限循环, 如:
while (true) { //do nothing }
- 【方法】
方法是可组合、可重用的代码最小单位,高内聚低耦合的设计,把代码有效组织起来。
》不要使用已标注为@Deprecated的方法、类、类的属性等,因为它是因各种原因被废弃,为了保持兼容性而没有删除。
》不应把方法的参数当成临时变量
》谨慎使用可变数量参数
》对于返回数值或容器的方法, 应返回长度为0的数组或容器,代替返回null
- 【类】
》可使用Lambda表达式或方法引用代替匿名类
》设计类时,要为类及成员设置最小的可访问性
》应避免定义public 且非final的类属性
》不要在父类的 构造方法 中调用可能被子类覆盖的方法
》构造方法如果有多个, 尽量重用
》避免基本类型与期包装类型的同名重载方法
》覆写equals方法时, 要同时覆写hashcode方法
》子类覆写父类方法或实现接口时,必须加上@Override注解
》使用类名调用 静态方法 ,不要使用实例或表达式来调用
- 【接口与面向对象编程】
》接口定义中去掉多余的修饰词
》可在接口中加上静态方法表示相关的工厂或助手方法
- 【异常处理】
》不要通过一个空的catch块忽略异常
》不要直接捕获异常的基类 Throwable, Exception, RuntimeException
》不要直接捕获可通过预检查进行处理的RuntimeException, 如:NullPointerException, IndexOutOfBoundsException等
》方法抛出的异常,应该与本身的抽象层次相对应
【反例】
public class Employer {
...
public TaxId getTaxId() {
...
throw new RuntimeException(); //把更底层的异常返回给了调用方,使调用方法与底层耦合起来。 应该改成抛出EmployerDataNotAvailable异常,抽象层次与方法一致
}
...
}
》在catch块中抛出新异常时, 避免丢失原始异常信息
》一个方法不应该抛出超过5个异常,并在Javadoc的@throws标签中记录每个抛出的异常及条件
》不要使用return, break, continue或抛出异常使得finally块非正常结束
》不要使用System.exit()终止 JVM
- 【并发与 多线程 】
》优先使用Java标准库提供的高级同步机制在多线程中共享数据
》针对 线程 安全性,需要进行文档 Javadoc说明
》可以使用CompletableFuture编写异步任务
》优先使用不可变对象在多线程间传递数据
》避免数据竞争 Data Race
从Java 5 开始,Java Memory Model采用happens-before关系,来规定并发执行中,读写操作允许读到什么值,不允许读到什么值。
两个线程分别对一个非volatile的共享变量进行访问操作,其中至少一个操作是写操作,且这两个操作之间没有happens-before关系,就是Data Race,通常必须避免 Data Race, 正确同步的执行是指没有Data Race的执行。
【正解】在下例中演示使用volatile变量同步。volatile变量的写操作(2)和读操作(3)之间建立了happens-before关系,保证读操作(4)必须看到写操作(1)写入的值42,而不是初始值0。
class Foo {
public volatile int nr; //(0)赋初值0
}
private volatile Foo sharedFoo; //定义volatile共享变量
//-------------------生产者:写共享变量--------------------------------
public void publisher() {
...
Foo newFoo = new Foo();
newFoo.nr = 42; //(1)写操作:值写为42
sharedFoo = newFoo; // (2) volatile变量的写操作, 写入新对象的引用
...
}
//-------------------使用者:读共享变量--------------------------------
public void consumer() {
...
Foo myFoo;
do {
myFoo = sharedFoo; //(3)volatile变量的读操作, 读出共享变量中的值
} while (myFoo == null); // 不等于null 就证明操作(2)happens-before 操作(3)
System.out.println(myFoo.nr); //(4)读操作,(2)happens-before(3),得到返回值为42
...
}
除了volatile变量以外,还可以使用锁同步或者使用 Thread .join()同步,都可以建立happens-before关系。
一般来说,如果使用锁,那么读和写都要加锁,而不是写线程需要加锁,而读的线程可以不加锁。
【反例】在下例中演示了不正确的用锁。只有写线程加了锁,读线程没有加锁,导致写操作(1)和读操作(6)之间没有happens-before关系,造成Data Race,在操作(6)中有可能读到data的初始值0。
int data; //(0)写初值0
boolean flag; // 初值false
Object lock;
//-------------------生产者:写共享变量--------------------------------
void thread1() {
data = 42; //(1)写共享变量的值为42
synchronized (lock) { //(2)加锁
flag = true; //(3) 写flag
} //(4) 解锁
//-------------------使用者:读共享变量--------------------------------
void thread2() {
boolean myFlag;
do {
myFlag = flag; //(5)读锁状态 flag
} while (!myFlag);
int myData = data; //(6)发现解锁,读共享变量的值data
System.out.println(myData); //输出可能不是42
}
》使用相同的顺序请求和释放锁来避免 死锁
》对共享变量做同步访问控制时需避开同步陷阱, 如基于高级并发对象的synchronized块,使用实例锁来同步静态共享变量,使用可被重用的对象锁,使用class类对象锁
》在异常条件下,保证释放已持有的锁
》避免在持有锁时执行耗时或阻塞性的操作
》避免使用不正确形式的双重检查锁
》禁止使用非线程安全的方法来覆写线程安全的方法
》使用新并发工具代替wait() , notify()
》创建新线程时必须指定线程名
》使用Thread对象的setUncaughtExceptionHandler方法注册未捕获异常处理者
》不要依赖线程调度器,线程优先级和 yield ()方法
》线程中断由业务代码来协助完成,慎用Thread.interrupt方法
》禁止使用Thread.stop()来终止线程
》避免不加控制地创建新线程,应该使用 线程池 来管控资源
》线程池中的任务结束后必须清理其自定义的 ThreadLocal 变量