Effective Java 2 Object
第1条:考虑静态工厂方法代替构造器
获取一个类的实例,最常用的是提供一个public constructor。还有一种就是提供一个static factory method,它只返回该类的一个实例。
|
|
优势:
- 静态工厂方法有名字会是API更为清晰。
- 他们返回自己的一个已经构建好的实例,避免创建不必要的重复对象。
- 可以返回任何子类型对象,提高APT灵活性
- 在创建参数化类型实例的时候,是代码更加简洁。如下:
|
|
缺点:
- 如果不含public/protected constructor,就不能子类化。子类在实例化的时候会调用父类的constructor,所有父类必须要给权限
- 他们和其他static method没有任何区别。一般给一些惯用名称。如:getInstance/newInstance/getType/newType/of/valueof
总结:遇到构建类的时候,要考虑static factory method
第2条:遇到多个构造器参数时考虑构建器
当一个类有许多个可选属性,那么在初始化时采用哪种构造器呢?
一般习惯用重叠构造器模式:第一个constructor有1个可选参数,第二个constructor有2个可选参数,第三个constructor有3个可选参数…这样下来当有许多参数的时候,会使代码难写阅读性差
采用Builder模式解决
|
|
注意把必须要求的参数放到Builder constructor里,可选参数通过option设置,最后通过build返回一个demo1对象。
|
|
可以参照建造者模式把build抽象为接口,方便理解
第3条:用private constructor或enum强化singleton属性
singleton实现有三种方式:
第1种:公有的实例
|
|
第2种:私有的实例+静态工厂方法
|
|
第3种:单元素的枚举类型实现单例(最佳方法)
设计思想:通过公有的静态final域为每个枚举常量导出实例的类,因为没有可以访问的构造器(private constructor),枚举类型是真正的final。既不能创建枚举类型的实例,也不能对他进行扩张。换句话说,枚举类型是受实例控制的,是单例的泛型化,本质是单元素的枚举。
并且枚举提供了序列化机制,防止了多次实例化,它是线程安全的,同时可以防止反射攻击
|
|
可以通过javap得到证明
|
|
第4条:通过private constructor强化不可实例化的能力
对于有些工具类(utility class)不希望被实例化。然而在缺少constructor的情况下,编译器会自动提供一个公有的无参缺省constructor。
此时可以提供一个private constructor(不能被实例化和extends),或修饰该类为final(不能extends)。
第5条:避免创建不必要的对象
编写code的时候,需要考虑性能问题。如一个实例需要重复使用,不妨把实例创建放到static块里,在创建类的时候初始化;Java里要考虑自动装箱和拆箱带来的性能问题。等等相关问题,在coding的时候需要考虑。
第6条:消除过期的对象引用
Java虽然拥有GC功能,但是内存泄露是很隐蔽的存在(或者称为无意识的对象保持)。如果一个对象被无意识的保留下来,那么GC就不会处理这个对象和它引用的其他对象,从而对性能产生重大影响。
解决方法:把这个对于引用=null。注意这是一种例外,只要当类自己管理内存时,一旦元素被释放掉,则该元素中包含的任何对象引用都应该=null。如:Stack的pop方法。
然而规范的做法是:让包含该引用的变量结束其生命周期,即将局部变量的作用域最小化
其他内存泄露情况:cache等,需要coding的时候具体考虑。
第7条:避免使用终结方法
使用finalizer方法通常不可预测一个对象从不可到达开始,到它的finalizer被执行,所花费的时间是不可预测的
所以,一般通过显式的终止方法及时释放资源,如InputStream,OutputStream里的close方法,
第8条:覆盖equals时遵守通用约定
1.自反性
对象必须等于自身,即对于非null的引用值x,x.equals(x)必须返回true。
2.对称性
对于非null的引用值x和y,当且仅当x.equals(y)返回true
时,y.equals(x)必须返回true
。
3.传递性
对于非null的引用值x、y和z,如果x.equals(y)=true并且y.equals(z)=true,那么x.equals(z)也必须返回true。
Demo:一个Point类包含x和y坐标,现在有个ColorPoint继承Point,并添加了color属性。如下进行初始化:
|
|
此时p1.equals(p2)和p2.equals(p3)都返回true,但是p1.equals(p3)返回false。显然违反了传递性。此时可以采用复合优先于继承解决问题。
|
|
此时p1.equals(p2)和p2.equals(p3)都返回false,并且p1.equals(p3)返回false满足条件。
4.一致性
如果2个对象相等,它们就必须始终保持相等,除非其中有对象被修改了。即无论类是否是可变的,都不要使equals依赖于不可靠的资源。
5.非空性
所有对象都必须不等于null,即非空对象x满足x.equals(null)必须返回false。
一般不需要对obj==null进行判断,因为
|
|
就包含了obj为null的情况。
Effective equals的诀窍
1.使用==检查是否是本对象的引用
如果是直接返回true。这样在比较操作代价高的时候,可以提高性能。
2.使用instance检查参数是否为正确的类型
即检查obj是否为equals方法所在的那个类
3.把参数obj转换为正确的类型
4.把类中的每个关键域都进行比较
应该先比较最可能不一致的关键域
5.写测试
通过单元测试来验证对称、传递、一致性。
第9条:覆盖equals时总有覆盖hashCode
Object规范:
- 只要equals比较的域没有被修改,则该对象的hashCode方法必须返回同一个整数。
- 如果2个对象equals比较相等,则他们的hashCode返回值也相等
- 如果2个对象equals比较不相等,则他们任一个对象hashCode返回的值不一定要产生不同的结果。但是,给不相等的对象不同的hashCode可能提高hash table(散列表)的性能。
|
|
|
|
我们希望打印出fedomn,但是为null。说明这个2个实例不相等,因为没有override hashCode方法,导致2个实例具有不相等的散列码,当在put时候把key放在一个散列桶,而get的时候根据key在另一个散列桶中查找,必然返回null。 解决方法:
|
|
其他类型的关键域都采用类似方法最终转成int类型。
第10条:始终要覆盖toString
虽然Object提供了toString方法,但是返回的是类名@散列码的无符号十六进制
所以重写toString方法,输出需要的信息。注意要写好返回值的注释。
第11条:谨慎覆盖clone
准备知识Java如何复制对象
首先,Cloneable接口表明该对象允许克隆(clone),但是它内部并不包含任何方法。
如果一个类实现了Cloneable,则Object的clone方法就会返回该对象的逐域拷贝
所以一个类实现Cloneable,它重写Object的clone方法如下:
|
|
注意返回的类型是PhoneNumber,体现了一条通则:永远不要让客户去做任何类库能够替客户完成的事情
但是,这样做依然有问题。如果类中包含引用类型如Object[] elements,调用super.clone()时,逐域拷贝的只是该对象的引用
,指向的仍是原来的内存
。此时可以通过递归调用clone,如下:
|
|
貌似这样就解决了问题,但是如果elements域为final的,就不可以通过clone方法赋值了。除非去除final修饰符。
递归clone也存在问题。如Demo类里包含一个自己实现的list类
|
|
此时对bucket进行的clone方法,产生的链表与原始链表是一样的(next看出),很容易产生混乱,此时就要进行deep copy
如下
|
|
总结:
clone复杂对象,先调用super.clone,把对象中的引用类型设置空白状态,再重新产生新的状态。如上deepCopy。
事实上实现拷贝对象可以提供copy constructor或者copy factory。而不是采用Cloneable/clone方法。
第12条:考虑实现Comparable接口
Comparable
compareTo方法没有在Object里声明,它是Comparable
接口中唯一的方法。 如果类实现了Comparable接口,就表明它的实例具有内在的排序关系。 实现此接口的对象列表(和数组)可以通过 Collections.sort(和 Arrays.sort)进行自动排序。 ComparaTo方法中域的比较是顺序的比较,而不是等同性比较,比较对象引用域可以通过递归地调用ComparTo方法实现
Comparator
如果一个域没有实现Comparable接口,可以使用一个显示的Comparator来代替 Comparable与Comparator区别 如PriorityQueue里的Comparator用来创建一个非标准的排序关系,来实现插入时排序(只能保证队列头的是最大或最小)
第13条:使类和成员的可访问性最小化
一个设计良好的模块会隐藏所有的实现细节,把它的API与实现清晰的隔离开,实现信息隐藏
。
Java实现信息隐藏规则:
- 尽可能地使每个类或者成员不被外界访问
对于顶层的类,只有2种访问级别:包级私有的(默认无修饰符)和公有的(public修饰符)。
对于成员有4中访问级别:
- 私有的(private):只有在声明该成员的顶层类内部才可以访问这个成员
- 包级私有的:无修饰符,声明该成员的包内部的任何类都可以访问这个成员
- 受保护的(protected):声明该成员的类的子类,声明该成员的类的包内任何类都可以访问
- 公有的(public):在任何地方都可以访问该成员
设计类的公有API时,始终尽可能的降低可访问性。公有类不应该包含公有域,除了不可变的公有静态final域(如Boolean.FALSE)因为包含公有可变域的类并不是线程安全的
第14条:在公有类中使用访问方法
而非公有域
如果类可以在它所在包的外部进行访问,就提供访问方法,不应该暴露可变的域。
如果类是包级私有的,或者是私有的嵌套类,直接暴露其数据域是可以的。
第15条:使可变性最小化
不可变的类指:每个实例包含的所有信息必须在创建该实例的时候就提供,并且在其生命周期内不能改变。(如final修饰的String类)
设计不可变类规则
- 不要提供任何会修改对象状态的方法(如Setter)。
- 保证类不会被扩展。一般通过final修饰该类,防止子类化。
- 使所有的域都是final的。(如String类里的private final char value[])
- 使所有的域都是私有的。防止客户端修改这些对象。
- 确保对于任何可变组件的互斥访问。
- 如果类包含可变对象的域,则必须保证客户端无法获得这些对象的引用,不要用客户端来初始化这些域(可采用保护性拷贝)。
不可变的对象本质是线程安全的,它们不要求同步。
不可变对象缺点:对于每个不同的值都需要一个单独的对象,创造这种对象的代价可能很高。
第16条:复合优先于继承
继承虽然实现了代码重用,但是跨包边界的继承,则是非常危险脆弱的。(如:子类依赖超类中的特定功能而实现,当超类随着版本更新发生改变,子类就可能会受到破坏。)
解决办法:不用去扩展现有类,在新类中增加一个私有域,来引用现有类的实例。这种方法称为复合composition
。
新类中的每个方法,都可以调用现有类中对应的方法,这被称为转发forwarding
。新类中的方法称为转发方法forwarding method
。
总之,只有当子类真正是超类的子类型的时候,才适合继承。如果子类和超类位于不同的包中,并且超类不是为继承而实现的,这时可以用复合和转发机制来代替继承。示例详见Decorator模式
第17条:要么为继承而设计,并提供文档说明,要么就禁止继承
为继承而设计的类,文档里必须包含覆盖该方法给其他方法所带来的影响
。
构造器决不能调用可被覆盖的方法:
因为超类的constructor在子类的constructor之前运行,所以子类覆盖版本的方法将会在子类constructor运行之前被调用,很有可能抛出NullPointerException。
那么对于普通的具体类,它们既不是final的,也不是为了子类化而设计的。这种情况下,每次对其修改,都会对扩展这个类的客户类产生破坏。
此时,最佳方案是:对于那些并非为了安全地进行子类化而设计和编写文档的类,要禁止子类化
一般的解决方法:
- 把类声明为final的。
- 把constructor变成private的或者package-private。
第18条:接口优于抽象类
由于Java只允许单继承,抽象类这种为继承而设计的类受到了很大的限制。
1.现有的类可以很容易被跟新,以实现新的接口。
如当Java引入Comparable接口时,会更新许多类以实现比较功能,此时就可实现Comparable接口,重写方法。
2.接口是定义mixin(混合类型)的理想选择。
mixin类型指:类除了实现它的基本类型外,还可以实现这个mixin类型,以表明它提供了某种可供选择的行为。 如类实现Comparable接口,表明它提供了可以比较对象的方法,允许该功能混入到类的主要功能中。
3.接口允许我们构造非层次结构的类型框架
如下:
|
|
现实生活中,如果一个Person既是Singer也是Writer,就可以定义SingerWriter接口,然后提供一个骨架抽象类(包含一些最基本的方法)来实现该接口。
虽然接口不允许包含实现方法,但是可以通过导出的接口提供一个抽象的骨架实现(skeletal implementation)类。
骨架实现被称为AbstractInterface
,如:AbstractCollection、AbstractSet、AbstractList、AbstractMap。
骨架实现类,为一些最基本的方法实现。如:JDK里的HashSet继承骨架实现类AbstractSet,并且实现了Set接口。
公有接口一旦公开,并广泛使用,修改几乎就不可能了。
总结,接口通常是定义允许多个实现的类型的最佳途径。如果导出了一个重要接口,就应该同时提供骨架实现类。
第19条:接口只用于定义类型
当类实现接口时,接口就充当可以引用这个类
的实例
的类型。
有一种常量接口如下:
|
|
常量接口模式是对接口的不良使用
,它的所有子类的命名空间也会被接口中的常量污染。
如要要导出常量可以使用枚举类型或不可实例化的工具类。
第20条:类层次优于标签类
标签类指:一个类中包含显式的标签域。如下Figure类包含了RECTANGLE,CIRCLE。这样破坏了可读性,内存占用也增加了。
|
|
解决方法:将标签类转变为类层次
- 为标签类里的每个方法,都定义为抽象类的抽象方法。(即把不依赖标签值的方法设为抽象方法)
- 把每个标签定义成具体类,标签用到的数据域定义到具体类中。
|
|
第21条:用函数对象表示策略
有些语言支持函数指针、代理、lambda表达式等,来允许程序具有调用特殊函数的能力。
Java里没有提供函数指针,但是可以调用对象上的方法来执行其它对象操作。
如果一个类仅仅导出这样的一个方法,它的实例实际上就等同于一个指向该方法的指针。这样的实例被称作函数对象
。如下:
|
|
其中,把该策略定义为一个接口,为每个具体的策略声明一个实现该接口的类。
- 当策略只使用一次时,可使用匿名类来声明和实例化具体策略类。
- 当策略被反复使用时,通常设计为私有的静态成员类,通过公有的静态实例来使用策略如下:
|
|
String类也利用了这种模式,通过它的CASE_INSENSITIVE_ORDER域,导出一个不区分大小写的字符串比较器。
第22条:优先考虑静态成员类
嵌套类只定义在一个类内部的类,它为外围类提供服务。 嵌套类有4种:
- 静态成员类(static member class)
- 非静态成员类(nonstatic member class)
- 匿名类(anonymous class)
- 局部类(local class)
1.静态成员类和非静态成员类
static member class是外围类的一个静态成员,与其他静态成员一样。
nonstatic member class的每个实例都包含一个额外指向外围对象的引用,在其内部可以访问外围实例上的方法
。
如HashMap里的KeySet类的contains方法就调用了外围类的containsKey方法。或者利用修饰过的this构造获得外围实例的引用
。
如HashMap里的KeySet类的clear方法,通过HashMap.this.clear()来获取外围实例执行方法。下面代码就是来自HashMap类里的非静态成员类KeySet
|
|
注意:
非静态成员类不能在没有外围类实例的情况下独立存在。一般在外围类里提供方法来创建非静态内部类。
如HashMap里的public Set
总结:
非静态成员类常用于定义一个Adapter,它允许外部类的实例被看做是另一个不相关的类的实例。
(解释:HashMap的实例也可以看做成KeySet的实例,因为HashMap通过keySet()方法会返回一个KeySet的实例。)而这些非静态成员类就可以来实现外围类的集合视图。
如果声明的成员类不要求访问外部实例,就要加上static变成静态的。
2.匿名内部类
它没有名字,使用的同时被声明和实例化。常用动态地创建函数对象。比如Runnable
3.局部类
在任何可以声明局部变量的地方,都可以声明局部类。它与成员类一样,有名字,可重复使用。
如果它要访问外围类实例,就可以为非静态的,否则为静态的。它要求简短,否则影响可读性。