明白事理的人使自己适应世界,不明事理的人想让世界适应自己。
—— 萧伯纳
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)); } }
|
字母 “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); System.out.println("两者是否相等:" + s1.equals(s2)); } }
|
保证三元操作符中的两个操作数类型一致,即可减少诸多可能错误的发生。
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(); client.calPrice(49900, 75); } } 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); } }
|
后++的操作,赋值时,会先取值,后运算++。因此避免同时++时赋值的操作。
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); } 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变量复杂化的赋值
类序列化后保存到磁盘(或网络传输)的对象文件包括两部分:
- 类描述信息
- 非瞬态(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(); out.writeInt(salary.getBasePay()); } private void readObject(java.io.ObjectInputStream in) throws Exception{ in.defaultReadObject(); salary = new Salary(in.readInt(), 0); }
}
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; } }
|
这样通过两个委托方法,再读出时会按照反序列化委托方法(回调),屏蔽掉了 绩效工资,不使其外泄。
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 { 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(); engine.eval(new FileReader("C:\\model.js")); if (engine instanceof Invocable) { Invocable in = (Invocable)engine; Double result = (Double) in.invokeFunction("formula", first, sec); System.out.println("运算结果" + result); } } }
|
17. 慎用动态编译
Java6 开始就支持动态编译了。
动态编译:在运行期间,直接编译 .java文件,执行 .class,并且能够获得相关的输入输出,甚至还能监听事件。
使用时需要注意:
- 在框架中谨慎使用
- 不要在要求高性能的项目中使用
- 动态编译要考虑安全问题
- 记录动态编译过程
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 断言判断的场景:
- 在私有方法中放置 assert 作为输入参数的校验
- 流程控制中不可能达到的区域
- 建立程序探针
eg:流程控制中不可能到达的区域示例
1 2 3 4 5 6 7
| public void doSomething() { int i = 7; while (i > 7) { } assert false : "到达这里就表示错误"; }
|
20. 不要只替换一个类
我们经常在系统中定义一个常量接口或常量类,以涵盖系统中所涉及的常量,从而简化代码,方便开发。
发布应用系统时进制使用类文件替换方式,整体WAR包发布才是完全之策。