Σ(っ°Д°;)っ找個(gè)對(duì)象"Object"還要用八股文?
本文轉(zhuǎn)載自微信公眾號(hào)「稀飯下雪」,作者帥氣的小飯飯。轉(zhuǎn)載本文請(qǐng)聯(lián)系稀飯下雪公眾號(hào)。
還是那句話,不管你是初級(jí)、中級(jí)、還是高開,甚至還是資深,不開玩笑的說,面試前都要刷八股文,因?yàn)槟銢]法保證的是遇見的面試官都會(huì)因?yàn)槟愕穆毼?,而和你聊?xiàng)目、架構(gòu)、源碼,我們能做的只能是做好準(zhǔn)備。
反正Object的我自覺八股文應(yīng)該是這些了,有興趣就看看。
Object八股文目錄,缺哪看哪,都缺都看!
- equals
- equlas 跟 == 的區(qū)別?
- 說說看對(duì)hashCode和equals方法的理解?
- 說說看hashCode的作用?
- 說說看hash沖突或者碰撞?
- clone方法
- 淺拷貝是啥?
- 實(shí)現(xiàn)淺拷貝的方法?
- 深拷貝是啥?
- 實(shí)現(xiàn)深拷貝的方法?
- sleep、wait、notify、notifyAll
- 使用wait、notify實(shí)現(xiàn)生產(chǎn)者、消費(fèi)者模式
- 說說看wait和sleep的異同?
- 為什么wait 需要在同步代碼塊中使用?
- 為什么wait notify notifyAll是定義在Object類 , 而 sleep是定義在Thread類中?
- wait屬于Object 對(duì)象, 如果調(diào)用Thread.wait會(huì)發(fā)生什么?
- 說說看notify和notifyAll的區(qū)別?
- notifyAll后所有線程都會(huì)再次的搶占鎖,如果搶占失敗怎么辦?
- 說說看finalize()的作用?
- Java類裝載過程是什么樣的?
- Class.forName()和ClassLoader.loadClass區(qū)別?
equals
沒什么區(qū)別。
test1會(huì)直接報(bào)空指針異常,你想想看,null.equals看不起來不就怪怪的嗎?空指針怎么可能有方法呢是吧,
「在日常開發(fā)中的意義:」 我們一般在企業(yè)開發(fā)中都會(huì)將已知的字面量放在equals,未知的參數(shù)放在equals后面,這樣可以避免空指針,編程新手容易犯這個(gè)異常,我review過的代碼這么實(shí)現(xiàn)的,說實(shí)話,挺多次的,不止編程新人,兩三年工作經(jīng)驗(yàn)的都會(huì)這么做。
equlas 跟 == 的區(qū)別?
equals方法比較的是字符串的內(nèi)容是否相等,而 == 比較的則是對(duì)象地址。
首先Java中的數(shù)據(jù)類型可以分為兩種,一種是基本數(shù)據(jù)類型,也稱原始數(shù)據(jù)類型,如byte,short,char,int,long,float,double,boolean 他們之間的比較,應(yīng)用雙等號(hào)(==),比較的是他們的值。
另一種是復(fù)合數(shù)據(jù)類型,包括類,當(dāng)他們用(==)進(jìn)行比較的時(shí)候,比較的是他們?cè)趦?nèi)存中的存放地址,所以,除非 是同一個(gè)new出來的對(duì)象,他們的比較后的結(jié)果為true,否則比較后結(jié)果為false。
而JAVA當(dāng)中所有的類都是繼承于Object這個(gè)基類的,在Object中的基類中定義了一個(gè)equals的方法,這個(gè)方法的初始行為是比較對(duì)象的內(nèi)存地址,但在一些類庫當(dāng)中這個(gè)方法被覆蓋掉了,如String,Integer,Date在這些類當(dāng)中equals有其自身的實(shí)現(xiàn),而不再是比較類在堆內(nèi)存中的存放地址了。
「在日常開發(fā)中的意義:」 沒記錯(cuò)的話,我剛java也是經(jīng)常在考慮到底用equals還是用 == 做對(duì)比。
說說看對(duì)hashCode和equals方法的理解?
如果兩個(gè)對(duì)象equals方法相等,則它們的hashCode一定相同;
如果兩個(gè)對(duì)象的hashCode相同,它們的equals()方法則不一定相等。
而兩個(gè)對(duì)象的hashCode()返回值相等不能判斷這兩個(gè)對(duì)象是相等的,但兩個(gè)對(duì)象的hashcode()返回值不相等則可以判定兩個(gè)對(duì)象一定不相等。
因?yàn)閷?duì)兩個(gè)對(duì)象是否相等的判斷都會(huì)通過先判斷hashCode,如果hashCode相等再判斷equals,保證對(duì)象一定相同。
說說看hashCode的作用?
hashCode的作用實(shí)際上是為了提高在散列結(jié)構(gòu)存儲(chǔ)中查找的效率,equals的實(shí)現(xiàn)會(huì)去判斷對(duì)象的所有成員一個(gè)個(gè)判斷,效率上來說是相對(duì)較慢的,,而hashCode則不一樣,hashCode是根據(jù)所有成員生成了一個(gè)值,將兩個(gè)值進(jìn)行對(duì)比,因此效率更高,等到hashCode相同了,再去調(diào)用equals判斷,因?yàn)閮蓚€(gè)對(duì)象hashCode相同,不一定意味著兩個(gè)對(duì)象相同,還存在著hash沖突呀。
說說看hash沖突或者碰撞?
對(duì)象Hash的前提是實(shí)現(xiàn)equals()和hashCode()兩個(gè)方法,那么HashCode()的作用就是保證對(duì)象返回唯一hash值,但當(dāng)兩個(gè)對(duì)象計(jì)算值一樣時(shí),這就發(fā)生了碰撞沖突。
那么如何解決hash沖突呢?
開放定址法
其實(shí)說穿了就是上次hash出來的值沖突了,那就再次散列,
- 線性探測(cè)再散列:沖突發(fā)生時(shí),順序查看表中下一單元,直到找出一個(gè)空單元或查遍全表。
- 二次探測(cè)再散列:沖突發(fā)生時(shí),在表的左右進(jìn)行跳躍式探測(cè),比較靈活。
- 偽隨機(jī)探測(cè)再散列:具體實(shí)現(xiàn)是建立一個(gè)偽隨機(jī)數(shù)發(fā)生器,(如i=(i+p) % m),并給定一個(gè)隨機(jī)數(shù)做起點(diǎn)。
再哈希法
當(dāng)哈希地址Hi=RH1(key)發(fā)生沖突時(shí),再計(jì)算Hi=RH2(key)……,直到?jīng)_突不再產(chǎn)生。這種方法不易產(chǎn)生聚集,但增加了計(jì)算時(shí)間。
鏈地址法
這種方法的基本思想是將所有哈希地址為i的元素構(gòu)成一個(gè)稱為同義詞鏈的單鏈表,并將單鏈表的頭指針存在哈希表的第i個(gè)單元中,因而查找、插入和刪除主要在同義詞鏈中進(jìn)行。鏈地址法適用于經(jīng)常進(jìn)行插入和刪除的情況。
貌似HashMap用的就是鏈地址法,當(dāng)插入的時(shí)候,會(huì)根據(jù)key的hash值然后計(jì)算出相應(yīng)的數(shù)組下標(biāo),計(jì)算方法是index = hashcode%table.length,當(dāng)這個(gè)下標(biāo)上面已經(jīng)存在元素的時(shí)候那么就會(huì)形成鏈表,將后插入的元素放到尾端,若是下標(biāo)上面沒有存在元素的話,那么將直接將元素放到這個(gè)位置上。當(dāng)進(jìn)行查詢的時(shí)候,同樣會(huì)根據(jù)key的hash值先計(jì)算相應(yīng)的下標(biāo),然后到相應(yīng)的位置上進(jìn)行查找,若是這個(gè)下標(biāo)上面有很多元素的話,那么將在這個(gè)鏈表上一直查找直到找到對(duì)應(yīng)的元素。
- 建立公共溢出區(qū)
將哈希表分為基本表和溢出表兩部分,凡是和基本表發(fā)生沖突的元素,一律填入溢出表。
「在日常開發(fā)中的意義:」 日常開發(fā)中容器用多了,有時(shí)候確實(shí)會(huì)遇見需要重寫equals和hashCode的情況,所以理解是有用的,另外就是有時(shí)候會(huì)遇見兩個(gè)對(duì)象明明不一樣的,但是被誤判一樣了的問題,最終找到是lombok注解@Data的原因,如果不了解equals和hashCode的原理,其實(shí)你確實(shí)找不到兇手。別肝,自然你還得知道@Data其實(shí)包括了重寫了hashCode和euqals。
clone方法
每次問到這道題,大部分人都是回答2,小部分人是回答1。
都錯(cuò),正確答案是直接報(bào)錯(cuò)
為什么?因?yàn)閏lone方法是Object的protect方法,需要子類顯示的去重寫clone方法,并且實(shí)現(xiàn)Cloneable 接口,這是規(guī)定。
淺拷貝是啥?
- 對(duì)于數(shù)據(jù)類型是基本數(shù)據(jù)類型的成員變量,淺拷貝會(huì)直接進(jìn)行值傳遞,也就是將該屬性值復(fù)制一份給新的對(duì)象。因?yàn)槭莾煞莶煌臄?shù)據(jù),所以對(duì)其中一個(gè)對(duì)象的該成員變量值進(jìn)行修改,不會(huì)影響另一個(gè)對(duì)象拷貝得到的數(shù)據(jù)。
- 對(duì)于數(shù)據(jù)類型是引用數(shù)據(jù)類型的成員變量,比如說成員變量是某個(gè)數(shù)組、某個(gè)類的對(duì)象等,那么淺拷貝會(huì)進(jìn)行引用傳遞,也就是只是將該成員變量的引用值(內(nèi)存地址)復(fù)制一份給新的對(duì)象。因?yàn)閷?shí)際上兩個(gè)對(duì)象的該成員變量都指向同一個(gè)實(shí)例。在這種情況下,在一個(gè)對(duì)象中修改該成員變量會(huì)影響到另一個(gè)對(duì)象的該成員變量值。
實(shí)現(xiàn)淺拷貝的方法?
- 通過拷貝構(gòu)造方法實(shí)現(xiàn)淺拷貝,這個(gè)沒什么好說的;
- 通過重寫clone()方法進(jìn)行淺拷貝:Object類是類結(jié)構(gòu)的根類,其中有一個(gè)方法為protected Object 「clone」() 這個(gè)方法就是進(jìn)行的淺拷貝。
有了這個(gè)淺拷貝模板,我們可以通過調(diào)用clone()方法來實(shí)現(xiàn)對(duì)象的淺拷貝。
但是需要注意:
1、Object類雖然有這個(gè)方法,但是這個(gè)方法是受保護(hù)的(被protected修飾),所以我們無法直接使用。
2、使用clone方法的類必須實(shí)現(xiàn)Cloneable接口,否則會(huì)拋出異常CloneNotSupportedException。
對(duì)于這兩點(diǎn),我們的解決方法是,在要使用clone方法的類中重寫clone()方法,通過super.clone()調(diào)用Object類中的原clone方法。
深拷貝是啥?
對(duì)于深拷貝來說,不僅要復(fù)制對(duì)象的所有基本數(shù)據(jù)類型的成員變量值,還要為所有引用數(shù)據(jù)類型的成員變量申請(qǐng)存儲(chǔ)空間,并復(fù)制每個(gè)引用數(shù)據(jù)類型成員變量所引用的對(duì)象,直到該對(duì)象可達(dá)的所有對(duì)象;
也就是說,對(duì)象進(jìn)行深拷貝要對(duì)整個(gè)對(duì)象圖進(jìn)行拷貝;
簡(jiǎn)單地說,深拷貝對(duì)引用數(shù)據(jù)類型的成員變量的對(duì)象圖中所有的對(duì)象都開辟了內(nèi)存空間;
而淺拷貝只是傳遞地址指向,新的對(duì)象并沒有對(duì)引用數(shù)據(jù)類型創(chuàng)建內(nèi)存空間。
實(shí)現(xiàn)深拷貝的方法?
通過重寫clone方法來實(shí)現(xiàn)深拷貝,與通過重寫clone方法實(shí)現(xiàn)淺拷貝的基本思路一樣,只需要為對(duì)象圖的每一層的每一個(gè)對(duì)象都實(shí)現(xiàn)Cloneable接口并重寫clone方法,最后在最頂層的類的重寫的clone方法中調(diào)用所有的clone方法即可實(shí)現(xiàn)深拷貝。
通過對(duì)象序列化實(shí)現(xiàn)深拷貝,將對(duì)象序列化為字節(jié)序列后,默認(rèn)會(huì)將該對(duì)象的整個(gè)對(duì)象圖進(jìn)行序列化,再通過反序列即可完美地實(shí)現(xiàn)深拷貝。
「在日常開發(fā)中的意義:」 拷貝這個(gè)知識(shí)點(diǎn)還是很重要的,企業(yè)開發(fā)中,如果clone對(duì)象沒有考慮深淺問題,可是分分鐘致命的。
sleep、wait、notify、notifyAll
使用wait、notify實(shí)現(xiàn)生產(chǎn)者、消費(fèi)者模式
- public class ConsumeAndProviderDesign {
- private static int size = 10000;
- public static void main(String[] args) {
- ConsumeAndProviderDesign design = new ConsumeAndProviderDesign();
- design.init();
- }
- private void init() {
- Container container = new Container();
- // 生產(chǎn)
- new Thread(() -> {
- try {
- for (int i = 1; i <= size; i++) {
- container.add();
- }
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- }).start();
- // 消費(fèi)
- new Thread(() -> {
- try {
- for (int i = 1; i <= size; i++) {
- container.remove();
- }
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- }).start();
- }
- class Container {
- private List<Date> list = new ArrayList<>();
- // 使用this.wait()和this.notify()必須給對(duì)象加鎖,否則會(huì)報(bào)IllegalMonitorStateException異常
- private synchronized void add() throws InterruptedException {
- // 之所以用while,是因?yàn)閠his.notify()喚醒的不一定是滿足條件的,因?yàn)閠his.notify()是隨機(jī)喚醒一條等待訪問Container監(jiān)視器的線程
- while (list.size() == size) {
- // 擁有該對(duì)象的線程會(huì)進(jìn)入等待
- this.wait();
- }
- list.add(new Date());
- System.out.println("倉(cāng)庫里有了 " + list.size() + " 個(gè)產(chǎn)品");
- // 隨機(jī)喚醒擁有該對(duì)象的線程
- this.notify();
- }
- // 使用this.wait()和this.notify()必須給對(duì)象加鎖,否則會(huì)報(bào)IllegalMonitorStateException異常
- private synchronized void remove() throws InterruptedException {
- // 之所以用while,是因?yàn)閠his.notify()喚醒的不一定是滿足條件的,因?yàn)閠his.notify()是隨機(jī)喚醒一條等待訪問Container監(jiān)視器的線程
- while (list.size() == 0) {
- // 擁有該對(duì)象的線程會(huì)進(jìn)入等待
- this.wait();
- }
- Date remove = list.remove(0);
- System.out.println("消耗了" + remove.toString() + " , 現(xiàn)在倉(cāng)庫還剩下 " + list.size());
- // 隨機(jī)喚醒擁有該對(duì)象的線程
- this.notify();
- }
- }
- }
核心關(guān)注點(diǎn):
- 使用this.wait()和this.notify()必須給對(duì)象加鎖
- this.notify()喚醒的不一定是滿足條件的,因?yàn)閠his.notify()是隨機(jī)喚醒一條等待訪問Container監(jiān)視器的線程,所以條件那里需要用while
「在日常開發(fā)中的意義:」 生產(chǎn)者消費(fèi)者模式一直都很重要。
說說看wait和sleep的異同?
- sleep方法
首先sleep使當(dāng)前線程進(jìn)入了停滯狀態(tài),也就是阻塞了當(dāng)前線程,讓出CPU的使用,目的是不讓當(dāng)前線程獨(dú)自霸占CPU資源,留一定時(shí)間給其他線程執(zhí)行的機(jī)會(huì)。
其次我們可以看到sleep是Thread類的靜態(tài)方法,因此不能改變持有對(duì)象的鎖,所以在當(dāng)一個(gè)Synchronized塊中調(diào)用sleep方法時(shí),線程雖然休眠了,但是持有對(duì)象的鎖并沒有被釋放,也就是說盡管線程睡著了,其他線程依舊無法獲得某對(duì)象的鎖。
- wait方法
可以看到wait方法是屬于Object類里邊的方法,當(dāng)一個(gè)線程執(zhí)行wait方法時(shí),就意味著他進(jìn)入到一個(gè)和某對(duì)象相關(guān)的等待池中,同時(shí)失去了某對(duì)象的鎖,這個(gè)時(shí)候其他線程就可以訪問了,等wait指定等待時(shí)間到或者外部調(diào)用了某對(duì)象的notify方法或者notifyAll方法才能喚醒當(dāng)前等待池中的對(duì)象。
另外就是wait方法必須放在synchronized或者lock中,否則執(zhí)行的時(shí)候會(huì)拋 java.lang.IllegalMonitorStateException 異常。
總結(jié)下來就是:
sleep睡眠時(shí),保持對(duì)象鎖;
wait等待時(shí),釋放對(duì)象鎖;
不過wait和sleep都可以通過Interrupt方法打斷線程的暫停狀態(tài),從而使線程立刻拋出InterruptedException。
為什么wait 需要在同步代碼塊中使用?
原因是避免CPU切換到其他線程,而其他線程又提前執(zhí)行了notify()方法,那這樣就達(dá)不到我們的預(yù)期(先wait再由其他線程來喚醒),所以需要一個(gè)同步鎖來保護(hù)。
為什么wait notify notifyAll是定義在Object類 , 而 sleep是定義在Thread類中?
首先wait、notify、notifyAll三者的作用時(shí)釋放鎖、喚醒線程,java中每個(gè)對(duì)象都是Object類型,而每個(gè)線程的鎖都是對(duì)象鎖,而不是線程鎖,每個(gè)線程想要執(zhí)行鎖內(nèi)的代碼,都必須先獲取此對(duì)象,因此定義釋放鎖、喚醒線程的這兩種行為必須放在Object類中,如果放在Thread類中,那么wait要讓線程等待的時(shí)哪個(gè)鎖就不明確了。
至于sleep方法,從sleep的作用入手即可,sleep的作用是讓線程在預(yù)期的時(shí)間內(nèi)執(zhí)行,其他時(shí)候不要來占用CPU,而且不需要釋放鎖,也就是說sleep是針對(duì)線程的,因此放在Thead類中合適。
總歸就是因?yàn)樵趈ava中,wait(),notify()和notifyAll()屬于鎖級(jí)別的操作,而鎖是屬于某個(gè)對(duì)象的,因此放在Object類中。
wait屬于Object 對(duì)象, 如果調(diào)用Thread.wait會(huì)發(fā)生什么?
Thread也是個(gè)對(duì)象,這樣調(diào)用是可以的,只是Thread是個(gè)特殊的對(duì)象,在線程退出的時(shí)候會(huì)自動(dòng)執(zhí)行notify,可能會(huì)導(dǎo)致和我們預(yù)期的設(shè)計(jì)不一致,所以一般不這么用。
說說看notify和notifyAll的區(qū)別?
notifyAll使所有原來在該對(duì)象上等待被notify的線程統(tǒng)統(tǒng)退出wait的狀態(tài),變成等待該對(duì)象上的鎖,一旦該對(duì)象被解鎖,他們就會(huì)去競(jìng)爭(zhēng)。
notify則文明得多,他只是選擇一個(gè)wait狀態(tài)線程進(jìn)行通知,并使他獲得該對(duì)象上的鎖,但不驚動(dòng)其他同樣在等待被該對(duì)象notify的線程們,當(dāng)?shù)谝粋€(gè)線程運(yùn)行完畢以后釋放對(duì)象上的鎖此時(shí)如果該對(duì)象沒有再次使用notify語句,則即便該對(duì)象已經(jīng)空閑,其他wait狀態(tài)等待的線程由于沒有得到該對(duì)象的通知,繼續(xù)處在wait狀態(tài),直到這個(gè)對(duì)象發(fā)出一個(gè)notify或notifyAll,它們等待的是被notify或notifyAll,而不是鎖。
「在日常開發(fā)中的意義:」 貌似在寫中間件的時(shí)候用過,所以還是有用的這個(gè)知識(shí)點(diǎn)。
notifyAll后所有線程都會(huì)再次的搶占鎖,如果搶占失敗怎么辦?
首先看下線程的生命周期
可以看到線程在調(diào)用了wait后便處于等待狀態(tài),而在被notifyAll后,可以看到進(jìn)行了可運(yùn)行的RUNNABLE狀態(tài),之后搶占失敗的則進(jìn)入了BLOCKED被阻塞狀態(tài)。
說說看finalize()的作用?
finalize()是在java.lang.Object里定義的,在對(duì)象被回收的時(shí)候調(diào)用,特殊情況下,需要我們實(shí)現(xiàn)finalize,當(dāng)對(duì)象被回收的時(shí)候釋放一些資源,比如:一個(gè)socket鏈接,在對(duì)象初始化時(shí)創(chuàng)建,整個(gè)生命周期內(nèi)有效,那么就需要實(shí)現(xiàn)finalize,關(guān)閉這個(gè)鏈接。
雖然一個(gè)對(duì)象的finalize()方法只會(huì)被調(diào)用一次,但是finalize()被調(diào)用不意味著gc會(huì)立即回收該對(duì)象,所以有可能調(diào)用finalize()后,該對(duì)象又不需要被回收了,然后到了真正要被回收的時(shí)候,因?yàn)榍懊嬲{(diào)用過一次,所以不會(huì)調(diào)用finalize(),導(dǎo)致出現(xiàn)Bug, 所以,推薦不要使用finalize()方法,它跟析構(gòu)函數(shù)還是不一樣的。
「在日常開發(fā)中的意義:」 知道了finalize后不會(huì)亂用就可以了,應(yīng)該說不會(huì)再用就可以了,控制生命周期的方式太多了,沒必要用它。
Java類裝載過程是什么樣的?
加載:通過類的全限定名獲取二進(jìn)制字節(jié)流,將二進(jìn)制字節(jié)流轉(zhuǎn)換成方法區(qū)中的運(yùn)行時(shí)數(shù)據(jù)結(jié)構(gòu),在內(nèi)存中生成Java.lang.Class對(duì)象;
鏈接:執(zhí)行下面的校驗(yàn)、準(zhǔn)備和解析步驟,其中解析步驟是可以選擇的;
驗(yàn)證:檢查導(dǎo)入類或接口的二進(jìn)制數(shù)據(jù)的正確性;(文件格式驗(yàn)證,元數(shù)據(jù)驗(yàn)證,字節(jié)碼驗(yàn)證,符號(hào)引用驗(yàn)證)
準(zhǔn)備:給類的靜態(tài)變量分配并初始化存儲(chǔ)空間;
解析:將常量池中的符號(hào)引用轉(zhuǎn)成直接引用;
初始化:激活類的靜態(tài)變量的初始化Java代碼和靜態(tài)Java代碼塊,并初始化要設(shè)置的變量值。
Class.forName()和ClassLoader.loadClass區(qū)別?
Class.forName(className)方法,內(nèi)部實(shí)際調(diào)用的方法是 Class.forName(className,true,classloader);
可以看到第2個(gè)boolean參數(shù)表示類是否需要初始化,Class.forName(className)默認(rèn)是需要初始化,
一旦初始化,就會(huì)觸發(fā)目標(biāo)對(duì)象的static塊代碼執(zhí)行,static參數(shù)也也會(huì)被再次初始化。
ClassLoader.loadClass(className)方法,內(nèi)部實(shí)際調(diào)用的方法是 ClassLoader.loadClass(className,false);
可以看到第2個(gè) boolean參數(shù),表示目標(biāo)對(duì)象是否進(jìn)行鏈接,false表示不進(jìn)行鏈接,由上面介紹可以,不進(jìn)行鏈接意味著不進(jìn)行包括初始化等一些列步驟,那么靜態(tài)塊和靜態(tài)對(duì)象就不會(huì)得到執(zhí)行
「在日常開發(fā)中的意義:」 還是有點(diǎn)用的, 其實(shí)在寫中間件的時(shí)候經(jīng)常會(huì)用上Class.forName(className),不開玩笑的說很少會(huì)考慮說靜態(tài)代碼塊被再次初始化了什么的問題, 不過確實(shí)可以不用。
原文鏈接:https://mp.weixin.qq.com/s/EKSfiMQjG2h3l5wlPUU9aQ