本文源碼都基于JDK1.8
概述
Java是一門面向?qū)ο蟮木幊陶Z言,在Java的世界里,萬物皆對象。而Object是一切對象的祖先。所以理解Object的常用方法就非常必要了,下面是Object的成員方法圖解:

問題
1、hashCode()方法的作用是什么?
2、equals()方法和hashCode()方法的關(guān)聯(lián)是什么?
3、equals()與“==”的區(qū)別是什么?
4、native關(guān)鍵字的作用是什么?
5、clone()的深拷貝和淺拷貝的區(qū)別是什么?
6、線程相關(guān)方法的用法
Native關(guān)鍵字
在初次見到Native關(guān)鍵字的時(shí)候,我還懵比,這個(gè)Native是干啥的,我怎么從來沒用過。為什么我找不到被Native修飾的方法的實(shí)現(xiàn)呢?
其實(shí)Native關(guān)鍵字是JNI的一部分,JNI全稱是Java Native Interface。JNI是JDK的一部分,它允許Java代碼與其他語言代碼進(jìn)行交互。Java本身是運(yùn)行在虛擬機(jī)上的,Java本身是不允許直接訪問硬件的,這就引出了Native關(guān)鍵字。
用Native修飾的方法,在虛擬機(jī)里都有一個(gè)與之同名的函數(shù),去做Java想要做的事情。 使用native關(guān)鍵字說明這個(gè)方法是原生函數(shù),也就是這個(gè)方法是用C/C++語言實(shí)現(xiàn)的,并且被編譯成了DLL,由java去調(diào)用。這些函數(shù)的實(shí)現(xiàn)體在DLL中,JDK的源代碼中并不包含,對于不同的平臺(tái)它們也是不同的。這也是java的底層機(jī)制,實(shí)際上java就是在不同的平臺(tái)上調(diào)用不同的native方法實(shí)現(xiàn)對操作系統(tǒng)的訪問的
hashCode()
hashCode()是一個(gè)native本地方法,其實(shí)默認(rèn)的hashCode()方法返回的就是對象對應(yīng)的內(nèi)存地址。當(dāng)我們在一些場景下復(fù)寫了hashCode()方法后,例如需要使用map來存放對象的時(shí)候,覆寫后hashCode返回的就不是對象的內(nèi)存地址了。
hash算法簡介
hash 算法,又被稱為散列算法,基本上,哈希算法就是將對象本身的鍵值,通過特定的數(shù)學(xué)函數(shù)運(yùn)算或者使用其他方法,轉(zhuǎn)化成相應(yīng)的數(shù)據(jù)存儲(chǔ)地址的。而哈希法所使用的數(shù)學(xué)函數(shù)就被稱為 『哈希函數(shù)』又可以稱之為散列函數(shù)。在常見的 hash 函數(shù)中有一種最簡單的方法交「除留余數(shù)法」,操作方法就是將要存入數(shù)據(jù)除以某個(gè)常數(shù)后,使用余數(shù)作為索引值。 下面看個(gè)例子:
將 323 ,458 ,25 ,340 ,28 ,969, 77 使用「除留余數(shù)法」存儲(chǔ)在長度為11的數(shù)組中。我們假設(shè)上邊說的某個(gè)常數(shù)即為數(shù)組長度11。 每個(gè)數(shù)除以11以后存放的位置如下圖所示:

試想一下我們現(xiàn)在想要拿到 77 在數(shù)組中的位置,是不是只需要 arr[77%11] = 77 就可以了。但是上述簡單的 hash 算法,缺點(diǎn)也是很明顯的,比如 77 和 88 對 11 取余數(shù)得到的值都是 0,但是角標(biāo)為 0 位置已經(jīng)存放了 77 這個(gè)數(shù)據(jù),那88就不知道該去哪里了。上述現(xiàn)象在哈希法中有個(gè)名詞叫碰撞:
碰撞:若兩個(gè)不同的數(shù)據(jù)經(jīng)過相同哈希函數(shù)運(yùn)算后,得到相同的結(jié)果,那么這種現(xiàn)象就做碰撞。
于是在設(shè)計(jì) hash 函數(shù)的時(shí)候我們就要盡可能做到:
降低碰撞的可能性。盡量將要存入的元素經(jīng)過 hash 函數(shù)運(yùn)算后的結(jié)果,盡量能夠均勻的分布在指定的容器(我們在稱之為桶)。
前文說了 hashCode 方法與 java 中使用散列表的集合類息息相關(guān),我們拿 Set 來舉例,我們都知道 Set 中是不允許存放重復(fù)的元素的。那么我們憑借什么來判斷已有的 Set 集合中是否有何要存入的元素重復(fù)的元素呢?有人可能會(huì)說我們可以通過 equals 來判斷兩個(gè)元素是否相同。那么問題又來,如果 Set 中已經(jīng)有 10000個(gè)元素了,那么之后在存入一個(gè)元素豈不是要調(diào)用 10000 次 equals 方法。顯然這不合理,性能低到令人發(fā)指。那要怎么辦才能保證即高效又不重復(fù)呢?答案就在于 hashCode 這個(gè)函數(shù)。經(jīng)過之前的分析我們知道 hash 算法是使用特定的運(yùn)算來得到數(shù)據(jù)的存儲(chǔ)位置的,那么 hashCode 方法就充當(dāng)了這個(gè)特定的函數(shù)運(yùn)算。這里我們可以簡單認(rèn)為調(diào)用 hashCode 方法后得到數(shù)值就是元素的存儲(chǔ)位置(其實(shí)集合內(nèi)部還做了進(jìn)一步的運(yùn)算,以保證盡可能的均勻分布在桶內(nèi))。當(dāng) Set 需要存放一個(gè)元素的時(shí)候,首先會(huì)調(diào)用 hashCode 方法去查看對應(yīng)的地址上有沒有存放元素,如果沒有則表示 Set 中肯定沒有相同的元素,直接存放在對應(yīng)位置就好,但是如果 hashCode 的結(jié)果相同,即發(fā)生了碰撞,那么我們在進(jìn)一步調(diào)用該位置元素的 equals 方法與要存放的元素進(jìn)行比較,如果相同就不存了,如果不相同就需要進(jìn)一步散列其它的地址。這樣我們就可以盡可能高效的保證了無重復(fù)元素的方法。
equals()
equals 方法屬于Object基類的方法,所有的對象都擁有這個(gè)方法,并有權(quán)重寫該方法。該方法返回了一個(gè)boolean類型的結(jié)果,代表比較的兩個(gè)對象是否相同。事實(shí)上很多java定義好的一些引用數(shù)據(jù)類型,都重寫了equals 方法。當(dāng)我們自定義引用數(shù)據(jù)類型的時(shí)候,如果判定兩個(gè)對象相等,需要根據(jù)具體的業(yè)務(wù)規(guī)則而定,但是必須遵循以下規(guī)則;
自反性(reflexive):對于任意不為null 的引用值x,x.equals(x)一定為true;
對稱性(symmetric): 對于任意不為null的引用值x和y,當(dāng)且僅當(dāng)x.equals(y)為true時(shí),y.equals(x) 為true;
傳遞性(transitive): 對于任意不為null的引用值x、y和z,如果x.equals(y)為true同時(shí)y.equal(z)為true,那么x.equals(z)也為true;
一致性(consistent):對于任意不為null的引用值x和y,如果用于equals比較的對象信息沒有被修改的話,多次調(diào)用時(shí) x.equals(y) 要么一致地返回 true 要么一致地返回 false。
null值要求:對于任意不為 null 的引用值 x,x.equals(null) 返回 false。
equals 與 == 的區(qū)別
java數(shù)據(jù)類型可以分為基礎(chǔ)數(shù)據(jù)類型和引用數(shù)據(jù)類型?;A(chǔ)數(shù)據(jù)類型包括short,int,byte,long,dubble,float,boolean,char八種。對于基礎(chǔ)數(shù)據(jù)類型,==判斷的是左右兩邊的值。
int a = 10;
int b = 10;
float c = 10.0f;
//以下輸出結(jié)果均為 true
System.out.println("(a == b) = " + (a == b));
System.out.println("(b == c) = " + (b == c));
而對于引用數(shù)據(jù)類型,==操作符判斷的就是左右兩邊對象的內(nèi)存地址是否相同。也就是說通過==判斷的兩個(gè)引用數(shù)據(jù)類型,如果相等,那么他們指向的肯定是同一個(gè)對象。
可以總結(jié)出兩者比較的結(jié)果如下:
1、如果==兩邊都是基礎(chǔ)數(shù)據(jù)類型,那么比較的是兩個(gè)的值是否相等;
2、如果==兩邊都是引用數(shù)據(jù)類型,那么比較的是兩者的內(nèi)存地址是否相同。若相同,則左右兩邊的是同一個(gè)對象。
3、Object基類的equals默認(rèn)比較的是兩者的內(nèi)存地址是否相等。在構(gòu)建的對象沒有重寫equals對象時(shí),equals與==作用相同。
4、equals用于比較引用數(shù)據(jù)類型是否相等。在滿足equals判斷規(guī)則的前提下,兩個(gè)對象只要規(guī)定的屬性相同,那么就認(rèn)為兩個(gè)對象是相同的。
equals 與 hashCode的關(guān)系
1、如果兩個(gè)對象調(diào)用equals方法返回的true,那么他們的hashCode一定相同;
2、如果兩個(gè)對象的hashCode相同,他們卻不一定是同一個(gè)對象,調(diào)用equals方法,不一定為true;但是如果兩個(gè)對象的hashCode不相同,那么他們一定不是同一個(gè)對象,調(diào)用equals方法一定返回false;
clone 深淺拷貝
在某些情況下,我們需要獲取一個(gè)對象的拷貝來處理某些事情。這個(gè)時(shí)候就需要用到object.clone方法,要是用clone方法的類,必須實(shí)現(xiàn)cloneable接口,才能夠使用clone方法,否則在使用時(shí)會(huì)拋出CloneNotSupportedException。而我們在實(shí)際應(yīng)用中可能會(huì)發(fā)現(xiàn),當(dāng)對象中包含可變的引用數(shù)據(jù)類型時(shí),在拷貝得到的新對象中對該引用數(shù)據(jù)類型的屬性進(jìn)行修改,原始對象相應(yīng)的屬性也會(huì)發(fā)生變化,這種現(xiàn)象就是“淺拷貝”。object默認(rèn)的clone方法就是淺拷貝。
在了解淺拷貝和深拷貝之前,我們需要先了解一點(diǎn)鋪墊知識(shí):Java中的數(shù)據(jù)類型分為基礎(chǔ)數(shù)據(jù)類型和引用數(shù)據(jù)類型。這兩種類型在進(jìn)行賦值操作和作為方法參數(shù)或返回值時(shí),會(huì)有值傳遞和引用地址傳遞的差別。
淺拷貝
我們來寫一個(gè)例子看一下clone()方法的淺拷貝現(xiàn)象:
/**
* @author xiongchenyang
* @Date 2019/6/21
**/
public class CloneTest {
public static void main(String[] args) throws CloneNotSupportedException {
Student student = new Student("張三","男",28 ,new Address("深圳","南海大道"));
Student cloneStudent = (Student) student.clone();
System.out.println("student的地址:"+student);
System.out.println("cloneStudent的地址:"+cloneStudent);
cloneStudent.setAge(44);
Address address = cloneStudent.getAddress();
address.setProvince("北京");
System.out.println("修改cloneStudent后結(jié)果為:");
System.out.println("cloneStudent:" + cloneStudent.display());
System.out.println("student:"+student.display());
}
static class Student implements Cloneable{
private String name;
private String sex;
private Integer age;
private Address address;
private Student(String name, String sex, Integer age, Address address) {
this.name = name;
this.sex = sex;
this.age = age;
this.address = address;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getSex() {
return sex;
}
public void setSex(String sex) {
this.sex = sex;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
public Address getAddress() {
return address;
}
public void setAddress(Address address) {
this.address = address;
}
public String display() {
return "Student{" +
"name='" + name + '\'' +
", sex='" + sex + '\'' +
", age=" + age +
", address=" + address +
'}';
}
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
static class Address {
private String province;
private String street;
public Address(String province, String street) {
this.province = province;
this.street = street;
}
public String getProvince() {
return province;
}
public void setProvince(String province) {
this.province = province;
}
public String getStreet() {
return street;
}
public void setStreet(String street) {
this.street = street;
}
@Override
public String toString() {
return "Address [province=" + province + ", street=" + street + "]";
}
}
}
上述代碼運(yùn)行后的結(jié)果為:
student的地址:com.xcy.test.CloneTest$Student@47fd17e3
cloneStudent的地址:com.xcy.test.CloneTest$Student@7cdbc5d3
修改cloneStudent后結(jié)果為:
cloneStudent:Student{name='張三', sex='男', age=44, address=Address [province=北京, street=南海大道]}
student:Student{name='張三', sex='男', age=28, address=Address [province=北京, street=南海大道]}
可以看到,clone之后我們得到的是兩個(gè)對象,我們改變clone得到的cloneStudent的基礎(chǔ)類型(值類型)屬性后,原始student的值不會(huì)隨之改變;但是我們改變了cloneStudent的引用類型屬性后,原始student的引用類型屬性也隨之改變了。
總結(jié):淺拷貝創(chuàng)建了一個(gè)新的對象,然后將當(dāng)前對象的非靜態(tài)字段復(fù)制到該對象,如果字段類型為基礎(chǔ)類型(值類型),那么復(fù)制該字段的值;如果字段類型為引用類型,那么復(fù)制該字段的引用到新的對象,而不是復(fù)制引用指向的值到新的對象。
此時(shí)新對象中的引用類型字段相當(dāng)于原始字段中引用類型字段你的一個(gè)副本,原始對象和新對象的引用字段指向的是同一個(gè)對象。
深拷貝
淺拷貝是對值類型進(jìn)行拷貝,對引用數(shù)據(jù)類型進(jìn)行引用的拷貝。那么深拷貝就是要講引用類型的屬性內(nèi)容也都拷貝一份新的。
我目前了解到的深拷貝實(shí)現(xiàn)方式,總共兩種:1、引用類型也實(shí)現(xiàn)cloneable接口,并重寫clone()方法;2、通過序列化和反序列化,實(shí)現(xiàn)。下面我們用兩種方式實(shí)現(xiàn)下深拷貝
1、實(shí)現(xiàn)cloneable接口
修改Address類:
static class Address implements Cloneable {
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
……
}
修改Student類:
static class Student implements Cloneable{
@Override
protected Object clone() throws CloneNotSupportedException {
Student student = (Student) super.clone();
student.address = (Address) address.clone();
return student;
}
}
……
執(zhí)行原測試代碼之后得到結(jié)果如下:
student的地址:com.xcy.test.CloneTest$Student@47fd17e3
cloneStudent的地址:com.xcy.test.CloneTest$Student@7cdbc5d3
修改cloneStudent后結(jié)果為:
cloneStudent:Student{name='張三', sex='男', age=44, address=Address [province=北京, street=南海大道]}
student:Student{name='張三', sex='男', age=28, address=Address [province=深圳, street=南海大道]}
可以看到重寫Clone方法后,執(zhí)行原測試代碼,修改cloneStudent的Address的province屬性后,原student對應(yīng)的值沒有發(fā)生改變。我們也不難想到,當(dāng)一個(gè)實(shí)體類中有多個(gè)引用數(shù)據(jù)類型時(shí),我們需要手動(dòng)引用多個(gè)引用數(shù)據(jù)類型的clone方法,不是很方便。而對于這種情況,我們可以考慮用序列化來進(jìn)行深拷貝。
編寫序列化和反序列化的深拷貝方法:
import java.io.*;
/**
* @author xiongchenyang
* @Date 2019/6/24
**/
public class DeepClone implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 利用序列化和反序列化進(jìn)行對象的深拷貝
* @return
* @throws Exception
*/
protected Object deepClone() throws Exception{
//序列化
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos);
oos.writeObject(this);
//反序列化
ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bis);
return ois.readObject();
}
}
Student和Address方法都繼承DeepClone。然后執(zhí)行下面的測試代碼:
public static void main(String[] args) throws Exception {
Student student = new Student("張三","男",28 ,new Address("深圳","南海大道"));
Student cloneStudent = (Student) student.deepClone();
System.out.println("student的地址:"+student);
System.out.println("cloneStudent的地址:"+cloneStudent);
cloneStudent.setAge(44);
Address address = cloneStudent.getAddress();
address.setProvince("北京");
System.out.println("修改cloneStudent后結(jié)果為:");
System.out.println("cloneStudent:" + cloneStudent.display());
System.out.println("student:"+student.display());
}
得到結(jié)果如下:
student的地址:com.xcy.test.CloneTest$Student@1b0375b3
cloneStudent的地址:com.xcy.test.CloneTest$Student@32d992b2
修改cloneStudent后結(jié)果為:
cloneStudent:Student{name='張三', sex='男', age=44, address=Address [province=北京, street=南海大道]}
student:Student{name='張三', sex='男', age=28, address=Address [province=深圳, street=南海大道]}
由以上結(jié)果可以發(fā)現(xiàn),修改克隆得到的cloneStudent值,對原student沒有任何影響
線程相關(guān)方法
Object中線程相關(guān)的方法有wait(),wait(long),wait(long,int),notify(),notifyAll()這五個(gè)方法,它們都屬于final方法,其中wait(long),notify(),notifyAll()又屬于native 方法,所以他們無法被子類重寫。這些方法有一個(gè)共同特點(diǎn):他們都必須在同步方法或者同步塊中執(zhí)行,因?yàn)樵谡{(diào)用他們的時(shí)候都必須持有對象鎖,如果方法沒有持有對象鎖,那么會(huì)拋出InterruptedException異常
wait(long)
public final native void wait(long timeout) throws InterruptedException;
當(dāng)執(zhí)行wait(long)方法時(shí),會(huì)釋放當(dāng)前鎖,讓出CPU資源,線程由Running狀態(tài)變?yōu)閃aiting狀態(tài),并將當(dāng)前線程放入到對象的等待隊(duì)列中。如果超出入?yún)⒌牡却龝r(shí)間,那么該線程將被喚醒,進(jìn)入同步隊(duì)列,由waiting狀態(tài)轉(zhuǎn)換為Blocked狀態(tài)。
wait(),wait(long,int)
這兩個(gè)方法的本質(zhì)是調(diào)用wait(long)方法,具體可以看下面的代碼:
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);
}
notify(),notifyAll()
notify()當(dāng)在同步方法或同步塊中,執(zhí)行該方法并退出當(dāng)前同步塊或同步方法后,會(huì)釋放鎖,并隨機(jī)喚醒當(dāng)前等待隊(duì)列中的某一線程,將該線程從等待隊(duì)列加入到同步隊(duì)列中。notify()默認(rèn)喚醒策略是:先進(jìn)入wait的線程先被喚醒 (可以自己設(shè)置策略)
notifyAll()則會(huì)將等待隊(duì)列中的所有線程都喚醒,加入到同步隊(duì)列中,然后這些線程會(huì)競爭對象鎖,競爭到的線程會(huì)執(zhí)行。notifyAll()默認(rèn)喚醒策略是:采用LIFO策略 (可以自己設(shè)置策略)
這里先簡要了解一下wait(),notify(),notifyAll()方法的作用,在后面學(xué)習(xí)到多線程相關(guān)知識(shí)的時(shí)候,會(huì)對object中的線程通信方法做一個(gè)詳細(xì)的分析。