一、前言
本篇文章沒有什么主題,就是一些零散點的總結。
周末沒事看了幾道螞蟻金服的面試題,其中有好幾道都是特別簡單的,基礎性的題目,就是我們平時用到的,但是發現要是完全說出來還是有一些不清楚的地方,所以小小的總結一下。
二、hashCode()方法理解
提到hashCode()必然會涉及equals()方法,二者是緊密相連的,其實面試中被問到這方面往往是考察集合存儲對象判斷相等的問題。
比如有如下Person類:
1public class Person { 2 3 private int age; 4 private String name; 5 6 public Person(int age, String name) { 7 this.age = age; 8 this.name = name; 9 } 10 11 public int getAge() { 12 return age; 13 } 14 15 public void setAge(int age) { 16 this.age = age; 17 } 18 19 public String getName() { 20 return name; 21 } 22 23 public void setName(String name) { 24 this.name = name; 25 } 26 27 @Override 28 public boolean equals(Object o) { 29 if (this == o) return true; 30 if (o == null || getClass() != o.getClass()) return false; 31 Person person = (Person) o; 32 return age == person.age && 33 Objects.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()方法:
1 public int hashCode() { 2 return identityHashCode(this); 3 }
不同對象會生成不同的hash值,所以嚴格來說hashcode()與equals()方法我們最好同時重寫,否則與集合類結合使用的時候會產生問題,改造Person類添加如下hashcode()方法:
1 @Override 2 public int hashCode() { 3 return 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> 2 extends AbstractSet<E> 3 implements Set<E>, Cloneable, java.io.Serializable 4{ 5 static final long serialVersionUID = -5024744406713321676L; 6 7 private transient HashMap<E,Object> map; 8 9 // Dummy value to associate with an Object in the backing Map 10 private static final Object PRESENT = new Object(); 11 12 public HashSet() { 13 map = new HashMap<>(); 14 } 15 16 public HashSet(Collection<? extends E> c) { 17 map = new HashMap<>(Math.max((int) (c.size()/.75f) + 1, 16)); 18 addAll(c); 19 } 20 21 public HashSet(int initialCapacity, float loadFactor) { 22 map = new HashMap<>(initialCapacity, loadFactor); 23 } 24 25 public HashSet(int initialCapacity) { 26 map = new HashMap<>(initialCapacity); 27 } 28 29 HashSet(int initialCapacity, float loadFactor, boolean dummy) { 30 map = new LinkedHashMap<>(initialCapacity, loadFactor); 31 } 32 33 public Iterator<E> iterator() { 34 return map.keySet().iterator(); 35 } 36 37 public int size() { 38 return map.size(); 39 } 40 41 public boolean isEmpty() { 42 return map.isEmpty(); 43 } 44 45 public boolean contains(Object o) { 46 return map.containsKey(o); 47 } 48 49 public boolean add(E e) { 50 return map.put(e, PRESENT)==null; 51 } 52 53 public boolean remove(Object o) { 54 return map.remove(o)==PRESENT; 55 } 56 57 public void clear() { 58 map.clear(); 59 }
這里只列出了部分方法,不過已經足夠了,幾個構造方法也就是初始化HashMap,其余的方法也都是調用HashMap對應的方法,所以你要是理解了HashMap那HashSet幾秒鍾就全都懂了,不理解HashMap請轉到:
Android版數據結構與算法(四):基於哈希表實現HashMap核心源碼徹底分析
我們在調用add(E e)方法的時候,key就是e,而value永遠是PRESENT,也就是Object()對象了。
這里注意一下:
1 HashSet(int initialCapacity, float loadFactor, boolean dummy) { 2 map = new LinkedHashMap<>(initialCapacity, loadFactor); 3 }
這個構造方法是給LinkedHashSet調用的,我們無法使用,沒有public修飾。
所以要是問你HashSet如何判斷元素重復的,也就是和HashMap一樣通過hashcode()與equals()方法來判斷。
LinkedHashSet
接下來看下LinkedHashSet源碼:
1public class LinkedHashSet<E> 2 extends HashSet<E> 3 implements Set<E>, Cloneable, java.io.Serializable { 4 5 private static final long serialVersionUID = -2851667679971038690L; 6 7 public LinkedHashSet(int initialCapacity, float loadFactor) { 8 super(initialCapacity, loadFactor, true); 9 } 10 11 public LinkedHashSet(int initialCapacity) { 12 super(initialCapacity, .75f, true); 13 } 14 15 public LinkedHashSet() { 16 super(16, .75f, true); 17 } 18 19 public LinkedHashSet(Collection<? extends E> c) { 20 super(Math.max(2*c.size(), 11), .75f, true); 21 addAll(c); 22 } 23 24 @Override 25 public Spliterator<E> spliterator() { 26 return Spliterators.spliterator(this, Spliterator.DISTINCT | Spliterator.ORDERED); 27 } 28}
就是那么簡短,LinkedHashSet繼承HashSet,初始化調用的就是HashSet中三個參數的構造函數,上面已經提到,也就map初始為LinkedHashMap,如果你對LinkedHashMap完全理解,那么這里就十分簡單了,如果不理解LinkedHashMap,請轉到:
Android版數據結構與算法(五):LinkedHashMap核心源碼徹底分析
總結一句話:LinkedHashSet是數據無重復並基於存入數據順序排序的集合,數據重復判斷的依據依然是hashcode()與equals()方法來判斷。
TreeMap
同樣看一下TreeMap中部分源碼,構造部分:
1 private transient NavigableMap<E,Object> m; 2 3 private static final Object PRESENT = new Object(); 4 5 TreeSet(NavigableMap<E,Object> m) { 6 this.m = m; 7 } 8 9 public TreeSet() { 10 this(new TreeMap<E,Object>()); 11 } 12 13 public TreeSet(Comparator<? super E> comparator) { 14 this(new TreeMap<>(comparator)); 15 }
看到了吧,構造時我們可以自己指定一個NavigableMap,如不指定則默認為TreeMap,所以TreeSet底層實現為TreeMap,加入數據的時候value同樣永遠是Object:
1 public boolean add(E e) { 2 return m.put(e, PRESENT)==null;//private static final Object PRESENT = new Object(); 3 }
TreeMap如不熟悉請轉到:
TreeMap是怎么比較數據是否相等的呢?怎么排序的呢?這里我們就要查看一下TreeMap中的put方法源碼了:
1 public V put(K key, V value) { 2 TreeMapEntry<K,V> t = root; 3 if (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 19 if (comparator != null) { 20 if (key == null) { 21 comparator.compare(key, key); 22 } 23 } else { 24 if (key == null) { 25 throw new NullPointerException("key == null"); 26 } else if (!(key instanceof Comparable)) { 27 throw new ClassCastException( 28 "Cannot cast" + key.getClass().getName() + " to Comparable."); 29 } 30 } 31 // END Android-changed: Work around buggy comparators. http://b/34084348 32 root = new TreeMapEntry<>(key, value, null); 33 size = 1; 34 modCount++; 35 return null; 36 } 37 int cmp; 38 TreeMapEntry<K,V> parent; 39 // split comparator and comparable paths 40 Comparator<? super K> cpr = comparator; 41 if (cpr != null) { 42 do { 43 parent = t; 44 cmp = cpr.compare(key, t.key); 45 if (cmp < 0) 46 t = t.left; 47 else if (cmp > 0) 48 t = t.right; 49 else 50 return t.setValue(value); 51 } while (t != null); 52 } 53 else { 54 if (key == null) 55 throw new NullPointerException(); 56 @SuppressWarnings("unchecked") 57 Comparable<? super K> k = (Comparable<? super K>) key; 58 do { 59 parent = t; 60 cmp = k.compareTo(t.key); 61 if (cmp < 0) 62 t = t.left; 63 else if (cmp > 0) 64 t = t.right; 65 else 66 return t.setValue(value); 67 } while (t != null); 68 } 69 TreeMapEntry<K,V> e = new TreeMapEntry<>(key, value, parent); 70 if (cmp < 0) 71 parent.left = e; 72 else 73 parent.right = e; 74 fixAfterInsertion(e); 75 size++; 76 modCount++; 77 return 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(); 2 map.put(1,"1"); 3 map.put(2,"2"); 4 map.put(3,"3"); 5 map.put(4,"4"); 6 7 Set entrySet = map.entrySet(); 8 Iterator<Map.Entry> iterator = entrySet.iterator(); 9 while (iterator.hasNext()){ 10 Map.Entry entry = iterator.next(); 11 if (entry.getKey().equals(2)){ 12 map.remove(entry.getKey());//調用集合類本身的刪除方法 13 } 14 System.out.println(entry.getKey()+"--->"+entry.getValue()); 15 }
運行程序如下:
1 1--->1 2 2--->2 3 Exception in thread "main" java.util.ConcurrentModificationException 4 at java.util.HashMap$HashIterator.nextNode(HashMap.java:1437) 5 at java.util.HashMap$EntryIterator.next(HashMap.java:1471) 6 at java.util.HashMap$EntryIterator.next(HashMap.java:1469) 7 at com.wanglei55.mjavalib.myClass.main(myClass.java:173)
至於產生問題原因這里我就不細說了,很基礎的。
改為如下用迭代器刪除就可以了:
1 HashMap map = new HashMap(); 2 map.put(1,"1"); 3 map.put(2,"2"); 4 map.put(3,"3"); 5 map.put(4,"4"); 6 7 Set entrySet = map.entrySet(); 8 Iterator<Map.Entry> iterator = entrySet.iterator(); 9 while (iterator.hasNext()){ 10 Map.Entry entry = iterator.next(); 11 if (entry.getKey().equals(2)){ 12 iterator.remove();//改為迭代器刪除 13 } 14 System.out.println(entry.getKey()+"--->"+entry.getValue()); 15 }
這里我們看一下迭代器怎么刪除的:
1 public final void remove() { 2 Node<K,V> p = current; 3 if (p == null) 4 throw new IllegalStateException(); 5 if (modCount != expectedModCount) 6 throw new ConcurrentModificationException(); 7 current = null; 8 K key = p.key; 9 // 迭代器調用的也是集合本身的刪除方法核心邏輯,我們知道集合本身刪除會改變modCount值,但是迭代器刪除后緊接着重新賦值expectedModCount = modCount,這樣就不會產生並發異常 10 removeNode(hash(key), key, null, false, false); 11 expectedModCount = modCount; 12 }
上面是在單線程下,如果多線程呢?我們看一下,改造代碼如下:
1final HashMap map = new HashMap(); 2 map.put(1,"1"); 3 map.put(2,"2"); 4 map.put(3,"3"); 5 map.put(4,"4"); 6 7 Thread t1 = new Thread(){ 8 @Override 9 public void run() { 10 Set entrySet = map.entrySet(); 11 Iterator<Map.Entry> iterator = entrySet.iterator(); 12 while (iterator.hasNext()){ 13 Map.Entry entry = iterator.next(); 14 if (entry.getKey().equals(2)){ 15 iterator.remove();//改為迭代器刪除 16 } 17 } 18 } 19 }; 20 21 Thread t2 = new Thread(){ 22 @Override 23 public void run() { 24 Set entrySet = map.entrySet(); 25 Iterator<Map.Entry> iterator = entrySet.iterator(); 26 while (iterator.hasNext()){ 27 Map.Entry entry = iterator.next(); 28 System.out.println(entry.getKey()+"--->"+entry.getValue()); 29 try { 30 Thread.sleep(500); 31 } catch (InterruptedException e) { 32 e.printStackTrace(); 33 } 34 } 35 } 36 }; 37 38 t1.start(); 39 t2.start();
運行程序,有時依然會報並發異常:
1 1--->1 2 Exception in thread "Thread-1" java.util.ConcurrentModificationException 3 at java.util.HashMap$HashIterator.nextNode(HashMap.java:1437) 4 at java.util.HashMap$EntryIterator.next(HashMap.java:1471) 5 at java.util.HashMap$EntryIterator.next(HashMap.java:1469) 6 at com.wanglei55.mjavalib.myClass$2.run(myClass.java:191)
上面我們在每個線程都獲取了迭代器: Iterator iterator = entrySet.iterator();我們看下獲取迭代器的源碼:
1 public final Iterator<Map.Entry<K,V>> iterator() { 2 return new EntryIterator(); 3 }
直接返回一個新的迭代器,也就是說每個線程有自己的迭代器,初始化的時候各自的expectedModCount等於modCount,當一個線程調用remove()方法后會改變共用的modCount,而另一個線程的expectedModCount依然等於原先的modCount,這樣另一個線程在進行迭代操作的時候就會發生並發異常。
那怎么解決呢?有同學估計會想到用Hashtable啊,Hashtable是線程安全的,其實你改造上述代碼為Hashtable也同樣會發生並發異常,Hashtable線程安全是指的put,get這些方法是線程安全的,而這里的問題是每個線程有自己的迭代器,我們需要給迭代過程加鎖,如下:
1 final HashMap map = new HashMap(); 2 3 for (int i = 0; i < 20; i++) { 4 map.put(i,i); 5 } 6 7 Thread t1 = new Thread(){ 8 @Override 9 public void run() { 10 synchronized (myClass.class){ 11 Set entrySet = map.entrySet(); 12 Iterator<Map.Entry> iterator = entrySet.iterator(); 13 while (iterator.hasNext()){ 14 Map.Entry entry = iterator.next(); 15 if (entry.getKey().equals(2)){ 16 iterator.remove();//改為迭代器刪除 17 } 18 } 19 } 20 } 21 }; 22 23 Thread t2 = new Thread(){ 24 @Override 25 public void run() { 26 synchronized (myClass.class){ 27 Set entrySet = map.entrySet(); 28 Iterator<Map.Entry> iterator = entrySet.iterator(); 29 while (iterator.hasNext()){ 30 Map.Entry entry = iterator.next(); 31 System.out.println(entry.getKey()+"--->"+entry.getValue()); 32 try { 33 Thread.sleep(500); 34 } catch (InterruptedException e) { 35 e.printStackTrace(); 36 } 37 } 38 } 39 } 40 }; 41 42 t2.start(); 43 t1.start();
或者改為支持並發的ConcurrentHashMap,這樣就可以解決並發問題了。
