java集合框架之HashCode
參考http://how2j.cn/k/collection/collection-hashcode/371.html
List查找的低效率
假設在List中存放著無重復名稱,沒有順序的2000000個Hero
要把名字叫做“hero 1000000”的對象找出來
List的做法是對每一個進行挨個遍歷,直到找到名字叫做“hero 1000000”的英雄。
最差的情況下,需要遍歷和比較2000000次,才能找到對應的英雄。
測試邏輯:
1. 初始化2000000個對象到ArrayList中
2. 打亂容器中的數據順序
3. 進行10次查詢,統計每一次消耗的時間
不同計算機的配置情況下,所花的時間是有區別的。 在本機上,花掉的時間大概是600毫秒左右
package collection; import java.util.ArrayList; import java.util.Collections; import java.util.List; import charactor.Hero;public class TestCollection { public static void main(String[] args) { List<Hero> heros = new ArrayList<Hero>(); for (int j = 0; j < 2000000; j++) { Hero h = new Hero("Hero " + j); heros.add(h); } // 進行10次查找,觀察大體的平均值for (int i = 0; i < 10; i++) { // 打亂heros中元素的順序 Collections.shuffle(heros); long start = System.currentTimeMillis(); String target = "Hero 1000000"; for (Hero hero : heros) { if (hero.name.equals(target)) { System.out.println("找到了 hero!" ); break; } } long end = System.currentTimeMillis(); long elapsed = end - start; System.out.println("一共花了:" + elapsed + " 毫秒"); } } }
HashMap的性能表現
使用HashMap 做同樣的查找
1. 初始化2000000個對象到HashMap中。
2. 進行10次查詢
3. 統計每一次的查詢消耗的時間
可以觀察到,幾乎不花時間,花費的時間在1毫秒以內
package collection; import java.util.HashMap; import charactor.Hero; public class TestCollection { public static void main(String[] args) { HashMap<String,Hero> heroMap = new HashMap<String,Hero>(); for (int j = 0; j < 2000000; j++) { Hero h = new Hero("Hero " + j); heroMap.put(h.name, h); } System.out.println("數據準備完成"); for (int i = 0; i < 10; i++) { long start = System.currentTimeMillis(); //查找名字是Hero 1000000的對象 Hero target = heroMap.get("Hero 1000000"); System.out.println("找到了 hero!" + target.name); long end = System.currentTimeMillis(); long elapsed = end - start; System.out.println("一共花了:" + elapsed + " 毫秒"); } } }
HashMap原理與字典
在展開HashMap原理的講解之前,首先回憶一下大家初中和高中使用的漢英字典。
比如要找一個單詞對應的中文意思,假設單詞是Lengendary,首先在目錄找到Lengendary在第 555頁。
然後,翻到第555頁,這頁不只一個單詞,但是量已經很少了,逐一比較,很快就定位目標單詞Lengendary。
555相當於就是Lengendary對應的hashcode
分析HashMap性能卓越的原因
分析HashMap性能卓越的原因
-----hashcode概念-----
所有的對象,都有一個對應的hashcode(散列值)
比如字符串“gareen”對應的是1001 (實際上不是,這裏是方便理解,假設的值)
比如字符串“temoo”對應的是1004
比如字符串“db”對應的是1008
比如字符串“annie”對應的也是1008
-----保存數據-----
準備一個數組,其長度是2000,並且設定特殊的hashcode算法,使得所有字符串對應的hashcode,都會落在0-1999之間
要存放名字是"gareen"的英雄,就把該英雄和名稱組成一個鍵值對,存放在數組的1001這個位置上
要存放名字是"temoo"的英雄,就把該英雄存放在數組的1004這個位置上
要存放名字是"db"的英雄,就把該英雄存放在數組的1008這個位置上
要存放名字是"annie"的英雄,然而 "annie"的hashcode 1008對應的位置已經有db英雄了,那麽就在這裏創建一個鏈表,接在db英雄後面存放annie
-----查找數據-----
比如要查找gareen,首先計算"gareen"的hashcode是1001,根據1001這個下標,到數組中進行定位,(根據數組下標進行定位,是非常快速的) 發現1001這個位置就只有一個英雄,那麽該英雄就是gareen.
比如要查找annie,首先計算"annie"的hashcode是1008,根據1008這個下標,到數組中進行定位,發現1008這個位置有兩個英雄,那麽就對兩個英雄的名字進行逐一比較(equals),因為此時需要比較的量就已經少很多了,很快也就可以找出目標英雄
這就是使用hashmap進行查詢,非常快原理。
這是一種用空間換時間的思維方式
HashSet判斷是否重復
HashSet的數據是不能重復的,相同數據不能保存在一起,到底如何判斷是否是重復的呢?
根據HashSet和HashMap的關系,我們了解到因為HashSet沒有自身的實現,而是裏面封裝了一個HashMap,所以本質上就是判斷HashMap的key是否重復。
再通過上一步的學習,key是否重復,是由兩個步驟判斷的:
hashcode是否一樣
如果hashcode不一樣,就是在不同的坑裏,一定是不重復的
如果hashcode一樣,就是在同一個坑裏,還需要進行equals比較
如果equals一樣,則是重復數據
如果equals不一樣,則是不同數據。
自定義字符串的hashcode
如下是Java API提供的String的hashcode生成辦法;
s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1] s[0] 表示第一位字符n表示字符串的長度
本練習並不是要求去理解這個算法,而是自定義一個簡單的hashcode算法,計算任意字符串的hashcode
因為String類不能被重寫,所以我們通過一個靜態方法來返回一個String的hashcode
如果字符串長度是0,則返回0。
否則: 獲取每一位字符,轉換成數字後,相加,最後乘以23
如果值超過了1999,則取2000的余數,保證落在0-1999之間。
如果是負數,則取絕對值。
隨機生成長度是2-10的不等的100個字符串,打印用本hashcode獲取的值分別是多少
HashCode代碼
package Test.testtest; /** * @Auther: 李景然 * @Date: 2018/5/24 22:48 * @Description: */ public class HashCode { public static void main(String[] args) { int i=0; while (i<100){ double strLength=Math.ceil(Math.random()*8+2); StringBuilder sb=new StringBuilder(); char c; int start=(int)‘0‘; int end=(int)‘z‘+1; for (int j=0;j<strLength;j++){ c=(char) (Math.random()*(end-start)+start); if(Character.isLetterOrDigit(c)){ sb.append(c); }else{ j--; } } System.out.println(sb.toString()+"---"+hashcode(sb.toString())); i++; } } public static int hashcode(String str){ int result=0; if(str==null||str.length()==0){ return 0; } char[] chars=str.toCharArray(); for (char c :chars){ result+=(int)c; } result*=23; if(result>1999){ result%=2000; } return result; } }
部分結果:
自定義MyHashMap
根據前面學習的hashcode的原理和自定義hashcode, 設計一個MyHashMap,實現接口IHashMap
MyHashMap內部由一個長度是2000的對象數組實現。
設計put(String key,Object value)方法
首先通過上一個自定義字符串的hashcode練習獲取到該字符串的hashcode,然後把這個hashcode作為下標,定位到數組的指定位置。
如果該位置沒有數據,則把字符串和對象組合成鍵值對Entry,再創建一個LinkedList,把鍵值對,放進LinkedList中,最後把LinkedList 保存在這個位置。
如果該位置有數據,一定是一個LinkedList,則把字符串和對象組合成鍵值對Entry,插入到LinkedList後面。
設計 Object get(String key) 方法
首先通過上一個自定義字符串的hashcode練習獲取到該字符串的hashcode,然後把這個hashcode作為下標,定位到數組的指定位置。
如果這個位置沒有數據,則返回空
如果這個位置有數據,則挨個比較其中鍵值對的鍵-字符串,是否equals,找到匹配的,把鍵值對的值,返回出去。找不到匹配的,就返回空
IhashMap代碼
package Test.testtest; /** * @Auther: 李景然 * @Date: 2018/5/24 22:46 * @Description: */ public interface IHashMap { public void put(String key,Object object); public Object get(String key); }
Entry代碼
package Test.testtest; /** * @Auther: 李景然 * @Date: 2018/5/24 22:46 * @Description: */ public class Entry { public Entry(Object key, Object value) { super(); this.key = key; this.value = value; } public Object key; public Object value; @Override public String toString() { return "[key=" + key + ", value=" + value + "]"; } }
MyHashMap代碼
package Test.testtest; import java.util.ArrayList; import java.util.LinkedList; import java.util.List; /** * @Auther: 李景然 * @Date: 2018/5/24 22:47 * @Description: */ public class MyHashMap implements IHashMap { private Object[] objects=new Object[2000]; public MyHashMap(){ } @Override public void put(String key, Object object) { int hashCode=HashCode.hashcode(key); if(objects[hashCode]==null){ //key值相同的時候,把value存放到鏈表中 LinkedList<Entry> list=new LinkedList<>(); list.add(new Entry(key,object)); objects[hashCode]=list; }else{ LinkedList<Entry> list=(LinkedList<Entry>)objects[hashCode]; list.add(new Entry(key,object)); objects[hashCode]=list; } } @Override public Object get(String key) { int hashCode=HashCode.hashcode(key); Object object=objects[hashCode]; if(object!=null){ LinkedList<Entry> list=(LinkedList<Entry>)object; for (Entry e:list){ if(e.key.equals(key)){ return e.value; } } } return null; } }
內容查找性能比較
重復前面的 練習-查找內容性能比較 ,不過不使用HashMap,而是使用上個練習中自定義的MyHashMap.
準備一個ArrayList其中存放100000(十萬個)Hero對象,其名稱是隨機的,格式是hero-[4位隨機數]
hero-3229
hero-6232
hero-9365
...
因為總數很大,所以幾乎每種都有重復,把名字叫做 hero-5555的所有對象找出來
要求使用兩種辦法來尋找
1. 不使用MyHashMap,直接使用for循環找出來,並統計花費的時間
2. 借助MyHashMap,找出結果,並統計花費的時間
MyHashMapTest代碼
package Test.testtest; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; /** * @Auther: 李景然 * @Date: 2018/5/24 23:04 * @Description: */ public class MyHashMapTest { public static void main(String[] args) { //初始化100000個英雄對象 ArrayList<Hero> arrayList=new ArrayList<>(); for (int i=0;i<100000;i++){ String str="hero-"+String.valueOf(Math.random()*9000+1000).substring(0,4); arrayList.add(new Hero(str)); } //初始化HashMap HashMap<String,ArrayList<Hero>> hashMap=new HashMap<>(); for (Hero h:arrayList){ ArrayList<Hero> list=hashMap.get(h.getName()); if(list==null){ list=new ArrayList<>(); hashMap.put(h.getName(),list); } list.add(h); } //初始化MyHashMap MyHashMap myHashMap=new MyHashMap(); for (Hero h:arrayList){ ArrayList<Hero> list=(ArrayList<Hero>)(myHashMap.get(h.getName())); if(list==null){ list=new ArrayList<>(); myHashMap.put(h.getName(),list); } list.add(h); } searchOfHashMapTest(hashMap,"hero-5555"); searchOfMyHashMapTest(myHashMap,"hero-5555"); } //使用hashMap循環來找hero-5555 private static void searchOfHashMapTest(HashMap<String,ArrayList<Hero>> hashMap,String key){ double startTime=System.currentTimeMillis(); ArrayList<Hero> list=hashMap.get(key); double endTime=System.currentTimeMillis(); if(list!=null){ System.out.println("使用hashMap循環來找hero-5555:找到"+list.size()+"個;用時:"+(endTime-startTime)+"毫秒"); }else{ System.out.println("使用hashMap循環來找hero-5555:找到"+0+"個;用時:"+(endTime-startTime)+"毫秒"); } } //使用myHashMap循環來找hero-5555 private static void searchOfMyHashMapTest(MyHashMap myHashMap,String key){ double startTime=System.currentTimeMillis(); int count=0; ArrayList<Hero> list=(ArrayList<Hero>) myHashMap.get(key); double endTime=System.currentTimeMillis(); if(list!=null){ System.out.println("使用myHashMap循環來找hero-5555:找到"+list.size()+"個;用時:"+(endTime-startTime)+"毫秒"); }else{ System.out.println("使用myHashMap循環來找hero-5555:找到"+0+"個;用時:"+(endTime-startTime)+"毫秒"); } }
運行結果:
雖然用HashMap或者MyHashMap查詢速度快,幾乎不花費時間。但是要用HashMap或者MyHashMap,我們首先要把ArrayList中的數據轉移到HashMap或者MyHashMap中,這個也需要消耗時間,降低性能(從這個中可以看出 練習-查找內容性能比較)
java集合框架之HashCode