java 集合类

 图中非常清楚的展示了java集合类中的各种依赖继承关系。所有的元素都实现了Iterator接口,用于遍历集合元素。集合分两大类,Collection和Map,Collection中又分List和Set,Map接口下有HashMap,Hashtable,TreeMap等。

目录

Iterator

Collection

Map

AbstractMap

SortedMap

ArrayList & Vector

LinkedList 

HashSet

TreeSet

HashMap

LinkedHashMap

WeakHashMap

HashTable


Iterator

Iterator模式用于遍历所有集合类的元素,他的设计是为了把所有的集合遍历逻辑抽出来,从而避免向客户暴露集合的内部结构。

1. Iterator

boolean hasNext():这个方法是在遍历的时候,判断是否还有更多的元素
E next(): 返回下一个元素
default void remove():这里涉及到了jdk8的特性,在接口定义中,将方法描述为default-虚拟扩展方法,就可以在接口中进行默认实现,从而提高接口的扩展性,避免在接口扩展的时候,破坏原有的实现。
default void forEachRemaining(Consumer<? super E> action):这个方面一般都用不到,不做具体描述。

2. ListIterator

ListIterator 是对Iterator的扩展,他增加了
boolean hasPrevious()
E previous()
int previousIndex()
void set(E e)
void add(E e) 5个方法。通过前3个方法可以进行向前遍历元素,后面两个set和add 可以插入元素,但是set是将元素插入到链表的最后位置,add是插入到当前返回的元素之前。

Collection

Collection作为一个集合类的顶层接口,他没有直接的子类实现,而是通过实现它的子接口来实现集合容器。Collection的特点是作为一个容器,他可以轻松的帮用户实现数据存储,动态扩容,还有方便的元素遍历和快速的增删改查等操作,这些特点都在接口定义的方法中一一体现出来,相比我们用array来存储数据方便不少。
Collection的子接口主要是三大类分别是List,Set和Queue。这三个接口各有特点。

1.List

是一个顺序存放的容器,他会保存元素的插入顺序,当然元素也可以通过下标位置直接插入和删除。List容器同时允许重复的元素插入和多个null元素。List提供了一个特殊的iterator,叫做ListIterator,这个接口在上文中也有所描述,可以进行双向遍历元素。

2.Set

集合最大的特点是元素不能重复,所有元素都是唯一的存在。Set集合不保证维护元素的顺序。

3.Queue

顾名思义就是队列,队列最大的特点就是FIFO先进先出,与之对应的有栈Stack后进先出。Queue在Collection的基础之上又新增加了几个方法:
offer(E e)与add方法类似,但是当容器存量超出达到上限以后,会插入失败,而报异常。这个方法推荐使用。
poll()返回并且删除队列的头元素,如果队列为空,返回null
element()返回但不删除头元素,如果队列为空,会报异常。
peek()返回但不删除头元素,如果队列为空,返回null。

Map

和Collection一样,Map也是集合容器的一个顶层接口。Map是通过key-value方式存储数据,key值都是唯一的,但key是否能为空,则要看他的不同子类的实现。我们可以把Map看成一个小型的数字字典,通过key值的方式存储数据性能非常快,比如他的子类Hashmap,底层就是通过散列表来实现存储,他的时间复杂度是O(1)。另一个典型的子类Treemap是基于红黑树实现的,时间复杂度为O(log n)。以下将介绍部分方法。

methoddesc
size()获取容器中元素的个数
isEmpty()判断容器中是否保函元素,如果不包含元素,返回true
containsKey(Object key)判断是否保函key值
containsValue(Object value)判断是否保函Value
get(Object key)通过key值获取元素
put(K key, V value)将一个元素通过键值对的方式存入容器,如果以存在这个key,则会替换value。
remove(Object key)移除一个元素
putAll(Map<? extends K, ? extends V> m)将容器中所有元素拷贝到当前容器。
interface Entry这个内部类其实也是一个顶层接口,所有存入容器的元素首先都会被包装成一个entry元素。Map的不同子类都会实现这个接口,而且实现各不相同。原因很简单,每个子类都有独特的数据结构来存储数据,TreeMap用的是红黑树,HashMap用的是散列表,而LinkedHashMap则是用散列表+双向链表实现的。

三个类对Entry的部分实现

#TreeMap:


K key;
V value;
Entry<K,V> left = null;
Entry<K,V> right = null;
Entry<K,V> parent;
boolean color = BLACK;

#HashMap:

final int hash;
final K key;
V value;
Node<K,V> next;

#LinkedHashMap

static class Entry<K,V> extends HashMap.Node<K,V> {
    Entry<K,V> before, after;
    Entry(int hash, K key, V value, Node<K,V> next) {
        super(hash, key, value, next);
    }
}

AbstractMap

AbstractMap这个类没特别的地方,他相当于一个骨架,对Map中的接口方法做了一个最简化的实现,设计他的目的是为了最小化接口实现的压力。

SortedMap

SortedMap是Map的子接口,他是一个排序接口,所有实现他的子类都事排序的,通常都是根据key进行排序。因此所有key值都必须实现Comparator接口。下面列举几个他特有的排序方法。

methoddesc
firstKey()获取第一个元素
lastKey()获取最后一个元素

ArrayList & Vector

  1. ArrayList和Vector都是List的实现类,他两处于同一个地位上的。他们所实现的功能大同小异,源码相似度90%以上。
  2. 他俩的区别,ArrayList是非线程安全,而Vector是线程安全的,那么表现在源码上是怎么样的区别呢?就是在每个ArrayList的方法前,加上synchronized。
// Vector的add方法
public synchronized boolean add(E e) {
    modCount++;
    ensureCapacityHelper(elementCount + 1);
    elementData[elementCount++] = e;
    return true;
}
// ArrayList的add方法
public boolean add(E e) {
    ensureCapacityInternal(size + 1);
    elementData[size++] = e;
    return true;
}

3.他俩都实现了Iterator和LinkIterator接口,具有相同的遍历方式。
4.ArrayList和Vector都具有动态扩容的特性,唯一的区别是,ArrayList扩容后是原来的1.5倍。Vector中有一个capacityIncrement变量,每次扩容都在原来大小基础上增加capacityIncrement。如果capacityIncrement==0,那么就在原大小基础上再扩充一倍。
5.Vector中有一个方法setSize(int newSize),而ArrayList并没有,我觉得这个方法有点鸡肋。setSize允许用户主动设置容器大小,如果newSize小于当前size,那么elementData数组中只会保留newSize个元素,多出来的会设为null。如果newSize大于当前size,那么就扩容到newSize大小,数组中多出来的部分设为null,以后添加元素的时候,之前多出来的部分就会以null的形式存在。

Vector<Integer> v2 = new Vector<Integer>();
    v2.add(1);
    v2.setSize(3);
    v2.add(3);
    System.out.println(v2.size());
    setSize之前:
    [1]
    setSize之后:
    [1, null, null]
    当我再次添加一个元素后:
    [1, null, null, 3]
    所以我觉得这个方法并没有太大实用意义。而且会是用户困惑,出现一些不必要的错误。

6.因为Vector是同步的,所以性能上肯定不如ArrayList,所以在不需要考虑多线程的环境下,建议使用ArrayList。

LinkedList 

一、双向链表

ArrayList是通过数组实现存储,而LinkedList则是通过链表来存储数据,而且他实现的是一个双向链表,简单的说一下什么是双向链表。双向链表是数据结构的一种形式,他的每个节点维护两个指针,prev指向上一个节点,next指向下一个节点。这种结构有什么特点呢?他可以实现双向遍历,这使得在链表中的数据读取变得非常灵活自由。同时,LinkedList中维护了两个指针,一个指向头部,一个指向尾部。维护这两个指针后,可以使得元素从头部插入,也可以使元素从尾部插入。基于方式,用户很容易就能实现FIFO(队列),LIFO(栈)等效果。

1.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;
    }
}
2.FIFO(队列)实现原理:

队列的原理就是每次都从链表尾部添加元素,从链表头部获取元素,就像生活中的排队叫号,总是有个先来后到。


// 队列尾部添加一个元素,建议使用这个,约定俗成吧。
public boolean offer(E e) {
    return add(e);
}
// 队列尾部添加一个元素
public boolean offerLast(E e) {
    addLast(e);
    return true;
}
// offer和offerLast底层调用的都是linkLast这个方法,顾名思义就是将元素添加到链表尾部。
void linkLast(E e) {
    final Node<E> l = last;
    // 创建一个节点,将prev指针指向链表的尾节点。
    final Node<E> newNode = new Node<>(l, e, null);
    // 将last指针指向新创建的这个节点。
    last = newNode;
    if (l == null)
        // 如果当前链表为空,那么将头指针也指向这个节点。
        first = newNode;
    else
        // 将链表的尾节点的next指针指向新建的节点,这样就完整的实现了在链表尾部添加一个元素的功能。
        l.next = newNode;
    size++;
    modCount++;
}
// 在链表头部删除一个元素,建议用这个,别问我为什么,我也不知道
public E poll() {
    final Node<E> f = first;
    return (f == null) ? null : unlinkFirst(f);
}
// 在链表头部删除一个元素
public E pollFirst() {
    final Node<E> f = first;
    return (f == null) ? null : unlinkFirst(f);
}
// poll和pollFirst底层调用的就是这个方法,将链表的头元素删除。
private E unlinkFirst(Node<E> f) {
    // assert f == first && f != null;
    final E element = f.item;
    final Node<E> next = f.next;
    f.item = null;
    f.next = null; // help GC
    first = next;
    if (next == null)
        last = null;
    else
        next.prev = null;
    size--;
    modCount++;
    return element;
}
// 获取头元素,但是不会删除他。
public E peek() {
    final Node<E> f = first;
    return (f == null) ? null : f.item;
}

3.LIFO(栈)实现原理:
栈的原理是每次从头部添加元素,也从头部获取元素,那么后进入的元素反而最先出来。就像我们平时叠盘子,洗好了就一个一个往上放,然后要用了就从上往下一个一个拿。

// 在链表的头部添加一个元素
public void push(E e) {
    addFirst(e);
}
// addFirst调用的就是linkFirst,这段代码就是实现将元素添加的链表头部。
private void linkFirst(E e) {
    final Node<E> f = first;
    // 创建一个新元素,将元素的next指针指向当前的头结点
    final Node<E> newNode = new Node<>(null, e, f);
    // 将头指针指向这个节点。
    first = newNode;
    if (f == null)
        // 如果当前节点为空,则把尾指针指向这个节点。
        last = newNode;
    else
        // 将当前头结点的prev指针指向此结点。
        f.prev = newNode;
    size++;
    modCount++;
}
// 弹出顶部结点。
public E pop() {
    return removeFirst();
}
// removeFirst调用的就是unlinkFirst,unlinkFirst实现将链表顶部元素删除
private E unlinkFirst(Node<E> f) {
    // assert f == first && f != null;
    final E element = f.item;
    final Node<E> next = f.next;
    f.item = null;
    f.next = null; // help GC
    first = next;
    if (next == null)
        last = null;
    else
        next.prev = null;
    size--;
    modCount++;
    return element;
}
// 获取顶部结点,但是不删除
public E peek() {
    final Node<E> f = first;
    return (f == null) ? null : f.item;
}

不管是栈也好,队列也好,元素都是从头部删除的unlinkFirst方法。但是用户在使用的过程中并不只用到上面两张方式,我们也可以从链表尾部删除元素如removeLast,peekLast,pollLast,unlinkLast等方法。

二、存取操作

上文讲到的功能,其实是实现了Deque接口,而现在要讲述的是实现与List的部分功能。那么最典型的操作就是直接对容器元素的读取,因为List容器的一大特点就是顺序存储,元素在容器中的位置和存入时是保持一致的,那么用户在读取元素的时候理所当然就可以通过元素下标来获取,下面就具体介绍这几种方法。

1.将元素插入容器的指定位置

// 将元素插入指定位置
public void add(int index, E element) {
    checkPositionIndex(index);
    if (index == size)
         // 如果位于末尾,则直接插入链表尾部
        linkLast(element);
    else
        // 如果不是位于末尾,那么就将元素插入指定位置的元素之前,
        linkBefore(element, node(index));
}
// 将元素插入指定元素前,链表插入元素是数据结构中最基础的知识,这里就不再赘述了。
void linkBefore(E e, Node<E> succ) {
    // assert succ != null;
    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++;
}

2.获取指定位置元素

public E get(int index) {
    checkElementIndex(index);
    return node(index).item;
}
// 获取指定位置的元素,使用的方式就是对链表进行顺序遍历,直到指定位置位置,不过他的处理越有一些小技巧值得学习,这个技巧也是利用了双向链表的特性。
Node<E> node(int index) {
    // assert isElementIndex(index);
    // 在遍历之前先判断元素位置是位于链表前半部,还是位于后半部,如果位于后半部,那么从尾部向前遍历则可以大大提高效率。
    if (index < (size >> 1)) {
        Node<E> x = first;
        for (int i = 0; i < index; i++)
            x = x.next;
        return x;
    } else {
        Node<E> x = last;
        for (int i = size - 1; i > index; i--)
            x = x.prev;
        return x;
    }
}

三、迭代器实现

LinkedList的迭代器实现有两个,一个是实现了Iterator接口的DescendingIterator,另一个则是实现了ListIterator接口的ListItr。

1.ListItr
ListItr遍历需要指定一个起始值

public ListIterator<E> listIterator(int index) {
    checkPositionIndex(index);
    return new ListItr(index);
}

ListItr会创建一个以index为起始值的迭代器,然后用户便可以以这个位置为起点,实现向前或者向后遍历。

ListItr(int index) {
    // 实例化的时候,将next指针指向指定位置的元素
    next = (index == size) ? null : node(index);
    nextIndex = index;
}
// 向后遍历
public E next() {
    checkForComodification();
    if (!hasNext())
        throw new NoSuchElementException();
    lastReturned = next;
    next = next.next;
    nextIndex++;
    return lastReturned.item;
}
// 向前遍历
public E previous() {
    checkForComodification();
    if (!hasPrevious())
        throw new NoSuchElementException();
    lastReturned = next = (next == null) ? last : next.prev;
    nextIndex--;
    return lastReturned.item;
}

2.DescendingIterator
DescendingIterator迭代器实现的是对链表从尾部向头部遍历的功能,他复用里ListItr中的previous方法,将当前位置指向链表尾部,然后逐个向前遍历。

private class DescendingIterator implements Iterator<E> {
    private final ListItr itr = new ListItr(size());
    public boolean hasNext() {
        return itr.hasPrevious();
    }
    public E next() {
        return itr.previous();
    }
    public void remove() {
        itr.remove();
    }
}

HashSet

前言:

HashSet继承了AbstractSet抽象类并实现了Set接口,AbstractSet的子类还包括TreeSet。
1.HashMap提供键值对的方式存储数据,而HashSet仅仅提供数据存储,并没有键值对应。他获取元素的方式也只能通过遍历的方式逐个获取
2.HashMap在存入数据的时候是更加key值的hash值判断,而HashSet需要重写hashCode和equals两个方法,如果不重写则会调用默认的实现,用户在使用HashSet的时候要特别注意元素的euqals判断,有必要的话要重写一个,以免出现问题。

一、HashSet创建

HashSet的创建其实就是实例化一个HashMap,可以创建默认大小的HashMap实例,也可以指定initialCapacity和loadFactor。

public HashSet() {
    map = new HashMap<>();
}
public HashSet(Collection<? extends E> c) {
    map = new HashMap<>(Math.max((int) (c.size()/.75f) + 1, 16));
    addAll(c);
}
public HashSet(int initialCapacity, float loadFactor) {
    map = new HashMap<>(initialCapacity, loadFactor);
}
public HashSet(int initialCapacity) {
    map = new HashMap<>(initialCapacity);
}

二、在HashSet中添加元素

HashMap中真正存放元素的地方就是HashMap,但是HashMap是K-V的形式,而HashSet中没有Key,那么他们是如何将HashSet中的元素映射到HashMap中的呢?原来作者将HashSet中的元素存放到了map的key中,而value则存放一个无意义的Object对象。这么做的好处还可以保证HashSet中的value值唯一。了解HashMap的同学知道,HashMap中校验key值唯一性的方式是通过hash值,然后根据hash值定位数组中的位置,但是也存在hash冲突的情况,那么解决hash冲突的方式就是在原来数组的位置增加一个链表,将hash值冲突,但是key值不同的元素存放在链表中。那么在判断key值是否相同的时候就用到了equals方法。从上面的描述中可以抓出几个关键点,第一hash值,第二equals方法。这就是为什么我们需要将存入HashSet的元素重写hashCode和euqals方法的根本原因。

public boolean add(E e) {
    return map.put(e, PRESENT)==null;
}

三、HashSet遍历

我找了HashSet的源码中找了一通,然并没有找到get方法,那么我们将如何获取元素呢?我发现了iterator接口,这个接口返回的Iterator并不是HashSet自己维护的Iterator,而是通过返回HashMap的keySet().Iterator,这个迭代器遍历的是HashMap的key值。也就是HashSet中保存的value。

public Iterator<E> iterator() {
    return map.keySet().iterator();
}

TreeSet

前言:

Set接口有三个实现类,HashSet,LinkedHashSet和TreeSet。内部通过维护一个HashMap存储数据,LinkedHashSet继承了HashSet,他与HashSet的唯一区别就是他维护的是一个LinkedHashMap,实现了双向链表的功能,维护数据插入的顺序,这部分内容我将在介绍Map类的时候详细介绍。TreeSet也就是本文要介绍的这个实现类,顾名思义他内部维护的是一个TreeMap,底层是红黑二叉树,他使得集合内都是有序的序列。
TreeSet实现的部分和HashSet非常类似,我不太希望重复描述哪些相似的地方,而把精力放在重点部分,读者可以结合HashSet一起阅读。

一、Comparator和TreeSet的创建

TreeSet的创建和HashSet类似,创建的是一个TreeMap实例。TreeMap是一个什么存在呢?简单的说,他是基于红黑树结构实现的,会对元素进行排序。

public TreeSet() {
    this(new TreeMap<E,Object>());
}
public TreeSet(Comparator<? super E> comparator) {
    this(new TreeMap<>(comparator));
}
public TreeSet(Collection<? extends E> c) {
    this();
    addAll(c);
}
public TreeSet(SortedSet<E> s) {
    this(s.comparator());
    addAll(s);
}

第二个,他通过Comparator实例来创建TreeMap,那么Comparator到底是何方神圣呢?通过阅读Comparator的源码发现,这是一个用于集合类排序的辅助接口,用户需要实现compare方法。如果用户用了这种方式创建TreeSet,那么集合元素就不需要做额外处理,否则集合元素都需要实现Comparable接口,因为Tree在排序的时候会调用compare或者compareTo方法(介绍TreeMap的时候会具体讲解)。

public class MyComparator implements Comparator<Person> {
        @Override
        public int compare(Person o1, Person o2) {
            return o1.age - o2.age;
        }
    }
public class Person {
    public Integer age;
    public Person(Integer value) {
        this.age = value;
    }
}
public static void TreeSetTest() {
    // TreeMap在底层put元素的时候会判断是否存在Comparator实例,如果存在,则每次添加元素排序比较的时候会调用compare接口。
    TreeSet<Person> set = new TreeSet<Person>(new MyComparator());
    Person p1 = new Person(1);
    Person p2 = new Person(1);
    Person p3 = new Person(5);
    Person p4 = new Person(9);
    Person p5 = new Person(10);
    set.add(p1);
    set.add(p2);
    set.add(p3);
    set.add(p4);
    set.add(p5);
    Iterator<Person> i = set.iterator();
    while (i.hasNext()) {
        Person p = (Person) i.next();
        System.out.println(p.age);
    }
}
打印结果:
1
5
9
10

二、NavigableSet接口介绍

// 返回比当前元素小的最近的一个元素
public E lower(E e) {
    return m.lowerKey(e);
}
// 返回小于等于当前元素的最近一个元素
public E floor(E e) {
    return m.floorKey(e);
}
// 返回大于等于当前元素的最近一个元素
public E ceiling(E e) {
    return m.ceilingKey(e);
}
// 返回大于当前元素的最近一个元素
public E higher(E e) {
    return m.higherKey(e);
}

HashMap

前言:

HashMap是Map的一个实现类,这个类很重要,是很多集合类的实现基础,底层用的就是他,比如前文中讲到的HashSet,下文要讲到的LinkedHashMap。我们可以将HashMap看成是一个小型的数字字典,他以key-value的方式保存数据,Key全局唯一,并且key和value都允许为null。

HashMap底层是通过维护一个数据来保存元素。当创建HashMap实例的时候,会通过指定的数组大小以及负载因子等参数创建一个空的数组,当在容器中添加元素的时候,首先会通过hash算法求得key的hash值,再根据hash值确定元素在数组中对应的位置,最后将元素放入数组对应的位置。在添加元素的过程中会出现hash冲突问题,冲突处理的方法就是判断key值是否相同,如果相同则表明是同一个元素,替换value值。如果key值不同,则把当前元素添加到链表尾部。这里引出了一个概念,就是HashMap的数据结构其实是:hash表+单向链表。通过链表的方式把所有冲突元素放在了数组的同一个位置。但是当链表过长的时候会影响HashMap的存取效率。因此我们在实际使用HashMap的时候就需要考虑到这个问题,那么该如何控制hash冲突的出现频率呢?HashMap中有一个负载因子(loadFactor)的概念。容器中实际存储元素的size = loadFactor * 数组长度,一旦容器元素超出了这个size,HashMap就会自动扩容,并对所有元素重新执行hash操作,调整位置。

一、Node结构介绍

Node类实现了Map.Entry接口,他是用于存放数据的实体,是容器中存放数据的最小单元。Node的数据结构是一个单向链表,为什么选用这种结构?那是因前文讲到的,HashMap存放数据的结构是:hash表+单向链表。

static class Node<K,V> implements Map.Entry<K,V> {
    final int hash;
    final K key;
    V value;
    Node<K,V> next;
    Node(int hash, K key, V value, Node<K,V> next) {
        this.hash = hash;
        this.key = key;
        this.value = value;
        this.next = next;
    }
    public final K getKey()        { return key; }
    public final V getValue()      { return value; }
    public final String toString() { return key + "=" + value; }
    public final int hashCode() {
        return Objects.hashCode(key) ^ Objects.hashCode(value);
    }
    public final V setValue(V newValue) {
        V oldValue = value;
        value = newValue;
        return oldValue;
    }
    public final boolean equals(Object o) {
        if (o == this)
            return true;
        if (o instanceof Map.Entry) {
            Map.Entry<?,?> e = (Map.Entry<?,?>)o;
            if (Objects.equals(key, e.getKey()) &&
                Objects.equals(value, e.getValue()))
                return true;
        }
        return false;
    }
}

这个结构非常简单,定义了一个hash和key,hash值是对key进行hash以后得到的。value保存实际要存储的对象。next指向下一个节点。当hash冲突以后,就会将冲突的元素放入这个单向链表中。

二、创建HashMap

创建HashMap实例有四个构造方法,看源码:

public HashMap(int initialCapacity, float loadFactor) {
    if (initialCapacity < 0)
        throw new IllegalArgumentException("Illegal initial capacity: " +
                                           initialCapacity);
    if (initialCapacity > MAXIMUM_CAPACITY)
        initialCapacity = MAXIMUM_CAPACITY;
    if (loadFactor <= 0 || Float.isNaN(loadFactor))
        throw new IllegalArgumentException("Illegal load factor: " +
                                           loadFactor);
    this.loadFactor = loadFactor;
    this.threshold = tableSizeFor(initialCapacity);
}
// HashMap的数组大小是有讲究的,他必须是2的幂,这里通过一个牛逼哄哄的位运算算法,找到大于或等于initialCapacity的最小的2的幂
static final int tableSizeFor(int cap) {
    int n = cap - 1;
    n |= n >>> 1;
    n |= n >>> 2;
    n |= n >>> 4;
    n |= n >>> 8;
    n |= n >>> 16;
    return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}

构造方法中有两个参数,第一个initialCapacity定义map的数组大小,第二个loadFactor意为负载因子,他的作用就是当容器中存储的数据达到loadFactor限度以后,就开始扩容。如果不设定这样参数的话,loadFactor就等于默认值0.75。但是细心的你会发现,容器创建以后,并没有创建数组,原来table是在第一次被使用的时候才创建的,而这个时候threshold = initialCapacity * loadFactor。 这才是这个容器的真正的负载能力。
tableSizeFor这个方法的目的是找到大于或等于initialCapacity的最小的2的幂,这个算法写的非常妙,值得我们细细品味。
假设cap=7
第一步 n = cap -1 = 6 = 00000110
第二步 n|= n>>>1:
n>>>1表示无符号右移1位,那么二进制表示为00000011,此时00000110 | 00000011 = 00000111
第三步 n|=n>>>2:
00000111 & 00000001 = 00000111
第四部 n|=n>>>4:
00000111 & 00000000 = 00000111
第五步 n|=n>>>8;
00000111 & 00000000 = 00000111
第六步 n|=n>>>16;
00000111 & 00000000 = 00000111
最后 n + 1 = 00001000
其实他的原理很简单,第一步先对cap-1是因为如果cap原本就是一个2的幂,那么最后一步加1,会使得这个值变成原来的两倍,但事实上原来这个cap就是2的幂,就是我们想要的值。接下来后面的几步无符号右移操作是把高位的1补到低位,经过一系列的位运算以后的值必定是000011111…他的低位必定全是1,那么最后一步加1以后,这个值就会成为一个00010000…(2的幂次),这就是通过cap找到2的幂的方法。看到如此简约高效的算法,我服了。

三、put添加元素

添加一个元素是所有容器中的标配功能,但是至于添加方式那就各有千秋,Map添加元素的方式是通过put,向容器中存入一个Key-Value对。

public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}
// 获取key的hash值,这里讲hash值的高16位右移和低16位做异或操作,目的是为了减少hash冲突,使hash值能均匀分布。
static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict) {
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    // 如果是第一次添加元素,那么table是空的,首先创建一个指定大小的table。
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    // 通过对hash与数组长度的与操作,确定key对应的数组位置,然后读取该位置中的元素。
    if ((p = tab[i = (n - 1) & hash]) == null)
        // 如果当前位置为空,那么就在当前数组位置,为这个key-value创建一个节点。
        tab[i] = newNode(hash, key, value, null);
    else {
        // 如果当前位置已经存在元素,那么就要逐个读取这条链表的元素。
        Node<K,V> e; K k;
        // 如果key和hash值都等于当前头元素,那么这存放的两个元素是相同的。
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            e = p;
        // 如果当前位置的链表类型是TreeNode,那么就讲当前元素以红黑树的形式存放。
        else if (p instanceof TreeNode)
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        else {
            for (int binCount = 0; ; ++binCount) {
                // 遍历链表的所有元素,如果都未找到相同key的元素,那么说明这个元素并不在容器中存在,因此将他添加到链表尾部,并结束遍历。
                if ((e = p.next) == null) {
                    p.next = newNode(hash, key, value, null);
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        treeifyBin(tab, hash);
                    break;
                }
                // 如果在遍历过程中,发现了相同的key值,那么就结束遍历。
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                p = e;
            }
        }
        // 如果e != null 说明在当前容器中,存在一个相同的key值,那么就要替换key所对应的value
        if (e != null) {
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            // 这是专门留给LinkedHashMap调用的回调函数,LinkedHashMap会实现这个方法。从这里可以看出,HashMap充分的考虑了他的扩展性。
            afterNodeAccess(e);
            return oldValue;
        }
    }
    ++modCount;
    // 这里判断当前元素的数量是否超过了容量的上限,如果超过了,就要重新进行扩容,并对当前元素重新hash,所以再次扩容以后的元素位置都是会改变的。
    if (++size > threshold)
        resize();
    // 此方法也是HashMap留给LinkedHashMap实现的回调方法。透露一下,因为LinkedHashMap在插入元素以后,都会维护他的一个双向链表
    afterNodeInsertion(evict);
    return null;
}

四、get获取元素

使用HashMap有一个明显的优点,就是他的存取时间开销基本维持在O(1),除非在数据量大了以后hash冲突的元素多了以后,对其性能有一定的影响。那么现在介绍的get方法很好的体现了这个优势。

public V get(Object key) {
    Node<K,V> e;
    return (e = getNode(hash(key), key)) == null ? null : e.value;
}
// 同一个key的hash值是相同的,通过hash就可以求出数组的下标,便可以在O(1)的时间内获取元素。
final Node<K,V> getNode(int hash, Object key) {
    Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
    // 在容器不为空,并且对应位置也存在元素的情况下,那么就要遍历链表,找到相同key值的元素。
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (first = tab[(n - 1) & hash]) != null) {
        // 如果第一个元素的key值相同,那么这个元素就是我们要找的。
        if (first.hash == hash &&
            ((k = first.key) == key || (key != null && key.equals(k))))
            return first;
        // 如果第一个元素不是我们要找的,接下来就遍历链表元素,如果遍历完了以后都没找到,说明不存在这个key值
        if ((e = first.next) != null) {
            if (first instanceof TreeNode)
                return ((TreeNode<K,V>)first).getTreeNode(hash, key);
            do {
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    return e;
            } while ((e = e.next) != null);
        }
    }
    return null;
}

五、remove删除元素

删除元素的实现原理和put,get都类似。remove通过给定的key值,找到在hash表中对应的位置,然后找出相同key值的元素,对其删除。

public V remove(Object key) {
    Node<K,V> e;
    return (e = removeNode(hash(key), key, null, false, true)) == null ?
        null : e.value;
}
// 通过key的hash值定位元素位置,并对其删除。这里的实现和put基本相同,我只在不同的地方做一下解释。
final Node<K,V> removeNode(int hash, Object key, Object value,
                           boolean matchValue, boolean movable) {
    Node<K,V>[] tab; Node<K,V> p; int n, index;
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (p = tab[index = (n - 1) & hash]) != null) {
        Node<K,V> node = null, e; K k; V v;
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            node = p;
        else if ((e = p.next) != null) {
            if (p instanceof TreeNode)
                node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
            else {
                do {
                    if (e.hash == hash &&
                        ((k = e.key) == key ||
                         (key != null && key.equals(k)))) {
                        node = e;
                        break;
                    }
                    p = e;
                } while ((e = e.next) != null);
            }
        }
        // 如果找到了相同的key,接下来就要判断matchValue参数,matchValue如果是true的话,就说明
        // 需要检查被删除的value是否相同,只有相同的情况下才能删除元素。如果matchValue是false的话
        // 就不需要判断value是否相同。
        if (node != null && (!matchValue || (v = node.value) == value ||
                             (value != null && value.equals(v)))) {
            if (node instanceof TreeNode)
                ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
            else if (node == p)
                tab[index] = node.next;
            else
                p.next = node.next;
            ++modCount;
            --size;
            afterNodeRemoval(node);
            return node;
        }
    }
    return null;
}

六、resize动态扩容

resize这个方法非常重要,他在添加元素的时候就会被调用到。resize的目的是在容器的容量达到上限的时候,对其扩容,使得元素可以继续被添加进来。这里需要关注两个参数threshold和loadFactor,threshold表示容量的上限,当容器中元素数量大于threshold的时候,就要扩容,并且每次扩容都是原来的两倍。loadFactor表示hash表的数组大小。这两个参数的配合使用可以有效的控制hash冲突数量。

final Node<K,V>[] resize() {
    Node<K,V>[] oldTab = table;
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    int oldThr = threshold;
    int newCap, newThr = 0;
    // 如果容器并不是第一次扩容的话,那么oldCap必定会大于0
    if (oldCap > 0) {
        if (oldCap >= MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return oldTab;
        }
        // threshold和数组大小cap共同扩大为原来的两倍
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                 oldCap >= DEFAULT_INITIAL_CAPACITY)
            newThr = oldThr << 1;
    }
    // 第一次扩容,并且设定了threshold值。
    else if (oldThr > 0)
        newCap = oldThr;
    else {
        // 如果在创建的时候并没有设置threshold值,那就用默认值
        newCap = DEFAULT_INITIAL_CAPACITY;
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }
    if (newThr == 0) {
        // 第一次扩容的时候threshold = cap * loadFactor
        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;
    // 如果不是第一次扩容,那么hash表中必然存在数据,需要将这些数据重新hash
    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
                    // 这里分两串,lo表示原先位置的所有,hi表示新的索引
                    Node<K,V> loHead = null, loTail = null;
                    Node<K,V> hiHead = null, hiTail = null;
                    Node<K,V> next;
                    do {
                        next = e.next;
                        // 因为cap都是2的幂次,假设oldCap == 10000,
                        // 假设e.hash= 01010 那么 e.hash & oldCap == 0。
                        // 老位置= e.hash & oldCap-1 = 01010 & 01111 = 01010
                        // newCap此时为100000,newCap-1=011111。
                        // 此时e.hash & newCap 任然等于01010,位置不变。
                        // 如果e.hash 假设为11010,那么 e.hash & oldCap != 0
                        // 原来的位置为 e.hash & oldCap-1 = 01010
                        // 新位置 e.hash & newCap-1 = 11010 & 011111 = 11010
                        // 此时 新位置 != 老位置  新位置=老位置+oldCap
                        // 因此这里分类两个索引的链表
                        if ((e.hash & oldCap) == 0) {
                            if (loTail == null)
                                loHead = e;
                            else
                                loTail.next = e;
                            loTail = e;
                        }
                        else {
                            if (hiTail == null)
                                hiHead = e;
                            else
                                hiTail.next = e;
                            hiTail = e;
                        }
                    } while ((e = next) != null);
                    if (loTail != null) {
                        loTail.next = null;
                        newTab[j] = loHead;
                    }
                    if (hiTail != null) {
                        hiTail.next = null;
                        newTab[j + oldCap] = hiHead;
                    }
                }
            }
        }
    }
    return newTab;
}

七、遍历

HashMap遍历有三种方式,一种是对key遍历,还有一种是对entry遍历和对value遍历。这三种遍历方式都是基于对HashIterator的封装,三种实现方式大同小异,因此我着重介绍EntryIterator的实现。

// 对HashMap元素进行遍历。
public Set<Map.Entry<K,V>> entrySet() {
    Set<Map.Entry<K,V>> es;
    // 第一次遍历的时候,实例化entrySet。
    return (es = entrySet) == null ? (entrySet = new EntrySet()) : es;
}
final class EntrySet extends AbstractSet<Map.Entry<K,V>> {
    public final int size()                 { return size; }
    public final void clear()               { HashMap.this.clear(); }
    public final Iterator<Map.Entry<K,V>> iterator() {
        return new EntryIterator();
    }
    public final boolean contains(Object o) {
        if (!(o instanceof Map.Entry))
            return false;
        Map.Entry<?,?> e = (Map.Entry<?,?>) o;
        Object key = e.getKey();
        Node<K,V> candidate = getNode(hash(key), key);
        return candidate != null && candidate.equals(e);
    }
    public final boolean remove(Object o) {
        if (o instanceof Map.Entry) {
            Map.Entry<?,?> e = (Map.Entry<?,?>) o;
            Object key = e.getKey();
            Object value = e.getValue();
            return removeNode(hash(key), key, value, true, true) != null;
        }
        return false;
    }
    public final Spliterator<Map.Entry<K,V>> spliterator() {
        return new EntrySpliterator<>(HashMap.this, 0, -1, 0, 0);
    }
    public final void forEach(Consumer<? super Map.Entry<K,V>> action) {
        Node<K,V>[] tab;
        if (action == null)
            throw new NullPointerException();
        if (size > 0 && (tab = table) != null) {
            int mc = modCount;
            for (int i = 0; i < tab.length; ++i) {
                for (Node<K,V> e = tab[i]; e != null; e = e.next)
                    action.accept(e);
            }
            if (modCount != mc)
                throw new ConcurrentModificationException();
        }
    }
}
final class EntryIterator extends HashIterator
    implements Iterator<Map.Entry<K,V>> {
    public final Map.Entry<K,V> next() { return nextNode(); }
}
// HashMap自己实现的遍历方法。上面的所有方法都是围绕这个类展开的。下面具体讲解这个类的实现原理。
abstract class HashIterator {
    Node<K,V> next;        // 指向下一个元素
    Node<K,V> current;     // 指向当前元素
    int expectedModCount;
    int index;             // 当前元素位置
    HashIterator() {
        expectedModCount = modCount;
        Node<K,V>[] t = table;
        current = next = null;
        index = 0;
        if (t != null && size > 0) { // 找到table中的第一个元素
            do {} while (index < t.length && (next = t[index++]) == null);
        }
    }
    public final boolean hasNext() {
        return next != null;
    }
    final Node<K,V> nextNode() {
        Node<K,V>[] t;
        Node<K,V> e = next;
        if (modCount != expectedModCount)
            throw new ConcurrentModificationException();
        if (e == null)
            throw new NoSuchElementException();
        // 判断当前元素是否为链表中的最后一个元素,如果在链表尾部,那么就需要重新遍历table,
        // 顺序找到下元素的位置。
        if ((next = (current = e).next) == null && (t = table) != null) {
            do {} while (index < t.length && (next = t[index++]) == null);
        }
        return e;
    }
    // 删除当前遍历的元素。
    public final void remove() {
        Node<K,V> p = current;
        if (p == null)
            throw new IllegalStateException();
        if (modCount != expectedModCount)
            throw new ConcurrentModificationException();
        current = null;
        K key = p.key;
        removeNode(hash(key), key, null, false, false);
        expectedModCount = modCount;
    }
}

LinkedHashMap

前言:

LinkedHashMap是HashMap的子类,他不仅实现了HashMap的所有功能,更是维护了元素的存储顺序。LinkedHashMap维护元素顺序的方式有两种,一种是维护他的存入顺序,另一种则是维护元素的读取顺序。基于这种功能,LinkedHashMap可以在用于LRU算法的实现。LRU为何物?LRU的全称是Least Recently Used翻译过来就是最近最久未使用,他通常应用于缓存的一种实现方式,当缓存数据满时,删除最少使用的缓存数据。
LinkedHashMap的结构是HashMap+双向链表。他通过继承HashMap得到了用hash表存储数据的能力,同时他又维护了一个双向链表实现了对元素的排序功能。HashMap部分上文已经介绍了,本文着重要介绍的是双向链表部分实现(这里有必要说明一下,我写的系列文章是基于jdk1.8的。jdk1.8和之前版本的实现有不少差异,LinkedHashMap部分就改动了不少,有兴趣的同学可以对照1.7的链表实现和1.8的链表实现,你会发现是两者差异很大,对于两种实现的优缺点可以自行思考哦)。

一、双向链表结构

jdk1.8的链表结构和1.7的差异很大,可以看出来1.8中的实现简化了不是,只维护了两个指针,befor和after。在整个链表中维护了head(头指针)和tail(尾指针)。这两个指针是有讲究的,head所指向的是eldest元素,也就是最老的元素,tail指向youngest元素,也就是最年轻的元素。在这个链表中,都是在队尾添加元素,队头删除元素,这种方式很像队列,但是还是有点区别。

static class Entry<K,V> extends HashMap.Node<K,V> {
    Entry<K,V> before, after;
    Entry(int hash, K key, V value, Node<K,V> next) {
        super(hash, key, value, next);
    }
}
// 指向eldest元素
transient LinkedHashMap.Entry<K,V> head;
// 指向youngest元素
transient LinkedHashMap.Entry<K,V> tail;

二、LinkedHashMap实例创建

LinkedHashMap的创建和HashMap没什么两样,就是这个构造方法中,加入了acessOrder的参数,告诉LinkedHashMap以哪种方式维护顺序。

// 元素遍历顺序,true维护元素的访问顺序,最新访问的放入队尾,false维护元素的插入顺序,最新插入的在队尾。
final boolean accessOrder;
public LinkedHashMap(int initialCapacity,
                     float loadFactor,
                     boolean accessOrder) {
    super(initialCapacity, loadFactor);
    this.accessOrder = accessOrder;
}

三、get获取元素

LinkedHashMap的get方法几乎就是复用了HashMap。唯一的区别就是多了一个accessOrder判断,如果accessOrder==true说明他需要维护元素的访问顺序,而afterNodeAccess是HashMap提供的回调方法,他也会在put元素的时候调用。afterNodeAccess方法的作用就是将当前访问的元素添加到队尾,因为这个链表都是从头部删除,因此这个元素会在最后才被删除。

public V get(Object key) {
    Node<K,V> e;
    if ((e = getNode(hash(key), key)) == null)
        return null;
    if (accessOrder)
        afterNodeAccess(e);
    return e.value;
}
void afterNodeAccess(Node<K,V> e) { // 将访问元素添加到队尾
    LinkedHashMap.Entry<K,V> last;
    if (accessOrder && (last = tail) != e) {
        LinkedHashMap.Entry<K,V> p =
            (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
        p.after = null;
        // 如果当前元素是头元素,那么就将head指向他的下一个节点
        if (b == null)
            head = a;
        else
            b.after = a;
        // 如果当前元素是尾元素,那么就将last指向他的上一个节点
        if (a != null)
            a.before = b;
        else
            last = b;
        if (last == null)
            head = p;
        else {
            p.before = last;
            last.after = p;
        }
        tail = p;
        ++modCount;
    }
}

四、put元素

LinkedHashMap并没有自己实现put方法,完完全全是复用了HashMap的,因为HashMap提供了两个回调方法作为他的扩展,LinkedHashMap只需要实现这两个方法即可,从这里也可以学到如何提供代码的扩展性,预先留出回调接口也是个不错的选择哦。在HashMap的put方法中,调用了两个回调方法,afterNodeAccess和afterNodeInsertion。第一个方法已经介绍了,下面就介绍afterNodeInsertion,这个方法的主要目的就是在map添加元素以后,维护链表的顺序,同时也会控制了对链表头元素的删除与否。

// 在插入元素以后,判断当前容器的元素是否已满,如果是的话,就删除当前最老的元素,也就是队头元素。
void afterNodeInsertion(boolean evict) {
    LinkedHashMap.Entry<K,V> first;
    if (evict && (first = head) != null && removeEldestEntry(first)) {
        K key = first.key;
        removeNode(hash(key), key, null, false, true);
    }
}
// 这是用户实现的回调方法,判断当前最老的元素是否需要删除,如果为true,就删除链表头元素
protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
    return false;
}

五、删除元素

在删除元素以后,LinkedHashMap需要维护当前链表的指针,也就是双向链表的head和tail指针的指向问题

void afterNodeRemoval(Node<K,V> e) {
    LinkedHashMap.Entry<K,V> p =
        (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
    p.before = p.after = null;
    // 如果当前元素是头元素,那么head指向他的下一个节点
    if (b == null)
        head = a;
    else
        b.after = a;
    // 如果当前元素是尾元素,那么tail指向他的上一个节点
    if (a == null)
        tail = b;
    else
        a.before = b;
}

六、LinkedHashIterator遍历

容器的遍历是一个亘古不变的话题,然而LinkedHashMap的遍历方式有他的特殊性。因为他在hash表的基础之上又维护了一个双向链表,而这个链表维护这元素的遍历顺序,因为LinkedHashMap在遍历的时候,只能遍历这个链表,而不能像HashMap一样遍历hash表。

abstract class LinkedHashIterator {
    LinkedHashMap.Entry<K,V> next;
    LinkedHashMap.Entry<K,V> current;
    int expectedModCount;
    LinkedHashIterator() {
        // 第一次从头开始遍历
        next = head;
        expectedModCount = modCount;
        current = null;
    }
    public final boolean hasNext() {
        return next != null;
    }
    // 对链表从头到尾开始遍历,顺序遍历的方式很简单就是next = e.after
    final LinkedHashMap.Entry<K,V> nextNode() {
        LinkedHashMap.Entry<K,V> e = next;
        if (modCount != expectedModCount)
            throw new ConcurrentModificationException();
        if (e == null)
            throw new NoSuchElementException();
        current = e;
        next = e.after;
        return e;
    }
    public final void remove() {
        Node<K,V> p = current;
        if (p == null)
            throw new IllegalStateException();
        if (modCount != expectedModCount)
            throw new ConcurrentModificationException();
        current = null;
        K key = p.key;
        removeNode(hash(key), key, null, false, false);
        expectedModCount = modCount;
    }
}

WeakHashMap

前言:
WeakHashMap这个类我看了好久,一直不知道怎么写,有两点原因。第一:我怕把他写简单了,光从WeakHashMap的功能实现是描述,他和HashMap等非常的相似,无非也是用来hash表+单向链表的结构作为底层数据存储,再写一遍没太大意思。 第二:WeakHashMap的特点是以一种弱引用的关系存储数据,存储对象长期不用,可以被垃圾回收。讲这部分内容非常有意思,但是关于jvm部分了解不深,又怕讲不好。因此托了很长时间没写。
权衡以后,决定先讲解WeakHashMap弱引用的实现原理,垃圾回收部分先缓一缓。

一、与HashMap异同

WeakHashMap的实现和HashMap非常类似,他们有相同的数据结构,类似的存储机制。数据底层都是通过hash表+单向链表的结构存储数据。
1.get
获取元素的流程非常类似,首先对key进行hash处理,通过hash值寻找到对应的元素位置。数组元素保存的是单向链表,这是hash冲突导致的结果。然后元每个entry对比key值是否相同,最终找到对应的元素。

2.put
添加元素的流程也和HashMap类似,首先对key值hash处理,通过hash值求出对应的元素位置,然后通过对比单向链表中的key值,将元素添加到链表头部。最后会更加当前元素的数量与threshold对比,进行动态扩容。扩容部分的原理也和HashMap类似。

3.getTable
getTable方法是WeakHashMap特有的,这个方法是干什么用的呢?因为我们知道WeakHashMap的元素是通过弱引用的关系存储的。在容器中,有部分元素长时间未用,会被垃圾回收,getTable的作用就是清除被垃圾回收的元素。源码如下:

private Entry<K,V>[] getTable() {
    expungeStaleEntries();
    return table;
}
// 清除所有被垃圾回收的元素
private void expungeStaleEntries() {
    // queue队列中保存的是已经被垃圾回收的元素key值。遍历队列
    for (Object x; (x = queue.poll()) != null; ) {
        synchronized (queue) {
            @SuppressWarnings("unchecked")
                Entry<K,V> e = (Entry<K,V>) x;
            int i = indexFor(e.hash, table.length);
            Entry<K,V> prev = table[i];
            Entry<K,V> p = prev;
            while (p != null) {
                Entry<K,V> next = p.next;
                if (p == e) {
                    if (prev == e)
                        table[i] = next;
                    else
                        prev.next = next;
                    e.value = null; // 有助于垃圾回收
                    size--;
                    break;
                }
                prev = p;
                p = next;
            }
        }
    }
}

从源码中可以看出,首先会遍历queue队列,队列中保存的元素是已经被垃圾回收的元素的key值。然后将该key从table中删除。这步方法在getTable,resize,size方法中使用到,并且在垃圾回收过程中也会使用,因此他是在多线程环境下调用的,所以该方法使用了同步方法,确保线程安全。
那么是谁将垃圾回收的后的元素放入queue中的呢?这就要从Entry的结构说起。

4.Entry结构介绍
WeakHashMap的Entry是Reference的子类。Entry实例化时会引用当前的queue,如果当前Entry被垃圾回收后,会将key注册的queue中。在后文中我会详细介绍Reference类。

Entry结构

private static class Entry<K,V> extends WeakReference<Object> implements Map.Entry<K,V> {
    V value;
    final int hash;
    Entry<K,V> next;
    Entry(Object key, V value,
          ReferenceQueue<Object> queue,
          int hash, Entry<K,V> next) {
        super(key, queue);
        this.value = value;
        this.hash  = hash;
        this.next  = next;
    }
    ......
}

二、弱键引用

上文简单介绍了WeakHashMap和HashMap的异同点,大同小异,不想过多重复。我认为WeakHashMap的重点是对元素进行垃圾回收的部分。下面将结合源码进行讲解。

public abstract class Reference<T> {
     private T referent;
    // 将回收的元素添加到队列中。
    volatile ReferenceQueue<? super T> queue;
    @SuppressWarnings("rawtypes")
    Reference next;
    transient private Reference<T> discovered;
    static private class Lock { };
    private static Lock lock = new Lock();
    // 垃圾收集器将回收的引用加入队列
    private static Reference<Object> pending = null;
    private static class ReferenceHandler extends Thread {
        ReferenceHandler(ThreadGroup g, String name) {
            super(g, name);
        }
        public void run() {
            for (;;) {
                Reference<Object> r;
                synchronized (lock) {
                    if (pending != null) {
                        r = pending;
                        pending = r.discovered;
                        r.discovered = null;
                    } else {
                        try {
                            try {
                                lock.wait();
                            } catch (OutOfMemoryError x) { }
                        } catch (InterruptedException x) { }
                        continue;
                    }
                }
                // Fast path for cleaners
                if (r instanceof Cleaner) {
                    ((Cleaner)r).clean();
                    continue;
                }
                ReferenceQueue<Object> q = r.queue;
                if (q != ReferenceQueue.NULL) q.enqueue(r);
            }
        }
    }
    static {
        ThreadGroup tg = Thread.currentThread().getThreadGroup();
        for (ThreadGroup tgn = tg;
             tgn != null;
             tg = tgn, tgn = tg.getParent());
        Thread handler = new ReferenceHandler(tg, "Reference Handler");
        handler.setPriority(Thread.MAX_PRIORITY);
        handler.setDaemon(true);
        handler.start();
    }
    
}

Reference的实现原理:

第一步通过静态代码块启动ReferenceHandler线程,所有Reference实例共享该线程。pending会被虚拟机调用,当有元素被垃圾回收以后,会添加到该队列中。Reference线程通过无线循环检查pending是否存在元素。当发现有元素被垃圾回收以后,会将该元素添加到queue中。如果当前没有发现被回收的元素,也就是pending为null时,会通过lock.wait() 阻塞线程。直到有元素被回收以后,会调用nodify唤醒线程。
该过程涉及到多线程的知识,jvm垃圾回收的原理等,这部分比较复杂,暂时无法很好的讲解。将回收的元素加入到queue队列的部分实现用了消费者模式。为了读者更好的理解该原理,我对他进行模仿,实现了一个简单的消费者模式的demo。这个demo主要功能如下:
1.多个生产者不断的生产某件产品,当产品数量大于10以后就停止生产,只要当产品数量小于10,就会生产。
2.多个消费者不断的消费产品,知道消费结束。
3.当生产者生产到10件以后,就阻塞生产线,通知消费者消费。
4.当消费者消费完结束以后,阻塞消费,通知生产者生产。

public class ProducerCustomerDemo {
    private static int index = 0;
    private static int size = 0;
    static private class Lock { };
    private static Lock lock = new Lock();
    private static Entry head = null;
    public synchronized int getSize() {
        return size;
    }
    public synchronized void addSize() {
        ProducerCustomerDemo.size++;
    }
    public synchronized void minusSize() {
        ProducerCustomerDemo.size--;
    }
    public synchronized int getIndex() {
        return index;
    }
    public synchronized void addIndex() {
        index++;
    }
    public synchronized void minusIndex() {
        index--;
    }
    public static void main(String[] args) {
        new Thread(new ProducerCustomerDemo().new Consumer("aaa")).start();
        new Thread(new ProducerCustomerDemo().new Consumer("bbb")).start();
        new Thread(new ProducerCustomerDemo().new Consumer("ccc")).start();
        new Thread(new ProducerCustomerDemo().new Consumer("ddd")).start();
        new Thread(new ProducerCustomerDemo().new Producer("张三")).start();
        new Thread(new ProducerCustomerDemo().new Producer("李四")).start();
    }
    class Entry {
        Entry next;
        int value;
        public Entry(int value) {
            this.value = value;
        }
    }
    class Consumer implements Runnable{
        private String name;
        public Consumer(String name) {
            this.name = name;
        }
        @Override
        public void run() {
            for (;;) {
                Entry temp = null;
                synchronized (lock) {
                    if (head != null) {
                        try {
                            Thread.sleep(1000);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        temp = head;
                        head = temp.next;
                        temp.next = null;
                        minusSize();
                        System.out.println("消费,当前产品数"+getSize()+"--"+name+":"+ temp.value);
                        lock.notify();
                    } else {
                        try {
                            lock.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }
        }
    }
    class Producer implements Runnable {
        private String name;
        public Producer(String name) {
            this.name = name;
        }
        @Override
        public void run() {
            for (;;) {
                synchronized (lock) {
                    if (getSize() < 10) {
                        try {
                            Thread.sleep(1000);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        addIndex();
                        Entry temp = new Entry(getIndex());
                        temp.next = head;
                        head = temp;
                        addSize();
                        System.out.println("生产,当前产品数"+getSize()+"--" + name + ":" + temp.value);
                        lock.notify();
                    } else {
                         try {
                         lock.wait();
                         } catch (InterruptedException e) {
                         e.printStackTrace();
                         }
                    }
                }
            }
        }
    }
}

HashTable

前言:

前文中已对HashMap实现原理做了详细介绍,HashTable与HashMap的原理相同,实现方式也几乎一致。除了以下几点不同:
1.HashMap非线程安全,HashTable线程安全。HashMap与HashTable的实现方法几乎一致,区别是HashTable对所有的方法进行了同步操作,确保了线程安全。但是有需要注意的是,他只确保单个操作的原子性,如果需要在并发环境下执行复合操作,那用户需要自行同步,否则会出现问题。
2.HashMap的key和value可以为null, HashTable的Key和value不能为null。

可以参考HashMap的实现原理

TreeMap

前言:

TreeMap和HashMap一样实现的是Map接口,但两者的实现方式天差地别。HashMap的底层是hash表+单向链表的形式存储数据,TreeMap底层是通过红黑树存储数据。HashMap因为是基于散列表的实现,所以时间开销为O(1),TreeMap的时间开销是O(lgn)。TreeMap的优势在于他是基于key值排序的。

红黑树有五大特性:
1)每个结点要么是红的,要么是黑的。
2)根结点是黑的。
3)每个叶结点,即空结点(NIL)是黑的。
4)如果一个结点是红的,那么它的俩个儿子都是黑的。
5)对每个结点,从该结点到其子孙结点的所有路径上包含相同数目的黑结点。

他的所有操作都是围绕着这五大特性展开的。这五大特性的最终目的就是为了维持二叉树的相对平衡性。当每次二叉树操作以后,有可能会出现违反特性的情况(也就是出现了失衡状况),这时二叉树需要通过左旋,右旋,重新着色等系列操作,再次找到平衡点。

我的理解是:红黑色的操作就两个要点。第一:遵循二叉查找树的规范,对所有元素进行排序,但这里存在着不确定情况,有可能出现左右子树深度极其不对称的情况,导致最坏时间复杂度出现O(n)的情况。 第二:正因为存在二叉树严重不平衡的情况,所以就出现了红黑二叉树,通过标记每个节点的颜色,动态的调整二叉树的结构,使其始终维持在相对平衡的状态,这样做的好处就是查找性能始终维持在O(lgn)的较高水平。

一、添加元素

在TreeMap中插入元素,原理就是在二叉查找树中插入元素。通过对二叉树节点大小的对比插入相应的位置,时间复杂度为O(lgn)。但是在插入完毕以后,有可能会导致红黑树被破坏的情况,因此需要修复当前结构,重新着色。

put方法:

public V put(K key, V value) {
    Entry<K,V> t = root;
    if (t == null) {
        compare(key, key); // type (and possibly null) check
        root = new Entry<>(key, value, null);
        size = 1;
        modCount++;
        return null;
    }
    int cmp;
    Entry<K,V> parent;
    // split comparator and comparable paths
    Comparator<? super K> cpr = comparator;
    if (cpr != null) {
        do {
            parent = t;
            cmp = cpr.compare(key, t.key);
            if (cmp < 0)
                t = t.left;
            else if (cmp > 0)
                t = t.right;
            else
                return t.setValue(value);
        } while (t != null);
    }
    else {
        if (key == null)
            throw new NullPointerException();
        @SuppressWarnings("unchecked")
            Comparable<? super K> k = (Comparable<? super K>) key;
        do {
            parent = t;
            cmp = k.compareTo(t.key);
            if (cmp < 0)
                t = t.left;
            else if (cmp > 0)
                t = t.right;
            else
                return t.setValue(value);
        } while (t != null);
    }
    Entry<K,V> e = new Entry<>(key, value, parent);
    if (cmp < 0)
        parent.left = e;
    else
        parent.right = e;
    fixAfterInsertion(e);
    size++;
    modCount++;
    return null;
}

插入元素后调整红黑树结构:

private void fixAfterInsertion(Entry<K,V> x) {
    x.color = RED;
    while (x != null && x != root && x.parent.color == RED) {
        if (parentOf(x) == leftOf(parentOf(parentOf(x)))) {
            Entry<K,V> y = rightOf(parentOf(parentOf(x)));
            if (colorOf(y) == RED) {
                setColor(parentOf(x), BLACK);
                setColor(y, BLACK);
                setColor(parentOf(parentOf(x)), RED);
                x = parentOf(parentOf(x));
            } else {
                if (x == rightOf(parentOf(x))) {
                    x = parentOf(x);
                    rotateLeft(x);
                }
                setColor(parentOf(x), BLACK);
                setColor(parentOf(parentOf(x)), RED);
                rotateRight(parentOf(parentOf(x)));
            }
        } else {
            Entry<K,V> y = leftOf(parentOf(parentOf(x)));
            if (colorOf(y) == RED) {
                setColor(parentOf(x), BLACK);
                setColor(y, BLACK);
                setColor(parentOf(parentOf(x)), RED);
                x = parentOf(parentOf(x));
            } else {
                if (x == leftOf(parentOf(x))) {
                    x = parentOf(x);
                    rotateRight(x);
                }
                setColor(parentOf(x), BLACK);
                setColor(parentOf(parentOf(x)), RED);
                rotateLeft(parentOf(parentOf(x)));
            }
        }
    }
    root.color = BLACK;
}

原文链接:GitHub - kexun/jdk_source_learning: jdk源码学习笔记,人话翻译

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Da白兔萘糖

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值