Java中所有的類都繼承自java.lang.Object
類,Object類中一共有11個方法:
public final native Class<?> getClass();
public native int hashCode();
public boolean equals(Object obj) {
return (this == obj);
}
protected native Object clone() throws CloneNotSupportedException;
public String toString() {
return getClass().getName() + "@" + Integer.toHexString(hashCode());
}
public final native void notify();
public final native void notifyAll();
public final native void wait(long timeout) throws InterruptedException;
public final void wait(long timeout, int nanos) throws InterruptedException {
if (timeout < 0) {
throw new IllegalArgumentException("timeout value is negative");
}
if (nanos < 0 || nanos > 999999) {
throw new IllegalArgumentException(
"nanosecond timeout value out of range");
}
if (nanos > 0) {
timeout++;
}
wait(timeout);
}
public final void wait() throws InterruptedException {
wait(0);
}
protected void finalize() throws Throwable { }
複製程式碼
getClass方法
這是一個native方法,並且是`final`的,也就是說這個方法不允許在子類中覆寫。
getClass方法返回的是當前例項對應的Class類,也就是說不管一個類有多少個例項,每個例項的getClass返回的Class物件是一樣的。請看下面的例子:
Integer i1 = new Integer(1);
Class i1Class = i1.getClass();
Integer i2 = new Integer(1);
Class i2Class = i2.getClass();
System.out.println(i1Class == i2Class);
複製程式碼
上面的程式碼執行結果為true
,也就是說兩個Integer的例項的getClass方法返回的Class物件是同一個。
Integer.class和int.class
Java中還有一個方法可以獲取Class,例如我們想獲取一個Integer例項對應的Class,可以直接通過Integer.class
來獲取,請看下面的例子:
Integer num = new Integer(1);
Class numClass = num.getClass();
Class integerClass = Integer.class;
System.out.println(numClass == integerClass);
複製程式碼
上面程式碼的執行結果為true
,也就是說通過呼叫例項的getClass方法和類.class
返回的Class是一樣的。與Integer物件的還有int型別的原生類,與Integer.class
對應,int.class
用於獲取一個原生型別int的Class。但是他們兩個返回的不是同一個物件。請看下面的例子:
Class intClass = int.class;
Class intTYPE = Integer.TYPE;
Class integerClass = Integer.class;
System.out.println(intClass == integerClass);
System.out.println(intClass == intTYPE);
複製程式碼
上面的程式碼執行結果是:
false
true
複製程式碼
Integer.class
和int.class
返回的不是一個物件,而int.class
返回的和Integer.TYPE
是同一個物件。Integer.TYPE
定義如下:
public static final Class<Integer> TYPE = (Class<Integer>) Class.getPrimitiveClass("int");
複製程式碼
Java中為原生型別(boolean,byte,char,short,int,long,float,double)和void都建立了一個預先定義好的型別,可以通過包裝類的TYPE靜態屬性獲取。上述的Integer類中TYPE和int.class
是等價的。
hashCode方法
物件的雜湊碼主要用於在雜湊表中的存放和查詢等。Java中對於物件hashCode方法的規約如下:
- 在java程式執行過程中,在一個物件沒有被改變的前提下,無論這個物件被呼叫多少次,hashCode方法都會返回相同的整數值。物件的雜湊碼沒有必要在不同的程式中保持相同的值。
- 如果2個物件使用equals方法進行比較並且相同的話,那麼這2個物件的hashCode方法的值也必須相等。
- 如果根據equals方法,得到兩個物件不相等,那麼這2個物件的hashCode值不需要必須不相同。但是,不相等的物件的hashCode值不同的話可以提高雜湊表的效能。
為了理解這三條規約,我們來先看一下HashMap中put一個entry的過程:
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null) // 這裡key的hashcode被用來定位當前entry在雜湊表中的index
tab[i] = newNode(hash, key, value, null); // 如果當前雜湊表中沒有key對應的entry,則直接插入
else {
// 當前雜湊表中已經有了key對應的entry了,則找到這個節點,然後看是否需要更新這個entry的value
Node<K,V> e; K k;
// 判斷當前節點就是已經存在的entry的條件:1:hashcode相等;2:當前節點的key和key是同一個物件(==)或者兩者的equals方法判定相等
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
// 是否需要更新舊值,如果需要更新則更新
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
複製程式碼
HashMap中put一個鍵值對時有以下幾個步驟:
- 計算key的雜湊碼,通過雜湊碼定位新增的entry應該處於雜湊表中的哪個位置,如果當前位置為null,則代表雜湊表中沒有key對應的entry,直接新插入一個節點就好了。
- 如果當前key在雜湊表中已經有了對映,則先查詢這個節點,判定當前是否為目標節點的條件有兩個:1)兩者的雜湊碼必須相等;2)兩者是同一個物件(==成立),或者兩者的equals方法判定兩者相等。
- 判斷是否需要更新舊值,需要的話就更新。
分析完了HashMap的put操作後,我們再來看看這三條規約:
- 第一條規約要求多次呼叫一個物件的hashCode方法返回的值要相等。設想如果一個key的hashCode方法每次返回值如果不同,則在put的時候就可能定位到雜湊表中不同的位置,就產生了歧義:明明兩個key是同一個,但是雜湊表中存在同一個key的多個不同對映。這就違背了雜湊表的key不能重複的原則了。
- 第二條規約也很好理解:如果兩個key的equals方法判定兩者相等,則說明雜湊表中只需要保留一個key就行了。如果equals判定相等,而hashcode不同,則就會違背這個事實。
- 第三條規約是用來優化雜湊表的效能的,如果雜湊表put時”碰撞”太多,勢必會造成查詢效能下降。
equals方法
equals方法用於判定兩個物件是否相等。Object中的equals方法其實預設比較的是兩個物件是否擁有相同的地址。也就是”==”對應的記憶體語義。
但是在子類中我們可以覆寫equals來判定子類的兩個例項是否為同一個物件。例如對於一個人來說,他有很多屬性,但是每個人的id是唯一的,如果兩個人的id一樣則證明兩個人是同一個人,而不管其他屬性是否一樣。這個時候我們就可以覆寫人的equals方法來保證這點了。
public class Person {
private long id;
private String name;
private int age;
private String nation;
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Person person = (Person) o;
return id == person.id; // 如果兩個person的id相同,則我們認為他們是同一個物件
}
@Override
public int hashCode() {
return Objects.hash(id);
}
}
複製程式碼
equals方法在非空物件引用上的特性:
- reflexive,自反性。任何非空引用值x,對於x.equals(x)必須返回true
- symmetric,對稱性。任何非空引用值x和y,如果x.equals(y)為true,那麼y.equals(x)也必須為true
- transitive,傳遞性。任何非空引用值x、y和z,如果x.equals(y)為true並且y.equals(z)為true,那麼x.equals(z)也必定為true
- consistent,一致性。任何非空引用值x和y,多次呼叫 x.equals(y) 始終返回 true 或始終返回 false,前提是物件上 equals 比較中所用的資訊沒有被修改
- 對於任何非空引用值 x,x.equals(null) 都應返回 false
Java要求一個類的equals方法和hashCode方法同時覆寫。我們剛剛分析了HashMap中對於key的處理過程:首先根據key的雜湊碼定位雜湊表中的位置,其次根據”==”或者equals方法判定兩個key是否相同。如果Person的equals方法沒有被覆寫,則兩個Person物件即使id一樣,但是不是指向同一塊記憶體地址,那麼雜湊表中就查詢不到已經存在的對映entry了。
clone方法
用於克隆一個物件,被克隆的物件需要implements Cloneable介面,否則呼叫這個物件的clone方法,將會丟擲CloneNotSupportedException異常。克隆的物件通常情況下滿足以下三條規則:
- x.clone() != x,克隆出來的物件和原來的物件不是同一個,指向不同的記憶體地址
- x.clone().getClass() == x.getClass()
- x.clone().equals(x)
一個物件進行clone時,原生型別和包裝型別的field的克隆原理不同。對於原生型別是直接複製一個,而對於包裝型別,則只是複製一個引用而已,並不會對引用型別本身進行克隆。
淺拷貝
淺拷貝例子:
public class ShallowCopy {
public static void main(String[] args){
Man man = new Man();
Man manShallowCopy = (Man) man.clone();
System.out.println(man == manShallowCopy);
System.out.println(man.name == manShallowCopy.name);
System.out.println(man.mate == manShallowCopy.mate);
}
}
class People implements Cloneable {
// primitive type
public int id;
// reference type
public String name;
@Override
public Object clone() {
try {
return super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return null;
}
}
class Man extends People implements Cloneable {
// reference type
public People mate = new People();
@Override
public Object clone() {
return super.clone();
}
}
複製程式碼
上面的程式碼的執行結果是:
false
true
true
複製程式碼
通過淺拷貝出來的Man物件manShallowCopy的name和mate屬性和原來的物件man都指向了相同的記憶體地址。在對Man的name和mate進行拷貝時淺拷貝只是對引用進行了拷貝,指向的還是同一塊記憶體地址。
深拷貝
對一個物件深拷貝時,對於物件的包裝型別的屬性,會對其再進行拷貝,從而達到深拷貝的目的,請看下面的例子:
public class DeepCopy {
public static void main(String[] args){
Man man = new Man();
Man manDeepCopy = (Man) man.clone();
System.out.println(man == manDeepCopy);
System.out.println(man.mate == manDeepCopy.mate);
}
}
class People implements Cloneable {
// primitive type
public int id;
// reference type
public String name;
@Override
public Object clone() {
try {
return super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return null;
}
}
class Man extends People implements Cloneable {
// reference type
public People mate = new People();
// 深拷貝Man
@Override
public Object clone() {
Man man = (Man) super.clone();
man.mate = (People) this.mate.clone(); // 再對mate屬性進行clone,從而達到深拷貝
return man;
}
}
複製程式碼
上面程式碼的執行結果為:
false
false
複製程式碼
Man物件的clone方法中,我們先對Man進行了clone,然後對mate屬性也進行了拷貝。因此man的mate和manDeepCopy的mate指向了不同的記憶體地址。也就是深拷貝。
通常來說對一個物件進行完完全全的深拷貝是不現實的,例如上面的例子中,雖然我們對Man的mate屬性進行了拷貝,但是無法對name(String型別)進行拷貝,拷貝的還是引用而已。
toString方法
Object中預設的toString方法如下:
public String toString() {
return getClass().getName() + "@" + Integer.toHexString(hashCode());
}
複製程式碼
也就是class的名稱+物件的雜湊碼。一般在子類中我們可以對這個方法進行覆寫。
notify方法
notify方法是一個final型別的native方法,子類不允許覆蓋這個方法。
notify方法用於喚醒正在等待當前物件監視器的執行緒,喚醒的執行緒是隨機的。一般notify方法和wait方法配合使用來達到多執行緒同步的目的。
在一個執行緒被喚醒之後,執行緒必須先重新獲取物件的監視器鎖(執行緒呼叫物件的wait方法之後會讓出物件的監視器鎖),才可以繼續執行。
一個執行緒在呼叫一個物件的notify方法之前必須獲取到該物件的監視器(synchronized),否則將丟擲IllegalMonitorStateException異常。同樣一個執行緒在呼叫一個物件的wait方法之前也必須獲取到該物件的監視器。
wait和notify使用的例子:
Object lock = new Object();
Thread t1 = new Thread(() -> {
synchronized (lock) {
System.out.println(Thread.currentThread().getName() + " is going to wait on lock`s monitor");
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " relinquishes the lock`s monitor");
}
});
Thread t2 = new Thread(() -> {
synchronized (lock) {
System.out.println(Thread.currentThread().getName() + " is going to notify a thread that waits on lock`s monitor");
lock.notify();
}
});
t1.start();
t2.start();
複製程式碼
notifyAll方法
notifyAll方法用於喚醒所有等待物件監視器鎖的執行緒,notify只喚醒所有等待執行緒中的一個。
同樣,如果當前執行緒不是物件監視器的所有者,那麼呼叫notifyAll同樣會發生IllegalMonitorStateException異常。
wait方法
public final native void wait(long timeout) throws InterruptedException;
複製程式碼
wait方法一般和上面說的notify方法搭配使用。一個執行緒呼叫一個物件的wait方法後,執行緒將進入WAITING
狀態或者TIMED_WAITING
狀態。直到其他執行緒喚醒這個執行緒。
執行緒在呼叫物件的wait方法之前必須獲取到這個物件的monitor鎖,否則將丟擲IllegalMonitorStateException異常。執行緒的等待是支援中斷的,如果執行緒在等待過程中,被其他執行緒中斷,則丟擲InterruptedException異常。
如果wait方法的引數timeout為0,代表等待過程是不會超時的,直到其他執行緒notify或者被中斷。如果timeout大於0,則代表等待支援超時,超時之後執行緒自動被喚醒。
finalize方法
protected void finalize() throws Throwable { }
複製程式碼
垃圾回收器在回收一個無用的物件的時候,會呼叫物件的finalize方法,我們可以覆寫物件的finalize方法來做一些清除工作。下面是一個finalize的例子:
public class FinalizeExample {
public static void main(String[] args) {
WeakReference<FinalizeExample> weakReference = new WeakReference<>(new FinalizeExample());
weakReference.get();
System.gc();
System.out.println(weakReference.get() == null);
}
@Override
public void finalize() {
System.out.println("I`m finalized!");
}
}
複製程式碼
輸出結果:
I`m finalized!
true
複製程式碼
finalize方法中物件其實還可以”自救”,避免垃圾回收器將其回收。在finalize方法中通過建立this的一個強引用來避免GC:
public class FinalizeExample {
private static FinalizeExample saveHook;
public static void main(String[] args) {
FinalizeExample.saveHook = new FinalizeExample();
saveHook = null;
System.gc();
System.out.println(saveHook == null);
}
@Override
public void finalize() {
System.out.println("I`m finalized!");
saveHook = this;
}
}
複製程式碼
輸出結果:
I`m finalized!
false
複製程式碼