今天分享Java学习的第五个专题——集合

集合可以看作是一种容器,用来存储对象信息。所有集合类都位于java.util包下,但支持多线程的集合类位于java.util.concurrent包下。

Java集合类主要由两个根接口CollectionMap派生出来的,Collection派生出了三个子接口:List、Set、Queue(Java5新增的队列,本文暂不提及),因此Java集合大致也可分成List、Set、Queue、Map四种接口体系,(注意:Map不是Collection的子接口)。

精通Java系列 | 集合及源码解析_数组

下面我们来一个一个看下这些接口,如何使用,以及源码分析。

Collection接口

所属java.util.Collection包下,常用的方法如下:

精通Java系列 | 集合及源码解析_ci_02

精通Java系列 | 集合及源码解析_数组_03

Iterator接口

不难发现,Collection接口继承了Iterable接口,而Iterable接口又包含了Iterator迭代器,这说明实现Collection接口的类都是可以迭代的。

public interface Collection<E>
extends Iterable<E>
  • 1.
  • 2.

我们遍历一个Collection接口的方式如下:

public class CollectionTest02 {
    @Test
    public void test1() {
        Collection coll = new ArrayList();
        coll.add("AA");
        coll.add(123);


        @SuppressWarnings("rawtypes")
        Iterator iterator = coll.iterator();
        while(iterator.hasNext()){
            System.out.println(iterator.next());
        }
    }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.

通过collection.iterator的方式声明了一个迭代器,使用这个迭代器遍历集合。

List接口

List集合代表一个有序可重复集合,集合中每个元素都有其对应的顺序索引。List集合默认按照元素的添加顺序设置元素的索引,可以通过索引(类似数组的下标)来访问指定位置的集合元素。

实现List接口的集合主要有:ArrayList、LinkedList、Vector

ArrayList类

ArrayList是一个动态数组,有序的容器,也是我们最常用的集合,是List类的典型实现。

它允许任何符合规则的元素插入甚至包括null

底层通过数组实现,每一个ArrayList都有一个初始容量(10),该容量代表了数组的大小。随着容器中的元素不断增加,容器的大小也会随着增加。

在每次向容器中增加元素的同时都会进行容量检查,当快溢出时,就会进行扩容操作。所以如果我们明确所插入元素的多少,最好指定一个初始容量值,避免过多的进行扩容操作而浪费时间、效率。

ArrayList擅长于随机访问,同时ArrayList是非同步的(线程不安全的)

ArrayList的源码分析:

ArrayList的签名

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable
  • 1.
  • 2.

属性

// ArrayList的底层数据结构是数组:
transient Object[] elementData;


// 空数组,传入容量为0时使用
private static final Object[] EMPTY_ELEMENTDATA = {};


/**
 * 空数组,传传入容量时使用,添加第一个元素的时候会重新初始为默认容量大小
 */
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};


// 集合中的元素个数    
private int size;


// 定义默认容量为10:
private static final int DEFAULT_CAPACITY = 10; //默认长度是10
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.

构造方法

// 指定长度的ArrayList构造方法
/*
传入初始容量,如果大于0就初始化elementData为对应大小,如果等于0就使用EMPTY_ELEMENTDATA空数组,如果小于0抛出异常。
*/
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);
    }
}


    /**
     * 不传初始容量,初始化为DEFAULTCAPACITY_EMPTY_ELEMENTDATA空数组,会在添加第一个元素的时候扩容为默认的大小,即10。
     */
    public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }


    /**
     * 传入集合并初始化elementData,这里会使用拷贝把传入集合的元素拷贝到elementData数组中,如果元素个数为0,则初始化为EMPTY_ELEMENTDATA空数组。
     */
    public ArrayList(Collection<? extends E> c) {
        Object[] a = c.toArray();
        if ((size = a.length) != 0) {
            if (c.getClass() == ArrayList.class) {
                elementData = a;
            } else {
                elementData = Arrays.copyOf(a, size, Object[].class);
            }
        } else {
            // replace with empty array.
            elementData = EMPTY_ELEMENTDATA;
        }
    }
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.

add()方法

public boolean add(E e) {
  // size为集合元素个数
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    elementData[size++] = e;
    return true;
}


private void ensureCapacityInternal(int minCapacity) {
        ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
    }


private void ensureExplicitCapacity(int minCapacity) {
        modCount++;


        // 如果添加元素后容量大于数组的长度,就需要扩容
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }


private static int calculateCapacity(Object[] elementData, int minCapacity) {
  // 如果传入的是一个空数组,就初始化为默认大小10
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            return Math.max(DEFAULT_CAPACITY, minCapacity);
        }


// 否则返回传入的minCapacity
        return minCapacity;
    }


//重点:扩容算法
private void grow(int minCapacity) {


        int oldCapacity = elementData.length;
  // 新容量为老容量的1.5倍
        int newCapacity = oldCapacity + (oldCapacity >> 1);
  // 如果新容量比所需容量还小
        if (newCapacity - minCapacity < 0)
  //新容量以需要的容量为准
            newCapacity = minCapacity;
  //如果新容量超过了最大容量
        if (newCapacity - MAX_ARRAY_SIZE > 0)
  //就以最大容量为主
            newCapacity = hugeCapacity(minCapacity);
        // 以新容量拷贝出来一个新数组
        elementData = Arrays.copyOf(elementData, newCapacity);
    }
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.
  • 46.
  • 47.
  • 48.
  • 49.
  • 50.
  • 51.
  • 52.
  • 53.

通过上面的代码,我们知道,add()方法的原理是:

(1)检查是否需要扩容;

(2)如果elementData等于DEFAULTCAPACITY_EMPTY_ELEMENTDATA(空数组),则初始化容量大小为DEFAULT_CAPACITY;

(3)一般情况下新容量是老容量的1.5倍,如果加了这么多容量发现比需要的容量还小,则以需要的容量为准;

(4)创建新容量的数组并把老数组拷贝到新数组;

add(int index, E element)

public void add(int index, E element) {
    // 检查是否越界
    rangeCheckForAdd(index);
    //检查是否需要扩容
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    //将index及其之后的元素往后挪一位,把index的位置空出来
    System.arraycopy(elementData, index, elementData, index + 1,
                     size - index);
    elementData[index] = element;
    size++;
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.

我们在特定位置插入元素,底层原理是调用了系统方法System.arraycopy

方法签名:

public static native void arraycopy(Object src,  int  srcPos,
                                    Object dest, int destPos,
                                    int length);
  • 1.
  • 2.
  • 3.

也就是将原数组elementData中位于第index位置的元素,拷贝到目标数组elementData(这里还是同数组)的第index+1位置处。移动的元素长度(个数)为size-index(因为是从第index个元素开始移动的)。

同样,remove方法的原理也大体类似,也是调用了系统方法System.arraycopy,这里就不再赘述了。

public E remove(int index) {
    rangeCheck(index);


    modCount++;
    E oldValue = elementData(index);


    int numMoved = size - index - 1;
    if (numMoved > 0)
        System.arraycopy(elementData, index+1, elementData, index,
                         numMoved);
    elementData[--size] = null; // clear to let GC do its work


    return oldValue;
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.

改Set:

public E set(int index, E element) {
    rangeCheck(index);


    // 获取当前下标的元素
    E oldValue = elementData(index);
    // 赋值新元素
    elementData[index] = element;
    //返回旧元素    
    return oldValue;
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.

查get:

public E get(int index) {
    rangeCheck(index);


    return elementData(index);
}


E elementData(int index) {
      return (E) elementData[index];
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.

LinkedList类

LinkedList是List接口的另一个实现,除了可以根据索引访问集合元素外,LinkedList还实现了Deque接口,可以当作双端队列来使用,也就是说,既可以当作“栈”使用,又可以当作队列使用。

LinkedList的实现机制与ArrayList的实现机制完全不同,ArrayLiat内部以数组的形式保存集合的元素,所以随机访问集合元素有较好的性能;LinkedList内部以链表的形式保存集合中的元素,所以随机访问集合中的元素性能较差,但在插入删除元素时有较好的性能。

来看LinkedList的源码

LinkedList签名:

public class LinkedList<E>
    extends AbstractSequentialList<E>
    implements List<E>, Deque<E>, Cloneable, java.io.Serializable
  • 1.
  • 2.
  • 3.

属性和构造方法

// 元素个数
transient int size = 0;


// 头节点
transient Node<E> first;


// 尾节点
transient Node<E> last;


// 构造方法
public LinkedList() {
}


//调用addAll方法,将集合c中的元素插入链表中
public LinkedList(Collection<? extends E> c) {
    this();
    addAll(c);
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.

节点结构:可以看出,底层是一个双向链表

private static class Node<E> {
    E item; //元素值
    Node<E> next; //后继指针
    Node<E> prev; //前驱指针


    Node {
        this.item = element;
        this.next = next;
        this.prev = prev;
    }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.

精通Java系列 | 集合及源码解析_数组_04

addAll()方法:linkedList调用了内部addAll(size, c)这个方法:

// 从第index下标开始插入集合c中的所有元素,传入size表示从尾部插入
public boolean addAll(int index, Collection<? extends E> c) {
    //检查下标是否越界:[0, size]
    checkPositionIndex(index);


    //拿到目标集合数组
    Object[] a = c.toArray();
    //插入元素个数
    int numNew = a.length;
    if (numNew == 0)
        return false;


    //index节点的前驱节点和后继节点
    Node<E> pred, succ;
    if (index == size) {
        succ = null;
        pred = last;
    } else {
    //取出index节点作为后继节点
        succ = node(index);
    //前驱节点为succ的前面的节点
        pred = succ.prev;
    }




    for (Object o : a) {
        @SuppressWarnings("unchecked") E e = (E) o;
        //构造新节点
        Node<E> newNode = new Node<>(pred, e, null);
        if (pred == null)
            first = newNode;
        else
            //插入到前驱节点之后
            pred.next = newNode;
        //前驱节点后移
        pred = newNode;
    }


    //如果后继节点为空,说明此时是在队尾append的,那么要设置尾指针
    if (succ == null) {
        last = pred;
    } else {
        //否则设置插入节点后,将这个节点的前驱指针和后继指针指向正确的位置
        pred.next = succ;
        succ.prev = pred;
    }


    size += numNew;
    modCount++;
    return true;
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.
  • 46.
  • 47.
  • 48.
  • 49.
  • 50.
  • 51.
  • 52.
  • 53.
  • 54.
  • 55.
  • 56.
  • 57.

看完了增,我们再来看删:remove()

public E remove(int index) {
    //检查下标是否越界:[0:size)
    checkElementIndex(index);
    //从链表上删除对应节点
    return unlink(node(index));
}


E unlink(Node<E> x) {
        // assert x != null;
        // 获取节点相关信息:元素值、前驱节点、后继节点
        final E element = x.item;
        final Node<E> next = x.next;
        final Node<E> prev = x.prev;


        //如果前驱节点为空,说明要删除的时头节点
        if (prev == null) {
            //设置链表头节点为next
            first = next;
        } else {
            // 删除节点的通用操作
            prev.next = next;
            x.prev = null;
        }


        //如果后置节点为空,删除的是尾节点
        if (next == null) {
            //将last节点置成prev
            last = prev;
        } else {
            //删除节点的通用操作
            next.prev = prev;
            x.next = null;
        }


        //notice:将当前元素置空
        x.item = null;
        size--;
        modCount++;
        return element;
    }
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.

改和查比较简单,这里贴个源码,相信你自己可以看懂:

查get:

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;
        }
    }
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.

改set:

public E set(int index, E element) {
    checkElementIndex(index);
    Node<E> x = node(index);
    E oldVal = x.item;
    x.item = element;
    return oldVal;
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.

再补充一个高频API:toArray()

public Object[] toArray() {
    Object[] result = new Object[size];
    int i = 0;
    for (Node<E> x = first; x != null; x = x.next)
        result[i++] = x.item;
    return result;
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.

Vector类

Vector与ArrayList相似,但是Vector是同步的。所以说Vector是线程安全的动态数组。它的操作与ArrayList几乎一样。

虽然实现与ArrayList类似,但我们还是照例看一下Vector的源码。

Vector类属于java.util包下。

类签名:

public class Vector<E>
    extends AbstractList<E>
    implements List<E>, RandomAccess, Cloneable, java.io.Serializable
  • 1.
  • 2.
  • 3.

可以看到,Vector的底层也是一个数组。

protected Object[] elementData;


protected int elementCount;


//每次扩容时自动增加的量
protected int capacityIncrement;
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.

构造方法:

public Vector(int initialCapacity, int capacityIncrement) {
    super();
    if (initialCapacity < 0)
        throw new IllegalArgumentException("Illegal Capacity: "+
                                           initialCapacity);
    this.elementData = new Object[initialCapacity];
    this.capacityIncrement = capacityIncrement;
}


public Vector(int initialCapacity) {
    this(initialCapacity, 0);
}


public Vector() {
    this(10);
}


public Vector(Collection<? extends E> c) {
    Object[] a = c.toArray();
    elementCount = a.length;
    if (c.getClass() == ArrayList.class) {
        elementData = a;
    } else {
        elementData = Arrays.copyOf(a, elementCount, Object[].class);
    }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.

支持无参构造和有参构造。当我们使用空参构造来创建Vector类对象时,则elementData数组的初始容量默认为10,如需再次扩容,则将elementData数组的当前容量扩容为2倍。

如果使用带参构造来创建Vector类对象,则elementData数组的初始容量即为传入形参的指定容量,如果需要扩容,则直接将该数组当前容量扩容至2倍。

也可以指定初始容量和每次扩容自动增加的容量。

Vector是线程同步的,即线程安全的,这是因为Vector类的操作方法带有synchronized修饰符。因此,在开发中需要线程同步安全时,考虑使用Vector类;如果是单线程情况下,建议优先使用ArrayList类,因为它的效率更高。

public synchronized boolean add(E e) {
    // synchronized关键字修饰,表示线程安全
    modCount++;
    //进行数组的动态扩容机制
    ensureCapacityHelper(elementCount + 1);
    //将元素添加到数组末尾    
    elementData[elementCount++] = e;
    return true;
}


private void ensureCapacityHelper(int minCapacity) {
        // overflow-conscious code
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }


private void grow(int minCapacity) {
        // overflow-conscious code
        // 将当前的长度赋值给OldCapacity
        int oldCapacity = elementData.length;
        //新的长度:如果扩容的量大于0,就按照扩容的量增加,否则就扩容为原来容量的2倍
        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);
    }


private static int hugeCapacity(int minCapacity) {
        // 这里不要忘了,minCapacity是add后需要的容量
        if (minCapacity < 0) // overflow
            throw new OutOfMemoryError();
        //如果需要的容量比最大的容量还大,就返回MAX_VALUE = 0x7fffffff,否则就使用MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8
        return (minCapacity > MAX_ARRAY_SIZE) ?
            Integer.MAX_VALUE :
            MAX_ARRAY_SIZE;
    }
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.

这里可能有一个小细节:为什么ensureCapacityHelper()这个方法没有加锁,主要是为了体现锁粗化的实现,什么是锁粗化?外围方法已经加了锁,里面没有必要再加锁了。

总结一下:当我们想要添加元素到数组时:

(1)首先会检查容量是否满足,如果需要的容量比数组容量大,则需要扩容;

(2)如果给定了每次增加的扩容量,则以给定的扩容量为准进行扩容;

(3)否则,扩容为原来数组容量的2倍;

(4)如果扩容后的容量超出了最大的容量MAX_MAX_ARRAY_SIZE,则扩容为MAX_VALUE,否则扩容为MAX_ARRAY_SIZE。

indexOf()

public int indexOf(Object o) {
        return indexOf(o, 0);
    }


public synchronized int indexOf(Object o, int index) {
    // notice:空对象也可以存到collection里,所以也要遍历,那为什么不统一搞呢?
    if (o == null) {
        for (int i = index ; i < elementCount ; i++)
            if (elementData[i]==null)
                return i;
    } else {
        for (int i = index ; i < elementCount ; i++)
            // 原因在于equals方法,null不能调用equals
            if (o.equals(elementData[i]))
                return i;
    }


    return -1;
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.

其他方法与ArrayList实现基本一致,这里就不再赘述了。

最后总结一下,比较一下List接口的常用实现类:ArrayList,LinkedList,Vector。

精通Java系列 | 集合及源码解析_ci_05

按照正常的逻辑,我们应该先看Set,但是由于我们要剖析源码,而Set接口中很多实现类的底层原理都借助到了Map接口的实现类,所以我们先来学习Map。

Map接口

Map接口采用键值对Map<K,V>的存储方式,保存具有映射关系的数据,因此,Map集合里保存两组值,一组值用于保存Map里的key,另外一组值用于保存Map里的value,key和value可以是任意引用类型的数据。key值不允许重复,可以为null。如果添加key-value对时Map中已经有重复的key,则新添加的value会覆盖该key原来对应的value。常用实现类有HashMap、HashTable、LinkedHashMap、TreeMap等。

HashMap类

HashMap最早是在jdk1.2中开始出现的,一直到jdk1.7一直没有太大的变化。但是到了jdk1.8突然进行了一个很大的改动。其中一个最显著的改动就是:

之前jdk1.7的存储结构是数组+链表,到了jdk1.8变成了数组+链表+红黑树

另外,HashMap是非线程安全的,也就是说在多个线程同时对HashMap中的某个元素进行增删改操作的时候,是不能保证数据的一致性的。

精通Java系列 | 集合及源码解析_ci_06

注意:不是说变成了红黑树效率就一定提高了,只有在链表的长度不小于8,而且数组的长度不小于64的时候才会将链表转化为红黑树,

红黑树是一个自平衡的二叉查找树,也就是说红黑树的查找效率是非常的高,查找效率会从链表的o(n)降低为o(logn)。如果之前没有了解过红黑树的话,也没关系,你就记住红黑树的查找效率很高就OK了。

那为什么不把整个链表变为红黑树呢?

这个问题的意思是这样的,就是说我们为什么非要等到链表的长度大于等于8的时候,才转变成红黑树?在这里可以从两方面来解释

(1)构造红黑树要比构造链表复杂,在链表的节点不多的时候,从整体的性能看来, 数组+链表+红黑树的结构可能不一定比数组+链表的结构性能高。就好比杀鸡焉用牛刀的意思。

(2)HashMap频繁的扩容,会造成底部红黑树不断的进行拆分和重组,这是非常耗时的。因此,也就是链表长度比较长的时候转变成红黑树才会显著提高效率。

定义一个HashMap要确定key和value。

HashMap<String, Integer> map = new HashMap<>();
map.put("wangbucuo", 100);
  • 1.
  • 2.

来看put的源码。

public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}
  • 1.
  • 2.
  • 3.

hash(key)是调用hash方法计算key的hash;

第四个参数是当键相同时,不修改已存在的值;

第五个参数,当为false时,一般表示数组处于创建模式中,一般为true。

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict) {
    // tab表示节点数组,p表示要插入的节点
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    // 数组是空的,通过resize方法创建一个新的数组
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;


    // i表示在数组中插入的位置,通过n-1 & hash计算,判断是否冲突,不冲突的话直接新建节点插入
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    //冲突,我们就要处理冲突
    else {
        Node<K,V> e; K k;
        // 要插入的节点p的hash如果和key的hash一致,说明p的key和table中的key是一致的
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            // 那我们就将p替换掉旧的值
            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);
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        treeifyBin(tab, hash);
                    break;
                }
                //如果旧的值和新的值一致,直接跳过
                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;
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.
  • 46.
  • 47.
  • 48.
  • 49.
  • 50.
  • 51.
  • 52.
  • 53.
  • 54.
  • 55.
  • 56.

怎么扩容呢?

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) {
            threshold = Integer.MAX_VALUE;
            return oldTab;
        }
        // 如果没有超过,则扩大阈值为旧阈值的两倍
        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);
    }
    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"})
    //定义一个新的table表,将旧的表复制到新的表中
    Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
    table = newTab;
    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
                    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;
                        }
                        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;
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.
  • 46.
  • 47.
  • 48.
  • 49.
  • 50.
  • 51.
  • 52.
  • 53.
  • 54.
  • 55.
  • 56.
  • 57.
  • 58.
  • 59.
  • 60.
  • 61.
  • 62.
  • 63.
  • 64.
  • 65.
  • 66.
  • 67.
  • 68.
  • 69.
  • 70.
  • 71.
  • 72.
  • 73.
  • 74.
  • 75.
  • 76.
  • 77.

总结一下,HaspMap扩容就是先计算 新的hash表容量和新的容量阀值,然后初始化一个新的hash表,将旧的键值对重新映射在新的hash表里。如果在旧的hash表里涉及到红黑树,那么在映射到新的hash表中还涉及到红黑树的拆分。

hashMap中处理hash冲突的方法就是链地址法

构造方法:

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(int initialCapacity) {
    this(initialCapacity, DEFAULT_LOAD_FACTOR);
}


public HashMap() {
    this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}


public HashMap(Map<? extends K, ? extends V> m) {
    this.loadFactor = DEFAULT_LOAD_FACTOR;
    putMapEntries(m, false);
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.

看第一个构造方法,涉及两个参数,initialCapacityloadFactor

(1)initialCapacity初始容量

官方要求我们要输入一个2的N次幂的值,比如说2、4、8、16等等这些,但是我们忽然一个不小心,输入了一个20怎么办?没关系,虚拟机会根据你输入的值,找一个离20最近的2的N次幂的值,比如说16离他最近,就取16为初始容量。

(2)loadFactor负载因子

负载因子,默认值是0.75。负载因子表示一个散列表的空间的使用程度,有这样一个公式:loadFactor=HashMap的容量 / initailCapacity。 所以负载因子越大则散列表的装填程度越高,也就是能容纳更多的元素,元素多了,链表大了,所以此时索引效率就会降低。反之,负载因子越小则链表中的数据量就越稀疏,此时会对空间造成烂费,但是此时索引效率高。

Hashtable类

Hashtable和HashMap的数据结构和实现方法类似,但Hashtable是线程安全的。

Hashtable<String, Integer> tab = new Hashtable<>();
tab.put("wangbucuo", 100);
  • 1.
  • 2.

属性和构造方法

public class Hashtable<K,V>
    extends Dictionary<K,V>
    implements Map<K,V>, Cloneable, java.io.Serializable {


    // 采用Entry数组存储键值对,Entry实际上是链表的表头
    private transient Entry<?,?>[] table;


    private transient int count;
    //扩容阈值,超过这个阈值,就进行扩容
    private int threshold;
    // 负载因子
    private float loadFactor;
    //用于快速失败(最后会讲解)
    private transient int modCount = 0;


    private static final long serialVersionUID = 1421746759512286392L;


    public Hashtable(int initialCapacity, float loadFactor) {
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal Capacity: "+
                                               initialCapacity);
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException("Illegal Load: "+loadFactor);


        if (initialCapacity==0)
            initialCapacity = 1;
        this.loadFactor = loadFactor;
        table = new Entry<?,?>[initialCapacity];
        threshold = (int)Math.min(initialCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
    }


    public Hashtable(int initialCapacity) {
        this(initialCapacity, 0.75f);
    }


    public Hashtable() {
        this(11, 0.75f);
    }


    public Hashtable(Map<? extends K, ? extends V> t) {
        this(Math.max(2*t.size(), 11), 0.75f);
        putAll(t);
    }


    // ......
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.
  • 46.
  • 47.
  • 48.
  • 49.
  • 50.
  • 51.
  • 52.
  • 53.
  • 54.
  • 55.

Entry数组如下:

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


    protected Entry(int hash, K key, V value, Entry<K,V> next) {
        this.hash = hash;
        this.key =  key;
        this.value = value;
        this.next = next;
    }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.

不难看出,Entry为单链表节点

put方法

public synchronized V put(K key, V value) {
    // Make sure the value is not null
    if (value == null) {
        throw new NullPointerException();
    }


    // Makes sure the key is not already in the hashtable.
    Entry<?,?> tab[] = table;
    //获取key的哈希
    int hash = key.hashCode();
    // 获取该key对应数组中的下标
    int index = (hash & 0x7FFFFFFF) % tab.length;
    //得到下标对应的Entry
    @SuppressWarnings("unchecked")
    Entry<K,V> entry = (Entry<K,V>)tab[index];
    //遍历对应的链表
    for(; entry != null ; entry = entry.next) {
        if ((entry.hash == hash) && entry.key.equals(key)) {
            // 存在key相等的节点,就替换这个节点,并且返回旧值
            V old = entry.value;
            entry.value = value;
            return old;
        }
    }


    //如果数组下标对应的节点为空,或者遍历链表后发现没有和该key相等的节点,则执行插入操作
    addEntry(hash, key, value, index);
    return null;
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
private void addEntry(int hash, K key, V value, int index) {
    modCount++;


    Entry<?,?> tab[] = table;
    if (count >= threshold) {
        // Rehash the table if the threshold is exceeded
        rehash();


        tab = table;
        hash = key.hashCode();
        index = (hash & 0x7FFFFFFF) % tab.length;
    }


    // Creates the new entry.
    @SuppressWarnings("unchecked")
    Entry<K,V> e = (Entry<K,V>) tab[index];
    tab[index] = new Entry<>(hash, key, value, e);
    count++;
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.

如果hashtable的元素个数超过了阈值,就需要扩容rehash

protected void rehash() {
    int oldCapacity = table.length;
    Entry<?,?>[] oldMap = table;


    // overflow-conscious code
    // 新的容量为旧容量的2倍+1
    int newCapacity = (oldCapacity << 1) + 1;
    // 新容量超过最大容量
    if (newCapacity - MAX_ARRAY_SIZE > 0) {
        if (oldCapacity == MAX_ARRAY_SIZE)
            // Keep running with MAX_ARRAY_SIZE buckets
            return;
        //新容量设为最大:“MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8”
        newCapacity = MAX_ARRAY_SIZE;
    }
    Entry<?,?>[] newMap = new Entry<?,?>[newCapacity];


    modCount++;
    //计算新的阈值,用于下次扩容
    threshold = (int)Math.min(newCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
    table = newMap;


    // 将旧的table每个元素存入新的table
    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;
        }
    }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.

remove、get方法就不再赘述了,他们都是线程安全的,用synchronized修饰,类似put方法。

LinkedHashMap类

LinkedHashMap,使用双向链表来维护key-value对的次序(其实只需要考虑key的次序即可),该链表负责维护Map的迭代顺序,与插入顺序一致,因此性能比HashMap低,但在迭代访问Map里的全部元素时有较好的性能。

LinkedHashMap 直接继承自HashMap ,哈希表由数组和单链表构成,并且当单链表长度超过 8 的时候转化为红黑树,扩容体系,这一切都跟 HashMap 一样。但LinkedHashMap比HashMap更强大。

LinkedHashMap<String, Integer> lmap = new LinkedHashMap<>();
lmap.put("wangbucuo", 100);
  • 1.
  • 2.

签名

public class LinkedHashMap<K,V>
    extends HashMap<K,V>
    implements Map<K,V>
  • 1.
  • 2.
  • 3.

属性

transient Entry<K,V> head;
transient Entry<K,V> tail;
  • 1.
  • 2.

显然,它具有头节点和尾节点,节点类型为Entry:

static class Entry<K,V> extends Node<K,V> {
    //before会在每次添加元素的适合链接上一次添加的元素,上一次添加元素的变量的after指向这次添加的元素,形成双链表
    Entry<K,V> before, after;
    Entry(int hash, K key, V value, Node<K,V> next) {
        super(hash, key, value, next);
    }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.

LinkedHashMap 的 put 方法实际上调用了父类 HashMap 的方法,只是在newNode的时候重写了这个方法,按照双向链表节点的方式重写。

最后来说下,使用LinkedHashMap去构造LRU的最简单方式。(后面讲完HashSet,我还要说回LinkedHashMap的遍历方式,以及如何理解LinkedHashMap的遍历顺序和访问顺序)

import java.util.LinkedHashMap;
import java.util.Map;


public class LRUTest<K, V> extends LinkedHashMap<K,V> {
    //static表示MAX_MODE_NUM是类的属性,不属于任何一个对象实例
    //final表示常量
    private static final int MAX_MODE_NUM = 2<<4;//16
    private int limit;


    public LRUTest(){
        this(MAX_MODE_NUM);
    }


    public LRUTest(int limit) {
        super(limit, 0.75f, true);
        this.limit = limit;
    }


    public V putValue(K key, V val) {
        return put(key, val);
    }


    public V getValue(K key) {
        return get(key);
    }


    //判断存储元素个数是否超过阈值
    @Override
    protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
        return size() > limit;
    }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.

代码先放这,等我们最后会把这个原理讲的非常明白。

LinkedHashMap先告一段落。

TreeMap类

TreeMap是SortedMap的实现类,是一个红黑树的数据结构,每个key-value对作为红黑树的一个节点。TreeMap存储key-value对时,需要根据key对节点进行排序。TreeMap也有两种排序方式:

♦ 自然排序:TreeMap的所有key必须实现Comparable接口,而且所有的key应该是同一个类的对象,否则会抛出ClassCastException。

♦ 定制排序:创建TreeMap时,传入一个Comparator对象,该对象负责对TreeMap中的所有key进行排序。

想要搞懂TreeMap的源码,需要先搞懂红黑树的原理,而红黑树的原理并不能简单说清楚,所以我后面会单独拿出一篇文章讲解红黑树的原理和TreeMap的源码分析。

同时,TreeMap的遍历也要等Set接口讲完再看,所以这里先停一下,看完Set再回来。

总结一下,HashMap、Hashtable、LinkedHashMap、TreeMap之间的对比:

精通Java系列 | 集合及源码解析_ci_07

Set接口

Set接口也是Collection接口的子接口,他最大的特点是元素无序且不可重复

Set接口的常用实现类:HashSet、LinkedHashSet、TreeSet。

HashSet类

HashSet是Set集合最常用实现类,是其经典实现。HashSet是按照hash算法来存储元素的,因此具有很好的存取和查找性能。

HashSet具有如下特点:

(1)不能保证元素的顺序。

(2)HashSet不是线程同步的,如果多线程操作HashSet集合,则应通过代码来保证其同步。

(3)集合元素值可以是null。 

(4)底层是HashMap,确保元素不重复。

public class HashSet<E>
    extends AbstractSet<E>
    implements Set<E>, Cloneable, java.io.Serializable
  • 1.
  • 2.
  • 3.

底层是HashMap:

private transient HashMap<E,Object> map;
  • 1.

添加元素直接调用HashMap的put方法;删除元素直接调用的是HashMap的remove方法;调用Hashmap的containsKey方法检查元素是否存在。

如果想要遍历Set元素,直接调用map的KeySet的迭代器。

Collection set1 = new HashSet();
set1.add(11);
set1.add(22);
set1.add(33);
set1.add(44);
Iterator iterator1 = set1.iterator();
while (iterator1.hasNext()){
    System.out.println(iterator1.next());
} //通过遍历发现,Set是无序的
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.

LinkedHashSet类

LinkedHashSet是HashSet的一个子类,具有HashSet的特性,也是根据元素的hashCode值来决定元素的存储位置。但它使用链表维护元素的次序,元素的顺序与添加顺序一致。由于LinkedHashSet需要维护元素的插入顺序,因此性能略低于HashSet,但在迭代访问Set里的全部元素时由很好的性能。

LinkedHashSet类的源码非常短小精悍:

public class LinkedHashSet<E>
    extends HashSet<E>
    implements Set<E>, Cloneable, java.io.Serializable {


    private static final long serialVersionUID = -2851667679971038690L;


    // 构造方法直接调用父类的构造方法:HashSet
    public LinkedHashSet(int initialCapacity, float loadFactor) {
        super(initialCapacity, loadFactor, true);
    }


    public LinkedHashSet(int initialCapacity) {
        super(initialCapacity, .75f, true);
    }


    public LinkedHashSet() {
        super(16, .75f, true);
    }


    public LinkedHashSet(Collection<? extends E> c) {
        super(Math.max(2*c.size(), 11), .75f, true);
        addAll(c);
    }


    @Override
    //可分割的迭代器,主要用于多线程并行迭代处理时使用
    public Spliterator<E> spliterator() {
        return Spliterators.spliterator(this, Spliterator.DISTINCT | Spliterator.ORDERED);
    }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.

TreeSet类

TreeSet底层是采用TreeMap实现的一种Set,它是有序的,同样也是非线程安全的。

TreeSet的底层元素也比较简单,可以直接看:

public class TreeSet<E> extends AbstractSet<E>
    implements NavigableSet<E>, Cloneable, java.io.Serializable
{
    //元素存储结构
    private transient NavigableMap<E,Object> m;


    private static final Object PRESENT = new Object();


    TreeSet(NavigableMap<E,Object> m) {
        this.m = m;
    }


    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);
    }


    //遍历是借助这样的迭代器实现的
    public Iterator<E> iterator() {
        return m.navigableKeySet().iterator();
    }


    //逆序迭代器
    public Iterator<E> descendingIterator() {
        return m.descendingKeySet().iterator();
    }


    //逆序返回一个新的TreeSet
    public NavigableSet<E> descendingSet() {
        return new TreeSet<>(m.descendingMap());
    }




    public boolean contains(Object o) {
        return m.containsKey(o);
    }


    // 核心方法调用父类方法
    public boolean add(E e) {
        return m.put(e, PRESENT)==null;
    }




    public boolean remove(Object o) {
        return m.remove(o)==PRESENT;
    }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.
  • 46.
  • 47.
  • 48.
  • 49.
  • 50.
  • 51.
  • 52.
  • 53.
  • 54.
  • 55.
  • 56.
  • 57.
  • 58.
  • 59.
  • 60.
  • 61.
  • 62.
  • 63.
  • 64.
  • 65.
  • 66.
  • 67.
  • 68.
  • 69.
  • 70.
  • 71.
  • 72.
  • 73.
  • 74.

在总结一下,Set接口的几个实现类:

精通Java系列 | 集合及源码解析_ci_08

好了,到此为止,我们学习完了Collection接口的所有核心子接口,并且分析了他们的源码,还有两个核心问题,我们需要交代一下。

1. 以LinkedHashMap为例,我们该如何遍历?他的底层原理是怎样的?

inkedHashMap的遍历顺序可以分为两种模式:插入顺序模式和访问顺序模式。

(1)插入顺序模式

在插入顺序模式下,遍历LinkedHashMap时的顺序是元素添加的顺序。即先添加的元素先被遍历到,后添加的元素后被遍历到。这种遍历顺序是通过维护一个双向链表实现的,链表的顺序就是元素的插入顺序。

LinkedHashMap<String, Integer> lmap = new LinkedHashMap<>();
lmap.put("a",1);
lmap.put("b",2);
lmap.put("c",3);
// 遍历lmap:按照插入顺序遍历
for (Map.Entry<String, Integer> entry : lmap.entrySet()){
    System.out.println(entry.getKey() + ":" + entry.getValue());
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.

精通Java系列 | 集合及源码解析_ci_09

(2)访问顺序模式

在访问顺序模式下,遍历LinkedHashMap时的顺序是元素最后一次访问的顺序,即最近访问的元素被最先遍历到,最早访问的元素被最后遍历到(LRU原则)。这种遍历顺序是通过维护一个双向链表和一个访问顺序队列实现的,每次访问元素时,将元素移到链表的末尾。

LinkedHashMap<String, Integer> lmap = new LinkedHashMap<>(16, 0.75f, true);
lmap.put("a",1);
lmap.put("b",2);
lmap.put("c",3);
lmap.get("b");
// 遍历lmap:按照插入顺序遍历
for (Map.Entry<String, Integer> entry : lmap.entrySet()){
    System.out.println(entry.getKey() + ":" + entry.getValue());
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.

精通Java系列 | 集合及源码解析_数组_10

需要注意的是,访问顺序模式需要在创建LinkedHashMap对象时传入参数accessOrder=true,默认为false,也就是插入顺序模式。

遍历的实现原理我们通过源码分析下:

public Set<Map.Entry<K,V>> entrySet() {
    Set<Map.Entry<K,V>> es;
    return (es = entrySet) == null ? (entrySet = new LinkedEntrySet()) : es;
}
  • 1.
  • 2.
  • 3.
  • 4.

通过将map的entry数组以Set视图展示,以遍历map。

2. modCount是做什么的?

我们在很多个子接口的实现类中都看到了这个属性,这个属性是做什么的?

以HashMap为例,我们介绍Fail-Fast 机制

我们知道 java.util.HashMap 不是线程安全的,因此如果在使用迭代器的过程中有其他线程修改了map,那么将抛出ConcurrentModificationException,这就是所谓fail-fast策略

这一策略在源码中的实现是通过 modCount 域,modCount 顾名思义就是修改次数,对HashMap 内容的修改都将增加这个值,那么在迭代器初始化过程中会将这个值赋给迭代器的 expectedModCount。

在迭代过程中,判断 modCount 跟 expectedModCount 是否相等,如果不相等就表示已经有其他线程修改了 Map:注意到 modCount 声明为 volatile,保证线程之间修改的可见性。