散列算法與散列碼(實(shí)例講解)
一、引入
/**
* Description:新建一個(gè)類(lèi)作為map的key
*/
public class Groundhog
{
protected int number;
public Groundhog(){
}
public Groundhog(int number)
{
this.number = number;
}
@Override
public String toString()
{
return "Groundhog{" + "number=" + number + '}';
}
}
/**
* Description:新建一個(gè)類(lèi)作為map的value
*/
public class Prediction
{
private boolean shadow=Math.random() > 0.5;
@Override
public String toString()
{
if (shadow) return "Six more weeks of Winter";
else return "Early Spring!";
}
}
/**
* Description:測(cè)試類(lèi)
*/
public class SpringDetector
{
public static void detectSpring(Class grondHotClass) throws Exception{
Constructor constructor = grondHotClass.getConstructor(new Class[]{int.class});
Map map=new HashMap();
for (int i=0;i<10;i++){
map.put(constructor.newInstance(new Object[]{new Integer(i)}),new Prediction());
}
System.out.println("map="+map);
Groundhog groundhog=(Groundhog)constructor.newInstance(new Object[]{new Integer(3)});
System.out.println(groundhog);
if (map.containsKey(groundhog)) {//查找這個(gè)key是否存在
System.out.println((Prediction)map.get(groundhog));
}else {
System.out.println("key not find:"+groundhog);
}
}
public static void main(String[] args)
{
try {
detectSpring(Groundhog.class);
} catch (Exception e) {
e.printStackTrace();
}
}
}

看這個(gè)結(jié)果,問(wèn)題就來(lái)了,map中明明存在Groudhog{number=3},為什么結(jié)果顯示的是Key not find呢??問(wèn)題出在哪里呢?原來(lái)是Groudhog類(lèi)沒(méi)有重寫(xiě)hashCode()方法,所以這里是使用Object的hashCode()方法生成散列碼,而他默認(rèn)是使用對(duì)象的地址計(jì)算散列碼。因此,由Groudhog(3)生成的第一個(gè)實(shí)例的散列碼與Groudhog(3)生成的散列碼是不同的,所以無(wú)法查找到 key。但是僅僅重寫(xiě)hashCode()還是不夠的,除非你重寫(xiě)equals()方法。原因在于不同的對(duì)象可能計(jì)算出同樣的hashCode的值,hashCode 的值并不是唯一的,當(dāng)hashCode的值一樣時(shí),就會(huì)使用equals()判斷當(dāng)前的“鍵”是否與表中的存在的鍵“相同”,即“
二、理解hashCode()
散列的價(jià)值在于速度:散列使得查詢得以快速執(zhí)行。由于速度的瓶頸是對(duì)“鍵”進(jìn)行查詢,而存儲(chǔ)一組元素最快的數(shù)據(jù)結(jié)構(gòu)是數(shù)組,所以用它來(lái)代表鍵的信息,注意:數(shù)組并不保存“鍵”的本身。而通過(guò)“鍵”對(duì)象生成一個(gè)數(shù)字,將其作為數(shù)組的下標(biāo)索引。這個(gè)數(shù)字就是散列碼,由定義在Object的hashCode()生成(或成為散列函數(shù))。同時(shí),為了解決數(shù)組容量被固定的問(wèn)題,不同的“鍵”可以產(chǎn)生相同的下標(biāo)。那對(duì)于數(shù)組來(lái)說(shuō)?怎么在同一個(gè)下標(biāo)索引保存多個(gè)值呢??原來(lái)數(shù)組并不直接保存“值”,而是保存“值”的 List。然后對(duì) List中的“值”使用equals()方法進(jìn)行線性的查詢。這部分的查詢自然會(huì)比較慢,但是如果有好的散列函數(shù),每個(gè)下標(biāo)索引只保存少量的值,只對(duì)很少的元素進(jìn)行比較,就會(huì)快的多。
不知道大家有沒(méi)有理解我上面在說(shuō)什么。不過(guò)沒(méi)關(guān)系,下面會(huì)有一個(gè)例子幫助大家理解。不過(guò)我之前一直被一個(gè)問(wèn)題糾結(jié):為什么一個(gè)hashCode的下標(biāo)存的會(huì)有多個(gè)值?因?yàn)閔ashMap里面只能有唯一的key啊,所以只能有唯一的value在那個(gè)下標(biāo)才對(duì)啊。這里就要提出一個(gè)新的概念哈希沖突的問(wèn)題,借用網(wǎng)上的一個(gè)例子:
比如:數(shù)組的長(zhǎng)度是5。這時(shí)有一個(gè)數(shù)據(jù)是6。那么如何把這個(gè)6存放到長(zhǎng)度只有5的數(shù)組中呢。按照取模法,計(jì)算6%5,結(jié)果是1,那么就把6放到數(shù)組下標(biāo)是1的位置。那么,7就應(yīng)該放到2這個(gè)位置。到此位置,哈希沖突還沒(méi)有出現(xiàn)。這時(shí),有個(gè)數(shù)據(jù)是11,按照取模法,11%5=1,也等于1。那么原來(lái)數(shù)組下標(biāo)是1的地方已經(jīng)有數(shù)了,是6。這時(shí)又計(jì)算出1這個(gè)位置,那么數(shù)組1這個(gè)位置,就必須儲(chǔ)存兩個(gè)數(shù)了。這時(shí),就叫哈希沖突。沖突之后就要按照順序來(lái)存放了。所以這里Java中用的解決方法就是在這個(gè)hashCode上存一個(gè)List,當(dāng)遇到相同的hashCode時(shí),就往這個(gè)List里add元素就可以了。這才是hash原理的精髓所在??!哈哈、糾結(jié)我一天。
三、HashMap的性能因子
容量(Capacity):散列表中的數(shù)量。
初始化容量(Initial capacity):創(chuàng)建散列表時(shí)桶的數(shù)量。HashMap 和 HashSet都允許你在構(gòu)造器中制定初始化容量。
尺寸(Size):當(dāng)前散列表中記錄的數(shù)量。
負(fù)載因子(Load factor):等于"size/capacity"。負(fù)載因子為0,表示空的散列表,0.5表示半滿的散列表,依次類(lèi)推。輕負(fù)載的散列表具有沖突少、適宜插入與適宜查詢的特點(diǎn)(但是使用迭代器遍歷會(huì)變慢)。HashMap和hashSet的構(gòu)造器允許你制定負(fù)載因子。這意味著,當(dāng)負(fù)載達(dá)到制定值時(shí),容器會(huì)自動(dòng)成倍的增加容量,并將原有的對(duì)象重新分配,存入新的容器內(nèi)(這稱為“重散列”rehashing)。HashMap默認(rèn)的負(fù)載因子為0.75,這很好的權(quán)衡了時(shí)間和空間的成本。
備注:為使散列分布均衡,Java的散列函數(shù)都使用2的整數(shù)次方來(lái)作為散列表的理想容量。對(duì)現(xiàn)代的處理器來(lái)說(shuō),除法和求余是最慢的動(dòng)作。使用2的整數(shù)次方的散列表,可用掩碼代替除法。因?yàn)間et()是使用最多的操作,求余數(shù)的%操作是其開(kāi)銷(xiāo)的大部分,而使用2的整數(shù)次方可以消除此開(kāi)銷(xiāo)(也可能對(duì)hashCode()有些影響)
四、怎么重寫(xiě)hashCode()
現(xiàn)在的IDE工具中,一般都能自動(dòng)的幫我們重寫(xiě)了hashCode()和equals()方法,但那或許并不是最優(yōu)的,重寫(xiě)hashCode()有兩個(gè)原則:
必須速度快,并且必須有意義。也就是說(shuō),它必須基于對(duì)象的內(nèi)容生成散列碼。
應(yīng)該產(chǎn)生分布均勻的散列碼。如果散列碼都集中在一塊,那么在某些區(qū)域的負(fù)載就會(huì)變得很重。
下面是怎么寫(xiě)出一份像樣的hashCode()的基本指導(dǎo):
1、給int變量result 賦予某個(gè)非零值常量,例如 17。
2、為每個(gè)對(duì)象內(nèi)每個(gè)有意義的屬性f (即每個(gè)可以做equals()的屬性)計(jì)算出一個(gè) int 散列碼c:

3、合并計(jì)算得到的散列值:result=37*result+c;
4、返回 result;
5、檢查hashCode()最后生成的結(jié)果,確保相同的對(duì)象有相同的散列碼。
五、自定義HashMap
下面我們將自己寫(xiě)一個(gè)hashMap,便于了解底層的原理,大家如果看的懂下面的代碼,也就很好的理解了hashCode的原理了。
/**
* Description:首先新建一個(gè)類(lèi)作為map中存儲(chǔ)的對(duì)象并重寫(xiě)了hashCode()和equals()方法
*/
public class MPair implements Map.Entry,Comparable
{
private Object key,value;
public MPair(Object key,Object value)
{
this.key = key;
this.value=value;
}
@Override
public int compareTo(Object o)
{
return ((Comparable)key).compareTo(((MPair)o).key);
}
@Override
public Object getKey()
{
return key;
}
@Override
public Object getValue()
{
return value;
}
@Override
public int hashCode()
{
int result = key != null ? key.hashCode() : 0;
result = 31 * result + (value != null ? value.hashCode() : 0);
return result;
}
@Override
public boolean equals(Object o)
{
return key.equals(((MPair)o).key);
}
@Override
public Object setValue(Object v)
{
Object result=value;
this.value=v;
return result;
}
@Override
public String toString()
{
return "MPair{" + "key=" + key + ", value=" + value + '}';
}
public class SimpleHashMap extends AbstractMap
{
private static final int SZ=3;//定一個(gè)初始大小的哈希表容量
private LinkedList[] linkedLists=new LinkedList[SZ];//建一個(gè)hash數(shù)組,用linkedList實(shí)現(xiàn)
public Object put(Object key,Object value){
Object result=null;
int index=key.hashCode() % SZ;//對(duì)key的值做求模法求出index
if (index<0) index=-index;
if (linkedLists[index]==null) linkedLists[index]=new LinkedList();//如果這個(gè)index位置沒(méi)有對(duì)象,就新建一個(gè)
LinkedList linkedList = linkedLists[index];//取出這個(gè)index的對(duì)象linkedList
MPair mPair = new MPair(key,value);//新建要存儲(chǔ)的對(duì)象mPair
ListIterator listIterator = linkedList.listIterator();
boolean found =false;
while (listIterator.hasNext()){//遍歷這個(gè)index位置的List,如果查找到跟之前一樣的對(duì)象(根據(jù)equals來(lái)比較),則更新那個(gè)key對(duì)應(yīng)的value
Object next = listIterator.next();
if (next.equals(mPair)){
result = ((MPair) next).getValue();
listIterator.set(mPair);//更新動(dòng)作
found=true;
break;
}
}
if (!found) linkedLists[index].add(mPair);//如果沒(méi)有找到這個(gè)對(duì)象,則在這index的List對(duì)象上新增一個(gè)元素。
return result;
}
public Object get(Object key){
int index = key.hashCode() % SZ;
if (index<0) index=-index;
if (linkedLists[index]==null) return null;
LinkedList linkedList = linkedLists[index];
MPair mPair=new MPair(key,null);//新建一個(gè)空的對(duì)象值,因?yàn)閑quals()的比較是看他們的key是否相等,而在List中的遍歷對(duì)象的時(shí)候,是通過(guò)key來(lái)查找對(duì)象的。
ListIterator listIterator = linkedList.listIterator();
while (listIterator.hasNext()){
Object next = listIterator.next();
if (next.equals(mPair)) return ((MPair)next).getValue();//找到了這個(gè)key就返回這個(gè)value
}
return null;
}
@Override
public Set<Entry> entrySet()
{
Set set=new HashSet();
for (int i=0;i<linkedLists.length;i++){
if (linkedLists[i]==null) continue;
Iterator iterator = linkedLists[i].iterator();
while (iterator.hasNext()){
set.add(iterator.next());
}
}
return set;
}
public static void main(String[] args)
{
SimpleHashMap simpleHashMap=new SimpleHashMap();
simpleHashMap.put("1", "1");
simpleHashMap.put("2", "2");
simpleHashMap.put("3","3");
simpleHashMap.put("4","4");//這里有四個(gè)元素,其中key是1和key是4的index是一樣的,所以index為1的List上面存了兩個(gè)元素。
System.out.println(simpleHashMap);
Object o = simpleHashMap.get("1");
System.out.println(o);
Object o1 = simpleHashMap.get("4");
System.out.println(o1);
}
}
六、結(jié)語(yǔ)
不知道大家理解了沒(méi)?整了我一天,終于還算大概理解了其中的原理了。文筆比較粗糙,大家湊活看吧,畢竟,不會(huì)做飯的作家不是好程序員??!哈哈...... 或者,可能我有很多理解的不到位的地方,還請(qǐng)大家不吝指教!
相關(guān)文章
SpringBoot無(wú)法訪問(wèn)webapp目錄下的文件問(wèn)題
這篇文章主要介紹了SpringBoot無(wú)法訪問(wèn)webapp目錄下的文件問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2024-05-05
struts2中實(shí)現(xiàn)多個(gè)文件同時(shí)上傳代碼
struts2中實(shí)現(xiàn)多個(gè)文件同時(shí)上傳代碼,需要的朋友可以參考一下2013-04-04
idea啟動(dòng)多個(gè)服務(wù)不顯示Services或者RunDashboard窗口的處理方法
這篇文章主要介紹了idea啟動(dòng)多個(gè)服務(wù)不顯示Services或者RunDashboard窗口,本文通過(guò)圖文并茂的形式給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2021-03-03
Spring + Mybatis 項(xiàng)目實(shí)現(xiàn)動(dòng)態(tài)切換數(shù)據(jù)源實(shí)例詳解
這篇文章主要介紹了Spring + Mybatis 項(xiàng)目實(shí)現(xiàn)動(dòng)態(tài)切換數(shù)據(jù)源的相關(guān)資料,需要的朋友參考下吧2017-04-04
java實(shí)現(xiàn)基于UDP協(xié)議網(wǎng)絡(luò)Socket編程(C/S通信)
這篇文章主要介紹了java實(shí)現(xiàn)基于UDP協(xié)議網(wǎng)絡(luò)Socket編程(C/S通信),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-10-10
IDEA中如何查找jar包之間的依賴關(guān)系并忽略依賴的某個(gè)包
這篇文章主要介紹了IDEA中如何查找jar包之間的依賴關(guān)系并忽略依賴的某個(gè)包?本文通過(guò)圖文并茂的形式給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-08-08
利用Java代碼實(shí)現(xiàn)區(qū)塊鏈技術(shù)
這篇文章主要介紹了利用Java代碼實(shí)現(xiàn)區(qū)塊鏈技術(shù),區(qū)塊鏈的應(yīng)用范圍幾乎無(wú)窮無(wú)盡,關(guān)于區(qū)塊鏈?zhǔn)侨绾芜\(yùn)作的,下文來(lái)看看具體的內(nèi)容介紹吧,需要的朋友可以參考一下2022-04-04
Java?swing創(chuàng)建一個(gè)窗口的簡(jiǎn)單步驟
這篇文章主要給大家介紹了關(guān)于Java?swing創(chuàng)建一個(gè)窗口的簡(jiǎn)單步驟,Java Swing是Java平臺(tái)下的GUI(Graphical User Interface,圖形用戶界面)工具包,提供了豐富的GUI組件,可以實(shí)現(xiàn)復(fù)雜的圖形界面應(yīng)用程序,需要的朋友可以參考下2024-06-06
spring?參數(shù)校驗(yàn)Validation示例詳解
Spring提供了Validation工具類(lèi)來(lái)實(shí)現(xiàn)對(duì)客戶端傳來(lái)的請(qǐng)求參數(shù)的有效校驗(yàn),本文給大家介紹spring?參數(shù)校驗(yàn)Validation示例詳解,感興趣的朋友一起看看吧2024-12-12

