hashCode()方法以及集合中Set的一些總結
一、前言
本篇文章沒有什麼主題,就是一些零散點的總結。
週末沒事看了幾道螞蟻金服的面試題,其中有好幾道都是特別簡單的,基礎性的題目,就是我們平時用到的,但是發現要是完全說出來還是有一些不清楚的地方,所以小小的總結一下。
二、hashCode()方法理解
提到hashCode()必然會涉及equals()方法,二者是緊密相連的,其實面試中被問到這方面往往是考察集合儲存物件判斷相等的問題。
比如有如下Person類:
1public class Person { 2 3private int age; 4private String name; 5 6public Person(int age, String name) { 7this.age = age; 8this.name = name; 9} 10 11public int getAge() { 12return age; 13} 14 15public void setAge(int age) { 16this.age = age; 17} 18 19public String getName() { 20return name; 21} 22 23public void setName(String name) { 24this.name = name; 25} 26 27@Override 28public boolean equals(Object o) { 29if (this == o) return true; 30if (o == null || getClass() != o.getClass()) return false; 31Person person = (Person) o; 32return age == person.age && 33Objects.equals(name, person.name); 34} 35}
很簡單吧,我這裡只重寫了equals方法,如果我們以Person類物件作為key儲存在HashMap中,如下:
1 HashMap map = new HashMap(); 2 map.put(new Person(45,"lisi"),"123"); 3 System.out.println(map.get(new Person(45,"lisi")));
試想一下能正常取出"lisi"值嗎?對HashMap原始碼看過的同學肯定知道取不出來,列印如下:
1 null
HashMap在取資料的時候會檢查對應的key是否已經儲存過,這個比較簡單來說就是比較key的hashcode()值以及equals()是否相等的比較,只有二者均相同才會認為已經儲存過,對於上述Person類我們只重寫了equals方法,對於hashcode()方法預設呼叫的是Object類中的hashcode()方法:
1public int hashCode() { 2return identityHashCode(this); 3}
不同物件會生成不同的hash值,所以嚴格來說hashcode()與equals()方法我們最好同時重寫,否則與集合類結合使用的時候會產生問題 ,改造Person類新增如下hashcode()方法:
1@Override 2public int hashCode() { 3return Objects.hash(age, name);//根據類中屬性生成對應hash值 4}
這樣就可以正常獲取對應值了。
HashMap中比較元素是否相同是根據Key的hashcode值以及equals來判斷是否相同的。
三、Set集合常用類相關問題
Set集合常用與儲存不重複的資料,也就是集合中資料都不相等,但是不同具體實現類判斷是否相等是不一樣,這也是面試中會問到的問題,比如TreeSet是怎麼判斷元素是否相同的?HashSet是怎麼判斷的?
其實稍微看一下原始碼就明白了,Set具體實現類都是依靠對應map來實現的:
- HashSet底層依靠HashMap來實現
- TreeSet底層依靠TreeMap來實現
- LinkedHashSet底層依靠LinkedHashMap來實現
HashSet
看一下HashSet原始碼吧:
1public class HashSet<E> 2extends AbstractSet<E> 3implements Set<E>, Cloneable, java.io.Serializable 4{ 5static final long serialVersionUID = -5024744406713321676L; 6 7private transient HashMap<E,Object> map; 8 9// Dummy value to associate with an Object in the backing Map 10private static final Object PRESENT = new Object(); 11 12public HashSet() { 13map = new HashMap<>(); 14} 15 16public HashSet(Collection<? extends E> c) { 17map = new HashMap<>(Math.max((int) (c.size()/.75f) + 1, 16)); 18addAll(c); 19} 20 21public HashSet(int initialCapacity, float loadFactor) { 22map = new HashMap<>(initialCapacity, loadFactor); 23} 24 25public HashSet(int initialCapacity) { 26map = new HashMap<>(initialCapacity); 27} 28 29HashSet(int initialCapacity, float loadFactor, boolean dummy) { 30map = new LinkedHashMap<>(initialCapacity, loadFactor); 31} 32 33public Iterator<E> iterator() { 34return map.keySet().iterator(); 35} 36 37public int size() { 38return map.size(); 39} 40 41public boolean isEmpty() { 42return map.isEmpty(); 43} 44 45public boolean contains(Object o) { 46return map.containsKey(o); 47} 48 49public boolean add(E e) { 50return map.put(e, PRESENT)==null; 51} 52 53public boolean remove(Object o) { 54return map.remove(o)==PRESENT; 55} 56 57public void clear() { 58map.clear(); 59}
這裡只列出了部分方法,不過已經足夠了,幾個構造方法也就是初始化HashMap,其餘的方法也都是呼叫HashMap對應的方法,所以你要是理解了HashMap那HashSet幾秒鐘就全都懂了,不理解HashMap請轉到:
Android版資料結構與演算法(四):基於雜湊表實現HashMap核心原始碼徹底分析
我們在呼叫add(E e)方法的時候,key就是e,而value永遠是PRESENT,也就是Object()物件了。
這裡注意一下:
1HashSet(int initialCapacity, float loadFactor, boolean dummy) { 2map = new LinkedHashMap<>(initialCapacity, loadFactor); 3}
這個構造方法是給LinkedHashSet呼叫的,我們無法使用,沒有public修飾。
所以要是問你HashSet如何判斷元素重複的,也就是和HashMap一樣通過hashcode()與equals()方法來判斷。
LinkedHashSet
接下來看下LinkedHashSet原始碼:
1public class LinkedHashSet<E> 2extends HashSet<E> 3implements Set<E>, Cloneable, java.io.Serializable { 4 5private static final long serialVersionUID = -2851667679971038690L; 6 7public LinkedHashSet(int initialCapacity, float loadFactor) { 8super(initialCapacity, loadFactor, true); 9} 10 11public LinkedHashSet(int initialCapacity) { 12super(initialCapacity, .75f, true); 13} 14 15public LinkedHashSet() { 16super(16, .75f, true); 17} 18 19public LinkedHashSet(Collection<? extends E> c) { 20super(Math.max(2*c.size(), 11), .75f, true); 21addAll(c); 22} 23 24@Override 25public Spliterator<E> spliterator() { 26return Spliterators.spliterator(this, Spliterator.DISTINCT | Spliterator.ORDERED); 27} 28}
就是那麼簡短,LinkedHashSet繼承HashSet,初始化呼叫的就是HashSet中三個引數的建構函式,上面已經提到,也就map初始為LinkedHashMap,如果你對LinkedHashMap完全理解,那麼這裡就十分簡單了,如果不理解LinkedHashMap,請轉到:
Android版資料結構與演算法(五):LinkedHashMap核心原始碼徹底分析
總結一句話:LinkedHashSet是資料無重複並基於存入資料順序排序的集合,資料重複判斷的依據依然是hashcode()與equals()方法來判斷。
TreeMap
同樣看一下TreeMap中部分原始碼,構造部分:
1private transient NavigableMap<E,Object> m; 2 3private static final Object PRESENT = new Object(); 4 5TreeSet(NavigableMap<E,Object> m) { 6this.m = m; 7} 8 9public TreeSet() { 10this(new TreeMap<E,Object>()); 11} 12 13public TreeSet(Comparator<? super E> comparator) { 14this(new TreeMap<>(comparator)); 15}
看到了吧,構造時我們可以自己指定一個NavigableMap,如不指定則預設為TreeMap,所以TreeSet底層實現為TreeMap,加入資料的時候value同樣永遠是Object:
1public boolean add(E e) { 2return m.put(e, PRESENT)==null;//private static final Object PRESENT = new Object(); 3}
TreeMap如不熟悉請轉到:
TreeMap是怎麼比較資料是否相等的呢?怎麼排序的呢?這裡我們就要檢視一下TreeMap中的put方法原始碼了:
1public V put(K key, V value) { 2TreeMapEntry<K,V> t = root; 3if (t == null) { 4// BEGIN Android-changed: Work around buggy comparators. http://b/34084348 5// We could just call compare(key, key) for its side effect of checking the type and 6// nullness of the input key. However, several applications seem to have written comparators 7// that only expect to be called on elements that aren't equal to each other (after 8// making assumptions about the domain of the map). Clearly, such comparators are bogus 9// because get() would never work, but TreeSets are frequently used for sorting a set 10// of distinct elements. 11// 12// As a temporary work around, we perform the null & instanceof checks by hand so that 13// we can guarantee that elements are never compared against themselves. 14// 15// **** THIS CHANGE WILL BE REVERTED IN A FUTURE ANDROID RELEASE **** 16// 17// Upstream code was: 18// compare(key, key); // type (and possibly null) check 19if (comparator != null) { 20if (key == null) { 21comparator.compare(key, key); 22} 23} else { 24if (key == null) { 25throw new NullPointerException("key == null"); 26} else if (!(key instanceof Comparable)) { 27throw new ClassCastException( 28"Cannot cast" + key.getClass().getName() + " to Comparable."); 29} 30} 31// END Android-changed: Work around buggy comparators. http://b/34084348 32root = new TreeMapEntry<>(key, value, null); 33size = 1; 34modCount++; 35return null; 36} 37int cmp; 38TreeMapEntry<K,V> parent; 39// split comparator and comparable paths 40Comparator<? super K> cpr = comparator; 41if (cpr != null) { 42do { 43parent = t; 44cmp = cpr.compare(key, t.key); 45if (cmp < 0) 46t = t.left; 47else if (cmp > 0) 48t = t.right; 49else 50return t.setValue(value); 51} while (t != null); 52} 53else { 54if (key == null) 55throw new NullPointerException(); 56@SuppressWarnings("unchecked") 57Comparable<? super K> k = (Comparable<? super K>) key; 58do { 59parent = t; 60cmp = k.compareTo(t.key); 61if (cmp < 0) 62t = t.left; 63else if (cmp > 0) 64t = t.right; 65else 66return t.setValue(value); 67} while (t != null); 68} 69TreeMapEntry<K,V> e = new TreeMapEntry<>(key, value, parent); 70if (cmp < 0) 71parent.left = e; 72else 73parent.right = e; 74fixAfterInsertion(e); 75size++; 76modCount++; 77return null; 78}
通過檢視put方法邏輯,初始化TreeMap可以自己指定比較器comparator,如果我們指定了comparator那麼資料的比較優先使用指定的comparator,是否存入null也由我們自己的比較器comparator決定,如果沒指定那麼存入的元素必須實現Comparable介面,否則丟擲異常。
回到TreeSet,也就是TreeSet比較元素是否相等時如果我們指定了comparator那麼就根據其compare方法返回值來比較,0代表相等,如果沒指定那麼就需要資料自己實現Comparable介面,是否相等根據compareTo返回值決定,0代表相等。
TreeSet也能保證資料的有序性,與LinkedHashSet基於插入順序排序不同,TreeSet排序是根據元素比較來排序的。
螞蟻金服有道面試題是:TreeSet存入資料有什麼要求?看完上面你知道怎麼回答了嗎?很簡單,如果我們沒指定TreeSet集合的比較器那麼插入的資料需要實現Comparable介面用來比較元素是否相等以及排序用。
好了,以上就是Set集合的一些總結。
四、HashMap執行緒不安全的體現
fail-fast機制
我們知道大部分集合類中在用迭代器迭代過程中要刪除集合中元素最好用迭代器的刪除方法,否則會發生併發異常,如下:
1 HashMap map = new HashMap(); 2map.put(1,"1"); 3map.put(2,"2"); 4map.put(3,"3"); 5map.put(4,"4"); 6 7Set entrySet = map.entrySet(); 8Iterator<Map.Entry> iterator = entrySet.iterator(); 9while (iterator.hasNext()){ 10Map.Entry entry = iterator.next(); 11if (entry.getKey().equals(2)){ 12map.remove(entry.getKey());//呼叫集合類本身的刪除方法 13} 14System.out.println(entry.getKey()+"--->"+entry.getValue()); 15}
執行程式如下:
11--->1 22--->2 3Exception in thread "main" java.util.ConcurrentModificationException 4at java.util.HashMap$HashIterator.nextNode(HashMap.java:1437) 5at java.util.HashMap$EntryIterator.next(HashMap.java:1471) 6at java.util.HashMap$EntryIterator.next(HashMap.java:1469) 7at com.wanglei55.mjavalib.myClass.main(myClass.java:173)
至於產生問題原因這裡我就不細說了,很基礎的。
改為如下用迭代器刪除就可以了:
1HashMap map = new HashMap(); 2map.put(1,"1"); 3map.put(2,"2"); 4map.put(3,"3"); 5map.put(4,"4"); 6 7Set entrySet = map.entrySet(); 8Iterator<Map.Entry> iterator = entrySet.iterator(); 9while (iterator.hasNext()){ 10Map.Entry entry = iterator.next(); 11if (entry.getKey().equals(2)){ 12iterator.remove();//改為迭代器刪除 13} 14System.out.println(entry.getKey()+"--->"+entry.getValue()); 15}
這裡我們看一下迭代器怎麼刪除的:
1 public final void remove() { 2Node<K,V> p = current; 3if (p == null) 4throw new IllegalStateException(); 5if (modCount != expectedModCount) 6throw new ConcurrentModificationException(); 7current = null; 8K key = p.key; 9// 迭代器呼叫的也是集合本身的刪除方法核心邏輯,我們知道集合本身刪除會改變modCount值,但是迭代器刪除後緊接著重新賦值expectedModCount = modCount,這樣就不會產生併發異常 10removeNode(hash(key), key, null, false, false); 11expectedModCount = modCount; 12}
上面是在單執行緒下,如果多執行緒呢?我們看一下,改造程式碼如下:
1final HashMap map = new HashMap(); 2map.put(1,"1"); 3map.put(2,"2"); 4map.put(3,"3"); 5map.put(4,"4"); 6 7Thread t1 = new Thread(){ 8@Override 9public void run() { 10Set entrySet = map.entrySet(); 11Iterator<Map.Entry> iterator = entrySet.iterator(); 12while (iterator.hasNext()){ 13Map.Entry entry = iterator.next(); 14if (entry.getKey().equals(2)){ 15iterator.remove();//改為迭代器刪除 16} 17} 18} 19}; 20 21Thread t2 = new Thread(){ 22@Override 23public void run() { 24Set entrySet = map.entrySet(); 25Iterator<Map.Entry> iterator = entrySet.iterator(); 26while (iterator.hasNext()){ 27Map.Entry entry = iterator.next(); 28System.out.println(entry.getKey()+"--->"+entry.getValue()); 29try { 30Thread.sleep(500); 31} catch (InterruptedException e) { 32e.printStackTrace(); 33} 34} 35} 36}; 37 38t1.start(); 39t2.start();
執行程式,有時依然會報併發異常:
11--->1 2Exception in thread "Thread-1" java.util.ConcurrentModificationException 3at java.util.HashMap$HashIterator.nextNode(HashMap.java:1437) 4at java.util.HashMap$EntryIterator.next(HashMap.java:1471) 5at java.util.HashMap$EntryIterator.next(HashMap.java:1469) 6at com.wanglei55.mjavalib.myClass$2.run(myClass.java:191)
上面我們在每個執行緒都獲取了迭代器: Iterator iterator = entrySet.iterator();我們看下獲取迭代器的原始碼:
1 public final Iterator<Map.Entry<K,V>> iterator() { 2return new EntryIterator(); 3}
直接返回一個新的迭代器,也就是說每個執行緒有自己的迭代器,初始化的時候各自的expectedModCount等於modCount,當一個執行緒呼叫remove()方法後會改變共用的modCount,而另一個執行緒的expectedModCount依然等於原先的modCount,這樣另一個執行緒在進行迭代操作的時候就會發生併發異常。
那怎麼解決呢?有同學估計會想到用Hashtable啊,Hashtable是執行緒安全的,其實你改造上述程式碼為Hashtable也同樣會發生併發異常,Hashtable執行緒安全是指的put,get這些方法是執行緒安全的,而這裡的問題是每個執行緒有自己的迭代器,我們需要給迭代過程加鎖,如下:
1final HashMap map = new HashMap(); 2 3for (int i = 0; i < 20; i++) { 4map.put(i,i); 5} 6 7Thread t1 = new Thread(){ 8@Override 9public void run() { 10synchronized (myClass.class){ 11Set entrySet = map.entrySet(); 12Iterator<Map.Entry> iterator = entrySet.iterator(); 13while (iterator.hasNext()){ 14Map.Entry entry = iterator.next(); 15if (entry.getKey().equals(2)){ 16iterator.remove();//改為迭代器刪除 17} 18} 19} 20} 21}; 22 23Thread t2 = new Thread(){ 24@Override 25public void run() { 26synchronized (myClass.class){ 27Set entrySet = map.entrySet(); 28Iterator<Map.Entry> iterator = entrySet.iterator(); 29while (iterator.hasNext()){ 30Map.Entry entry = iterator.next(); 31System.out.println(entry.getKey()+"--->"+entry.getValue()); 32try { 33Thread.sleep(500); 34} catch (InterruptedException e) { 35e.printStackTrace(); 36} 37} 38} 39} 40}; 41 42t2.start(); 43t1.start();
或者改為支援併發的ConcurrentHashMap,這樣就可以解決併發問題了。