Effective Java 读书笔记(1-18)

原创
2018/04/07 21:21
阅读数 195

1. 使用静态工厂方法取代构造函数。

  1. 可以限制类的创建,尤其是单例类,以提高性能,减少内存占用。
  2. 可以提供有意义的创建名称,有一些约定的名字,如 String.valueOf(),Connection.getInstance()。
  3. 可以返回子类对象。
  4. 缺点,静态工厂方法本身只是一个静态方法,除非遵守约定,否则使用时可能会有理解问题。

2. 将单例类的构造函数私有化。

注意:如果单例需要序列化,请在类中添加 readResolve 方法,因为默认的反序列化会返回一个新对象,这样就不是单例了。

// readResolve method to preserve singleton property
private Object readResolve() throws ObjectStreamException {
  return INSTANCE; // 返回单例对象
}

3. 我们有时候会编写一些工具类Util,其中提供的全部都是静态方法,请将这一类类的构造函数设置为private,或protected(如果需要继承)。总而言之,减少不需要实例化的类被实例化的可能,减少对象数量。

4. 避免创建重复对象

  1. 不需要 new String("str"),因为 "str" 本身就是一个String对象,这一条语句其实新建了两个String对象。
  2. 如果对象提供静态工厂方法(见1),则尽量优先使用静态工厂方法,静态工厂方法中可能提供对象的缓存,但使用构造函数则总会新建一个对象。(如果有其他方法新建对象,则尽可能的不要使用 new 关键字)
  3. 可以通过延迟初始化来减少对象的新建。
  4. 尽管本条说的是尽可能重用对象而不是重建,但请尽量不要自己专门实现对象缓冲池,因为这会过于复杂而影响程序的风格和性能。

5. 消除过期引用

本条在代码上的表现是,如果声明的一个变量不再使用,并且你无法明确其可以被自动释放,请将其主动赋值为 null。

尤其需要注意 Collections/Array或者对象容器中的对象引用(因为容易被忽略)。

尤其需要注意缓存,因为其意义本身就在于其较长的生命周期,请确保一个对象在不使用的时候同时也在缓存中被清除,否则因为缓存中的引用,该对象将无法被自动回收(考虑使用WeakHashMap)。

但这种主动设置的代码应该是个例外,因为对象的生命周期往往不应该太长。这样你就可以确定对象会随着代码的运行而被自动回收。

6. 不要使用 终结/析构函数(finalize)

因为析构函数的执行是不稳定的,任何需要被正常执行的代码都不要放在这里。

析构函数唯一的用途是可以用来执行确认操作,如重新执行资源的释放,以确认资源已经得到了释放。

7. 遵守 equals 的重写约定

  1. 不需要重写 equals 的情况 (1) 如果每一个实例都是不一致的,那么继承Object 的equals即可。 如Thread,Random。 (2)如果父类的实现不需要修改。 (3)类不需要提供这个方法,则请重写equals并在其中抛出 UnsupportedOperationException
  2. 改写equals需要注意的情况 (1)数学概念,自反true==x.equals(x) 对称 x.equals(y)==y.equals(x) 传递 x.equals(y)==y.equals(z)==z.equals(x) 一致 x,y不变,则x.equals(y) 永远不变。
  3. 建议:因为改写一个equals是这么的难,建议总是强制类型进行比较(就是永远先判断 instanceof )

8. 改写equals的时候也需要改写 hashCode

  1. 相同的对象需要有相同的 hashCode。
  2. 不同的对象需要有不同的hashCode
  3. 不可变对象,可以将散列码缓存起来。
  4. 不要基于hashCode进行业务编码,这是散列表才需要使用的内置函数。

9. 总是改写toString对象

toString 方法是重要的,它可以返回一个程序可以识别的类的内容,让类的使用更加友好,同时如果有必要的话也可以提供 valueOf(String) 或者 getInstance(String) 来进行字符串的反序列化。

但需要注意的是,如果类本身没有约定字符串序列化的格式,请尽量不要依靠其 toString() 得到的字符串格式,因为其可能会改变,这样我们的程序就会报错了。

10. 谨慎的改写 clone

因为 clone 需要考虑的事情很多,不能改变既有对象,还需要考虑类的继承关系(因为父类内容也需要clone过来)。 所以,建议是提供其他方法替代对象拷贝(如静态工厂或者拷贝构造函数,就是参数是本类实例的构造函数),或者干脆不提供深复制的能力。

11. 实现Comparable接口

如同 hashCode函数是所有散列算法的基础,compareTo 函数也是所有比较算法的基础,其约定是, a.compareTo(b) { return b.equals(a) ? 0 : (b < a ? -1 : 1); } ,在极少数的情况下 a.compareTo(b)可以不等于 a.equals(b) ,但请做出额外说明。

comparaTo函数的实现类似 equals,只有很小的区别,既如果参数类型不一致,可以抛出ClassCastException,如果参数为 null 则可以抛出 npe,但 equals 函数的约定只是返回false。

12. 使类和成员的可访问级别最小化。

保持封装性,是软件设计的最基本原则。

13. 支持非可变性

如果类在设计上不需要进行修改,则可以将其设计为不可变类。其有以下特征:

  1. 所有属性设置为private final。
  2. 不提供修改数据的方法。
  3. 保证子类不会改写数据。
  4. 保证引用类属性中的引用,其他地方无法得到。

不可变类,其实是函数式编程中常见的做法,其本质上线程安全。唯一的缺点是对于不同的值需要不同的对象,所以可能造成对象过多。最好的方法是同时提供一个可变版本,如 String - StringBuffer ,BigInteger - BitSet。

对于不能被设计为不可变类的类,你仍然需要尽可能的限制它的可变性,在构造时应该完全初始化对象,而不是将某些属性默认为null。

14. 复合优先于继承

继承打破了封装性。你可以使用父类的内部实现而又无法明确其内部实现的情况是很危险的,造成的错误可能完全无法排查,所以如果需要使用一个类的功能,请优先考虑复合的形式。

15. 如果一个类可能会被作为父类,请给出文档说明,否则禁止继承

好的api文档应该描述一个方法做了什么工作,而并非描述他是怎么做到的

请写明改写每一个函数可能造成的影响,否则改写就是危险的。 对于某些操作,父类需要提供一些切面,使子类可以进入到父类的工作流程中,这就需要父类进行过专门的设计。

所以最好的方式就是将类设置为 final 类,禁止子类化。

16. 接口优于抽象类

接口可以多重继承,便于在类上增加功能,便于类的功能混合,也方便设计继承层级。

可以在提供接口的同时提供一个骨架实现的抽象类,这个好处是可以让子类写更少的代码。

但抽象类有一个额外的好处,就是抽象类本身的演化(添加功能),比接口容易,如果接口添加了一个方法,则所有的子类都需要添加实现,但抽象类自己可以提供一个基础实现,这样子类就不需要修改了。但在实际的设计中,这个优点并不突出,因为往往实现类也需要各自的实现。

所以,接口的设计需要非常的谨慎,因为修改接口的代价非常巨大。

17. 接口只是被用于定义类型

请不要使用常量接口(既在接口中定义了很多常量),而是类型安全枚举类(typesafe enum class) 或者不可被实例化的常量类(final Constants)

18. 优先考虑静态成员类

事实上,静态成员类跟其他的类区别并不大,最大的区别是其定义在某一个类的里面,需要通过该类访问。 这提供了封装与该类相关的某些操作的极好的方法。如条目17中的常量类,就可以考虑使用静态成员类实现。

展开阅读全文
打赏
0
0 收藏
分享
加载中
更多评论
打赏
0 评论
0 收藏
0
分享
返回顶部
顶部