最近细读了秦小波老师的《编写高质量代码改善Jaav程序的151个建议》,要说是151个建议,其实更合适的说是避免Java的一些冷门的坑,下面整理了20个比较有趣的建议重新学习了一遍。
三元操作符的类型务必一致
三元操作符运算也称为三目运算,其表达式形如:"条件表达式 ? 表达式1 : 表达式2",在大部分语言中都有这样的三元操作符,其目的就是为了简化if-else,当条件表达式为真时执行表达式1,否则执行表达式2。 来分析一下下面这段代码:
public static void main(String[] args){
int i = 80;
String s = String.valueOf(i < 100 ? 80 : 100);
String s1 = String.valueOf(i < 100 ? 80 : 100.0);
boolean equals = s.equals(s1);
// 两者是否相等:false, 字符串s的值:80, 字符串s1的值:80.0
System.out.println("两者是否相等:" + equals);
System.out.println("字符串s的值:" + s);
System.out.println("字符串s1的值:" + s1);
}
说明:如果三目运算符的类型不一致,返回的结果也不一致。
避免带有变长参数的方法重载
public class Client {
private static final Logger log = LoggerFactory.getLogger(Client.class);
public static void main(String[] args) {
Client client = new Client();
client.calPrice(5000, 80);
}
/**
* calPrice 简单折扣计算
*
* @param price 价格
* @param discount 折扣
* @description
* @author luokangyuan
* @date 2019-4-2 14:58
* @version 1.0.0
*/
private void calPrice(int price, int discount) {
float knockdownPrice = price * discount / 100.0F;
log.info("简单的折扣后的价格:{}", knockdownPrice);
}
/**
* calPrice
*
* @param price 价格
* @param discounts 折扣
* @description 复杂折扣计算
* @author luokangyuan
* @date 2019-4-2 15:08
* @version 1.0.0
*/
private void calPrice(int price, int... discounts) {
float knockdownPrice = price;
for (int discount : discounts) {
knockdownPrice = knockdownPrice * discount / 100;
}
log.info("复杂折扣后的价格:{}", knockdownPrice);
}
}
说明:方法重载就是方法名相同,参数类型或者参数数量不同,在上述例子中Java
编辑器会根据方法签名找到合适的合适的方法,上述测试调用的就是简单的折扣计算,而非复杂折扣计算。
不要只替换一个类
public class Constant{
public final static int MAX_AGE = 150;
}
public class Client{
public static void main(String[] args){
System.out.println("人类寿命极限是:" + Constant.MAX_AGE);
}
}
对于final修饰的基本类型和String类型,编译器会认为它是稳定态的(Immutable Status)所以在编译时就直接把值编译到字节码中了,避免了在运行期引用(Run-time Reference),以提高代码的执行效率。对于我们的例子来说,Client类在编译时字节码中就写上了"150",这个常量,而不是一个地址引用,因此无论你后续怎么修改常量类,只要不重新编译Client类,输出还是照旧。
对于final修饰的类(即非基本类型),编译器会认为它不是稳定态的(Mutable Status),编译时建立的则是引用关系(该类型也叫作Soft Final)。如果Client类引入的常量是一个类或实例,及时不重新编译也会输出最新值。
用偶判断,不用奇判断
String s = n % 2 == 1 ? "奇数" : "偶数";
String s1 = n % 2 == 0 ? "偶数" : "奇数";
说明:通常使用第二种偶数判断,使用第一种的话。-1
也会被判断为偶数。
用整数类型处理货币
// 0.40000000000000036
System.out.println(10.00 - 9.60);
说明:Java中的浮点数是不准确的,在处理货币上使用浮点数就会存在问题,因此使用BigDecimal
,类来进行计算,或者使用整数,将需要计算的数放大100倍,计算后在缩小。
1、使用BigDecimal
BigDecimal
是专门为弥补浮点数无法精确计算的缺憾而设计的类,并且它本身也提供了加减乘除的常用数学算法。特别是与数据库Decimal类型的字段映射时,BigDecimal是最优的解决方案。
2、使用整型
把参与运算的值扩大100倍,并转为整型,然后在展现时再缩小100倍,这样处理的好处是计算简单,准确,一般在非金融行业(如零售行业)应用较多。此方法还会用于某些零售POS机,他们输入和输出的全部是整数,那运算就更简单。
使用String直接赋值
public static void main(String[] args) {
String str1 = "China";
String str2 = "China";
String str3 = new String("China");
String str4 = str3.intern();
System.out.println(str1 == str2); // true
System.out.println(str1 == str3); // false
System.out.println(str1 == str4); // true
}
说明:建议使用String str1 = "China";
这中方式对字符串赋值,而不是通过new String("China");
这种方式,在Java中会给定义的常量存放在一个常量池中,如果池中存在就不会在重复定义,所以str1 == str2
返回true
。new
出的是一个对象,不会去检查字符串常量池是否存在,所以str1 == str3
是不同的引用,返回false
。经过intern()
处理后,返回true
,是因为intern()
会去对象常量池中检查是否存在字面上相同得引用对象。
asList产生的list不可修改
private static void arraysTest() {
String[] week = {"Mon", "Tue", "Wed", "Thu"};
List strings = Arrays.asList(week);
strings.add("Fri");
}
说明:运行报错,asList产生的list不可修改。
别让null值和空值威胁到变长方法
public void countSum(String type, Integer... is){}
public void countSum(String type, String... strs){}
public static void main(String[] args) {
ClientReload clientReload = new ClientReload();
clientReload.countSum("China", 0);
clientReload.countSum("China", "People");
// 编译报错
clientReload.countSum("China");
// 编译报错
clientReload.countSum("China", null);
}
说明:同样是含有变长参数的重载方法,外部调用的使用使用NULL
或者空值都会出现编译不通过的错误,这是应为NULL
和空值在上述重载的方法中都满足参数条件,所以编译器不知道调什么方法,在重载方法的设计中违反了KISS
原则,同时在外部调用的时候,外部调用这隐藏了实参的类型,如将调用代码做如下修改,就不存在编译报错了。
String strs = null;
clientReload.countSum("China", strs);
警惕自增的陷阱
public static void main(String[] args) {
int count = 0;
for (int i = 0; i < 100; i++) {
int i1 = count++;
count = i1;
System.out.println("每次count++的值:" + i1);
}
System.out.println(count);
}
说明:结果是0
,而不是我们100
,这是count++
是一个表达式,返回的是自加之前count
的值。
break不可忘记
public static void main(String[] args) {
String s = toChineseNumber(2);
log.info("转换结果:{}", s);
}
private static String toChineseNumber(int n) {
String chineseNumber = "";
switch (n) {
case 0 : chineseNumber = "零";
case 1 : chineseNumber = "壹";
case 2 : chineseNumber = "贰";
case 3 : chineseNumber = "叁";
case 4 : chineseNumber = "肆";
case 5 : chineseNumber = "伍";
case 6 : chineseNumber = "陆";
case 7 : chineseNumber = "柒";
case 8 : chineseNumber = "捌";
case 9 : chineseNumber = "玖";
case 10 : chineseNumber = "拾";
}
return chineseNumber;
}
说明:在switch
中break
一定不能少。
不要让类型悄悄转换
/** 光速*/
private static final int LIGHT_SPEED = 30 * 10000 * 1000;
public static void main(String[] args) {
long dis = LIGHT_SPEED * 60 * 8;
// -2028888064
System.out.println(dis);
}
说明:LIGHT_SPEED * 60 * 8
计算后是int
类型,可能存在越界问题,虽然,我们在代码中写了转换为Long
型,但是,在Java中是先运算后在进行类型转换的,也就是LIGHT_SPEED * 60 * 8
计算后是int
型,超出了长度,从头开始,所以为负值,修改为显示的定义类型。如下:
/** 光速*/
private static final long LIGHT_SPEED = 30L * 10000 * 1000;
public static void main(String[] args) {
long dis = LIGHT_SPEED * 60 * 8;
System.out.println(dis);
}
避免带有变长参数的方法重载
public class MainTest {
public static void main(String[] args) throws ExecutionException, InterruptedException {
System.out.println(PriceTool.calPrice(12, 1)); // 1
}
}
class PriceTool {
public static int calPrice(int price, int discount) {
return 1;
}
public static int calPrice(int price, int... discount) {
return 2;
}
}
说明:编译器会从最简单的开始猜想,只要符合编译条件的即采用。
少用静态导入
import static java.lang.Math.PI;
public double calCircleArea(double r) {
return Math.PI * r * r;
}
public double calBallArea (double r) {
return 4 * PI * r * r;
}
说明:静态导入可以减少代码的量,但不易于阅读,可读性差。
提防包装类型的null值
public static int f(List list){
int count = 0;
for (Integer i : list){
count += (i != null) ? i : 0;
}
return count;
}
说明:包装对象和拆箱对象可以自由转换,这不假,但是要剔除null值,null值并不能转换为基本类型。对于此问题,我们谨记一点:包装类型参与运算时,要做null值校验。
谨慎包装类型的大小比较
举个例子。i==j false
。Integer是引用类型。
public static void main(String[] args){
Integer i = new Integer(100);
Integer j = new Integer(100);
System.out.println(i == j);
}
避免instanceof非预期结果
instanceof是一个简单的二元操作符,它是用来判断一个对象是否是一个类实例的,两侧操作符需要有继承或实现关系。
public static void main(String[] args) {
// String对象是否是Object的实例 - true,"String"是要给字符串,字符串继承了Object,当然是Object的实例。
boolean b1 = "String" instanceof Object;
// String对象是否是String类的实例 - true,一个类的对象当然是一个类的实例。
boolean b2 = new String() instanceof String;
// Object对象是否是String的实例,编译报错,Object是父类。
boolean b3 = new Object() instanceof String;
// 拆箱类型是否是装箱类型的实例,编译报错,“A”是一个Char型,是一个基本类型,instanceof只能用于对象判断。
boolean b4 = "A" instanceof Character;
// 空对象是否是String的实例 - false,instanceof的规则,左边为null,无论右边什么类型,都返回false。
boolean b5 = null instanceof String;
// 类型转换后的空对象是否是String的实例 - false,null可以说是没有类型,类型转换后还是null。
boolean b6 = (String) null instanceof String;
// Date对象是否是String的实例,编译报错,Date类和String类没有继承关系
boolean b7 = new Date() instanceof String;
}
不要随便设置随机数种子
在Java中,随机数的产生取决于种子,随机数和种子之间的关系遵从以下两个原则: 种子不同,产生不同的随机数 ;种子相同,即使实例不同也产生相同的随机数。
public static void main(String[] args)
{
Random r = new Random();
for (int i = 1; i < 4; i++)
{
System.out.println("第" + i + "次:" + r.nextInt());
}
}
运行结果:
第1次:846339168
第2次:756051503
第3次:1696875906
程序启动后,生成的随机数会不同。但是每次启动程序,生成的都会是三个随机数。产生随机数和种子之间的关系如下:
1)种子不同,产生不同的随机数。
2)种子相同,即使实例不同也产生相同的随机数。
Random的默认种子(无参构造)是System.nanoTime()
的返回值(jdk1.5以前是System.currentTimeMillis()),这个值是距离某一个固定时间点的纳秒数,不同的操作系统和硬件有不同的固定时间点,随机数自然也就不同了。
避免在构造函数中初始化其它类
public class Client35 {
public static void main(String[] args) {
Son son = new Son();
son.doSomething();
}
}
// 父类
class Father {
public Father() {
new Other();
}
}
// 相关类
class Other {
public Other() {
new Son();
}
}
// 子类
class Son extends Father {
public void doSomething() {
System.out.println("Hi, show me Something!");
}
}
说明:造成构造方法循环调用。
优先使用整型池
Integer缓存了-128-127的Integer对象。所以通过装箱(Integer.valueOf())获得的对象可以复用。
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
性能考虑,首选数组
private static int getSumForArray(int[] array) {
int sum = 0;
for (int i = 0; i < array.length; i++) {
sum += array[i];
}
return sum;
}
private static int getSumForList(List list) {
return list.stream().mapToInt(Integer::intValue).sum();
}