HashMap[键值对存储]和ArrayList[顺序存储]是Java集合中使用频率最高的两个集合。经常我们都会自然的使用它们来存放数据,然后用Java JDK自带方法来操作他们(添加,修改,删除,迭代等)来实现我们的业务逻辑。
在多线程环境下,需要考虑数据安全性,应该优先考虑其同步类,如HashTable和Vector。ConcurrentHashMap也值得推荐。但是如果不涉及修改和删除,仅有只读的情况下,使用HashMap将会使性能大大提升。
一.HashMap的数据结构。
数组的特点是:寻址容易,插入和删除困难;而链表的特点是:寻址困难,插入和删除容易。HashMap则是数组和链表的综合体,具有两者的优点,被称为链表的数组。
哈希算法是为了能够进行快速查找定位所使用的的一种技术。
哈希表是一个以空间换时间的数据结构。
下图描述了HashMap底层的存储结构,为一个默认长度为16的数组,数组的每个元素存放的是一个链表。例如496%16=0,则存放在index=0的第一个位置,而896%16=0,也是存放在数组index=0的位置,由于第一个位置已经被496占据了,则存放在第二个位置,依次链式存储。【链地址法:java中解决hash冲突的方法】
还有个办法可以很清晰的看到HashMap内部结构。在Eclipse下写个测试类Debug下,inspect查看map对象,可看到各个属性的值。默认长度为16,加载因子为0.75,当前存放4个键值对。
map默认打印enrySet中键值对情况。
而选中table属性,更可以看到键值对的散列情况。
二.HashMap如何存放数据。
查看JDK中源码:
/** * Associates the specified value with the specified key in this map. * If the map previously contained a mapping for the key, the old * value is replaced. * * @param key key with which the specified value is to be associated * @param value value to be associated with the specified key * @return the previous value associated with <tt>key</tt>, or * <tt>null</tt> if there was no mapping for <tt>key</tt>. * (A <tt>null</tt> return can also indicate that the map * previously associated <tt>null</tt> with <tt>key</tt>.) */ public V put(K key, V value) { if (key == null) return putForNullKey(value); int hash = hash(key.hashCode()); int i = indexFor(hash, table.length); for (Entry<K,V> e = table[i]; e != null; e = e.next) { Object k; if (e.hash == hash && ((k = e.key) == key || key.equals(k))) { V oldValue = e.value; e.value = value; e.recordAccess(this); return oldValue; } } modCount++; addEntry(hash, key, value, i); return null; }可以看到,当map键值对put进来的时候,如果key为空,则直接放在table[0]处。
不为空,则根据hash(key.hashCode())与table的长度(默认16)的模。来确定该键值对所在table中的索引。然后遍历该索引处table[i]所对应的Entry链表,根据key的二次hash值和key的equals()的值,来判断是否已经存在该键值对。如果存在则覆盖原键值对的value,如果没有则添加至链表的最后。
三.HashMap如何读取数据。
查看JDK中源码:
/** * Returns the value to which the specified key is mapped, * or {@code null} if this map contains no mapping for the key. * * <p>More formally, if this map contains a mapping from a key * {@code k} to a value {@code v} such that {@code (key==null ? k==null : * key.equals(k))}, then this method returns {@code v}; otherwise * it returns {@code null}. (There can be at most one such mapping.) * * <p>A return value of {@code null} does not <i>necessarily</i> * indicate that the map contains no mapping for the key; it's also * possible that the map explicitly maps the key to {@code null}. * The {@link #containsKey containsKey} operation may be used to * distinguish these two cases. * * @see #put(Object, Object) */ public V get(Object key) { if (key == null) return getForNullKey(); int hash = hash(key.hashCode()); for (Entry<K,V> e = table[indexFor(hash, table.length)]; e != null; e = e.next) { Object k; if (e.hash == hash && ((k = e.key) == key || key.equals(k))) return e.value; } return null; }
可以看到,map在通过key来get对应的value时,和存储时很类似。当key为null时,直接去table[0]的链表去获得key为null的value。不为空则根据二次hash值找到对应的索引,再在索引所在的链表中,通过先前的二次hash值和key的equal方法来确定链表节点,如果找到,则返回该value。
四.一些常见问题。
1. 两个对象比较equals()为true,则hashCode也相同吗? 反之呢?
回答:不一定。在没有重写hashCode方法的前提下,他们比较的Object里的hashCode返回的值,如果重写了,则是使用自定义的hashCode方法的返回值。如果两个String值equals为true则他们hashCode相同,而实际项目中,我们会覆盖hashCode方法来重写我们业务的判断逻辑,这样即使equals相等,hashCode也不同。
反之,hashCode相同,equals比较一定相等。因为equals比较的依据就是hashCode的值。
2. 如何定位一个数值的index。
回答:hashMap的处理方法是,将key二次hash之后的值,与table的长度进行取模运算。F返回值即为当前table的index。
int hash = hash(key.hashCode()); int i = indexFor(hash, table.length); //。。。。。。 /** * Returns index for hash code h. */ static int indexFor(int h, int length) { return h & (length-1); }
3. HashMap存放数据,是否会发生数据冲撞?
回答:会。当他们对象的hashcode相同时,他们会保存在同一个table[i]的同一个索引上,此时发生碰撞。HashMap拥有一个LinkList机制,将发生碰撞的值以链表形式存储在同一table[i]上。
4. hashMap是否会无限存放数据?
回答:会。当table的容量达到当前容量*加载因子(默认的capacity * load factor是16*0.75)是,table的容量会增加一倍,来应对存储量的扩增。
/** * Adds a new entry with the specified key, value and hash code to * the specified bucket. It is the responsibility of this * method to resize the table if appropriate. * * Subclass overrides this to alter the behavior of put method. */ void addEntry(int hash, K key, V value, int bucketIndex) { Entry<K,V> e = table[bucketIndex]; table[bucketIndex] = new Entry<K,V>(hash, key, value, e); if (size++ >= threshold) resize(2 * table.length); }
5. hashmap如何散列数值的?取模为什么是16,而不是15?
回答:hashmap里的hash算法设计的目的是使元素的排列尽量分散,也就是散列。尽量使得每个位置上的元素数量只有一个。此算法加入了高位计算,防止低位不变,高位变化时,造成的hash冲突。
/** * Applies a supplemental hash function to a given hashCode, which * defends against poor quality hash functions. This is critical * because HashMap uses power-of-two length hash tables, that * otherwise encounter collisions for hashCodes that do not differ * in lower bits. Note: Null keys always map to hash 0, thus index 0. */ static int hash(int h) { // This function ensures that hashCodes that differ only by // constant multiples at each bit position have a bounded // number of collisions (approximately 8 at default load factor). h ^= (h >>> 20) ^ (h >>> 12); return h ^ (h >>> 7) ^ (h >>> 4); } /** * Returns index for hash code h. */ static int indexFor(int h, int length) { return h & (length-1); }
当length总是2的n次方时,h& (length-1)运算等价于对length取模,也就是h%length,但是&比%具有更高的效率。只有是2的n次方,才能保证每次二进制与运算之后的结果笼罩所有的数。
6. 何如合理设计一个hashMap。
五.自己设计一个HashMap。
代码copy至http://www.cnblogs.com/xwdreamer/archive/2012/05/14/2499339.html
基本与JDK源码一致:
Entry.java
public class Entry<K, V> { final K key; V value; Entry<K, V> next;// 下一个结点 // 构造函数 public Entry(K k, V v, Entry<K, V> n) { key = k; value = v; next = n; } public final K getKey() { return key; } public final V getValue() { return value; } public final V setValue(V newValue) { V oldValue = value; value = newValue; return oldValue; } public final boolean equals(Object o) { if (!(o instanceof Entry)) return false; Entry e = (Entry) o; Object k1 = getKey(); Object k2 = e.getKey(); if (k1 == k2 || (k1 != null && k1.equals(k2))) { Object v1 = getValue(); Object v2 = e.getValue(); if (v1 == v2 || (v1 != null && v1.equals(v2))) return true; } return false; } public final int hashCode() { return (key == null ? 0 : key.hashCode()) ^ (value == null ? 0 : value.hashCode()); } public final String toString() { return getKey() + "=" + getValue(); } }
MyHashMap.java
public class MyHashMap<K, V> { private Entry[] table;// Entry数组表 static final int DEFAULT_INITIAL_CAPACITY = 16;// 默认数组长度 private int size; // 构造函数 public MyHashMap() { table = new Entry[DEFAULT_INITIAL_CAPACITY]; size = DEFAULT_INITIAL_CAPACITY; } // 获取数组长度 public int getSize() { return size; } // 求index static int indexFor(int h, int length) { return h % (length - 1); } // 获取元素 public V get(Object key) { if (key == null) return null; int hash = key.hashCode();// key的哈希值 int index = indexFor(hash, table.length);// 求key在数组中的下标 for (Entry<K, V> e = table[index]; e != null; e = e.next) { Object k = e.key; if (e.key.hashCode() == hash && (k == key || key.equals(k))) return e.value; } return null; } // 添加元素 public V put(K key, V value) { if (key == null) return null; int hash = key.hashCode(); int index = indexFor(hash, table.length); // 如果添加的key已经存在,那么只需要修改value值即可 for (Entry<K, V> e = table[index]; e != null; e = e.next) { Object k = e.key; if (e.key.hashCode() == hash && (k == key || key.equals(k))) { V oldValue = e.value; e.value = value; return oldValue;// 原来的value值 } } // 如果key值不存在,那么需要添加 Entry<K, V> e = table[index];// 获取当前数组中的e table[index] = new Entry<K, V>(key, value, e);// 新建一个Entry,并将其指向原先的e return null; } }
MyHashMapTest.java
public class MyHashMapTest { public static void main(String[] args) { MyHashMap<Integer, Integer> map = new MyHashMap<Integer, Integer>(); map.put(1, 90); map.put(2, 95); map.put(17, 85); System.out.println(map.get(1)); System.out.println(map.get(2)); System.out.println(map.get(17)); System.out.println(map.get(null)); } }