NO.1 不要在常量和变量中出现易混淆的字母
给long类型的变量赋值时,将长整型变量的标示字母“l”进行大写。
NO.2 莫让常量蜕变成变量
这种常量的定义方式不可取,常量就是常量,务必让常量的值在运行期保持不变
NO.3 三元操作符的类型务必一致
这段代码的结果是false,在使用三目运算符时,第一个操作数是int,第二个操作数是double,会默认进行隐式类型转换返回范围最大的数据类型,即double,那么s2的结果实际上是90.0,最终导致结果是false。
那么会有小伙伴提出疑问了,为什么是整形转为浮点,而不是浮点转为整形呢?这就涉及到三目运算符的转换规则:
(1)若两个操作数不可转换,则不做转换,返回值类型为Object
(2)若两个操作数是明确类型的表达式(比如变量),则按照正常的二进制数字来转换,int转为long,long转为float等
(3)若两个操作数中有一个是数字S,另一个是表达式,且其类型标示为T,那么,若数字S在T的范围内,则转换为T类型,若S超出了T的范围,则T转换为S类型
(4)若两个操作数都是直接量数字,则返回值类型为取值范围最大者
所以,为减少错误的产生,要保证三目运算符中的两个操作数类型一致
NO.4 避免带有变长参数的方法重载
public class Client {
//简单折扣计算
public void calPrice(double price, int discount) {
double knockdownPrice = price * discount / 100;
System.out.println("简单折扣后价格:" + formatCurrency(knockdownPrice));
}
//简单折扣计算
public void calPrice(double price, int... discounts) {
double knockdownPrice = price;
for (int discount : discounts) {
knockdownPrice = knockdownPrice * discount / 100;
}
System.out.println("简单折扣后价格:" + formatCurrency(knockdownPrice));
}
private String formatCurrency(double price) {
return NumberFormat.getCurrencyInstance().format(price / 100);
}
public static void main(String[] args) {
Client client = new Client();
client.calPrice(49900, 75);
}
}
程序执行结果如下:
这是一个计算商品价格折扣的类,里面有两个方法,一个是简单折扣,一个是复杂折扣,可以理解为折上折,那么这两个方法是重载吗?当然是,符合重载方法名一致,参数类型或参数个数不一致的特征,并且,第二个方法实际上是包含第一个方法在内的,那么为什么编译器在执行时会选择简单折扣方法,而不是第二个变长参数的方法呢?
这是因为Java在编译时,会首先根据实参的数量和类型进行处理,也就是查找到calPrice(double,int)方法,并没有把int转为int数组。
所以,为了程序能被“人类”看懂,还是慎重选择变长参数的方法重载吧,否则伤人脑筋不说,说不定哪天就陷入小陷阱了。
NO.5 别让null值和空值威胁到变长方法
public void methodA(String str, Integer... is){ }
public void methodA(String str, String... strs){ }
public static void main(String[] args) {
Client client = new Client();
client.methodA("china", 0);
client.methodA("china", "people");
client.methodA("china");
client.methodA("china",null);
}
上面的代码是编译不过的,有两处出现问题,client.methodA(“china”)和client.methodA(“china”,null),两处的提示是相同的:方法模糊不清,编译器不知道该使用哪个方法,但是这两处反映的代码坏味道却是不同的。
对于client.methodA(“china”),根据实参“china”,判定两个方法都符合形参格式,编译器不知道该选择哪个,所以报错。此处是由于设计者违反了KISS原则(Keep It Simple,Stupid,即懒人原则),按照此规则设计的方法应该很容易调用,可是在遵循规范的情况下,程序竟然出错了,这对设计者和开发者而言都是严禁出现的。
对于client.methodA(“china”,null),直接量null是没有类型的,所以对于两个重载方法都是满足的,就导致编译器不知道选择哪个,此处除了不符合懒人原则外,也隐藏了一个非常不好的编码习惯,调用者隐藏了实参类型,这是非常危险的,不仅调用者需要猜测该调用哪个方法,被调用者也可能产生内部逻辑混乱的情况。对于本例应该做如下修改:
public static void main(String[] args) {
Client client = new Client();
String[] strs = null;
client.methodA("china",strs);
}
让编译器知道这个strs是String[]类型的,也就减少了错误的发生。
NO.6 覆写变长方法也循规蹈矩
static class Base{
void fun(double price, int... discounts){
System.out.println("Base..........fun");
}
}
static class Sub extends Base{
@Override
void fun(double price, int[] discounts) {
System.out.println("Sub...........fun");
}
}
public static void main(String[] args) {
Base base = new Sub();
base.fun(100, 50);
Sub sub = new Sub();
sub.fun(100, 50);
}
以上代码在sub.fun(100, 50)是编译不通过的,但是base调用时却是正确的,这是因为base对象把子类对象Sub做了向上转型,也就是说形参列表是由父类决定的,由于是变长参数,所以50这个实参会被编译成“{50}”数组,在由子类执行,这是没有错的。
但是直接调用子类时,第二个形参的类型也是数组,但是编译器不会在两个没有继承关系的类之间做类型转换,所以编译器会去找fun(double,int)这个方法,类型不匹配编译器自然就会拒绝执行,并提示错误。
当然,这只是个特例,你会发现覆写的方法参数列表竟然和父类不同,这违背了覆写的定义。覆写的方法参数与父类相同,不仅仅是类型、数量,还包括显示形式
NO.7 警惕自增的陷阱
public static void main(String[] args) {
int count = 0;
for (int i = 0; i < 10; i++) {
count = count++;
}
System.out.println("count=" + count);
}
看这个问题之前,首先要说明一下i++和++i,大学C++老师就说过,自增有两种形式,i++和++i,i++是先赋值在自增,++i是先自增在赋值,这样理解的很多年也没出现问题,知道出现这段代码才开始怀疑自己。
因为这短代码的结果是:0.
count++是一个表达式,是有返回值的,返回值就是count自增前的值,也就是0
Java对自增是这样处理的:
step1:JVM把count的值拷贝到临时变量区
step2:count值加1,此时count=1
step3:返回临时变量区的值,注意这个值是0,没有被修改过
step4:返回值赋值给count,此时count被重置为0
解决方法其实很简单:只需要把count=count++修改为count++即可
NO.8 不要让旧语法困扰你
static void saveDefault(){}
static void save(int fee){}
public static void main(String[] args) {
int fee = 200;
saveDefault():save(fee);
}
这段代码看的我也很迷,毕竟我在敲这段代码的时候已经编译不通过了,报的语法错误,但是这是在《编写高质量代码 改善Java程序的151个建议》书中作者实际遇到的一段代码,据说当时并没有编译错误,甚至还可以运行,不过现在是无法复现了,只是举个例子避免这种情况。
saveDefault():save(fee)是一种C语言中的goto语法,其中的“:”称之为标号,goto语句中有着“double face”作用的关键字,它可以让程序从多层循环中跳出,不需要一层层退出,这点确实很好,但同时带来了代码结构混乱的问题,而且程序跳来跳去,出现问题时调试起来让人头晕。
但其实众所周知,Java上已经没有goto语句了,但是依然保留了该关键字,只是不进行语义处理而已,并且Java扩展了break和continue关键字,他们的后面都可以加上标号做跳转,完全实现了goto的功能,只不过也把goto的诟病也带了出来,所以在阅读大佬的源码时,几乎很少见到break和continue后跟标号的情况,甚至都很少看到break和continue,这可是提高代码可读性的一剂良药,还是舍弃旧语法吧,新语法真香!
NO.9 少用静态导入
public class MathUtils {
public static double calCircleArea(double r) {
return Math.PI * r * r;
}
public static double calBallArea(double r) {
return 4 * Math.PI * r * r;
}
}
这是一个计算面积的工具类,当其中的方法过多时,需要过多的书写Math.PI(圆周率常量,Math类自带),繁琐且多余,所以此时可以使用静态导入的方法来解决:
import static java.lang.Math.PI;
public class MathUtils {
public static double calCircleArea(double r) {
return PI * r * r;
}
public static double calBallArea(double r) {
return 4 * PI * r * r;
}
}
由此可见,使用了静态导入,代码中就不需要再写类名了,但是我们知道“类是一类事物的描述”,缺少了类名的修饰,静态属性和静态方法的表象意义可以被无限放大,这会让读者很难弄清楚这个属性和方法的意义何在,特别是在一个类中有多个静态导入语句时,若同时用了通配符,简直是一场灾难。举个例子:
这段代码看着就让人头大,分不清哪个方法是哪个类的,有什么作用。
所以,对于静态导入,一定要遵循两个原则:
(1)不使用通配符,除非是导入静态常量类
(2)方法名是具有明确、清晰表象意义的工具类
何为具有明确、清晰表象意义的工具类?
assertEquals断言是否相等
assertFalse断言是否为假
这样子的方法清晰直观,可用静态导入
NO.10 不要在本类中覆盖静态导入的变量和方法
import static java.lang.Math.PI;
import static java.lang.Math.abs;
pulic class client{
public static final String PI="祖冲之";
public static final int abs(int abs){
return 0;
}
public static void main(String[] args) {
System.out.println("PI="+PI);
System.out.println("abs(100)="+abs(-100));
}
}
运行结果:
很明显是本类中的静态属性和方法被引用了,那为什么不是Math类中的属性和方法呢,那是因为编译器有一个“最短路径”原则,如果能够在本类中查找到的变量,常量,方法,就不会到其他包或父类、接口中查找,以确保本类中的属性、方法优先。
因此,如果要变更一个被静态导入的方法,最好的办法是在原始类中重构,而不是在本类中覆盖。