文档章节

Java8 HashMap源码分析

EumJi
 EumJi
发布于 2018/01/21 10:18
字数 5395
阅读 1184
收藏 37

HashMap介绍

HashMap是基于hash表的map的实现,使用key-value形式存储键值对,并允许使用 null 值和 null 键,但是key只能有一个为null. Map不保证映射的顺序,其内部是根据hash值去模运算去排列的。HashMap内部使用entry数组作为存储的介质.

本文是基于Java8版本写的,因为Java8对HashMap有较大的改变,采用数组+链表+红黑树方式来记录键值对. 而且Java8还重写了resize方法,Java8之前很有可能造成扩容时,链表的循环问题.

源码解读

本文中的代码比较多,大多数的说明都在注释上体现了,所以可能文字上表述的不是很多,本文也尽可能介绍了一些java8中的新方法.需要注意的是其中很多地方都进行了修改和补充,看完本篇文章一定和之前看的Java7 HashMap有非常多的不同之处,也可以思考一下为什么Java8要做这么多改变.

Node介绍

Node是map的接口中的内部接口Map.Entry的实现类,用于存储HashMap中键值对的对象,是HashMap中非常重要的一个内部类,随便提一下,HashMap中有非常多的内部类,本文没做过多的介绍,读者可以自己翻看一下源码,因为篇幅实在太长了...在这里就简单的讲一下,大部分的内部类都是用于集合操作的,如keySet,values,entrySet等方法.

内部组成

static class Node<K,V> implements Map.Entry<K,V> {
//key是不可变的
final K key;
//value
V value;
//指向下一个entry对象,具体作用后面介绍
Node<K,V> next;
//hash值
int hash;
}

HashMap组成

HashMap主要参数

static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; //默认初始容量
static final int MAXIMUM_CAPACITY = 1 << 30; //最大容量
static final float DEFAULT_LOAD_FACTOR = 0.75f; //默认负载因子,相当于只有多少可用
transient Node<K,V>[] table;//要存储值的hash表
transient int size; //实际大小
int threshold; //阈值
final float loadFactor;//负载因子
transient int modCount;  //记录操作次数的

相比之前版本的HashMap,Java8更钟爱与位操作. >> 表示逻辑右移 >>> 表示无符号逻辑右移.高位补零

HashMap构造方法

//为空时候使用默认分配的大小16,负载因子0.75f,默认的容量为12,当size>threshold默认容量时候就会去扩容
public HashMap() {
      this.loadFactor = DEFAULT_LOAD_FACTOR;
  }
//构造方法 初始化负载因子和阈值
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);
}

HashMap方法

本文主要挑选几个比较常用且重要的方法进行分析.因为这写方法已经涵盖了大部分的方法调用关系.

hash方法 使用hashcode 异或 hashcode右移16位 得到hash值,等同于高位都取零 随带提一句,int类型的hashcode就是本身.

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

put方法解析 put方法,是HashMap中非常重要的方法,其中牵涉到了非常多的知识点,需要仔细的阅读.


public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}
/**
 * onlyIfAbsent 是否替换,为true时,如果存在值则替换
 * evict 主要用于LinkedHashMap移除最近未使用的节点
 */
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;
    //如果不存在,直接put
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    else {
        Node<K,V> e; K k;
        //根据条件获取不同的node节点,有可能tab[i]就是存在的元素
        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;
            //用于linkedHashMap,本文不做介绍
            afterNodeAccess(e);
            return oldValue;
        }
    }
    //添加成功,判断是否要扩容
    ++modCount;
    if (++size > threshold)
        resize();
    //用于linkedHashMap
    afterNodeInsertion(evict);
    return null;
}

下面详细介绍一下扩容的方法.看起来非常庞大.其实逻辑不复杂

/**
 * 有几种情况
 * 1.当为空的时候,也就是没有初始化的时候
 * 2.当到达最大值时候
 * 3.普通扩容时候
 */
final Node<K,V>[] resize() {
    Node<K,V>[] oldTab = table;
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    int oldThr = threshold;
    int newCap, newThr = 0;
    //存在旧值
    if (oldCap > 0) {
        //大于2<<30 最大容量设置为2<<32 - 1
        if (oldCap >= MAXIMUM_CAPACITY) {
            //但是不移动.,没有空间移动
            threshold = Integer.MAX_VALUE;
            return oldTab;
        }
        //如果属于正常范围的扩容 容量 x2
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                oldCap >= DEFAULT_INITIAL_CAPACITY)
            newThr = oldThr << 1; // double threshold
    }
    //用户自己设定了初始化大小
    else if (oldThr > 0) {
        newCap = oldThr;
    }
    //如果没使用,使用默认值初始化
    else {
        newCap = DEFAULT_INITIAL_CAPACITY;
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }
    //用户自定义了map的初始化操作
    if (newThr == 0) {
        float ft = (float)newCap * loadFactor;
        newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                (int)ft : Integer.MAX_VALUE);
    }
    //新的容量
    threshold = newThr;
    @SuppressWarnings({"rawtypes","unchecked"})
    Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
    table = newTab;
    //如果是扩容操作
    if (oldTab != null) {
        //遍历map的数据
        for (int j = 0; j < oldCap; ++j) {
            Node<K,V> e;
            if ((e = oldTab[j]) != null) {
                oldTab[j] = null;
                //如果当前位置只有一个元素,直接移动到新的位置
                if (e.next == null)
                    newTab[e.hash & (newCap - 1)] = e;
                //如果是红黑树
                else if (e instanceof TreeNode)
                    ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                //如果没超过8个 是链表
                else { // preserve order
                    Node<K,V> loHead = null, loTail = null;
                    Node<K,V> hiHead = null, hiTail = null;
                    Node<K,V> next;
                    //此处的操作是这样子的 因为是扩容一倍的操作,所以与旧的容量进行与操作后只有两个值0 和 1
                    //如果是0就位置不变,如果是1就移动n+old的位置,
                    //个人认为这么做的好处是:
                    /**
                     * 1.不会像之前1.7发生循环依赖的问题
                     * 2.从概率的角度上来看可以均匀分配,(一般来说高位和低位比例差不多)
                     * 3.提高效率
                     */
                    do {
                        next = e.next;
                        //如果和旧容量位运算后的值是0,记得当前节点和存放在链表的尾部
                        if ((e.hash & oldCap) == 0) {
                            if (loTail == null)
                                loHead = e;
                            else
                                loTail.next = e;
                            loTail = e;
                        }
                        //同上
                        else {
                            if (hiTail == null)
                                hiHead = e;
                            else
                                hiTail.next = e;
                            hiTail = e;
                        }
                    } while ((e = next) != null);
                    //为0的还是存放在当前位置
                    if (loTail != null) {
                        loTail.next = null;
                        newTab[j] = loHead;
                    }
                    //为1的就放在扩容的j + oldCap那边去
                    if (hiTail != null) {
                        hiTail.next = null;
                        newTab[j + oldCap] = hiHead;
                    }
                }
            }
        }
    }
    return newTab;
}

因为不像Java8之前的HashMap有初始化操作,此处选择将初始化和扩容放在了一起,并且又增加了红黑树的概念,所以导致整个方法的判断次数非常多,也是这个方法比较庞大的主要原因.

值得一体的是,在扩容后重新计算位置的时候,对链表进行优化,有兴趣可以搜索一下HashMap导致cpu百分之百的问题 而在Java中通过巧妙的进行&操作,然后获得高位是为0还是1.最终移动的位置就是低位的链表留在原地,高位的放在index+oldsize的地方就可以了,不用为每一个元素计算hash值,然后移动到对应的位置,再判断是否是链表,是否需要转换成树的操作.如下所示.

hashcode: 1111111111111101212
oldcap:   0000000000000010000

很容易知道这个&操作之后就是为0,因为oldcap都是2的倍数,只有高位为1,所以通过&确认高位要比%取余高效. 此时在看一下上面的扩容操作也许就更清晰了.

下面介绍一些newNode方法.就是新建一个节点.可以思考一下为什么要把newNode抽离出来?文章末尾给答案.

Node<K,V> newNode(int hash, K key, V value, Node<K,V> next) {
    return new Node<>(hash, key, value, next);
}

添加节点到红黑树的方法是Java8中新添加的,需要满足链表的长度到8,才会转换成红黑树,其主要目的是防止某个下标处的链表太长,导致在找到的时候速度很慢,下面看一下实现

//尝试着往树节点添加值
final TreeNode<K,V> putTreeVal(HashMap<K,V> map, Node<K,V>[] tab,int h, K k, V v) {
    Class<?> kc = null;
    boolean searched = false;
    //找到根节点
    TreeNode<K,V> root = (parent != null) ? root() : this;
    for (TreeNode<K,V> p = root;;) {
        int dir, ph; K pk;
        if ((ph = p.hash) > h)
            dir = -1;
        else if (ph < h)
            dir = 1;
        //存在的话直接返回,用于替换
        else if ((pk = p.key) == k || (k != null && k.equals(pk)))
            return p;
        //判断节点类型是否相同,不相同
        else if ((kc == null &&
                  (kc = comparableClassFor(k)) == null) ||
                 (dir = compareComparables(kc, k, pk)) == 0) {
            //没有搜索过,搜索子节点,搜过了说明没有就跳过.
            if (!searched) {
                TreeNode<K,V> q, ch;
                searched = true;
                //去子节点去找
                if (((ch = p.left) != null &&(q = ch.find(h, k, kc)) != null) ||((ch = p.right) != null &&
                     (q = ch.find(h, k, kc)) != null))
                    return q;
            }
            //对比hash值,决定查找的方向
            dir = tieBreakOrder(k, pk);
        }

        TreeNode<K,V> xp = p;
        //找到子节点为空,就可以加进去,设置层级关系
        if ((p = (dir <= 0) ? p.left : p.right) == null) {
            Node<K,V> xpn = xp.next;
            TreeNode<K,V> x = map.newTreeNode(h, k, v, xpn);
            if (dir <= 0)
                xp.left = x;
            else
                xp.right = x;
            xp.next = x;
            x.parent = x.prev = xp;
            if (xpn != null)
                ((TreeNode<K,V>)xpn).prev = x;
            moveRootToFront(tab, balanceInsertion(root, x));
            return null;
        }
    }
}

这里简单的梳理一下流程. 1.从根节点查找,找到了返回,如果没找到,找字节点 2.判断往哪个方向去查找 3.如果不存在,在子节点末端添加新节点

下面再看一下树的split方法,主要是扩容操作,重新结算位置需要分裂树,之前讲过,扩容会根据和旧map容量进行&操作,移动高位为1的节点.并且验证新的节点列表是否需要重新转换成链表的形式.

final void split(HashMap<K,V> map, Node<K,V>[] tab, int index, int bit) {
    TreeNode<K,V> b = this;
    // 设置记录高低位的node,和链表一样都是计算高位是0还是1
    TreeNode<K,V> loHead = null, loTail = null;
    TreeNode<K,V> hiHead = null, hiTail = null;
    int lc = 0, hc = 0;
    for (TreeNode<K,V> e = b, next; e != null; e = next) {
        next = (TreeNode<K,V>)e.next;
        e.next = null;
        //还是和旧的容量做位运算,为0的放在lo中
        if ((e.hash & bit) == 0) {
            //判断是否为头部
            if ((e.prev = loTail) == null)
                loHead = e;
            else
                loTail.next = e;
            loTail = e;
            ++lc;
        }
        //获取为1的放在hi中
        else {
            if ((e.prev = hiTail) == null)
                hiHead = e;
            else
                hiTail.next = e;
            hiTail = e;
            ++hc;
        }
    }
    //lo链表的处理
    if (loHead != null) {
        //如果小于7,那么当做链表处理
        if (lc <= UNTREEIFY_THRESHOLD)
            tab[index] = loHead.untreeify(map);
        else {
            //转换成树
            tab[index] = loHead;
            if (hiHead != null) // (else is already treeified)
                loHead.treeify(tab);
        }
    }
    //同上
    if (hiHead != null) {
        if (hc <= UNTREEIFY_THRESHOLD)
            tab[index + bit] = hiHead.untreeify(map);
        else {
            tab[index + bit] = hiHead;
            if (loHead != null)
                hiHead.treeify(tab);
        }
    }
}
//把树转换成链表
final Node<K,V> untreeify(HashMap<K,V> map) {
    Node<K,V> hd = null, tl = null;
    for (Node<K,V> q = this; q != null; q = q.next) {
      Node<K,V> p = map.replacementNode(q, null);
      if (tl == null)
        hd = p;
      else
        tl.next = p;
      tl = p;
    }
    return hd;
}

转化成红黑树的详情本文就没详细介绍了,我相信很容易看懂. 这里借`美团点评技术团队`的一张图来展示一下put方法的流程

get方法

逻辑其实很简单,就是首先通过hashcode确认位置,然后分数据结构类型继续不同方式的查找

public V get(Object key) {
    Node<K,V> e;
    return (e = getNode(hash(key), key)) == null ? null : e.value;
}
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 &&((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;
}

remove方法

public V remove(Object key) {
    Node<K,V> e;
    return (e = removeNode(hash(key), key, null, false, true)) == null ?
        null : e.value;
}
/**
  * 1.寻找是否存在,如果存在分数据类型分别处理
  * 2.如果为树,稍微复杂一点,需要判断是否要转换成链表,然后就是树的移动
  */
final Node<K,V> removeNode(int hash, Object key, Object value,boolean matchValue, boolean movable) {
  Node<K,V>[] tab; Node<K,V> p; int n, index;
  //找到节点
  if ((tab = table) != null && (n = tab.length) > 0 &&(p = tab[index = (n - 1) & hash]) != null) {
      Node<K,V> node = null, e; K k; V v;
      if (p.hash == hash &&((k = p.key) == key || (key != null && key.equals(k))))
          node = p;
      else if ((e = p.next) != null) {
         //从树节点获取
          if (p instanceof TreeNode)
              node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
          else {
              do {
                  if (e.hash == hash &&
                      ((k = e.key) == key ||
                       (key != null && key.equals(k)))) {
                      node = e;
                      break;
                  }
                  p = e;
              } while ((e = e.next) != null);
          }
      }
      //找到了对应的节点
      if (node != null && (!matchValue || (v = node.value) == value ||
                           (value != null && value.equals(v)))) {
          //树节点处理 主要是两点,存在删除,删除了是否需要转换成链表
          if (node instanceof TreeNode)
              ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
          //一个元素
          else if (node == p)
              tab[index] = node.next;
          else
              //指向node的下一个
              p.next = node.next;
          ++modCount;
          --size;
          afterNodeRemoval(node);
          return node;
      }
  }
  return null;
}

本文就不看树的详细删除过程了,内容太多太长.可以自己想象一下树的删除过程. replace方法

@Override
public V replace(K key, V value) {
    Node<K,V> e;
    //找到节点替换
    if ((e = getNode(hash(key), key)) != null) {
        V oldValue = e.value;
        e.value = value;
        //与linkedHashMap有关
        afterNodeAccess(e);
        return oldValue;
    }
    return null;
}

从基本的方法中我们可以看出,最复杂的就是put方法,put方法设计非常多的方法,后续的get,replace,remove都是建立在put方法基础之上.

补充方法

上面介绍了几个基本的方法,另外现在介绍一些有用的小方法.都是在Java8新增的.

merge方法 merge方法的主要作用是如果不存在就进行添加,如果存在的话按照自己指定的remappingFunction进行操作,如果操作之后value为null的话删除元素,否则替换,下面是代码详情

public V merge(K key, V value,BiFunction<? super V, ? super V, ? extends V> remappingFunction) {
    if (value == null)
        throw new NullPointerException();
    if (remappingFunction == null)
        throw new NullPointerException();
    int hash = hash(key);
    Node<K,V>[] tab; Node<K,V> first; int n, i;
    int binCount = 0;
    TreeNode<K,V> t = null;
    Node<K,V> old = null;
    //扩容操作
    if (size > threshold || (tab = table) == null ||(n = tab.length) == 0)
        n = (tab = resize()).length;
    //通过key找到元素,分为树和链表两种情况
    if ((first = tab[i = (n - 1) & hash]) != null) {
        if (first instanceof TreeNode)
            old = (t = (TreeNode<K,V>)first).getTreeNode(hash, key);
        else {
            Node<K,V> e = first; K k;
            do {
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k)))) {
                    old = e;
                    break;
                }
                ++binCount;
            } while ((e = e.next) != null);
        }
    }
    //存在旧的结点,进行合并操作
    if (old != null) {
        V v;
        if (old.value != null)
            //具体的合并操作
            v = remappingFunction.apply(old.value, value);
        else
            v = value;
        //合并成功,执行afterNodeAccess方法,子类linkedHashMap有用
        if (v != null) {
            old.value = v;
            afterNodeAccess(old);
        }
        //合并之后没有值,删除元素
        else
            removeNode(hash, key, null, false, true);
        return v;
    }
    //没有旧结点,直接添加,考虑扩容,链表和树的情况
    if (value != null) {
        if (t != null)
            t.putTreeVal(this, tab, hash, key, value);
        else {
            tab[i] = newNode(hash, key, value, first);
            if (binCount >= TREEIFY_THRESHOLD - 1)
                treeifyBin(tab, hash);
        }
        ++modCount;
        ++size;
        afterNodeInsertion(true);
    }
    return value;
}

compute方法

public V compute(K key,BiFunction<? super K, ? super V, ? extends V> remappingFunction) {
  //省略部分基本和merge方法的前半段一直,不重复展示

  //获取旧元素
  V oldValue = (old == null) ? null : old.value;
  //计算出新值
  V v = remappingFunction.apply(key, oldValue);
  //存在旧元素,旧赋值到旧元素上
  if (old != null) {
      if (v != null) {
          old.value = v;
          afterNodeAccess(old);
      }
      //计算结果为空则删除
      else
          removeNode(hash, key, null, false, true);
  }
  //r如果没有旧元素,但计算出的值不为空,添加操作,和merge方法相同,省略

  return v;
}

看到这里我相信大家都有疑问,为什么要放两个几乎相同的两个方法,

我们详细对比一下两个方法,发现会有几点不同:

  1. 参数不同,merge方法要求value不为null,compute方法没有要求;
  2. merge方法要求old.value也不为null.compute的方法依然没有要求.

另外compute还有两个扩展方法,简单介绍一下.

computeIfAbsent方法 如果oldvalue不为null就不替换,如果计算后的值不存在直接返回,否则如果old不为null,通过key计算出值替换,否则添加到map中


public V computeIfAbsent(K key,Function<? super K, ? extends V> mappingFunction) {
    //扩容等操作省略,同上
      V oldValue;
      if (old != null && (oldValue = old.value) != null) {
          afterNodeAccess(old);
          return oldValue;
      }
    //注意这里
    V v = mappingFunction.apply(key);
    if (v == null) {
        return null;
    } else if (old != null) {
        old.value = v;
        afterNodeAccess(old);
        return v;
    }
    //添加操作省略
}

computeIfPresent方法 不进行扩容的判断(因为不存在找不到就添加这样的操作).直接通过key查找,如果找到,且计算的结果不为null,替换,否则就直接删除

public V computeIfPresent(K key,BiFunction<? super K, ? super V, ? extends V> remappingFunction) {
        if (remappingFunction == null)
            throw new NullPointerException();
        Node<K,V> e; V oldValue;
        int hash = hash(key);
        if ((e = getNode(hash, key)) != null &&
            (oldValue = e.value) != null) {
            //注意这里
            V v = remappingFunction.apply(key, oldValue);
            if (v != null) {
                e.value = v;
                afterNodeAccess(e);
                return v;
            }
            else
                removeNode(hash, key, null, false, true);
        }
        return null;
    }

再次总结一下三个compute方法的异同点

  1. compute方法和computeIfPresent方法都需要oldValue,computeIfAbsent不需要
  2. compute的remappingFunction.apply方法不限制oldvalue是否为null,但是computeIfPresent限制必须不为null才进行,computeIfAbsent方法必须要old或者oldvalue为null才会进行后续操作
  3. computeIfPresent只有oldvalue存在则进行apply方法,然后根据条件替换或者删除操作,而compute和computeIfAbsent方法则是如果old不存在,还会根据条件额外进行添加操作

简单点说就是:

  1. 如果是不存在oldvalue才进行操作,这也可以认为我们在声明了String a = null这样的操作,现在需要进行初始化,选择computeIfAbsent方法,
  2. 如果必须存在oldvalue才操作,而且只进行删除或者修改的操作则选择computeIfPresent方法,类似看看还有没有修改的价值,没价值就干掉了.
  3. 其他选择compute方法

快速失败和安全失败问题

一:快速失败(fail—fast)
    在用迭代器遍历一个集合对象时,如果遍历过程中对集合对象的内容进行了修改(增加、删除、修改),则会抛出Concurrent Modification Exception。
    原理:迭代器在遍历时直接访问集合中的内容,并且在遍历过程中使用一个 modCount 变量。集合在被遍历期间如果内容发生变化,就会改变modCount的值。每当迭代器使用hashNext()/next()遍历下一个元素之前,都会检测modCount变量是否为expectedmodCount值,是的话就返回遍历;否则抛出异常,终止遍历。
   注意:这里异常的抛出条件是检测到 modCount!=expectedmodCount 这个条件。如果集合发生变化时修改modCount值刚好又设置为了expectedmodCount值,则异常不会抛出。因此,不能依赖于这个异常是否抛出而进行并发操作的编程,这个异常只建议用于检测并发修改的bug。
   场景:java.util包下的集合类都是快速失败的,不能在多线程下发生并发修改(迭代过程中被修改)。
二:安全失败(fail—safe)
   采用安全失败机制的集合容器,在遍历时不是直接在集合内容上访问的,而是先复制原有集合内容,在拷贝的集合上进行遍历。
   原理:由于迭代时是对原集合的拷贝进行遍历,所以在遍历过程中对原集合所作的修改并不能被迭代器检测到,所以不会触发Concurrent Modification Exception。
   缺点:基于拷贝内容的优点是避免了Concurrent Modification Exception,但同样地,迭代器并不能访问到修改后的内容,即:迭代器遍历的是开始遍历那一刻拿到的集合拷贝,在遍历期间原集合发生的修改迭代器是不知道的。
   场景:java.util.concurrent包下的容器都是安全失败,可以在多线程下并发使用,并发修改。

解答

解释一下刚才在文章设置的悬念,为什么要把newNode方法单独提出来,其实这里很大一部分原因是因为LinkedhashMap会需要重写此方法进行额外的操作.具体是什么可以自己查看一下源码,或者看我的另一篇文章(还没发布),map集合类总结.

总结

改版后的HashMap看起来更加的庞大和神秘了,因为相比之前看起来可能方法更大而且还有红黑树这个数据结构,看到代码就会让人感觉好难受,同时也呼吁每个人都不要写这么庞大的方法.尽可能将方法拆小.变得更加的简洁明了.但是Java8对HashMap的改变,使得HashMap在一定程度上提升了性能,并且还新填了不少的方法.

当然本文只是个人的一些看法,如果存在不足或者错误的地方,欢迎大家指正!!!

结语

与君共勉!!!

© 著作权归作者所有

EumJi
粉丝 5
博文 5
码字总数 9461
作品 0
深圳
程序员
私信 提问
加载中

评论(4)

EumJi
EumJi 博主

引用来自“地球上的码农”的评论

Linkedhashnap
已改正,谢谢提醒
地球上的码农
地球上的码农
Linkedhashnap
EumJi
EumJi 博主

引用来自“FrankNie”的评论

大小写问题还是更正一下吧:HashMap,看不下去
好的,感谢提醒
F
FrankNie
大小写问题还是更正一下吧:HashMap,看不下去
JDK1.8HashMap源码分析

HashMap和Hashtable的主要区别是: 1. Hashtable是线程安全,而HashMap则非线程安全,Hashtable的实现方法里面大部分都添加了synchronized关键字来确保线程同步,因此相对而言HashMap性能会高...

小小明童鞋
2018/10/16
88
2
HashMap在并发场景下踩过的坑

本文来自网易云社区 作者:张伟 关于HashMap在并发场景下的问题有很多人,很多公司遇到过!也很多人总结过,我们很多时候都认为这样都坑距离自己很远,自己一定不会掉入这样都坑。可是我们随...

网易云
2018/09/06
0
0
java集合框架(一):HashMap

有大半年没有写博客了,虽然一直有在看书学习,但现在回过来看读书基本都是一种知识“输入”,很多时候是水过无痕。而知识的“输出”会逼着自己去找出没有掌握或者了解不深刻的东西,你要把一...

chenzanjin
2017/10/25
197
2
Java8之HashMap源码分析

高级Java架构交流群:283943715 java高级交流群 一、前言   在分析jdk1.8后的HashMap源码时,发现网上好多分析都是基于之前的jdk,而Java8的HashMap对之前做了较大的优化,其中最重要的一个...

小炎哥
2017/06/19
604
1
集合源码学习(十一):LinkedHashMap

何为LinkedHashMap LinkedHashMap是一个,具有顺序的HashMap,也就是使用Iterator进行迭代时,顺序与put进来的顺序是一致的。 先看LinkedHashMap的定义: 如上,LinkedHashMap继承自HashMap...

anLA_
2017/10/20
0
0

没有更多内容

加载失败,请刷新页面

加载更多

nginx主备模式笔记

(1)两台服务器 192.168.17.129 和 192.168.17.131 (2)在两台服务器安装 keepalived 安装 keepalived (1)使用 yum 命令进行安装 yum install keepalived –y (2)安装之后,在 etc 里面...

行者终成事
今天
4
0
004-Docker镜像

Docker镜像 一个通用的私有仓库,可以提升效率 Docker镜像构建分为两种,一种是手动构建,一种是Dockerfile(自动构建) 基于centos镜像构建手动制作nginx镜像 docker run --name testdocker -...

伟大源于勇敢的开始
今天
5
0
OSChina 周一乱弹 —— 我就加班,不去世不休息

Osc乱弹歌单(2019)请戳(这里) 【今日歌曲】 @巴拉迪维 :《For Forever》90后那些小鲜肉歌手中,好像只有花花的歌能吸引我,这小家伙对音乐的感觉真是天才一般!#今日歌曲推荐# 《For F...

小小编辑
今天
9
1
【领会要领】web前端-轻量级框架应用(jQuery基础)

作者 | Jeskson 来源 | 达达前端小酒馆 jquery的安装和语法,jquery的多种选择器,dom操作和jquery事件。 jQuery框架,简介,优势,安装,语法,jQuery选择器,id选择器,类选择器,标记选择...

达达前端小酒馆
今天
6
0
MySQL 常用命令

无须死记硬背,直接 copy 就好。 1. 查看目前 mysql 用户 select user,host,password from mysql.user; 2. 修改 root 密码(使用内置函数修改) set password for root@localhost=password('y......

HuaiAnGG
昨天
6
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部