Java 中的容器

List(链表):常用的 ArrayList 和 LinkedList

  1. ArrayList 底层采用数组方式存储:
ArrayList.class 部分源码

// ArrayList 将所有的元素存储到 elementData 数组中
transient Object[] elementData;

// 实际存储方法
public boolean add(E e) {
    // 存储前进行剩余容量判断,防止数组元素溢出
    ensureCapacityInternal(size + 1); 
    elementData[size++] = e;
    return true;
}

// ensureCapacityInternal(...) 会调用 该方法进行扩容
// 对 elementData 数组进行扩容
private void grow(int minCapacity) {
    // overflow-conscious code
    int oldCapacity = elementData.length;
    // TODO 这个增长因子是重点
    // 进行数组扩容,oldCapacity >> 1 结果为 oldCapacity 的一半,也就是说:每次扩容的增长因子为 1.5 ,即原来的 1.5 倍
    int newCapacity = oldCapacity + (oldCapacity >> 1);
    if (newCapacity - minCapacity < 0)
    newCapacity = minCapacity;
    if (newCapacity - MAX_ARRAY_SIZE > 0)
    newCapacity = hugeCapacity(minCapacity);
    // 将原来的数组复制到长度为 newCapacity 的 新数组中,返回给 elementData
    elementData = Arrays.copyOf(elementData, newCapacity);
}

  1. LinkedList 底层采用链表形式存储:
LinkedList.class 部分源码

// 底层的存储结构
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;
    }
}

LinkedList 类中,有三个比较关键的成员变量:

// 链表首元素
transient Node<E> first;
// 链表末尾元素
transient Node<E> last;
// 链表长度
transient int size = 0;

// 添加元素的方法如下:
public boolean add(E e) {
    linkLast(e);
    return true;
}
// 实际添加操作由该方法完成
void linkLast(E e) {
	// 获取到链表中的最后一个元素
    final Node<E> l = last;
    // 为当前添加的元素创建一个新的 Node 对象,指定新 Node 对象的上一个节点是 l,
    // 注意,没有对 Node 的 next 属性赋值。
    final Node<E> newNode = new Node<>(l, e, null);
    // 将最后一个元素调整为上一步新建的 Node 对象
    last = newNode;
    if (l == null)
    first = newNode;
    else
    l.next = newNode;
    size++;
    modCount++;
}

Set(集合):常用实现类 HashSet

HashSet 是通过持有一个 HashMap 的变量,来进行数据的存储的:

HashSet.class
    
// 实际进行元素存取操作的对象
private transient HashMap<E,Object> map;
// add(...) 方法会用到它
private static final Object PRESENT = new Object();

// HashSet 的构造方法
public HashSet() {
	// map 变量初始化
	map = new HashMap<>();
}
// 当向 HashSet 中添加元素时,元素是存储到了 HashMap 的 key 中,value 存储的是一个 Object 对象
public boolean add(E e) {
    return map.put(e, PRESENT)==null;
}

Map(key-value形式的容器):

常用的实现类有:Hashtable、HashMap、ConcurrentHashMap


  1. Hashtable:Java 早期的 Key-Value 形式的存储容器,JDK1.0版本便已经存在。来看源码:
// 类声明如下:
public class Hashtable<K,V> extends Dictionary<K,V> implements Map<K,V>, Cloneable, java.io.Serializable {
    //通过一个 Entry 类型的数组来进行元素的存取操作的
    private transient Entry<?,?>[] table;
    
    // 上面那个 Entry 类型,是 Hashtable 的一个私有内部类
    private static class Entry<K,V> implements Map.Entry<K,V> {
        // 只需要关注这四个属性就可以
        // 通过 key.hashCode() 方法生成
        final int hash;
        // 元素的 key 值
        final K key;
        // 元素的 value 值
        V value;
        // 当前元素的下一个元素
        Entry<K,V> next;
    	
        // other Method ...
    }
    
    // 当前的 哈希表中的总的元素个数
    private transient int count;
    // 当 count 的大小超过 threshold 时,会对 哈希表进行重新整理,可以理解为扩容;
    // threshold = 初始化的 table 长度 * loadFactor
    private int threshold;
    // 哈希表的负载因子
    private float loadFactor;
    
    // Hashtable 的默认构造方法
    public Hashtable() {
        // 这里的这两个参数:
        //	11 表示 初始化的 table 数组的长度
        //	0.75f 表示 默认的负载因子,即 loadFactor = 0.75f
        this(11, 0.75f);
    }
    
    // 来看一下 Hashtable 的 put(key,value) 方法
    // 通过 synchronized 关键字修饰,意味着方法是同步的
    public synchronized V put(K key, V value) {
        // 保证 Hashtable 的 value 不能为空
        if (value == null) {
            throw new NullPointerException();
        }

        Entry<?,?> tab[] = table;
        int hash = key.hashCode();
        // 通过 hash 值来获取到元素在 table 数组中存储的索引
        int index = (hash & 0x7FFFFFFF) % tab.length;
        // 获取到该索引下的第一个 entry 元素
        @SuppressWarnings("unchecked")
        Entry<K,V> entry = (Entry<K,V>)tab[index];
        // 从 entry 开始依次调用其 next 属性,进行 hash值 与 key值 的判断,当新增元素的 hash值 与 key值 在 table 数组中存		  // 在,不管 value 是否相等,都用 新增元素的 value 代替已存在的 value 
        // 这里可能不是很理解,没关系,先往下看,后续会画一个 Hashtable 存储元素的 结构图,结合着图会很容易明白的
        for(; entry != null ; entry = entry.next) {
            if ((entry.hash == hash) && entry.key.equals(key)) {
                V old = entry.value;
                entry.value = value;
                return old;
            }
        }
	   // 当 table (哈希表) 中,索引为 hash 的位置为空时,则将当前添加的元素置为索引下的第一个元素
        addEntry(hash, key, value, index);
        return null;
    }
    
    private void addEntry(int hash, K key, V value, int index) {
        ...
        Entry<?,?> tab[] = table;
        // 当 count >= threshold 时,进行 table 扩容
        if (count >= threshold) {
            rehash();
            tab = table;
            hash = key.hashCode();
            index = (hash & 0x7FFFFFFF) % tab.length;
        }

        // 获取到索引位置,将要存储的对象放到链表头的位置。
        @SuppressWarnings("unchecked")
        Entry<K,V> e = (Entry<K,V>) tab[index];
        tab[index] = new Entry<>(hash, key, value, e);
        count++;
    }
    
    // 再来看最后一个 rehash() 方法,这个方法是对 table 进行实际扩容的方法
    protected void rehash() {
        int oldCapacity = table.length;
        Entry<?,?>[] oldMap = table;

        // 新的长度 = 老的长度 * 2 + 1
        int newCapacity = (oldCapacity << 1) + 1;
        // 新长度最大为 MAX_ARRAY_SIZE,即 2 的 31次方 - 9
        if (newCapacity - MAX_ARRAY_SIZE > 0) {
            if (oldCapacity == MAX_ARRAY_SIZE)
                return;
            newCapacity = MAX_ARRAY_SIZE;
        }
        Entry<?,?>[] newMap = new Entry<?,?>[newCapacity];
        ...
        threshold = (int)Math.min(newCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
        table = newMap;
	    // 将老的哈希表中的元素依次存入新哈希表
        for (int i = oldCapacity ; i-- > 0 ;) {
            for (Entry<K,V> old = (Entry<K,V>)oldMap[i] ; old != null ; ) {
                Entry<K,V> e = old;
                old = old.next;
                int index = (e.hash & 0x7FFFFFFF) % newCapacity;
                e.next = (Entry<K,V>)newMap[index];
                newMap[index] = e;
            }
        }
    }
    
}

Hashtable 的数据结构图在这里:
在这里插入图片描述
另外,还有很重要的一点需要记住:Hashtable 对外提供的方法都是经过 synchronized 关键字修饰的,也就是说,这些方法都是同步方法。


  1. HashMap:JDK1.2 以后出现的,功能与 Hashtable 大致相同,只是操作方法不是同步的,并且支持 key、value 为 null
// 来看一下 HashMap 类的 部分关键方法
public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable {
	// 默认的负载因子
	static final float DEFAULT_LOAD_FACTOR = 0.75f;
	// 所有的元素都存储在这个 Node 类型的数组对象中
    transient Node<K,V>[] table;
	public HashMap() {
		// 将负载因子设置为 0.75f;也就是说:当哈希表中实际存储元素数量占哈希表容量的 0.75 倍时,会进行 扩容操作;
		// 增长因子为 2,即容量增长一倍;关于增长因子先不用管,稍后我们再来看它的具体实现代码
        this.loadFactor = DEFAULT_LOAD_FACTOR;
    }
    
    // HashMap 比较复杂的是它的 put(key,value) 方法,方法有点长,我尽量标注的清楚一些
    public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }
    
    // 实际进行 put 操作的 方法
    // 参数说明:
    //	hash : 生成方式为 (h = key.hashCode()) ^ (h >>> 16)
    //	key : 当前的 key 值
    //	value : 当前的 value 值
    //	onlyIfAbsent :如果设置为 true,则当哈希表中储存在当前的相同 hash、key 的 key-value 对时,不覆盖已经存在的 value
    // 	evict : 如果为 false,则哈希表处于创建模式
    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {
        Node<K,V>[] tab = table; 
        Node<K,V> p; 
        int n = tab.length;
        int i;
        // 当 这个 HashMap 对象为刚创建的对象时,table 这个成员变量还未被初始化,
        // 则初始化 tab,由于 tab 指向 table,所以,相当于初始化了 table
        if (tab == null || n == 0)
            n = (tab = resize()).length;
        // 这里分为两种情况:
        //		1.初始化 tab 后,由于 tab 中还没有元素,所以,存储的元素即为索引下的第一个元素,直接存入
        //		2.(n - 1) & hash 计算结果得到的 tab 该索引下还未存入元素,则直接存入
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
            Node<K,V> e; K k;
            // 如果 tab 索引下的第一个元素恰好与 当前要存入元素的 hash、key 相同,则拿到该元素
            // 直接进行 是否覆盖 value 的判断
            if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            // 如果当前的 HashMap 存储结构是 树状结构
            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) 
                            treeifyBin(tab, hash);
                        break;
                    }
                    // 如果索引下的链表中存在 与当前要插入元素的 hash、key 相同的元素,则跳出 for 循环
                    // 直接进行 是否覆盖 value 的判断
                    if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            // 如果哈希表中存在与当前插入元素同 hash、key 的元素,则 根据 onlyIfAbsent 判断是否进行 value 覆盖
            if (e != null) { 
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                ...
                return oldValue;
            }
        }
        ...
        // 当 存储的元素长度 达到一定的阈值时,进行扩容;
        // threshold = 初始化长度 * 0.75f
        if (++size > threshold)
            resize();
        ...
        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) {
            // 当数组长度大于等于 1 << 30 ,即 1073741824 时,则直接将负载阈值设置为 : 2 的 31 次方 -1
            if (oldCap >= MAXIMUM_CAPACITY) {
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
            // 当数组长度大于等于 16 ,且 新长度设置 老长度的两倍后,新长度依然小于 1 << 30 ,即 1073741824 时,
            // 将负载阈值也设置为老负载阈值的两倍
            // 多数情况下,会走该 if 内的设置
            // 也就是说,扩容时,HashMap 的增长因子为 2,即容量增长为原来的两倍
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY)
                newThr = oldThr << 1;
        }
        // 这里省略其他处理
        ...
        return newTab;
    }
}

HashMap 的数据结构图:与Hashtable 的结构图结合着看会更容易理解一些
在这里插入图片描述


  1. ConcurrentHashMap : JDK1.5 以后出现。支持高并发的 key-value 容器

    上面介绍 Hashtable 的时候,提到:Hashtable 实现线程安全的方式是通过给所有的函数增加 synchronized 关键字来保证线程安全,但是呢,由于 synchronized 添加的位置是在方法级别,作为方法的修饰词来使用的,也就是在使用的过程中,当线程调用 synchronized 修饰的方法时,持有的锁是当前的类对象锁(这里可能还没有概念,先不用管他,只需要记住 synchronized 修饰方法时拿的是类对象锁,后面咱们会单独再来说一下多线程中锁的区别),由此造成该方法虽然是线程安全的,但是也是阻塞的,并发性并不是很高(并发性不高:多个线程同时访问这个方法时,只能有一个线程访问成功,其他线程阻塞等待,知道访问成功的那个线程运行完该方法,这时阻塞等待的线程们再去争抢类对象锁,抢到的运行方法,其他阻塞等待,依次循环,知道所有线程运行完该方法)。而并发性不高,则就意味着代码的执行效率不高,所以, 就有了 ConcurrentHashMap 这个支持高并发的 key-value 容器的出现。

    而 ConcurrentHashMap 支持高并发的方式也很简单,就是通过桶锁的方式来实现的,桶锁这个概念是不是有点陌生呢,不用担心,它其实很简单,下面,我们先从 ConcurrentHashMap 的源码看起:

public class ConcurrentHashMap<K,V> extends AbstractMap<K,V> implements ConcurrentMap<K,V>, Serializable {
	
	// 实际存储元素的是一个 Node 数组;留意这里的 volatile 关键字
	// 看吧,ConcurrentHashMap、HashMap、Hashtable 的底层都是通过一个数组类型的对象来进行元素的存储的
	transient volatile Node<K,V>[] table;
	
	// 默认的负载因子,ConcurrentHashMap、HashMap、Hashtable 他们的负载因子都是 0.75f
	private static final float LOAD_FACTOR = 0.75f;
    
    // table 的初始化长度,默认为 16 
    private transient volatile int sizeCtl;
	
    // 下面来看它的 put 方法
    public V put(K key, V value) {
        return putVal(key, value, false);
    }
	
    // 源码这个方法有点长,在这里我省略了部分不需要关心的内容,咱们只看关键的部分
	// 参数说明:
    //		key : key 值
    //		value : value 值
    //		onlyIfAbsent : 如果为 false,则表示当当前存入元素的 hash、key 在 哈希表中已经存在时,则覆盖老的 value 值
    final V putVal(K key, V value, boolean onlyIfAbsent) {
        // ConcurrentHashMap 不允许 key 或者 value 任一为空
        // 这里我们回顾一下:Hashtable 仅是不允许 value 为空
        // 				  HashMap 则允许 key value 为空
        if (key == null || value == null) throw new NullPointerException();
        int hash = spread(key.hashCode());
        int binCount = 0;
        for (Node<K,V>[] tab = table;;) {
            Node<K,V> f; int n, i, fh;
            // 哈希表为空,则证明是第一次向该 ConcurrentHashMap 中添加元素,进行哈希表初始化
            if (tab == null || (n = tab.length) == 0)
                tab = initTable();
            // 当 (n - 1) & hash 这个索引下还是空的时,则将当前插入的元素作为链表的头元素放到索引下;然后直接返回成功
            else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
                if (casTabAt(tab, i, null,new Node<K,V>(hash, key, value, null)))
                    break;                   
            }
            // 这个 else if 不用关注,仅留意 fh = f.hash 即可
            else if ((fh = f.hash) == MOVED)
                tab = helpTransfer(tab, f);
            else {
                V oldVal = null;
                // 使用 synchronized 代码块,要进入 synchronized 代码块的线程需要持有 f 的锁才可以
                // 而 f 则是 table 数组下,某一索引下的头元素,或者说是索引下链表的表头节点
                synchronized (f) {
                    // 双重所判断机制,确定 ConcurrentHashMap 的哈希表结构未被改变,i 索引下的链表头节点依然是 f
                    if (tabAt(tab, i) == f) {
                        if (fh >= 0) {
                            binCount = 1;
                            // 开始通过 next 属性,从链表头开始依次获取下一个 节点
                            for (Node<K,V> e = f;; ++binCount) {
                                K ek;
                                // 如果当前存入节点的 hash、key 与 链表中某节点的 hash、key 相同
                                // 则根据 onlyIfAbsent 判断 是否覆盖老的 value 值 
                                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;
                                // 如果到了链表末尾,链表中不存在 hash、key 与但前插入元素的 hash、key 相同的元素,
                                // 则将新增的元素插入的链表末尾
                                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;
                            }
                        }
                    }
                }
                // 当链表深度超过或者达到 8 时,根据 table 的长度决定是否 进行 链表结构 转化为 二叉树结构
                // 这个 结构转换的代码可以参考 HashMap 的 链表 转 二叉树 结构的那个结构图的介绍,基本大同小异
                if (binCount != 0) {
                    if (binCount >= TREEIFY_THRESHOLD)
                        treeifyBin(tab, i);
                    if (oldVal != null)
                        return oldVal;
                    break;
                }
            }
        }
        ...
        return null;
    }
    
    // 这个方法我们只关心容量扩容的部分
    private final void treeifyBin(Node<K,V>[] tab, int index) {
        Node<K,V> b; int n, sc;
        if (tab != null) {
            // 如果 tab 长度 小于 64 
            if ((n = tab.length) < MIN_TREEIFY_CAPACITY)
                // 该方法中会直接调用 当前的 table 变量,n << 1 为的 table 的长度,而 n << 1 的结果为 table.leng * 2
                // 也就是说,ConcurrentHashMap 的增长因子为 2
                // 至此,ConcurrentHashMap、HashMap、Hashtable 的增长因子我们都已经知道,均为 2 ,即容量增长为原来的 2 倍
                // 同时,还需注意,他们的负载因子均为 0.75f ,即当实际存入元素数量达到 table 的 0.75 倍,则进行扩容
                tryPresize(n << 1);
            else if ...
        }
    }
}

下面这个图是 ConcurrentHashMap 的存储结构图:与 HashMap 大同小异,只是多了一个桶锁的概念
在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值