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

编写高质量代码:改善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的匿名类,只是没有任何的覆写方法而已,其代码类似于:

    • image-20220809101516481
  • (2)l3=new ArrayList(){{}}这个语句就有点怪了,还带了两对大括号,我们分开来解释就会明白了,这也是一个匿名类的定义,它的代码类似于:

    • image-20220809101541758
  • 当然,一个类中的构造函数块可以是多个,也就是说可以出现如下代码:

    •   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方法。
    • image-20220809101824787
  • 上面在加入元素时,如果判断出当前队列已满,则返回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)子类型必须完全实现父类型的方法

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

BinBin_Bang

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值