Rule 1. 原子数据类型(int等)与包装类型(Integer等)的使用原则
1.1 【推荐】需要序列化的POJO类属性使用包装数据类型
1.2 【推荐】RPC方法的返回值和参数使用包装数据类型
1.3 【推荐】局部变量尽量使用基本数据类型
包装类型的坏处:
1)Integer 24字节,而原子类型 int 4字节。
2)包装类型每次赋予还需要额外创建对象,除非在缓存区(见Integer.IntegerCache与Long.LongCache),Integer var = ?在缓存区间的赋值,会复用h缓存对象。默认缓存区间为-127到128,受启动参数的影响,如-XX:AutoBoxCacheMax=20000。
3)包装类型还有==比较的陷阱(见规则3)
包装类型的好处:
1)包装类型能表达Null的语义。
比如数据库的查询结果可能是null,如果用基本数据类型有NPE风险。又比如显示成交总额涨跌情况,如果调用的RPC服务不成功时,应该返回null,显示成-%,而不是0%。
2)集合需要包装类型,除非使用数组,或者特殊的原子类型集合。
3)泛型需要包装类型,如Result<Integer>
。
Rule 2.原子数据类型与包装类型的转换原则
2.1【推荐】自动转换(AutoBoxing)有一定成本,调用者与被调用函数间尽量使用同一类型,减少默认转换
//WRONG, sum 类型为Long, i类型为long,每次相加都需要AutoBoxing。
Long sum=0L;
for( long i = 0; i < 10000; i++) {
sum+=i;
}
//RIGHT, 准确使用API返回正确的类型
Integer i = Integer.valueOf(str);
int i = Integer.parseInt(str);
2.2 【推荐】自动拆箱有可能产生NPE,要注意处理
//如果intObject为null,产生NPE
int i = intObject;
Rule 3. 数值equals比较的原则
3.1【强制】 所有包装类对象之间值的比较,全部使用equals方法比较
==判断对象是否同一个。Integer var = ?在缓存区间的赋值(见规则1),会复用已有对象,因此这个区间内的Integer使用 ==进行判断可通过,但是区间之外的所有数据,则会在堆上新产生,不会通过。因此如果用== 来比较数值,很可能在小的测试数据中通过,而到了生产环境才出问题。
3.2【强制】 BigDecimal需要使用compareTo()
因为BigDecimal的equals()还会比对精度,2.0与2.00不一致。
- Facebook-Contrib: Correctness - Method calls BigDecimal.equals()
3.3【强制】 Atomic 系列,不能使用equals方法*
因为 Atomic* 系列没有覆写equals方法。
//RIGHT
if (counter1.get() == counter2.get()){...}
3.4【强制】 double及float的比较,要特殊处理
因为精度问题,浮点数间的equals非常不可靠,在vjkit的NumberUtil中有对应的封装函数。
float f1 = 0.15f;
float f2 = 0.45f/3; //实际等于0.14999999
//WRONG
if (f1 == f2) {...}
if (Double.compare(f1,f2)==0)
//RIGHT
static final float EPSILON = 0.00001f;
if (Math.abs(f1-f2)<EPSILON) {...}
Rule 4. 数字类型的计算原则
4.1【强制】数字运算表达式,因为先进行等式右边的运算,再赋值给等式左边的变量,所以等式两边的类型要一致
例子1: int与int相除后,哪怕被赋值给float或double,结果仍然是四舍五入取整的int。
需要强制将除数或被除数转换为float或double。
double d = 24/7; //结果是3.0
double d = (double)24/7; //结果是正确的3.42857
例子2: int与int相乘,哪怕被赋值给long,仍然会溢出。
需要强制将乘数的一方转换为long。
long l = Integer.MAX_VALUE * 2; // 结果是溢出的-2
long l = Integer.MAX_VALUE * 2L; //结果是正确的4294967294
另外,int的最大值约21亿,留意可能溢出的情况。
4.2【强制】数字取模的结果不一定是正数,负数取模的结果仍然负数
取模做数组下标时,如果不处理负数的情况,很容易ArrayIndexOutOfBoundException。
另外,Integer.MIN_VALUE取绝对值也仍然是负数。因此,vjkit的MathUtil对上述情况做了安全的封装。
-4 % 3 = -1;
Math.abs(Integer.MIN_VALUE) = -2147483648;
- Findbugs: Style - Remainder of hashCode could be negative
4.3【推荐】 double 或 float 计算时有不可避免的精度问题
float f = 0.45f/3; //结果是0.14999999
double d1 = 0.45d/3; //结果是正确的0.15
double d2 = 1.03d - 0.42d; //结果是0.6100000000000001
尽量用double而不用float,但如果是金融货币的计算,则必须使用如下选择:
选项1, 使用性能较差的BigDecimal。BigDecimal还能精确控制四舍五入或是其他取舍的方式。
选项2, 在预知小数精度的情况下,将浮点运算放大为整数计数,比如货币以"分"而不是以"元"计算。
Rule 5. 【推荐】如果变量值仅有有限的可选值,用枚举类来定义常量
尤其是变量还希望带有名称之外的延伸属性时,如下例:
//WRONG
public String MONDAY = "SPRING";
public int MONDAY_SEQ = 1;
//RIGHT
public enum SeasonEnum {
SPRING(1), SUMMER(2), AUTUMN(3), WINTER(4);
int seq;
SeasonEnum(int seq) { this.seq = seq; }
}
业务代码中不要依赖ordinary()函数进行业务运算,而是自定义数字属性,以免枚举值的增减调序造成影响。 例外:永远不会有变化的枚举,比如上例的一年四季。
Rule 6. 字符串拼接的原则
6.1 【推荐】 当字符串拼接不在一个命令行内写完,而是存在多次拼接时(比如循环),使用StringBuilder的append()
String s = "hello" + str1 + str2; //Almost OK,除非初始长度有问题,见第3点.
String s = "hello"; //WRONG
if (condition) {
s += str1;
}
String str = "start"; //WRONG
for (int i = 0; i < 100; i++) {
str = str + "hello";
}
反编译出的字节码文件显示,其实每条用+
进行字符拼接的语句,都会new出一个StringBuilder对象,然后进行append操作,最后通过toString方法返回String对象。所以上面两个错误例子,会重复构造StringBuilder,重复toString()造成资源浪费。
6.2 【强制】 字符串拼接对象时,不要显式调用对象的toString()
如上,+
实际是StringBuilder,本身会调用对象的toString(),且能很好的处理null的情况。
//WRONG
str = "result:" + myObject.toString(); // myObject为Null时,抛NPE
//RIGHT
str = "result:" + myObject; // myObject为Null时,输出 result:null
6.3【强制】使用StringBuilder,而不是有所有方法都有同步修饰符的StringBuffer
因为内联不成功,逃逸分析并不能抹除StringBuffer上的同步修饰符
6.4 【推荐】当拼接后字符串的长度远大于16时,指定StringBuilder的大概长度,避免容量不足时的成倍扩展
6.5 【推荐】如果字符串长度很大且频繁拼接,可考虑ThreadLocal重用StringBuilder对象
参考BigDecimal的toString()实现,及vjkit中的StringBuilderHolder。
Rule 7. 【推荐】字符操作时,优先使用字符参数,而不是字符串,能提升性能
//WRONG
str.indexOf("e");
//RIGHT
stringBuilder.append('a');
str.indexOf('e');
str.replace('m','z');
其他包括split等方法,在JDK String中未提供针对字符参数的方法,可考虑使用Apache Commons StringUtils 或Guava的Splitter。
Rule 8. 【推荐】利用好正则表达式的预编译功能,可以有效加快正则匹配速度
反例:
//直接使用String的matches()方法
result = "abc".matches("[a-zA-z]");
//每次重新构造Pattern
Pattern pattern = Pattern.compile("[a-zA-z]");
result = pattern.matcher("abc").matches();
正例:
//在某个地方预先编译Pattern,比如类的静态变量
private static Pattern pattern = Pattern.compile("[a-zA-z]");
...
//真正使用Pattern的地方
result = pattern.matcher("abc").matches();