轉(zhuǎn)https://blog.csdn.net/LL1187740947/article/details/78419637
問題的引入:
問題一:
String str1 = "abc";
String str2 = "abc";
System.out.println(str1==str2); //true
問題二:
String str1 =new String ("abc");
String str2 =new String ("abc");
System.out.println(str1==str2); // false
問題三:
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
由于以上問題讓我含糊不清,于是特地搜集了一些有關(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)超過變量的作用域后,Java會(huì)自動(dòng)釋放掉為該變量所分配的內(nèi)存空間,該內(nèi)存空間可以立即被另作他用。
堆內(nèi)存用來存放由new創(chuàng)建的對(duì)象和數(shù)組。
在堆中分配的內(nèi)存,由Java虛擬機(jī)的自動(dòng)垃圾回收器來管理。
在堆中產(chǎn)生了一個(gè)數(shù)組或?qū)ο蠛?,還可以在棧中定義一個(gè)特殊的變量,讓棧中這個(gè)變量的取值等于數(shù)組或?qū)ο笤诙褍?nèi)存中的首地址,棧中的這個(gè)變量就成了數(shù)組或?qū)ο蟮囊米兞俊?/p>
引用變量就相當(dāng)于是為數(shù)組或?qū)ο笃鸬囊粋€(gè)名稱,以后就可以在程序中使用棧中的引用變量來訪問堆中的數(shù)組或?qū)ο蟆?/p>
具體的說:
棧與堆都是Java用來在Ram中存放數(shù)據(jù)的地方。與C++不同,Java自動(dòng)管理?xiàng):投?,程序員不能直接地設(shè)置棧或堆。
Java的堆是一個(gè)運(yùn)行時(shí)數(shù)據(jù)區(qū),類的(對(duì)象從中分配空間。這些對(duì)象通過new、newarray、anewarray和multianewarray等指令建立,它們不需要程序代碼來顯式的釋放。堆是由垃圾回收來負(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è)值,如果沒找到,就將3存放進(jìn)來,然后將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值,如果沒有,則將4存放進(jìn)來,并令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";
兩種的形式來創(chuàng)建,第一種是用new()來新建對(duì)象的,它會(huì)在存放于堆中。每調(diào)用一次就會(huì)創(chuàng)建一個(gè)新的對(duì)象。
而第二種是先在棧中創(chuàng)建一個(gè)對(duì)String類的對(duì)象引用變量str,然后查找棧中有沒有存放"abc",如果沒有,則將"abc"存放進(jìn)棧,并令str指向”abc”,如果已經(jīng)有”abc” 則直接令str指向“abc”。
比較類里面的數(shù)值是否相等時(shí),用equals()方法;當(dāng)測(cè)試兩個(gè)包裝類的引用是否指向同一個(gè)對(duì)象時(shí),用==,下面用例子說明上面的理論。
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ì)象而已. 這種寫法有利與節(jié)省內(nèi)存空間. 同時(shí)它可以在一定程度上提高程序的運(yùn)行速度,因?yàn)镴VM會(huì)自動(dòng)根據(jù)棧中數(shù)據(jù)的實(shí)際情況來決定是否有必要?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ì)象可能并沒有被創(chuàng)建!而可能只是指向一個(gè)先前已經(jīng)創(chuàng)建的對(duì)象。只有通過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)致編譯程序無法計(jì)算準(zhǔn)確的存儲(chǔ)空間需求.
棧式存儲(chǔ)分配也可稱為動(dòng)態(tài)存儲(chǔ)分配,是由一個(gè)類似于堆棧的運(yùn)行棧來實(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ǔ)分配要求在過程的入口處必須知道所有的存儲(chǔ)要求,而堆式存儲(chǔ)分配則專門負(fù)責(zé)在編譯時(shí)或運(yùn)行時(shí)模塊入口處都無法確定存儲(chǔ)要求的數(shù)據(jù)結(jié)構(gòu)的內(nèi)存分配,比如可變長(zhǎng)度串和對(duì)象實(shí)例.堆由大片的可利用塊或空閑塊組成,堆中的內(nèi)存可以按照任意順序分配和釋放.
2、堆和棧的比較
上面的定義從編譯原理的教材中總結(jié)而來,除靜態(tài)存儲(chǔ)分配之外,都顯得很呆板和難以理解,下面撇開靜態(tài)存儲(chǔ)分配,集中比較堆和棧:
從堆和棧的功能和作用來通俗的比較,堆主要用來存放對(duì)象的,棧主要是用來執(zhí)行程序的.而這種不同又主要是由于堆和棧的特點(diǎn)決定的:
在編程中,例如C/C++中,所有的方法調(diào)用都是通過棧來進(jìn)行的,所有的局部變量,形式參數(shù)都是從棧中分配內(nèi)存空間的。實(shí)際上也不是什么分配,只是從棧頂向上用就行,就好像工廠中的傳送帶(conveyor belt)一樣,Stack Pointer會(huì)自動(dòng)指引你到放東西的位置,你所要做的只是把東西放下來就行.退出函數(shù)的時(shí)候,修改棧指針就可以把棧中的內(nèi)容銷毀.這樣的模式速度最快, 當(dāng)然要用來運(yùn)行程序了.需要注意的是,在分配的時(shí)候,比如為一個(gè)即將要調(diào)用的程序模塊分配數(shù)據(jù)區(qū)時(shí),應(yīng)事先知道這個(gè)數(shù)據(jù)區(qū)的大小,也就說是雖然分配是在程序運(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)存分配,所以在分配和銷毀時(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)致我們剛才所說的效率低的原因,看來列寧同志說的好,人的優(yōu)點(diǎn)往往也是人的缺點(diǎn),人的缺點(diǎn)往往也是人的優(yōu)點(diǎn)(暈~).
3、JVM中的堆和棧
JVM是基于堆棧的虛擬機(jī).JVM為每個(gè)新創(chuàng)建的線程都分配一個(gè)堆棧.也就是說,對(duì)于一個(gè)Java程序來說,它的運(yùn)行就是通過對(duì)堆棧的操作來完成的。堆棧以幀為單位保存線程的狀態(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è)幀將用來保存參數(shù),局部變量,中間計(jì)算過程和其他數(shù)據(jù).這個(gè)幀在這里和編譯原理中的活動(dòng)紀(jì)錄的概念是差不多的.
從Java的這種分配機(jī)制來看,堆棧又可以這樣理解:堆棧(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ì)象的引用卻是在堆棧中分配,也就是說在建立一個(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í),在堆中開辟了一快很小的內(nèi)存,叫字符串常量池,用來存放特定的字符串對(duì)象。
關(guān)于String對(duì)象的創(chuàng)建,兩種方式是不同的,第一種不用new的簡(jiǎn)單語法,即
String s1="JAVA";
創(chuàng)建步驟是先看常量池中有沒有與"JAVA"相同的的字符串對(duì)象,如果有,將s1指向該對(duì)象,若沒有,則創(chuàng)建一個(gè)新對(duì)象,并讓s1指向它。
第二種是new語法
String s2=new String ("JAVA");
這種語法是在堆而不是在常量池中創(chuàng)建對(duì)象,并將s2指向它,然后去字符串常量池中看看,是否有與之相同的內(nèi)容的對(duì)象,如果有,則將new出來的字符串對(duì)象與字符串常量池中的對(duì)象聯(lián)系起來,如果沒有,則在字符串常量池中再創(chuàng)建一個(gè)包含該內(nèi)容的字符串對(duì)象,并將堆內(nèi)存中的對(duì)象與字符串常量池中新建出來的對(duì)象聯(lián)系起來。
這就是字符串的一次投入,終生回報(bào)的內(nèi)存機(jī)制,對(duì)字符串的比較帶來好處。
http://blog.csdn.net/xyz1982510/archive/2008/09/12/2916467.aspx
棧(stack)與堆(heap)都是Java用來在Ram中存放數(shù)據(jù)的地方。與C++不同,Java自動(dòng)管理?xiàng):投眩绦騿T不能直接地設(shè)置棧或堆。
棧的優(yōu)勢(shì)是,存取速度比堆要快,僅次于直接位于CPU中的寄存器。但缺點(diǎn)是,存在棧中的數(shù)據(jù)大小與生存期必須是確定的,缺乏靈活性。另外,棧數(shù)據(jù)可以共享,詳見第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ù)類型包裝起來的類。這些類數(shù)據(jù)全部存在于堆中,Java用new()語句來顯示地告訴編譯器,在運(yùn)行時(shí)才根據(jù)需要?jiǎng)討B(tài)創(chuàng)建,因此比較靈活,但缺點(diǎn)是要占用更多的時(shí)間。String是一個(gè)特殊的包裝類數(shù)據(jù)。即可以用String str = new String("abc");的形式來創(chuàng)建,也可以用String str = "abc";的形式來創(chuàng)建。前者是規(guī)范的類的創(chuàng)建過程,即在Java中,一切都是對(duì)象,而對(duì)象是類的實(shí)例,全部通過new()的形式來創(chuàng)建。
值得注意的是,一般String類中字符串值都是直接存值的。但像String str = "abc";這種場(chǎng)合下,其字符串值卻是保存了一個(gè)指向存在棧中數(shù)據(jù)的引用!
用new()來新建對(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í)際情況來決定是否有必要?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)該說是 特殊的堆
因?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淺見
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出來的對(duì)象,他們的比較后的結(jié)果為true,否則比較后結(jié)果為false。
對(duì)于復(fù)合數(shù)據(jù)類型之間進(jìn)行equals比較,在沒有覆寫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)然,你也可以重寫Object的equals方法,這兒就有個(gè)問題啦,參加公司筆試的時(shí)候相信N多人都被要求回答過這樣的問題:在重寫了對(duì)象的equals方法后,還需要重寫hashCode方法嗎?為什么?
我認(rèn)為,出于程序完整性的考慮,在重寫了對(duì)象的equals方法后,是有必要重寫對(duì)象的hashCode方法的。
因?yàn)?,你重寫了equals方法,你調(diào)用它來進(jìn)行對(duì)象間的比較,你可以達(dá)到你的比較目的,但是,當(dāng)你想將你的對(duì)象存入類似HashSet這類對(duì)象中時(shí),問題就出現(xiàn)了(沒有重寫hashCode方法的情況下)。
Set(集)中是不允許有重復(fù)的值的,而判斷值是否重復(fù),是通過比較他們的hashCode值的。你通過你重寫后的equals比較對(duì)象,結(jié)果是相等,但用hashCode值比較他們時(shí)是不相等的,所以,為了比較結(jié)果的一致性,需要重寫hashCode方法。
下面是String類重寫了的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)算出來的,
//字符串值相等則他們的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;
}
問:對(duì)象的hashcode是用來干什么的?
簡(jiǎn)答:容器類經(jīng)常用到hascode,比如說set判斷重復(fù)值,比如說hashmap散列。等等。
解析Java對(duì)象的equals()和hashCode()的使用
文章分類:Java編程
前言
在Java語言中,equals()和hashCode()兩個(gè)函數(shù)的使用是緊密配合的,你要是自己設(shè)計(jì)其中一個(gè),就要設(shè)計(jì)另外一個(gè)。在多數(shù)情況下,這兩個(gè)函數(shù)是不用考慮的,直接使用它們的默認(rèn)設(shè)計(jì)就可以了。但是在一些情況下,這兩個(gè)函數(shù)最好是自己設(shè)計(jì),才能確保整個(gè)程序的正常運(yù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ù)必須開發(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ì)好了。所以你也不用自己來重寫,浪費(fèi)不必要的時(shí)間。
對(duì)象內(nèi)容的比較才是設(shè)計(jì)equals()的真正目的,Java語言對(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è)用來進(jìn)行赫希操作的整型代號(hào),請(qǐng)不要把這個(gè)代號(hào)和前面所說的參閱變量所代表的代號(hào)弄混了。后者不僅僅是個(gè)代號(hào)還具有在內(nèi)存中才查找對(duì)象的位置的功能。hashCode()所返回的值是用來分類對(duì)象在一些特定的收集對(duì)象中的位置。這些對(duì)象是HashMap, Hashtable, HashSet,等等。這個(gè)函數(shù)和上面的equals()函數(shù)必須自己設(shè)計(jì),用來協(xié)助HashMap, Hashtable, HashSet,等等對(duì)自己所收集的大量對(duì)象進(jìn)行搜尋和定位。
這些收集對(duì)象究竟如何工作的,想象每個(gè)元對(duì)象hashCode是一個(gè)箱子的編碼,按照編碼,每個(gè)元對(duì)象就是根據(jù)hashCode()提供的代號(hào)歸入相應(yīng)的箱子里。所有的箱子加起來就是一個(gè)HashSet,HashMap,或 Hashtable對(duì)象,我們需要尋找一個(gè)元對(duì)象時(shí),先看它的代碼,就是hashCode()返回的整型值,這樣我們找到它所在的箱子,然后在箱子里,每個(gè)元對(duì)象都拿出來一個(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來說吧,HashSet可以擁有一個(gè)或更多的箱子,在同一個(gè)箱子中可以有一個(gè)或更多的獨(dú)特元對(duì)象(HashSet所容納的必須是獨(dú)特的元對(duì)象)。這個(gè)例子說明一個(gè)元對(duì)象可以和其他不同的元對(duì)象擁有相同的hashCode。但是一個(gè)元對(duì)象只能和擁有同樣內(nèi)容的元對(duì)象相等。所以這兩個(gè)規(guī)則必須成立。
設(shè)計(jì)這兩個(gè)函數(shù)所要注意到的:
如果你設(shè)計(jì)的對(duì)象類型并不使用于收集性對(duì)象,那么沒有必要自己再設(shè)計(jì)這兩個(gè)函數(shù)的處理方式。這是正確的面向?qū)ο笤O(shè)計(jì)方法,任何用戶一時(shí)用不到的功能,就先不要設(shè)計(jì),以免給日后功能擴(kuò)展帶來麻煩。
如果你在設(shè)計(jì)時(shí)想別出心裁,不遵守以上的兩套規(guī)則,那么勸你還是不要做這樣想入非非的事。我還沒有遇到過哪一個(gè)開發(fā)者和我說設(shè)計(jì)這兩個(gè)函數(shù)要違背前面說的兩個(gè)規(guī)則,我碰到這些違反規(guī)則的情況時(shí),都是作為設(shè)計(jì)錯(cuò)誤處理。
當(dāng)一個(gè)對(duì)象類型作為收集型對(duì)象的元對(duì)象時(shí),這個(gè)對(duì)象應(yīng)該擁有自己處理equals(),和/或處理hashCode()的設(shè)計(jì),而且要遵守前面所說的兩種原則。equals()先要查null和是否是同一類型。查同一類型是為了避免出現(xiàn)ClassCastException這樣的異常給丟出來。查 null是為了避免出現(xiàn)NullPointerException這樣的異常給丟出來。
如果你的對(duì)象里面容納的數(shù)據(jù)過多,那么這兩個(gè)函數(shù) equals()和hashCode()將會(huì)變得效率低。如果對(duì)象中擁有無法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ù)的值并沒有存儲(chǔ)下來,再重新由二進(jìn)制數(shù)據(jù)流轉(zhuǎn)換成對(duì)象后,兩者(對(duì)象在serialization 之前和之后)的狀態(tài)已經(jīng)不同了。這也是要注意的。
知道以上這些能夠幫助你:
- 進(jìn)行更好的設(shè)計(jì)和開發(fā)。
- 進(jìn)行更好的測(cè)試案例開發(fā)。
- 在面試過程中讓面試者對(duì)你的學(xué)識(shí)淵博感到滿意。
hashCode的作用
總的來說,Java中的集合(Collection)有兩類,一類是List,再有一類是 Set。你知道它們的區(qū)別嗎?前者集合內(nèi)的元素是有序的,元素可以重復(fù);后者元素?zé)o序,但元素不可重復(fù)。那么這里就有一個(gè)比較嚴(yán)重的問題了:要想保證元素不重復(fù),可兩個(gè)元素是否重復(fù)應(yīng)該依據(jù)什么來判斷呢?這就是Object.equals方法了。但是,如果每增加一個(gè)元素就檢查一次,那么當(dāng)元素很多時(shí),后添加到集合中的元素比較的次數(shù)就非常多了。也就是說,如果集合中現(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í)際可能并不是)。 這樣一來,當(dāng)集合要添加新的元素時(shí),先調(diào)用這個(gè)元素的hashCode方法,就一下子能定位到它應(yīng)該放置的物理位置上。如果這個(gè)位置上沒有元素,它就可以直接存儲(chǔ)在這個(gè)位置上,不用再進(jìn)行任何比較了;如果這個(gè)位置上已經(jīng)有元素了,就調(diào)用它的equals方法與新元素進(jìn)行比較,相同的話就不存了,不相同就散列其它的地址。所以這里存在一個(gè)沖突解決的問題。這樣一來實(shí)際調(diào)用equals方法的次數(shù)就大大降低了,幾乎只需要一兩次。所以,Java對(duì)于eqauls方法和hashCode方法是這樣規(guī)定的:1、如果兩個(gè)對(duì)象相同,那么它們的hashCode值一定要相同;2、如果兩個(gè)對(duì)象的hashCode相同,它們并不一定相同,上面說的對(duì)象相同指的是用eqauls方法比較。你當(dāng)然可以不按要求去做了,但你會(huì)發(fā)現(xiàn),相同的對(duì)象可以出現(xiàn)在Set集合中。同時(shí),增加新元素的效率會(huì)大大下降。
我們知道,equals()函數(shù)是用來做比較的。java中的比較有兩種:一種是內(nèi)存地址的比較,一種是內(nèi)容的比較。而比較個(gè)體也有兩種:一種是簡(jiǎn)單類型(這類簡(jiǎn)單說來無所謂內(nèi)存地址的比較或者內(nèi)容比較的區(qū)別);還有一種是對(duì)象的比較,本文中說的主要是后者
在java中,(對(duì)象)內(nèi)存地址的比較,是通過==完成的。比如
這樣的語句中,我們認(rèn)為,如果obj1和obj2的內(nèi)存地址相同,則返回true
而equals()通常是比較內(nèi)容的。這里說“通常” ,是因?yàn)樵谧罡镜腛bject類中,equal()函數(shù)做的是地址的比較。而在其他幾乎所有的類中,equals()都經(jīng)過重載,進(jìn)行內(nèi)容的比較。
而在說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è)問題,因?yàn)槲覀兺ǔK褂玫膋ey都是簡(jiǎn)單類型,或者是String, Long等一些特殊的對(duì)象(其特殊性請(qǐng)參看筆者在寫java 淺拷貝和深拷貝時(shí)的討論),這時(shí)候,這個(gè)問題被我們無意間繞過了
有人已經(jīng)概括了這種我們忽略了的情況:“如果你想將一個(gè)對(duì)象A放入另一個(gè)收集(集合)對(duì)象B里,或者使用這個(gè)對(duì)象A為查找一個(gè)元對(duì)象在收集對(duì)象B里位置的鑰匙(key),并支持是否容納(isContains()),刪除收集對(duì)象B里的元對(duì)象(remove()?)這樣的操作,那么,equals()和hashCode()函數(shù)必須開發(fā)者自己定義?!?(括號(hào)為筆者添加)