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
|
import java.lang.Object; public class Employee extends Object
{
private String
name;
public Employee(String
name) {
this .name
= name;
}
public String
getName() {
return name;
}
public static void main(String[]
args) {
Employee
emp = new Employee( "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
|
public class Employee {
private String
name;
public Employee(String
name)
{
this .name
= name;
}
public String
getName()
{
return name;
}
public static void main(String[]
args)
{
Employee
emp = new Employee( "John
Doe" );
System.out.println(emp.getName());
} } |
就像代碼清單1一樣,這裏的Employee
類繼承了Object
,所以可以使用它的函數。
克隆Object
類
問:clone()
函數是用來做什麼的?
答:clone()
可以產生一個相同的類並且返回給調用者。
問:clone()
是如何工作的?
答:Object
將clone()
作爲一個本地方法來實現,這意味着它的代碼存放在本地的庫中。當代碼執行的時候,將會檢查調用對象的類(或者父類)是否實現了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
|
public class CloneDemo implements Cloneable
{
int x;
public static void main(String[]
args) throws CloneNotSupportedException
{
CloneDemo
cd = new CloneDemo();
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()
了。否則,將會看到“clone
在Object
中是被保護的”提示,因爲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
|
class Data implements Cloneable
{
int x;
@Override
public Object
clone() throws CloneNotSupportedException
{
return super .clone();
} } public class CloneDemo
{
public static void main(String[]
args) throws CloneNotSupportedException
{
Data
data = new Data();
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
|
class Employee implements Cloneable
{
private String
name;
private int age;
private Address
address;
Employee(String
name, int age,
Address address) {
this .name
= name;
this .age
= age;
this .address
= address;
}
@Override
public Object
clone() throws CloneNotSupportedException
{
return super .clone();
}
Address
getAddress() {
return address;
}
String
getName() {
return name;
}
int getAge()
{
return age;
} } class Address
{
private String
city;
Address(String
city) {
this .city
= city;
}
String
getCity() {
return city;
}
void setCity(String
city) {
this .city
= city;
} } public class CloneDemo
{
public static void main(String[]
args) throws CloneNotSupportedException
{
Employee
e = new Employee( "John
Doe" , 49 , new Address( "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給出了Employee
、Address
和CloneDemo
類。Employee
聲明瞭name
、age
、address
成員變量,是可以被克隆的類;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
|
class Employee implements Cloneable {
private String name;
private int age;
private Address
address;
Employee( String name, int age,
Address address)
{
this .name
= name;
this .age
= age;
this .address
= address;
}
@Override
public Employee
clone() throws CloneNotSupportedException
{
Employee
e = (Employee) super .clone();
e.address
= address.clone();
return e;
}
Address
getAddress()
{
return address;
}
String getName()
{
return name;
}
int getAge()
{
return age;
} } class Address {
private String city;
Address( String city)
{
this .city
= city;
}
@Override
public Address
clone()
{
return new Address( new String (city));
}
String getCity()
{
return city;
}
void setCity( String city)
{
this .city
= city;
} } public class CloneDemo {
public static void main( String []
args) throws CloneNotSupportedException
{
Employee
e = new Employee( "John
Doe" , 49 , new Address( "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
。這個檢查異常只可能在調用Object
類clone()
的時候拋出。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
|
class City
{
private String
name;
City(String
name) {
this .name
= name;
}
String
getName() {
return name;
}
void setName(String
name) {
this .name
= name;
} } public class CloneDemo
{
public static void main(String[]
args) {
double []
temps = { 98.6 , 32.0 , 100.0 , 212.0 , 53.5 };
for ( double temp
: temps)
System.out.printf( "%.1f
" , temp);
System.out.println();
double []
temps2 = temps.clone();
for ( double temp
: temps2)
System.out.printf( "%.1f
" , temp);
System.out.println();
System.out.println();
City[]
cities = { new City( "Denver" ), new City( "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
|
class Employee { private String
name; private int age; Employee(String name, int age) { this .name
= name; this .age
= age; } @Override public boolean equals(Object
o) { if (!(o instanceof Employee)) return false ; Employee e = (Employee) o; return e.getName().equals(name)
&& e.getAge() == age; } String getName() { return name; } int getAge() { return age; } } public class EqualityDemo { public static void main(String[]
args) { Employee e1 = new Employee( "John
Doe" , 29 ); Employee e2 = new Employee( "Jane
Doe" , 33 ); Employee e3 = new Employee( "John
Doe" , 29 ); Employee e4 = new Employee( "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 ( int i
= 0 ;
i < code>
|
代碼清單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
|
public class EqualityDemo { public static void main(String[]
args) { int x[]
= { 1 , 2 , 3 }; int y[]
= { 1 , 2 , 3 }; 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
|
import java.util.Arrays; public class EqualityDemo { public static void main(String[]
args) { Integer x[] = { 1 , 2 , 3 }; Integer y[] = { 1 , 2 , 3 }; Integer z[] = { 3 , 2 , 1 }; 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.FileInputStream
的void
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 protected void finalize() throws Throwable { 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
|
class LargeObject { byte []
memory = new byte [ 1024 * 1024 * 4 ]; @Override protected void finalize() throws Exception { System.out.println( "finalized" ); } } public class FinalizeDemo { public static void main(String[]
args) { while ( true ) new LargeObject(); } } |
代碼清單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
。另外一種就是調用Class
的foeName()
方法。類字面常量更加簡潔,並且編譯器強制類型安全;如果找不到指定的類編譯就不會通過。通過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.HashSet
和java.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
|
import java.util.HashMap; import java.util.Map; final class Employee { private String
name; private int age; Employee(String name, int age) { this .name
= name; this .age
= age; } @Override public boolean equals(Object
o) { if (!(o instanceof Employee)) return false ; Employee e = (Employee) o; return e.getName().equals(name)
&& e.getAge() == age; } String getName() { return name; } int getAge() { return age; } } public class HashDemo { public static void main(String[]
args) { Map map = new HashMap<>(); Employee emp = new Employee( "John
Doe" , 29 ); map.put(emp, "first
employee" ); System.out.println(map.get(emp)); System.out.println(map.get( new Employee( "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的方法類似。
- 聲明一個
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. 】如果初始的result
爲0
的話,最後的哈希值不會被它影響到,所以衝突的機率會增加。這個非零result
值是任意的。 - 對每一個對象中有意義的具體值(在
equals()
中所涉及的值),f
,進行以下步驟的處理:- 按照以下步驟計算f的基於
int
型的哈希值hc
:- 對於一個
boolean
型變量,hc = f? 0 : 1;
。 - 對於一個
byte
,char
,short
,或者int
型變量,hc = (int)f;
. - 對於一個
long
型變量,hc = (int) (f ^ (f <<< 32));
.這個表達式是將long
型變量作爲32位(long
型最多有32位)來計算的; - 對於一個
float
型變量,hc = Float.floatToIntBits(f);
. - 對於一個
double
型變量,long l = Double.doubleToLongBits(f); hc = (int) (l ^ (l <<< 32));
. - 對於引用類型的變量,如果類中的
equals()
方法遞歸的調用equals()
類比較成員變量,那麼就遞歸調用hashCode()
;如果需要更復雜的比較,就計算這個值的“標準表示”來腳痠標準的哈希值;如果引用類型的值爲null
,f = 0
. - 對於一個數組類型的引用,將每一個元素視爲單獨的變量,對於每一個有意義的值,調用對應的方法計算其哈希值,最後如步驟2.2的描述那樣將所有的哈希值合併。
- 對於一個
- 計算
result = 37*result+hc
,將所有的hc
合併到哈希值中。乘法使哈希值取決於它的值的規則,當一個類中存在多種相似的值時,就增加了哈希表的離散性。 - 返回result。
- 完成
hashCode()
之後,要確保相同的對象調用hashCode()
得到相同的哈希值。
- 按照以下步驟計算f的基於
舉例說明上面這個方法,代碼清單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
|
import java.util.HashMap; import java.util.Map; final class Employee { private String
name; private int age; Employee(String name, int age) { this .name
= name; this .age
= age; } @Override public boolean equals(Object
o) { if (!(o instanceof Employee)) return false ; Employee e = (Employee) o; return e.getName().equals(name)
&& e.getAge() == age; } String getName() { return name; } int getAge() { return age; } @Override public int hashCode() { int result
= 31 ; result = 37 *result+name.hashCode(); result = 37 *result+age; return result; } } public class HashDemo { public static void main(String[]
args) { Map map = new HashMap<>(); Employee emp = new Employee( "John
Doe" , 29 ); map.put(emp, "first
employee" ); System.out.println(map.get(emp)); System.out.println(map.get( new Employee( "John
Doe" , 29 ))); } } |
代碼清單3的Employee
類中聲明瞭兩個在hashCode()
都涉及到的值。覆蓋的hashCode()
方法首先初始化result
爲31
,然後將String
類型的name
變量和int
型的age
變量的哈希值合併到result
中,隨後返回result
。
編譯(javac HashDemo.java
)並運行(java
HashDemo
)代碼清單3,你將看到如下輸出結果:
1
2
|
first employee first employee |
字符串形式的表現
Q1:toString()
方法實現了什麼功能?
A1:toString()
方法將根據調用它的對象返回其對象的字符串形式,通常用於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
|
public class Employee { private String
name; private int age; public Employee(String
name, int age) { this .name
= name; this .age
= age; } @Override public String
toString() { return name
+ ": " +
age; } } |
代碼清單1:返回一個非默認的字符串形式
代碼清單1聲明瞭 Employee
類,被私有修飾符修飾的 name
和 age
變量,構造器將其初始化。該類覆蓋了 toString()
方法,並返回一個包含對象值和一個冒號的 String
對象。
字符串和
StringBuilder
當編譯器遇到
name + ": " + age
的表達時,會生成一個java.lang.StringBuilder
對象,並調用append()
方法來對字符串添加變量值和分隔符。最後調用toString()
方法返回一個包含各個元素的字符串對象。
Q4:如何得到字符串的表達形式?
A4:根據對象的引用,調用引用的 toString()
。例如,假設 emp
包含了一個 Employee
引用,調用 emp.toString()
就會得到這個對象的字符串形式。
Q5:System.out.println(o.toString());
和 System.out.println(o)
的區別是什麼?
A5:System.out.println(o.toString());
和 System.out.println(o)
兩者的輸出結果中都包含了對象的字符串形式。區別是,System.out.println(o.toString());
直接調用toString()
方法,而System.out.println(o)
則是隱式調用了 toString()
。
等待和喚醒
Q6:wait()
,notify()
和 notifyAll()
是用來幹什麼的?
A6:wait()
,notify()
和 notifyAll()
可以讓線程協調完成一項任務。例如,一個線程生產,另一個線程消費。生產線程不能在前一產品被消費之前運行,而應該等待前一個被生產出來的產品被消費之後才被喚醒,進行生產。同理,消費線程也不能在生產線程之前運行,即不能消費不存在的產品。所以,應該等待生產線程執行一個之後才執行。利用這些方法,就可以實現這些線程之間的協調。從本質上說,一個線程等待某種狀態(例如一個產品被生產),另一個線程正在執行,知道產生了某種狀態(例如生產了一個產品)。
Q7:不同的 wait()
方法之間有什麼區別?
A7:沒有參數的 wait()
方法被調用之後,線程就會一直處於睡眠狀態,直到本對象(就是 wait()
被調用的那個對象)調用 notify()
或 notifyAll()
方法。相應的wait(long
timeout)
和wait(long timeout, int nanos)
方法中,當等待時間結束或者被喚醒時(無論哪一個先發生)將會結束等待。
Q8:notify()
和 notifyAll()
方法有什麼區別?
A8:notify()
方法隨機喚醒一個等待的線程,而 notifyAll()
方法將喚醒所有在等待的線程。
Q9:線程被喚醒之後會發生什麼?
A9:當一個線程被喚醒之後,除非本對象(調用 notify()
或 notifyAll()
的對象)的同步鎖被釋放,否則不會立即執行。喚醒的線程會按照規則和其他線程競爭同步鎖,得到鎖的線程將執行。所以notifyAll()
方法執行之後,可能會有一個線程立即運行,也可能所有的線程都沒運行。
Q10:爲什麼在使用等待、喚醒方法時,要放在同步代碼中?
A10::將等待和喚醒方法放在同步代碼中是非常必要的,這樣做是爲了避免競爭條件。鑑於要等待的線程通常在調用
wait()之前會確認一種情況存在與否(通常是檢查某一變量的值),而另一線程在調用
notify()`之前通常會設置某種情況(通常是通過設置一個變量的值)。以下這種情況引發了競爭條件:
- 線程一檢查了情況和變量,發現需要等待。
- 線程二設置了變量。
- 線程二調用了
notify()
。此時,線程一還沒有等待,所以這次調用什麼用都沒有。 - 線程一調用了
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
|
public class WaitNotifyDemo { public static void main(String[]
args) { class Shared { private String
msg; synchronized void send(String
msg) { while ( this .msg
!= null ) try { wait(); } catch (InterruptedException
ie) { } this .msg
= msg; notify(); } synchronized String
receive() { while (msg
== null ) try { wait(); } catch (InterruptedException
ie) { } String temp = msg; msg = null ; notify(); return temp; } } final Shared
shared = new Shared(); Runnable rsender; rsender = new Runnable() { @Override public void run() { for ( int i
= 0 ;
i < code>
|
代碼清單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)
。爲什麼呢?
A16:Java語言規範的9.6.3.4部分中清楚說明了,接口有相當於 Object
中成員那樣的公共抽象成員。此外,如果接口中聲明瞭Object
中的成員函數(例如,聲明的函數相當於覆蓋 Object
中的public
方法),則認爲是接口覆蓋了他們,可以用 @Override
註釋。
爲什麼要在接口中聲明非final
的public
Object
方法(可能還帶有 @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
類嗎?。
譯文鏈接: http://www.importnew.com/10304.html