编写高质量代码:改善Java程序的151个建议
秦小波
67个笔记
前言
- 本书附带有大量的源码(下载地址见华章网站www.hzbook.com
建议11:养成良好习惯,显式声明UID
-
SerialVersionUID,也叫做流标识符(Stream Unique Identifier),即类的版本定义的,它可以显式声明也可以隐式声明。显式声明格式如下:private static final long serialVersionUID = XXXXXL;而隐式声明则是我不声明,你编译器在编译的时候帮我生成。生成的依据是通过包名、类名、继承关系、非私有的方法和属性,以及参数、返回值等诸多因子计算得出的,极度复杂,基本上计算出来的这个值是唯一的。
-
JVM在反序列化时,会比较数据流中的serialVersionUID与类的serialVersionUID是否相同,如果相同,则认为类没有发生改变,可以把数据流load为实例对象;如果不相同,对不起,我JVM不干了,抛个异常InvalidClassException给你瞧瞧
-
刚开始生产者和消费者持有的Person类版本一致,都是V1.0,某天生产者的Person类版本变更了,增加了一个“年龄”属性,升级为V2.0,而由于种种原因(比如程序员疏忽、升级时间窗口不同等)消费端的Person还保持为V1.0版本,代码如下:public class Person implements Serializable{ private static final long serialVersionUID = 5799L; private int age; /age、name的getter/setter方法省略/}此时虽然生产者和消费者对应的类版本不同,但是显式声明的serialVersionUID相同,反序列化也是可以运行的,所带来的业务问题就是消费端不能读取到新增的业务属性(age属性)而已。
建议12:避免用序列化类在构造函数中为不变量赋值
-
如果final属性是一个直接量,在反序列化时就会重新计算。对这基本规则不多说
-
反序列化时构造函数不会执行。
-
在序列化类中,不使用构造函数为final变量赋值。
建议13:避免为final变量复杂赋值
-
上个建议所说final会被重新赋值,其中的“值”指的是简单对象。简单对象包括:8个基本类型,以及数组、字符串(字符串情况很复杂,不通过new关键字生成String对象的情况下,final变量的赋值与基本类型相同),但是不能方法赋值。
-
如果是复杂对象,也简单,连该对象和关联类信息一起保存,并且持续递归下去(关联类也必须实现Serializable接口,否则会出现序列化异常)
-
总结一下,反序列化时final变量在以下情况下不会被重新赋值:通过构造函数为final变量赋值。通过方法返回值为final变量赋值。final修饰的属性不是基本类型
建议14:使用序列化类的私有方法巧妙解决部分属性持久化问题
-
Java调用ObjectOutputStream类把一个对象转换成流数据时,会通过反射(Reflection)检查被序列化的类是否有writeObject方法,并且检查其是否符合私有、无返回值的特性。若有,则会委托该方法进行对象序列化,若没有,则由ObjectOutputStream按照默认规则继续序列化。同样,在从流数据恢复成实例对象时,也会检查是否有一个私有的readObject方法,如果有,则会通过该方法读取属性值。此处有几个关键点要说明
-
(1)out. defaultWriteObject()告知JVM按照默认的规则写入对象,惯例的写法是写在第一句话里。(2)in. defaultReadObject()告知JVM按照默认规则读入对象,惯例的写法也是写在第一句话里。(3)out. writeXX和in.readXX分别是写入和读出相应的值,类似一个队列,先进先出,如果此处有复杂的数据逻辑,建议按封装Collection对象处理。
建议18:避免instanceof非预期结果
-
‘A’ instanceof Character这句话可能有读者会猜错,事实上它编译不通过,为什么呢?因为’A’是一个char类型,也就是一个基本类型,不是一个对象,instanceof只能用于对象的判断,不能用于基本类型的判断。
-
null instanceof String返回值是false,这是instanceof特有的规则:若左操作数是null,结果就直接返回false,不再运算右操作数是什么类。这对我们的程序非常有利,在使用instanceof操作符时,不用关心被判断的类(也就是左操作数)是否为null,这与我们经常用到的equals、toString方法不同。
-
(String)null instanceof String返回值是false,不要看这里有个强制类型转换就认为结果是true,不是的,null是一个万用类型,也可以说它没类型,即使做类型转换还是个null。
建议26:提防包装类型的null值
- 包装类型参与运算时,要做null值校验。
第3章 类、对象及方法
- 书读得多而不思考,你会觉得自己知道的很多。书读得多而思考,你会觉得自己不懂的越来越多
建议33:不要覆写静态方法
- 不要覆写静态方法
建议36:使用构造代码块精炼程序
- 很简单,编译器会把构造代码块插入到每个构造函数的最前端
建议37:构造代码块会想你所想
- 编译器只是把构造代码块插入到super方法之后执行而已
建议39:使用匿名类的构造函数
-
1)l2=new ArrayList(){}l2代表的是一个匿名类的声明和赋值,它定义了一个继承于ArrayList的匿名类,只是没有任何的覆写方法而已,其代码类似于:
-
(2)l3=new ArrayList(){{}}这个语句就有点怪了,还带了两对大括号,我们分开来解释就会明白了,这也是一个匿名类的定义,它的代码类似于:
-
当然,一个类中的构造函数块可以是多个,也就是说可以出现如下代码:
-
List l3 = new ArrayList(){{} {} {}};
-
建议40:匿名类的构造函数很特殊
-
而匿名类因为没有名字,只能由构造代码块代替,也就无所谓的有参和无参构造函数了,它在初始化时直接调用了父类的同参数构造,然后再调用了自己的构造代码块,也就是说上面的匿名类与下面的代码是等价的:
-
它首先会调用父类有两个参数的构造函数,而不是无参构造,这是匿名类的构造函数与普通类的差别
建议41:让多重继承成为现实
-
让多重继承成为现实
-
幸运的是Java中提供的内部类可以曲折地解决此问题,我们来看一个案例
-
这也是内部类的一个重要特性:内部类可以继承一个与外部类无关的类,保证了内部类的独立性,正是基于这一点,多重继承才会成为可能
建议42:让工具类不可实例化
- 让工具类不可实例化
建议44:推荐使用序列化实现对象的拷贝
- 推荐使用序列化实现对象的拷贝
建议51:不要主动进行垃圾回收
- System.gc要停止所有的响应(Stop the world),才能检查内存中是否有可回收的对象,这对一个应用系统来说风险极大,如果是一个Web应用,所有的请求都会暂停,等待垃圾回收器执行完毕,若此时堆内存(Heap)中的对象少的话则还可以接受,一旦对象较多(现在的Web项目是越做越大,框架、工具也越来越多,加载到内存中的对象当然也就更多了),那这个过程就非常耗时了,可能0.01秒,也可能是1秒,甚至是20秒,这就会严重影响到业务的正常运行。
第7章 泛型和反射
-
项目的编码规则上便多了一条:优先使用泛型。
-
Java的泛型在编译期有效,在运行期被删除
-
List<String>、List<Integer>、List<T>擦除后的类型为List。List<String>[]擦除后的类型为List[]。List<?extends E>、List<?super E>擦除后的类型为List<E>。List<T extends Serializable&Cloneable>擦除后为List<Serializable>。
-
可以声明一个带有泛型参数的数组,但是不能初始化该数组
建议94:不能初始化泛型参数和数组
- 泛型类型在编译期被擦除,我们在类初始化时将无法获得泛型的具体参数,比如这样的代码
建议98:建议采用的顺序是List<T>、List<?>、List<Object>
-
建议采用的顺序是List<T>、List<?>、List<Object>
-
List<T>是确定的某一个类型
-
List<T>可以进行读写操作
-
List<?>是只读类型的,不能进行增加、修改操作,因为编译器不知道List中容纳的是什么类型的元素,也就无法校验类型是否安全了,而且List<?>读取出的元素都是Object类型的,需要主动转型,所以它经常用于泛型方法的返回值。注意,List<?>虽然无法增加、修改元素,但是却可以删除元素,比如执行remove、clear等方法,那是因为它的删除动作与泛型类型无关
建议102:适时选择getDeclared×××和get×××
- getMethod方法获得的是所有public访问级别的方法,包括从父类继承的方法,而getDeclaredMethod获得是自身类的所有方法,包括公用(public)方法、私有(private)方法等,而且不受限于访问权限。
建议105:动态加载不适合数组
- 因为数组比较特殊,要想动态创建和访问数组,基本的反射是无法实现的,“上帝对你关闭一扇门,同时会为你打开另外一扇窗”,于是Java就专门定义了一个Array数组反射工具类来实现动态探知数组的功能。
第8章 异常
-
Java中的异常一次只能抛出一个
-
第一个逻辑片段中抛出异常,则第二个逻辑片段就不再执行了,也就无法抛出第二个异常了,现在的问题是:如何才能一次抛出两个(或多个)异常呢
-
异常容器
建议115:使用Throwable获得栈信息
- 使用Throwable获得栈信息
建议120:不使用stop方法停止线程
- interrupt方法不能终止一个线程状态,它只会改变中断标志位(如果在t1.interrupt()前后输出t1.isInterrupted()则会发现分别输出了false和true),如果需要终止该线程,还需要自行进行判断,例如我们可以使用interrupt编写出更加简洁、安全的终止线程代码
建议121:线程优先级只使用三个等级
- 线程优先级推荐使用MIN_PRIORITY、NORM_PRIORITY、MAX_PRIORITY三个级别,不建议使用其他7个数字。
建议122:使用线程异常处理器提升系统可靠性
- Java 1. 5版本以后在Thread类中增加了setUncaughtExceptionHandler方法,实现了线程异常的捕捉和处理
建议126:适时选择不同的线程池来实现
- Java的线程池实现从最根本上来说只有两个:ThreadPoolExecutor类和Scheduled-ThreadPoolExecutor类,这两个类还是父子关系,但是Java为了简化并行计算,还提供了一个Executors的静态类,它可以直接生成多种不同的线程池执行器
建议127:Lock与synchronized是不一样的
- 更简单地说把Lock定义为多线程类的私有属性是起不到资源互斥作用的,除非是把Lock定义为所有线程的共享变量。
建议128:预防线程死锁
-
线程死锁(DeadLock)是多线程编码中最头疼的问题,也是最难重现的问题,因为Java是单进程多线程语言,一旦线程死锁,则很难通过外科手术式的方法使其起死回生,很多时候只有借助外部进程重启应用才能解决问题。我们看看下面的多线程代码是否会产生死锁:
-
class Foo implements Runnable{ public void run(){ //执行递归函数 fun(10); } //递归函数 public synchronized void fun(int i) { if (--i > 0) { for (int j = 0; j < i; j++) { System.out.print("*"); } System.out.println(i); fun(i); } } }
-
-
注意fun方法是一个递归函数,而且还加上了synchronized关键字,它保证同时只有一个线程能够执行,想想synchronized关键字的作用:当一个带有synchronized关键字的方法在执行时,其他synchronized方法会被阻塞,因为线程持有该对象的锁。比如有这样的代码:
-
static class Foo { public synchronized void m1() { try { Thread.sleep(1000); } catch (InterruptedException e) { // 异常处理 } System.out.println("m1执行完毕"); } public synchronized void m2() { System.out.println("m2执行完毕"); } } public static void main(String[] args) throws Exception { final Foo foo = new Foo(); // 定义一个线程 Thread t = new Thread(new Runnable() { public void run() { foo.m1();
-
建议129:适当设置阻塞队列长度
- ArrayBlockingQueue
- add方法。
- 上面在加入元素时,如果判断出当前队列已满,则返回false,表示插入失败,之后再包装成队列满异常。此处需要注意offer方法,如果我们直接调用offer方法插入元素,在超出容量的情况下,它除了返回false外,不会提供任何其他信息,如果我们的代码不做插入判断,那就会造成数据的“默默”丢失
- put方法了,它的作用也是把元素加入到队列中,但它与add、offer方法不同,它会等待队列空出元素,再让自己加入进去,通俗地讲,put方法提供的是一种“无赖”式的插入,无论等待多长时间都要把该元素插入到队列中,它的实现代码如下:
- put方法的目的就是确保元素肯定会加入到队列中,问题是此种等待是一个循环,会不停地消耗系统资源,当等待加入的元素数量较多时势必会对系统性能产生影响,那该如何解决呢?JDK已经想到了这个问题,它提供了带有超时时间的offer方法,其实现方法与put比较类似,只是使用Condition的awaitNanos方法来判断当前线程已经等待了多少纳秒,超时则返回false
建议137:调整JVM参数以提升性能
- 32位的机器上设置超过1.8GB的内存就有可能产生莫名其妙的错误。设置初始化堆内存为1GB(也就是最小堆内存),最大堆内存为1.5GB可以用如下的参数
建议140:推荐使用Guava扩展工具包
-
推荐使用Guava扩展工具包
-
多值Map
-
Table表
建议142:推荐使用Joda日期时间扩展包
-
推荐使用Joda日期时间扩展包
-
Joda还有一个优点,它可以与JDK的日期库方便地进行转换,可以从java.util.Date类型转为Joda的DateTime类型,也可以从Joda的DateTime转为java.util.Date,代码如下:
-
经过这样的转换,Joda可以很好地与现有的日期类保持兼容,在需要复杂的日期计算时使用Joda,在需要与其他系统通信或写到持久层中时则使用JDK的Date。Joda是一种令人惊奇的高效工具,无论是计算日期、打印日期,或是解析日期,Joda都是首选,当然日期工具类也可以选择date4j
建议143:可以选择多种Collections扩展
- 这里要特别注意的是大容量集合,什么叫大容量集合呢?我们知道一个Collection的最大容量是Integer的最大值(2 147 483 647),不能超过这个容量,一旦我们需要把一组超大的数据放到集合中,就必须要考虑对此进行拆分了,这会导致程序的复杂性提高,而fastutil则提供了Big系列的集合,它的最大容量是Long的最大值,这已经是一个非常庞大的数字了,超过这个容量基本上是不可能的。但在使用它的时候需要考虑内存溢出的问题
建议148:增强类的可替换性
-
通俗点讲,只要父类型能出现的地方子类型就可以出现,而且将父类型替换为子类型还不会产生任何错误或异常,使用者可能根本就不需要知道是父类型还是子类型。但是,反过来就不行了,有子类型出现的地方,父类型未必就能适应。
-
为了增强类的可替换性,就要求我们在设计类的时候考虑以下三点
-
(1)子类型必须完全实现父类型的方法