《java集合》--HashMap

HashMap

参考:Java8 HashMap实现原理探究

特点

  • 基于Hash表的Map接口实现
  • 线程非安全,并且允许key与value都为null值,HashTable与之相反,为线程安全,key与value都不允许null值。
  • 不保证映射的顺序,特别是它不保证顺序恒久不变,resize时会重排
  • 当数组没有链表存在时,HashMap性能最好为O(1)。而最差为O(threshould)即所有元素存在一个链表上。
  • hashMap会根据存入的size和负载英子*数组初始容量进行扩容

数据结构

HashMap实际上是一个链表散列的数据结构,即数组和链表的结合体,HashMap底层就是一个数组结构,数组中的每一项又是一个链表

image

jdk8优化,如果数组某个位置链表长度超过8个会转成红黑树,jdk8之前一直是链表,链表查询的复杂度是O(n),而红黑树由于其自身的特点,查询的复杂度是O(log(n)),会提高遍历性能

image

源码解析

  • 构造函数初始化

    1、initialCapacity:数组的初始化长度,2的n次方

    2、loadFactor:负载英子,默认是0.75

    3、threshold:最大容量,threshold=initialCapacity*loadFactor,当entry的数量超过capacity*loadFactor时,容器将自动扩容并重新哈希。

    4、在对HashMap进行迭代时,需要遍历整个table以及后面跟的冲突链表。因此对于迭代比较频繁的场景,不宜将HashMap的初始大小设的过大。

    5、增大负载因子可以减少数组所占用的内存空间,但会增加查询数据的时间开销,而查询是最频繁的的操作(HashMap 的 get() 与 put()方法都要用到查询);减小负载因子会提高数据查询的性能,但会增加Hash表所占用的内存空间。

    6、将对向放入到HashMap或HashSet中时,有两个方法需要特别关心:hashCode()和equals()。hashCode()方法决定了对象会被放到哪个bucket里,当多个对象的哈希值冲突时,equals()方法决定了这些对象是否是“同一个对象”。所以,如果要将自定义的对象放入到HashMap或HashSet中,需要@Override hashCode()和equals()方法。


// 以指定初始化容量、负载因子创建 HashMap   
 public HashMap(int initialCapacity, float loadFactor)   
 {   
   // 初始容量不能为负数  
   if (initialCapacity < 0)   
     throw new IllegalArgumentException(   
    "Illegal initial capacity: " +   
       initialCapacity);   
   // 如果初始容量大于最大容量,table数组长度为最大长度
   if (initialCapacity > MAXIMUM_CAPACITY)   
     initialCapacity = MAXIMUM_CAPACITY;   
   // 负载因子必须大于 0 的数值  
   if (loadFactor <= 0 || Float.isNaN(loadFactor))   
     throw new IllegalArgumentException(   
     loadFactor);   
   // 计算出大于 initialCapacity 的最小的 2 的 n 次方值。  
   int capacity = 1;   
   while (capacity < initialCapacity)   
     capacity <<= 1;   
   this.loadFactor = loadFactor;   
   // 设置容量极限等于容量 * 负载因子  
   threshold = (int)(capacity * loadFactor);   
   // 底层是一个数组  
   table = new Entry[capacity]; 
   init();   
 }

//

Entry对象,实现了单向链表
static class Entry<K,V> implements Map.Entry<K,V> {
        final K key;//key
        V value;//值
        Entry<K,V> next;//next指针
        final int hash;//key的hash值


        Entry(int h, K k, V v, Entry<K,V> n) {
            value = v;
            next = n;
            key = k;
            hash = h;
        }
        //重写了hashCode方法
        public final int hashCode() {
            return (key==null   ? 0 : key.hashCode()) ^
                   (value==null ? 0 : value.hashCode());
        }
}
  • put方法
public V put(K key, V value) {
    // HashMap允许存放null键和null值。
    // 当key为null时,调用putForNullKey方法,将value放置在数组第一个位置。  
    if (key == null)
        return putForNullKey(value);
    // 根据key的keyCode重新计算hash值。
    int hash = hash(key.hashCode());
    // 搜索指定hash值在对应table中的索引。
    int i = indexFor(hash, table.length);
    // 如果 i 索引处的 Entry 不为 null,通过循环不断遍历 e 元素的下一个元素。
    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;
        }
    }
    // 如果i索引处的Entry为null,表明此处还没有Entry。
    modCount++;
    // 将key、value添加到i索引处。
    addEntry(hash, key, value, i);
    return null;
}
处理key值为空的情况
private V putForNullKey(V value) {
    for (Entry<K,V> e = table[0]; e != null; e = e.next) {
        if (e.key == null) {
            V oldValue = e.value;
            e.value = value;
            //LinkedHashMap 有不同的处理
            e.recordAccess(this);
            return oldValue;
        }
    }
    modCount++;
    addEntry(0, null, value, 0);
    return null;
}
根据key的hash值进行hash运算,如果key的hash值一样,调用HashMap的hash运算后得到的结果是一样的
static int hash(int h)   
{   
    h ^= (h >>> 20) ^ (h >>> 12);   
    return h ^ (h >>> 7) ^ (h >>> 4);   
}
HashMap对key值得hash值运算后获取table数组的下标table数组的长度总是2的n次方,默认是16,通过 & 运算得到的就是数组的下标,最大不会超过数组的长度,保证数组长度总是2的n次方,这样长度-1后二进制所有位置都是1,跟key的hash值进行&运算时会更加均匀的分布entry到数组上。
static int indexFor(int h, int length)   
{   
    return h & (length-1);   
}
将entry存入数组中,如果数组该位置上已经存放有其他元素了,那么在这个位置上的元素将以链表的形式存放,新加入的放在链头,最先加入的放在链尾。如果数组该位置上没有元素,就直接将该元素放到此数组中的该位置上
void addEntry(int hash, K key, V value, int bucketIndex) {
    //获取数组当前位置的entry
    Entry<K,V> e = table[bucketIndex];
    //将要保存的entry存入数组当前位置,之前的entry的指针作为当前entry的一个next属性
    table[bucketIndex] = new Entry<K,V>(hash, key, value, e);
    //size+1并且和允许最大比较,如果超过则扩展数组的长度为之前的2倍
    if (size++ >= threshold)
        resize(2 * table.length);
}
扩展数组的长度,每次扩展原数组长度的2倍,保证数组的长度为2的n次方,扩展的实质是创建一个2倍长度的新的数组,原数组中的entry必须重新计算其在新数组中的位置,并放进去,这就是resize。
 void resize(int newCapacity) {
    Entry[] oldTable = table;
    int oldCapacity = oldTable.length;
    //如果已经是所支持的最大长度,设置最大存储的entry数量为Interger的最大值
    if (oldCapacity == MAXIMUM_CAPACITY) {
        threshold = Integer.MAX_VALUE;
        return;
    }
    //创建新的
    Entry[] newTable = new Entry[newCapacity];
    transfer(newTable);
    table = newTable;
    threshold = (int)(newCapacity * loadFactor);
}
//原数组上entry通过hash计算存放到新的数组上面
void transfer(Entry[] newTable) {
    Entry[] src = table;
    int newCapacity = newTable.length;
    for (int j = 0; j < src.length; j++) {
        Entry<K,V> e = src[j];
        if (e != null) {
            src[j] = null;
            do {
                Entry<K,V> next = e.next;
                int i = indexFor(e.hash, newCapacity);
                e.next = newTable[i];
                newTable[i] = e;
                e = next;
            } while (e != null);
        }
    }
}
  • get方法
public V get(Object key) {
    if (key == null)
        return getForNullKey();
    //计算hash值
    int hash = hash(key.hashCode());
    //遍历数组
    for (Entry<K,V> e = table[indexFor(hash, table.length)];
         e != null;
         e = e.next) {
        Object k;
        //比较key值得hash属性,相同的hash代表一个key
        if (e.hash == hash && ((k = e.key) == key || key.equals(k)))
            return e.value;
    }
    return null;
}
//单独处理key为空的情况
private V getForNullKey() {
    for (Entry<K,V> e = table[0]; e != null; e = e.next) {
        if (e.key == null)
            return e.value;
    }
    return null;
}
  • remove方法
public V remove(Object key) {
    Entry<K,V> e = removeEntryForKey(key);
    return (e == null ? null : e.value);
}


final Entry<K,V> removeEntryForKey(Object key) {

    int hash = (key == null) ? 0 : hash(key.hashCode());
    int i = indexFor(hash, table.length);
    Entry<K,V> prev = table[i];
    Entry<K,V> e = prev;

    while (e != null) {
        Entry<K,V> next = e.next;
        Object k;
        if (e.hash == hash &&
            ((k = e.key) == key || (key != null && key.equals(k)))) {
            modCount++;
            //长度-1
            size--;
            //操作链表
            if (prev == e)
                table[i] = next;
            else
                prev.next = next;
            e.recordRemoval(this);
            return e;
        }
        prev = e;
        e = next;
    }

    return e;
}
  • 迭代器实现
final class KeyIterator extends HashIterator
    implements Iterator<K> {
    public final K next() { return nextNode().key; }
}

final class ValueIterator extends HashIterator
    implements Iterator<V> {
    public final V next() { return nextNode().value; }
}

final class EntryIterator extends HashIterator
    implements Iterator<Map.Entry<K,V>> {
    public final Map.Entry<K,V> next() { return nextNode(); }
}
abstract class HashIterator {
    Node<K,V> next;        // next entry to return
    Node<K,V> current;     // current entry
    int expectedModCount;  // for fast-fail
    int index;             // current slot

    HashIterator() {
        expectedModCount = modCount;
        Node<K,V>[] t = table;
        current = next = null;
        index = 0;
        if (t != null && size > 0) { 
            //遍历table直到找到第一个元素,指向next
            do {} while (index < t.length && (next = t[index++]) ==null);
        }
    }

    public final boolean hasNext() {
        return next != null;
    }

    final Node<K,V> nextNode() {
        Node<K,V>[] t;
        Node<K,V> e = next;
        if (modCount != expectedModCount)
            throw new ConcurrentModificationException();
        if (e == null)
            throw new NoSuchElementException();
        //current用来记录上一个,删除的时候用
        if ((next = (current = e).next) == null && (t = table) != null) {
            do {} while (index < t.length && (next = t[index++]) ==null);
        }
        return e;
    }

    public final void remove() {
        Node<K,V> p = current;
        if (p == null)
            throw new IllegalStateException();
        if (modCount != expectedModCount)
            throw new ConcurrentModificationException();
        current = null;
        K key = p.key;
        //调用hashMap的方法删除
        removeNode(hash(key), key, null, false, false);
        expectedModCount = modCount;
    }
}

HashSet

hashSet是对HashMap的简单包装,对HashSet的函数调用都会转换成合适的HashMap方法,因此HashSet的实现非常简单,只有不到300行代码。这里不再赘述。

//HashSet是对HashMap的简单包装
public class HashSet<E>
{
    ......
    private transient HashMap<E,Object> map;//HashSet里面有一个HashMap
    // Dummy value to associate with an Object in the backing Map
    private static final Object PRESENT = new Object();
    public HashSet() {
        map = new HashMap<>();
    }
    ......
    public boolean add(E e) {//简单的方法转换
        return map.put(e, PRESENT)==null;
    }
    ......
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值