《effective Java》读后笔记

本文介绍了Java编程中的多种最佳实践,包括使用静态工厂方法代替构造器、合理使用构造器模式、枚举实现单例模式、避免创建不必要的对象、合理管理对象引用、正确实现equals方法、覆盖hashCode方法、使用clone方法的注意事项等。

说明:一下内容全部摘自《effective java》一书

1. 使用静态工厂方法代替构造器

  • 优势:

    1. 有名称,可以区分不同的重载构造器

    2. 不必再每次调用的时候都创建一个新对象(重复利用,返回成员变量);返回同一个对象,当用equals判断式可以用 == 代替,因为 == 方法更快可提升性能。实例受控:Singleton,不可实例化,确保不会存在另个相等的实例

    3. 可以返回类型的任何子类型

  • 缺点:类如果不含公有的或者受保护的构造器,就不能被子类化

2. 当构造器有多个参数时,使用构建器

  • 当有多个参数时,可使用:

    1. 重叠构造器:提供第一个只有必要参数的构造器,第二个构造器有一个可选参数,第三个有两个可选参数

    2. JavaBean模式:调用一个无参构造器来创建对象,然后调用其setter方法来设置每个必要参数。缺点:分成了多个set调用,类是可变的(因为拥有setter方法),存在线程安全问题

    3. Builder模式:线程安全的,因为不可变(不存在setter方法,用builder来设置值),其 Builder是静态内部类,拥有和外部类相同的变量,有一个build方法 返回 外部对象实例(外部拥有一个private构造器,参数为 builder)。缺点:为了创建对象,必须先创建它的构建器,所以只在有很多参数的时候才使用

3. 用枚举类型强化Singleton

  • 一般的单例模式可以通过反射机制创建多个

  • 如果单例需要 序列化,则需要把所有实例域设置为瞬时的,防止反序列化生成新实例,并提供一个readResolve方法(该方法返回的对象引用将被返回,取代新建的对象)

  • 枚举:防止多次实例化,即便在序列化和反射时

4. 避免创建不必要的对象

  • 重用对象,快速;如果对象是不可变的,则它始终可以被重用

  • 在Map对象,每次调用 keySet 都返回同样的 Set 实例

5. 消除过期的对象引用

  • 在用数组实现的栈中,如果不释放内存,则会存在内存泄漏(在java中,就是没有置为null)。因为是通过size来控制访问,但是数组里面的值一直存在,只是通过 size 访问不了

  • 来源:缓存,自己管理内存的时候

6. 终结方法

  • System.gc  和 System.runFinalization 方法并不保证终结方法一定被执行,如果异常发生在终结方法之中,则不会抛出异常信息

  • 如果累的对象中封装的资源确实需要终止,只需提供一个显示的终止方法(设置状态标志),如果在终止之后被调用,其他的方法就必须检查这个域,并抛出 IllegalStateException

7. equals 方法 **

  • 等价关系:

    1. 自反性  x.equals(x) 返回true(contains方法的时候)

    2. 对称性,x.equals(y) 与 y.equals(x) 返回结果一致

    3. 传递性:x.equals(y)  y.equals(z) 则 a.equals(z)

    4. 一致性:多次调用结果一直

8. 覆盖 equals 方法时总要覆盖 hashCode 方法

  • 理由:为了使基于散列的集合正常运作(HashMap,HashSet、Hashtable)

  • 如果两个对象根据equals 方法比较是相等的,那么调用这两个对象任意一个对象的hashCode方法搜必须产生同样的整数结果

9. clone

  • Cloneable 接口的目的只是表明实现了这个接口的类的实例允许克隆。如果一个类实现了 Cloneable,Object的clone方法就返回该对象的浅拷贝(clone方法为 protected,所以需要重写),否则就会抛出 CloneNotSupportException异常。在拷贝过程中没有调用构造器,调用 super.clone方法会返回当前实例的

  • 引用对象需要单独 clone,因为clone方法为浅拷贝。当为对象数组时,数组中的对象也会被拷贝一份,即使自己不再对数组对象进行调用clone方法。如果一个对象的成员是引用对象,引用对象的成员也有引用成员,则需要一层层的重写 clone 方法

  • clone 方法不应该在构造过程中,调用新对象中任何非final的方法,防止子类通过多态改变其行为(final or private)

  • 对于基本类型的属性,或者指向不可变对象的引用,可不 clone

  • 对于一个专门为了继承而设计的类,如果未提供clone方法,则它的子类就不能实现 Cloneable??(子类调用super.clone时,但是super并未实现Cloneable接口的话就会报错??但是继承了Object的方法呀)

10. Comparable 接口

  • 与 equals 不同的是,在跨越不同类的时候,compareTo可以不做比较,可以抛出 ClassCastException

  • 自反性、对称性、传递性

11. 是访问性最小化

  • 对于顶层的类和接口:public 、 private(不写的时候默认)

  • 成员:public 、protected、default、private、

  • 若子类方法覆盖了超类中的一个方法,子类中的访问级别不允许低于超类中的访问级别(因为当父类中调用被覆盖方法的时候会因为访问权限而访问不了)

12. 使类的可变性最小化

  • 缘由:因为不可变类不容易出错且更加安全(不可变对象本质上是线程安全的,它们不需要同步)

  • 不可变类:

    1. 没有更改状态的方法

    2. 类不会 被扩展(final)

    3. 所有域都为 final

    4. 域为私有的

    5. 对可变组件进行互斥访问:保护性拷贝技术。使客户端无法获得指向这些对象的引用

    6. 把类的构造器变成私有的或者包级私有的并添加公有的静态工厂来代替公有的构造器

  • 缺点:每个不同的值都需要一个单独的对象

13. 复合优先于继承

  • 超类的实现发生变化,子类会遭到破坏(比如 addAll 方法是基于 add 方法实现的),构造器不能调用可被覆盖的方法,因为子类还未被初始化。因为超类的构造器再子类的构造器之前运行,所以,子类中覆盖版本的方法将会在子类的构造器运行之前就先被调用。无论是clone还是readObject,都不可以调用可覆盖的方法,不管是以直接还是间接方式

14. 接口优于抽象类

  • 接口与抽象类的区别:

    1. 抽象类允许包含某些方法的实现,说那是接口不允许

    2. 接口只能包含常量,因为没有构造器

    3. 抽象类更注重父子关系,接口只是表明他提供了可选择的一组行为

    4. 抽象类-单继承

  • 当使用接口时,可以用骨架实现类(抽象类)实现一部分方法,把接口和抽象类的优点结合起来了

  • 实现了接口的类可以把对于接口方法的调用,转发到一个内部私有类的实力上,这个内部私有类扩展了骨架实现类,用这种方法可以模拟多继承(maybe组合也可以呢)

  • 当在接口中增加方法时,所有实现类都会被破坏,所以可以在骨架实现类实现那个新方法

15. 优先考虑使用静态成员类

  • 内部类存在的目的只是为其外围类提供服务(可实现其他接口或者继承其他类,所以...)

  • 4 种内部类;

    1. 静态成员类:公有的辅助类,只能访问外部的静态成员。当内部类不需要访问外部类时,可以声明为私有静态,可防止浪费空间和时间

    2. 非静态成员类(含有外部实例),可以调用外围实例上的方法,或者利用修饰过的this构造获得外围实例的引用。当外围类的某个实例方法的内部调用非静态成员类的构造器时,这种关联关系被自动建立起来。但是不能声明static成员

    3. 匿名类:匿名类没有名字,它不是外围类的一个成员,它并不与其他成员一起被声明,而是在使用的同时被声明和实例化。可以在抽象类和接口类上使用匿名内部类。当且仅当匿名类出现在非静态的环境中时,它才有外围实例。但是即使它们出现在静态环境中,也不可能拥有任何静态成员匿名类不能实现接口也不能扩展一个类,因为匿名类的客户端无法调用任何新增成员

      • 在使用匿名内部类的时候,外部类和局部变量会作为匿名类的构造方法的参数

      • 为什么局部变量要作为内部类的构造方法的参数传入?

        • 为了解决:局部变量的生命周期与局部内部类的对象生命周期不一致问题。当外部类有一个成员变量指向这个匿名内部类,而匿名类所在的方法运行完毕时,这个方法的局部变量生命周期结束,所以,局部变量的生命周期结束。但是如果匿名内部类的某个方法使用了这个方法的局部变量,而外部类还有成员指向这个匿名类,当调用某个包含局部变量的方法时,就会造成局部变量不存在的问题

        • 解决办法:通过将局部变量赋值一份,复制品直接作为局部内部类的数据成员,这样,匿名类的实例访问的就是这个复制品,而不用在意局部变量生命周期结束

      • 为什么局部变量要声明为 final

        • 为了保证局部 变量和内部类中复制品的数据一致性

        • 对于基本类型,如果是final,由于值不能改变所以可以保证一致性

        • 对于引用类型,变量是final的,由于其引用不变(指向同一个对象),所以也不能指向其他对象,而引用的对象的值可以改变,但由于是同一个对象,所以可以保持一致性

        • 如果不是final的,如果匿名内部类修改了值或者把引用指向其他对象,则会造成方法的局部变量的值和匿名内部类的值不一致问题

      • 在jdk 1.8 之前,不用final修饰会编译报错。在jdk 1.8 中,不用final修饰不会报错,但是一旦改变了变量的值就会报错

    4. 局部类

    • 3 & 4 不能拥有静态成员

  • 如果一个内部类需要在多个方法之外仍然是可见的,或者是它太长了,不适合于放在方法内部,就应该使用成员类。如果成员类的每个实例都需要一个指向其外围实例的引用,就要把成员类做成非静态的;否则,就做成静态的。如果一个类只属于一个方法的内部,如果只需要在一个地方创建实例,并且已经有了一个预置的类型可以说明这个类的特征(抽象类或者接口),就要把它做成匿名类,否则,就做成局部类

16. 检查参数的正确性

  • 如果断言失败,将会抛出AssertionError,如果没起到作用,也不会有成本开销。在方法执行计算任务之前,应该先检查它的参数

17.保护性拷贝

  • 为了防止进入内部数据后再被修改,防止客户端对组件进行修改,导致原组件被破坏,所以不使用原对象,重新构造一个新对象

  • 对于可以被子类化的参数的类(非final类),不要使用clone方法进行保护性拷贝,因为可能会返回一个不可信的子类的实例

18.慎用重载

  • 调用哪个重载的方法是在编译时做出决定的,其选择是静态的,而重写的方法的选择则是动态的

  • List<E> 有两个重载的remove方法 remove(E )和 remove(int index)[优先级更高]

  • 应该避免发生的情景:同一组的参数只需要经过类型转换就可以被传递给不同的重载方法

  • 应该保证:当传递同样的参数时,所有重载方法的行为必须一致,或者各个重载方法再调用一般化的重载方法

19. 可变参数

  • 可变参数方法接受 0 个 或者多个指定类型的参数,可变参数机制通过先创建一个数组,数组的大小为在调用位置所传递的参数数量(这也是为什么可变参数只能作为参数列表的最后一个参数,因为个数不确定),然后将参数值传到数组中,最后将数组传递给方法

  • 当调用数组的 toString 方法的时候,会产生没意义的字符串,而list会打印集合中的元素。对于 Arrays.asList 方法是把参数改造成可变参数方法,然后把可变参数的每个参数的对象引用集中到数组的单个元素数组中,所以如果asList的参数是一个数组,则当调用 toString方法时会调用参数的toString方法,依然会打印出没有意义的字符。Arrays.toString根据每个类型的数组转成字符串,是重载

  • 可变参数的每次调用都会导致进行一次数组分配和初始化

20. 返回零长度的数组或者集合,而不是null

  • 如果返回 null ,则客户端必须有额外的代码来处理null返回值

  • null返回值比零长度数组更好的这个观点是错误的:

    1. 分配数组所需要的开销并不是造成性能问题的真正源头

    2. 对于不反悔任何元素的调用,可以每次都返回同一个零长度的数组,因为零长度数组时不可变的,可以自由的共享

  • Collections.emptySet、emptyList 和 emptyMap方法返回空集合

21. for-each 循环优先于传统的for循环

  • for-each循环,通过完全隐藏迭代器或者索引变量,避免了混乱和出错的可能,并且它对数组索引的边界值只计算一次

  • for-each 循环不仅可以遍历集合和数组,还可以遍历任何实现 Iterable接口的对象(可以自己implements 此接口)

  • 无法使用  for-each循环的情况:

    1. 过滤:遍历集合,并删除选定的元素,就需要使用显示的迭代器(也就是对原集合会改变的情况)

22. 计算精确用BigDecimal

  1. 可以使用 int 或者  long 进行换算单位后再进行计算,比如把元换成分,然后进行计算。如果树枝范围没有超过9位十进制数字,就可以使用 int;如果不超过18位数字,就可以使用long。如果数值超过 18 位数字,就必须使用 BigDecimal

23. 基本类型优先于装箱基本类型

  • 区别:

    1. 基本类型只有值,而装箱基本类型可以具有相同的值而不是同一个对象

    2. 基本类型只有功能完备的值,而装箱基本类型还有个非功能值 null

    3. 基本类型通常比装箱基本类型更节省时间和空间

  • 当两个 Integer进行大小于比较的时候,会进行自动拆箱,而进行 == 判断 的时候不拆箱

  • 在一项操作中混合使用基本类型和装箱类型时,装箱基本类就会自动拆箱,但是如果 null 对象引用被自动拆箱,就会抛出 NullPointerException

  • 对于 Long sum = 0 ; sum +=1;会先拆箱然后装箱,如果是在一个循环中,则会反复的拆箱装箱,导致性能下降

  • 什么时候用装箱基本类型

    1. 作为集合中国的元素、键和值的时候

    2. 泛型,作为参数类型

    3. 进行反射的方法调用时,必须使用装箱基本类型

24.不要使用 String 连接字符串,应该使用 StringBuilder 的append方法,或者StringBuffer

25.谨慎使用本地方法

  • 本地方法的用途:

    1. 提供了“访问特定于平台的机制”的能力,比如访问注册表和文件锁

    2. 提供了访问遗留代码库的能力,从而可以访问遗留数据

    3. 可以通过本地语言,编写应用程序中注重性能的部分,以提高系统的性能

  • 缺点:本地语言不是安全的,使用本地方法的应用程序也不再能免受内存毁坏错误的影响。因为本地语言是与平台相关的,使用本地方法的应用程序也不再是可自由移植的。在进入和退出本地代码时,需要相关的固定开销

26.命名惯例

  • edu.cqupt、com.alibaba

  • 常量:大写的单词,中间用下划线符号隔开,常量是唯一推荐使用下划线的情形

  • T表示任意类型,E表示集合的元素类型,K 和 V 表示映射的键和值类型,X表示异常。任何类型序列可以是 T、U、V 或者 T1、T2、T3

  • 类用名词短语命名

  • 接口以 able 、ible结果的形容词来命名

  • 转换对象类型的方法 toType,如toString、toArray

27.对可恢复的情况使用受检异常,对编程错误使用运行时异常

  • 分类

    1. 受检的异常:可恢复的情况

    2. 运行时异常:程序错误

    3. 错误:被JVM保留用于表示资源不足、约束失败或者其他使程序无法继续执行的条件

  • 如果程序没有捕捉到可抛出结果,将会导致当前线程停止,系统会自动打印出该异常的堆栈轨迹。但是如果catch到了,则会继续运行下去

  • IllegalArgumentException:参数不合适

  • IllegalStateException:调用非法,在某个对象还未被初始化前被调用

  • ConcurrentModificationException:并发的修改

  • NullPointerExcetion、IndexOutOfBoundsExcetion

  • 异常转译:catch到异常后,重新抛出更高级的异常。如果不能阻止或者处理来自底层的异常,一般的做法是使用异常转译,除非底层方法碰巧可以保证它抛出的所有异常对高层也合适才可以将异常从底层传播到高层

  • 只要将异常传播给外界,至少会导致程序迅速失败,从而有助于调试该失败条件的信息

28.保持原子性

  • 想要从异常中恢复,则失败的方法调用应该是对象保持在被调用之前的状态,这种属性的方法称为具有失败原子性

  • 方法:

    1. 不可变的对象:一致性

    2. 可变对象在执行操作之前检查参数的有效性,可以使得在对象的状态被修改之前,先抛出适当的异常;或者调整计算处理过程的顺序,使得任何可能会失败的计算部分都在对象状态被修改之前发生

    3. 编写一段恢复代码,由它拦截操作过程中发生的失败,以及使对象回滚到操作开始之前的状态上

    4. 在对象的一份临时拷贝上执行操作,当操作完成之后再用临时拷贝中的结果代替对象的内容

29.同步访问共享的可变数据

  • 当一个线程对一个变量进行访问时,而另外一个线程改变了这个变量的值,则第一个线程是不能可见改变的值的,所以需要同步访问,使变量可见

  • 当多个线程共享可变数据的时候,每个读或者写数据的线程都必须执行同步

  • 如果只需要线程之间的交互通信,而不需要互斥,volatile修饰符就是一种可以接受的同步形式

  • 在一个同步的区域内部,不要调用可被子类覆盖的方法,或者是由客户端以函数对象的形式提供的方法(策略模式), 因为外来方法可能会导致一次恒、死锁或者数据损坏

  • 不想要锁也可以安全的遍历列表:拷贝,CopyOnWriteArrayList

  • 如果方法修改了静态域,那么也必须同步对这个域的访问,即使往往只用于单个线程,因为客户要在这种方法上执行外部同步是不可能的,因为不可能保证其他不相关的客户也会执行外部同步

30. executor 和 task优先于线程

  • 关键的抽象是工作单元称作任务:Runnable 以及 Callable(与Runnable类似,但是会返回值)

  • Executor中有一个ScheduledThreadPoolExecutor 可以代替Timer,如果Timer唯一的线程抛出未被捕获的异常,timer就会停止运行,而被调度的线程池 executor支持多个线程,并且优雅的从抛出未受检异常的任务中恢复

31.线程安全性的文档化

  • 如果一个对象代表了零一个对象的一个视图,客户通常就必须在后台对象上同步,以防止其他线程直接修改后台对象(已经测试过:如果作为了锁对象,其他线程还是可以修改锁对象的值)

  • 如果一个类使用公有可访问的锁对象的时候,客户端可能会刻意超时保持公有可访问锁即可发起拒绝服务攻击。所以应该使用一个私有锁对象来代替同步的方法(隐含this可访问锁?),把锁对象封装在它所同步的对象中,锁对象可以被声明为final的,这样可以方式不小心改变他的内容

  • 私有锁对象模式特别适用于那些专门为继承而设计的类,这样可以防止客户端程序和子类的不同步干扰,能够在后续的版本中灵活的对并发控制采取更加复杂的方法

32.慎用延迟初始化

  • 如果多个线程共享一个延迟初始化的域,采用某种形式的同步是很必要的

    1. 对于静态域:用到的时候才初始化

       
      private static class FieldHolder{
        static final FieldType field = computeFieldValue();
      }
      static FieldType getField(){
          return FieldHodler.field;
      }
    2. 实例域:使用双重检查模式

       
      private volatile FieldType field;
      FiledType getFiled(){
        FieldType result = field;
        if(result == null){
        synchronized(this){
        result = field;
          if(result == null){
              field = result = computeFieldValue();
                  }
              }
          }
      }
    3. 对于可以接受重复初始化的实例域,可以采用单重检查模式,也就可以不用加锁了

33. 不要依赖于线程调度器

  • 线程优先级是不可移植的

  • Thread.yeild 方法不做实质性的工作,只是将控制权返回给了调用者,应该使用 Thread.sleep(1) 代替yield进行并发测试

34.谨慎的实现 Serializable 接口

  • 序列化是用来将对象编码成字节流,并从字节流编码中重新构建对象。一旦对象被序列化后,它的编码就可以从一台正在运行的虚拟机被传递到另一台虚拟机上,或者被存储到磁盘上,供以后反序列化时用

  • 每个可序列化的类都有一个唯一标识号与它相关联,若未指定,系统就会自动生成(根据类的信息生成,如果类发生改变,则自动生成的id也会发生变化,就会导致兼容性遭到破坏,在运行时导致InvalidClassException异常)。兼容性 & 性能问题

  • 序列化机制是一种对象创建机制,是一个隐藏的构造器

  • 好处:如果一个类要加入到某个框架中,并且该框架依赖于序列化来实现对象传输或者持久化,对于这种类来说,实现 Serializable 就很有必要了。因为 HttpServlet 实现了 Serializable接口,因此会话状态可以被缓存

  • 如果一个专门为了继承设计的类不是可序列化的,就不可能编写出可序列化的子类,除非提供无参构造器

  • 内部类不应该实现 Serializable 接口,而静态成员类却可以实现Serializable 接口。因为内部类使用编译器产生的合成域来保存指向外围实例的引用,而这些域并没有对应到类定义中

35.自定义序列化形式

  • 默认的序列化形式描述了该对象内部所包含的数据

  • 如果使用默认的序列化形式,并且把一个或者多个域标记为 transient,当一个实例而被序列化的时候,这些域将被初始化为它们的默认值,所以需要提供一个readObject方法,它首先调用 defaultReadObject(ObjectStream.defaultReadObject),然后把这些 transient域恢复为可接受的值

  • readObject方法实际上相当于另一个共有构造器,如同其他的构造器一样,它也要求注意同样的所有注意事项(约束),构造器必须检查其参数的有效性。readObject是一个“用字节流作为唯一参数”的构造器。如果修改了序列化字节文件,则可以违反约束条件 。如果包含了私有的可变组件(final引用依然是可变的),那么在他的 readObject 方法中,必须对这些组件进行保护性拷贝(field=new() ,这个field只能是非final),不然可以通过修改字节流,用其他引用到这个可变对象,然后对那个引用进行修改,则依然修改了原来的那个对象,就可以违反约束性条件

  • 建议:

    1. 对于对象引用域必须保持为私有的类,要保护性的拷贝这些域中的每个对象。比如不可变类的可变组件

    2. 对于约束性检查,检查动作应该在所有的保护性拷贝之后,如果检查失败,抛出 InvalidObjectException

    3. readObject 方法不要调用类中任何可被覆盖的方法(不管是直接还是间接)

36.对于实例控制,枚举类型优先于 readResolve

  • 一个正在被反序列化的对象,如果他的类定义了一个readResolve 方法,那么在反序列化之后,新对象上的readResolve 方法就会被调用,然后,该方法返回的对象引用将被返回,取代新建的对象

  • 如果依赖readResolve进行实例控制(单例,readResolve方法直接返回那个 INSTANCE就可以了),带有对象引用类型的所有实例域则必须声明为 transient。原因:在序列流中,可以用盗用者 类的实例代替SIngleton的非transient域,从而产生了不同的实例。

  • 可以将一个序列化的实例受控的类编写成枚举,就可以绝对保证除了所有声明的常量之外,不会有别的实例

37.序列化代理

  • 序列化代理就是一哥可序列化的类中有一个私有的静态内部类,这个内部类有一个单独的构造器和外围类所具有的filed(在内部类中为final),其参数类型就是外围类。外围类有一个 writePlace方法,返回这个代理类,这个方法会在序列化之前,将外围类的实例转变为序列化代理,而内部类的readResolve方法返回一个新的外围类实例(内部类field都是根据外围类设立的,所有不用考虑约束条件了)。为了防止攻击,要把外围类的readObject方法抛出异常。外围类和内部类都需要implements Serializable接口。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值