本系列文章是總結Effective Java文章中我認爲最重點的內容,給很多沒時間看書的朋友以最短的時間看到這本書的精華。
第一篇《Effective Java——創建和銷燬對象》
第三章對於所有對象都通用的方法
第8條:覆蓋equals時請遵守通用約定
符合以下條件則不需要覆蓋equals情況
類的每個實例本質上都是唯一的
對於代表活動實體而不是值(value)的類。例如:Thread
,他只關注該類能完成的任務或者功能,而不是像Integer
關心他中存在的值(value),對於Thread
這樣的類Object
提供的默認equals
方法完全能夠滿足要求。
不關心類是否提供了“邏輯相等”的測試功能
例如:Random
類,如果對他進行覆蓋equals
方法將毫無意義,使用者根本不關心多個Random
實例產生的隨機數是否相等。
邏輯相等:實例內存在的值是否相等,例如:Integer
的兩個實例用equals
來判斷是否邏輯相等。
超類已經覆蓋了equals,從超類繼承過來的行爲對於子類也是合適的
集合類:Set實現都從AbstractSet繼承equals實現,List實現從AbstractList繼承equals實現,Map實現從AbstractMap繼承equals實現。
類是私有的或是包級私有的,可以確定他的equals方法永遠不會被調用
私有的或是包級私有的類不可能對外部提供。
需要覆蓋equals方法
對於“值類(value class)”來說,它們需要判斷“邏輯相等”,且超類還沒有覆蓋equals
實現期望的行爲,這時我們需要覆蓋equals
方法。
值類:例如,Integer
、Date
,僅僅表示一個值,我們在調用equals
時希望知道它們邏輯上是否相等,而不是想了解他們是否指向同一個對象。
覆蓋equals遵守的約定
自反性
對於任何非null
的引用值x,x.equals(x)
必須返回true
。
對稱性
對於任何非null
的引用值x和y,如果x.equals(y)
返回true
,則y.equals(x)
也必須返回true
。
傳遞性
對於任何非null
的引用值x、y和z,如果x.equals(y)
返回true
且y.equals(z)
返回true
則x.equals(z)
也必須返回true
。
一致性
對於任何非null
的引用值x和y,只要equals
方法內部比較所用到的字段內容沒有被修改,那麼多次調用x.equals(y)
就會一致地返回true,或者false。
非空性
所有比較的對象都不爲null
,如下代碼:
@Override
public boolean equals(Object o){
if(null == o){
return false;
}
......
}
其實如上代碼根本不必要我們只需要用instanceof
來判斷參數類型,如果參數時null
那麼instanceof
一定會返回false
,如下代碼:
@Override
public boolean equals(Object o){
if(!(o instanceof MyClass)){
return false;
}
......
}
編寫equals規則
1. 使用==操作符檢查“參數是否爲這個對象的引用”
2. 使用instanceof操作符檢查“參數是否爲正確的類型”
3. 把參數轉換成正確的類型
4. 對於該類中的每個“關鍵”域,檢查參數中的域是否與該對象中對應的域相匹配
在這裏要多說幾點:
對於float
字段,要用Float.compare
方法來比較,對於double
字段,則使用Double.compare
方法,由於存在着Float.NaN、-0.0f
以及類似的double
常量。
爲了獲得equals方法的最佳性能,應該最先比較最有可能不一致的字段,或者開銷最低的字段。
5. 當編寫完成equals方法之後,檢查他是否滿足,對稱,傳遞,一致性
如下代碼:
public class TestClass{
private String field;
@Override
public boolean equals(Object obj) {
//第一步
if(this == obj){
return true;
}
//第二步
if(!(obj instanceof TestClass)){
return false;
}
//第三步
TestClass testClass = (TestClass) obj;
//第四步
return null == this.field ? null == testClass.field : this.field.equals(testClass.field);
}
}
編寫equals的忠告
- 覆蓋equals時一定要覆蓋hashCode方法
- 要儘量讓equals方法簡單一些
- 不要將equals方法聲明中的參數轉換爲其他類型
第9條:覆蓋equals時總要覆蓋hashCode
hashCode約定
- 在同一個實例對象中,只要equals方法用到的信息沒有被修改,那麼這個對象多次調用hashCode方法應該返回結果相同。
- 如果兩個對象根據equals方法比較是相等的,那麼這兩個對象hashCode方法產生的結果也必須相等。
- 如果兩個對象根據equals方法比較是不相等的,那麼這兩個對象hashCode方法產生的結果有可能相等,也有可能不相等。如果能實現不相等,可以提高散列表的性能。
public static final class PhoneNumber{
private final int areaCode;
private final int prefix;
private final int lineNumber;
public PhoneNumber(int areaCode, int prefix, int lineNumber){
this.areaCode = areaCode;
this.prefix = prefix;
this.lineNumber = lineNumber;
}
@Override
public boolean equals(Object obj) {
if(this == obj){
return true;
}
if(!(obj instanceof PhoneNumber)){
return false;
}
PhoneNumber phoneNumber = (PhoneNumber) obj;
return this.areaCode == phoneNumber.areaCode
&& this.prefix == phoneNumber.prefix
&& this.lineNumber == phoneNumber.lineNumber;
}
}
如上代碼只覆蓋了equals
方法沒有覆蓋hashCode
,當我們把PhoneNumber
應用到Map
集合上如下代碼:
Map<PhoneNumber, String > map = new HashMap<>();
map.put(new PhoneNumber(010,1111,2222),"Jenny");
String value = map.get((new PhoneNumber(010,1111,2222));
我們期望value=Jenny
,實際上返回的null
。
這是由於有兩個PhoneNumber
實例,第一個實例用於put
插入到HashMap
中,第二個實例用於get
從HashMap
中獲取,由於沒有覆蓋hashCode
方法所以兩個實例的hashCode
是不相等的。因此第一個實例根據自己的散列碼來保存數據,第二個實力根據自己的散列碼來獲取數據,由於兩個散列碼不相等所以get
方法返回爲null
。
對於HashMap
如何根據key
來保存value
請查看這篇文章,說的非常想詳細.
解決這個問題就是在PhoneNumber
類中根據上面的約定來覆蓋hashCode
方法,
舉例如下代碼:
@Override
public int hashCode() {
return lineNumber;
}
這段代碼只是舉個例子,具體的哈希值的計算非常複雜這裏就不做講解了。哈希值計算的好壞直接影響HashMap
的效率,有興趣可以在網上查查。
第10條:始終要覆蓋toString
- 對於任何類都推薦覆蓋
toString
方法,尤其是“值類”。 -
toString
返回的字符串描述了這個類的所有有用的信息,如下代碼:
@Override
public String toString() {
return "PhoneNumber{" +
"areaCode=" + areaCode +
", prefix=" + prefix +
", lineNumber=" + lineNumber +
'}';
}
-
對
toString
的返回值格式進行約束。例如返回字符串在文檔中約束爲Json格式,那麼無論何種情況都不能修改它的返回格式。如果修改了,那麼之前代碼中對這個返回值進行解析的代碼都會報錯。 -
對
toString
的返回值格式不進行約束。在文檔中不限定他的返回格式,返回格式是可變的,有可能是Json有可能是Xml形式。如果代碼中用到解析toString
返回格式的代碼其正確性就應該由使用者來保證了。 - 對於
toString
中用到的有用信息,類中都要有相關的方法來返回值。避免使用者來解析格式不固定的toString
方法的返回值.
如下代碼:
@Override
public String toString() {
return "PhoneNumber{" +
"areaCode=" + areaCode +
", prefix=" + prefix +
", lineNumber=" + lineNumber +
'}';
}
//對於上面toString方法用到的信息都需要提供getX方法來獲取他們的值
public int getAreaCode() {
return areaCode;
}
public int getPrefix() {
return prefix;
}
public int getLineNumber() {
return lineNumber;
}
第9條:謹慎的覆蓋clone
設計模式的原型模式可以由Object
的clone
方法來實現。
實現clone
- 當前對象實現
Cloneable
接口。 - 當前對象在
public Object clone()
方法中調用super.clone
得到當前類的一個對象。前提是父類實現的clone
方法沒有問題,也就是所有字段(可變字段)都已經clone
好了(深拷貝)。 - 把克隆對象淺拷貝的字段指向該對象
clone
得到的地址空間。
如下代碼:
public class E implements Cloneable {
@Override
public Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
public class C implements Cloneable {
@Override
public Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
public class D implements Cloneable {
public E e = new E();
@Override
public Object clone() throws CloneNotSupportedException {
D d = (D) super.clone();
d.e = (E) this.e.clone();
return d;
}
}
public class B implements Cloneable{
public D d = new D();
@Override
public Object clone() throws CloneNotSupportedException {
B b = (B) super.clone();
b.d = (D) this.d.clone();
return b;
}
}
public class A extends B implements Cloneable {
private static String TAG = "aaaaa";
private int count = 100;
public C c = new C();
@Override
public Object clone() throws CloneNotSupportedException {
A a = (A) super.clone();
a.c = (C) this.c.clone();
return a;
}
public static void main(String[] args) throws CloneNotSupportedException{
System.out.println("main");
A a = new A();
A a1 = (A) a.clone();
System.out.println("a : " + a.toString());
System.out.println("a1 : " + a1.toString());
System.out.println("a.c : " + a.c);
System.out.println("a1.c : " + a1.c);
}
}
//打印信息
a : com.example.test.A@723279cf
a1 : com.example.test.A@10f87f48
a.c : com.example.test.C@b4c966a
a1.c : com.example.test.C@2f4d3709
深拷貝
以上代碼實現了深拷貝,每一個可變字段都調用clone
方法產生一個最新的對象,每個類都實現了Cloneable
接口並且覆蓋了public Object clone()
方法。
淺拷貝
將上述代碼稍微修改就可以變成淺拷貝
public class A extends B implements Cloneable {
private static String TAG = "aaaaa";
private int count = 100;
public C c = new C();
@Override
public Object clone() throws CloneNotSupportedException {
A a = (A) super.clone();
//如果類中的可變字段沒有調用如下代碼,那麼這個類就是淺拷貝
// a.c = (C) this.c.clone();
return a;
}
public static void main(String[] args) throws CloneNotSupportedException{
System.out.println("main");
A a = new A();
A a1 = (A) a.clone();
System.out.println("a : " + a.toString());
System.out.println("a1 : " + a1.toString());
System.out.println("a.c : " + a.c);
System.out.println("a1.c : " + a1.c);
}
}
//打印信息如下:
a : com.example.test.A@6e0be858
a1 : com.example.test.A@61bbe9ba
a.c : com.example.test.C@610455d6
a1.c : com.example.test.C@610455d6
以上是淺拷貝,在A
類的public Object clone()
方法中沒有對可變對象C
進行拷貝,導致打印的信息中C
對象的地址是相同的。
代替clone的方案
寫代碼很少覆蓋Cloneable
接口,說兩種代替方案
- 拷貝構造器
例如HashMap
類的構造方法public HashMap(Map<? extends K, ? extends V> map);
就是一個拷貝構造器 - 拷貝工廠方法
例如public static Yum newInstance(Yum yum);
最好不要覆蓋
Cloneable
接口,使用clone
方法。
第12條:考慮實現Comparable接口
實現這個接口主要是爲了對象之間的排序,Java平臺類庫中的所有“值類”都實現了這個接口。
規則:
將這個對象與指定對象進行比較。當該對象小於、等於、大於指定對象的時候,分別返回負數、零、正整數。如果由於指定對象的類型而無法與該對象進行比較,則拋出ClassCastException異常。
如下代碼:
public class PhoneNumber implements Comparable<PhoneNumber> {
public final int area;
public final int phone;
public PhoneNumber(int area, int phone) {
this.area = area;
this.phone = phone;
}
@Override
public int compareTo(@NonNull PhoneNumber pn) {
if (area < pn.area) {
return -1;
}
if (area > pn.area) {
return 1;
}
if (phone < pn.phone) {
return -1;
}
if (phone > pn.phone) {
return 1;
}
return 0;
}
@Override
public String toString() {
return "PhoneNumber{" +
"area=" + area +
", phone=" + phone +
'}';
}
public static void main(String[] args) throws CloneNotSupportedException {
PhoneNumber[] phoneNumbers = {
new PhoneNumber(100,300),
new PhoneNumber(200,500),
new PhoneNumber(50,10),
};
List<PhoneNumber> phoneNumberList = Arrays.asList(phoneNumbers);
System.out.println(phoneNumberList);
Collections.sort(phoneNumberList);
System.out.println(phoneNumberList);
}
}
打印日誌
[PhoneNumber{area=100, phone=300}, PhoneNumber{area=200, phone=500}, PhoneNumber{area=50, phone=10}]
[PhoneNumber{area=50, phone=10}, PhoneNumber{area=100, phone=300}, PhoneNumber{area=200, phone=500}]