轉(zhuǎn)https://blog.csdn.net/LL1187740947/article/details/78419637
問(wèn)題的引入:
問(wèn)題一:
String str1 = "abc";
String str2 = "abc";
System.out.println(str1==str2); //true
問(wèn)題二:
String str1 =new String ("abc");
String str2 =new String ("abc");
System.out.println(str1==str2); // false
問(wèn)題三:
String s1 = "ja";
String s2 = "va";
String s3 = "java";
String s4 = s1 + s2;
System.out.println(s3 == s4);//false
System.out.println(s3.equals(s4));//true
由于以上問(wèn)題讓我含糊不清嵌赠,于是特地搜集了一些有關(guān)java內(nèi)存分配的資料,以下是網(wǎng)摘:
Java 中的堆和棧
Java把內(nèi)存劃分成兩種:一種是棧內(nèi)存,一種是堆內(nèi)存熄赡。
在函數(shù)中定義的一些基本類型的變量和對(duì)象的引用變量都在函數(shù)的棧內(nèi)存中分配姜挺。
當(dāng)在一段代碼塊定義一個(gè)變量時(shí),Java就在棧中為這個(gè)變量分配內(nèi)存空間彼硫,當(dāng)超過(guò)變量的作用域后炊豪,Java會(huì)自動(dòng)釋放掉為該變量所分配的內(nèi)存空間凌箕,該內(nèi)存空間可以立即被另作他用。
堆內(nèi)存用來(lái)存放由new創(chuàng)建的對(duì)象和數(shù)組词渤。
在堆中分配的內(nèi)存牵舱,由Java虛擬機(jī)的自動(dòng)垃圾回收器來(lái)管理。
在堆中產(chǎn)生了一個(gè)數(shù)組或?qū)ο蠛笕迸埃€可以在棧中定義一個(gè)特殊的變量芜壁,讓棧中這個(gè)變量的取值等于數(shù)組或?qū)ο笤诙褍?nèi)存中的首地址,棧中的這個(gè)變量就成了數(shù)組或?qū)ο蟮囊米兞俊?/p>
引用變量就相當(dāng)于是為數(shù)組或?qū)ο笃鸬囊粋€(gè)名稱高氮,以后就可以在程序中使用棧中的引用變量來(lái)訪問(wèn)堆中的數(shù)組或?qū)ο蟆?/p>
具體的說(shuō):
棧與堆都是Java用來(lái)在Ram中存放數(shù)據(jù)的地方慧妄。與C++不同,Java自動(dòng)管理?xiàng):投鸭羯郑绦騿T不能直接地設(shè)置椚停或堆。
Java的堆是一個(gè)運(yùn)行時(shí)數(shù)據(jù)區(qū),類的(對(duì)象從中分配空間紊浩。這些對(duì)象通過(guò)new窖铡、newarray、anewarray和multianewarray等指令建立坊谁,它們不需要程序代碼來(lái)顯式的釋放费彼。堆是由垃圾回收來(lái)負(fù)責(zé)的,堆的優(yōu)勢(shì)是可以動(dòng)態(tài)地分配內(nèi)存大小口芍,生存期也不必事先告訴編譯器箍铲,因?yàn)樗窃谶\(yùn)行時(shí)動(dòng)態(tài)分配內(nèi)存的,Java的垃圾收集器會(huì)自動(dòng)收走這些不再使用的數(shù)據(jù)鬓椭。但缺點(diǎn)是颠猴,由于要在運(yùn)行時(shí)動(dòng)態(tài)分配內(nèi)存,存取速度較慢小染。
棧的優(yōu)勢(shì)是翘瓮,存取速度比堆要快,僅次于寄存器裤翩,棧數(shù)據(jù)可以共享资盅。但缺點(diǎn)是,存在棧中的數(shù)據(jù)大小與生存期必須是確定的踊赠,缺乏靈活性呵扛。棧中主要存放一些基本類型的變量(,int, short, long, byte, float, double, boolean, char)和對(duì)象句柄。
棧有一個(gè)很重要的特殊性筐带,就是存在棧中的數(shù)據(jù)可以共享今穿。假設(shè)我們同時(shí)定義:
int a = 3;
int b = 3;
編譯器先處理int a = 3伦籍;首先它會(huì)在棧中創(chuàng)建一個(gè)變量為a的引用蓝晒,然后查找棧中是否有3這個(gè)值腮出,如果沒(méi)找到,就將3存放進(jìn)來(lái)拔创,然后將a指向3利诺。接著處理int b = 3;在創(chuàng)建完b的引用變量后剩燥,因?yàn)樵跅V幸呀?jīng)有3這個(gè)值慢逾,便將b直接指向3。這樣灭红,就出現(xiàn)了a與b同時(shí)均指向3的情況侣滩。這時(shí),如果再令a=4变擒;那么編譯器會(huì)重新搜索棧中是否有4值君珠,如果沒(méi)有,則將4存放進(jìn)來(lái)娇斑,并令a指向4策添;如果已經(jīng)有了毫缆,則直接將a指向這個(gè)地址唯竹。因此a值的改變不會(huì)影響到b的值。要注意這種數(shù)據(jù)的共享與兩個(gè)對(duì)象的引用同時(shí)指向一個(gè)對(duì)象的這種共享是不同的,因?yàn)檫@種情況a的修改并不會(huì)影響到b, 它是由編譯器完成的旺拉,它有利于節(jié)省空間晋涣。而一個(gè)對(duì)象引用變量修改了這個(gè)對(duì)象的內(nèi)部狀態(tài),會(huì)影響到另一個(gè)對(duì)象引用變量赌莺。
String是一個(gè)特殊的包裝類數(shù)據(jù)【〕可以用:
String str = new String("abc");
String str = "abc";
兩種的形式來(lái)創(chuàng)建续搀,第一種是用new()來(lái)新建對(duì)象的禁舷,它會(huì)在存放于堆中牵咙。每調(diào)用一次就會(huì)創(chuàng)建一個(gè)新的對(duì)象碟嘴。
而第二種是先在棧中創(chuàng)建一個(gè)對(duì)String類的對(duì)象引用變量str雀瓢,然后查找棧中有沒(méi)有存放"abc"玉掸,如果沒(méi)有司浪,則將"abc"存放進(jìn)棧,并令str指向”abc”,如果已經(jīng)有”abc” 則直接令str指向“abc”憔足。
比較類里面的數(shù)值是否相等時(shí)揭绑,用equals()方法他匪;當(dāng)測(cè)試兩個(gè)包裝類的引用是否指向同一個(gè)對(duì)象時(shí)菇存,用==,下面用例子說(shuō)明上面的理論邦蜜。
String str1 = "abc";
String str2 = "abc";
System.out.println(str1==str2); //true
可以看出str1和str2是指向同一個(gè)對(duì)象的依鸥。
String str1 =new String ("abc");
String str2 =new String ("abc");
System.out.println(str1==str2); // false
用new的方式是生成不同的對(duì)象。每一次生成一個(gè)悼沈。
因此用第一種方式創(chuàng)建多個(gè)”abc”字符串,在內(nèi)存中其實(shí)只存在一個(gè)對(duì)象而已. 這種寫(xiě)法有利與節(jié)省內(nèi)存空間. 同時(shí)它可以在一定程度上提高程序的運(yùn)行速度贱迟,因?yàn)镴VM會(huì)自動(dòng)根據(jù)棧中數(shù)據(jù)的實(shí)際情況來(lái)決定是否有必要?jiǎng)?chuàng)建新對(duì)象。而對(duì)于String str = new String("abc")絮供;的代碼衣吠,則一概在堆中創(chuàng)建新對(duì)象,而不管其字符串值是否相等壤靶,是否有必要?jiǎng)?chuàng)建新對(duì)象缚俏,從而加重了程序的負(fù)擔(dān)。
另一方面, 要注意: 我們?cè)谑褂弥T如String str = "abc";的格式定義類時(shí)袍榆,總是想當(dāng)然地認(rèn)為,創(chuàng)建了String類的對(duì)象str塘揣。擔(dān)心陷阱包雀!對(duì)象可能并沒(méi)有被創(chuàng)建!而可能只是指向一個(gè)先前已經(jīng)創(chuàng)建的對(duì)象亲铡。只有通過(guò)new()方法才能保證每次都創(chuàng)建一個(gè)新的對(duì)象才写。由于String類的immutable性質(zhì),當(dāng)String變量需要經(jīng)常變換其值時(shí)奖蔓,應(yīng)該考慮使用StringBuffer類赞草,以提高程序效率。
java中內(nèi)存分配策略及堆和棧的比較
1吆鹤、內(nèi)存分配策略
按照編譯原理的觀點(diǎn),程序運(yùn)行時(shí)的內(nèi)存分配有三種策略,分別是靜態(tài)的,棧式的,和堆式的.
靜態(tài)存儲(chǔ)分配是指在編譯時(shí)就能確定每個(gè)數(shù)據(jù)目標(biāo)在運(yùn)行時(shí)刻的存儲(chǔ)空間需求,因而在編譯時(shí)就可以給他們分配固定的內(nèi)存空間.這種分配策略要求程序代碼中不允許有可變數(shù)據(jù)結(jié)構(gòu)(比如可變數(shù)組)的存在,也不允許有嵌套或者遞歸的結(jié)構(gòu)出現(xiàn),因?yàn)樗鼈兌紩?huì)導(dǎo)致編譯程序無(wú)法計(jì)算準(zhǔn)確的存儲(chǔ)空間需求.
棧式存儲(chǔ)分配也可稱為動(dòng)態(tài)存儲(chǔ)分配,是由一個(gè)類似于堆棧的運(yùn)行棧來(lái)實(shí)現(xiàn)的.和靜態(tài)存儲(chǔ)分配相反,在棧式存儲(chǔ)方案中,程序?qū)?shù)據(jù)區(qū)的需求在編譯時(shí)是完全未知的,只有到運(yùn)行的時(shí)候才能夠知道,但是規(guī)定在運(yùn)行中進(jìn)入一個(gè)程序模塊時(shí),必須知道該程序模塊所需的數(shù)據(jù)區(qū)大小才能夠?yàn)槠浞峙鋬?nèi)存.和我們?cè)跀?shù)據(jù)結(jié)構(gòu)所熟知的棧一樣, 棧式存儲(chǔ)分配按照先進(jìn)后出的原則進(jìn)行分配厨疙。
靜態(tài)存儲(chǔ)分配要求在編譯時(shí)能知道所有變量的存儲(chǔ)要求,棧式存儲(chǔ)分配要求在過(guò)程的入口處必須知道所有的存儲(chǔ)要求,而堆式存儲(chǔ)分配則專門(mén)負(fù)責(zé)在編譯時(shí)或運(yùn)行時(shí)模塊入口處都無(wú)法確定存儲(chǔ)要求的數(shù)據(jù)結(jié)構(gòu)的內(nèi)存分配,比如可變長(zhǎng)度串和對(duì)象實(shí)例.堆由大片的可利用塊或空閑塊組成,堆中的內(nèi)存可以按照任意順序分配和釋放.
2、堆和棧的比較
上面的定義從編譯原理的教材中總結(jié)而來(lái),除靜態(tài)存儲(chǔ)分配之外,都顯得很呆板和難以理解,下面撇開(kāi)靜態(tài)存儲(chǔ)分配,集中比較堆和棧:
從堆和棧的功能和作用來(lái)通俗的比較,堆主要用來(lái)存放對(duì)象的疑务,棧主要是用來(lái)執(zhí)行程序的.而這種不同又主要是由于堆和棧的特點(diǎn)決定的:
在編程中沾凄,例如C/C++中,所有的方法調(diào)用都是通過(guò)棧來(lái)進(jìn)行的,所有的局部變量,形式參數(shù)都是從棧中分配內(nèi)存空間的知允。實(shí)際上也不是什么分配,只是從棧頂向上用就行,就好像工廠中的傳送帶(conveyor belt)一樣,Stack Pointer會(huì)自動(dòng)指引你到放東西的位置,你所要做的只是把東西放下來(lái)就行.退出函數(shù)的時(shí)候撒蟀,修改棧指針就可以把棧中的內(nèi)容銷(xiāo)毀.這樣的模式速度最快, 當(dāng)然要用來(lái)運(yùn)行程序了.需要注意的是,在分配的時(shí)候,比如為一個(gè)即將要調(diào)用的程序模塊分配數(shù)據(jù)區(qū)時(shí),應(yīng)事先知道這個(gè)數(shù)據(jù)區(qū)的大小,也就說(shuō)是雖然分配是在程序運(yùn)行時(shí)進(jìn)行的,但是分配的大小多少是確定的,不變的,而這個(gè)"大小多少"是在編譯時(shí)確定的,不是在運(yùn)行時(shí).
堆是應(yīng)用程序在運(yùn)行的時(shí)候請(qǐng)求操作系統(tǒng)分配給自己內(nèi)存,由于從操作系統(tǒng)管理的內(nèi)存分配,所以在分配和銷(xiāo)毀時(shí)都要占用時(shí)間温鸽,因此用堆的效率非常低.但是堆的優(yōu)點(diǎn)在于,編譯器不必知道要從堆里分配多少存儲(chǔ)空間保屯,也不必知道存儲(chǔ)的數(shù)據(jù)要在堆里停留多長(zhǎng)的時(shí)間,因此,用堆保存數(shù)據(jù)時(shí)會(huì)得到更大的靈活性。事實(shí)上,面向?qū)ο蟮亩鄳B(tài)性,堆內(nèi)存分配是必不可少的,因?yàn)槎鄳B(tài)變量所需的存儲(chǔ)空間只有在運(yùn)行時(shí)創(chuàng)建了對(duì)象之后才能確定.在C++中涤垫,要求創(chuàng)建一個(gè)對(duì)象時(shí)姑尺,只需用 new命令編制相關(guān)的代碼即可。執(zhí)行這些代碼時(shí)雹姊,會(huì)在堆里自動(dòng)進(jìn)行數(shù)據(jù)的保存.當(dāng)然股缸,為達(dá)到這種靈活性,必然會(huì)付出一定的代價(jià):在堆里分配存儲(chǔ)空間時(shí)會(huì)花掉更長(zhǎng)的時(shí)間吱雏!這也正是導(dǎo)致我們剛才所說(shuō)的效率低的原因,看來(lái)列寧同志說(shuō)的好,人的優(yōu)點(diǎn)往往也是人的缺點(diǎn),人的缺點(diǎn)往往也是人的優(yōu)點(diǎn)(暈~).
3敦姻、JVM中的堆和棧
JVM是基于堆棧的虛擬機(jī).JVM為每個(gè)新創(chuàng)建的線程都分配一個(gè)堆棧.也就是說(shuō),對(duì)于一個(gè)Java程序來(lái)說(shuō),它的運(yùn)行就是通過(guò)對(duì)堆棧的操作來(lái)完成的歧杏。堆棧以幀為單位保存線程的狀態(tài)镰惦。JVM對(duì)堆棧只進(jìn)行兩種操作:以幀為單位的壓棧和出棧操作。
我們知道,某個(gè)線程正在執(zhí)行的方法稱為此線程的當(dāng)前方法.我們可能不知道,當(dāng)前方法使用的幀稱為當(dāng)前幀犬绒。當(dāng)線程激活一個(gè)Java方法,JVM就會(huì)在線程的 Java堆棧里新壓入一個(gè)幀旺入。這個(gè)幀自然成為了當(dāng)前幀.在此方法執(zhí)行期間,這個(gè)幀將用來(lái)保存參數(shù),局部變量,中間計(jì)算過(guò)程和其他數(shù)據(jù).這個(gè)幀在這里和編譯原理中的活動(dòng)紀(jì)錄的概念是差不多的.
從Java的這種分配機(jī)制來(lái)看,堆棧又可以這樣理解:堆棧(Stack)是操作系統(tǒng)在建立某個(gè)進(jìn)程時(shí)或者線程(在支持多線程的操作系統(tǒng)中是線程)為這個(gè)線程建立的存儲(chǔ)區(qū)域,該區(qū)域具有先進(jìn)后出的特性。
每一個(gè)Java應(yīng)用都唯一對(duì)應(yīng)一個(gè)JVM實(shí)例茵瘾,每一個(gè)實(shí)例唯一對(duì)應(yīng)一個(gè)堆礼华。應(yīng)用程序在運(yùn)行中所創(chuàng)建的所有類實(shí)例或數(shù)組都放在這個(gè)堆中,并由應(yīng)用所有的線程共享.跟C/C++不同,Java中分配堆內(nèi)存是自動(dòng)初始化的拗秘。Java中所有對(duì)象的存儲(chǔ)空間都是在堆中分配的圣絮,但是這個(gè)對(duì)象的引用卻是在堆棧中分配,也就是說(shuō)在建立一個(gè)對(duì)象時(shí)從兩個(gè)地方都分配內(nèi)存,在堆中分配的內(nèi)存實(shí)際建立這個(gè)對(duì)象雕旨,而在堆棧中分配的內(nèi)存只是一個(gè)指向這個(gè)堆對(duì)象的指針(引用)而已扮匠。
JVM運(yùn)行時(shí),將內(nèi)存分為堆和棧凡涩,堆中存放的是創(chuàng)建的對(duì)象棒搜,JAVA字符串對(duì)象內(nèi)存實(shí)現(xiàn)時(shí),在堆中開(kāi)辟了一快很小的內(nèi)存活箕,叫字符串常量池力麸,用來(lái)存放特定的字符串對(duì)象。
關(guān)于String對(duì)象的創(chuàng)建育韩,兩種方式是不同的末盔,第一種不用new的簡(jiǎn)單語(yǔ)法,即
String s1="JAVA";
創(chuàng)建步驟是先看常量池中有沒(méi)有與"JAVA"相同的的字符串對(duì)象座慰,如果有陨舱,將s1指向該對(duì)象,若沒(méi)有版仔,則創(chuàng)建一個(gè)新對(duì)象游盲,并讓s1指向它。
第二種是new語(yǔ)法
String s2=new String ("JAVA");
這種語(yǔ)法是在堆而不是在常量池中創(chuàng)建對(duì)象蛮粮,并將s2指向它益缎,然后去字符串常量池中看看,是否有與之相同的內(nèi)容的對(duì)象然想,如果有莺奔,則將new出來(lái)的字符串對(duì)象與字符串常量池中的對(duì)象聯(lián)系起來(lái),如果沒(méi)有变泄,則在字符串常量池中再創(chuàng)建一個(gè)包含該內(nèi)容的字符串對(duì)象令哟,并將堆內(nèi)存中的對(duì)象與字符串常量池中新建出來(lái)的對(duì)象聯(lián)系起來(lái)。
這就是字符串的一次投入妨蛹,終生回報(bào)的內(nèi)存機(jī)制屏富,對(duì)字符串的比較帶來(lái)好處。
http://blog.csdn.net/xyz1982510/archive/2008/09/12/2916467.aspx
棧(stack)與堆(heap)都是Java用來(lái)在Ram中存放數(shù)據(jù)的地方蛙卤。與C++不同狠半,Java自動(dòng)管理?xiàng):投沿溃绦騿T不能直接地設(shè)置棧或堆神年。
棧的優(yōu)勢(shì)是已维,存取速度比堆要快,僅次于直接位于CPU中的寄存器已日。但缺點(diǎn)是衣摩,存在棧中的數(shù)據(jù)大小與生存期必須是確定的,缺乏靈活性捂敌。另外,棧數(shù)據(jù)可以共享既琴,詳見(jiàn)第3點(diǎn)占婉。堆的優(yōu)勢(shì)是可以動(dòng)態(tài)地分配內(nèi)存大小,生存期也不必事先告訴編譯器甫恩,Java的垃圾收集器會(huì)自動(dòng)收走這些不再使用的數(shù)據(jù)逆济。但缺點(diǎn)是,由于要在運(yùn)行時(shí)動(dòng)態(tài)分配內(nèi)存磺箕,存取速度較慢奖慌。
Java中的數(shù)據(jù)類型有兩種。
一種是基本類型(primitive types)出于追求速度的原因松靡,就存在于棧中简僧。
出于追求速度的原因,就存在于棧中雕欺。
另一種是包裝類數(shù)據(jù)岛马,如Integer, String, Double等將相應(yīng)的基本數(shù)據(jù)類型包裝起來(lái)的類。這些類數(shù)據(jù)全部存在于堆中屠列,Java用new()語(yǔ)句來(lái)顯示地告訴編譯器啦逆,在運(yùn)行時(shí)才根據(jù)需要?jiǎng)討B(tài)創(chuàng)建,因此比較靈活笛洛,但缺點(diǎn)是要占用更多的時(shí)間夏志。String是一個(gè)特殊的包裝類數(shù)據(jù)。即可以用String str = new String("abc");的形式來(lái)創(chuàng)建苛让,也可以用String str = "abc"沟蔑;的形式來(lái)創(chuàng)建。前者是規(guī)范的類的創(chuàng)建過(guò)程狱杰,即在Java中溉贿,一切都是對(duì)象,而對(duì)象是類的實(shí)例浦旱,全部通過(guò)new()的形式來(lái)創(chuàng)建宇色。
值得注意的是,一般String類中字符串值都是直接存值的。但像String str = "abc"宣蠕;這種場(chǎng)合下例隆,其字符串值卻是保存了一個(gè)指向存在棧中數(shù)據(jù)的引用!
用new()來(lái)新建對(duì)象的抢蚀,都會(huì)在堆中創(chuàng)建镀层,而且其字符串是單獨(dú)存值的,即使與棧中的數(shù)據(jù)相同皿曲,也不會(huì)與棧中的數(shù)據(jù)共享唱逢。
使用String str = "abc";的方式屋休,可以在一定程度上提高程序的運(yùn)行速度坞古,因?yàn)镴VM會(huì)自動(dòng)根據(jù)棧中數(shù)據(jù)的實(shí)際情況來(lái)決定是否有必要?jiǎng)?chuàng)建新對(duì)象。而對(duì)于String str = new String("abc")劫樟;的代碼痪枫,則一概在堆中創(chuàng)建新對(duì)象,而不管其字符串值是否相等叠艳,是否有必要?jiǎng)?chuàng)建新對(duì)象奶陈,從而加重了程序的負(fù)擔(dān)。
字符串池在DataSegument里附较。既不在堆也不再棧中吃粒。 用+創(chuàng)建應(yīng)該是在堆里。
應(yīng)該說(shuō)是 特殊的堆
因?yàn)橄鄬?duì)于其他堆中的對(duì)象一旦失去引用就可能會(huì)被當(dāng)做垃圾回收掉
但是字符串對(duì)象就算失去唯一的引用也不會(huì)被回收
127 以下的整數(shù)是相等的 128以上就作為不同對(duì)象處理了
Integer i=100;
Integer j=100;
System.out.println(i==j); // 打印 true
Integer i=200;
Integer j=200;
System.out.println(i==j); //打印false
Java中的==和equals淺見(jiàn)
java中的數(shù)據(jù)類型拒课,可分為兩類:
1.基本數(shù)據(jù)類型声搁,也稱原始數(shù)據(jù)類型。byte,short,char,int,long,float,double,boolean
他們之間的比較捕发,應(yīng)用雙等號(hào)(==),比較的是他們的值疏旨。
2.復(fù)合數(shù)據(jù)類型(類)
當(dāng)他們用(==)進(jìn)行比較的時(shí)候,比較的是他們?cè)趦?nèi)存中的存放地址扎酷,所以檐涝,除非是同一個(gè)new出來(lái)的對(duì)象,他們的比較后的結(jié)果為true法挨,否則比較后結(jié)果為false谁榜。
對(duì)于復(fù)合數(shù)據(jù)類型之間進(jìn)行equals比較,在沒(méi)有覆寫(xiě)equals方法的情況下凡纳,他們之間的比較還是基于他們?cè)趦?nèi)存中的存放位置的地址值的窃植,因?yàn)镺bject的equals方法也是用雙等號(hào)(==)進(jìn)行比較的,所以比較后的結(jié)果跟雙等號(hào)(==)的結(jié)果相同荐糜。
//Object中的equals方法
public boolean equals(Object obj) {
return (this == obj);
}
當(dāng)然巷怜,你也可以重寫(xiě)Object的equals方法葛超,這兒就有個(gè)問(wèn)題啦,參加公司筆試的時(shí)候相信N多人都被要求回答過(guò)這樣的問(wèn)題:在重寫(xiě)了對(duì)象的equals方法后延塑,還需要重寫(xiě)hashCode方法嗎绣张?為什么?
我認(rèn)為关带,出于程序完整性的考慮侥涵,在重寫(xiě)了對(duì)象的equals方法后,是有必要重寫(xiě)對(duì)象的hashCode方法的宋雏。
因?yàn)槲咂阒貙?xiě)了equals方法,你調(diào)用它來(lái)進(jìn)行對(duì)象間的比較磨总,你可以達(dá)到你的比較目的嗦明,但是,當(dāng)你想將你的對(duì)象存入類似HashSet這類對(duì)象中時(shí)舍败,問(wèn)題就出現(xiàn)了(沒(méi)有重寫(xiě)hashCode方法的情況下)。
Set(集)中是不允許有重復(fù)的值的敬拓,而判斷值是否重復(fù)邻薯,是通過(guò)比較他們的hashCode值的。你通過(guò)你重寫(xiě)后的equals比較對(duì)象乘凸,結(jié)果是相等厕诡,但用hashCode值比較他們時(shí)是不相等的,所以营勤,為了比較結(jié)果的一致性灵嫌,需要重寫(xiě)hashCode方法。
下面是String類重寫(xiě)了的equals方法和hashCode方法:
//此方法的目的是葛作,實(shí)現(xiàn)在不同的String對(duì)象之間比較寿羞,比較的是他們的字符串值
public boolean equals(Object anObject) {
if (this == anObject) {
return true;
}
if (anObject instanceof String) {
String anotherString = (String)anObject;
int n = count;
if (n == anotherString.count) {
char v1[] = value; //字符串的值,用字符數(shù)組表示
char v2[] = anotherString.value;
int i = offset;
int j = anotherString.offset;
while (n-- != 0) {
if (v1[i++] != v2[j++])
return false;
}
return true;
}
}
return false;
}
//返回的值是基于字符串的值運(yùn)算出來(lái)的赂蠢,
//字符串值相等則他們的hashCode值也相等,否則绪穆,不相等
public int hashCode() {
int h = hash;
if (h == 0) {
int off = offset;
char val[] = value; //字符串的值,用字符數(shù)組表示
int len = count;
//基于字符串的值產(chǎn)生hash值
for (int i = 0; i < len; i++) {
h = 31*h + val[off++];
}
hash = h;
}
return h;
}
問(wèn):對(duì)象的hashcode是用來(lái)干什么的虱岂?
簡(jiǎn)答:容器類經(jīng)常用到hascode,比如說(shuō)set判斷重復(fù)值玖院,比如說(shuō)hashmap散列。等等第岖。
解析Java對(duì)象的equals()和hashCode()的使用
文章分類:Java編程
前言
在Java語(yǔ)言中难菌,equals()和hashCode()兩個(gè)函數(shù)的使用是緊密配合的,你要是自己設(shè)計(jì)其中一個(gè)蔑滓,就要設(shè)計(jì)另外一個(gè)郊酒。在多數(shù)情況下遇绞,這兩個(gè)函數(shù)是不用考慮的,直接使用它們的默認(rèn)設(shè)計(jì)就可以了猎塞。但是在一些情況下试读,這兩個(gè)函數(shù)最好是自己設(shè)計(jì),才能確保整個(gè)程序的正常運(yùn)行荠耽。最常見(jiàn)的是當(dāng)一個(gè)對(duì)象被加入收集對(duì)象(collection object)時(shí)钩骇,這兩個(gè)函數(shù)必須自己設(shè)計(jì)。更細(xì)化的定義是:如果你想將一個(gè)對(duì)象A放入另一個(gè)收集對(duì)象B里铝量,或者使用這個(gè)對(duì)象A為查找一個(gè)元對(duì)象在收集對(duì)象B里位置的鑰匙倘屹,并支持是否容納,刪除收集對(duì)象B里的元對(duì)象這樣的操作慢叨,那么纽匙,equals()和hashCode()函數(shù)必須開(kāi)發(fā)者自己定義。其他情況下拍谐,這兩個(gè)函數(shù)是不需要定義的烛缔。
equals():
它是用于進(jìn)行兩個(gè)對(duì)象的比較的,是對(duì)象內(nèi)容的比較轩拨,當(dāng)然也能用于進(jìn)行對(duì)象參閱值的比較践瓷。什么是對(duì)象參閱值的比較?就是兩個(gè)參閱變量的值得比較亡蓉,我們都知道參閱變量的值其實(shí)就是一個(gè)數(shù)字晕翠,這個(gè)數(shù)字可以看成是鑒別不同對(duì)象的代號(hào)。兩個(gè)對(duì)象參閱值的比較砍濒,就是兩個(gè)數(shù)字的比較淋肾,兩個(gè)代號(hào)的比較。這種比較是默認(rèn)的對(duì)象比較方式爸邢,在Object這個(gè)對(duì)象中樊卓,這種方式就已經(jīng)設(shè)計(jì)好了。所以你也不用自己來(lái)重寫(xiě)杠河,浪費(fèi)不必要的時(shí)間简识。
對(duì)象內(nèi)容的比較才是設(shè)計(jì)equals()的真正目的,Java語(yǔ)言對(duì)equals()的要求如下感猛,這些要求是必須遵循的七扰。否則,你就不該浪費(fèi)時(shí)間:
* 對(duì)稱性:如果x.equals(y)返回是“true”陪白,那么y.equals(x)也應(yīng)該返回是“true”颈走。
* 反射性:x.equals(x)必須返回是“true”。
* 類推性:如果x.equals(y)返回是“true”咱士,而且y.equals(z)返回是“true”立由,那么z.equals(x)也應(yīng)該返回是“true”轧钓。
* 還有一致性:如果x.equals(y)返回是“true”,只要x和y內(nèi)容一直不變锐膜,不管你重復(fù)x.equals(y)多少次毕箍,返回都是“true”。
* 任何情況下道盏,x.equals(null)而柑,永遠(yuǎn)返回是“false”;x.equals(和x不同類型的對(duì)象)永遠(yuǎn)返回是“false”荷逞。
hashCode():
這個(gè)函數(shù)返回的就是一個(gè)用來(lái)進(jìn)行赫希操作的整型代號(hào)媒咳,請(qǐng)不要把這個(gè)代號(hào)和前面所說(shuō)的參閱變量所代表的代號(hào)弄混了。后者不僅僅是個(gè)代號(hào)還具有在內(nèi)存中才查找對(duì)象的位置的功能种远。hashCode()所返回的值是用來(lái)分類對(duì)象在一些特定的收集對(duì)象中的位置涩澡。這些對(duì)象是HashMap, Hashtable, HashSet,等等坠敷。這個(gè)函數(shù)和上面的equals()函數(shù)必須自己設(shè)計(jì)妙同,用來(lái)協(xié)助HashMap, Hashtable, HashSet,等等對(duì)自己所收集的大量對(duì)象進(jìn)行搜尋和定位膝迎。
這些收集對(duì)象究竟如何工作的粥帚,想象每個(gè)元對(duì)象hashCode是一個(gè)箱子的編碼,按照編碼弄抬,每個(gè)元對(duì)象就是根據(jù)hashCode()提供的代號(hào)歸入相應(yīng)的箱子里茎辐。所有的箱子加起來(lái)就是一個(gè)HashSet宪郊,HashMap掂恕,或 Hashtable對(duì)象,我們需要尋找一個(gè)元對(duì)象時(shí)弛槐,先看它的代碼懊亡,就是hashCode()返回的整型值,這樣我們找到它所在的箱子乎串,然后在箱子里店枣,每個(gè)元對(duì)象都拿出來(lái)一個(gè)個(gè)和我們要找的對(duì)象進(jìn)行對(duì)比,如果兩個(gè)對(duì)象的內(nèi)容相等叹誉,我們的搜尋也就結(jié)束鸯两。這種操作需要兩個(gè)重要的信息,一是對(duì)象的 hashCode()长豁,還有一個(gè)是對(duì)象內(nèi)容對(duì)比的結(jié)果钧唐。
hashCode()的返回值和equals()的關(guān)系如下:
* 如果x.equals(y)返回“true”,那么x和y的hashCode()必須相等匠襟。
* 如果x.equals(y)返回“false”钝侠,那么x和y的hashCode()有可能相等该园,也有可能不等。
為什么這兩個(gè)規(guī)則是這樣的帅韧,原因其實(shí)很簡(jiǎn)單里初,拿HashSet來(lái)說(shuō)吧,HashSet可以擁有一個(gè)或更多的箱子忽舟,在同一個(gè)箱子中可以有一個(gè)或更多的獨(dú)特元對(duì)象(HashSet所容納的必須是獨(dú)特的元對(duì)象)双妨。這個(gè)例子說(shuō)明一個(gè)元對(duì)象可以和其他不同的元對(duì)象擁有相同的hashCode。但是一個(gè)元對(duì)象只能和擁有同樣內(nèi)容的元對(duì)象相等萧诫。所以這兩個(gè)規(guī)則必須成立斥难。
設(shè)計(jì)這兩個(gè)函數(shù)所要注意到的:
如果你設(shè)計(jì)的對(duì)象類型并不使用于收集性對(duì)象,那么沒(méi)有必要自己再設(shè)計(jì)這兩個(gè)函數(shù)的處理方式帘饶。這是正確的面向?qū)ο笤O(shè)計(jì)方法哑诊,任何用戶一時(shí)用不到的功能,就先不要設(shè)計(jì)及刻,以免給日后功能擴(kuò)展帶來(lái)麻煩镀裤。
如果你在設(shè)計(jì)時(shí)想別出心裁,不遵守以上的兩套規(guī)則缴饭,那么勸你還是不要做這樣想入非非的事暑劝。我還沒(méi)有遇到過(guò)哪一個(gè)開(kāi)發(fā)者和我說(shuō)設(shè)計(jì)這兩個(gè)函數(shù)要違背前面說(shuō)的兩個(gè)規(guī)則,我碰到這些違反規(guī)則的情況時(shí)颗搂,都是作為設(shè)計(jì)錯(cuò)誤處理担猛。
當(dāng)一個(gè)對(duì)象類型作為收集型對(duì)象的元對(duì)象時(shí),這個(gè)對(duì)象應(yīng)該擁有自己處理equals()丢氢,和/或處理hashCode()的設(shè)計(jì)傅联,而且要遵守前面所說(shuō)的兩種原則。equals()先要查null和是否是同一類型疚察。查同一類型是為了避免出現(xiàn)ClassCastException這樣的異常給丟出來(lái)蒸走。查 null是為了避免出現(xiàn)NullPointerException這樣的異常給丟出來(lái)。
如果你的對(duì)象里面容納的數(shù)據(jù)過(guò)多貌嫡,那么這兩個(gè)函數(shù) equals()和hashCode()將會(huì)變得效率低比驻。如果對(duì)象中擁有無(wú)法serialized的數(shù)據(jù),equals()有可能在操作中出現(xiàn)錯(cuò)誤岛抄。想象一個(gè)對(duì)象x别惦,它的一個(gè)整型數(shù)據(jù)是transient型(不能被serialize成二進(jìn)制數(shù)據(jù)流)。然而equals()和hashCode()都有依靠這個(gè)整型數(shù)據(jù)夫椭,那么掸掸,這個(gè)對(duì)象在serialization之前和之后,是否一樣益楼?答案是不一樣猾漫。因?yàn)閟erialization之前的整型數(shù)據(jù)是有效的數(shù)據(jù)点晴,在serialization之后,這個(gè)整型數(shù)據(jù)的值并沒(méi)有存儲(chǔ)下來(lái)悯周,再重新由二進(jìn)制數(shù)據(jù)流轉(zhuǎn)換成對(duì)象后粒督,兩者(對(duì)象在serialization 之前和之后)的狀態(tài)已經(jīng)不同了。這也是要注意的禽翼。
知道以上這些能夠幫助你:
- 進(jìn)行更好的設(shè)計(jì)和開(kāi)發(fā)屠橄。
- 進(jìn)行更好的測(cè)試案例開(kāi)發(fā)。
- 在面試過(guò)程中讓面試者對(duì)你的學(xué)識(shí)淵博感到滿意闰挡。
hashCode的作用
總的來(lái)說(shuō)锐墙,Java中的集合(Collection)有兩類,一類是List长酗,再有一類是 Set溪北。你知道它們的區(qū)別嗎?前者集合內(nèi)的元素是有序的夺脾,元素可以重復(fù)之拨;后者元素?zé)o序,但元素不可重復(fù)咧叭。那么這里就有一個(gè)比較嚴(yán)重的問(wèn)題了:要想保證元素不重復(fù)蚀乔,可兩個(gè)元素是否重復(fù)應(yīng)該依據(jù)什么來(lái)判斷呢?這就是Object.equals方法了菲茬。但是吉挣,如果每增加一個(gè)元素就檢查一次,那么當(dāng)元素很多時(shí)婉弹,后添加到集合中的元素比較的次數(shù)就非常多了睬魂。也就是說(shuō),如果集合中現(xiàn)在已經(jīng)有1000個(gè)元素马胧,那么第1001個(gè)元素加入集合時(shí)汉买,它就要調(diào)用1000次 equals方法衔峰。這顯然會(huì)大大降低效率佩脊。 于是,Java采用了哈希表的原理垫卤。哈希(Hash)實(shí)際上是個(gè)人名威彰,由于他提出一哈希算法的概念,所以就以他的名字命名了穴肘。哈希算法也稱為散列算法歇盼,是將數(shù)據(jù)依特定算法直接指定到一個(gè)地址上。如果詳細(xì)講解哈希算法评抚,那需要更多的文章篇幅豹缀,我在這里就不介紹了伯复。初學(xué)者可以這樣理解,hashCode方法實(shí)際上返回的就是對(duì)象存儲(chǔ)的物理地址(實(shí)際可能并不是)邢笙。 這樣一來(lái)啸如,當(dāng)集合要添加新的元素時(shí),先調(diào)用這個(gè)元素的hashCode方法氮惯,就一下子能定位到它應(yīng)該放置的物理位置上叮雳。如果這個(gè)位置上沒(méi)有元素,它就可以直接存儲(chǔ)在這個(gè)位置上妇汗,不用再進(jìn)行任何比較了帘不;如果這個(gè)位置上已經(jīng)有元素了,就調(diào)用它的equals方法與新元素進(jìn)行比較杨箭,相同的話就不存了寞焙,不相同就散列其它的地址。所以這里存在一個(gè)沖突解決的問(wèn)題互婿。這樣一來(lái)實(shí)際調(diào)用equals方法的次數(shù)就大大降低了棺弊,幾乎只需要一兩次。所以擒悬,Java對(duì)于eqauls方法和hashCode方法是這樣規(guī)定的:1模她、如果兩個(gè)對(duì)象相同,那么它們的hashCode值一定要相同懂牧;2侈净、如果兩個(gè)對(duì)象的hashCode相同,它們并不一定相同僧凤,上面說(shuō)的對(duì)象相同指的是用eqauls方法比較畜侦。你當(dāng)然可以不按要求去做了,但你會(huì)發(fā)現(xiàn)躯保,相同的對(duì)象可以出現(xiàn)在Set集合中旋膳。同時(shí),增加新元素的效率會(huì)大大下降途事。
我們知道验懊,equals()函數(shù)是用來(lái)做比較的。java中的比較有兩種:一種是內(nèi)存地址的比較尸变,一種是內(nèi)容的比較义图。而比較個(gè)體也有兩種:一種是簡(jiǎn)單類型(這類簡(jiǎn)單說(shuō)來(lái)無(wú)所謂內(nèi)存地址的比較或者內(nèi)容比較的區(qū)別);還有一種是對(duì)象的比較召烂,本文中說(shuō)的主要是后者
在java中碱工,(對(duì)象)內(nèi)存地址的比較,是通過(guò)==完成的。比如
這樣的語(yǔ)句中怕篷,我們認(rèn)為历筝,如果obj1和obj2的內(nèi)存地址相同,則返回true
而equals()通常是比較內(nèi)容的廊谓。這里說(shuō)“通陈龋” ,是因?yàn)樵谧罡镜腛bject類中蹂析,equal()函數(shù)做的是地址的比較舔示。而在其他幾乎所有的類中,equals()都經(jīng)過(guò)重載电抚,進(jìn)行內(nèi)容的比較惕稻。
而在說(shuō)equals()的時(shí)候我們還涉及hashCode()是因?yàn)樵谟行?yīng)用中(比如,HashMap的key是對(duì)象)蝙叛,必須在重載equals()的同時(shí)重載hashCode()俺祠。因?yàn)閖ava中默認(rèn)(Object)的hashCode是根據(jù)對(duì)象的地址計(jì)算得到的。
我們通常不會(huì)注意到這個(gè)問(wèn)題借帘,因?yàn)槲覀兺ǔK褂玫膋ey都是簡(jiǎn)單類型蜘渣,或者是String, Long等一些特殊的對(duì)象(其特殊性請(qǐng)參看筆者在寫(xiě)java 淺拷貝和深拷貝時(shí)的討論),這時(shí)候肺然,這個(gè)問(wèn)題被我們無(wú)意間繞過(guò)了
有人已經(jīng)概括了這種我們忽略了的情況:“如果你想將一個(gè)對(duì)象A放入另一個(gè)收集(集合)對(duì)象B里蔫缸,或者使用這個(gè)對(duì)象A為查找一個(gè)元對(duì)象在收集對(duì)象B里位置的鑰匙(key),并支持是否容納(isContains())际起,刪除收集對(duì)象B里的元對(duì)象(remove()?)這樣的操作拾碌,那么,equals()和hashCode()函數(shù)必須開(kāi)發(fā)者自己定義街望⌒O瑁” (括號(hào)為筆者添加)