【Java基礎(chǔ)概念】object常用方法

本文源碼都基于JDK1.8

概述

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

8032730f.png

問題

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以后存放的位置如下圖所示:

3fbbfa06.png

試想一下我們現(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ì)的分析。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容