Java 集合类实现原理

Collection & Map

  1. Collection 子类有 ListSet

  2. List --> ArrayList / LinkedList / Vector

  3. Set --> HashSet / TreeSet

  4. Map --> HashMap / HashTable / TreeMap / LinkedHashMap

一、ArrayList

ArrayList 是 List 接口的可变数组的实现。实现了所有可选列表操作,并允许包括 null 在内的所有元素。除了实现 List 接口外, 此类还提供一些方法来操作内部用来存储列表的数组的大小。

每个 ArrayList 实例都有一个容量,该容量是指用来存储列表元素的数组的大小。它总是至少等于列表的大小。随着向 ArrayList 中不断添加元素,其容量也自动增长 (每次调用添加操作时,都会调用 ensureCapacity 方法,判断是否需要自增,如果需要则自增数组) 。自动增长会带来数据向新数组的重新拷贝,因此,如果可预知数据量的多少,可在构造 ArrayList 时指定其容量。在添加大量元素前,应用程序也可以使用 ensureCapacity 操作来增加 ArrayList 实例的容量,这可以减少递增式再分配的数量。

注意,此实现不是同步的。如果多个线程同时访问一个 ArrayList 实例,而其中至少一个线程从结构上修改了列表,那么它必须保持外部同步。(结构上的修改是指任何添加或删除一个或多个元素的操作,或者显式调整底层数组的大小;仅仅设置元素的值不是结构上的修改。)

不管是 ArrayList、 Vector、LinkedList 他们的 set,remove 方法的返回值都是原来该位置的元素,add 方法返回 boolean 值为是否成功插入

1、实现的接口

  1. 继承 AbstractList (实现了 List 接口)

  2. Cloneable 可克隆, Serializable 可序列化,RandomAccess 为 List 提供快速访问功能(RandomAccess 为空接口,只是一个可以快速访问的标识),即通过序号获取元素

2、构造方法

  1. 创建长度为 10 的数组

  2. 创建指定长度数组,小于 0 抛出异常

  3. 根据集合创建数组,创建长度为集合长度的数组并拷贝

3、增删查方法

  1. 每次操作之前都会创建一个新的数组引用指向被操作数组,使用新的引用操作。

  2. set 方法,指定位置赋值,检查 index ,如果不合法则抛出异常

  3. add 方法,末尾位置添加,如果超出,先创建新数组替换旧数组,新数组长度为旧数组的 1.5 倍再加 1;

  4. add(int index,Object obj) 指定位置添加,检查 index ,如不合法则抛出异常。指定位置插入时,会将原来的数组以 index 为界,将 index 后的数据后移一位,后移的实现通过 System.arraycopy 方法实现。再在 index 位置插入需要插入的数据。 System.arraycopy 为 Native 层的方法,可以高效复制数组元素。

  5. remove(int index) 根据索引删除,直接操作数组,返回值为被移除的对象。将该对象所在位置之后的数组内容复制到从该位置开始,将末尾置为 null

  6. remove(Object obj) 根据对象删除,遍历数组,如果存在,将该对象所在位置之后的数组内容复制到从该位置开始,将末尾置为 null

  7. 当我们可预知要保存的元素的多少时,要在构造 ArrayList 实例时,就指定其容量,以避免数组扩容的发生。或者根据实际需求,通过调用ensureCapacity 方法来手动增加 ArrayList 实例的容量。

  8. ArrayList基于数组实现,可以通过下标索引直接查找到指定位置的元素,因此查找效率高,但每次插入或删除元素,就要大量地移动元素,插入删除元素的效率低。

  9. 在查找给定元素索引值等的方法中,源码都将该元素的值分为null和不为null两种情况处理,ArrayList中允许元素为null。

二、Vector

  1. Vector也是基于数组实现的,是一个动态数组,其容量能自动增长。
  2. Vector是JDK1.0引入了,它的很多实现方法都加入了同步语句,使用 synchronized 修饰,因此是线程安全的(其实也只是相对安全,有些时候还是要加入同步语句来保证线程的安全),可以用于多线程环境。
  3. Vector没有实现 Serializable 接口,因此它不支持序列化,实现了 RandomAccess
  4. Vector 的构造函数中可以指定容量增长系数,如果不指定增长系数,增加时为增加一倍,这点有别于 ArrayList。

Vector的源码实现总体与ArrayList类似,关于Vector的源码,给出如下几点总结:

1、Vector有四个不同的构造方法。无参构造方法的容量为默认值10,仅包含容量的构造方法则将容量增长量(从源码中可以看出容量增长量的作用,第二点也会对容量增长量详细说)明置为0。

2、注意扩充容量的方法ensureCapacityHelper。与ArrayList相同,Vector在每次增加元素(可能是1个,也可能是一组)时,都要调用该方法来确保足够的容量。当容量不足以容纳当前的元素个数时,就先看构造方法中传入的容量增长量参数CapacityIncrement是否为0,如果不为0,就设置新的容量为就容量加上容量增长量,如果为0,就设置新的容量为旧的容量的2倍,如果设置后的新容量还不够,则直接新容量设置为传入的参数(也就是所需的容量),而后同样用Arrays.copyof()方法将元素拷贝到新的数组。

3、很多方法都加入了synchronized同步语句,来保证线程安全。

4、同样在查找给定元素索引值等的方法中,源码都将该元素的值分为null和不为null两种情况处理,Vector中也允许元素为null

5、其他很多地方都与ArrayList实现大同小异,Vector现在已经基本不再使用。

三、LinkedList

LinkedList 和 ArrayList 一样,实现了 List 接口,但其内部的数据结构有本质不同。LinkedList 是基于双向循环链表实现的,所以它的插入和删除操作比 ArrayList 更高效,不过由于是基于链表的,随机访问的效率要比 ArrayList 差。

实现了 Searializable 接口,支持序列化,实现了 Cloneable 接口,可被克隆

是非线程安全的,只是用于单线程环境下,多线程环境下可以采用concurrent并发包下的concurrentHashMap。

1、数据结构

LinkedList 是基于链表结构的实现,每一个节点的类都包含了 previous 和 next 两个 Link 指针对象,由 Link 保存,Link 中包含了上一个节点和下一个节点的引用,这样就构成了双向的链表,每个 Link 只能知道自己的前一个和后一个节点。
注意:不同版本类名不同,但是原理一样,有的版本类名是 Node

Link

private static final class Link<ET> {
    ET data;

    Link<ET> previous, next;

    Link(ET o, Link<ET> p, Link<ET> n) {
        data = o;
        previous = p;
        next = n;
    }
}

2、插入数据

  1. LinkedList 内部的 Link 对象 voidLink ,其 previous 执向链表最后一个对象,next 指向第一个链表第一个对象,初始化 LinkedList 时默认初始化 voidLink 的前后都指向自己。

  2. 注意两个不同的构造方法。无参构造方法直接建立一个仅包含head节点的空链表,包含Collection的构造方法,先调用无参构造方法建立一个空链表,而后将Collection中的数据加入到链表的尾部后面。

  3. 往最后插入,会创建新的 Link 对象,并将 新对象的 previous 赋值为 voidLind 的 previous,将新对象的 next 赋值为 voidLink,最后将 voidLink 的 previous 指向 新对象,将之前 voidLind 的 previous 的对象的 next 指向新对象

  4. 往非末尾插入,会比较 index 与链表的中间值的大小,缩小检索比例,调用从后往前检索或从前往后检索,如果从前往后,会循环调用 voidLink 的 next 方法
    直到需要插入的位置得到当前位置的元素 link (注意,voidLink的 next 指向第一个元素,所以遍历next之后的位置为需要插入的位置),创建新对象,新对象的 previous 指向原来当前元素 link 的 previous ,新对象的 next 指向 link,link 的 previous 执向新对象,原来 link 的 previous 对象的 next 指向 新元素,这样就准确插入。从后往前的道理相同。

  5. LinkedList 获取非首尾元素时,也会使用与插入时相同的判断位置的加速机制

  6. 在查找和删除某元素时,源码中都划分为该元素为 null 和不为 null 两种情况来处理,LinkedList 支持插入的元素为 null

  7. LinkedList是基于链表实现的,因此不存在容量不足的问题,所以这里没有扩容的方法。

  8. LinkedList是基于链表实现的,因此插入删除效率高,查找效率低(虽然有一个加速动作)。

  9. 要注意源码中还实现了栈和队列的操作方法,因此也可以作为栈、队列和双端队列来使用 push(向顶部插入元素)、pop(删除并返回第一个元素) 等方法。

  10. Iterator 中通过元素索引是否等于“双向链表大小”来判断是否达到最后。

四、HashMap

HashMap 是基于哈希表实现的,每一个元素是一个 key-value 对,其内部通过 单链表 解决冲突问题,容量不足(超过了阀值)时,同样会自动增长。

HashMap是非线程安全的,只是用于单线程环境下,多线程环境下可以采用concurrent并发包下的concurrentHashMap。

HashMap 实现了Serializable接口,因此它支持序列化,实现了Cloneable接口,能被克隆

默认长度 16 扩容为 2 倍每次,如果扩容后还是不够则创建目标长度数组,将旧数组复制到新数组中

实现方式为数组,每个数组中都可以是一个单链表,插入时,根据 hashcode 计算在数组中位置,判断是否存在相同元素后,根据情况在相应位置的链表头中插入新元素。

初始容量: 初始哈希数组的长度,默认 16
最大容量: 2 的 30 次幂
加载因子: 默认 0.75
阈值:用于判断是否需要调整 HashMap 容量,等于 容量 * 加载因子

总结

  1. 加载因子,如果加载因子越大,对空间的利用更充分,但是查找效率会降低(链表长度会越来越长);如果加载因子太小,那么表中的数据将过于稀疏(很多空间还没用,就开始扩容了),对空间造成严重浪费。如果我们在构造方法中不指定,则系统默认加载因子为0.75,这是一个比较理想的值,一般情况下我们是无需修改的。

  2. 最大容量,无论我们指定的容量为多少,构造方法都会将实际容量设为不小于指定容量的2的次方的一个数,且最大值不能超过2的30次方。要求为 2 的整数次幂是为了使不同hash值发生碰撞的概率较小,这样就能使元素在哈希表中均匀地散列。

  3. HashMap中key和value都允许为null。

HashMap 的数据结构

HashMap 中的数组就是哈希表,也称为哈希数组,数组的每个元素都是一个单链表的头节点,链表是用来解决冲突的,如果不同的key映射到了数组的同一位置处,就将其放入单链表中。

数组中的每一个元素都是一个 HashMapEntry,也是一个单链表的表头,其 next 指向链表中下一元素。通过 HashMapEntry 中的 key 可以计算出其在数组也就是哈希数组中的位置,得到该位置之后,就可以在链表中根据 key 的 equals 方法确定某元素

其中还有一个成员遍历 entryForNullKey ,表示 key 为 null 的元素,访问和修改 key 为 null 的元素时,直接操作该值,之前是将 key 为 null 的元素放到了数组的第一个位置中的链表中,不同版本处理不同

static class HashMapEntry<K, V> implements Entry<K, V> {
    final K key;
    V value;
    final int hash;
    HashMapEntry<K, V> next;

    HashMapEntry(K key, V value, int hash, HashMapEntry<K, V> next) {
        this.key = key;
        this.value = value;
        this.hash = hash;
        this.next = next;
    }

    public final K getKey() {
        return key;
    }

    public final V getValue() {
        return value;
    }

    public final V setValue(V value) {
        V oldValue = this.value;
        this.value = value;
        return oldValue;
    }

    @Override public final boolean equals(Object o) {
        if (!(o instanceof Entry)) {
            return false;
        }
        Entry<?, ?> e = (Entry<?, ?>) o;
        return Objects.equal(e.getKey(), key)
                && Objects.equal(e.getValue(), value);
    }

    @Override public final int hashCode() {
        return (key == null ? 0 : key.hashCode()) ^
                (value == null ? 0 : value.hashCode());
    }

    @Override public final String toString() {
        return key + "=" + value;
    }
}

插入 put(K key, V value)

@Override public V put(K key, V value) {
    if (key == null) {
        return putValueForNullKey(value);
    }

    int hash = Collections.secondaryHash(key);
    HashMapEntry<K, V>[] tab = table;
    int index = hash & (tab.length - 1);
    for (HashMapEntry<K, V> e = tab[index]; e != null; e = e.next) {
        if (e.hash == hash && key.equals(e.key)) {
            preModify(e);
            V oldValue = e.value;
            e.value = value;
            return oldValue;
        }
    }

    // No entry for (non-null) key is present; create one
    modCount++;
    if (size++ > threshold) {
        tab = doubleCapacity(); // 每次加入键值对时,都要判断当前已用的槽的数目是否大于等于阀值(容量*加载因子),如果大于等于,则进行扩容,将容量扩为原来容量的2倍。
        index = hash & (tab.length - 1);
    }
    addNewEntry(key, value, hash, index);
    return null;
}

void addNewEntry(K key, V value, int hash, int index) {
    table[index] = new HashMapEntry<K, V>(key, value, hash, table[index]);
}
  1. 如果 key 为 null 且存在 key 为 null 的元素,如果没有则在数组中添加一个 key 为 null 的元素,如果有 key 为 null 的元素,则将该元素对应的 value 设置为新的 value

  2. key 不为 null 情况下,由 key 计算 hash 值,由 hash 值确定该元素在哈希数组中的位置

  3. 如果当前哈希数组该位置中有值,且在当前链表中有 key 与新元素的 key 相同的元素(hash 值一样,equals 也一样) 说明是修改,则将旧元素的 value 更新为新的 value ,并将旧的 value 返回,put 方法结束

  4. 如果当前哈希数组该元素为 null 或者当前位置的链表中没有 key 与新元素 key 相同的元素,那么就是插入操作。HashMap 中元素总数量加一,执行插入方法

  5. 插入时,构造新的 HashEntry ,如果当前位置为 null 就直接放入数组,如果该位置不为 null ,就讲新 HashEntry 的 next 指向当前位置的 HashEntry ,并将数组当前位置赋值为新的 HashEntry,插入结束。插入成功返回值为 null。说明每次put键值对的时候,总是将新的该键值对放在table[bucketIndex]处(即头结点处)。

删除 remove(Object key)

  1. 如果 key 为 null 则直接将 key 为 null 位置置为 null,并将其 value 返回

  2. 如果 key 不为 null,则根据 key 计算 hash 值再计算在哈希数组中的位置

  3. 如果当前位置 HashEntry 不为 null,则遍历单链表,找到元素的 key 与要删除的 key 相同的元素,将其上一位置的 next 指向其 next,将该元素从链表中移除并返回

  4. 如果当前位置为 null,或者遍历完链表没有 key 匹配的元素,直接返回 null

@Override public V remove(Object key) {
    if (key == null) {
        return removeNullKey();
    }
    int hash = Collections.secondaryHash(key);
    HashMapEntry<K, V>[] tab = table;
    int index = hash & (tab.length - 1);
    for (HashMapEntry<K, V> e = tab[index], prev = null;
            e != null; prev = e, e = e.next) {
        if (e.hash == hash && key.equals(e.key)) {
            if (prev == null) {
                tab[index] = e.next;
            } else {
                prev.next = e.next;
            }
            modCount++;
            size--;
            postRemove(e);
            return e.value;
        }
    }
    return null;
}

查询 get(Object key)

public V get(Object key) {
    if (key == null) {
        HashMapEntry<K, V> e = entryForNullKey;
        return e == null ? null : e.value;
    }

    int hash = Collections.secondaryHash(key);
    HashMapEntry<K, V>[] tab = table;
    for (HashMapEntry<K, V> e = tab[hash & (tab.length - 1)];
            e != null; e = e.next) {
        K eKey = e.key;
        if (eKey == key || (e.hash == hash && key.equals(eKey))) {
            return e.value;
        }
    }
    return null;
}

由插入和删除的分析,查询就比较简单了,不再分析

四、HashTable

  1. Hashtable同样是基于哈希表实现的,同样每个元素是一个key-value对,其内部也是通过单链表解决冲突问题,容量不足(超过了阀值)时,同样会自动增长。

  2. Hashtable也是JDK1.0引入的类,是线程安全的,能用于多线程环境中。

  3. Hashtable同样实现了Serializable接口,它支持序列化,实现了Cloneable接口,能被克隆。

针对Hashtable,我们同样给出几点比较重要的总结,但要结合与HashMap的比较来总结

  1. 二者的存储结构和解决冲突的方法都是相同的。

  2. HashTable在不指定容量的情况下的默认容量为11,而HashMap为16,Hashtable不要求底层数组的容量一定要为2的整数次幂,而HashMap则要求一定为2的整数次幂

  3. Hashtable中key和value都不允许为null,而HashMap中key和value都允许为null(key只能有一个为null,而value则可以有多个为null)。但是如果在Hashtable中有类似put(null,null)的操作,编译同样可以通过,因为key和value都是Object类型,但运行时会抛出NullPointerException异常,这是JDK的规范规定的。

  4. Hashtable扩容时,将容量变为原来的2倍加1,而HashMap扩容时,将容量变为原来的2倍

  5. Hashtable计算hash值,直接用key的hashCode(),而HashMap重新计算了key的hash值,Hashtable在求hash值对应的位置索引时,用取模运算,而HashMap在求位置索引时,则用与运算。取模运算开销比较大

五、LinkedHashMap

LinkedHashMap 是 HashMap 的子类,是有序的,放入顺序和访问顺序两种初始化方式

LinkedHashMap 可以用来实现LRU算法

LinkedHashMap 同样是非线程安全的,只在单线程环境下使用

LinkedHashMap 中有个 boolean accessOrder 成员变量,表示双向链表中元素排序规则的标志位。accessOrder为false,表示按插入顺序排序,accessOrder为true,表示按访问顺序排序

数据结构

实际上就是 HashMap 和 LinkedList 两个集合类的存储结构的结合。在 LinkedHashMapMap 中,所有 put 进来的 Entry 都保存在哈希表中,但它又额外定义了一个 head 为头结点的空的双向循环链表,每次 put 进来 HashMapEntry ,除了将其保存到对哈希表中对应的位置上外,还要将其插入到双向循环链表的尾部。

static class LinkedEntry<K, V> extends HashMapEntry<K, V> {
    LinkedEntry<K, V> nxt;
    LinkedEntry<K, V> prv;

    /** Create the header entry */
    LinkedEntry() {
        super(null, null, 0, null);
        nxt = prv = this;
    }

    /** Create a normal entry */
    LinkedEntry(K key, V value, int hash, HashMapEntry<K, V> next,
                LinkedEntry<K, V> nxt, LinkedEntry<K, V> prv) {
        super(key, value, hash, next);
        this.nxt = nxt;
        this.prv = prv;
    }
}

LinkedHashMap 中元素的类型为 LinkedEntry,其继承了 HashMapEntry,并添加了 nxt、prv 两个成员,指向该元素在双链表中的前后节点

put 方法

LinkedHashMap 并没有重写 put 方法,而是重写了 HashMap 中添加元素时调用的 preModify 方法和 addNewEntry 方法,proModify 在 HashMap 为空实现,在 LinkedHashMap 中调用了 makeTail 方法,接着来看:

preModify 方法是加入元素时判断有对应 key 元素存在的情况执行的方法,说明原来的哈希数组中跟双向链表中有该元素,在哈希数组中会直接修改该元素的 value 值,但是双链表中的处理确有不同。此时会先将链表中该处的元素移除,再重新将元素的 value 赋值,之后重新将元素接到链表的尾端。

// 先将链表中该处的元素移除,再重新将元素的 value 赋值,之后重新将元素接到链表的尾端
private void makeTail(LinkedEntry<K, V> e) {
    // Unlink e
    e.prv.nxt = e.nxt;
    e.nxt.prv = e.prv;

    // Relink e as tail
    LinkedEntry<K, V> header = this.header;
    LinkedEntry<K, V> oldTail = header.prv;
    e.nxt = header;
    e.prv = oldTail;
    oldTail.nxt = header.prv = e;
    modCount++;
}

// 如果原链表中没有匹配的 key 对应的元素,则直接将新元素添加到尾端
// 并将新元素的 next 执行原来哈希数组中该位置元素,并为哈希数组中对应位置赋值为新元素
@Override void addNewEntry(K key, V value, int hash, int index) {
    LinkedEntry<K, V> header = this.header;
    
    // Remove eldest entry if instructed to do so.
    LinkedEntry<K, V> eldest = header.nxt;
    if (eldest != header && removeEldestEntry(eldest)) { // removeEldestEntry 默认返回 false,作用最后再说
        remove(eldest.key);
    }

    // Create new entry, link it on to list, and put it into table
    LinkedEntry<K, V> oldTail = header.prv;
    LinkedEntry<K, V> newTail = new LinkedEntry<K,V>(
            key, value, hash, table[index], header, oldTail);
    table[index] = oldTail.nxt = header.prv = newTail;
}
  1. 如果当前双链表中有该元素,则将该元素移出链表,再重新将该元素接到双链表尾端
  2. 如果双链表中没有对应元素,则创建一个新的 LinkedEntry ,其前一节点指向链表末端,其下节点指向 header,将其插入到链表末端,并将原来末端的下一节点指向新元素,将 header 的前一节点指向新元素,并将新元素的 next 执行原来哈希数组中该位置元素,并为哈希数组中对应位置赋值为新元素

get(Object key)

由 HashMap 中遍历数组,改为了遍历双链表,效率更高。

需要注意的是如果 LinkedHashMap 的 accessOrder 为 true 时,会将需要获取的元素移出双链表,并重新连接到链表的尾端。

@Override public V get(Object key) {
    /*
     * This method is overridden to eliminate the need for a polymorphic
     * invocation in superclass at the expense of code duplication.
     */
    if (key == null) {
        HashMapEntry<K, V> e = entryForNullKey;
        if (e == null)
            return null;
        if (accessOrder)
            makeTail((LinkedEntry<K, V>) e);
        return e.value;
    }

    int hash = Collections.secondaryHash(key);
    HashMapEntry<K, V>[] tab = table;
    for (HashMapEntry<K, V> e = tab[hash & (tab.length - 1)];
            e != null; e = e.next) {
        K eKey = e.key;
        if (eKey == key || (e.hash == hash && key.equals(eKey))) {
            if (accessOrder)
                makeTail((LinkedEntry<K, V>) e);
            return e.value;
        }
    }
    return null;
}

remove(Object key)

LinkedHashMap 重写了 HashMap 的 postRemove 方法,HashMap 的 remove 方法中会将哈希数组中的元素移除,同时还会调用 postRemove 方法,该方法在 HashMap 中是空实现,在 LinkedHashMap 中实现如下:

@Override void postRemove(HashMapEntry<K, V> e) {
    LinkedEntry<K, V> le = (LinkedEntry<K, V>) e;
    le.prv.nxt = le.nxt;
    le.nxt.prv = le.prv;
    le.nxt = le.prv = null; // Help the GC (for performance)
}

postRemove 方法中,会将对应元素在双链表中删除

LinkedHashMap 实现 LRU 算法

刚才分析 addNewEntry 时提到了 removeEldestEntry 方法,其在 LinkedHashMap 中是个空实现


@Override void addNewEntry(K key, V value, int hash, int index) {
    ...
    LinkedEntry<K, V> eldest = header.nxt;
    if (eldest != header && removeEldestEntry(eldest)) {
        remove(eldest.key);
    }
    ...
}

protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
    return false;
}

在每次添加元素是都会调用 removeEldestEntry 方法,如果该方法返回 true 则删除 header.nxt 处的元素,其实也就是删除哈希数组中对应元素与双向链表的头部的元素,因为在 accessOrder 为 true 时每次插入和访问都会将最近访问的元素移动到双向链表的尾部,这样链表头部的元素就是最久没有被访问到的。在 removeEldestEntry 中可以根据当前链表节点数到达最大容量时返回 true,此时就会删除链表头部节点,这样就完成了 LRU 算法。

总结

  1. 实际上就是HashMap和LinkedList两个集合类的存储结构的结合。在LinkedHashMapMap中,所有put进来的Entry都保存在如第一个图所示的哈希表中,但它又额外定义了一个以head为头结点的空的双向循环链表,每次put进来Entry,除了将其保存到对哈希表中对应的位置上外,还要将其插入到双向循环链表的尾部。

2、LinkedHashMap由于继承自HashMap,因此它具有HashMap的所有特性,同样允许key和value为null。

  1. 注意构造方法,前四个构造方法都将accessOrder设为false,说明默认是按照插入顺序排序的,而第五个构造方法可以自定义传入的accessOrder的值,因此可以指定双向循环链表中元素的排序规则,一般要用LinkedHashMap实现LRU算法,就要用该构造方法,将accessOrder置为true。

  2. 最后说说LinkedHashMap是如何实现LRU的。首先,当accessOrder为true时,才会开启按访问顺序排序的模式,才能用来实现LRU算法。我们可以看到,无论是put方法还是get方法,都会导致目标Entry成为最近访问的Entry,因此便把该Entry加入到了双向链表的末尾(get方法通过调用recordAccess方法来实现,put方法在覆盖已有key的情况下,也是通过调用recordAccess方法来实现,在插入新的Entry时,则是通过createEntry中的addBefore方法来实现),这样便把最近使用了的Entry放入到了双向链表的后面,多次操作后,双向链表前面的Entry便是最近没有使用的,这样当节点个数满的时候,删除的最前面的Entry(head后面的那个Entry)便是最近最少使用的Entry。

六、TreeMap

TreeMap是基于红黑树实现的,这里只对红黑树做个简单的介绍,红黑树是一种特殊的二叉排序树,红黑树通过一些限制,使其不会出现二叉树排序树中极端的一边倒的情况,相对二叉排序树而言,这自然提高了查询的效率。

红黑树规则

  1. 每个节点都只能是红色或者黑色
  2. 根节点是黑色
  3. 每个叶节点(NIL节点,空节点)是黑色的。
  4. 如果一个结点是红的,则它两个子节点都是黑的。也就是说在一条路径上不能出现相邻的两个红色结点。
  5. 从任一节点到其每个叶子的所有路径都包含相同数目的黑色节点。

正是这些性质的限制,使得红黑树中任一节点到其子孙叶子节点的最长路径不会长于最短路径的2倍,因此它是一种接近平衡的二叉树。

构造方法

  1. 采用无参构造方法,不指定比较器,这时候,排序的实现要依赖key.compareTo()方法,因此key必须实现Comparable接口,并覆写其中的compareTo方法。

  2. 采用带比较器的构造方法,这时候,排序依赖该比较器,key可以不用实现Comparable接口。

  3. 带Map的构造方法,该构造方法同样不指定比较器,调用putAll方法将Map中的所有元素加入到TreeMap中。putAll的源码如下:

  1. TreeMap是根据key进行排序的,它的排序和定位需要依赖比较器或覆写Comparable接口,也因此不需要key覆写hashCode方法和equals方法,就可以排除掉重复的key,而HashMap的key则需要通过覆写hashCode方法和equals方法来确保没有重复的key。

  2. TreeMap的查询、插入、删除效率均没有HashMap高,一般只有要对key排序时才使用TreeMap。

  3. TreeMap的key不能为null,而HashMap的key可以为null。

七、HashSet

实现原理基于 HashMap set 中的 value 都一样,key 为添加的元素

HashSet 中有一个 HashMap 成员,每次操作时都是操作该 HashMap,操作的元素的 key 为 Set 中要操作的元素,value 为 HashSet 本身的引用。

八、TreeSet

实现原理基于 TreeMap

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 156,630评论 4 359
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,405评论 1 289
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 106,382评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,548评论 0 203
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 51,853评论 3 285
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,276评论 1 209
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,638评论 2 309
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,350评论 0 195
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,015评论 1 238
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,309评论 2 240
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,836评论 1 256
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,206评论 2 251
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,767评论 3 231
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 25,972评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,715评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,319评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,222评论 2 258

推荐阅读更多精彩内容