01-20条Java通用高质量准则

明白事理的人使自己适应世界,不明事理的人想让世界适应自己。 —— 萧伯纳

1. 不要在常量和变量中出现易混淆的字母

包名全小写、类名首字母全大写、常量全部大写并下划线分割、变量采用驼峰命名等,这些是最基本的Java编码规范。

1
2
3
4
5
6
public class TestDemo {
public static void main(String[] args) {
long i = 1l;
System.out.println("i的两倍是:" + (i+i)); //2
}
}

字母 “l” 作为长整型标志时,务必大写!

2. 不要让常量变成变量

1
2
3
4
5
6
7
8
public class TestDemo {
public static void main(String[] args) {
System.out.println("这次常量值是:" + Const.RAND_CONST); // 每次运行值不一样
}
}
interface Const {
public static final int RAND_CONST = new Random().nextInt();
}

务必让常量的值在运行期保持不变。

3. 三元操作符的类型必须一致

1
2
3
4
5
6
7
8
9
public class TestDemo {
public static void main(String[] args) {
int i = 80;
String s1 = String.valueOf(i < 100 ? 90 : 100);
String s2 = String.valueOf(i < 100 ? 90 : 100.0);
System.out.println(s1 + " " + s2); // 90 90.0
System.out.println("两者是否相等:" + s1.equals(s2)); // false
}
}

保证三元操作符中的两个操作数类型一致,即可减少诸多可能错误的发生。

4. 避免带有变长参数的方法重载

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class TestDemo {
public static void main(String[] args) {
Client client = new Client();
// 499元的货物,75折 == 374.25
client.calPrice(49900, 75); // 简单折扣的价格:374.25
}
}
class Client{
public void calPrice(int price, int discount) {
float knockdownPrice = price * discount / 100.0F;
System.out.println("简单折扣的价格:" + formateCurrency(knockdownPrice));
}
public void calPrice(int price, int... discounts) {
float knockdownPrice = price;
for (int i : discounts) {
knockdownPrice = knockdownPrice * i / 100;
}
System.out.println("复杂折扣的价格:" + formateCurrency(knockdownPrice));
}
// 格式化货币形式
private String formateCurrency(float price) {
return NumberFormat.getCurrencyInstance().format(price/100);
}
}

避免带有变长参数的方法重载,以免陷入某些伤脑筋的小陷阱里。

5. 别让 null 值和空值威胁到变长方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class TestDemo {
public static void main(String[] args) {
TestDemo t = new TestDemo();

String[] ss = null;
t.m1("china", ss); // 此处可以编译过
t.m1("aaa", 0);
t.m1("china", "people");
t.m1("china", null); // 此处编译不过,为何?
}

public void m1(String s, Integer... is) {
System.out.println("111");
}

public void m1(String s, String... strs) {
System.out.println("222");
}
}

null值对于可变长参数来说,需要确保让编译器知道其类型,否则会有编译报错。

6. 重写变长方法也循规蹈矩

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class TestDemo {
public static void main(String[] args) {
Base b = new Sub();
b.fun(100, 50);
Sub sub = new Sub();
sub.fun(100, 50); // 报错!!!
}
}
class Base {
void fun(int p, int... d) {
}
}
class Sub extends Base {
void fun(int p, int[] d) {

}
}

重写的方法参数与父类相同,不仅仅是类型、数量,还需要包括显示形式。

7. 警惕自增的陷阱

1
2
3
4
5
6
7
8
9
public class TestDemo {
public static void main(String[] args) {
int count = 0;
for (int i = 0; i < 10; i++) {
count = count++;
}
System.out.println("count=" + count); // 0
}
}

后++的操作,赋值时,会先取值,后运算++。因此避免同时++时赋值的操作。

8. 不要让旧语法困扰你

1
2
3
4
5
6
7
8
9
10
public class TestDemo {
public static void main(String[] args) {
int f = 200;
saveDefault:save(f); // 编译不报错,输出 200
}
static void saveDefault() { }
static void save(int fee) {
System.out.println(fee);
}
}

可读性优先,旧的语法纵然不报错,也得让它随风去吧…

9. 少用静态导入

从 Java5 开始引入了静态导入(import static)目的是为了减少字符输入量,提高代码可读性。

1
2
3
4
5
6
7
import static java.lang.Math.PI;
public class MathUtils {
// 计算圆面积
public static double calCircleArea(double r) {
return PI * r * r;
}
}

滥用静态导入,会使程序更难阅读,更难维护。

10. 不要在本类中覆盖静态导入的变量和方法

不要出现类中的成员变量或方法,与静态导入的相同的情况。

因为如果本类中可以找到对应的变量或方法,就不会到其他包或父类中查找,本类属性、方法优先。

11. 显示声明UID是好习惯

在类实现了 Serializable 接口(序列化标志接口)时,目的是为了可持久化,比如网络传输或者本地存储等。

在类的数据传输或者本地存储时,JVM根据 SerialVersionUID(流标识符)来判断一个类的版本。

1
private static final long serialVersionUID = XXXXXL;

12. 避免用序列化类在狗仔函数中为不变量赋值

序列化:

1
2
3
4
5
6
7
public class Person implements Serializable {
private static final long serialVersionUID = 91282334L;
public final String name;
public Person() {
name = "大天使";
}
}

反序列化:

1
2
3
4
5
public static void main(String[] args) {
// 反序列化
Persons ps = (Persons)SerializaUtils.readObject();
System.out.println(ps.name);
}

JVM中,对于反序列化的操作时,构造函数不会执行。

在序列化类中,不使用构造函数为final变量赋值。

13. 避免为final变量复杂化的赋值

类序列化后保存到磁盘(或网络传输)的对象文件包括两部分:

  1. 类描述信息
  2. 非瞬态(transient关键字)和非静态(static关键字)的实例变量

因此,反序列化时,final变量在以下情况下不会被重新赋值:

  • 通过构造函数为 final 变量赋值
  • 通过方法返回值为 final 变量赋值
  • final 修饰的属性不是基本类型

14. 使用序列化类的私有方法解决部分属性持久化问题

部分属性持久化问题,一般将不需要持久化的属性加上瞬态关键字 transient 关键字即可。

但比如在计算工资,包含基本工资和绩效工资的时候,绩效工资不能泄露到外系统。

需要使用序列化回调:Java调用ObjectOutputStream类把一个对象转换成流数据时,会通过反射检查被序列化的类是否有writeObject方法,并且检查其是否是私有、无返回值的特性。若有,则会委托该方法进行对象序列化;若没有,则由ObjectOutputStream按照默认规则继续序列化。同样,从流中恢复为实例对象时,也会检查是否有一个私有的readObject方法,如果有则会通过该方法读取属性值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public class Person implements Serializable{
private static final long serialVersionUID = 60407L;
private String name;
private transient Salary salary;
public Person(String _name, Salary _salary) {
name = _name;
salary = _salary;
}
// 序列化委托方法
private void writeObject(java.io.ObjectOutputStream out) throws IOException {
out.defaultWriteObject(); // 告知JVM按照规则写入对象,惯例的写法是写在第一句
out.writeInt(salary.getBasePay());
}
// 反序列化时委托方法
private void readObject(java.io.ObjectInputStream in) throws Exception{
in.defaultReadObject(); // 告知JVM按照规则读出对象,惯例的写法是写在第一句
salary = new Salary(in.readInt(), 0);
}
/* out.writeXX 和 in.readXX
* 分别是写入和读出相应的值,类似一个队列,先进先出,如果此处有复杂的数据逻辑,建议按封装Collection对象处理。
*/
}

class Salary implements Serializable {
private static final long serialVersionUID = 60407L;
private int basePay; // 基本工资
private int bonus; // 绩效工资

public Salary(int _basePay, int _bonus) {
this.basePay = _basePay;
this.bonus = _bonus;
}
/* getter/setter*/
}

这样通过两个委托方法,再读出时会按照反序列化委托方法(回调),屏蔽掉了 绩效工资,不使其外泄。

15. break万万不可忘

case语句后手写break,通过集成开发工具进行配置警告级别最佳。

若非特殊需求,case尽可能只处理一种单个情况,特殊需要处理的一定要对 break 慎重再慎重。

16. Java中运行JavaScript中的方法

脚本语言,如PHP、Ruby、Groovy、JavaScript、Python等…

脚本语言三大特性:

  • 灵活。动态类型,变量不用声明直接使用,也可在运行时改变类型
  • 便捷。解释型语言,不需要编译为二进制码或Java字节码,运行期变更代码容易,无需停止应用
  • 简单。部分脚本语言简单,如Groovy

Java6 开始支持正式支持脚本语言。

如在Java中运行JavaScript文件model.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public static void main(String[] args) throws Exception {
// 获得一个JavaScript的执行引擎
ScriptEngine engine = new ScriptEngineManager().getEngineByName("javascript");
// 建立上下文变量
Bindings bind = engine.createBindings();
bind.put("factor", 1);
// 绑定上下文,作用域是当前引擎范围
engine.setBindings(bind, ScriptContext.ENGINE_SCOPE);
Scanner input = new Scanner(System.in);
while (input.hasNext()) {
int first = input.nextInt();
int sec = input.nextInt();
// 执行js代码
engine.eval(new FileReader("C:\\model.js"));
// 是否可调用方法
if (engine instanceof Invocable) {
Invocable in = (Invocable)engine;
// 执行js中的函数,传入输入的参数
Double result = (Double) in.invokeFunction("formula", first, sec);
System.out.println("运算结果" + result);
}
}
}
/*
* C:\model.js 脚本语言函数:
* function formula(var1, var2) {
* return var1 + var2 * factor;
* }
*/

17. 慎用动态编译

Java6 开始就支持动态编译了。

动态编译:在运行期间,直接编译 .java文件,执行 .class,并且能够获得相关的输入输出,甚至还能监听事件。

使用时需要注意:

  1. 在框架中谨慎使用
  2. 不要在要求高性能的项目中使用
  3. 动态编译要考虑安全问题
  4. 记录动态编译过程

18. 避免 instanceof 非预期结果

instanceof 是一个简单的二元操作符,用来判断一个对象是否是一个类实例,返回一个 boolean 结果。

  • “String” instanceof Object 返回 true
  • new String() instanceof String 返回true,一个类的对象当然是它的实例
  • new Object() instanceof String 返回false,可以编译通过,但结果非预期
  • ‘A’ instanceof Character 编译error,因为字符’A’是char类型,即基本类型,不是对象,不能使用instanceof判断
  • null instanceof String 返回false,若instanceof左侧为null,直接返回false,右侧操作值并不关心
  • (String)null instanceof String 返回false,即使强转,左侧还是null,结果永远false
  • new Date() instanceof String 编译error,没有继承或实现关系,直接报错
  • new GenericClass().isDateInstance(“”) 编译通过,返回false,此句等价于 Object instanceof Date

19. assert 断言绝对不是鸡肋

在防御式编程中经常使用到断言(Assertion)对参数和环境做出判断,避免程序因不当的输入或错误的环境而产生逻辑异常。

Java中的断言使用 assert 关键字,基本用法如下:

1
2
assert 布尔表达式
assert 布尔表达式 : 错误信息

assert 的语法简单,有以下两个特性:

① assert 默认是不启用的

② assert 抛出的异常 AssertionError 是继承自 Error 的

可以使用 assert 断言判断的场景:

  1. 在私有方法中放置 assert 作为输入参数的校验
  2. 流程控制中不可能达到的区域
  3. 建立程序探针

eg:流程控制中不可能到达的区域示例

1
2
3
4
5
6
7
public void doSomething() {
int i = 7;
while (i > 7) {
/* 业务处理 */
}
assert false : "到达这里就表示错误";
}

20. 不要只替换一个类

我们经常在系统中定义一个常量接口或常量类,以涵盖系统中所涉及的常量,从而简化代码,方便开发。

发布应用系统时进制使用类文件替换方式,整体WAR包发布才是完全之策。


01-20条Java通用高质量准则
https://janycode.github.io/2016/04/28/02_编程语言/01_Java/05_高质量代码/01-20条Java通用高质量准则/
作者
Jerry(姜源)
发布于
2016年4月28日
许可协议