Java集合

一、概述

          JAVA中有许多的集合,常用的有List,Set,Queue,Map。

          其中List,Set,Queue都是Collection(集合),其每个元素都是单独的一个对象,如List<String>,Set<Integer>等,String和Integer就是单独的一个对象。

          而Map是一种图,其每个元素都是两个对象的一一对应,如Map<Integer, String>中的Integer是键 (key),String是这个键所对应的值(value)。每个元素都是一对Integer和String。

          

         

        

二、集合和数组的区别

          

三、Collection集合的方法

          

四、List和Set集合详解

          list和set的区别:

          

       (1)ArrayList:底层数据结构是数组,查询快,增删慢,线程不安全,效率高,可以存储重复元素

       (2)LinkedList 底层数据结构是链表,查询慢,增删快,线程不安全,效率高,可以存储重复元素

       (3)Vector:底层数据结构是数组,查询快,增删慢,线程安全,效率低,可以存储重复元素

         

         Array即数组,顾名思义,ArrayList是用数组实现的一个List(列表)。它实现了List接口声明的的所有方法,而且允许加入包括null在内的所有元素。除了实现列表接口之外,这个类还提供了一些方法来操作内部用于存储列表的数组的大小。

         在每一个ArrayList实例中,都有专门保存容量的capacity属性,我在jdk1.8中找到了它的默认容量大小是

         DEFAULT_CAPACITY = 10,如下是jdk1.8中对DEFAULT_CAPACITY的定义

         

public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable{
    /*
        some code
    */
 
    private static final int DEFAULT_CAPACITY = 10;
 
    /*
        some code
    */
}

          接下来讨论ArrayList中对capacity的控制

          举个栗子,ArrayList的add()方法,首先需要说明几个变量的含义,我从jdk1.8中找来了这些变量的声明语句包括其注释:

          

public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
    /*
        some code
    */
 
    /**
     * Default initial capacity.
     */
    private static final int DEFAULT_CAPACITY = 10;
 
    /**
     * Shared empty array instance used for empty instances.
     */
    private static final Object[] EMPTY_ELEMENTDATA = {};
 
    /**
     * Shared empty array instance used for default sized empty instances. We
     * distinguish this from EMPTY_ELEMENTDATA to know how much to inflate when
     * first element is added.
     */
    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
 
    /**
     * The array buffer into which the elements of the ArrayList are stored.
     * The capacity of the ArrayList is the length of this array buffer. Any
     * empty ArrayList with elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA
     * will be expanded to DEFAULT_CAPACITY when the first element is added.
     */
    transient Object[] elementData; // non-private to simplify nested class access
 
    /**
     * The size of the ArrayList (the number of elements it contains).
     *
     * @serial
     */
    private int size;
 
    /*
        some code
    */
}

           其中各个变量的含义如下:

private static final int DEFAULT_CAPACITY = 10;

是默认的容量大小。当不指定容量大小的时候,就是用默认容量10

private static final Object[] EMPTY_ELEMENTDATA = {};

private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

这两个作用类似,在创建一个新的ArrayList对象的时候引用。初始化ArrayList中的数组(即elementData变量)。

两个Object数组本身并没有区别,定义这样没区别的两个是为了区分使用的是哪种构造方法。在ArrayList()方法中用的是DEFAULTCAPACITY_EMPTY_ELEMENTDATA,而在ArrayList(int initialCapacity)和ArrayList(Collection<? extends E> c)方法中用的是EMPTY_ELEMENTDATA。

 

transient Object[] elementData;

tip:这里有一个transient关键字,是关于序列化和反序列化的。简单说一下,序列化就是把对象转换成字节序列的形式,这些字节序列包含了对象的数据和信息,一个序列化后的对象可以被写到数据库或文件中,也可以用于网络传输。一般当我们使用缓存cache(内存空间不够有可能会本地存储到硬盘)或远程调用rpc(网络传输)的时候,经常需要让我们的实体类实现Serializable接口,目的就是为了让其可序列化。序列化之后再反序列化就能得到原来的java对象。而transient修饰的数据将不会被自动序列化。但是,elementData会被writeObject方法手动序列化,这个不多说了。

这个变量就是ArrayList中的Array,ArrayList中的元素就是保存在这里。

private int size;

数组中元素的个数,当我们调用size()方法的时候返回的就是这个size。跟容量(capacity)是两个不同的概念,不要混淆。

protected transient int modCount = 0;

还有一个继承自其父类AbstractList的属性,modCount,表示发生结构化改变的次数。关于结构化改变(Structural modifications),AbstractList中是这样子解释的

The number of times this list has been structurally modified. Structural modifications are those that change the size of the list, or otherwise perturb it in such a fashion that iterations in progress may yield incorrect results.

意即当size改变了,这个modCount就会+1。

测试代码是这样的:

public class Test{
  public static void main(String[] args) {
    List<String> list = new ArrayList();
    for(int i = 0; i <= 10; i++){
        //向list中依次添加“ii”。
        //例如,当i为0时,向list添加"00";当i为1时,向list添加"11"。
        StringBuilder sb = new StringBuilder();
        sb.append(i)
          .append(i);
        list.add(sb.toString());
    }
  }
}

运行结果是ArrayList的size为11,容量为15

下面是ArrayList在执行add()方法的时候所涉及的方法,我给放在一个页面里方便查找。

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable{
    /*
        some code
    */
 
    public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }
 
    /*
        some code
    */
 
    private static int calculateCapacity(Object[] elementData, int minCapacity) {
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            return Math.max(DEFAULT_CAPACITY, minCapacity);
        }
        return minCapacity;
    }
 
    private void ensureCapacityInternal(int minCapacity) {
        ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
    }
 
    private void ensureExplicitCapacity(int minCapacity) {
        modCount++;
 
        // overflow-conscious code
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }
    
    /*
        some code
    */
 
    private void grow(int minCapacity) {
        // overflow-conscious code
        int oldCapacity = elementData.length;
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);
        // minCapacity is usually close to size, so this is a win:
        elementData = Arrays.copyOf(elementData, newCapacity);
    }
 
    /*
        some code
    */
 
    public boolean add(E e) {
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        elementData[size++] = e;
        return true;
    }
 
    /*
        some code
    */
}

当i = 0时,此时是第一次向list中添加元素,elementData容量不足,需要加大elementData的容量。初始的list是一个空的Object[],依次调用add(), ensureCapacityInternal(), ensureExplicitCapacity(), grow(), calculateCapacity()方法。

在calculateCapacity()方法中,由于list是由ArrayList的无参构造方法构造的,所以elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA的结果为true,执行语句:return Math.max(DEFAULT_CAPACITY, minCapacity);会得到DEFAULT_CAPACITY即10。

返回的结果交给ensureExplicitCapacity(),modCount先自增一次,表示list必然会做一次结构性修改。此时minCapacity - elementData.length > 0结果为true(nimCapacity = 10, elementData.length = 0),执行grow(minCapacity);

在grow()方法中,oldCapacity = 0,newCapacity = 0,minCapacity = 10。(oldCapacity >> 1相当于oldCapacity / 2)故会进入if (newCapacity - minCapacity < 0)条件判断体中的语句,执行newCapacity = minCapacity,令newCapacity = 10,再执行Array.copyOf()方法,将elementData扩充到容量为10。

之后,回到add()方法体中,elementData[size++] = e; 将elemtntData[0]的值赋为"00",然后size再将赋值为1。

可见,扩容发生在grow()方法中,而在ensureExplicitCapacity()方法中决定是否要扩容。

i = 1~9的过程中,elementData的容量都没有发生改变。不做叙述。

当i = 10时,容量为10的elementData已经被填充满了,需要再次扩容,经过之前提到的方法调用顺序,得到新的容量应该为15。进行扩容。

set集合

不包含重复元素的集合。更确切地说,是不同时包含使得e1.equals(e2)成立的e1和e2(因为e1与e2的equals()逻辑可以由使用者自己定义)。并且最多包含一个空元素。这个接口是数学集合的一个抽象建模。
注意:如果可变的(mutable)对象用作集合元素,则必须非常小心。当一个对象是集合中的元素时,以影响equals()比较结果的方式更改对象的值,则无法预测集合的行为。有一个特殊情况是,不允许一个集合作为一个元素来包含它自己。所以还是尽量使用immutable的对象作为Set的元素。

HashSet是基于HashMap实现的,所以需要先理解HashMap。

在HashSet中,保存数据的变量名为map:

private transient HashMap<E,Object> map; 

一个HashMap,map的key是E类型,即HashSet中的元素;每个key的value是Object类型,这是HashSet类中定义的一个常量,名为PRESENT

// Dummy value to associate with an Object in the backing Map
private static final Object PRESENT = new Object();

这只是为了使用HashMap而创建的一个Object对象,用来占掉value的位置,无其他意义 。

HashSet不保证集合的迭代顺序;具体来说,它不保证顺序将随时间保持不变(除非你自己实现一个迭代器去保证迭代顺序)。它的一些基本操作(add()、remove()、contains()和size())的时间复杂度是常量级的,前提是散列函数在存储桶中正确地分散元素(即要把hashCode()写好,让equals()方法判定为相等的两个对象的hashCode也相等,尽量让equals()方法判定为不等的两个对象的hashCode不等)。

HashSet底层数据结构采用哈希表实现,元素无序且唯一,线程不安全,效率高,可以存储null元素,元素的唯一性是靠所存储元素类型是否重写hashCode()和equals()方法来保证的,如果没有重写这两个方法,则无法保证元素的唯一性。
具体实现唯一性的比较过程:存储元素首先会使用hash()算法函数生成一个int类型hashCode散列值,然后已经的所存储的元素的hashCode值比较,如果hashCode不相等,则所存储的两个对象一定不相等,此时存储当前的新的hashCode值处的元素对象;如果hashCode相等,存储元素的对象还是不一定相等,此时会调用equals()方法判断两个对象的内容是否相等,如果内容相等,那么就是同一个对象,无需存储;如果比较的内容不相等,那么就是不同的对象,就该存储了,此时就要采用哈希的解决地址冲突算法,在当前hashCode值处类似一个新的链表, 在同一个hashCode值的后面存储存储不同的对象,这样就保证了元素的唯一性。
Set的实现类的集合对象中不能够有重复元素,HashSet也一样他是使用了一种标识来确定元素的不重复,HashSet用一种算法来保证HashSet中的元素是不重复的, HashSet采用哈希算法,底层用数组存储数据。默认初始化容量16,加载因子0.75。
Object类中的hashCode()的方法是所有子类都会继承这个方法,这个方法会用Hash算法算出一个Hash(哈希)码值返回,HashSet会用Hash码值去和数组长度取模, 模(这个模就是对象要存放在数组中的位置)相同时才会判断数组中的元素和要加入的对象的内容是否相同,如果不同才会添加进去。

五、List和Set总结

        (1)、List,Set都是继承自Collection接口,Map则不是

        (2)、List特点:元素有放入顺序,元素可重复 ,Set特点:元素无放入顺序,元素不可重复,重复元素会覆盖掉,(注意:元素虽然无放入顺序,但是元素在set中的位置是有该元素的HashCode决定的,其位置其实是固定的,加入Set 的Object必须定义equals()方法 ,另外list支持for循环,也就是通过下标来遍历,也可以用迭代器,但是set只能用迭代,因为他无序,无法用下标来取得想要的值。)

        (3).Set和List对比:

          Set:检索元素效率低下,删除和插入效率高,插入和删除不会引起元素位置改变。

          List:和数组类似,List可以动态增长,查找元素效率高,插入删除元素效率低,因为会引起其他元素位置改变。

        (4)、ArrayList与LinkedList的区别和适用场景

          Arraylist:

          优点:ArrayList是实现了基于动态数组的数据结构,因为地址连续,一旦数据存储好了,查询操作效率会比较高(在内存里是连着放的)。

          缺点:因为地址连续, ArrayList要移动数据,所以插入和删除操作效率比较低。

          LinkedList:

         优点:LinkedList基于链表的数据结构,地址是任意的,所以在开辟内存空间的时候不需要等一个连续的地址,对于新增和删除操作add和remove,LinedList比较占优势。LinkedList 适用于要头尾操作或插入指定位置的场景

         缺点:因为LinkedList要移动指针,所以查询操作性能比较低。

         适用场景分析

         当需要对数据进行对此访问的情况下选用ArrayList,当需要对数据进行多次增加删除修改时采用LinkedList。

         ArrayList与Vector的区别和适用场景

         ArrayList有三个构造方法:

public ArrayList(int initialCapacity)//构造一个具有指定初始容量的空列表。    
public ArrayList()      //默认构造一个初始容量为10的空列表。    
public ArrayList(Collection<? extends E> c)//构造一个包含指定 collection 的元素的列表

         Vector有四个构造方法:

public Vector()//使用指定的初始容量和等于0的容量增量构造一个空向量。    
public Vector(int initialCapacity)//构造一个空向量,使其内部数据数组的大小,其标准容量增量为零。    
public Vector(Collection<? extends E> c)//构造一个包含指定 collection 中的元素的向量    
public Vector(int initialCapacity,int capacityIncrement)//使用指定的初始容量和容量增量构造一个空的向量

        ArrayList和Vector都是用数组实现的,主要有这么三个区别:

  1. Vector是多线程安全的,线程安全就是说多线程访问同一代码,不会产生不确定的结果。而ArrayList不是,这个可以从源码中看出,Vector类中的方法很多有synchronized进行修饰,这样就导致了Vector在效率上无法与ArrayList相比;
  2. 两个都是采用的线性连续空间存储元素,但是当空间不足的时候,两个类的增加方式是不同。
  3. Vector可以设置增长因子,而ArrayList不可以。
  4. Vector是一种老的动态数组,是线程同步的,效率很低,一般不赞成使用。

       适用场景分析

  1. Vector是线程同步的,所以它也是线程安全的,而ArrayList是线程异步的,是不安全的。如果不考虑到线程的安全因素,一般用ArrayList效率比较高。
  2. 如果集合中的元素的数目大于目前集合数组的长度时,在集合中使用数据量比较大的数据,用Vector有一定的优势。

      HashSet和TreeSet:

  1. TreeSet 是二差树(红黑树的树据结构)实现的,Treeset中的数据是自动排好序的,不允许放入null值
  2. HashSet 是哈希表实现的,HashSet中的数据是无序的,可以放入null,但只能放入一个null,两者中的值都不能重复,就如数据库中唯一约束
  3. HashSet要求放入的对象必须实现HashCode()方法,放入的对象,是以hashcode码作为标识的,而具有相同内容的String对象,hashcode是一样,所以放入的内容不能重复。但是同一个类的对象可以放入不同的实例

     适用场景分析:HashSet是基于Hash算法实现的,其性能通常都优于TreeSet。为快速查找而设计的Set,我们通常都应该使用HashSet,在我们需要排序的功能时,我们才使用TreeSet。

     何时使用:

     

六、HashMap

          背景:

          HashMap是Java程序员使用频率最高的用于映射(键值对)处理的数据类型。随着JDK(Java Developmet Kit)版本的更新,JDK1.8对HashMap底层的实现进行了优化,例如引入红黑树的数据结构和扩容的优化等。

          Java为数据结构中的映射定义了一个接口java.util.Map,此接口主要有四个常用的实现类,分别是HashMap、Hashtable、LinkedHashMap和TreeMap,类继承关系如下图所示:

          

          (1) HashMap:它根据键的hashCode值存储数据,大多数情况下可以直接定位到它的值,因而具有很快的访问速度,但遍历顺序却是不确定的。 HashMap最多只允许一条记录的键为null,允许多条记录的值为null。HashMap非线程安全,即任一时刻可以有多个线程同时写HashMap,可能会导致数据的不一致。如果需要满足线程安全,可以用 Collections的synchronizedMap方法使HashMap具有线程安全的能力,或者使用ConcurrentHashMap。

          (2) Hashtable:Hashtable是遗留类,很多映射的常用功能与HashMap类似,不同的是它承自Dictionary类,并且是线程安全的,任一时间只有一个线程能写Hashtable,并发性不如ConcurrentHashMap,因为ConcurrentHashMap引入了分段锁。Hashtable不建议在新代码中使用,不需要线程安全的场合可以用HashMap替换,需要线程安全的场合可以用ConcurrentHashMap替换。

          (3) LinkedHashMap:LinkedHashMap是HashMap的一个子类,保存了记录的插入顺序,在用Iterator遍历LinkedHashMap时,先得到的记录肯定是先插入的,也可以在构造时带参数,按照访问次序排序。

          (4) TreeMap:TreeMap实现SortedMap接口,能够把它保存的记录根据键排序,默认是按键值的升序排序,也可以指定排序的比较器,当用Iterator遍历TreeMap时,得到的记录是排过序的。如果使用排序的映射,建议使用TreeMap。在使用TreeMap时,key必须实现Comparable接口或者在构造TreeMap传入自定义的Comparator,否则会在运行时抛出java.lang.ClassCastException类型的异常。

           存储结构:

           从结构实现来讲,HashMap是数组+链表+红黑树(JDK1.8增加了红黑树部分)实现的,如下图所示。

           

          HashMap就是使用哈希表来存储的。哈希表为解决冲突,可以采用开放地址法和链地址法等来解决问题,Java中HashMap采用了链地址法。链地址法,简单来说,就是数组加链表的结合。在每个数组元素上都一个链表结构,当数据被Hash后,得到数组下标,把数据放在对应下标元素的链表上。

          HashMap的put方法:

          

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;
    // 1.校验table是否为空或者length等于0,如果是则调用resize方法进行初始化
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    // 2.通过hash值计算索引位置,将该索引位置的头节点赋值给p,如果p为空则直接在该索引位置新增一个节点即可
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    else {
        // table表该索引位置不为空,则进行查找
        Node<K,V> e; K k;
        // 3.判断p节点的key和hash值是否跟传入的相等,如果相等, 则p节点即为要查找的目标节点,将p节点赋值给e节点
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            e = p;
        // 4.判断p节点是否为TreeNode, 如果是则调用红黑树的putTreeVal方法查找目标节点
        else if (p instanceof TreeNode)
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        else {
            // 5.走到这代表p节点为普通链表节点,则调用普通的链表方法进行查找,使用binCount统计链表的节点数
            for (int binCount = 0; ; ++binCount) {
                // 6.如果p的next节点为空时,则代表找不到目标节点,则新增一个节点并插入链表尾部
                if ((e = p.next) == null) {
                    p.next = newNode(hash, key, value, null);
                    // 7.校验节点数是否超过8个,如果超过则调用treeifyBin方法将链表节点转为红黑树节点,
                    // 减一是因为循环是从p节点的下一个节点开始的
                    if (binCount >= TREEIFY_THRESHOLD - 1)
                        treeifyBin(tab, hash);
                    break;
                }
                // 8.如果e节点存在hash值和key值都与传入的相同,则e节点即为目标节点,跳出循环
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                p = e;  // 将p指向下一个节点
            }
        }
        // 9.如果e节点不为空,则代表目标节点存在,使用传入的value覆盖该节点的value,并返回oldValue
        if (e != null) {
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            afterNodeAccess(e); // 用于LinkedHashMap
            return oldValue;
        }
    }
    ++modCount;
    // 10.如果插入节点后节点数超过阈值,则调用resize方法进行扩容
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict);  // 用于LinkedHashMap
    return null;
}

  

    ① 先获取类变量table赋给tab,table即为存储“桶”的数组,table为空或者长度为0就必须进行首次resize(扩容)操作

    ② 接下来通过(n - 1)&hash找到<key,value>在数组中的位置,即哪个“桶”,然后查找应该插入的位置。
·如果这个桶未被使用则newNode并放进去。

     ·如果这个桶已经被使用了,需要分三种情况讨论

      a) 桶的首个元素的key与待插入节点的key相同

      b) 桶的首个元素key与待插入节点的key不同且首个节点是红黑树节点,调用putTreeVal方法;

      c) 桶的首个元素key与待插入节点的key不同且首个节点是链表节点,继续执行本方法。在此过程中,如果发现链表长度大于8就调用treeifyBin方法将链表转为红黑树

    ③ 找到应该放置的位置后,如果onlyIfAbsent为false或value为null就更新值,如果onlyIfAbsent为true则拒绝更新

    ④ 更新size(键值对个数),并与threshold(capacity*loadFactor)进行比较,如果超过threshold就进行resize(扩容)操作

         HashMap 和 Hashtable 的区别:

  1. HashMap 允许 key 和 value 为 null,Hashtable 不允许。
  2. HashMap 的默认初始容量为 16,Hashtable 为 11。
  3. HashMap 的扩容为原来的 2 倍,Hashtable 的扩容为原来的 2 倍加 1。
  4. HashMap 是非线程安全的,Hashtable是线程安全的。
  5. HashMap 的 hash 值重新计算过,Hashtable 直接使用 hashCode。
  6. HashMap 去掉了 Hashtable 中的 contains 方法。
  7. HashMap 继承自 AbstractMap 类,Hashtable 继承自 Dictionary 类。

        总结:

  1. HashMap 的底层是个 Node 数组(Node<K,V>[] table),在数组的具体索引位置,如果存在多个节点,则可能是以链表或红黑树的形式存在。
  2. 增加、删除、查找键值对时,定位到哈希桶数组的位置是很关键的一步,源码中是通过下面3个操作来完成这一步:1)拿到 key 的 hashCode 值;2)将 hashCode 的高位参与运算,重新计算 hash 值;3)将计算出来的 hash 值与 “table.length - 1” 进行 & 运算。
  3. HashMap 的默认初始容量(capacity)是 16,capacity 必须为 2 的幂次方;默认负载因子(load factor)是 0.75;实际能存放的节点个数(threshold,即触发扩容的阈值)= capacity * load factor。
  4. HashMap 在触发扩容后,阈值会变为原来的 2 倍,并且会对所有节点进行重 hash 分布,重 hash 分布后节点的新分布位置只可能有两个:“原索引位置” 或 “原索引+oldCap位置”。例如 capacity 为16,索引位置 5 的节点扩容后,只可能分布在新表 “索引位置5” 和 “索引位置21(5+16)”。
  5. 导致 HashMap 扩容后,同一个索引位置的节点重 hash 最多分布在两个位置的根本原因是:1)table的长度始终为 2 的 n 次方;2)索引位置的计算方法为 “(table.length - 1) & hash”。HashMap 扩容是一个比较耗时的操作,定义 HashMap 时尽量给个接近的初始容量值。
  6. HashMap 有 threshold 属性和 loadFactor 属性,但是没有 capacity 属性。初始化时,如果传了初始化容量值,该值是存在 threshold 变量,并且 Node 数组是在第一次 put 时才会进行初始化,初始化时会将此时的 threshold 值作为新表的 capacity 值,然后用 capacity 和 loadFactor 计算新表的真正 threshold 值。
  7. 当同一个索引位置的节点在增加后达到 9 个时,并且此时数组的长度大于等于 64,则会触发链表节点(Node)转红黑树节点(TreeNode),转成红黑树节点后,其实链表的结构还存在,通过 next 属性维持。链表节点转红黑树节点的具体方法为源码中的 treeifyBin 方法。而如果数组长度小于64,则不会触发链表转红黑树,而是会进行扩容。
  8. 当同一个索引位置的节点在移除后达到 6 个时,并且该索引位置的节点为红黑树节点,会触发红黑树节点转链表节点。红黑树节点转链表节点的具体方法为源码中的 untreeify 方法。
  9. HashMap 在 JDK 1.8 之后不再有死循环的问题,JDK 1.8 之前存在死循环的根本原因是在扩容后同一索引位置的节点顺序会反掉。
  10. HashMap 是非线程安全的,在并发场景下使用 ConcurrentHashMap 来代替。

           

 

 

 

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值