读书笔记----《编写高质量代码:改善Java程序的151个建议》第一章

第一章 通用方法和准则

  1. 包名全小写,类名首字母全大写,常量全部大写并用下划线分隔,变量采用驼峰命名法(Camel Case)命名。Long型数据标志使用L代替l,使用O(字母O)时加注释。

  2. randm 的数据不要定义成常量

  3. 三元操作符的类型务必一致:
    转换规则:

    • 若两个操作数不可转换,则不做转换,返回值为Object类型。
    • 两个操作数是明确类型的表达式(比如变量),则按照正常的二进制数字来转换,int类型转换为long类型,long类型转换为float类型等。
  4. 避免带有变长参数的方法重载。
    下面是个例子:

public void calPrice(int price, int discount)
public void calPrice(int price, int... discounts)

//call:
calPrice(49900,75)

此时第一个方法会被调用,因为int是一个原生数据类型,而数组本身是一个对象,编译器想要“偷懒”,于是它会从最简单的开始“猜想”,只要符合编译条件的即可通过,于是就出现了此问题。
5. 避免让null值和空值威胁到变长方法

public class Client{
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类的设计者,他违反了KISS原则(Keep It Simple, Stupid,即懒人原则),按照此规则设计的方法应该很容易调用的。
改进:

public static void main(String[]args){
Client client=new Client();
String[]strs=null;
client.methodA("China",strs);
}

也就是说让编译器知道这个null值是String类型的,编译即可顺利通过。
6. 覆写变长方法也循规蹈矩
子类覆写父类中的方法要符合开闭原则(Open-Closed Principle):

  • 重写方法不能缩小访问权限。
  • 参数列表必须与被重写方法相同。
  • 返回类型必须与被重写方法的相同或是其子类。
  • 重写方法不能抛出新的异常,或者超出父类范围的异常,但是可以抛出更少、更有限的异常,或者不抛出异常。
public class Client{
public static void main(String[]args){
//向上转型
Base base=new Sub();
base.fun(10050);
//不转型
Sub sub=new Sub();
sub.fun(10050);
}
}
//基类
class Base{
void fun(int price, int... discounts){
System.out.println("Base……fun");
}
}
//子类,覆写父类方法
class Sub extends Base{
@Override
void fun(int price, int[]discounts){
System.out.println("Sub……fun");
}

编译通不过。报错位置:sub.fun(100,50)
加上@Override注解没有问题,只是Eclipse会提示这不是一种很好的编码风格。
事实上,base对象是把子类对象Sub做了向上转型,形参列表是由父类决定的,由于是变长参数,在编译时,“base.fun(100,50)”中的“50”这个实参会被编译器“猜测”而编译成“{50}”数组,再由子类Sub执行。这时编译器并不会把“50”做类型转换,因为这时该参数已经被理解为一个数组对象,数组对象并不是int的子类,所以肯定报错。
注意:虽然变长参数方法最后的参数会被隐式转换为int[],但两者并不等同,要用fun(1,2,3,4)去调用fun(int a,int[] b)是不能成功的。
7. 警惕自增的陷阱
下次如果看到某人T恤上印着“i=i++”,千万不要鄙视他,记住,能够以不同的语言解释清楚这句话的人绝对不简单,应该表现出“如滔滔江水”般的敬仰,心理默念着“高人,绝世高人哪”。
8. 不要让旧语法困扰你:不要用goto,少用break和continue。
9. 少用静态导入 import static
即使用也要遵守以下规则:
- 不使用*(星号)通配符,除非是导入静态常量类(只包含常量的类或接口)。
- 方法名是具有明确、清晰表象意义的工具类
10. 不要在本类中覆盖静态导入的变量和方法
编译器有一个“最短路径”原则:如果能够在本类中查找到的变量、常量、方法,就不会到其他包或父类、接口中查找,以确保本类中的属性、方法优先。
11. 显式声明serialVersionUID
有时候我们需要一点特例场景,例如:我的类改变不大,JVM是否可以把我以前的对象反序列化过来?就是依靠显式声明serialVersionUID,向JVM撒谎说“我的类版本没有变更”,如此,我们编写的类就实现了向上兼容。
注意:显式声明serialVersionUID可以避免对象不一致,但尽量不要以这种方式向JVM“撒谎”。
12. 避免用序列化类在构造函数中为不变量赋值
示例使用了person类带一个final的name属性,但是是在构造函数中给name赋值,然后又写了一个相同serialVersionUID的Persion,构造中name不同,输出后发现name还是跟新前的。
解释
反序列化的执行过程是这样的:JVM从数据流中获取一个Object对象,然后根据数据流中的类文件描述信息(在序列化时,保存到磁盘的对象文件中包含了类描述信息,注意是类描述信息,不是类)查看,发现是final变量,需要重新计算,于是引用Person类中的name值,而此时JVM又发现name竟然没有赋值,不能引用,于是它很“聪明”地不再初始化,保持原值状态,所以结果就是原来的值了。
注意: 在序列化类中,不使用构造函数为final变量赋值
13. 避免为final变量复杂赋值
例子类似12

public class Person implements Serializable{
private static final long serialVersionUID=91282334L;
//通过方法返回值为final变量赋值
public fnal String name=initName();
//初始化方法名
public String initName(){
return"混世魔王";
}
}

Person类写好了(定义为V1.0版本),先把它序列化,存储到本地文件,其代码与上一建议的Serialize类相同

public class Person implements Serializable{
private static final long serialVersionUID=91282334L;
//通过方法返回值为final变量赋值
public final String name=initName();
//初始化方法名
public String initName(){
return"德天使";
}
}

但是name也是没有变,原因就是使用了方法给final字段赋值。
事实上保存到磁盘上(或网络传输)的对象文件包括两部分:
(1)类描述信息
包括包路径、继承关系、访问权限、变量描述、变量访问权限、方法签名、返回值,以及变量的关联类信息。要注意的一点是,它并不是class文件的翻版,它不记录方法、构造函数、static变量等的具体实现。之所以类描述会被保存,很简单,是因为能去也能回嘛,这保证反序列化的健壮运行。
(2)非瞬态(transient关键字)和非静态(static关键字)的实例变量值
注意,这里的值如果是一个基本类型,好说,就是一个简单值保存下来;如果是复杂对象,也简单,连该对象和关联类信息一起保存,并且持续递归下去(关联类也必须实现Serializable接口,否则会出现序列化异常),也就是说递归到最后,其实还是基本数据类型的保存。
总结:反序列化时final变量在以下情况下不会被重新赋值:
(1) 通过构造函数为final变量赋值。
(2)通过方法返回值为final变量赋值。
(3)final修饰的属性不是基本类型。
14. 使用序列化类的私有方法巧妙解决部分属性持久化问题
业务需求:一个计税系统和人力资源系统(HR系统)通过RMI(Remote Method Invocation,远程方法调用)对接,计税系统需要从HR系统获得人员的姓名和基本工资,以作为纳税的依据,而HR系统的工资分为两部分:基本工资和绩效工资,基本工资没什么秘密,根据工作岗位和年限自己都可以计算出来,但绩效工资却是保密的,不能泄露到外系统
方案1:在绩效工资前加上transient关键字,缺点:Salary类失去了分布式部署的功能。
方案2:

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 IOException, Class-
NotFoundException{
in.defaultReadObject();
//告知JVM按照默认规则读入对象,惯例的写法也是写在第一句话里
salary=new Salary(in.readInt(),0);
}
}

缺点:Person类失去了分布式部署的能力,但好在它的分布式部署需求并不高。
15. break万万不可忘【尤其switch中】
16. 易变业务使用脚本语言编写
PHP、Ruby、Groovy、JavaScript等全是同一类语言—脚本语言,它们都是在运行期解释执行的。脚本语言的三大特征,如下所示:
- 灵活。脚本语言一般都是动态类型,可以不用声明变量类型而直接使用,也可以在运行期改变类型。
- 便捷。脚本语言是一种解释型语言,不需要编译成二进制代码,也不需要像Java一样生成字节码。它的执行是依靠解释器解释的,因此在运行期变更代码非常容易,而且不用停止应用。
- 简单。只能说部分脚本语言简单,比如Groovy, Java程序员若转到Groovy程序语言上,只需要两个小时,看完语法说明,看完Demo即可使用了,没有太多的技术门槛。
17. 慎用动态编译
在使用动态编译时,需要注意以下几点:
(1)在框架中谨慎使用
比如要在Struts中使用动态编译,动态实现一个类,它若继承自ActionSupport就希望它成为一个Action。能做到,但是debug很困难;再比如在Spring中,写一个动态类,要让它动态注入到Spring容器中,这是需要花费老大功夫的。
(2)不要在要求高性能的项目使用
动态编译毕竟需要一个编译过程,与静态编译相比多了一个执行环节,因此在高性能项目中不要使用动态编译。不过,如果是在工具类项目中它则可以很好地发挥其优越性,比如在Eclipse工具中写一个插件,就可以很好地使用动态编译,不用重启即可实现运行、调试功能,非常方便。
(3)动态编译要考虑安全问题
如果你在Web界面上提供了一个功能,允许上传一个Java文件然后运行,那就等于说:“我的机器没有密码,大家都来看我的隐私吧”,这是非常典型的注入漏洞,只要上传一个恶意Java程序就可以让你所有的安全工作毁于一旦。
(4)记录动态编译过程
建议记录源文件、目标文件、编译过程、执行过程等日志,不仅仅是为了诊断,还是为了安全和审计,对Java项目来说,空中编译和运行是很不让人放心的,留下这些依据可以更好地优化程序。
18. 避免instanceof非预期结果

public class Client{
public static void main(String[]args){

boolean b1="Sting"instanceof Object;
//返回值是true

boolean b2=new String()instanceof String;
//返回值是true

boolean b3=new Object()instanceof String;
//返回值是false

//拆箱类型是否是装箱类型的实例
boolean b4='A'instanceof Character;
//编译不通过,因为'A'是一个char类型,也就是一个基本类型,不是一个对象,instanceof只能用于对象的判断。

//空对象是否是String的实例
boolean b5=null instanceof String;
//返回值是false,这是instanceof特有的规则:若左操作数是null,结果就直接返回false,不再运算右操作数是什么类。

//类型转换后的空对象是否是String的实例
boolean b6=(String)null instanceof String;
//null是一个万用类型,也可以说它没类型,即使做类型转换还是个null。

//Date对象是否是String的实例
boolean b7=new Date()instanceof String;
//编译通不过,因为Date类和String没有继承或实现关系,instanceof操作符的左右操作数必须有继承或实现关系,否则编译会失败。

//在泛型类中判断String对象是否是Date的实例
boolean b8=new GenericClass<String>().isDateInstance("");
//编译通过了,返回值是false, T是个String类型,与Date之间没有继承或实现关系,为什么''t instanceof Date''会编译通过呢?那是因为Java的泛型是为编码服务的,在编译成字节码时,T已经是Object类型了,传递的实参是String类型,也就是说T的表面类型是Object,实际类型是String,那''t instanceof Date''这句话就等价于''Object instance of Date''了
}
}
class GenericClass<T>{
//判断是否是Date类型
public boolean isDateInstance(T t){
return t instanceof Date;
}
}

以上的示例一定要注意。
19. 断言绝对不是鸡肋
AssertionError错误,它继承自Error,注意,这是一个错误,是不可恢复的,也就表示这是一个严重问题。
注意:
(1)在对外公开的方法中不能用断言做输入校验
(2)在断言中不要执行逻辑代码【因为生产系统中一般默认关闭断言】
20. 不要只替换一个类

public class Constant{
//定义人类寿命极限
public fnal static int MAX_AGE=150;
}
public class Client{
public static void main(String[]args){
System.out.println("人类寿命极限是:"+Constant.MAX_AGE);
}
}
//之后替换原有Constant类
public class Constant{
//定义人类寿命极限
public fnal static int MAX_AGE=150;
}
//输出的结果是:“人类寿命极限是:150”

原因是:对于final修饰的基本类型和String类型,编译器会认为它是稳定态(Immutable Status),所以在编译时就直接把值编译到字节码中了,避免了在运行期引用(Run-time Reference),以提高代码的执行效率。针对我们的例子来说,Client类在编译时,字节码中就写上了“150”这个常量,而不是一个地址引用,因此无论你后续怎么修改常量类,只要不重新编译Client类,输出还是照旧。
而对于final修饰的类(即非基本类型),编译器认为它是不稳定态(Mutable Status),在编译时建立的则是引用关系(该类型也叫做Soft Final),如果Client类引入的常量是一个类或实例,即使不重新编译也会输出最新值。
在IDE中不能重现该问题,若修改了Constant类,IDE工具会自动编译所有的引用类,“智能”化屏蔽了该问题,但潜在的风险其实仍然存在。
注意 发布应用系统时禁止使用类文件替换方式,整体WAR包发布才是万全之策。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值