JDK集合 - HashMap源码阅读

概述

HashMap是Java中一个非常重要的集合类,它允许影射的集合中出现null键和null值。相比于List类型,他不能保证映射的顺序,而且其内部的顺序也不一定是
恒定不变的。HashMap利用hash表技术,并且其实现了Map接口的所有方法。其中,两个重要的参数影响了HashMap的性能,初始化容量(桶的数量)和加载因子。
如果很看重HashMap的迭代性能,则不能将初始容量设置的太大或者加载因子设置的太小。然而,在多线程环境下,HashMap是不安全的。
此时,必须使用其他工具对其加锁(Map m = Collections.synchronizedMap(new HashMap(…)))或使用其替代技术。

细节分析

  • 基本属性介绍

    从HashMap的源码中可以看出,默认初始化容量是16,并且可以人为设定初始化容量,但必须是2的多少次方的值。最大容量为2^30,同理加载因子也可以自行设置(也可以大于1,但是建议不要轻易修改,除非情况非常特殊),默认的加载因子为0.75。具体如下代码所示:

    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
    /**
    * The maximum capacity, used if a higher value is implicitly specified
    * by either of the constructors with arguments.
    * MUST be a power of two <= 1<<30.
    */
    static final int MAXIMUM_CAPACITY = 1 << 30;
    static final float DEFAULT_LOAD_FACTOR = 0.75f;

    其提供的构造函数如下:

    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;
       this.threshold = tableSizeFor(initialCapacity);
    }
    public HashMap(int initialCapacity) {
       this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }
    
    public HashMap() {
       this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
    }
    
    public HashMap(Map<? extends K, ? extends V> m) {
       this.loadFactor = DEFAULT_LOAD_FACTOR;
       putMapEntries(m, false);
    }
    

    从中可知,可以使用默认参数生成HashMap实例,也可以指定初始化容量和加载因子,并且也可以直接从一个集合实例生成HashMap对象,并且获取其中的数据。
    其中,加载因子 loadFactor 衡量的是一个散列表的空间的使用程度,加载因子子越大表示散列表的装填程度越高,
    反之愈小。对于使用链表法的散列表来说,查找一个元素的平均时间是 O(1+a),因此如果加载因子越大,对空间的利用更充分,
    然而后果是查找效率的降低;如果加载因子太小,那么散列表的数据将过于稀疏,对空间造成严重浪费。\
    HashMap中其衡量容量的门限值是threshold = capacity * loadFactor。当元素数目,超过此值得时候,就会调用resize()方法对其进行扩容处理,
    每次扩容后的容量是扩容前的2倍,并此时的threshold也是前一次的2倍。默认的加载因子为0.75,是对时间和空间效率的综合平衡作用的结果。

  • 存储技术

    从源码中可知,其内部的存储是实现了Map.Entry<K,V>Node<K,V>数组,并且数组中的每一项中又利用了链表技术。因此,其可以看做是数组和链表的结合体(JDK1.8增加了红黑树部分)。
    Node的源码如下所示:

    static class Node<K,V> implements Map.Entry<K,V> { // Node<K,V>在之前的版本叫Entry<K,V>
    
      final int hash;
    
      final K key;
    
      V value;
    
      Node<K,V> next;
    
    
      Node(int hash, K key, V value, Node<K,V> next) {
    
          this.hash = hash;
    
          this.key = key;
    
          this.value = value;
    
          this.next = next;
    
      }
    
    
      public final K getKey()        { return key; }
    
      public final V getValue()      { return value; }
    
      public final String toString() { return key + "=" + value; }
    
    
      public final int hashCode() {
    
          return Objects.hashCode(key) ^ Objects.hashCode(value);
    
      }
    
    
      public final V setValue(V newValue) {
    
          V oldValue = value;
    
          value = newValue;
    
          return oldValue;
    
      }
    
    
      public final boolean equals(Object o) {
    
          if (o == this)
    
              return true;
    
          if (o instanceof Map.Entry) {
    
              Map.Entry<?,?> e = (Map.Entry<?,?>)o;
    
              if (Objects.equals(key, e.getKey()) &&
    
                  Objects.equals(value, e.getValue()))
    
                  return true;
    
          }
    
          return false;
    
      }
    
    }

    其中,每一个Node项中,含有hash值、key值、value值和指向下一个的应用地址。并且结合构造函数中的内容还可以看出,其在构造函数中并没有创建存储数组的实例对象。阅读后面的源码可知,Node数组对象是在resize()方法中实现的,这一点与之前的版本中在构造函数中实现不同。

  • 核心方法分析

    1.从已有的map集合中创建HashMap实例

    final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
      int s = m.size();
      if (s > 0) {
          if (table == null) { // pre-size
              float ft = ((float)s / loadFactor) + 1.0F;
              int t = ((ft < (float)MAXIMUM_CAPACITY) ?
                       (int)ft : MAXIMUM_CAPACITY);
              if (t > threshold)
                  threshold = tableSizeFor(t);
          }
          else if (s > threshold)
              resize();
          for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) {
              K key = e.getKey();
              V value = e.getValue();
              putVal(hash(key), key, value, false, evict);
          }
      }
    }

    其中每一个数据也是通过遍历后调用putVal一个一个插入其中。

    2.HashMap增加数据方法
    增加数据的两个重要方法如下:

    public V put(K key, V value) {
      return putVal(hash(key), key, value, false, true);
    }
    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                 boolean evict) {
      Node<K,V>[] tab; Node<K,V> p; int n, i;
      if ((tab = table) == null || (n = tab.length) == 0)
          n = (tab = resize()).length;
      if ((p = tab[i = (n - 1) & hash]) == null)
          tab[i] = newNode(hash, key, value, null);
      else {
          Node<K,V> e; K k;
          if (p.hash == hash &&
              ((k = p.key) == key || (key != null && key.equals(k))))
              e = p;
          else if (p instanceof TreeNode)
              e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
          else {
              for (int binCount = 0; ; ++binCount) {
                  if ((e = p.next) == null) {
                      p.next = newNode(hash, key, value, null);
                      if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                          treeifyBin(tab, hash);
                      break;
                  }
                  if (e.hash == hash &&
                      ((k = e.key) == key || (key != null && key.equals(k))))
                      break;
                  p = e;
              }
          }
          if (e != null) { // existing mapping for key
              V oldValue = e.value;
              if (!onlyIfAbsent || oldValue == null)
                  e.value = value;
              afterNodeAccess(e);
              return oldValue;
          }
      }
      ++modCount;
      if (++size > threshold)
          resize();
      afterNodeInsertion(evict);
      return null;
    }
    public void putAll(Map<? extends K, ? extends V> m) {
      putMapEntries(m, true);
    }

    由此可知,HashMap在存储数据时,利用键key的Hash值作为Node中hash的属性值,并当中会判断是否扩容处理。其允许存放null的key和null的value,
    当其key为null时,调用putForNullKey方法,放入到table[0]的这个位置。这里的实现跟之前的版本也不一样。

    if ((p = tab[i = (n - 1) & hash]) == null)
      tab[i] = newNode(hash, key, value, null);

    并且每次添加数据时会判断table是否为空,或者其中的数据长度是否为0。这样可以resize()处理。
    其hash值的实现如下:

    static final int hash(Object key) {
      int h;
      return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

    3.HashMap获取数据的方法

    public V get(Object key) {
      Node<K,V> e;
      return (e = getNode(hash(key), key)) == null ? null : e.value;
    }
    
    /**
    * Implements Map.get and related methods
    *
    * @param hash hash for key
    * @param key the key
    * @return the node, or null if none
    */
    final Node<K,V> getNode(int hash, Object key) {
      Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
      if ((tab = table) != null && (n = tab.length) > 0 &&
          (first = tab[(n - 1) & hash]) != null) {
          if (first.hash == hash && // always check first node
              ((k = first.key) == key || (key != null && key.equals(k))))
              return first;
          if ((e = first.next) != null) {
              if (first instanceof TreeNode)
                  return ((TreeNode<K,V>)first).getTreeNode(hash, key);
              do {
                  if (e.hash == hash &&
                      ((k = e.key) == key || (key != null && key.equals(k))))
                      return e;
              } while ((e = e.next) != null);
          }
      }
      return null;
    }

    同理,也是根据key的hash值以及key值自身去获取相应的数据。即,从 HashMap 中 get 元素时,首先计算 key 的 hashCode,
    找到数组中对应位置的某一元素,然后通过 key 的 equals 方法在对应位置的链表中找到需要的元素。

    4.其他核心方法

    public int size() {
      return size;
    } // 当前数量
    public boolean isEmpty() {
      return size == 0;
    } // 是否是空
    public boolean containsKey(Object key) {
      return getNode(hash(key), key) != null;
    } // 是否包含某个键
    
    public V remove(Object key) {
      Node<K,V> e;
      return (e = removeNode(hash(key), key, null, false, true)) == null ?
          null : e.value;
    } // 删除键为key的数据
    
    public void clear() {
      Node<K,V>[] tab;
      modCount++;
      if ((tab = table) != null && size > 0) {
          size = 0;
          for (int i = 0; i < tab.length; ++i)
              tab[i] = null;
      }
    } // 清空数据
    
    public boolean containsValue(Object value) {
      Node<K,V>[] tab; V v;
      if ((tab = table) != null && size > 0) {
          for (int i = 0; i < tab.length; ++i) {
              for (Node<K,V> e = tab[i]; e != null; e = e.next) {
                  if ((v = e.value) == value ||
                      (value != null && value.equals(v)))
                      return true;
              }
          }
      }
      return false;
    } // 判断是否包含某个值
    
    public Set<K> keySet() {
      Set<K> ks = keySet;
      if (ks == null) {
          ks = new KeySet();
          keySet = ks;
      }
      return ks;
    } // 获取所有键组成的Set()集合
    final class KeySet extends AbstractSet<K> {
      public final int size()                 { return size; }
      public final void clear()               { HashMap.this.clear(); }
      public final Iterator<K> iterator()     { return new KeyIterator(); }
      public final boolean contains(Object o) { return containsKey(o); }
      public final boolean remove(Object key) {
          return removeNode(hash(key), key, null, false, true) != null;
      }
      public final Spliterator<K> spliterator() {
          return new KeySpliterator<>(HashMap.this, 0, -1, 0, 0);
      }
      public final void forEach(Consumer<? super K> action) {
          Node<K,V>[] tab;
          if (action == null)
              throw new NullPointerException();
          if (size > 0 && (tab = table) != null) {
              int mc = modCount;
              for (int i = 0; i < tab.length; ++i) {
                  for (Node<K,V> e = tab[i]; e != null; e = e.next)
                      action.accept(e.key);
              }
              if (modCount != mc)
                  throw new ConcurrentModificationException();
          }
      }
    } // keySet 
    
    public Collection<V> values() {
      Collection<V> vs = values;
      if (vs == null) {
          vs = new Values();
          values = vs;
      }
      return vs;
    } // 获取所有值的集合
    
    public Set<Map.Entry<K,V>> entrySet() {
      Set<Map.Entry<K,V>> es;
      return (es = entrySet) == null ? (entrySet = new EntrySet()) : es;
    } // 获取所有项的Set集合

    5.两种遍历数据的方式

    1.效率高的遍历方式:

    Map map = new HashMap();
    Iterator iter = map.entrySet().iterator();
    while (iter.hasNext()) {
    Map.Entry entry = (Map.Entry) iter.next();
    Object key = entry.getKey();
    Object val = entry.getValue();
    }

    2.效率低的遍历方式,就是现获取所有的键值,然后根据键,再调用get方法取得值:

    Map map = new HashMap();
    Iterator iter = map.keySet().iterator();
    while (iter.hasNext()) {
    Object key = iter.next();
    Object val = map.get(key);
    }

    此种方式,大多数人容易使用。

总结

  • HashMap最多只允许一条记录的键为null,允许多条记录的值为null。
  • HashMap非线程安全,即任一时刻可以有多个线程同时写HashMap,可能会导致数据的不一致。如果需要满足线程安全,
    可以用 Collections的synchronizedMap方法使HashMap具有线程安全的能力,或者使用ConcurrentHashMap。
  • 从结构实现来讲,HashMap是数组+链表+红黑树(JDK1.8增加了红黑树部分)实现的。
  • HashMap就是使用哈希表来存储的。哈希表为解决冲突,可以采用开放地址法和链地址法等来解决问题,Java中HashMap采用了链地址法。
  • 负载因子是可以修改的,也可以大于1,但是建议不要轻易修改,除非情况非常特殊。
  • 在多线程环境下,HashMap是不安全的。此时,必须使用其他工具对其加锁(Map m = Collections.synchronizedMap(new HashMap(…)))
    或使用其替代技术。
  • JDK1.8和JDK1.7中HashMap实现方式不一样,性能提高了,还有红黑树(好难懂)。
阅读更多
个人分类: Java
想对作者说点什么? 我来说一句

没有更多推荐了,返回首页

不良信息举报

JDK集合 - HashMap源码阅读

最多只允许输入30个字

加入CSDN,享受更精准的内容推荐,与500万程序员共同成长!
关闭
关闭