zoukankan      html  css  js  c++  java
  • java集合容器汇总

    Java 集合面试题

    说说常见的集合有哪些吧?

    HashMap说一下,其中的Key需要重写hashCode()和equals()吗?

    HashMap中key和value可以为null吗?允许几个为null呀?

    HashMap线程安全吗?ConcurrentHashMap和hashTable有什么区别?

    List和Set说一下,现在有一个ArrayList,对其中的所有元素按照某一属性大小排序,应该怎么做?

    ArrayList 和 Vector 的区别

    list 可以删除吗,遍历的时候可以删除吗,为什么

    面向对象语言对事物的体现都是以对象的形式,所以为了方便对多个对象的操作,需要将对象进行存储,集合就是存储对象最常用的一种方式,也叫容器。

    img

    从上面的集合框架图可以看到,Java 集合框架主要包括两种类型的容器

    • 一种是集合(Collection),存储一个元素集合
    • 另一种是图(Map),存储键/值对映射。

    Collection 接口又有 3 种子类型,List、Set 和 Queue,再下面是一些抽象类,最后是具体实现类,常用的有 ArrayList、LinkedList、HashSet、LinkedHashSet、HashMap、LinkedHashMap 等等。

    集合框架是一个用来代表和操纵集合的统一架构。所有的集合框架都包含如下内容:

    • 接口:是代表集合的抽象数据类型。例如 Collection、List、Set、Map 等。之所以定义多个接口,是为了以不同的方式操作集合对象

    • 实现(类):是集合接口的具体实现。从本质上讲,它们是可重复使用的数据结构,例如:ArrayList、LinkedList、HashSet、HashMap。

    • 算法:是实现集合接口的对象里的方法执行的一些有用的计算,例如:搜索和排序。这些算法被称为多态,那是因为相同的方法可以在相似的接口上有着不同的实现。


    说说常用的集合有哪些吧?

    Map 接口和 Collection 接口是所有集合框架的父接口:

    1. Collection接口的子接口包括:Set、List、Queue
    2. List是有序的允许有重复元素的Collection,实现类主要有:ArrayList、LinkedList、Stack以及Vector等
    3. Set是一种不包含重复元素且无序的Collection,实现类主要有:HashSet、TreeSet、LinkedHashSet等
    4. Map没有继承Collection接口,Map提供key到value的映射。实现类主要有:HashMap、TreeMap、Hashtable、ConcurrentHashMap 以及 Properties 等

    ArrayList 和 Vector 的区别

    相同点:

    • ArrayList 和 Vector 都是继承了相同的父类和实现了相同的接口(都实现了List,有序、允许重复和null)

      extends AbstractList<E>
              implements List<E>, RandomAccess, Cloneable, java.io.Serializable
      
    • 底层都是数组(Object[])实现的

    • 初始默认长度都为10

    不同点:

    • 同步性:Vector 中的 public 方法多数添加了 synchronized 关键字、以确保方法同步、也即是 Vector 线程安全、ArrayList 线程不安全

    • 性能:Vector 存在 synchronized 的锁等待情况、需要等待释放锁这个过程、所以性能相对较差

    • 扩容大小:ArrayList在底层数组不够用时在原来的基础上扩展 0.5 倍,Vector默认是扩展 1 倍

      扩容机制,扩容方法其实就是新创建一个数组,然后将旧数组的元素都复制到新数组里面。其底层的扩容方法都在 grow() 中(基于JDK8)

      • ArrayList 的 grow(),在满足扩容条件时、ArrayList以1.5 倍的方式在扩容(oldCapacity >> 1 ,右移运算,相当于除以 2,结果为二分之一的 oldCapacity)

        private void grow(int minCapacity) {
            // overflow-conscious code
            int oldCapacity = elementData.length;
            //newCapacity = oldCapacity + O.5*oldCapacity,此处扩容0.5倍
            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);
        }
        
      • Vector 的 grow(),Vector 比 ArrayList多一个属性,扩展因子capacityIncrement,可以扩容大小。当扩容容量增量大于0时、新数组长度为原数组长度+扩容容量增量、否则新数组长度为原数组长度的2

        private void grow(int minCapacity) {
            // overflow-conscious code
            int oldCapacity = elementData.length;
            //
            int newCapacity = oldCapacity + ((capacityIncrement > 0) ?
                                             capacityIncrement : oldCapacity);
            if (newCapacity - minCapacity < 0)
                newCapacity = minCapacity;
            if (newCapacity - MAX_ARRAY_SIZE > 0)
                newCapacity = hugeCapacity(minCapacity);
            elementData = Arrays.copyOf(elementData, newCapacity);
        }
        

    ArrayList 与 LinkedList 区别

    • 是否保证线程安全: ArrayList 和 LinkedList 都是不同步的,也就是不保证线程安全;
    • 底层数据结构: Arraylist 底层使用的是 Object 数组;LinkedList 底层使用的是双向循环链表数据结构;
    • 插入和删除是否受元素位置的影响:
      • ArrayList 采用数组存储,所以插入和删除元素的时间复杂度受元素位置的影响。 比如:执行 add(E e)方法的时候, ArrayList 会默认在将指定的元素追加到此列表的末尾,这种情况时间复杂度就是O(1)。但是如果要在指定位置 i 插入和删除元素的话( add(intindex,E element))时间复杂度就为 O(n-i)。因为在进行上述操作的时候集合中第 i 和第 i 个元素之后的(n-i)个元素都要执行向后位/向前移一位的操作。
      • LinkedList 采用链表存储,所以插入,删除元素时间复杂度不受元素位置的影响,都是近似 $O(1)$,而数组为近似 $O(n)$。
      • ArrayList 一般应用于查询较多但插入以及删除较少情况,如果插入以及删除较多则建议使用 LinkedList
    • 是否支持快速随机访问: LinkedList 不支持高效的随机元素访问,而 ArrayList 实现了 RandomAccess 接口,所以有随机访问功能。快速随机访问就是通过元素的序号快速获取元素对象(对应于 get(intindex)方法)。
    • 内存空间占用: ArrayList 的空间浪费主要体现在在 list 列表的结尾会预留一定的容量空间,而 LinkedList 的空间花费则体现在它的每一个元素都需要消耗比 ArrayList 更多的空间(因为要存放直接后继和直接前驱以及数据)。

    高级工程师的我,可不得看看源码,具体分析下:

    • ArrayList工作原理其实很简单,底层是动态数组,每次创建一个 ArrayList 实例时会分配一个初始容量(没有指定初始容量的话,默认是 10),以add方法为例,如果没有指定初始容量,当执行add方法,先判断当前数组是否为空,如果为空则给保存对象的数组分配一个最小容量,默认为10。当添加大容量元素时,会先增加数组的大小,以提高添加的效率;

    • LinkedList 是有序并且支持元素重复的集合,底层是基于双向链表的,即每个节点既包含指向其后继的引用也包括指向其前驱的引用。链表无容量限制,但双向链表本身使用了更多空间,也需要额外的链表指针操作。按下标访问元素 get(i)/set(i,e) 要悲剧的遍历链表将指针移动到位(如果i>数组大小的一半,会从末尾移起)。插入、删除元素时修改前后节点的指针即可,但还是要遍历部分链表的指针才能移动到下标所指的位置,只有在链表两头的操作add(), addFirst()removeLast()或用 iterator() 上的 remove() 能省掉指针的移动。此外 LinkedList 还实现了 Deque(继承自Queue接口)接口,可以当做队列使用。

    不会囊括所有方法,只是为了学习,记录思想。

    ArrayList 和 LinkedList 两者都实现了 List 接口

    public class ArrayList<E> extends AbstractList<E>
            implements List<E>, RandomAccess, Cloneable, java.io.Serializable{
    
    public class LinkedList<E>
        extends AbstractSequentialList<E>
        implements List<E>, Deque<E>, Cloneable, java.io.Serializable
    

    构造器

    ArrayList 提供了 3 个构造器,①无参构造器 ②带初始容量构造器 ③参数为集合构造器

    public class ArrayList<E> extends AbstractList<E>
            implements List<E>, RandomAccess, Cloneable, java.io.Serializable{
       
    public ArrayList(int initialCapacity) {
       if (initialCapacity > 0) {
           // 创建初始容量的数组
         this.elementData = new Object[initialCapacity];
        } else if (initialCapacity == 0) {
         this.elementData = EMPTY_ELEMENTDATA;
        } else {
       throw new IllegalArgumentException("Illegal Capacity: "+initialCapacity);
      }
    }
    public ArrayList() {
      // 默认为空数组
      this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }
        
    public ArrayList(Collection<? extends E> c) { //...}       
    }
    

    LinkedList 提供了 2 个构造器,因为基于链表,所以也就没有初始化大小,也没有扩容的机制,就是一直在前面或者后面插插插~~

    public LinkedList() {
    }
    
    public LinkedList(Collection<? extends E> c) {
        this();
        addAll(c);
    }
    // LinkedList 既然作为链表,那么肯定会有节点
    private static class Node<E> {
        E item;
        Node<E> next;
        Node<E> prev;
    
        Node(Node<E> prev, E element, Node<E> next) {
            this.item = element;
            this.next = next;
            this.prev = prev;
        }
    }
    

    插入

    ArrayList:

    public boolean add(E e) {
        // 确保数组的容量,保证可以添加该元素
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        // 将该元素放入数组中
        elementData[size++] = e;
        return true;
    }
    private void ensureCapacityInternal(int minCapacity) {
        // 如果数组是空的,那么会初始化该数组
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            // DEFAULT_CAPACITY 为 10,所以调用无参默认 ArrayList 构造方法初始化的话,默认的数组容量为 10
            minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
        }
    
        ensureExplicitCapacity(minCapacity);
    }
    
    private void ensureExplicitCapacity(int minCapacity) {
        modCount++;
    
        // 确保数组的容量,如果不够的话,调用 grow 方法扩容
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }
    //扩容具体的方法
    private void grow(int minCapacity) {
        // 当前数组的容量
        int oldCapacity = elementData.length;
        // 新数组扩容为原来容量的 1.5 倍
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        // 如果新数组扩容容量还是比最少需要的容量还要小的话,就设置扩充容量为最小需要的容量
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
        //判断新数组容量是否已经超出最大数组范围,MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8
        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);
    }
    

    当然也可以插入指定位置,还有一个重载的方法 add(int index, E element)

    public void add(int index, E element) {
        // 判断 index 有没有超出索引的范围
        rangeCheckForAdd(index);
        // 和之前的操作是一样的,都是保证数组的容量足够
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        // 将指定位置及其后面数据向后移动一位
        System.arraycopy(elementData, index, elementData, index + 1,
                         size - index);
        // 将该元素添加到指定的数组位置
        elementData[index] = element;
        // ArrayList 的大小改变
        size++;
    }
    

    可以看到每次插入指定位置都要移动元素,效率较低。

    再来看 LinkedList 的插入,也有插入末尾,插入指定位置两种,由于基于链表,肯定得先有个 Node

    private static class Node<E> {
        E item;
        Node<E> next;
        Node<E> prev;
    
        Node(Node<E> prev, E element, Node<E> next) {
            this.item = element;
            this.next = next;
            this.prev = prev;
        }
    }
    
    public boolean add(E e) {
        // 直接往队尾加元素
        linkLast(e);
        return true;
    }
    
    void linkLast(E e) {
        // 保存原来链表尾部节点,last 是全局变量,用来表示队尾元素
        final Node<E> l = last;
        // 为该元素 e 新建一个节点
        final Node<E> newNode = new Node<>(l, e, null);
        // 将新节点设为队尾
        last = newNode;
        // 如果原来的队尾元素为空,那么说明原来的整个列表是空的,就把新节点赋值给头结点
        if (l == null)
            first = newNode;
        else
        // 原来尾结点的后面为新生成的结点
            l.next = newNode;
        // 节点数 +1
        size++;
        modCount++;
    }
    
    public void add(int index, E element) {
        // 检查 index 有没有超出索引范围
        checkPositionIndex(index);
        // 如果追加到尾部,那么就跟 add(E e) 一样了
        if (index == size)
            linkLast(element);
        else
        // 否则就是插在其他位置
         linkBefore(element, node(index));
    }
    
    //linkBefore方法中调用了这个node方法,类似二分查找的优化
    Node<E> node(int index) {
        // assert isElementIndex(index);
        // 如果 index 在前半段,从前往后遍历获取 node
        if (index < (size >> 1)) {
            Node<E> x = first;
            for (int i = 0; i < index; i++)
                x = x.next;
            return x;
        } else {
            // 如果 index 在后半段,从后往前遍历获取 node
            Node<E> x = last;
            for (int i = size - 1; i > index; i--)
                x = x.prev;
            return x;
        }
    }
    
    void linkBefore(E e, Node<E> succ) {
        // assert succ != null;
        // 保存 index 节点的前节点
        final Node<E> pred = succ.prev;
        // 新建一个目标节点
        final Node<E> newNode = new Node<>(pred, e, succ);
        succ.prev = newNode;
        // 如果是在开头处插入的话
        if (pred == null)
            first = newNode;
        else
            pred.next = newNode;
        size++;
        modCount++;
    }
    

    获取

    ArrayList 的 get() 方法很简单,就是在数组中返回指定位置的元素即可,所以效率很高

    public E get(int index) {
        // 检查 index 有没有超出索引的范围
        rangeCheck(index);
        // 返回指定位置的元素
        return elementData(index);
    }
    

    LinkedList 的 get() 方法,就是在内部调用了上边看到的 node() 方法,判断在前半段还是在后半段,然后遍历得到即可。

    public E get(int index) {
        checkElementIndex(index);
        return node(index).item;
    }
    

    HashMap的底层实现

    什么时候会使用HashMap?他有什么特点?

    你知道HashMap的工作原理吗?

    你知道get和put的原理吗?equals()和hashCode()的都有什么作用?

    你知道hash的实现吗?为什么要这样实现?

    如果HashMap的大小超过了负载因子(load factor)定义的容量,怎么办?

    HashMap 在 JDK 7 和 JDK8 中的实现方式略有不同。分开记录。

    深入 HahsMap 之前,先要了解的概念

    1. initialCapacity:初始容量。指的是 HashMap 集合初始化的时候自身的容量。可以在构造方法中指定;如果不指定的话,总容量默认值是 16 。需要注意的是初始容量必须是 2 的幂次方。(1.7中,已知HashMap中将要存放的KV个数的时候,设置一个合理的初始化容量可以有效的提高性能

      static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
      
    2. size:当前 HashMap 中已经存储着的键值对数量,即 HashMap.size() 。

    3. loadFactor:加载因子。所谓的加载因子就是 HashMap (当前的容量/总容量) 到达一定值的时候,HashMap 会实施扩容。加载因子也可以通过构造方法中指定,默认的值是 0.75 。举个例子,假设有一个 HashMap 的初始容量为 16 ,那么扩容的阀值就是 0.75 * 16 = 12 。也就是说,在你打算存入第 13 个值的时候,HashMap 会先执行扩容。

    4. threshold:扩容阀值。即 扩容阀值 = HashMap 总容量 * 加载因子。当前 HashMap 的容量大于或等于扩容阀值的时候就会去执行扩容。扩容的容量为当前 HashMap 总容量的两倍。比如,当前 HashMap 的总容量为 16 ,那么扩容之后为 32 。

    5. table:Entry 数组。我们都知道 HashMap 内部存储 key/value 是通过 Entry 这个介质来实现的。而 table 就是 Entry 数组。

    JDK1.7 实现

    JDK1.7 中 HashMap 由 数组+链表 组成(“链表散列” 即数组和链表的结合体),数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的(HashMap 采用 “拉链法也就是链地址法” 解决冲突),如果定位到的数组位置不含链表(当前 entry 的 next 指向 null ),那么对于查找,添加等操作很快,仅需一次寻址即可;如果定位到的数组包含链表,对于添加操作,其时间复杂度依然为 O(1),因为最新的 Entry 会插入链表头部,即需要简单改变引用链即可,而对于查找操作来讲,此时就需要遍历链表,然后通过 key 对象的 equals 方法逐一比对查找。

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

    源码解析

    构造方法

    《阿里巴巴 Java 开发手册》推荐集合初始化时,指定集合初始值大小。(说明:HashMap 使用HashMap(int initialCapacity) 初始化)建议原因: https://www.zhihu.com/question/314006228/answer/611170521

    // 默认的构造方法使用的都是默认的初始容量和加载因子
    // DEFAULT_INITIAL_CAPACITY = 16,DEFAULT_LOAD_FACTOR = 0.75f
    public HashMap() {
        this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
    }
    
    // 可以指定初始容量,并且使用默认的加载因子
    public HashMap(int initialCapacity) {
        this(initialCapacity, 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;
        threshold = initialCapacity;
        // 空方法
        init();
    }
    
    public HashMap(Map<? extends K, ? extends V> m) {
      this(Math.max((int) (m.size() / DEFAULT_LOAD_FACTOR) + 1,
                    DEFAULT_INITIAL_CAPACITY), DEFAULT_LOAD_FACTOR);
      inflateTable(threshold);
      putAllForCreate(m);
    }
    

    HashMap 的前 3 个构造方法最后都会去调用 HashMap(int initialCapacity, float loadFactor) 。在其内部去设置初始容量和加载因子。而最后的 init() 是空方法,主要给子类实现,比如LinkedHashMap。

    put() 方法
    public V put(K key, V value) {
        // 如果 table 数组为空时先创建数组,并且设置扩容阀值
        if (table == EMPTY_TABLE) {
            inflateTable(threshold);
        }
        // 如果 key 为空时,调用 putForNullKey 方法特殊处理
        if (key == null)
            return putForNullKey(value);
        // 计算 key 的哈希值
        int hash = hash(key);
        // 根据计算出来的哈希值和当前数组的长度计算在数组中的索引
        int i = indexFor(hash, table.length);
        // 先遍历该数组索引下的整条链表
        // 如果该 key 之前已经在 HashMap 中存储了的话,直接替换对应的 value 值即可
        for (Entry<K,V> e = table[i]; e != null; e = e.next) {
            Object k;
           //先判断hash值是否一样,如果一样,再判断key是否一样,不同对象的hash值可能一样
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }
    
        modCount++;
        // 如果该 key 之前没有被存储过,那么就进入 addEntry 方法
        addEntry(hash, key, value, i);
        return null;
    }
    
    void addEntry(int hash, K key, V value, int bucketIndex) {
        // 当前容量大于或等于扩容阀值的时候,会执行扩容
        if ((size >= threshold) && (null != table[bucketIndex])) {
            // 扩容为原来容量的两倍
            resize(2 * table.length);
            // 重新计算哈希值
            hash = (null != key) ? hash(key) : 0;
            // 重新得到在新数组中的索引
            bucketIndex = indexFor(hash, table.length);
        }
        // 创建节点
        createEntry(hash, key, value, bucketIndex);
    }
    
    //扩容,创建了一个新的数组,然后把数据全部复制过去,再把新数组的引用赋给 table
    void resize(int newCapacity) {
        Entry[] oldTable = table;  //引用扩容前的Entry数组
        int oldCapacity = oldTable.length;
        if (oldCapacity == MAXIMUM_CAPACITY) { //扩容前的数组大小如果已经达到最大(2^30)了
            threshold = Integer.MAX_VALUE;  //修改阈值为int的最大值(2^31-1),这样以后就不会扩容了
            return;
        }
        // 创建新的 entry 数组
        Entry[] newTable = new Entry[newCapacity];
        // 将旧 entry 数组中的数据复制到新 entry 数组中
        transfer(newTable, initHashSeedAsNeeded(newCapacity));
        // 将新数组的引用赋给 table
        table = newTable;
        // 计算新的扩容阀值
        threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
    }
    
    void transfer(Entry[] newTable) {
         Entry[] src = table;                   //src引用了旧的Entry数组
         int newCapacity = newTable.length;
         for (int j = 0; j < src.length; j++) { //遍历旧的Entry数组
             Entry<K,V> e = src[j];             //取得旧Entry数组的每个元素
             if (e != null) {
                 src[j] = null;//释放旧Entry数组的对象引用(for循环后,旧的Entry数组不再引用任何对象)
                 do {
                     Entry<K,V> next = e.next;
                     int i = indexFor(e.hash, newCapacity); //!!重新计算每个元素在数组中的位置
                     e.next = newTable[i]; //标记[1]
                     newTable[i] = e;      //将元素放在数组上
                     e = next;             //访问下一个Entry链上的元素
                 } while (e != null);
             }
         }
    } 
    
    void createEntry(int hash, K key, V value, int bucketIndex) {
       // 取出table中下标为bucketIndex的Entry
        Entry<K,V> e = table[bucketIndex];
       // 利用key、value来构建新的Entry
       // 并且之前存放在table[bucketIndex]处的Entry作为新Entry的next
       // 把新创建的Entry放到table[bucketIndex]位置
        table[bucketIndex] = new Entry<>(hash, key, value, e);
        // 当前 HashMap 的容量加 1
        size++;
    }
    

    最后的 createEntry() 方法就说明了当hash冲突时,采用的拉链法来解决hash冲突的,并且是把新元素是插入到单边表的表头。

    get() 方法
    public V get(Object key) {
        // 如果 key 是空的,就调用 getForNullKey 方法特殊处理
        if (key == null)
            return getForNullKey();
        // 获取 key 相对应的 entry 
        Entry<K,V> entry = getEntry(key);
    
        return null == entry ? null : entry.getValue();
    }
    
    //找到对应 key 的数组索引,然后遍历链表查找即可
    final Entry<K,V> getEntry(Object key) {
        if (size == 0) {
            return null;
        }
        // 计算 key 的哈希值
        int hash = (key == null) ? 0 : hash(key);
        // 得到数组的索引,然后遍历链表,查看是否有相同 key 的 Entry
        for (Entry<K,V> e = table[indexFor(hash, table.length)];
             e != null;
             e = e.next) {
            Object k;
            if (e.hash == hash &&
                ((k = e.key) == key || (key != null && key.equals(k))))
                return e;
        }
        // 没有的话,返回 null
        return null;
    }
    

    JDK1.8 实现

    JDK 1.7 中,如果哈希碰撞过多,拉链过长,极端情况下,所有值都落入了同一个桶内,这就退化成了一个链表。通过 key 值查找要遍历链表,效率较低。 JDK1.8在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)时,将链表转化为红黑树,以减少搜索时间。

    TreeMap、TreeSet以及 JDK1.8 之后的 HashMap 底层都用到了红黑树。红黑树就是为了解决二叉查找树的缺陷,因为二叉查找树在某些情况下会退化成一个线性结构。

    源码解析

    构造方法

    JDK8 构造方法改动不是很大

    public HashMap() {
      this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
    }
    
    public HashMap(int initialCapacity) {
      this(initialCapacity, 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);
    }
    
    public HashMap(Map<? extends K, ? extends V> m) {
      this.loadFactor = DEFAULT_LOAD_FACTOR;
      putMapEntries(m, false);
    }
    
    确定哈希桶数组索引位置(hash 函数的实现)
    //方法一:
    static final int hash(Object key) { //jdk1.8 & jdk1.7
     int h;
     // h = key.hashCode() 为第一步 取hashCode值
     // h ^ (h >>> 16)  为第二步 高位参与运算
     return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }
    //方法二:
    static int indexFor(int h, int length) { //jdk1.7的源码,jdk1.8没有提取这个方法,而是放在了其他方法中,比如 put 的p = tab[i = (n - 1) & hash]
     return h & (length-1); //第三步 取模运算
    }
    

    HashMap定位数组索引位置,直接决定了hash方法的离散性能。Hash算法本质上就是三步:取key的hashCode值、高位运算、取模运算

    hash

    为什么要这样呢?

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

    目的当然是为了减少哈希碰撞,使 table 里的数据分布的更均匀。

    1. HashMap 中桶数组的大小 length 总是2的幂,此时,h & (table.length -1) 等价于对 length 取模 h%length。但取模的计算效率没有位运算高,所以这是是一个优化。假设 h = 185table.length-1 = 15(0x1111),其实散列真正生效的只是低 4bit 的有效位,所以很容易碰撞。

      img

    2. 图中的 hash 是由键的 hashCode 产生。计算余数时,由于 n 比较小,hash 只有低4位参与了计算,高位的计算可以认为是无效的。这样导致了计算结果只与低位信息有关,高位数据没发挥作用。为了处理这个缺陷,我们可以上图中的 hash 高4位数据与低4位数据进行异或运算,即 hash ^ (hash >>> 4)。通过这种方式,让高位数据与低位数据进行异或,以此加大低位信息的随机性,变相的让高位数据参与到计算中。此时的计算过程如下:

      img

      在 Java 中,hashCode 方法产生的 hash 是 int 类型,32 位宽。前16位为高位,后16位为低位,所以要右移16位,即 hash ^ (hash >>> 16) 。这样还增加了hash 的复杂度,进而影响 hash 的分布性。

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

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

    这个算法应该如何设计呢?

    我们首先可能会想到采用%取余的操作来实现。但是,重点来了。

    取余操作中如果除数是2的幂次则等价于其除数减一的与操作,也就是说hash%length=hash&(length-1),但前提是length是2的n次方,并且采用&运算比%运算效率高,这也就解释了HashMap的长度为什么是2的幂次方。

    put() 方法

    public V put(K key, V value) {
      // 对key的hashCode()做hash
      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;
      // tab为空则创建
      if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
      // 计算index,并对null做处理
      if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
      else {
        Node<K,V> e; K k;
        // 节点key存在,直接覆盖value
        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);
              //链表长度大于8转换为红黑树进行处理
              if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                treeifyBin(tab, hash);
              break;
            }
            //key已经存在直接覆盖value
            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;
    }
    
    resize() 扩容
    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) {
        // 超过最大值就不再扩充了,就只好随你碰撞了
        if (oldCap >= MAXIMUM_CAPACITY) {
          //修改阈值为int的最大值(2^31-1),这样以后就不会扩容了
          threshold = Integer.MAX_VALUE;
          return oldTab;
        }
         // 没超过最大值,就扩充为原来的2倍
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                 oldCap >= DEFAULT_INITIAL_CAPACITY)
          newThr = oldThr << 1; // double threshold
      }
      else if (oldThr > 0) // initial capacity was placed in threshold
        newCap = oldThr;
      else {               // zero initial threshold signifies using defaults
        newCap = DEFAULT_INITIAL_CAPACITY;
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
      }
      // 计算新的resize上限
      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;
      // 把每个bucket都移动到新的buckets中
      if (oldTab != null) {
        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);
            else { // preserve order  链表优化重hash的代码块
              Node<K,V> loHead = null, loTail = null;
              Node<K,V> hiHead = null, hiTail = null;
              Node<K,V> next;
              do {
                // 原索引
                next = e.next;
                if ((e.hash & oldCap) == 0) {
                  if (loTail == null)
                    loHead = e;
                  else
                    loTail.next = e;
                  loTail = e;
                }
                // 原索引+oldCap
                else {
                  if (hiTail == null)
                    hiHead = e;
                  else
                    hiTail.next = e;
                  hiTail = e;
                }
              } while ((e = next) != null);
              // 原索引放到bucket里
              if (loTail != null) {
                loTail.next = null;
                newTab[j] = loHead;
              }
              // 原索引+oldCap放到bucket里
              if (hiTail != null) {
                hiTail.next = null;
                newTab[j + oldCap] = hiHead;
              }
            }
          }
        }
      }
      return newTab;
    }
    
    get() 方法
    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 && // always check first node
            ((k = first.key) == key || (key != null && key.equals(k))))
          return first;
        // 未命中
        if ((e = first.next) != null) {
          // 如果 first 是 TreeNode 类型,则调用黑红树查找方法
          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;
    }
    

    Hashtable

    Hashtable 和 HashMap 都是散列表,也是用”拉链法“实现的哈希表。保存数据和 JDK7 中的 HashMap 一样,是 Entity 对象,只是 Hashtable 中的几乎所有的 public 方法都是 synchronized 的,而有些方法也是在内部通过 synchronized 代码块来实现,效率肯定会降低。且 put() 方法不允许空值。

    HashMap 和 Hashtable 的区别

    1. 线程是否安全: HashMap 是非线程安全的,HashTable 是线程安全的;HashTable 内部的方法基本都经过 synchronized 修饰。(如果你要保证线程安全的话就使用 ConcurrentHashMap 吧!);

    2. 效率: 因为线程安全的问题,HashMap 要比 HashTable 效率高一点。另外,HashTable 基本被淘汰,不要在代码中使用它;

    3. 对Null key 和Null value的支持: HashMap 中,null 可以作为键,这样的键只有一个,可以有一个或多个键所对应的值为 null。。但是在 HashTable 中 put 进的键值只要有一个 null,直接抛出 NullPointerException。

    4. 初始容量大小和每次扩充容量大小的不同 :

      ① 创建时如果不指定容量初始值,Hashtable 默认的初始大小为11,之后每次扩充,容量变为原来的2n+1。HashMap 默认的初始化大小为16。之后每次扩充,容量变为原来的2倍。

      ② 创建时如果给定了容量初始值,那么 Hashtable 会直接使用你给定的大小,而 HashMap 会将其扩充为2的幂次方大小。也就是说 HashMap 总是使用2的幂次方作为哈希表的大小,后面会介绍到为什么是2的幂次方。

    5. 底层数据结构: JDK1.8 以后的 HashMap 在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)时,将链表转化为红黑树,以减少搜索时间。Hashtable 没有这样的机制。

    6. HashMap的迭代器(Iterator)是fail-fast迭代器,但是 Hashtable的迭代器(enumerator)不是 fail-fast的。如果有其它线程对HashMap进行的添加/删除元素,将会抛出ConcurrentModificationException,但迭代器本身的remove方法移除元素则不会抛出异常。这条同样也是 Enumeration 和 Iterator 的区别。

    ConcurrentHashMap

    HashMap在多线程情况下,在put的时候,插入的元素超过了容量(由负载因子决定)的范围就会触发扩容操作,就是rehash,这个会重新将原数组的内容重新hash到新的扩容数组中,在多线程的环境下,存在同时其他的元素也在进行put操作,如果hash值相同,可能出现同时在同一数组下用链表表示,造成闭环,导致在get时会出现死循环,所以HashMap是线程不安全的。

    Hashtable,是线程安全的,它在所有涉及到多线程操作的都加上了synchronized关键字来锁住整个table,这就意味着所有的线程都在竞争一把锁,在多线程的环境下,它是安全的,但是无疑是效率低下的。

    JDK1.7 实现

    Hashtable 容器在竞争激烈的并发环境下表现出效率低下的原因,是因为所有访问 Hashtable 的线程都必须竞争同一把锁,那假如容器里有多把锁,每一把锁用于锁容器其中一部分数据,那么当多线程访问容器里不同数据段的数据时,线程间就不会存在锁竞争,,这就是ConcurrentHashMap所使用的锁分段技术。

    在 JDK1.7版本中,ConcurrentHashMap 的数据结构是由一个 Segment 数组和多个 HashEntry 组成。Segment 数组的意义就是将一个大的 table 分割成多个小的 table 来进行加锁。每一个 Segment 元素存储的是 HashEntry数组+链表,这个和 HashMap 的数据存储结构一样。

    ConcurrentHashMap 类中包含两个静态内部类 HashEntry 和 Segment。
    HashEntry 用来封装映射表的键值对,Segment 用来充当锁的角色,每个 Segment 对象守护整个散列映射表的若干个桶。每个桶是由若干个 HashEntry 对象链接起来的链表。一个 ConcurrentHashMap 实例中包含由若干个 Segment 对象组成的数组。每个 Segment 守护着一个 HashEntry 数组里的元素,当对 HashEntry 数组的数据进行修改时,必须首先获得它对应的 Segment 锁。

    Segment 类

    Segment 类继承于 ReentrantLock 类,从而使得 Segment 对象能充当可重入锁的角色。一个 Segment 就是一个子哈希表,Segment 里维护了一个 HashEntry 数组,并发环境下,对于不同 Segment 的数据进行操作是不用考虑锁竞争的。

    从源码可以看到,Segment 内部类和我们上边看到的 HashMap 很相似。也有负载因子,阈值等各种属性。

    static final class Segment<K,V> extends ReentrantLock implements Serializable {
      
      static final int MAX_SCAN_RETRIES =
        Runtime.getRuntime().availableProcessors() > 1 ? 64 : 1;
      transient volatile HashEntry<K,V>[] table;
      transient int count;
      transient int modCount;  //记录修改次数
      transient int threshold;
      final float loadFactor;
    
      Segment(float lf, int threshold, HashEntry<K,V>[] tab) {
        this.loadFactor = lf;
        this.threshold = threshold;
        this.table = tab;
      }
    
      //put 方法会有加锁操作,
      final V put(K key, int hash, V value, boolean onlyIfAbsent) {
        HashEntry<K,V> node = tryLock() ? null :
                    scanAndLockForPut(key, hash, value);
        // ...
      }
    
      @SuppressWarnings("unchecked")
      private void rehash(HashEntry<K,V> node) {
        // ...
      }
    
      private HashEntry<K,V> scanAndLockForPut(K key, int hash, V value) {
     		//...
      }
    
      private void scanAndLock(Object key, int hash) {
     		//...
      }
    
      final V remove(Object key, int hash, Object value) {
        //...
      }
    
      final boolean replace(K key, int hash, V oldValue, V newValue) {
      	//...
      }
    
      final V replace(K key, int hash, V value) {
        //...
      }
    
      final void clear() {
    		//...
      }
    }
    

    HashEntry 类

    HashEntry 是目前我们最小的逻辑处理单元。一个ConcurrentHashMap 维护一个 Segment 数组,一个Segment维护一个 HashEntry 数组。

    static final class HashEntry<K,V> {
      final int hash;
      final K key;
      volatile V value;   // value 为 volatie 类型,保证可见
      volatile HashEntry<K,V> next;
    	//...
    }
    

    ConcurrentHashMap 类

    默认的情况下,每个ConcurrentHashMap 类会创建16个并发的 segment,每个 segment 里面包含多个 Hash表,每个 Hash 链都是由 HashEntry 节点组成的。

    public class ConcurrentHashMap<K, V> extends AbstractMap<K, V>
            implements ConcurrentMap<K, V>, Serializable {
    	//默认初始容量为 16,即初始默认为 16 个桶
      static final int DEFAULT_INITIAL_CAPACITY = 16;
      static final float DEFAULT_LOAD_FACTOR = 0.75f;
      //默认并发级别为 16。该值表示当前更新线程的估计数
      static final int DEFAULT_CONCURRENCY_LEVEL = 16;
      
      static final int MAXIMUM_CAPACITY = 1 << 30;
      static final int MIN_SEGMENT_TABLE_CAPACITY = 2;  
      static final int MAX_SEGMENTS = 1 << 16; // slightly conservative  
      static final int RETRIES_BEFORE_LOCK = 2;
      final int segmentMask;  //段掩码,主要为了定位Segment
      final int segmentShift;
      final Segment<K,V>[] segments;   //主干就是这个分段锁数组
      
      //构造器
      public ConcurrentHashMap(int initialCapacity,
                                 float loadFactor, int concurrencyLevel) {
            if (!(loadFactor > 0) || initialCapacity < 0 || concurrencyLevel <= 0)
                throw new IllegalArgumentException();
          //MAX_SEGMENTS 为1<<16=65536,也就是最大并发数为65536
            if (concurrencyLevel > MAX_SEGMENTS)
                concurrencyLevel = MAX_SEGMENTS;
            // 2的sshif次方等于ssize,例:ssize=16,sshift=4;ssize=32,sshif=5
            int sshift = 0;
            // ssize 为segments数组长度,根据concurrentLevel计算得出
            int ssize = 1;
            while (ssize < concurrencyLevel) {
                ++sshift;
                ssize <<= 1;
            }
            this.segmentShift = 32 - sshift;
            this.segmentMask = ssize - 1;
            if (initialCapacity > MAXIMUM_CAPACITY)
                initialCapacity = MAXIMUM_CAPACITY;
            int c = initialCapacity / ssize;
            if (c * ssize < initialCapacity)
                ++c;
            int cap = MIN_SEGMENT_TABLE_CAPACITY;
            while (cap < c)
                cap <<= 1;
            // 创建segments数组并初始化第一个Segment,其余的Segment延迟初始化
            Segment<K,V> s0 =
                new Segment<K,V>(loadFactor, (int)(cap * loadFactor),
                                 (HashEntry<K,V>[])new HashEntry[cap]);
            Segment<K,V>[] ss = (Segment<K,V>[])new Segment[ssize];
            UNSAFE.putOrderedObject(ss, SBASE, s0); // ordered write of segments[0]
            this.segments = ss;
        }
    }
    

    put() 方法

    1. **定位segment并确保定位的Segment已初始化 **
    2. 调用 Segment的 put 方法。
    public V put(K key, V value) {
      Segment<K,V> s;
      //concurrentHashMap不允许key/value为空
      if (value == null)
        throw new NullPointerException();
      //hash函数对key的hashCode重新散列,避免差劲的不合理的hashcode,保证散列均匀
      int hash = hash(key);
      //返回的hash值无符号右移segmentShift位与段掩码进行位运算,定位segment
      int j = (hash >>> segmentShift) & segmentMask;
      if ((s = (Segment<K,V>)UNSAFE.getObject          // nonvolatile; recheck
           (segments, (j << SSHIFT) + SBASE)) == null) //  in ensureSegment
        s = ensureSegment(j);
      return s.put(key, hash, value, false);
    }
    

    get() 方法

    get方法无需加锁,由于其中涉及到的共享变量都使用volatile修饰,volatile可以保证内存可见性,所以不会读取到过期数据

    public V get(Object key) {
      Segment<K,V> s; // manually integrate access methods to reduce overhead
      HashEntry<K,V>[] tab;
      int h = hash(key);
      long u = (((h >>> segmentShift) & segmentMask) << SSHIFT) + SBASE;
      if ((s = (Segment<K,V>)UNSAFE.getObjectVolatile(segments, u)) != null &&
          (tab = s.table) != null) {
        for (HashEntry<K,V> e = (HashEntry<K,V>) UNSAFE.getObjectVolatile
             (tab, ((long)(((tab.length - 1) & h)) << TSHIFT) + TBASE);
             e != null; e = e.next) {
          K k;
          if ((k = e.key) == key || (e.hash == h && key.equals(k)))
            return e.value;
        }
      }
      return null;
    }
    

    JDK1.8 实现

    img

    ConcurrentHashMap 在 JDK8 中进行了巨大改动,光是代码量就从1000多行增加到6000行!1.8摒弃了Segment(锁段)的概念,采用了 CAS + synchronized 来保证并发的安全性。

    可以看到,和HashMap 1.8的数据结构很像。底层数据结构改变为采用数组+链表+红黑树的数据形式。

    和HashMap1.8相同的一些地方

    • 底层数据结构一致
    • HashMap初始化是在第一次put元素的时候进行的,而不是init
    • HashMap的底层数组长度总是为2的整次幂
    • 默认树化的阈值为 8,而链表化的阈值为 6
    • hash算法也很类似,但多了一步& HASH_BITS,该步是为了消除最高位上的负符号,hash的负在ConcurrentHashMap中有特殊意义表示在扩容或者是树节点
    static final int HASH_BITS = 0x7fffffff; // usable bits of normal node hash
    
    static final int spread(int h) {
        return (h ^ (h >>> 16)) & HASH_BITS;
    }
    

    一些关键属性

    private static final int MAXIMUM_CAPACITY = 1 << 30; //数组最大大小 同HashMap
    
    private static final int DEFAULT_CAPACITY = 16;//数组默认大小
    
    static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8; //数组可能最大值,需要与toArray()相关方法关联
    
    private static final int DEFAULT_CONCURRENCY_LEVEL = 16; //兼容旧版保留的值,默认线程并发度,类似信号量
    
    private static final float LOAD_FACTOR = 0.75f;//默认map扩容比例,实际用(n << 1) - (n >>> 1)代替了更高效
    
    static final int TREEIFY_THRESHOLD = 8; // 链表转树阀值,大于8时
    
    static final int UNTREEIFY_THRESHOLD = 6; //树转链表阀值,小于等于6(tranfer时,lc、hc=0两个计数器分别++记录原bin、新binTreeNode数量,<=UNTREEIFY_THRESHOLD 则untreeify(lo))。【仅在扩容tranfer时才可能树转链表】
    
    static final int MIN_TREEIFY_CAPACITY = 64;
    
    private static final int MIN_TRANSFER_STRIDE = 16;//扩容转移时的最小数组分组大小
    
    private static int RESIZE_STAMP_BITS = 16;//本类中没提供修改的方法 用来根据n生成位置一个类似时间戳的功能
    
    private static final int MAX_RESIZERS = (1 << (32 - RESIZE_STAMP_BITS)) - 1; // 2^15-1,help resize的最大线程数
    
    private static final int RESIZE_STAMP_SHIFT = 32 - RESIZE_STAMP_BITS; // 32-16=16,sizeCtl中记录size大小的偏移量
    
    static final int MOVED = -1; // hash for forwarding nodes(forwarding nodes的hash值)、标示位
    
    static final int TREEBIN = -2; // hash for roots of trees(树根节点的hash值)
    
    static final int RESERVED = -3; // ReservationNode的hash值
    
    static final int HASH_BITS = 0x7fffffff; // 用在计算hash时进行安位与计算消除负hash
    
    static final int NCPU = Runtime.getRuntime().availableProcessors(); // 可用处理器数量
    
    /* ---------------- Fields -------------- */
    
    transient volatile Node<K,V>[] table; //装载Node的数组,作为ConcurrentHashMap的数据容器,采用懒加载的方式,直到第一次插入数据的时候才会进行初始化操作,数组的大小总是为2的幂次方。
    
    private transient volatile Node<K,V>[] nextTable; //扩容时使用,平时为null,只有在扩容的时候才为非null
    
    /**
    * 实际上保存的是hashmap中的元素个数  利用CAS锁进行更新但它并不用返回当前hashmap的元素个数 
    */
    private transient volatile long baseCount;
    
    /**
    *该属性用来控制table数组的大小,根据是否初始化和是否正在扩容有几种情况:
    *当值为负数时:如果为-1表示正在初始化,如果为-N则表示当前正有N-1个线程进行扩容操作;
    *当值为正数时:如果当前数组为null的话表示table在初始化过程中,sizeCtl表示为需要新建数组的长度;若已经初始化了,表示当前数据容器(table数组)可用容量也可以理解成临界值(插入节点数超过了该临界值就需要扩容),具体指为数组的长度n 乘以 加载因子loadFactor;当值为0时,即数组长度为默认初始值。
    */
    private transient volatile int sizeCtl;
    

    put() 方法

    1. 首先会判断 key、value是否为空,如果为空就抛异常!
    2. spread()方法获取hash,减小hash冲突
    3. 判断是否初始化table数组,没有的话调用initTable()方法进行初始化
    4. 判断是否能直接将新值插入到table数组中
    5. 判断当前是否在扩容,MOVED为-1说明当前ConcurrentHashMap正在进行扩容操作,正在扩容的话就进行协助扩容
    6. 当table[i]为链表的头结点,在链表中插入新值,通过synchronized (f)的方式进行加锁以实现线程安全性。
      1. 在链表中如果找到了与待插入的键值对的key相同的节点,就直接覆盖
      2. 如果没有找到的话,就直接将待插入的键值对追加到链表的末尾
    7. 当table[i]为红黑树的根节点,在红黑树中插入新值/覆盖旧值
    8. 根据当前节点个数进行调整,否需要转换成红黑树(个数大于等于8,就会调用treeifyBin方法将tabel[i]第i个散列桶拉链转换成红黑树)
    9. 对当前容量大小进行检查,如果超过了临界值(实际大小*加载因子)就进行扩容
    final V putVal(K key, V value, boolean onlyIfAbsent) {
        // key 和 value 均不允许为 null
        if (key == null || value == null) throw new NullPointerException();
        // 根据 key 计算出 hash 值
        int hash = spread(key.hashCode());
        int binCount = 0;
        for (Node<K,V>[] tab = table;;) {
            Node<K,V> f; int n, i, fh;
             // 判断是否需要进行初始化
            if (tab == null || (n = tab.length) == 0)
                tab = initTable();
            // f 即为当前 key 定位出的 Node,如果为空表示当前位置可以写入数据,利用 CAS 尝试写入,失败则自旋保证成功
            else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
                if (casTabAt(tab, i, null,
                             new Node<K,V>(hash, key, value, null)))
                    break;                   // no lock when adding to empty bin
            }
            // 如果当前位置的 hashcode == MOVED == -1,则需要进行扩容
            else if ((fh = f.hash) == MOVED)
                tab = helpTransfer(tab, f);
            // 如果都不满足,则利用 synchronized 锁写入数据
            else {
              // 剩下情况又分两种,插入链表、插入红黑树
                V oldVal = null;
              //采用同步内置锁实现并发控制
                synchronized (f) {
                    if (tabAt(tab, i) == f) {
                        // 如果 fh=f.hash >=0,当前为链表,在链表中插入新的键值对
                        if (fh >= 0) {
                            binCount = 1;
                          //遍历链表,如果找到对应的 node 节点,修改 value,否则直接在链表尾部加入节点
                            for (Node<K,V> e = f;; ++binCount) {
                                K ek;
                                if (e.hash == hash &&
                                    ((ek = e.key) == key ||
                                     (ek != null && key.equals(ek)))) {
                                    oldVal = e.val;
                                    if (!onlyIfAbsent)
                                        e.val = value;
                                    break;
                                }
                                Node<K,V> pred = e;
                                if ((e = e.next) == null) {
                                    pred.next = new Node<K,V>(hash, key,
                                                              value, null);
                                    break;
                                }
                            }
                        }
                        // 当前为红黑树,将新的键值对插入到红黑树中
                        else if (f instanceof TreeBin) {
                            Node<K,V> p;
                            binCount = 2;
                            if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
                                                           value)) != null) {
                                oldVal = p.val;
                                if (!onlyIfAbsent)
                                    p.val = value;
                            }
                        }
                    }
                }
                // 插入完键值对后再根据实际大小看是否需要转换成红黑树
                if (binCount != 0) {
                    if (binCount >= TREEIFY_THRESHOLD)
                        treeifyBin(tab, i);
                    if (oldVal != null)
                        return oldVal;
                    break;
                }
            }
        }
        // 对当前容量大小进行检查,如果超过了临界值(实际大小*加载因子)就需要扩容 
        addCount(1L, binCount);
        return null;
    }
    

    我们可以发现JDK8中的实现也是锁分离的思想,只是锁住的是一个Node,而不是JDK7中的Segment,而锁住Node之前的操作是无锁的并且也是线程安全的,建立在之前提到的原子操作上。

    get() 方法

    get方法无需加锁,由于其中涉及到的共享变量都使用volatile修饰,volatile可以保证内存可见性,所以不会读取到过期数据

    public V get(Object key) {
      Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
      int h = spread(key.hashCode());
      // 判断数组是否为空
      if ((tab = table) != null && (n = tab.length) > 0 &&
          (e = tabAt(tab, (n - 1) & h)) != null) {
        // 判断node 节点第一个元素是不是要找的,如果是直接返回
        if ((eh = e.hash) == h) {
          if ((ek = e.key) == key || (ek != null && key.equals(ek)))
            return e.val;
        }
        // // hash小于0,说明是特殊节点(TreeBin或ForwardingNode)调用find
        else if (eh < 0)
          return (p = e.find(h, key)) != null ? p.val : null;
        // 不是上面的情况,那就是链表了,遍历链表
        while ((e = e.next) != null) {
          if (e.hash == h &&
              ((ek = e.key) == key || (ek != null && key.equals(ek))))
            return e.val;
        }
      }
      return null;
    }
    

    Hashtable 和 ConcurrentHashMap 的区别

    ConcurrentHashMap 和 Hashtable 的区别主要体现在实现线程安全的方式上不同。

    • 底层数据结构: JDK1.7的 ConcurrentHashMap 底层采用 分段的数组+链表 实现,JDK1.8 采用的数据结构和HashMap1.8的结构类似,数组+链表/红黑二叉树。Hashtable 和 JDK1.8 之前的 HashMap 的底层数据结构类似都是采用 数组+链表 的形式,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的;
    • 实现线程安全的方式(重要):
      • 在JDK1.7的时候,ConcurrentHashMap(分段锁) 对整个桶数组进行了分割分段(Segment),每一把锁只锁容器其中一部分数据,多线程访问容器里不同数据段的数据,就不会存在锁竞争,提高并发访问率。(默认分配16个Segment,比Hashtable效率提高16倍。) 到了 JDK1.8 的时候已经摒弃了Segment的概念,而是直接用 Node 数组+链表/红黑树的数据结构来实现,并发控制使用 synchronized 和 CAS 来操作。(JDK1.6以后 对 synchronized锁做了很多优化) 整个看起来就像是优化过且线程安全的 HashMap,虽然在 JDK1.8 中还能看到 Segment 的数据结构,但是已经简化了属性,只是为了兼容旧版本;
      • Hashtable(同一把锁) :使用 synchronized 来保证线程安全,效率非常低下。当一个线程访问同步方法时,其他线程也访问同步方法,可能会进入阻塞或轮询状态,如使用 put 添加元素,另一个线程不能使用 put 添加元素,也不能使用 get,竞争越激烈效率越低。

    Java快速失败(fail-fast)和安全失败(fail-safe)区别

    快速失败(fail—fast)

    在用迭代器遍历一个集合对象时,如果遍历过程中对集合对象的内容进行了修改(增加、删除、修改),则会抛出ConcurrentModificationException。

    原理:迭代器在遍历时直接访问集合中的内容,并且在遍历过程中使用一个 modCount 变量。集合在被遍历期间如果内容发生变化,就会改变 modCount 的值。每当迭代器使用 hashNext()/next() 遍历下一个元素之前,都会检测 modCount 变量是否为 expectedmodCount 值,是的话就返回遍历;否则抛出异常,终止遍历。

    注意:这里异常的抛出条件是检测到 modCount!=expectedmodCount 这个条件。如果集合发生变化时修改modCount 值刚好又设置为了 expectedmodCount 值,则异常不会抛出。因此,不能依赖于这个异常是否抛出而进行并发操作的编程,这个异常只建议用于检测并发修改的bug。

    场景:java.util包下的集合类都是快速失败的,不能在多线程下发生并发修改(迭代过程中被修改)。

    安全失败(fail—safe)

    采用安全失败机制的集合容器,在遍历时不是直接在集合内容上访问的,而是先复制原有集合内容,在拷贝的集合上进行遍历。

    原理:由于迭代时是对原集合的拷贝进行遍历,所以在遍历过程中对原集合所作的修改并不能被迭代器检测到,所以不会触发 Concurrent Modification Exception。

    缺点:基于拷贝内容的优点是避免了Concurrent Modification Exception,但同样地,迭代器并不能访问到修改后的内容,即:迭代器遍历的是开始遍历那一刻拿到的集合拷贝,在遍历期间原集合发生的修改迭代器是不知道的。

    场景:java.util.concurrent包下的容器都是安全失败,可以在多线程下并发使用,并发修改。

    快速失败和安全失败是对迭代器而言的。 快速失败:当在迭代一个集合的时候,如果有另外一个线程在修改这个集合,就会抛出ConcurrentModification异常,java.util下都是快速失败。 安全失败:在迭代时候会在集合二层做一个拷贝,所以在修改集合上层元素不会影响下层。在java.util.concurrent下都是安全失败

    如何避免fail-fast ?

    • 在单线程的遍历过程中,如果要进行remove操作,可以调用迭代器 ListIterator 的 remove 方法而不是集合类的 remove方法。看看 ArrayList中迭代器的 remove方法的源码,该方法不能指定元素删除,只能remove当前遍历元素。
    public void remove() {
        if (lastRet < 0)
            throw new IllegalStateException();
        checkForComodification();
    
        try {
            SubList.this.remove(lastRet);
            cursor = lastRet;
            lastRet = -1;
            expectedModCount = ArrayList.this.modCount;  //
        } catch (IndexOutOfBoundsException ex) {
            throw new ConcurrentModificationException();
        }
    }
    
    • 使用并发包(java.util.concurrent)中的类来代替 ArrayList 和 hashMap
      • CopyOnWriterArrayList 代替 ArrayList
      • ConcurrentHashMap 代替 HashMap

    Iterator 和 Enumeration 区别

    在Java集合中,我们通常都通过 “Iterator(迭代器)” 或 “Enumeration(枚举类)” 去遍历集合。

    public interface Enumeration<E> {
        boolean hasMoreElements();
        E nextElement();
    }
    
    public interface Iterator<E> {
        boolean hasNext();
        E next();
        void remove();
    }
    
    • 函数接口不同,Enumeration只有2个函数接口。通过Enumeration,我们只能读取集合的数据,而不能对数据进行修改。Iterator只有3个函数接口。Iterator除了能读取集合的数据之外,也能数据进行删除操作。
    • Iterator支持 fail-fast机制,而Enumeration不支持。Enumeration 是JDK 1.0添加的接口。使用到它的函数包括Vector、Hashtable等类,这些类都是JDK 1.0中加入的,Enumeration存在的目的就是为它们提供遍历接口。Enumeration本身并没有支持同步,而在Vector、Hashtable实现Enumeration时,添加了同步。
      而Iterator 是JDK 1.2才添加的接口,它也是为了HashMap、ArrayList等集合提供遍历接口。Iterator是支持fail-fast机制的:当多个线程对同一个集合的内容进行操作时,就可能会产生fail-fast事件

    Comparable 和 Comparator接口有何区别?

    Java中对集合对象或者数组对象排序,有两种实现方式:

    • 对象实现Comparable 接口

      • Comparable 在 java.lang 包下,是一个接口,内部只有一个方法 compareTo()

        public interface Comparable<T> {
            public int compareTo(T o);
        }
        
      • Comparable 可以让实现它的类的对象进行比较,具体的比较规则是按照 compareTo 方法中的规则进行。这种顺序称为 自然顺序

      • 实现了 Comparable 接口的 List 或则数组可以使用 Collections.sort() 或者 Arrays.sort() 方法进行排序

    • 定义比较器,实现 Comparator接口

      • Comparator 在 java.util 包下,也是一个接口,JDK 1.8 以前只有两个方法:

        public interface Comparator<T> {
            public int compare(T lhs, T rhs);
            public boolean equals(Object object);
        }
        

    comparable相当于内部比较器。comparator相当于外部比较器

    区别:

    • Comparator 位于 java.util 包下,而 Comparable 位于 java.lang 包下

    • Comparable 接口的实现是在类的内部(如 String、Integer已经实现了 Comparable 接口,自己就可以完成比较大小操作),Comparator 接口的实现是在类的外部(可以理解为一个是自已完成比较,一个是外部程序实现比较)

    • 实现 Comparable 接口要重写 compareTo 方法, 在 compareTo 方法里面实现比较。一个已经实现Comparable 的类的对象或数据,可以通过 Collections.sort(list) 或者 Arrays.sort(arr)实现排序。通过 Collections.sort(list,Collections.reverseOrder()) 对list进行倒序排列。

    • 实现Comparator需要重写 compare 方法

    HashSet

    HashSet是用来存储没有重复元素的集合类,并且它是无序的。HashSet 内部实现是基于 HashMap ,实现了 Set 接口。

    从 HahSet 提供的构造器可以看出,除了最后一个 HashSet 的构造方法外,其他所有内部就是去创建一个 Hashap 。没有其他的操作。而最后一个构造方法不是 public 的,所以不对外公开。

    HashSet如何检查重复

    HashSet的底层其实就是HashMap,只不过我们HashSet是实现了Set接口并且把数据作为K值,而V值一直使用一个相同的虚值来保存,HashMap的K值本身就不允许重复,并且在HashMap中如果K/V相同时,会用新的V覆盖掉旧的V,然后返回旧的V。

    Iterater 和 ListIterator 之间有什么区别?

    • 我们可以使用Iterator来遍历Set和List集合,而ListIterator只能遍历List
    • ListIterator有add方法,可以向List中添加对象,而Iterator不能
    • ListIterator和Iterator都有hasNext()和next()方法,可以实现顺序向后遍历,但是ListIterator有hasPrevious()和previous()方法,可以实现逆向(顺序向前)遍历。Iterator不可以
    • ListIterator可以定位当前索引的位置,nextIndex()和previousIndex()可以实现。Iterator没有此功能
    • 都可实现删除操作,但是 ListIterator可以实现对象的修改,set()方法可以实现。Iterator仅能遍历,不能修改

    参考与感谢

    所有内容都是基于源码阅读和各种大佬之前总结的知识整理而来,输入并输出,奥利给。

    https://www.javatpoint.com/java-arraylist

    https://www.runoob.com/java/java-collections.html

    https://www.javazhiyin.com/21717.html

    https://yuqirong.me/2018/01/31/LinkedList内部原理解析/

    https://youzhixueyuan.com/the-underlying-structure-and-principle-of-hashmap.html

    《HashMap源码详细分析》http://www.tianxiaobo.com/2018/01/18/HashMap-源码详细分析-JDK1-8/

    《ConcurrentHashMap1.7源码分析》https://www.cnblogs.com/chengxiao/p/6842045.html

    http://www.justdojava.com/2019/12/18/java-collection-15.1/

    原文链接

    https://www.cnblogs.com/lazyegg/p/12841338.html

  • 相关阅读:
    (转)C#特性详解
    PHP MSSQL数据操作PDO API
    DWR
    配置JAVA的环境变量
    关​于​h​i​b​e​r​n​a​t​e​中​双​向​外​键​关​联​o​n​e​-​t​o​-​o​n​e​的​p​r​o​p​e​r​t​y​-​r​e​f​=​的​问​题(转)
    Hibernate 二级缓存 总结整理(转)
    EhCache 分布式缓存/缓存集群(转)
    MySQL分区表(转)
    通用权限设计
    (转)关于Struts 2 拦截器参数丢失问题
  • 原文地址:https://www.cnblogs.com/mjtabu/p/12841417.html
Copyright © 2011-2022 走看看