Java:Object類詳解

Java 問答:終極父類


Java的一些特性會讓初學者感到困惑,但在有經驗的開發者眼中,卻是合情合理的。例如,新手可能不會理解Object類。這篇文章分成三個部分講跟Object類及其方法有關的問題。

上帝類

問:什麼是Object類?

答:Object類存儲在java.lang包中,是所有java類(Object類除外)的終極父類。當然,數組也繼承了Object類。然而,接口是不繼承Object類的,原因在這裏指出:Section 9.6.3.4 of the Java Language Specification:“Object類不作爲接口的父類”。
Object類中聲明瞭以下函數,我會在下文中作詳細說明。

  • protected Object clone()
  • boolean equals(Object obj)
  • protected void finalize()
  • Class< > getClass()
  • int hashCode()
  • void notify()
  • void notifyAll()
  • String toString()
  • void wait()
  • void wait(long timeout)
  • void wait(long timeout, int nanos)

java的任何類都繼承了這些函數,並且可以覆蓋不被final修飾的函數。例如,沒有final修飾的toString()函數可以被覆蓋,但是final wait()函數就不行。

問:可以聲明要“繼承Object類”嗎?

答:可以。在代碼中明確地寫出繼承Object類沒有語法錯誤。參考代碼清單1。

代碼清單1:明確的繼承Object

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
importjava.lang.Object;
publicclassEmployee extendsObject {
privateString name;
publicEmployee(String name) {
this.name = name;
}
publicString getName() {
returnname;
}
publicstaticvoidmain(String[] args) {
Employee emp = newEmployee("John Doe");
System.out.println(emp.getName());
}
}

你可以試着編譯代碼1(javac Employee.java),然後運行Employee.class(java Employee),可以看到John Doe 成功的輸出了。

因爲編譯器會自動引入java.lang包中的類型,即 import java.lang.Object; 沒必要聲明出來。Java也沒有強制聲明“繼承Object類”。如果這樣的話,就不能繼承除Object類之外別的類了,因爲java不支持多繼承。然而,即使不聲明出來,也會默認繼承了Object類,參考代碼清單2。

代碼清單2:默認繼承Object

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
publicclassEmployee
{
privateString name;
publicEmployee(String name)
{
this.name = name;
}
publicString getName()
{
returnname;
}
publicstaticvoidmain(String[] args)
{
Employee emp = newEmployee("John Doe");
System.out.println(emp.getName());
}
}

就像代碼清單1一樣,這裏的Employee類繼承了Object,所以可以使用它的函數。

克隆Object

問:clone()函數是用來做什麼的?

答:clone()可以產生一個相同的類並且返回給調用者。

問:clone()是如何工作的?

答:Objectclone()作爲一個本地方法來實現,這意味着它的代碼存放在本地的庫中。當代碼執行的時候,將會檢查調用對象的類(或者父類)是否實現了java.lang.Cloneable接口(Object類不實現Cloneable)。如果沒有實現這個接口,clone()將會拋出一個檢查異常()——java.lang.CloneNotSupportedException,如果實現了這個接口,clone()會創建一個新的對象,並將原來對象的內容複製到新對象,最後返回這個新對象的引用。

問:怎樣調用clone()來克隆一個對象?

答:用想要克隆的對象來調用clone(),將返回的對象從Object類轉換到克隆的對象所屬的類,賦給對象的引用。這裏用代碼清單3作一個示例。

代碼清單3:克隆一個對象

1
2
3
4
5
6
7
8
9
10
11
publicclassCloneDemo implementsCloneable {
intx;
publicstaticvoidmain(String[] args) throwsCloneNotSupportedException {
CloneDemo cd = newCloneDemo();
cd.x = 5;
System.out.printf("cd.x = %d%n", cd.x);
CloneDemo cd2 = (CloneDemo) cd.clone();
System.out.printf("cd2.x = %d%n", cd2.x);
}
}

代碼清單3聲明瞭一個繼承Cloneable接口的CloneDemo類。這個接口必須實現,否則,調用Object的clone()時將會導致拋出異常CloneNotSupportedException

CloneDemo聲明瞭一個int型變量x和主函數main()來演示這個類。其中,main()聲明可能會向外拋出CloneNotSupportedException異常。

Main()先實例化CloneDemo並將x的值初始化爲5。然後輸出x的值,緊接着調用clone() ,將克隆的對象傳回CloneDemo。最後,輸出了克隆的x的值。

編譯代碼清單3(javac CloneDemo.java)然後運行(java CloneDemo)。你可以看到以下運行結果:

1
2
cd.x = 5
cd2.x = 5

問:什麼情況下需要覆蓋clone()方法呢?

答:上面的例子中,調用clone()的代碼是位於被克隆的類(即CloneDemo類)裏面的,所以就不需要覆蓋clone()了。但是,如果調用別的類中的clone(),就需要覆蓋clone()了。否則,將會看到“cloneObject中是被保護的”提示,因爲clone()Object中的權限是protected。(譯者注:protected權限的成員在不同的包中,只有子類對象可以訪問。代碼清單3的CloneDemo類和代碼清單4的Data類是Object類的子類,所以可以調用clone(),但是代碼清單4中的CloneDemo類就不能直接調用Data父類的clone())。代碼清單4在代碼清單3上稍作修改來演示覆蓋clone()

代碼清單4:從別的類中克隆對象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
classData implementsCloneable {
intx;
@Override
publicObject clone() throwsCloneNotSupportedException {
returnsuper.clone();
}
}
publicclassCloneDemo {
publicstaticvoidmain(String[] args) throwsCloneNotSupportedException {
Data data = newData();
data.x = 5;
System.out.printf("data.x = %d%n", data.x);
Data data2 = (Data) data.clone();
System.out.printf("data2.x = %d%n", data2.x);
}
}

代碼清單4聲明瞭一個待克隆的Data類。這個類實現了Cloneable接口來防止調用clone()的時候拋出異常CloneNotSupportedException,聲明瞭int型變量x,覆蓋了clone()方法。這個方法通過執行super.clone()來調用父類的clone()(這個例子中是Object的)。通過覆蓋來避免拋出CloneNotSupportedException異常。

代碼清單4也聲明瞭一個CloneDemo類來實例化Data,並將其初始化,輸出示例的值。然後克隆Data的對象,同樣將其值輸出。

編譯代碼清單4(javac CloneDemo.java)並運行(java CloneDemo),你將看到以下運行結果:

1
2
data.x = 5
data2.x = 5

問:什麼是淺克隆?

A:淺克隆(也叫做淺拷貝)僅僅複製了這個對象本身的成員變量,該對象如果引用了其他對象的話,也不對其複製。代碼清單3和代碼清單4演示了淺克隆。新的對象中的數據包含在了這個對象本身中,不涉及對別的對象的引用。

如果一個對象中的所有成員變量都是原始類型,並且其引用了的對象都是不可改變的(大多情況下都是)時,使用淺克隆效果很好!但是,如果其引用了可變的對象,那麼這些變化將會影響到該對象和它克隆出的所有對象!代碼清單5給出一個示例。

代碼清單5:演示淺克隆在複製引用了可變對象的對象時存在的問題

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
classEmployee implementsCloneable {
privateString name;
privateintage;
privateAddress address;
Employee(String name, intage, Address address) {
this.name = name;
this.age = age;
this.address = address;
}
@Override
publicObject clone() throwsCloneNotSupportedException {
returnsuper.clone();
}
Address getAddress() {
returnaddress;
}
String getName() {
returnname;
}
intgetAge() {
returnage;
}
}
classAddress {
privateString city;
Address(String city) {
this.city = city;
}
String getCity() {
returncity;
}
voidsetCity(String city) {
this.city = city;
}
}
publicclassCloneDemo {
publicstaticvoidmain(String[] args) throwsCloneNotSupportedException {
Employee e = newEmployee("John Doe"49newAddress("Denver"));
System.out.printf("%s: %d: %s%n", e.getName(), e.getAge(),
e.getAddress().getCity());
Employee e2 = (Employee) e.clone();
System.out.printf("%s: %d: %s%n", e2.getName(), e2.getAge(),
e2.getAddress().getCity());
e.getAddress().setCity("Chicago");
System.out.printf("%s: %d: %s%n", e.getName(), e.getAge(),
e.getAddress().getCity());
System.out.printf("%s: %d: %s%n", e2.getName(), e2.getAge(),
e2.getAddress().getCity());
}
}

代碼清單5給出了EmployeeAddressCloneDemo類。Employee聲明瞭nameageaddress成員變量,是可以被克隆的類;Address聲明瞭一個城市的地址並且其值是可變的。CloneDemo類驅動這個程序。

CloneDemo的主函數main()創建了一個Employee對象並且對其進行克隆,然後,改變了原來的Employee對象中address值城市的名字。因爲原來的Employee對象和其克隆出來的對象引用了相同的Address對象,所以兩者都會提現出這個變化。

編譯 (javac CloneDemo.java) 並運行 (java CloneDemo)代碼清單5,你將會看到如下輸出結果:

1
2
3
4
John Doe: 49: Denver
John Doe: 49: Denver
John Doe: 49: Chicago
John Doe: 49: Chicago

問:什麼是深克隆?

答:深克隆(也叫做深複製)會複製這個對象和它所引用的對象的成員變量,如果該對象引用了其他對象,深克隆也會對其複製。例如,代碼清單6在代碼清單5上稍作修改演示深克隆。同時,這段代碼也演示了協變返回類型和一種更爲靈活的克隆方式。

代碼清單6:深克隆成員變量address

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
classEmployee implementsCloneable
{
privateStringname;
privateintage;
privateAddress address;
Employee(Stringname, intage, Address address)
{
this.name = name;
this.age = age;
this.address = address;
}
@Override
publicEmployee clone() throws CloneNotSupportedException
{
Employee e = (Employee) super.clone();
e.address = address.clone();
returne;
}
Address getAddress()
{
returnaddress;
}
StringgetName()
{
returnname;
}
intgetAge()
{
returnage;
}
}
classAddress
{
privateStringcity;
Address(Stringcity)
{
this.city = city;
}
@Override
publicAddress clone()
{
returnnewAddress(newString(city));
}
StringgetCity()
{
returncity;
}
voidsetCity(Stringcity)
{
this.city = city;
}
}
publicclassCloneDemo
{
publicstaticvoidmain(String[] args) throws CloneNotSupportedException
{
Employee e = newEmployee("John Doe"49newAddress("Denver"));
System.out.printf("%s: %d: %s%n", e.getName(), e.getAge(),
e.getAddress().getCity());
Employee e2 = (Employee) e.clone();
System.out.printf("%s: %d: %s%n", e2.getName(), e2.getAge(),
e2.getAddress().getCity());
e.getAddress().setCity("Chicago");
System.out.printf("%s: %d: %s%n", e.getName(), e.getAge(),
e.getAddress().getCity());
System.out.printf("%s: %d: %s%n", e2.getName(), e2.getAge(),
e2.getAddress().getCity());
}
}

Java支持協變返回類型,代碼清單6利用這個特性,在Employee類中覆蓋父類clone()方法時,將返回類型從Object類的對象改爲Employee類型。這樣做的好處就是,Employee類之外的代碼可以不用將這個類轉換爲Employee類型就可以對其進行復制。

Employee類的clone()方法首先調用super().clone(),對name,age,address這些成員變量進行淺克隆。然後,調用成員變量Address對象的clone()來對其引用Address對象進行克隆。

Address類中的clone()函數可以看出,這個clone()和我們之前寫的clone()有些不同:

  • Address類沒有實現Cloneable接口。因爲只有在Object類中的clone()被調用時才需要實現,而Address是不會調用clone()的,所以沒有實現Cloneable()的必要。
  • 這個clone()函數沒有聲明拋出CloneNotSupportedException。這個檢查異常只可能在調用Objectclone()的時候拋出。clone()是不會被調用的,因此這個異常也就沒有被處理或者傳回調用處的必要了。
  • Object類的clone()沒有被調用(這裏沒有調用super.clone())。因爲這不是對Address的對象進行淺克隆——只是一個成員變量複製而已。

爲了克隆Address的對象,需要創建一個新的Address對象並對其成員進行初始化操作。最後將新創建的Address對象返回。

編譯(javac CloneDemo.java)代碼清單6並且運行這個程序,你將會看到如下輸出結果(java CloneDemo):

1
2
3
4
John Doe: 49: Denver
John Doe: 49: Denver
John Doe: 49: Chicago
John Doe: 49: Denver

Q:如何克隆一個數組?

A:對數組類型進行淺克隆可以利用clone()方法。對數組使用clone()時,不必將clone()的返回值類型轉換爲數組類型,代碼清單7示範了數組克隆。

代碼清單7:對兩個數組進行淺克隆

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
classCity {
privateString name;
City(String name) {
this.name = name;
}
String getName() {
returnname;
}
voidsetName(String name) {
this.name = name;
}
}
publicclassCloneDemo {
publicstaticvoidmain(String[] args) {
double[] temps = { 98.632.0100.0212.053.5};
for(doubletemp : temps)
System.out.printf("%.1f ", temp);
System.out.println();
double[] temps2 = temps.clone();
for(doubletemp : temps2)
System.out.printf("%.1f ", temp);
System.out.println();
System.out.println();
City[] cities = { newCity("Denver"), newCity("Chicago") };
for(City city : cities)
System.out.printf("%s ", city.getName());
System.out.println();
City[] cities2 = cities.clone();
for(City city : cities2)
System.out.printf("%s ", city.getName());
System.out.println();
cities[0].setName("Dallas");
for(City city : cities2)
System.out.printf("%s ", city.getName());
System.out.println();
}
}

代碼清單7聲明瞭一個City類存儲名字,還有一些有關城市的數據(比如人口)。CloneDemo類提供了主函數main()來演示數組克隆。

main()函數首先聲明瞭一個雙精度浮點型數組來表示溫度。在輸出數組的值之後,克隆這個數組——注意沒有運算符。之後,輸出克隆的完全相同的數據。

緊接着,main()聲明瞭一個City對象的數組,輸出城市的名字,克隆這個數組,輸出克隆的這個數組中城市的名字。爲了證明淺克隆完成(比如,這兩個數組引用了相同的City對象),main()最後改變了原來的數組中第一個城市的名字,輸出第二個數組中所有城市的名字。我們馬上就可以看到,第二個數組中的名字也改變了。

編譯 (javac CloneDemo.java)並運行 (java CloneDemo)代碼清單7,你將會看到如下輸出結果:

1
2
3
4
5
6
98.6 32.0 100.0 212.0 53.5
98.6 32.0 100.0 212.0 53.5
Denver Chicago
Denver Chicago
Dallas Chicago

Equality

問:euqals()函數是用來做什麼的?

答:equals()函數可以用來檢查一個對象與調用這個equals()的這個對象是否相等。

問:爲什麼不用“==”運算符來判斷兩個對象是否相等呢?

答:雖然“==”運算符可以比較兩個數據是否相等,但是要來比較對象的話,恐怕達不到預期的結果。就是說,“==”通過是否引用了同一個對象來判斷兩個對象是否相等,這被稱爲“引用相等”。這個運算符不能通過比較兩個對象的內容來判斷它們是不是邏輯上的相等。

問:使用Object類的equals()方法可以用來做什麼樣的對比?

答:Object類默認的eqauls()函數進行比較的依據是:調用它的對象和傳入的對象的引用是否相等。也就是說,默認的equals()進行的是引用比較。如果兩個引用是相同的,equals()函數返回true;否則,返回false.

問:覆蓋equals()函數的時候要遵守那些規則?

答:覆蓋equals()函數的時候需要遵守的規則在Oracle官方的文檔中都有申明:

  • 自反性:對於任意非空的引用值x,x.equals(x)返回值爲真。
  • 對稱性:對於任意非空的引用值x和y,x.equals(y)必須和y.equals(x)返回相同的結果。
  • 傳遞性:對於任意的非空引用值x,y和z,如果x.equals(y)返回真,y.equals(z)返回真,那麼x.equals(z)也必須返回真。
  • 一致性:對於任意非空的引用值x和y,無論調用x.equals(y)多少次,都要返回相同的結果。在比較的過程中,對象中的數據不能被修改。
  • 對於任意的非空引用值x,x.equals(null)必須返回假。

問:能提供一個正確覆蓋equals()的示例嗎?

答:當然,請看代碼清單8。

代碼清單8:對兩個對象進行邏輯比較

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
classEmployee
{
privateString name;
privateintage;
Employee(String name, intage)
{
this.name = name;
this.age = age;
}
@Override
publicbooleanequals(Object o)
{
if(!(o instanceofEmployee))
returnfalse;
Employee e = (Employee) o;
returne.getName().equals(name) && e.getAge() == age;
}
String getName()
{
returnname;
}
intgetAge()
{
returnage;
}
}
publicclassEqualityDemo
{
publicstaticvoidmain(String[] args)
{
Employee e1 = newEmployee("John Doe"29);
Employee e2 = newEmployee("Jane Doe"33);
Employee e3 = newEmployee("John Doe"29);
Employee e4 = newEmployee("John Doe"27+2);
// 驗證自反性。
System.out.printf("Demonstrating reflexivity...%n%n");
System.out.printf("e1.equals(e1): %b%n", e1.equals(e1));
// 驗證對稱性。
System.out.printf("%nDemonstrating symmetry...%n%n");
System.out.printf("e1.equals(e2): %b%n", e1.equals(e2));
System.out.printf("e2.equals(e1): %b%n", e2.equals(e1));
System.out.printf("e1.equals(e3): %b%n", e1.equals(e3));
System.out.printf("e3.equals(e1): %b%n", e3.equals(e1));
System.out.printf("e2.equals(e3): %b%n", e2.equals(e3));
System.out.printf("e3.equals(e2): %b%n", e3.equals(e2));
// 驗證傳遞性。
System.out.printf("%nDemonstrating transitivity...%n%n");
System.out.printf("e1.equals(e3): %b%n", e1.equals(e3));
System.out.printf("e3.equals(e4): %b%n", e3.equals(e4));
System.out.printf("e1.equals(e4): %b%n", e1.equals(e4));
// 驗證一致性。
System.out.printf("%nDemonstrating consistency...%n%n");
for(inti = 0; i < code>5; i++)
{
System.out.printf("e1.equals(e2): %b%n", e1.equals(e2));
System.out.printf("e1.equals(e3): %b%n", e1.equals(e3));
}
// 驗證傳入非空集合時,返回值爲false。
System.out.printf("%nDemonstrating null check...%n%n");
System.out.printf("e1.equals(null): %b%n", e1.equals(null));
}
}

代碼清單8聲明瞭一個包含名字、年齡成員變量的Employee對象。這個對象覆蓋了equals()函數來對Employee對象進行適當的對比。

ps:覆蓋hashCode()函數
當覆蓋equals()函數的時候,就相當於覆蓋了hashCode()函數,我將在下篇文章討論hashCode()的時候詳細說明。

equals()函數首先要檢查傳入的確實是一個Employee對象。如果不是,返回false。這個檢查是靠instanceof運算來判斷的,當傳入null值的時候,同樣也返回false。因此,遵守了“對於任意的非空引用值x,x.equals(null)必須返回假”這個規則。

這樣,我們就保證了傳入的對象是Employee類型。因爲之前的instanceof判斷保證了傳入值必須是Employee類型的對象,所以在這裏我們就不必擔心拋出ClassCastException異常了。接下來,equals()方法對兩個對象的name和age的值進行了比較。

編譯(javac EqualityDemo.java)並運行(java EqualityDemo)代碼清單8,你可以看到以下輸出結果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
Demonstrating reflexivity...
e1.equals(e1): true
Demonstrating symmetry...
e1.equals(e2): false
e2.equals(e1): false
e1.equals(e3): true
e3.equals(e1): true
e2.equals(e3): false
e3.equals(e2): false
Demonstrating transitivity...
e1.equals(e3): true
e3.equals(e4): true
e1.equals(e4): true
Demonstrating consistency...
e1.equals(e2): false
e1.equals(e3): true
e1.equals(e2): false
e1.equals(e3): true
e1.equals(e2): false
e1.equals(e3): true
e1.equals(e2): false
e1.equals(e3): true
e1.equals(e2): false
e1.equals(e3): true
Demonstrating null check...
e1.equals(null): false

equals()和繼承

當Employee類被繼承的時候,代碼清單8就存在一些問題。例如,SaleRep類繼承了Employee類,這個類中也有基於字符串類型的變量,equals()可以對其進行比較。假設你創建的Employee對象和SaleRep對象都有相同的“名字”和“年齡”。但是,SaleRep中還是添加了一些內容。

假設你在Employee對象中調用equals()方法並且傳入了一個SaleRep對象。由於SaleRep對象繼承了Employee,也是一種Employee的對象,instanceof判斷會通過,並且執行equals()方法來判斷名字和年齡。因爲這兩個對象有完全相同的名字和年齡,所以equals()方法返回true。如果拿SaleRep對象中Employee的部分來和Employee比較的話,返回true值是正確的,但是,如果拿整個SaleRep對象來和Employee對象比較,返回true值就不妥了。

現在假設在SaleRep對象中調用equals()方法並將Employee傳入。因爲Employee不是SaleRep類型的對象(否則的話,你可以訪問Employee對象中不存在的Region域,這會導致虛擬機崩潰),無法通過instanceof判斷,equals()方法返回false。綜上,equals()在一種判斷中爲true卻在另一判斷中爲false,違背了“對稱性原則”。

Joshua Bloch在《Effective Java Programming Language Guide》第七版中指出:我們無法擴展可被實例化的類(例如Employee)並向其中增加一個域(如Region域),而同時維持equals()方法的對稱性。儘管也有辦法來維持對稱性,但代價是破壞傳遞性。Bloch指出解決這個問題需要在繼承上支持組合:不是讓SaleRep來擴展Employee,SaleRep應該引用一個私有的Employee值。獲得更多信息可以參考Bloch的書。

問:可以使用equals()函數來判斷兩個數組是否相等嗎?

答:可以調用equals()函數來比較數組的引用是否相等。但是,由於在數組對象中無法覆蓋equals(),所以只能對數組的引用進行比較,因爲不是很常用。參見代碼清單9。

代碼清單9:嘗試通過equals()函數來比較兩個數組

1
2
3
4
5
6
7
8
9
10
11
publicclassEqualityDemo
{
publicstaticvoidmain(String[] args)
{
intx[] = { 123};
inty[] = { 123};
System.out.printf("x.equals(x): %b%n", x.equals(x));
System.out.printf("x.equals(y): %b%n", x.equals(y));
}
}

代碼清單9的main()函數中聲明瞭一對類型與內容完全相等的數組。然後嘗試對第一個數組和它自己、第一個數組和第二個數組分別進行比較。由於equals()對數組來說比較的僅僅是引用,而不比較內容,所以x.equals(x)返回true(因爲自反性——一個對象與它自己相等),但是x.equals(y)返回false。

編譯(javac EqualityDemo.java) 並運行(java EqualityDemo)代碼清單9,你將會看到以下輸出結果:

1
2
x.equals(x): true
x.equals(y): false

如果你想要比較的是兩個數組的內容,也不要絕望。 可以使用java.util.Arrays 類中聲明的 static boolean deepEquals(Object[] a1, Object[] a2) 方法來實現。代碼清單10演示了這個方法。

代碼清單10:通過deepEquals()函數來比較兩個數組

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
importjava.util.Arrays;
publicclassEqualityDemo
{
publicstaticvoidmain(String[] args)
{
Integer x[] = { 123};
Integer y[] = { 123};
Integer z[] = { 321};
System.out.printf("x.equals(x): %b%n", Arrays.deepEquals(x, x));
System.out.printf("x.equals(y): %b%n", Arrays.deepEquals(x, y));
System.out.printf("x.equals(z): %b%n", Arrays.deepEquals(x, z));
}
}

由於deepEquals()方法要求傳入的數組元素必須是對象,所以之前在代碼清單9中的元素類型要從int[]改爲Integer[]。Java語言的自動封裝特性會把integer常量轉換成Integer對象存放在數組中。接下來要將數組傳入到deepEquals()就是小事一樁了。

編譯(javac EqualityDemo.java)並運行(java EqualityDemo)代碼清單10,你將看到以下輸出結果。

1
2
3
x.equals(x): true
x.equals(y): true
x.equals(z): false

用deepEquals()方法比較的相等是“深度”的相等:這要求每個元素對象所包含的的成員、對象相等。成員對象如果還包含了對象,也要相等,以此類推,纔算是“相等”(另外,兩個空的數組引用也是“深度”的相等,因此Arrays.deepEquals(null, null)返回true)。


終止

問:finalize()方法是用來做什麼的?

答:finalize()方法可以被子類對象所覆蓋,然後作爲一個終結者,當GC被調用的時候完成最後的清理工作(例如釋放系統資源之類)。這就是終止。默認的finalize()方法什麼也不做,當被調用時直接返回。

對於任何一個對象,它的finalize()方法都不會被JVM執行兩次。如果你想讓一個對象能夠被再次調用的話(例如,分配它的引用給一個靜態變量),注意當這個對象已經被GC回收的時候,finalize()方法不會被調用第二次。

問: 有人說要避免使用finalize()方法,這是真的嗎?

答: 通常來講,你應該儘量避免使用finalize()。相對於其他JVM實現,終結器被調用的情況較少——可能是因爲終結器線程的優先級別較低的原因。如果你依靠終結器來關閉文件或者其他系統資源,可能會將資源耗盡,當程序試圖打開一個新的文件或者新的系統資源的時候可能會崩潰,就因爲這個緩慢的終結器。

問: 應該使用什麼來替代終結器?

答: 提供一個明確的用來銷燬這個對象的方法(例如,java.io.FileInputStreamvoid close()方法),並且在代碼中使用try - finally結構來調用這個方法,以確保無論有沒有異常從try中拋出,都會銷燬這個對象。參考下面釋放鎖的代碼:

1
2
3
4
5
6
7
8
9
10
Lock l = ...; // ... is a placeholder for the actual lock-acquisition code
l.lock();
try
{
// access the resource protected by this lock
}
finally
{
l.unlock();
}

這段代碼保證了無論try是正常結束還是拋出異常都會釋放鎖。

問: 什麼情況下適合使用終結器?

答: 終結器可以作爲一個安全保障,以防止聲明的終結方法(像是java.io.FileOutputStream對象的close()方法或者java.util.concurrent.Lock對象的Lock()方法)沒有被調用。萬一這種情況出現,終結器可以在最後被調用,釋放臨街資源。

問: 怎麼寫finalize()

答: 可以遵循下面這個模式寫finalize()方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
@Override
protectedvoidfinalize() throwsThrowable
{
try
{
// Finalize the subclass state.
// ...
}
finally
{
super.finalize();
}
}

子類終結器一般會通過調用父類的終結器來實現。當被調用時,先執行try模塊,然後再在對應的finally中調用super.finalize();這就保證了無論try會不會拋出異常父類都會被銷燬。

問: 如果finalize()拋出異常會怎樣?

答: 當finalize()拋出異常的時候會被忽略。而且,對象的終結將在此停止,導致對象處在一種不確定的狀態。如果另一個進程試圖使用這個對象的話,將產生不確定的結果。通常拋出異常將會導致線程終止併產生一個提示信息,但是從finalize()中拋出異常就不會。

問: 我想實踐一下finalize()方法,能提供一個範例嗎?

答: 參考代碼清單1.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
classLargeObject
{
byte[] memory = newbyte[1024*1024*4];
@Override
protectedvoidfinalize() throwsException
{
System.out.println("finalized");
}
}
publicclassFinalizeDemo
{
publicstaticvoidmain(String[] args)
{
while(true)
newLargeObject();
}
}

代碼清單1:實踐finalize()

代碼清單1中的代碼寫了一個FinalizeDemo程序,重複地對largeObject類實例化。每一個Largeobject對象將產生4M的數組。在這種情況下,由於沒有指向該對象的引用,所以LargeObject對象將被GC回收。

GC會調用對象的finalize()方法來回收對象。LargeObject重載的finalize()方法被調用的時候會想標準輸出流打印一條信息。它沒有調用父類的finalize()方法,因爲它的父類是Object,即父類的finalize()方法什麼也不做。

編譯(javac FinalizeDemo.java)並運行(java FinalizeDemo)代碼清單1.當我在我的環境下(64位win7平臺)使用JDK7u6來編譯運行的時候,我看到一列finalized的信息。但是在JDK8的環境下時,在幾行finalized之後拋出了java.lang.OutOfMemoryError

因爲finalize()方法對於虛擬機來說不是輕量級的程序,所以不能保證你一定會在你的環境下觀察到輸出信息。

得到對象的類

問:gerClass()方法是用來做什麼的?

答: 通過gerClass()方法可以得到一個和這個類有關的java.lang.Class對象。返回的Class對象是一個被static synchronized方法封裝的代表這個類的對象;例如,static sychronized void foo(){}。這也是指向反射API。因爲調用gerClass()的對象的類是在內存中的,保證了類型安全。

問: 還有其他方法得到Class對象嗎?

答: 獲取Class對象的方法有兩種。可以使用類字面常量,它的名字和類型相同,後綴位.class;例如,Account.class。另外一種就是調用ClassfoeName()方法。類字面常量更加簡潔,並且編譯器強制類型安全;如果找不到指定的類編譯就不會通過。通過forname()可以動態地通過指定包名載入任意類型地引用。但是,不能保證類型安全,可能會導致Runtime異常。

問: 實現equals()方法的時候,getClass()instanceof哪一個更好?

答: 使用getClass()還是instanceof的話題一直都是Java社區爭論的熱點,Angelika Langer的Secrets of equals – Part 1這片文章可以幫助你做出選擇。關於正確覆蓋equals()方法(例如保證對稱性)的討論,Lang的這篇文章可以作爲一個很好的參考手冊。


哈希碼

問:hashCode()方法是用來做什麼的?

答:hashCode()方法返回給調用者此對象的哈希碼(其值由一個hash函數計算得來)。這個方法通常用在基於hash的集合類中,像java.util.HashMap,java.until.HashSetjava.util.Hashtable.

問: 在類中覆蓋equals()的時候,爲什麼要同時覆蓋hashCode()

答: 在覆蓋equals()的時候同時覆蓋hashCode()可以保證對象的功能兼容於hash集合。這是一個好習慣,即使這些對象不會被存儲在hash集合中。

問:hashCode()有什麼一般規則?

答:hashCode()的一般規則如下:

  • 在同一個Java程序中,對一個相同的對象,無論調用多少次hashCode()hashCode()返回的整數必須相同,因此必須保證equals()方法比較的內容不會更改。但不必在另一個相同的Java程序中也保證返回值相同。
  • 如果兩個對象用equals()方法比較的結果是相同的,那麼這兩個對象調用hashCode()應該返回相同的整數值。
  • 當兩個對象使用equals()方法比較的結果是不同的,hashCode()返回的整數值可以不同。然而,hashCode()的返回值不同可以提高哈希表的性能。

問: 如果覆蓋了equals()卻不覆蓋hashCode()會有什麼後果?

答: 當覆蓋equals()卻不覆蓋hashCode()的時候,在hash集合中存儲對象時就會出現問題。例如,參考代碼清單2.

代碼清單2:當hash集合只覆蓋equals()時的問題

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
importjava.util.HashMap;
importjava.util.Map;
finalclassEmployee
{
privateString name;
privateintage;
Employee(String name, intage)
{
this.name = name;
this.age = age;
}
@Override
publicbooleanequals(Object o)
{
if(!(o instanceofEmployee))
returnfalse;
Employee e = (Employee) o;
returne.getName().equals(name) && e.getAge() == age;
}
String getName()
{
returnname;
}
intgetAge()
{
returnage;
}
}
publicclassHashDemo
{
publicstaticvoidmain(String[] args)
{
Map map = newHashMap<>();
Employee emp = newEmployee("John Doe"29);
map.put(emp, "first employee");
System.out.println(map.get(emp));
System.out.println(map.get(newEmployee("John Doe"29)));
}
}

代碼清單2聲明瞭一個Employee類,覆蓋了equals()方法但是沒有覆蓋hashCode()。同時聲明瞭一個一個HashDemo類,來演示將Employee作爲鍵存儲時時產生的問題。

main()函數首先在實例化Employee之後創建了一個hashmap,將Employee對象作爲鍵,將一個字符串作爲值來存儲。然後它將這個對象作爲鍵來檢索這個集合並輸出結果。同樣地,再通過新建一個具有相同內容的Employee對象作爲鍵來檢索集合,輸出信息。

編譯(javac HashDemo.java)並運行(java HashDemo)代碼清單2,你將看到如下輸出結果:

1
2
first employee
null

如果hashCode()方法被正確的覆蓋,你將在第二行看到first employee而不是null,因爲這兩個對象根據equals()方法比較的結果是相同的,根據上文中提到的規則2:如果兩個對象用equals()方法比較的結果是相同的,那麼這兩個對象調用hashCode()應該返回相同的整數值。

問: 如何正確的覆蓋hashCode()

答: Joshua Bloch的《Effective Java》第八版中給出了一個四步法來正確的覆蓋hashCode()。下面的步驟和Bloch的方法類似。

  1. 聲明一個int型的變量,命名爲result(或者其他你喜歡的名字),然後初始化爲一個不爲零的常量(比如31)。使用一個不爲零的常量會影響到所有的初始的哈希值(步驟2.1的結果)爲零的值。【A nonzero value is used so that it will be affected by any initial fields whose hash value (computed in Step 2.1) is zero. 】如果初始的result0的話,最後的哈希值不會被它影響到,所以衝突的機率會增加。這個非零result值是任意的。
  2. 對每一個對象中有意義的具體值(在equals()中所涉及的值),f,進行以下步驟的處理:
    1. 按照以下步驟計算f的基於int型的哈希值hc
      1. 對於一個boolean型變量,hc = f? 0 : 1;
      2. 對於一個byte,char,short,或者int型變量,hc = (int)f;.
      3. 對於一個long型變量,hc = (int) (f ^ (f <<< 32));.這個表達式是將long型變量作爲32位(long型最多有32位)來計算的;
      4. 對於一個float型變量,hc = Float.floatToIntBits(f);.
      5. 對於一個double型變量,long l = Double.doubleToLongBits(f); hc = (int) (l ^ (l <<< 32));.
      6. 對於引用類型的變量,如果類中的equals()方法遞歸的調用equals()類比較成員變量,那麼就遞歸調用hashCode();如果需要更復雜的比較,就計算這個值的“標準表示”來腳痠標準的哈希值;如果引用類型的值爲nullf = 0.
      7. 對於一個數組類型的引用,將每一個元素視爲單獨的變量,對於每一個有意義的值,調用對應的方法計算其哈希值,最後如步驟2.2的描述那樣將所有的哈希值合併。
    2. 計算result = 37*result+hc,將所有的hc合併到哈希值中。乘法使哈希值取決於它的值的規則,當一個類中存在多種相似的值時,就增加了哈希表的離散性。
    3. 返回result。
    4. 完成hashCode()之後,要確保相同的對象調用hashCode()得到相同的哈希值。

舉例說明上面這個方法,代碼清單3是代碼清單2的第二個版本,它的Employee類重寫了hashCode()

代碼清單3:正確地覆蓋hashCode()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
importjava.util.HashMap;
importjava.util.Map;
finalclassEmployee
{
privateString name;
privateintage;
Employee(String name, intage)
{
this.name = name;
this.age = age;
}
@Override
publicbooleanequals(Object o)
{
if(!(o instanceofEmployee))
returnfalse;
Employee e = (Employee) o;
returne.getName().equals(name) && e.getAge() == age;
}
String getName()
{
returnname;
}
intgetAge()
{
returnage;
}
@Override
publicinthashCode()
{
intresult = 31;
result = 37*result+name.hashCode();
result = 37*result+age;
returnresult;
}
}
publicclassHashDemo
{
publicstaticvoidmain(String[] args)
{
Map map = newHashMap<>();
Employee emp = newEmployee("John Doe"29);
map.put(emp, "first employee");
System.out.println(map.get(emp));
System.out.println(map.get(newEmployee("John Doe"29)));
}
}

代碼清單3的Employee類中聲明瞭兩個在hashCode()都涉及到的值。覆蓋的hashCode()方法首先初始化result31,然後將String類型的name變量和int型的age變量的哈希值合併到result中,隨後返回result

編譯(javac HashDemo.java)並運行(java HashDemo)代碼清單3,你將看到如下輸出結果:

1
2
first employee
first employee

字符串形式的表現

Q1toString() 方法實現了什麼功能?
A1toString() 方法將根據調用它的對象返回其對象的字符串形式,通常用於debug。

Q2:當 toString() 方法沒有被覆蓋的時候,返回的字符串通常是什麼樣子的?
A2:當 toString() 沒有被覆蓋的時候,返回的字符串格式是 類名@哈希值,哈希值是十六進制的。舉例說,假設有一個 Employee 類,toString() 方法返回的結果可能是 Empoyee@1c7b0f4d

Q3:能提供一個正確覆蓋 toString() 方法的例子嗎?
A3:見代碼清單1:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
publicclassEmployee
{
privateString name;
privateintage;
publicEmployee(String name, intage)
{
this.name = name;
this.age = age;
}
@Override
publicString toString()
{
returnname + ": "+ age;
}
}

代碼清單1:返回一個非默認的字符串形式

代碼清單1聲明瞭 Employee 類,被私有修飾符修飾的 name 和 age 變量,構造器將其初始化。該類覆蓋了 toString() 方法,並返回一個包含對象值和一個冒號的 String 對象。

字符串和 StringBuilder

當編譯器遇到 name + ": " + age 的表達時,會生成一個 java.lang.StringBuilder 對象,並調用 append() 方法來對字符串添加變量值和分隔符。最後調用 toString() 方法返回一個包含各個元素的字符串對象。

Q4:如何得到字符串的表達形式?
A4:根據對象的引用,調用引用的 toString() 。例如,假設 emp 包含了一個 Employee 引用,調用 emp.toString() 就會得到這個對象的字符串形式。

Q5System.out.println(o.toString()); 和 System.out.println(o) 的區別是什麼?
A5System.out.println(o.toString()); 和 System.out.println(o) 兩者的輸出結果中都包含了對象的字符串形式。區別是,System.out.println(o.toString()); 直接調用toString() 方法,而System.out.println(o) 則是隱式調用了 toString()

等待和喚醒

Q6wait()notify() 和 notifyAll() 是用來幹什麼的?
A6wait()notify() 和 notifyAll() 可以讓線程協調完成一項任務。例如,一個線程生產,另一個線程消費。生產線程不能在前一產品被消費之前運行,而應該等待前一個被生產出來的產品被消費之後才被喚醒,進行生產。同理,消費線程也不能在生產線程之前運行,即不能消費不存在的產品。所以,應該等待生產線程執行一個之後才執行。利用這些方法,就可以實現這些線程之間的協調。從本質上說,一個線程等待某種狀態(例如一個產品被生產),另一個線程正在執行,知道產生了某種狀態(例如生產了一個產品)。

Q7:不同的 wait() 方法之間有什麼區別?
A7:沒有參數的 wait() 方法被調用之後,線程就會一直處於睡眠狀態,直到本對象(就是 wait() 被調用的那個對象)調用 notify() 或 notifyAll() 方法。相應的wait(long timeout)wait(long timeout, int nanos)方法中,當等待時間結束或者被喚醒時(無論哪一個先發生)將會結束等待。

Q8notify() 和 notifyAll() 方法有什麼區別?
A8notify() 方法隨機喚醒一個等待的線程,而 notifyAll() 方法將喚醒所有在等待的線程。

Q9:線程被喚醒之後會發生什麼?
A9:當一個線程被喚醒之後,除非本對象(調用 notify() 或 notifyAll() 的對象)的同步鎖被釋放,否則不會立即執行。喚醒的線程會按照規則和其他線程競爭同步鎖,得到鎖的線程將執行。所以notifyAll()方法執行之後,可能會有一個線程立即運行,也可能所有的線程都沒運行。

Q10:爲什麼在使用等待、喚醒方法時,要放在同步代碼中?
A10::將等待和喚醒方法放在同步代碼中是非常必要的,這樣做是爲了避免競爭條件。鑑於要等待的線程通常在調用wait()之前會確認一種情況存在與否(通常是檢查某一變量的值),而另一線程在調用notify()`之前通常會設置某種情況(通常是通過設置一個變量的值)。以下這種情況引發了競爭條件:

  1. 線程一檢查了情況和變量,發現需要等待。
  2. 線程二設置了變量。
  3. 線程二調用了notify()。此時,線程一還沒有等待,所以這次調用什麼用都沒有。
  4. 線程一調用了wait()。這下它永遠不會被喚醒了。

Q11:如果在同步代碼之外使用這些方法會怎麼樣呢?
A11:如果在同步代碼之外使用了這些情況,就會拋出java.lang.IllegalMonitorStateException異常。

Q12:如果在同步代碼中調用這些方法呢?
A12:當 wait() 方法在同步代碼中被調用時,會根據同步代碼中方法的優先級先後執行。在wait()方法返回值之前,該同步代碼一直持有鎖,這樣就不會出現競爭條件了。在wait()方法可以接受喚醒之前,鎖一直不會釋放。

Q13:爲什麼要把wait()調用放在while循環中,而不是if判斷中呢?
A13:爲了防止假喚醒,可以在 stackoverflow上了解有關這類現象的更多信息——假喚醒真的會發生嗎?

Q14:能提供一個使用等待與喚醒方法的範例嗎?
A14:見代碼清單2:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
publicclassWaitNotifyDemo
{
publicstaticvoidmain(String[] args)
{
classShared
{
privateString msg;
synchronizedvoidsend(String msg)
{
while(this.msg != null)
try
{
wait();
}
catch(InterruptedException ie)
{
}
this.msg = msg;
notify();
}
synchronizedString receive()
{
while(msg == null)
try
{
wait();
}
catch(InterruptedException ie)
{
}
String temp = msg;
msg = null;
notify();
returntemp;
}
}
finalShared shared = newShared();
Runnable rsender;
rsender = newRunnable()
{
@Override
publicvoidrun()
{
for(inti = 0; i < code>10; i++)
{
shared.send("A"+i);
try
{
Thread.sleep((int)(Math.random()*200));
}
catch(InterruptedException ie)
{
}
}
shared.send("done");
}
};
Thread sender = newThread(rsender);
Runnable rreceiver;
rreceiver = newRunnable()
{
@Override
publicvoidrun()
{
String msg;
while(!(msg = shared.receive()).equals("done"))
{
System.out.println(msg);
try
{
Thread.sleep((int)(Math.random()*200));
}
catch(InterruptedException ie)
{
}
}
}
};
Thread receiver = newThread(rreceiver);
sender.start();
receiver.start();    
}
}

代碼清單2:發送與接收信息

代碼清單2聲明瞭一個WaitNotifyDemo類。其中,main()方法有一對發送和接收信息的線程。

main()方法首先聲明瞭Shard本地類,包含接收和發送信息的任務。Share聲明瞭一個String類型的smg私有成員變量來存儲要發送的信息,同時聲明瞭同步的 send()receive()方法來執行接收和發送動作。

發送線程調用的是send()。因爲上一次調用send()的信息可能還沒有被接收到,所以這個方法首先要通過計算this.msg != null的值來判斷信息發送狀態。如果返回值爲true,那麼信息處於被等待發送的狀態,就會調用 wait() 。一旦信息被接收到,接受的線程就會給msg賦值爲null並存儲新信息,調用notify()喚醒等待的線程。

接收線程調用的是receive()因爲可能沒有信息處於被接收狀態,這個方法首先會通過計算mas == null的值來驗證信息有沒有等待被接收的狀態。如果表達式返回值爲true,就表示沒有信息等待被接收,此線程就要調用 wait() 方法。如果有信息發送,發送線程就會給 msg 分配值並且調用notify()喚醒接收線程。

編譯(javac WaitNotifyDemo.java)並運行(java WaitNotifyDemo)源代碼,將會看到以下輸出結果:

1
2
3
4
5
6
7
8
9
10
A0
A1
A2
A3
A4
A5
A6
A7
A8
A9

Q15:我想更加深入的學習等待和喚醒的機制,能提供一些資源嗎?
A15:可以在artima參考Bill Venners的書《Inside the Java Virtual Machine(深入理解 Java 虛擬機)》中第20章 Chapter 20: Thread Synchronization

Object接口和Java8

Q16:在第一部分中提到過接口是不繼承Object的。然而,我發現有些接口中聲明瞭Object中的方法。比如java.util.Comparator接口有boolean.equals(Object.obj)。爲什麼呢?
A16Java語言規範的9.6.3.4部分中清楚說明了,接口有相當於 Object 中成員那樣的公共抽象成員。此外,如果接口中聲明瞭Object中的成員函數(例如,聲明的函數相當於覆蓋 Object中的public方法),則認爲是接口覆蓋了他們,可以用 @Override註釋。

爲什麼要在接口中聲明非finalpublicObject方法(可能還帶有 @Override)呢?舉例來說,Comparator接口中就有boolean equals(Object obj)聲明,這個方法在接口中聲明就是爲了此接口的特殊情況。

此外,這個方法只有在傳入的類是一個比較規則相同的比較器的時候,才能返回 true

因爲這種情況是可選的,所以並不強制實現 Comparator。這取決於有沒有equals,只有在遇到一個比較規則相同的比較器的時候才返回true的需求。儘管類並不要求覆蓋equals,但是文檔中卻支持這樣做來提高性能。

注意,不覆蓋 Object.equals(Object)是安全的。但是,覆蓋這個方發可能在一些情況下提高性能,比如讓程序判斷兩個不同的比較器是不是用的相同規則。

Q17:哪一個Employee方法被覆蓋了?是Object中的,還是接口中的?
A17:更早的文檔中說,被覆蓋的方法是在Object中的。

Q18:Java 8支持接口中的默認方法。可以在接口中默認實現Employee方法或者Object中的其他方法嗎?
A18:不可以。Object中的任何public的非final方法都是不允許在接口中默認實現的。這個限制的基本原理在Brian Goetz的允許默認方法覆蓋Object中的方法一文中有說明。

Q19:能提供更多關於接口中 Object 方法的學習資源嗎?
A19:可以參考這篇接口繼承了Object類嗎?


原文鏈接: Javaworld 翻譯: ImportNew.com 賴 信濤
譯文鏈接: http://www.importnew.com/10304.html
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章