本文源碼都基于JDK1.8
概述
Java是一門面向對象的編程語言理郑,在Java的世界里蹄溉,萬物皆對象。而Object是一切對象的祖先您炉。所以理解Object的常用方法就非常必要了柒爵,下面是Object的成員方法圖解:
問題
1、hashCode()方法的作用是什么赚爵?
2棉胀、equals()方法和hashCode()方法的關聯(lián)是什么?
3冀膝、equals()與“==”的區(qū)別是什么唁奢?
4、native關鍵字的作用是什么窝剖?
5麻掸、clone()的深拷貝和淺拷貝的區(qū)別是什么?
6赐纱、線程相關方法的用法
Native關鍵字
在初次見到Native關鍵字的時候脊奋,我還懵比,這個Native是干啥的疙描,我怎么從來沒用過诚隙。為什么我找不到被Native修飾的方法的實現呢?
其實Native關鍵字是JNI的一部分起胰,JNI全稱是Java Native Interface久又。JNI是JDK的一部分,它允許Java代碼與其他語言代碼進行交互效五。Java本身是運行在虛擬機上的地消,Java本身是不允許直接訪問硬件的,這就引出了Native關鍵字畏妖。
用Native修飾的方法犯建,在虛擬機里都有一個與之同名的函數,去做Java想要做的事情瓜客。 使用native關鍵字說明這個方法是原生函數,也就是這個方法是用C/C++語言實現的,并且被編譯成了DLL谱仪,由java去調用玻熙。這些函數的實現體在DLL中,JDK的源代碼中并不包含疯攒,對于不同的平臺它們也是不同的嗦随。這也是java的底層機制,實際上java就是在不同的平臺上調用不同的native方法實現對操作系統(tǒng)的訪問的
hashCode()
hashCode()是一個native本地方法敬尺,其實默認的hashCode()方法返回的就是對象對應的內存地址枚尼。當我們在一些場景下復寫了hashCode()方法后,例如需要使用map來存放對象的時候砂吞,覆寫后hashCode返回的就不是對象的內存地址了署恍。
hash算法簡介
hash 算法弓熏,又被稱為散列算法挟冠,基本上激才,哈希算法就是將對象本身的鍵值离斩,通過特定的數學函數運算或者使用其他方法其掂,轉化成相應的數據存儲地址的迄沫。而哈希法所使用的數學函數就被稱為 『哈希函數』又可以稱之為散列函數膘怕。在常見的 hash 函數中有一種最簡單的方法交「除留余數法」捞烟,操作方法就是將要存入數據除以某個常數后赎瑰,使用余數作為索引值王悍。 下面看個例子:
將 323 ,458 餐曼,25 压储,340 ,28 晋辆,969渠脉, 77 使用「除留余數法」存儲在長度為11的數組中。我們假設上邊說的某個常數即為數組長度11瓶佳。 每個數除以11以后存放的位置如下圖所示:
試想一下我們現在想要拿到 77 在數組中的位置芋膘,是不是只需要 arr[77%11] = 77 就可以了。但是上述簡單的 hash 算法霸饲,缺點也是很明顯的为朋,比如 77 和 88 對 11 取余數得到的值都是 0,但是角標為 0 位置已經存放了 77 這個數據厚脉,那88就不知道該去哪里了习寸。上述現象在哈希法中有個名詞叫碰撞:
碰撞:若兩個不同的數據經過相同哈希函數運算后,得到相同的結果傻工,那么這種現象就做碰撞霞溪。
于是在設計 hash 函數的時候我們就要盡可能做到:
降低碰撞的可能性孵滞。盡量將要存入的元素經過 hash 函數運算后的結果,盡量能夠均勻的分布在指定的容器(我們在稱之為桶)鸯匹。
前文說了 hashCode 方法與 java 中使用散列表的集合類息息相關坊饶,我們拿 Set 來舉例,我們都知道 Set 中是不允許存放重復的元素的殴蓬。那么我們憑借什么來判斷已有的 Set 集合中是否有何要存入的元素重復的元素呢匿级?有人可能會說我們可以通過 equals 來判斷兩個元素是否相同。那么問題又來染厅,如果 Set 中已經有 10000個元素了痘绎,那么之后在存入一個元素豈不是要調用 10000 次 equals 方法。顯然這不合理肖粮,性能低到令人發(fā)指孤页。那要怎么辦才能保證即高效又不重復呢?答案就在于 hashCode 這個函數尿赚。經過之前的分析我們知道 hash 算法是使用特定的運算來得到數據的存儲位置的散庶,那么 hashCode 方法就充當了這個特定的函數運算。這里我們可以簡單認為調用 hashCode 方法后得到數值就是元素的存儲位置(其實集合內部還做了進一步的運算凌净,以保證盡可能的均勻分布在桶內)悲龟。當 Set 需要存放一個元素的時候,首先會調用 hashCode 方法去查看對應的地址上有沒有存放元素冰寻,如果沒有則表示 Set 中肯定沒有相同的元素须教,直接存放在對應位置就好,但是如果 hashCode 的結果相同斩芭,即發(fā)生了碰撞轻腺,那么我們在進一步調用該位置元素的 equals 方法與要存放的元素進行比較,如果相同就不存了划乖,如果不相同就需要進一步散列其它的地址贬养。這樣我們就可以盡可能高效的保證了無重復元素的方法。
equals()
equals 方法屬于Object基類的方法琴庵,所有的對象都擁有這個方法误算,并有權重寫該方法。該方法返回了一個boolean類型的結果迷殿,代表比較的兩個對象是否相同儿礼。事實上很多java定義好的一些引用數據類型,都重寫了equals 方法庆寺。當我們自定義引用數據類型的時候蚊夫,如果判定兩個對象相等,需要根據具體的業(yè)務規(guī)則而定懦尝,但是必須遵循以下規(guī)則知纷;
自反性(reflexive):對于任意不為null 的引用值x壤圃,x.equals(x)一定為true;
對稱性(symmetric): 對于任意不為null的引用值x和y,當且僅當x.equals(y)為true時屈扎,y.equals(x) 為true埃唯;
傳遞性(transitive): 對于任意不為null的引用值x、y和z鹰晨,如果x.equals(y)為true同時y.equal(z)為true,那么x.equals(z)也為true止毕;
一致性(consistent):對于任意不為null的引用值x和y模蜡,如果用于equals比較的對象信息沒有被修改的話,多次調用時 x.equals(y) 要么一致地返回 true 要么一致地返回 false扁凛。
null值要求:對于任意不為 null 的引用值 x忍疾,x.equals(null) 返回 false。
equals 與 == 的區(qū)別
java數據類型可以分為基礎數據類型和引用數據類型谨朝÷倍剩基礎數據類型包括short,int,byte,long,dubble,float,boolean,char八種。對于基礎數據類型字币,==判斷的是左右兩邊的值则披。
int a = 10;
int b = 10;
float c = 10.0f;
//以下輸出結果均為 true
System.out.println("(a == b) = " + (a == b));
System.out.println("(b == c) = " + (b == c));
而對于引用數據類型,==操作符判斷的就是左右兩邊對象的內存地址是否相同洗出。也就是說通過==判斷的兩個引用數據類型士复,如果相等,那么他們指向的肯定是同一個對象翩活。
可以總結出兩者比較的結果如下:
1阱洪、如果==兩邊都是基礎數據類型,那么比較的是兩個的值是否相等菠镇;
2冗荸、如果==兩邊都是引用數據類型,那么比較的是兩者的內存地址是否相同利耍。若相同蚌本,則左右兩邊的是同一個對象。
3堂竟、Object基類的equals默認比較的是兩者的內存地址是否相等魂毁。在構建的對象沒有重寫equals對象時,equals與==作用相同出嘹。
4席楚、equals用于比較引用數據類型是否相等。在滿足equals判斷規(guī)則的前提下税稼,兩個對象只要規(guī)定的屬性相同烦秩,那么就認為兩個對象是相同的垮斯。
equals 與 hashCode的關系
1、如果兩個對象調用equals方法返回的true只祠,那么他們的hashCode一定相同兜蠕;
2、如果兩個對象的hashCode相同抛寝,他們卻不一定是同一個對象熊杨,調用equals方法,不一定為true盗舰;但是如果兩個對象的hashCode不相同晶府,那么他們一定不是同一個對象,調用equals方法一定返回false钻趋;
clone 深淺拷貝
在某些情況下川陆,我們需要獲取一個對象的拷貝來處理某些事情。這個時候就需要用到object.clone方法蛮位,要是用clone方法的類较沪,必須實現cloneable接口,才能夠使用clone方法失仁,否則在使用時會拋出CloneNotSupportedException尸曼。而我們在實際應用中可能會發(fā)現,當對象中包含可變的引用數據類型時陶因,在拷貝得到的新對象中對該引用數據類型的屬性進行修改骡苞,原始對象相應的屬性也會發(fā)生變化,這種現象就是“淺拷貝”楷扬。object默認的clone方法就是淺拷貝解幽。
在了解淺拷貝和深拷貝之前,我們需要先了解一點鋪墊知識:Java中的數據類型分為基礎數據類型和引用數據類型烘苹。這兩種類型在進行賦值操作和作為方法參數或返回值時躲株,會有值傳遞和引用地址傳遞的差別。
淺拷貝
我們來寫一個例子看一下clone()方法的淺拷貝現象:
/**
* @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后結果為:");
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 + "]";
}
}
}
上述代碼運行后的結果為:
student的地址:com.xcy.test.CloneTest$Student@47fd17e3
cloneStudent的地址:com.xcy.test.CloneTest$Student@7cdbc5d3
修改cloneStudent后結果為:
cloneStudent:Student{name='張三', sex='男', age=44, address=Address [province=北京, street=南海大道]}
student:Student{name='張三', sex='男', age=28, address=Address [province=北京, street=南海大道]}
可以看到镣衡,clone之后我們得到的是兩個對象霜定,我們改變clone得到的cloneStudent的基礎類型(值類型)屬性后,原始student的值不會隨之改變廊鸥;但是我們改變了cloneStudent的引用類型屬性后望浩,原始student的引用類型屬性也隨之改變了。
總結:淺拷貝創(chuàng)建了一個新的對象惰说,然后將當前對象的非靜態(tài)字段復制到該對象磨德,如果字段類型為基礎類型(值類型),那么復制該字段的值;如果字段類型為引用類型,那么復制該字段的引用到新的對象典挑,而不是復制引用指向的值到新的對象酥宴。
此時新對象中的引用類型字段相當于原始字段中引用類型字段你的一個副本,原始對象和新對象的引用字段指向的是同一個對象您觉。
深拷貝
淺拷貝是對值類型進行拷貝拙寡,對引用數據類型進行引用的拷貝。那么深拷貝就是要講引用類型的屬性內容也都拷貝一份新的琳水。
我目前了解到的深拷貝實現方式肆糕,總共兩種:1、引用類型也實現cloneable接口在孝,并重寫clone()方法擎宝;2、通過序列化和反序列化浑玛,實現。下面我們用兩種方式實現下深拷貝
1噩咪、實現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í)行原測試代碼之后得到結果如下:
student的地址:com.xcy.test.CloneTest$Student@47fd17e3
cloneStudent的地址:com.xcy.test.CloneTest$Student@7cdbc5d3
修改cloneStudent后結果為:
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對應的值沒有發(fā)生改變涨享。我們也不難想到,當一個實體類中有多個引用數據類型時仆百,我們需要手動引用多個引用數據類型的clone方法厕隧,不是很方便。而對于這種情況俄周,我們可以考慮用序列化來進行深拷貝吁讨。
編寫序列化和反序列化的深拷貝方法:
import java.io.*;
/**
* @author xiongchenyang
* @Date 2019/6/24
**/
public class DeepClone implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 利用序列化和反序列化進行對象的深拷貝
* @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后結果為:");
System.out.println("cloneStudent:" + cloneStudent.display());
System.out.println("student:"+student.display());
}
得到結果如下:
student的地址:com.xcy.test.CloneTest$Student@1b0375b3
cloneStudent的地址:com.xcy.test.CloneTest$Student@32d992b2
修改cloneStudent后結果為:
cloneStudent:Student{name='張三', sex='男', age=44, address=Address [province=北京, street=南海大道]}
student:Student{name='張三', sex='男', age=28, address=Address [province=深圳, street=南海大道]}
由以上結果可以發(fā)現峦朗,修改克隆得到的cloneStudent值建丧,對原student沒有任何影響
線程相關方法
Object中線程相關的方法有wait(),wait(long),wait(long,int)波势,notify(),notifyAll()這五個方法翎朱,它們都屬于final方法,其中wait(long),notify(),notifyAll()又屬于native 方法尺铣,所以他們無法被子類重寫拴曲。這些方法有一個共同特點:他們都必須在同步方法或者同步塊中執(zhí)行,因為在調用他們的時候都必須持有對象鎖凛忿,如果方法沒有持有對象鎖澈灼,那么會拋出InterruptedException異常
wait(long)
public final native void wait(long timeout) throws InterruptedException;
當執(zhí)行wait(long)方法時,會釋放當前鎖侄非,讓出CPU資源蕉汪,線程由Running狀態(tài)變?yōu)閃aiting狀態(tài)流译,并將當前線程放入到對象的等待隊列中。如果超出入參的等待時間者疤,那么該線程將被喚醒福澡,進入同步隊列,由waiting狀態(tài)轉換為Blocked狀態(tài)驹马。
wait(),wait(long,int)
這兩個方法的本質是調用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()當在同步方法或同步塊中,執(zhí)行該方法并退出當前同步塊或同步方法后糯累,會釋放鎖算利,并隨機喚醒當前等待隊列中的某一線程,將該線程從等待隊列加入到同步隊列中泳姐。notify()默認喚醒策略是:先進入wait的線程先被喚醒 (可以自己設置策略)
notifyAll()則會將等待隊列中的所有線程都喚醒效拭,加入到同步隊列中,然后這些線程會競爭對象鎖胖秒,競爭到的線程會執(zhí)行缎患。notifyAll()默認喚醒策略是:采用LIFO策略 (可以自己設置策略)
這里先簡要了解一下wait(),notify(),notifyAll()方法的作用,在后面學習到多線程相關知識的時候阎肝,會對object中的線程通信方法做一個詳細的分析挤渔。