《Effective Java 第3版》读书笔记,持续更新中…
资源
百度网盘资源链接(包含第2版和第3版的中英文 pdf 电子书)提取码:zunh
目录
第 2 章 创建和销毁对象
第 1 条:用静态工厂方法代替构造器
原文摘抄
- P9
静态工厂和公有构造器都各有用处,我们需要理解它们各自的长处。静态工厂经常更加合适,因此切忌第一反应就是提供公有的构造器,而不先考虑静态工厂。
本条总结
本条讲了静态工厂方法相比于公有构造器的五大优势和两个缺点,还提到了享元模式。
第 2 条:遇到多个构造器参数时要考虑使用构建器
原文摘抄
- P15
如果类的构造器或者静态工厂中具有多个参数,设计这种类时,Builder 模式就是一种不错的选择。
本条总结
本条讲了当一个类中有多个参数时,这个类的构建方式,提到了重叠构造器、JavaBeans 模式,并分析了它们的缺点。
第 3 条:用私有构造器或者枚举类型强化 Singleton 属性
原文摘抄
- P17
如果 Singleton 必须扩展一个超类,而不是扩展 Enum 的时候,则不宜使用这个方法(虽然可以声明枚举去实现接口)。
本条总结
单例模式的各种实现已经很熟悉了,但还是有很多需要注意的细节问题。比如本条中提到的通过反射机制调用私有构造器,可以采用在构造器中创建实例的时候判断持有的实例引用不为空则抛出异常的方式。另外防止序列化创建新的实例,本条中提到了 transient 关键字还有 readResolve 方法,这个没有接触过,有机会再看。
第 4 条:通过私有构造器强化不可实例化的能力
本条总结
只包含静态方法和静态域的类,据我理解也就是所谓的工具类,如 java.lang.Math、java.util.Collections 等。这些类也可以聚集与之相关的 final 类的静态方法,因为 final 类没有子类。
从 Java 8 开始,可以把这些方法组织到一个接口里。
如果是组织到一个类中,那么你肯定不希望这个类被实例化,具体做法就是给这个类写一个私有的构造器,并在里面抛出 AssertionError(),并写好注释。
第 5 条:优先考虑依赖注入来引用资源
原文摘抄
P18
静态工具类和 Singleton 类不适合于需要引用底层资源的类。
P18
当创建一个新的实例时,就将该资源传到构造器中。
本条总结
本条讲了依赖注入模式,还顺带讲了 Java 8 的 Supplier< T > 接口,以及工厂模式在此模式下的应用。
第 6 条:避免创建不必要的对象
原文摘抄
- P22
要优先使用基本类型而不是装箱基本类型,要当心无意识的自动装箱。
本条总结
本条讲了在 String s = new String("abc")
中,”abc” 本来就是一个 String 实例,不如直接重用方式写成 String s = "abc"
。
然后讲了正则表达式匹配创建 Pattern 实例的成本很高,因为需要将正则表达式编译成一个有限状态机。所以应该用变量将这个实例缓存起来,在用到的时候复用。另外不要使用延迟初始化,这样每次调用都会判定是否初始化,反而会降低性能。
本章还讲了适配器模式和 Map 接口的 keySet 方法,但我没太 get 到作者想表达的意思。
后面讲到要避免频繁自动装箱引起的不必要的性能损耗。
第 7 条:消除过期的对象引用
原文摘抄
P24
清空对象引用应该是一种例外,而不是一种规范行为。
P24
只要类是自己管理内存,程序员就应该警惕内存泄漏问题。
内存泄漏的另一个常见来源是缓存。
内存泄漏的第三个常见来源是监听器和其他回调。
本条总结
本条以自己实现的栈为例,当栈弹出元素时,应该立刻将其赋值为 null,否则这些元素可能既访问不到又不会被垃圾回收,从而造成内存泄漏。
本章还提到了几个弱引用相关的集合,例如 WeakHashMap、LinkedHashMap。
第 8 条:避免使用终结方法和清除方法
原文摘抄
- P29
总而言之,除非是作为安全网,或者是为了终止非关键的本地资源,否则请不要使用清除方法,对于在 Java 9 之前的发行版本,则尽量不要使用终结方法。若使用了终结方法或者清除方法,则要注意它的不确定性和性能后果。
本条总结
本条说了挺多但其实只有一点,别用这俩方法!别用这俩方法!别用这俩方法!
第 9 条:try-with-resources 优先于 try-finally
原文摘抄
- P32
结论很明显:在处理必须关闭的资源时,始终要优先考虑用 try-with-resources,而不是用 try-finally。
本条总结
本条讲了使用 try-finally 在遇到多个资源时写法的复杂、finally 中 close 可能会抛出异常然后覆盖 try 中的异常等缺点。然后给出了优先使用 try-with-resources 的结论。
第 3 章 对于所有对象都通用的方法
第 10 条:覆盖 equals 时请遵守通用约定
原文摘抄
P33
最容易避免这类问题的办法就是不覆盖 equals 方法,在这种情况下,类的每个实例都只与它自身相等。如果满足了以下任何一个条件,这就是所期望的结果:
- 类的每个实例本质上都是唯一的。
- 类没有必要提供“逻辑相等(logical equality)的测试功能。”
- 超类已经覆盖率 equals,超类的行为对于这个类也是合适的。
- 类是私有的,或者是包级私有的,可以确定它的 equals 方法永远不会被调用。
P34
equals 方法实现了等价关系(equivalence relation),其属性如下:
- 自反性
- 对称性
- 传递性
- 一致性
- 对于任何非 null 的引用值 x,x.equals(null) 必须返回 false
P36
一旦违反了 equals 约定,当其他对象面对你的对象时,你完全不知道这些对象的行为会怎么样。
P38
我们无法在扩展可实例化的类的同时,既能增加新的组件,同时又保留 equals 约定,除非愿意放弃面向对象的抽象所带来的优势。
P41
结合所有这些要求,得出了以下实现高质量 equals 方法的诀窍:
- 使用 == 操作符检查,“参数是否为这个对象的引用”。
- 使用 instanceof 操作符检查“参数是否为正确的类型”。
- 把参数转换成正确的类型。
- 对于该类中的每个“关键”(significant)域,检查参数中的域是否与该对象中对应的域相匹配。
P43
覆盖 equals 时总要覆盖 hashCode。
不要企图让 equals 方法过于智能。
不要将 equals 声明中的 Object 对象替换为其他的类型。P44
总而言之,不要轻易覆盖 equals 方法,除非迫不得已。
本条总结
本条讲了覆盖 equals 方法要注意的点,并提到了 Google 开源的 AutoValue 框架或者 IDE 可以自动生成。另外还提到了 java.sql.Timestamp 对 java.util.Date 进行了扩展,并增加了 nanoseconds 域,它的 equals 方法实现违反了对称性。还提到了 java.net.URL 的 equals 方法违反了一致性。
第 11 条:覆盖 equals 时总要覆盖 hashCode
原文摘抄
P44
在每个覆盖了 equals 方法的类中,都必须覆盖 hashCode 方法。
相等的对象必须具有相等的散列码。P46
必须排除 equals 比较计算中没有用到的任何域,否则很有可能违反 hashCode 约定的第二条。
P47
如果一个类是不可变的,并且计算散列码的开销也比较大,就应该考虑把散列码缓存在对象内部,而不是每次请求的时候都重新计算散列码。
不要试图从散列码计算中排除掉一个对象的关键域来提高性能。
本条总结
本条讲了覆盖 hashCode 的方法和实现细节以及需要注意的地方,提到了 Objects 类的 hash 方法会引发数组的创建,所以运行速度更慢一些。以及利用 AutoValue 框架和 IDE 可以自动生成。
第 12 条:始终要覆盖 toString
原文摘抄
P49
在实际应用中,toString 方法应该返回对象中包含的所有值得关注的信息。
无论是否决定指定格式,都应该在文档中明确地表明你的意图。P50
无论是否指定格式,都为 toString 返回值中包含的所有信息提供一种可以通过编程访问之的途径。
本条总结
本条讲了覆盖 toString 方法要注意的点。
第 13 条:谨慎地覆盖 clone
原文摘抄
P51
事实上,实现 Cloneable 接口的类是为了提供一个功能适当的公有的 clone 方法。
P52
不可变的类永远都不应该提供 clone 方法。
P53
实际上,clone 方法就是另一个构造器;必须确保它不会伤害到原始的对象,并确保正确地创建被克隆对象中的约束条件(invariant)。
在数组上调用 clone 返回的数组,其编译时的类型与被克隆数组的类型相同。这是复制数组的最佳习惯做法,事实上,数组是 clone 方法唯一吸引人的用法。
Cloneable 架构与引用可变对象的 final 域的正常用法是不相兼容的。P56
公有的 clone 方法应该省略 throws 声明。
P57
所有实现了 Cloneable 接口的类都应该覆盖 clone 方法。
对象拷贝的更好的办法是提供一个拷贝构造器(copy constructor)或拷贝工厂(copy factory)。P58
总之,复制功能最好由构造器或者工厂提供。这条规则最绝对例外的是数组,最好利用 clone 方法复制数组。
本条总结
本条详细讲了 Cloneable 接口、Object 类里的 clone 方法。一般只需要记住,复制对象用构造器或工厂,复制数组用 clone 方法。
第 14 条:考虑实现 Comparable 接口
原文摘抄
P58
Java 平台类库中的所有值类,以及所有的枚举类型都实现了 Comparable 接口。如果你正在编写一个值类,它具有非常明显的内在排序关系,比如按字母顺序、按数值顺序或者按年代顺序,那你就应该坚决考虑实现 Comparable 接口。
P60
由 compareTo 方法施加的等同性测试,也必须遵守相同于 equals 约定所施加的限制条件:自反性、对称性和传递性。因此,下面的告诫也同样适用:无法在用新的值组件扩展可实例化的类时,同时保持 compareTo 约定,除非愿意放弃面向对象的抽象优势。
P61
在 compareTo 方法中使用关系操作符 < 和 > 是非常烦琐的,并且容易出错,因此不再建议使用。
P63
总而言之,每当实现一个对排序敏感的类时,都应该让这个类实现 Comparable 接口,以便其实例可以轻松地被分类、搜索,以及用在基于比较的集合中。每当在 compareTo 方法的实现中比较域值时,都要避免使用 < 和 > 操作符,而应该在装箱基本类型的类中使用静态的 compare 方法,或者在 Comparator 接口中使用比较器构造方法。
本条总结
本条讲了 Comparable 接口相关内容,实现 compareTo 方法和 equals 方法的异同,并以 new BigDecimal(“1.0”) 和 new BigDecimal(“1.00”) 的 equals() 比较不相等但 compareTo() 比较相等为例说明。
第 4 章 类和接口
第 15 条:使类和成员的可访问性最小化
原文摘抄
P66
尽可能地使每个类或者成员不被外界访问。
P67
这条规则有一个特例:如果一个类实现了一个接口,那么接口中所有的方法在这个类中也都必须被声明为公有的。
公有类的实例域决不能是公有的。
包含公有可变域的类通常不是线程安全的。P68
注意,长度非零的数组总是可变的,所以让类具有公有的静态 final 数组域,或者返回这种域的访问方法,这是错误的。
P69
现在说模块将在 JDK 之外获得广泛的使用,还为时过早。同时,似乎最好不用它们,除非你的需求非常迫切。
本条总结
本条讲了尽量让类和类中的成员访问级别最小化。讲了两种使得类中私有数组不被外界修改的两种方法。
第 16 条:要在公有类而非公有域中使用访问方法
原文摘抄
P69
如果类可以在它所在的包之外进行访问,就提供访问方法。
P70
如果类是包级私有的,或者是私有的嵌套类,直接暴露它的数据域并没有本质的错误。
本条总结
简而言之,公有类永远都不应该暴露可变的域。虽然还是有问题,但是让公有类暴露不可变的域,其危害相对来说比较小。但有时候会需要用包级私有的或者私有的嵌套类来暴露域,无论这个类是可变的还是不可变的。
第 17 条:使可变性最小化
原文摘抄
P71
- 不要提供任何会修改对象状态的方法(也称为设值方法)。
- 保证类不会被扩展。
- 声明所有的域都是 final 的。
- 声明所有的域都为私有的。
- 确保对于任何可变组件的互斥访问。
P73
不可变对象本质上是线程安全的,它们不要求同步。
不可变对象可以被自由地共享。
不仅可以共享不可变对象,甚至也可以共享它们的内部信息。
不可变对象为其他对象提供了大量的构件。
不可变对象无偿地提供了失败的原子性。
不可变类真正唯一的缺点是,对于每个不同的值都需要一个单独的对象。P76
除非有很好的理由要让类称为可变的类,否则它就应该是不可变的。
如果类不能被做成不可变的,仍然应该尽可能地限制它的可变性。
除非有令人信服的理由要使域变成是非 final 的,否则要使每个域都是 private final 的。
构造器应该创建完全初始化的对象,并建立起所有的约束关系。
本条总结
本条讲了使类成为不可变要遵循的规则,不可变对象的优势和缺点,以及其缺点的解决思路,还提到了 BigInteger 和 BigDecimal 由于可以被继承覆盖类中的方法,若要保证不可变性必须进行检查。
不可变类也可以拥有非 final 域用于缓存一些昂贵的计算结果。例如 String 类中的 hashCode() 方法。
还提到了一点不可变类序列化的知识,不过现在我不太熟悉。
提到了重用对象的“重新初始化方法”与增加的复杂性相比,通常并没有带来太多的性能提升。
第 18 条:复合优先于继承
原文摘抄
P77
与方法调用不同的是,继承打破了封装性。
P80
包装类不适合用于回调框架;在回调框架中,对象把自身的引用传递给其他的对象,用于后续的调用(“回调”)。因为被包装起来的对象并不知道它外面的包装对象,所以它传递一个指向自身的引用(this),回调时避开了外面的包装对象。这被称为 SELF 问题。
这一点没太懂为啥。
本条总结
本条讲了只有当子类和超类之间确实存在子类型关系时,使用继承才是恰当的。可以用复合和转发机制来代替继承。并以 HashSet 的 addAll() 方法实现依赖 add() 方法举例说明“自用性”可能导致继承来扩展功能的问题。复合和转发机制避免了父类添加新的方法时子类不适用的情况。
第 19 条:要么设计继承并提供文档说明,要么禁止继承
原文摘抄
P81
该类必须有文档说明它可覆盖(overridable)的方法的自用性(self-use)。
P82
类必须以精心挑选的受保护的(protected)方法的形式,提供适当的钩子(hook),以便进入其内部工作中。
P83
对于为了继承而设计的类,唯一的测试方法就是编写子类。
必须在发布类之前先编写子类对类进行测试。
构造器决不能调用可被覆盖的方法。P85
无论是 clone 还是 readObject,都不可以调用可覆盖的方法,不管是以直接还是间接的方式。
为了继承而设计类,对这个类会有一些实质性的限制。
这个问题的最佳解决方案是,对于那些并非为了安全地进行子类化而设计和编写文档的类,要禁止子类化。
本条总结
看完这条,发现继承真的是被(我)滥用了,特别是覆盖方法的自用性,可能会导致很多问题。
第 20 条:接口优于抽象类
原文摘抄
P86
现有的类可以很容易被更新,以实现新的接口。
接口是定义 mixin(混合类型)的理想选择。P87
接口允许构造非层次结构的类型框架。
接口使得安全地增强类的功能成为可能。P88
实现了这个接口的类可以把对于接口方法的调用转发到一个内部私有类的实例上,这个内部私有类扩展了骨架实现类。这种方法被称作模拟多重继承。
P89
对于骨架实现类而言,好的文档绝对是非常必要的。
本条总结
因为一个类可以实现多个接口,所以接口适合作为类型定义。
Java 9 允许接口中有私有的静态方法。
总而言之,接口通常是定义允许多个实现的类型的最佳途径。如果你导出了一个重要的接口,就应该坚决考虑同时提供骨架实现类。而且,还应该尽可能地通过缺省方法在接口中提供骨架实现,以便接口的所有实现类都能使用。也就是说,对于接口的限制,通常也限制了骨架实现会采用的抽象类的形式。
第 21 条:为后代设计接口
原文摘抄
P90
在 Java 8 中,增加了缺省方法(default method)构造,目的就是允许给现有的接口添加方法。
并非每一个可能的实现的所有变体,始终都可以编写出一个缺省方法。P91
有了缺省方法,接口的现有实现就不会出现编译时没有报错或警告,运行时却失败的情况。
这句没太明白。
本条总结
本条讲了要谨慎设计接口,并围绕 Java 8 新增的缺省方法,以 Collection 接口中的缺省方法 removeIf 方法为例,讲了虽然它的实现很好,但仍然在某些实现它却没有覆盖它的类里仍然存在多线程问题。多线程相关细节我不太懂。
第 22 条:接口只用于定义类型
原文摘抄
P92
常量接口模式是对接口的不良使用。
如果这些常量与某个现有的类或者接口紧密相关,就应该把这些常量添加到这个类或者接口中。
否则,应该使用不可实例化的工具类(utility class)(详见第 4 条)来导出这些常量。P93
如果大量利用工具类导出的常量,可以通过利用静态导入(static import)机制,避免用类名来修饰常量名。
本条总结
简而言之,接口应该只被用来定义类型,它们不应该被用来导出常量。
本条还提到了 Java 7 允许数字字面量用下划线隔开以便于阅读的特性。
第 23 条:类层次优于标签类
原文摘抄
- P94
标签类过于冗长、容易出错,并且效率低下。
标签类正是对类层次的一种简单的仿效。
本条总结
本条其实可以概括为类的单一职责原则,不要用一个类来包含多个类的域和方法。
第 24 条:静态成员类优于非静态成员类
原文摘抄
- P97
如果声明成员类不要求访问外围实例,就要始终把修饰符 static 放在它的声明中。
本条总结
如果一个嵌套类需要在单个方法之外仍然是可见的,或者它太长了,不适合放在方法内部,就应该使用成员类。如果成员类的每个实例都需要一个指向其外围实例的引用,就要把成员类做成非静态的;否则,就做成静态的。假设这个嵌套类属于一个方法的内部,如果你只需要在一个地方创建实例,并且已经有了一个预置的类型可以说明这个类的特征,就要把它做成匿名类;否则,就做成局部类。
第 25 条:限制源文件为单个顶级类
原文摘抄
本条总结
结论显而易见:永远不要把多个顶级类或者接口放在一个源文件中。遵循这个规则可以确保编译时一个类不会有多个定义。这么做反过来也能确保编译产生的类文件,以及程序结果的行为,都不会受到源文件被传给编译器时的顺序的影响。
第 5 章 泛型
第 26 条:请不要使用原生态类型
原文摘抄
P103
如果使用原生态类型,就失掉了泛型在安全性和描述性方面的所有优势。
如果使用像 List 这样的原生态类型,就会失掉类型安全性,但是如果使用像 List< Object > 这样的参数化类型,则不会。P104
不能将任何元素(除了 null 之外)放到 Collection< ? > 中。
必须在类文字(class literal)中使用原生态类型。
本条总结
总而言之,使用原生态类型会在运行时导致异常,因此不要使用。原生态类型只是为了与引入泛型之前的遗留代码进行兼容和互用而提供的。让我们做个快速的回顾:Set< Object > 是个参数化类型,表示可以包含任何对象类型的一个集合;Set< ? > 则是一个通配符类型,表示只能包含某种未知对象类型的一个集合;Set 是一个原生态类型,它脱离了泛型系统。前两种是安全的,最后一种不安全。
第 27 条:消除非受检的警告
原文摘抄
P107
要尽可能地消除每一个非受监警告。
如果无法消除警告,同时可以证明引起警告的代码是类型安全的,(只有在这种情况下)才可以用一个 @SuppressWarnings(“unchecked”) 注解来禁止这条警告。
应该始终在尽可能小的范围内使用 SuppressWarnings 注解。P108
每当使用 SuppressWarnings(“unchecked”) 注解时,都要添加一条注释,说明为什么这么做是安全的。
本条总结
总而言之,非受检警告很重要,不要忽略它们。每一条警告都表示可能在运行时抛出 ClassCastException 异常。要尽最大的努力消除这些警告。如果无法消除非受检警告,同时可以证明引起警告的代码是类型安全的,就可以再尽可能小的范围内使用 @SuppressWarnings(“unchecked”) 注解禁止该警告。要用注释把禁止该警告的原因记录下来。
第 28 条:列表优于数组
原文摘抄
P109
唯一可具体化的(reifiable)参数化类型是无限制的通配符类型,如 List< ? > 和 Map< ?, ? >(详见第 26 条)。虽然不常用,但是创建无限制通配类型的数组是合法的。
P110
当你得到泛型数组创建错误时,最好的解决办法通常是优先使用集合类型 List< E >,而不是数组类型 E[]。这样可能会损失一些性能或者简洁性,但是换回的却是更高的类型安全性和互用性。
本条总结
总而言之,数组和泛型有着截然不同的类型规则。数组是协变且可以具体化的;泛型是不可变的且可以被擦除的。因此,数组提供了运行时的类型安全,但是没有编译时的类型安全,反之,对于泛型也一样。一般来说,数组和泛型不能很好地混合使用。如果你发现自己将它们混合起来使用,并且得到了编译时的错误或者警告,你的第一反应就应该是用列表代替数组。
第 29 条:优先考虑泛型
原文摘抄
本条总结
本条通过一个简单的 Stack 类说明了如何把一个类写成泛型类,并讲了泛型数组的两种处理方式,顺带提到了堆污染概念。最后提到,使用泛型比使用需要在客户端代码中进行转换的类型更加安全,也更加容易,只要时间允许,就把现有的类型都泛型化。
第 30 条:优先考虑泛型方法
原文摘抄
P116
声明类型参数的类型参数列表,处在方法的修饰符及其返回值之间。
P118
类型限制 < E extends Comparable< E > > ,可以读作“针对可以与自身进行比较的每个类 E”
本条总结
总而言之,泛型方法就像泛型一样,使用起来比要求客户端转换输入参数并返回值的方法来得更加安全,也更加容易。就像类型一样,你应该确保方法不用转换就能使用,这通常意味着要将它们泛型化。并且就像类型一样,还应该将现有的方法泛型化,使新用户使用起来更加轻松,且不会破坏现有的客户端。
第 31 条:利用有限制通配符来提升 API 的灵活性
原文摘抄
P121
为了获得最大限度的灵活性,要在表示生产者或者消费者的输入参数上使用通配符类型。
PECS 表示 producer-extends,consumer-superP122
不要用通配符类型作为返回类型。
如果类的用户必须考虑通配符类型,类的 API 或许就会出错。P123
使用时始终应该是 Comparable< ? super T > 优先于 Comparable< T >。
使用时始终应该是 Comparator< ? super T > 优先于 Comparator< T >。
如果类型参数只在方法声明中出现一次,就可以用通配符取代它。
本条总结
总而言之,在 API 中使用通配符类型虽然比较需要技巧,但是会使 API 变得灵活得多。如果编写的是将被广泛使用的类库,则一定要适当地利用通配符类型。记住基本的原则:producer-extends,consumer-super(PECS)。还要记住所有的 comparable 和 comparator 都是消费者。
第 32 条:谨慎并用泛型和可变参数
原文摘抄
P125
将值保存在泛型可变参数数组参数中是不安全的。
P126
SafeVarargs 注解是通过方法的设计者做出承诺,声明这是类型安全的。
P127
允许另一个方法访问一个泛型可变参数数组是不安全的。有两种情况例外:将数组传给另一个用 @SafeVarargs 正确注解过的可变参数方法是安全的,将数组传给只计算数组内容部分函数的非可变参数方法也是安全的。
对于每一个带有泛型可变参数或者参数化类型的方法,都要用 @SafeVarargs 进行注解。
本条总结
本条很多细节没有看懂。讲了显式创建泛型数组是非法的,但用泛型可变参数声明方法却是合法的,这是因为带有泛型可变参数或者参数化类型的方法在实践中用处很大,因此 Java 语言的设计者选择容忍这一矛盾的存在。
总而言之,可变参数和泛型不能良好地合作,这是因为可变参数设施是构建在顶级数组之上的一个技术露底,泛型数组有不同的类型规则。虽然泛型可变参数不是类型安全的,但它们是合法的。如果选择编写带有泛型(或者参数化)可变参数的方法,首先要确保该方法是类型安全的,然后用 @SafeVarargs 对它进行注解,这样使用起来就不会出现不愉快的情况了。
第 33 条:优先考虑类型安全的异构容器
原文摘抄
本条总结
本条讲真没太看懂,讲了类型令牌的概念,大概意思就是讲怎么实现一个可以把不同类型作为键的类似 Map 的容器类。