数组:采用一定的存储单元存储一群数据,对于指定下标查找,时间复杂度为O(1),但对定值查找,则需要遍历整个数据,一个一个的比较,时间复杂度是O(n),如果说是有序数组,可以采用二分查找法,增大查找效率,不过对于新增删除等涉及到数组元素的移动,时间复杂度都是O(n)
链表:对于链表的新增,删除,只需要将对应的节点引用上去就ok,时间复杂度是O(1),而查找则需要遍历链表,时间复杂度为O(N)
二叉树:对相对平衡的有序二叉树,查找,删除,插入等操作,复杂度为0(logn)
哈希表:哈希表中进行添加,删除,查找等操作,性能十分之高,不考虑哈希冲突的情况下,仅需一次定位即可完成,时间复杂度为O(1)
哈希冲突:哈希表的主干就是数组,插入一个新值的时候,会通过某个算法求出这个值的具体位置,而这个算法的优劣就直接决定哈希表的整个效率,而所谓的哈希冲突就是因为算出来的位置可能是相等的,所以冲突,由此可见这个hash算法多么的重要。哈希算法的设计要尽量的保证散列分布均匀,雨漏均沾。
HashMap1.7实现原理
主干是一个Entry的数组 ,Entry是HashMap中的静态内部类
transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;
static class Entry<K,V> implements Map.Entry<K,V> {
final K key;
V value;
Entry<K,V> next;
int hash;
/**
* Creates new entry.
*/
Entry(int h, K k, V v, Entry<K,V> n) {
value = v;
next = n;
key = k;
hash = h;
}
}
简单来说,HashMap由数组+链表组成的,数组是HashMap的主体,链表则是主要为了解决哈希冲突而存在的,如果定位到的数组位置不含链表(当前entry的next指向null),那么对于查找,添加等操作很快,仅需一次寻址即可;如果定位到的数组包含链表,对于添加操作,其时间复杂度依然为O(1),因为最新的Entry会插入链表头部,急需要简单改变引用链即可,而对于查找操作来讲,此时就需要遍历链表,然后通过key对象的equals方法逐一比对查找。所以,性能考虑,HashMap中的链表出现越少,性能才会越好。
基本属性说明:
//默认初始化化容量,即16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;//最大容量,即2的30次方
static final int MAXIMUM_CAPACITY = 1 << 30;//默认装载因子
static final float DEFAULT_LOAD_FACTOR = 0.75f;//HashMap内部的存储结构是一个数组,此处数组为空,即没有初始化之前的状态
static final Entry<?,?>[] EMPTY_TABLE = {};//空的存储实体
transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;//实际存储的key-value键值对的个数
transient int size;//阈值,当table == {}时,该值为初始容量(初始容量默认为16);当table被填充了,也就是为table分配内存空间后,threshold一般为 capacity*loadFactory。HashMap在进行扩容时需要参考threshold
int threshold;//负载因子,代表了table的填充度有多少,默认是0.75
final float loadFactor;//用于快速失败,由于HashMap非线程安全,在对HashMap进行迭代时,如果期间其他线程的参与导致HashMap的结构发生变化了(比如put,remove等操作),需要抛出异常ConcurrentModificationException
transient int modCount;//默认的threshold值
static final int ALTERNATIVE_HASHING_THRESHOLD_DEFAULT = Integer.MAX_VALUE;
构造函数
public HashMap(int initialCapacity, float loadFactor) { if (initialCapacity < 0) //判断参数是否正确 throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity); if (initialCapacity > MAXIMUM_CAPACITY) initialCapacity = MAXIMUM_CAPACITY; if (loadFactor <= 0 || Float.isNaN(loadFactor)) throw new IllegalArgumentException("Illegal load factor: " + loadFactor); this.loadFactor = loadFactor; threshold = initialCapacity; init(); //在HashMap中没有实现,子类LinkedHashMap实现了 }
从上面这段代码我们可以看出,我们在构造HashMap的时候,并没有马上为数组table分配内存空间(有一个入参为指定Map的构造器例外),事实上是在执行第一次put操作的时候才真正构建table数组。
put操作流程:
public V put(K key, V value) { if (table == EMPTY_TABLE) { //table为空,第一次put,创建数组 inflateTable(threshold); } if (key == null) return putForNullKey(value); //如果key为null,存储位置为table[0]或table[0]的冲突链上 int hash = hash(key); //所谓的hash算法,尽可能保证均匀分布 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; //如果value值相等,覆盖 e.recordAccess(this); return oldValue; } } modCount++; addEntry(hash, key, value, i); //value不相等,新添加一个Entry return null; }void addEntry(int hash, K key, V value, int bucketIndex) { if ((size >= threshold) && (null != table[bucketIndex])) { //判断是否需要扩容 resize(2 * table.length); hash = (null != key) ? hash(key) : 0; bucketIndex = indexFor(hash, table.length); } createEntry(hash, key, value, bucketIndex); }
void createEntry(int hash, K key, V value, int bucketIndex) { Entry<K,V> e = table[bucketIndex]; table[bucketIndex] = new Entry<>(hash, key, value, e); //JDK1.7版本put一个数据,是从头部插入 size++; }
扩容操作:
//按新的容量扩容Hash表 void resize(int newCapacity) { Entry[] oldTable = table;//老的数据 int oldCapacity = oldTable.length;//获取老的容量值 if (oldCapacity == MAXIMUM_CAPACITY) {//老的容量值已经到了最大容量值 threshold = Integer.MAX_VALUE;//修改扩容阀值 return; } //新的结构 Entry[] newTable = new Entry[newCapacity]; transfer(newTable, initHashSeedAsNeeded(newCapacity));//将老的表中的数据拷贝到新的结构中 table = newTable;//修改HashMap的底层数组 threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);//修改阀值 }transfer(newTable, initHashSeedAsNeeded(newCapacity))这个方法将老数组中的数据逐个链表地遍历,就不展开了,重新计算后放入新的扩容后的数组中,我们的数组索引位置的计算是通过 对key值的hashcode进行hash扰乱运算后,再通过和 length-1进行位运算得到最终数组索引位置。
get操作,其实就是遍历数组+链表,这边就不详述啦
重写equals方法需同时重写hashCode方法
如果我们已经对HashMap的原理有了一定了解,这个结果就不难理解了。尽管我们在进行get和put操作的时候,使用的key从逻辑上讲是等值的(通过equals比较是相等的),但由于没有重写hashCode方法,所以put操作时,key(hashcode1)-->hash-->indexFor-->最终索引位置 ,而通过key取出value的时候 key(hashcode2)-->hash-->indexFor-->最终索引位置,由于hashcode1不等于hashcode2,导致没有定位到一个数组位置而返回逻辑上错误的值null(也有可能碰巧定位到一个数组位置,但是也会判断其entry的hash值是否相等,上面get方法中有提到。)
所以,在重写equals的方法的时候,必须注意重写hashCode方法,同时还要保证通过equals判断相等的两个对象,调用hashCode方法要返回同样的整数值。而如果equals判断不相等的两个对象,其hashCode可以相同(只不过会发生哈希冲突,应尽量避免)