hashCode()方法以及集合中Set的一些总结
一、前言
本篇文章没有什么主题,就是一些零散点的总结。
周末没事看了几道蚂蚁金服的面试题,其中有好几道都是特别简单的,基础性的题目,就是我们平时用到的,但是发现要是完全说出来还是有一些不清楚的地方,所以小小的总结一下。
二、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,这样就可以解决并发问题了。