<Effective Java> 读书笔记
Table of Contents
第2章 创建和销毁对象 (1 - 7)
第1条: 考虑用静态工厂方法代替构造器
优点 1. 有名称(方法名), 可以确切地描述正被返回的对象 2. 不必在每次调用它们的时候都创建一个新对象, 例如 Boolean.valueOf(boolean) 3. 可以返回类型的任何子类型的对象 4. 在创建忽视类型实例的时候, 代码可以更简单, 例如 new ArrayList<Integer>() (1.8已改进) 缺点 1. 类如果不含公有或受保护的构造器, 就不能被子例化 2. 跟其他的静态方法实际上没有任何区别 静态方法的一些惯用名 - valueOf 类型转换 - of valueOf的简洁写法, 在EnumSet中流行 - getInstance 返回实例 - newInstance 保证返回的是不同的实例 - getType 在不同类中使用, Type表示返回的对象类型 - newType 在不同类中使用, Type表示返回的对象类型
第2条: 遇到多个构造器参数时要考虑用构造器
设计模式中的: Builder模式
第3条: 用私有构造器或者枚举类型强化 Singleton 属性
单元素的枚举类型已经成为实现 Singleton 的最佳方法
第4条: 通过私有构造器强化不可实例化的能力
如果类不允许被实例化, 需要显式的声明, 例如 Math public final class Math { /** Don't let anyone instantiate this class. */ private Math() {} ... }
第5条: 避免创建不必要的对象
当应该重用现有对象时, 就不要创建新的对象
第6条: 消除过期的对象引用
清空对象引用应该是一种例外, 而不是一种规范行为 只要类是自己管理内存, 就应该警惕内存泄漏问题
第7条: 避免使用终结方法
(如果不是看这本书, 我都不知道 Object.finalize() 方法) 不要依赖终结方法释放重要资源, 因为无法保证该方法一定会被调用 两种合理用法 1. 作为保护网, 比如 FileInputStream 中的 finalize() 检查用户有没有显示的调用close() 2. 终止非关键的本地资源
第3章 对于所有对象都通用的方法 (8 - 12)
第8条: 覆盖 equals 时请遵守通用约定
不覆盖 equals 的情况 - 类的每个实例在本质上都是唯一的, 例如: Thread, Object - 不关心类是否提供了“逻辑相等”的测试功能 - 超类已经覆盖了 equals, 从超类继承过来的行为对于子类也是合适的 - 类是私有的或是包级私有的, 可以确定它的equals方法永远不会被调用 equals 的等价关系 自反性: x.equals(x) == true 对称性: if x.equals(y) == true, then y.equals(x) == true 传递性: if x.equals(y) == true, y.equals(z) == true, then x.equals(z) == true 一致性: 如果对象中比较的值没有修改, 那么多次调用的结果一样 非空性: x.equals(null) == false 实现高质量 equals 的诀窍 1. 使用 == 操作符检查“参数是否为找个对象的引用”, 如果是, 返回 true 2. 使用 instanceof 操作符检查“参数是否为正确的类型”, 如果不是, 返回 false 3. 把参数转换为正确的类型 4. 以此对比该类中的关键域, 检查参数中的域是否与该对象中对应的域相匹配 5. 问自己3个问题: 是否对称? 是否传递? 是否一致?
第9条: 覆盖 equals 时总要覆盖 hashCode
相等的对象必须具有相等的散列码(在覆盖了 equals 方法的类中, 也必须覆盖 hashcode 方法) // 摘自 String.hashCode(), 注意要选取一个质数 int h = 0; char val[] = value; for (int i = 0; i < value.length; i++) { h = 31 * h + val[i]; } hash = h;
第10条: 始终要覆盖 toString
建议所有的子类都覆盖这个方法; 这个方法应该返回对象中包含的所有值得关注的信息 如果指定了格式, 最好再提供一个相匹配的静态工厂或构造器
第11条: 谨慎地覆盖 clone
最好不要覆盖这个方法, 也不要调用它(除非要拷贝数组) 如果一定要实现对象拷贝: 请实现一个拷贝构造器或拷贝工厂(转换构造器或转换工厂)
第12条: 考虑实现 Comparable 接口
如果正在编写一个值类, 最好实现Comparable接口 实现条件: 1. x.compareTo(y) == -y.compareTo(x) 2. x.compareTo(y) > 0 && y.compareTo(z) > 0, 则: x.compareTo(z) > 0 3. x.compareTo(y) == 0, 则: x.compareTo(z) == y.compareTo(z) 4. (建议) (x.compareTo(y) == 0) == (x.equals(y))
第4章 类和接口 (13 - 22)
第13条: 使类和成员的可访问性最小化
尽可能地使每个类或者成员不被外界访问. 设计良好的模块, 把它的API与它的实现清晰的隔离开. 模块之间通过它们的API通信, 一个模块不需要知道其他模块内部工作情况.
第14条: 在公有类中使用访问方法而非共有域
如果类可以在它所在的包的外部进行访问, 就提供访问方法. 如果类是包级私有的, 或者是私有的嵌套类, 直接暴露它的数据域并没有本质的错误
第15条: 使可变性最小化
不可变类的优点: 1. 不可变对象本质上是线程安全的, 它们不要求同步. 2. 不仅可以共享不可变对象, 甚至也可以共享它们的内部信息. 3. 不可变对象为其他对象提供了大量的构建 不可变类的缺点: 对于每个不同的值都需要一个单独的对象 使类成为不可变, 要遵循的规则: 1. 不要提供任何会修改对象状态的方法. 2. 保证类不会被扩展. 3. 使所有的域都是 final 的. 4. 是所有的域都成为私有的. 5. 确保对于任何可变组件的互斥访问 如果要对一个不可变类进行复杂的多阶段操作, 最好的方法是提供一个可变配套类, 如 String 与 StringBuilder
第16条: 复合优先于继承
只有当子类真正是超类的子类型时, 才适合用继承. 在包内使用继承是安全的, 通常情况, 一个包内的子类和超类的实现是在同一个程序员的控制下 跨包继承是危险的操作, 1:不清楚内部细节 2:后续如果超类升级, 子类也会受影响 为了解决跨包继承的问题, 可以使用一种叫做"复合"的设计, 实现一个转发类, 然后继承这个转发类. 复合: 不用扩展现在的类, 而是在新的类中增加一个私有域, 它引用现有类的一个实例. 转发方法: 新类中每个实例方法都可以调用被包含的现有类实例中对应的方法, 并返回它的结果.
第17条: 要么为继承而设计, 并提供文档说明, 要么就禁止继承
为继承而设计的类, 必须要有文档说明它可覆盖的方法的自用性. 为了继承而设计的类, 唯一的测试方法就是编写子类, 测试通过才可以发布类. 构造器不能调用可被覆盖的方法
第18条: 接口优于抽象类
- 现有的类可以很容易被更新, 以实现新的接口 - 接口是定义 mixin 的理想选择 - 接口允许我们构造非层次结构的类型框架 为重要的接口提供一个抽象的骨架实现类, 把接口和抽象类的优点结合起来. 接口的作用仍然是定义类型, 但骨架实现类接管了所有与接口实现相关的工作
第19条: 接口只用于定义类型
如果要导出常量, 应该用工具类代替常量接口
第20条: 类层次优于标签类
(没啥好说的, 基本操作)
第21条: 用函数对象表示策略
函数指针的主要用途就是实现策略模式
第22条: 优先考虑静态成员类
嵌套类: 静态成员类, 非静态成员类, 匿名类, 局部类 静态成员类: 外围类是类本身. 常见用法作为公有辅助类 非静态成员类: 外围类是类的实例, 常见用法定义一个 Adapter
第5章 泛型 (23 - 29)
术语: List<String> 参数化类型 String 实际类型参数 List<E> 泛型 E 形式类型参数 List<?> 无限制通配符类型 List 原生态类型 <E extends Number> 有限制类型参数 <T extends Comparable<T>> 递归类型限制 List<? extend Number> 有限制通配符类型 static <E> List<E> asList(E[] a) 泛型方法 String.class 类型令牌
第23条: 请不要在新代码中使用原生态类型
使用原生态类型的缺点: 编译器不会对参数进行类型检查
第24条: 消除非受检警告
如果无法消除警告, 同时可以证明引起警告的代码是类型安全的. 可以用@SuppressWarnings("unchecked")注解来禁止这条警告, 使用时应该始终在尽可能小的范围中(永远不要使用在整个类上), 同时再添加一条注释, 说明为什么这么做是安全的.
第25条: 列表优于数组
如果代码有错误, 用数组时, 运行时发现; 用列表时, 编译器发现. 绝大多数时, 请用列表代替数组
第26条: 优先考虑泛型
public class Stack<E> { ... }
第27条: 优先考虑泛型方法
public static <E> Set<E> union(Set<E> s1, Set<E> s2) { ... }
第28条: 利用有限制通配符来提升API的灵活性
PECS = producer-extends, consumer-super 注: comparable, comparator 都是 consumer Collection<? extend E> or Consumer<? super E> Iterable<E> 可以理解为: E的Iterable接口 Iterable<? extend E> 可以理解为: E的某个子类型的Iterable接口 这两种写法后者更好. 因为在这个方法中, 你无须 形式类型参数 是什么 public static <E> void swap(List<E> list, int i, int j); public static void swap(List<?> list, int i, int j);
第29条: 优先考虑类型安全的异构容器
异构容器的实现方式: Map<Class<?>, Object> map; 类的类型转换, 优先用下面的方法 public class Class<T> { public <U> Class<? extends U> asSubclass(Class<U> clazz); public T cast(Object obj); ... }
第6章 枚举和注解 (30 - 37)
第31条: 用实例域代替序数
永远不要根据枚举的序数导出与它关联的值, 而是要将它保存在一个实例域中. 所有枚举都一个 ordinal() 方法, 它返回每个枚举常量在类型中的数字位置. 但大多数情况都用不到这个方法.
第32条: 用EnumSet代替位域
EnumSet的底层是long存储的, 所以性能上跟位域是差不多的, 而且封装了大部分的方法. --- old --- int STYLE_BOLD = 1 << 0; // 1 int STYLE_ITALIC = 1 << 1; // 2 int STYLE_UNDERLINE = 1 << 2; // 4 text.applyStyles(STYLE_BOLD | STYLE_ITALIC); --- new --- enum STYLE {BOLD, ITALIC, UNDERLINE}; text.applyStyles(EnumSet.of(Style.BOLD, Style.ITALIC);
第33条: 用EnumMap代替序数索引
如果要用到形如 "Set<E>[]" 或 "E[][]" 的结构, 请用EnumMap代替.
第34条: 用接口模拟可伸缩的枚举
少用这种模式, 除了这样的枚举类型: 它的元素表示在某种机器上的操作(比如操作码)
第35条: 注解优于命名空间
注解的功能要比命令空间强大的多, 所以一定要用注解代替命名空间
第36条: 坚持使用Override注解
在你想要覆盖超类声明的每个方法声明中使用Override注解 1. 如果确定要覆盖父类的方法时, 加上注解, 则编译器会帮你"检查错误" 2. 如果不小心写了一个方法, 恰好跟父类的方法一样, 则编译器会提醒你"无意识的覆盖"
第37条: 用标记接口定义类型
如果想要定义类型, 一定要使用接口
第7章 方法 (38 - 44)
第38条: 检查参数的有效性
每当编写方法或者构造器的时候, 应该考虑它的参数有哪些限制. 应该把这些限制写到文档中, 并在方法体的开头,通过显式的检查来实施这些限制. 导出的方法可以抛出异常, 内部的方法可以使用断言.
第39条: 必要时进行保护性拷贝
如果类具有从客户端得到或者返回到客户端的可变组件, 类就必须保护性地拷贝这些组件.
第40条: 谨慎设计方法签名
- 谨慎地选择方法的名称 - 不要过于追求提供便利的方法 - 避免过长的参数列表 1. 把方法拆分成多个方法 2. 创建辅助类来保存参数的分组, 通常用静态成员类 3. 参数采用Builder模式 对于参数类型, 优先使用接口而不是类 对于 boolean 参数, 要优先使用两个元素的枚举类型
第41条: 慎用重载
对于重载方法的选择是静态的, 而对于被覆盖的方法的选择则是动态的 安全而保守的策略是, 永远不要导出两个具有相同参数数目的重载方法 反例: List<Integer> list; list.remove(1) 与 list.remove((Integer)1)
第42条: 慎用可变参数
必要时, 可以考虑 1个参数和多个, 而不是0个和多个 重视性能的情况下, 每个重载方法带有0至3个普通参数, 当参数的数目超过3个时, 就使用一个可变参数方法
第43条: 返回零长度的数组或者集合, 而不是null
返回类型为数组或者集合的方法没理由返回null, 而是返回一个零长度的数组或者集合.
第44条: 为所有导出的API元素编写文档注释
为了正确地编写API文档, 必须在每个被导出的类, 接口, 构造器, 方法和域声明之前增加一个文档注释 方法的文档注释应该简洁地描述出它和客户端之间的约定 同一个类或者接口中的两个成员或者构造器, 不应该具有同样的概要描述 为泛型或者方法编写文档时, 确保要在文档中说明所有的类型参数 为枚举类型编写文档时, 要确保在文档中说明常量 为注解类型编写文档时, 要确保在文档中说明所有成员
第8章 通用程序设计 (45 - 56)
第45条: 将局部变量的作用域最小化
要使局部变量的作用域最小化, 最有力的方法就是在第一次使用它的地方声明.
第46条: for-each循环优先传统的for循环
实现 Iterable 接口的对象都可以用for-each循环.
第47条: 了解和使用类库
不要重复造轮子, 如果一个比较常用的功能, 优先去标准库里找. 好处: 1. 完整的测试, 发行, 超多人的使用, 工业标准, 性能最优. 2. 查看源码可以学习主流的编码风格
第48条: 如果需要精确的答案, 请避免使用float和double
1. 使用 BigDecimal, 比较繁琐, 好处是可以完全控制舍入. 2. 使用 int 或 long, 优先使用这条.
第49条: 基本类型优先于装箱基本类型
当在一项操作中混合使用基本类型和装箱类型时, 装箱类型会自动拆箱(有风险, 比如空指针). 应当只有在集合中才使用装箱类型.
第50条: 如果其他类型更合适, 则尽量避免使用字符串
不要用字符串代替其他类型(如int, boolean, enum).
第51条: 当心字符串连接的性能
因为字符串是不可变的, 所以当两个字符串连接时(+), 它们的内容都要被拷贝(性能问题). 当需要大量连接的时候, 用 StringBuilder(非同步) / StringBuffer(同步) 代替 String
第52条: 通过接口引用对象
Vector<Subscriber> subscribers = new Vector<Subscriber>(); // Bad List<Subscriber> subscribers = new Vector<Subscriber>(); // Good, 更灵活 ... = new ArrayList<Subscriber>(); // 易于更换实现
第53条: 接口优先于反射机制
反射缺点: 1. 丧失了编译时类型检查的好处 2. 执行反射访问所需要的代码非常笨拙和冗长 3. 性能损失 反射好处: 访问编译时无法获取的类
第54条: 谨慎的使用本地方法
使用本地方法来提高性能的做法不值得提倡
第55条: 谨慎地进行优化
在设计之初就要考虑性能问题 要努力编写好的程序而不是快的程序
第56条: 遵守普遍接受的命名惯例
几个例子 Package com.google.inject, org.joda.time.format Class or Interface Timer, FutureTask, LinkedHashMap, HttpServlet Method or Field remove, ensureCapacity, getCrc Constant Field MIN_VALUE, NEGATIVE_INFINITY Local Variable i, xref, houseNumber Type Parameter T, E, K, V, X, T1, T2
第9章 异常 (57 - 65)
第57条: 只针对异常的情况才使用异常
异常应该只用于异常的情况下, 它们永远不应该用于正常的控制流
第58条: 对可恢复的情况使用受检异常, 对变成错误使用运行时异常
对可恢复的情况, 使用受检异常; 对于程序错误, 使用运行时异常(RuntimeException 的子类)
第59条: 避免不必要地使用受检的异常
受检的异常, 会给调用者带来额外的负担(需要编写try-catch块)
第60条: 优先使用标准的异常
优先熟悉并使用标准库, 没什么好说的. 常用的异常: IllegalArgumentException 非null的参数值不正确 IllegalStateException 对于方法调用而言, 对象状态不合适 NullPointerException 在禁止使用null的情况下参数值为null IndexOUtOfBoundsException 下标参数值越界 ConcurrentModificationException 在禁止并发修改的情况下, 检测到对象的并发修改 UnsupportedOperationException 对象不支持用户请求的方法
第61条: 抛出与抽象相对应的异常
异常转译: 更高层的实现应该捕获低层的异常, 同时抛出可以按照高层抽象进行解释的异常 异常链: 在异常转译的基础上, 低层异常作为参数构造出高层异常
第62条: 每个方法抛出的异常都要有文档
永远不要声明一个方法 "throws Exception" 或 "throws Throwable" 只有受检的异常才包含在方法的声明中
第63条: 在细节消息中包含能捕获失败的信息
为了捕获失败, 异常的细节信息应该包含所有 "对该异常有贡献" 的参数和域的值 例如: java.lang.IndexOutOfBoundsException的异常细节只有越界的index 更好的异常细节应该包含: 上界, 下界, index
第64条: 努力使失败保持原子性
定义: 失败的方法调用应该使对象保持在被调用之前的状态 (以下方法难度递增) 1. 设计不可变对象 2. 先对参数进行检查 3. 调整计算处理过程的顺序, 可能失败的计算在修改对象的之前执行 4. 编写一段恢复代码 5. 在对象的一份临时拷贝上执行, 操作成功之后用临时拷贝的结果代替对象的内容
第65条: 不要忽略异常
要相信API的设计者声明一个方法将抛出某个异常的时候, 他们一定正在试图说明某些事情, 不要忽略它! 捕获一个异常的时候, 一定不要用空的catch块, 至少加个注释说明为什么这样做.
第10章 并发 (66 - 73)
第66条: 同步访问共享的可变数据
当多个线程共享可变数据的时候, 每个读或写数据的线程都必须执行同步. 基本的方式有: synchronized, volatile, AtomicXXX
第67条: 避免过度同步
在同步区内尽量少调用外部方法 在同步区内做尽可能少的工作(获得锁, 检查共享数据, 根据需要转换数据, 释放锁) 如果在内部同步了类, 可以使用分拆锁/分离锁/非阻塞来控制并发度
第68条: executor 和 task 优于线程
以前 Thread 是即充当工作单元, 又是执行机制, 现在工作单元和执行机制是分开的. 工作单元: 也称为 task, 有两种 Runnable(无返回值) 和 Callable(有返回值) 执行机制: executor service 由于有不同的 executor, 所以这种方式极大的提高了灵活性 通常用 Executors 来创建 Executor 建议用 ScheduledThreadPoolExecutor 代替 Timer
第69条: 并发工具优于 wait 和 notify
由于JDK1.5新增了并发工具, 所以没有理由在新代码中使用 wait 和 notify 如果维护在使用 wait 和 notify 的代码, 一定要用标准模式的 while 循环调用 wait 一般情况, 应该优先使用 notifyAll, 而不是使用notify 并发工具分成三类: Executor Framework, 并发集合, 同步器
第70条: 线程安全性的文档化
当一个类的实例或者静态方法被并发使用的时候, 一定要看文档如果描述其行为的并发程度 每个类都应该清楚地在文档中说明它的线程安全属性 几种线程安全级别: - 不可变的. 如: String, Long - 无条件的线程安全. 如: Random, ConcurrentHashMap - 有条件的线程安全. 如: Collections.synchronized包装返回的集合, 他们的迭代器要求外部同步 - 非线程安全. 如: ArrayList, HashMap - 线程对立的. 非常少见, 可以忽略
第71条: 慎用延迟初始化
正常的初始化要优先于延迟初始化. 除非绝对必要, 否则不要用延迟初始化 延时化技术 静态域: 使用 lazy initialization holder class 模式 private static class FieldHolder { static final FieldType type = computeFielValue(); } static FieldType getField() { return FieldHolder.field; } 实例域: 使用双重检查模式, 如果可以接受重复初始化, 可以使用单重检查模式 注意实例域一定要声明成 volalite
第72条: 不要依赖于线程调度器
任何依赖于线程调度器来达到正确性或者性能要求的程序, 很有可能都是不可以移植的 作为推论, 不要依赖 Thread.yield 或者线程优先级, 它们仅仅是对调度器做些暗示
第11章 序列化 (74 - 78)
第74条: 谨慎地实现 Serializable 接口
代价1: 一旦一个被发布, 就大大降低了"改变这个类的实现"的灵活性 代价2: 它增加了出现 Bug 和安全漏洞的可能性 代价3: 随着类发行新的版本, 相关的测试负担也增加了 实现 Serializable 接口并不是一个很轻松就可以做出的决定 为了继承而设计的类应该尽可能少地去实现 Serializable 接口, 用户的接口也应该尽可能少地继承 Serializable 接口 为了继承而设计的不可序列化的类, 应该考虑提供一个无参数构造器
第75条: 考虑使用自定义的序列化形式
如果一个对象的物理表示法等同于它的逻辑内容, 可能就适用于使用默认的序列化形式 private void writeObject(ObjectOutputStream s) throws IOException; private void readObject(ObjectInputStream s) throws IOException, ClassNotFoundException;
第76条: 保护性地编写readObject方法
对于对象引用域必须保持为私有的类, 要保护性地拷贝这些域中的每个对象. 不可变类的组件就属于这一类别 对于任何约束条件, 如果检查失败, 则抛出一个InvalidObjectException异常. 这些检查动作应该跟在所有的保护性拷贝之后 如果整个对象图在被反序列化之后必须进行验证, 就应该使用ObjectInputValidation接口 无论是直接方式还是间接方式, 都不要调用类中任何可被覆盖的方法
第77条: 对于实例控制, 枚举类型优先于readResolve
ANY-ACCESS-MODIFIER Object readResolve() throws ObjectStreamException;
第78条: 考虑用序列化代理代替序列化实例
ANY-ACCESS-MODIFIER Object writeReplace() throws ObjectStreamException;