effective java 笔记
[TOC]
创建和销毁对象
1. 用静态工厂的方法代替构造器
- 返回类实例的静态方法
- 与构造器相比优势:
- 有名称(相对于参数不同的多个构造器)
- 调用时不用创建一个新对象
- 可以返回原返回类型的任何子类型对象(类似工厂模式)
- 创建参数化类型实例时代码简洁
- Map
m = new HashMap () - Map
m = HashMap.newInstance();
- Map
- 缺点:
- 使用静态工厂方法来创建对象,将构造器设为私有,不能被子类化。(鼓励使用复合,避免继承)
- 和静态方法没有区别
2. 遇到多个构造器参数时要考虑用构建器
- 构造器:
- 多参数时使用重叠构造器
- 缺点:不够灵活,同时参数多了无法阅读相应参数的含义
- javaBeans:
- 调用无参构造函数,然后调用set方法来赋值
- 缺点:构造过程分到了几个调用中,会导致线程不安全
Builder模式
- 容易编写,易于阅读
构造器或静态工厂有多个参数,考虑使用Builder
12345678910111213141516171819202122232425public class Test {private int a;public static class Builder {private int a;public Builder(int a) {this.a = a;}public Builder a(int a) {this.a = a;}public test build() {return new Test(this);}}private Test(Builder builder) {a = builder.a;}}获取实例: Test test = new Test.Builder(1).a(1).build();
3. 用私有构造器或者枚举类型强化Singleton属性
|
|
4. 不希望被实例化的类(工具类等),使用私有的构造器来防止被实例化。(不能子类化)
5. 避免创造不必要的对象
- 尽量重用对象
- 对于不可变类或已知不会修改的可变对象,静态工厂方法优于构造器,构造器每次都创建新对象。
- 优先使用基本类型而不是装箱基本类型(自动装箱有一定的成本)
6. 消除过期对象引用
- 防止内存泄露(无意识的对象保持)
- 对象引用已经过期,手动指定为null
- 当缓存项生命周期由外部引用决定时,考虑使用WeakHashMap实现缓存。(没有引用时自动删除)
- 清除缓存中老的对象,使用LinkedHashMap.removeEldestEntry方法在添加新对象时进行清理。
- 监听器和其他回调也是内存泄露的来源,通过保存弱引用来避免。
7. 避免使用终结方法(finalizer)
- 问题:finalizer不能保证被及时的执行
- 替换方案:通过显示的终止方法来替代finalizer(try-finally)
- finalizer可以充当安全网,忘记显示终止时,保证资源的释放(不建议依赖,建议修改代码)
所有对象都通用的方法
非final的Object方法相关的约定
8. equals的通用约定
- 尽量避免覆盖equals方法
- 不需要覆盖equals方法的情况:
- 类的实例本质上都是唯一的,如thread,枚举
- 类不需要提供逻辑相等的功能,如Random
- 超类覆盖了equals方法,且对子类适用
- equals不会被调用,类私有或是包级私有
- 值类的情况下需要覆盖equals方法。(未满足不需要覆盖的情况)
- 覆盖equals方法需要遵守的约定:
- 自反性(reflexive):x.equals(x) == true
- 对称性(symmetric):x.equals(y) == true && y.equals(x) == true
- 传递性(transitive):x.equals(y) == true && y.equals(z) == true ==> x.equals(z) == true
- 一致性(consistent):多次调用 x.equals(y) 都为true
- x非空,x.equals(null) == false
- 实现高质量equals的原则:
- 使用==操作符检查“参数是否为这个对象的引用”
- 使用instanceof操作符检查参数是否为正确的类型。
- 参数转换成正确的类型进行比较
- 保证每个关键域都匹配
- 编写的equals保证对称性,传递性,一致性
- 其他告诫:
- 覆盖equals同时覆盖hashCode
- 不要企图让equals过于智能
- 不要把equals声明中的Object对象替换为其他的类型
9. 覆盖equals时同时覆盖hashCode
- 覆盖hashCode的原则:
- 如果equals用到的信息没有被修改,多次调用hashCode返回同一个整数
- equals相等的对象必须具有相等的hashCode(防止出现equals相等hashCode不相等)
- equals不相等的对象不一定要返回不相等的hashCode,但是尽量保证不同能提高hash table的性能(hashmap 使用hashCode生成key)
10. 始终覆盖toString
- toString方法返回对象中值得关注的信息,更容易阅读
11. 谨慎覆盖clone
- clone不调用构造器,生成出来的对象不能对原对象产生影响(deep clone)
- 实现Cloneable接口的类都应该用一个公有方法覆盖clone,此方法先调用super.clone,然后修正部分域(deep clone)。
- 尽量避免覆盖clone,拷贝对象应采用deep clone。
12. 考虑实现comparable接口
- 使实例具有被排序的能力(集合实现排序依赖)
类和接口
13. 使类和成员的可访问性最小化
- 封装:隐藏实现细节,API与实现隔离开,模块间只通过API通信。有效解除各模块之间的耦合。
- 访问控制机制:
- 尽可能使每个类或者成员不被外界访问,明确成员的最小访问级别(私有,包级私有,受保护(子类+包内访问),公有)
- 实例域不能是公有的。
- 静态final域可以设置为公有来暴露常量,该域不能指向可变对象的引用。
14. 公有类中使用访问方法而非公有域
- 公有类不改暴露可变的域,暴露不可变的域危害小。
15. 使可变性最小化
- 多使用不可变类 = 实例不能被修改,包含信息创建时提供。
- 不可变类遵循规则:
- 不提供修改对象状态的方法
- 类不能被扩展,子类化
- 所有域都是final的
- 所有域都私有
- 确保对任何可变组件的互斥访问
16. 复合优先于继承
- decorator模式
- 继承打破了封装性,子类依赖超类,超类修改可能会使子类异常。只有当子类和超类之间确实存在子类型的关系时,才适用继承。
- 同个包内合理的设计,来通过继承是比较安全的(但仍然有风险)。跨包继承则很危险。
- 复合:新的类中增加一个私有域,引用现有类的一个实例,调用该实例的方法来进行转发。尽量用复合和转发来替代继承。
17. 要么为继承而设计,并提供文档说明,要不就禁止继承
18. 接口优于抽象类
- 抽象类允许包含某些方法的实现,接口则不允许。
- 接口是定义混合类型的理想选择,允许我们构造费层次结构的类型框架。接口使得安全地增强类的功能成为可能。
- 设计公有接口要谨慎,因为一旦被广泛使用,再想改变接口几乎是不可能的(会导致所有实现类编译出错)
- 提供接口的同时,考虑同时提供骨架实现类(类似AbstractCollection)
19. 接口只用于定义类型
- 接口应该只被用来定义类型,不应该用来导出常量。
- 使用枚举类或者不可实例化的工具类来导出常量。
20. 类层次优于标签类
- 标签类:多种不同风格的实现通过枚举挤在单个类中。如一个图案类既可以是圆也可以是正方形。标签类过于冗长、容易出错,并且效率低下。
- 不使用标签类,通过抽象转换为类层次结构。
21. 用函数对象表示策略
- 使用策略模式
22. 优先考虑静态成员类
- 嵌套类:定义在一个类内部的类,为了给外围类提供服务。分为静态成员类,非静态成员类,匿名类,局部类。
- 声明成员类不要求访问外围实例时,使用静态成员类,否则使用非静态成员类。
- 匿名类常用来创建函数对象(策略模式),过程对象(Runnable、Thread),静态工厂方法(18条)
泛型
(未很好理解)
23. 不要使用原生态类型
- 原生态类型没有类型安全性检查,表述性也不如泛型。尽量使用泛型,避免是哟经原生态类型。
24. 消除非受检警告
- 尽可能消除每个非受检警告(unchecked warnings),确保代码的类型安全。
- 对于无法消除警告,但是可以证明引起警告的代码是类型安全的,可以用@SuppressWarnings(“unchecked”)注解来禁止该条警告。尽可能在小范围中使用SuppressWarnings注解。每次使用时,都要添加注释,说明为什么是安全。
25. 列表优先于于数组
26. 优先考虑泛型
- 通过将对象泛型化来强化对象。
- 参考stack的实现
27. 优先考虑泛型方法
- 静态工具方法适合泛型化,如Collections包含的算法方法。
28. 利用有限制通配符来提升API的灵活性
29. 优先考虑类型安全的异构容器
- 使用Class
作为key,构建一个value值是不同类型的类map类。
枚举和注解
30. 用enum代替int常量
- 有多种不同类型的int枚举常量时,使用起来可能会用混,而enum可以避免这个问题。
- 枚举是final和单例的。枚举类型允许添加任意的方法和域,并实现任意的接口。(参考RoundingMode)
31. 用实例域代替序数
- 避免使用ordinal方法获取序数,而是增加实例域用来保存枚举值关联的值。
- ordinal方法仅在设计像EnumSet和EnumMap这样的类的时候才使用。
32. 用EnumSet代替位域(bit field)
- 位域可读性差
- EnumSet 内部也是采用位运算的方式实现
33. 用EnumMap代替序数索引
- 不要用序数来索引数组,使用EnumMap(不要使用ordinal)
34. 用接口模拟可伸缩的枚举
- 枚举实现接口
35. 多使用注解
36. 坚持使用Override注解
- Override只用在方法声明中,表示被注解的方法声明覆盖了超类型中的一个声明
- 对要覆盖超类声明的每个方法声明都必须使用Override注释(覆盖抽象方法时除外)
37. 用标记接口定义类型
- 标记接口,没有方法声明的接口,只是为了指明一个类具有某种属性(Serializable)
- 标记接口用于定义类型,标记注解用来给注解元素添加更多信息。
方法
38. 检查参数的有效性
- 方法或构造器的参数限制写在文档中,同时进行显式的检查,尽量减少因参数不符合限制而产生的异常。
39. 必要时进行保护性拷贝
- 对于构造器的每个可变参数进行保护性拷贝。
40. 谨慎设计方法前面
- 方法名称易于理解
- 不要提供太多方法,提供功能齐全方法为主,有常用的情况才提供快捷方式
- 参数列表不要太长,考虑最多4个
- 参数类型优先使用接口
- 两个元素的枚举类型优于boolean参数
41. 慎用重载
- 永远不要导出两个具有相同参数数目的重载方法
42. 慎用可变参数
- int… args(通过创建一个数组来传递参数列表)
43. 返回零长度的数组或者稽核,而不是null
- 返回类型为数组或集合的方法没理由返回null,避免上层还需要对null做额外的处理
44. 为所有导出的API元素编写文档注释
- 在每个被导出的类、接口、构造器、方法和域声明之前增加文档注释
- 注释简洁描述出它和客户端之间的约定。(条件,副作用)
通用程序设计
45. 局部变量作用域最小化
- 第一次使用的地方声明局部变量
- 方法小而集中
46. for-each循环优先于传统的for循环
- 传统的for循环迭代器和索引变量出现多次,可能会出现编译器无法发现的错误。相对于for-each有性能损失。(for-each对数组索引的边界值只计算一次)
- for-each不适用的情况:
- 过滤:遍历删除选定元素
- 转换:部分元素值进行操作
- 平行迭代:并行遍历多个集合
47. 了解和使用类库
- 熟悉类库以及新特性,不要重复造轮子。
48. 需要精确答案时,避免使用float和double
- float和double不提供精确结果,使用BigDecimal、int、long进行精确计算
- BigDecimal使用不便,性能不如基本运算类型。性能敏感的功能中,考虑把小数转换成int或者long进行处理。
49. 基本类型优于装箱基本类型
- 基本类型(int,double,boolean)
- 引用类型(String,List,基本类型对应的引用类型 = 装箱基本类型,Double等)
- 当操作中混合使用基本类型和装箱基本类型时,装箱基本类型会自动拆箱,避免自动拆箱导致的NullPointerException
- 装箱基本类型使用场景:集合中的元素、键和值;泛型;反射的方法调用
50. 如果有其他类型更适合,避免使用字符串
- 数值,枚举,含多个属性的实体不要用string进行代替
51. 当心字符串连接的性能
- 连接字符串使用StringBuilder
52. 通过接口引用对象
- 面向接口编程,是程序更加灵活,可以方便的更换实现,而不修改其他代码。
53. 接口优先于反射机制
- 反射:通过程序来访问类的信息,缺点:
- 丧失了编译时类型检查的好处
- 执行反射访问所需要的代码非常笨拙和冗长
- 性能损失
- 应用程序运行时不应该以反射方式访问对象
- 通过反射创建实例(构造器不带参数的情况可以直接使用Class.newInstance)
54. 谨慎地使用本地方法(native method)
55. 谨慎的进行优化
- 编写好的程序而不是快的程序
- 设计系统时考虑性能
- 依赖性能测量来进行优化
56. 遵守普遍接受的命名管理
异常
57. 只针对异常的情况才使用异常
- 异常不应该用于正常的控制流
58. 对可恢复的情况使用受检异常,对编程错误使用运行时异常
- 受检异常(checked exception):调用者能够适当地恢复时使用该类异常,要求继续抛出或者进行捕获
- 非受检异常:不需要也不应该被捕获的可抛出异常,可以不处理
- 运行时异常(run-time exception)
- 错误(error)
59. 避免不必要的使用受检异常
- 会抛出受检异常的API使用起来不方便,需要额外的代码。
- 把受检异常变成非受检异常
60. 优先使用标准的异常
61. 抛出与抽象相对应的异常
- 异常转译:高层的实现应该捕获低层的异常,同时抛出可以按照高层抽象进行解释的异常
- 最好的方法是避免低层抛出异常,或者只记录日志而不抛出异常,无法处理低层异常时才通过异常转译抛给高层。
62. 每个方法抛出的异常都要有文档
- 单独地声明受检异常,用@throws标记记录抛出异常的条件
- 用@throws记录每个可能抛出的未受检异常,不要使用throws关键字将未受检的异常包含在方法的声明中。
63. 在细节消息中包含能捕获失败的信息
- 异常的细节包含异常相关的参数和域的值
64. 努力使失败保持原子性
- 失败原子性:失败的方法调用应该使对象保持在被调用之前的状态。
65. 不要忽略异常
- 空的catch来忽略异常
并发(阅读完java并发编程后再阅读)
序列化
将对象编码成字节流,并从字节流编码中重新构建对象。
74. 谨慎地实现Serializable接口
- 实现Serializable接口带来的问题:
- 一旦类被发布,会降低改变类实现的灵活性。(新旧序列化形式不兼容)
- bug和安全性。反序列化是隐藏的构造器,会破坏对象的约束关系。(单例)
- 测试负担增加,新旧兼容测试
- 为了继承而设计的类尽可能少的去实现Serializable接口,用户的接口也应该少的继承Serializable接口。减少扩展的负担。
- 内部类不应该实现Serializable