HashMap详解数据结构(对元素增删查改)

编程

1、HashMap

JDK1.8之前

JDK1.8之前HashMap由数组+链表组成的,数组是HashMap的主体,链表则是主要为了解决哈希冲突而存在的(“拉链法”解决冲突)

所谓扰动函数指的就是 HashMap 的 hash 方法。使用 hash 方法也就是扰动函数是为了防止一些实现比较差的 hashCode() 方法 换句话说使用扰动函数之后可以减少碰撞。

所谓 “拉链法” 就是:将链表和数组相结合。也就是说创建一个链表数组,数组中每一格就是一个链表。若遇到哈希冲突,则将冲突的值加到链表中即可

JDK1.8

jdk1.8在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)时,将链表转化为红黑树,以减少搜索时间。

2、HashMap源码解读

如何读源码:

  • 根据原理(HashMap 数组+链表):列出问题,然后把问题解决(数组如何初始化,如何扩容,节点存放依据?)

  • 入口方法,构造方法初始化(有四个构造方法):

      // 指定容器大小和加载因子

    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);

      }

      /**

        * 默认

        * Constructs an empty <tt>HashMap</tt> with the default initial capacity

        * (16) and the default load factor (0.75).

        */

      public HashMap() {

          this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted

      }

      // 包含另一个Map

      public HashMap(Map<? extends K, ? extends V> m) {

          this.loadFactor = DEFAULT_LOAD_FACTOR;

          putMapEntries(m, false);

      }

     

  • put(添加元素)流程:

    对putVal方法添加元素的分析如下:

    • ①如果定位到的数组位置没有元素 就直接插入。

    • ②如果定位到的数组位置有元素就和要插入的key比较,如果key相同就直接覆盖,如果key不相同,就判断p是否是一个树节点,如果是就调用e = ((TreeNode)p).putTreeVal(this, tab, hash, key, value)将元素添加进入。如果不是就遍历链表插入(插入的是链表尾部)。

    public V put(K key, V value) {

      return putVal(hash(key), key, value, false, true);

    }

    /**

    * Implements Map.put and related methods

    *

    * @param hash hash for key

    * @param key the key

    * @param value the value to put

    * @param onlyIfAbsent if true, don"t change existing value

    * @param evict if false, the table is in creation mode.

    * @return previous value, or null if none

    */

    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,

                      boolean evict) {

      Node<K,V>[] tab; Node<K,V> p; int n, i;

      // table未初始化或者长度为0,进行扩容

      if ((tab = table) == null || (n = tab.length) == 0)

          n = (tab = resize()).length;

      // (n - 1) & hash 确定元素放在哪个桶中,桶为空,新生成结点放入桶中(这个结点是放在数组中)

      if ((p = tab[i = (n - 1) & hash]) == null)

          tab[i] = newNode(hash, key, value, null);

      // 桶中已经存在元素

      else {

          Node<K,V> e; K k;

          // 比较桶中第一个元素(数组中的结点)的hash值相等,key相等

          if (p.hash == hash &&

              ((k = p.key) == key || (key != null && key.equals(k))))

                  // 将第一个元素赋值给e,用e来记录

                  e = p;

          // hash值不相等,即key不相等;为红黑树结点

          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;

                  }

                  // 判断链表中结点的key值与插入的元素的key值是否相等

                  if (e.hash == hash &&

                      ((k = e.key) == key || (key != null && key.equals(k))))

                      // 相等,跳出循环

                      break;

                  // 用于遍历桶中的链表,与前面的e = p.next组合,可以遍历链表

                  p = e;

              }

          }

          // 表示在桶中找到key值、hash值与插入元素相等的结点

          if (e != null) {

              // 记录e的value

              V oldValue = e.value;

              // onlyIfAbsent为false或者旧值为null

              if (!onlyIfAbsent || oldValue == null)

                  //用新值替换旧值

                  e.value = value;

              // 访问后回调

              afterNodeAccess(e);

              // 返回旧值

              return oldValue;

          }

      }

      // 结构性修改

      ++modCount;

      // 实际大小大于阈值则扩容

      if (++size > threshold)

          resize();

      // 插入后回调

      afterNodeInsertion(evict);

      return null;

    }

     

  • resize 扩容方法(略)
  • // hash 值

    static final int hash(Object key) {

        // key.hashCode():返回散列值也就是hashcode

        // ^ :按位异或

        // >>>:无符号右移,忽略符号位,空位都以0补齐

      int h;

      return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);

    }

3、HashMap 的长度为什么是2的幂次方

为了能让 HashMap 存取高效,尽量较少碰撞,也就是要尽量把数据分配均匀,Hash 值的范围值-2147483648到2147483647,前后加起来大概40亿的映射空间,只要哈希函数映射得比较均匀松散,一般应用是很难出现碰撞的。但问题是一个40亿长度的数组,内存是放不下的。所以这个散列值是不能直接拿来用的。用之前还要先做对数组的长度取模运算,得到的余数才能用来要存放的位置也就是对应的数组下标。这个数组下标的计算方法是“ (n - 1) & hash”。(n代表数组长度)。这也就解释了 HashMap 的长度为什么是2的幂次方。

4、如何选用集合

主要根据集合的特点来选用,比如并发环境下推荐使用 ConcurrentHashMap ,需要根据键值获取到元素值时就选用Map接口下的集合,需要排序时选择TreeMap,不需要排序时就选择HashMap,需要保证线程安全就选用ConcurrentHashMap.当我们只需要存放元素值时,就选择实现Collection接口的集合,需要保证元素唯一时选择实现Set接口的集合比如TreeSet或HashSet,不需要就选择实现List接口的比如ArrayList或LinkedList,然后再根据实现这些接口的集合的特点来选用。

[]

 

善良一点,因为每个人都在和人生苦战!

 

以上是 HashMap详解数据结构(对元素增删查改) 的全部内容, 来源链接: utcz.com/z/514240.html

回到顶部