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
類嗎?。
原文鏈接: Javaworld 翻譯: ImportNew.com - 賴 信濤
譯文鏈接: http://www.importnew.com/10304.html