Skip to content

右侧栏

容器

Java 容器主要分为CollectionMap 两大部分,collection存储着对象的集合,而Map存储这键值对(两个对象)的映射表

Java容器里只能放对象,对于基本类型(int, long, float, double等),需要将其包装成对象类型后(Integer, Long, Float, Double等)才能放到容器里。很多时候拆包装和解包装能够自动完成。这虽然会导致额外的性能和空间开销,但简化了设计和编程

Java容器能够容纳任何类型的对象,这一点表面上是通过泛型机制完成。Java泛型不是什么神奇的东西,只是编译器为我们提供的一个“语法糖”,泛型本身并不需要Java虚拟机的支持,只需要在编译阶段做一下简单的字符串替换即可。实质上Java的单继承机制才是保证这一特性的根本,因为所有的对象都是Object的子类,容器里只要能够存放Object对象就行了。 事实上,所有容器的内部存放的都是Object对象,泛型机制只是简化了编程,由编译器自动帮我们完成了强制类型转换而已。JDK 1.4以及之前版本不支持泛型,类型转换需要程序员显式完成

image-20240828144423810

上述接口的通用实现见下表:

Implementations
Hash TableResizable ArrayBalanced TreeLinked ListHash Table + Linked List
InterfacesSet
List
Deque
Map

List

ArrayList

基于动态数组实现,支持随机访问

ArrayList实现了List接口,是顺序容器,即元素存放的数据与放进去的顺序相同,允许放入null元素,底层通过数组实现。除该类未实现同步外,其余跟Vector大致相同。**每个ArrayList都有一个容量(capacity)**,表示底层数组的实际大小,容器内存储元素的个数不能多于当前容量。当向容器中添加元素时,如果容量不足,容器会自动增大底层数组的大小。前面已经提过,Java泛型只是编译器提供的语法糖,所以这里的数组是一个Object数组,以便能够容纳任何类型的对象。

image-20240828161430500

size()isEmpt()get()set()方法均能在常数时间内完成,add()方法的时间开销跟插入位置有关,addAll() 方法的时间开销跟添加元素的个数成正比。其余方法大都是线性时间。为了追求效率,ArrayList 没有实现同步(synchronized),如果需要多个线程并发访问,用户可以手动同步,也可以使用Vector替代。

底层数据接口

java
/**
     * The array buffer into which the elements of the ArrayList are stored.
     * The capacity of the ArrayList is the length of this array buffer. Any
     * empty ArrayList with elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA
     * will be expanded to DEFAULT_CAPACITY when the first element is added.
     */
    transient Object[] elementData; // non-private to simplify nested class access

    /**
     * The size of the ArrayList (the number of elements it contains).
     *
     * @serial
     */
    private int size;

构造函数

java
	/**
     * Constructs an empty list with the specified initial capacity.
     *
     * @param  initialCapacity  the initial capacity of the list
     * @throws IllegalArgumentException if the specified initial capacity
     *         is negative
     */
    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);
        }
    }

    /**
     * Constructs an empty list with an initial capacity of ten.
     */
    public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }

    /**
     * Constructs a list containing the elements of the specified
     * collection, in the order they are returned by the collection's
     * iterator.
     *
     * @param c the collection whose elements are to be placed into this list
     * @throws NullPointerException if the specified collection is null
     */
    public ArrayList(Collection<? extends E> c) {
        elementData = c.toArray();
        if ((size = elementData.length) != 0) {
            // c.toArray might (incorrectly) not return Object[] (see 6260652)
            if (elementData.getClass() != Object[].class)
                elementData = Arrays.copyOf(elementData, size, Object[].class);
        } else {
            // replace with empty array.
            this.elementData = EMPTY_ELEMENTDATA;
        }
    }

自动扩容

每当向数组中添加元素时,都要去检查添加后元素的个数是否会超出当前数组的长度,如果超出,数组将会进行扩容,以满足添加数据的需求。数组扩容通过一个公开的方法ensureCapacity(int minCapacity)来实现。在实际添加大量元素前,我也可以使用ensureCapacity来手动增加ArrayList实例的容量,以减少递增式再分配的数量。

数组进行扩容时,会将老数组中的元素重新拷贝一份到新的数组中**,每次数据容量的增长大约是其原容量的1.5倍。**这种操作的代价是很高的,因此在实际使用时,**我们应该尽量避免数组容量的扩张。当我们可以预知要保存的元素的多少时,要在构造ArrayList实例时,就指定其容量,以避免数组扩容的发生。**或者根据实际需求,通过调用ensureCapacity方法来手动增加ArrayList实例的容量。

java
/**
     * Increases the capacity of this <tt>ArrayList</tt> instance, if
     * necessary, to ensure that it can hold at least the number of elements
     * specified by the minimum capacity argument.
     *
     * @param   minCapacity   the desired minimum capacity
     */
    public void ensureCapacity(int minCapacity) {
        int minExpand = (elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA)
            // any size if not default element table
            ? 0
            // larger than default for default empty table. It's already
            // supposed to be at default size.
            : DEFAULT_CAPACITY;

        if (minCapacity > minExpand) {
            ensureExplicitCapacity(minCapacity);
        }
    }

    private void ensureCapacityInternal(int minCapacity) {
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
        }

        ensureExplicitCapacity(minCapacity);
    }

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

        // overflow-conscious code
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }

    /**
     * The maximum size of array to allocate.
     * Some VMs reserve some header words in an array.
     * Attempts to allocate larger arrays may result in
     * OutOfMemoryError: Requested array size exceeds VM limit
     */
    private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

    /**
     * Increases the capacity to ensure that it can hold at least the
     * number of elements specified by the minimum capacity argument.
     *
     * @param minCapacity the desired minimum capacity
     */
    private void grow(int minCapacity) {
        // overflow-conscious code
        int oldCapacity = elementData.length;
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);
        // minCapacity is usually close to size, so this is a win:
        elementData = Arrays.copyOf(elementData, newCapacity);
    }

    private static int hugeCapacity(int minCapacity) {
        if (minCapacity < 0) // overflow
            throw new OutOfMemoryError();
        return (minCapacity > MAX_ARRAY_SIZE) ?
            Integer.MAX_VALUE :
            MAX_ARRAY_SIZE;
    }

image-20240828183547072

方法剖析

  • set()

    既然底层是一个数组,ArrayList的Set()方法也就变得非常简单,直接对数组的指定位置赋值即可。

    java
    public E set(int index, E element) {
        rangeCheck(index);//下标越界检查
        E oldValue = elementData(index);
        elementData[index] = element;//赋值到指定位置,复制的仅仅是引用
        return oldValue;
    }
  • get()

    get()方法同样很简单,唯一要注意的是由于底层数组是Object[],得到元素后需要进行类型转换。

    java
    public E get(int index) {
        rangeCheck(index);
        return (E) elementData[index];//注意类型转换
    }
  • add()

    C++ 的*vector不同,ArrayList没有push_back()方法,对应的方法是add(E e)ArrayList也没有insert()方法,对应的方法是add(int index, E e)。这两个方法都是向容器中添加新元素,这可能会导致capacity*不足,因此在添加元素之前,都需要进行剩余空间检查,如果需要则自动扩容。扩容操作最终是通过grow()方法完成的。

    java
    /**
         * Appends the specified element to the end of this list.
         *
         * @param e element to be appended to this list
         * @return <tt>true</tt> (as specified by {@link Collection#add})
         */
        public boolean add(E e) {
            ensureCapacityInternal(size + 1);  // Increments modCount!!
            elementData[size++] = e;
            return true;
        }
    
        /**
         * Inserts the specified element at the specified position in this
         * list. Shifts the element currently at that position (if any) and
         * any subsequent elements to the right (adds one to their indices).
         *
         * @param index index at which the specified element is to be inserted
         * @param element element to be inserted
         * @throws IndexOutOfBoundsException {@inheritDoc}
         */
        public void add(int index, E element) {
            rangeCheckForAdd(index);
    
            ensureCapacityInternal(size + 1);  // Increments modCount!!
            System.arraycopy(elementData, index, elementData, index + 1,
                             size - index);
            elementData[index] = element;
            size++;
        }
    java
    private void grow(int minCapacity) {
        int oldCapacity = elementData.length;
        int newCapacity = oldCapacity + (oldCapacity >> 1);//原来的1.5倍
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);
        elementData = Arrays.copyOf(elementData, newCapacity);//扩展空间并复制
    }

    由于java GC 自动管理的内存,这里也就不需要考虑源数组释放的问题。

    image-20240828163732893

    空间的问题解决后,插入过程就显得非常简单

    image-20240828163824523

    add(int index,E e)需要先对元素进行移动,然后完成插入操作,也就意味着该方法有着线性的时间复杂度。

  • addAll()

    addAll()方法能够一次添加多个元素,根据位置不同也有两个版本,一个是在末尾添加的addAll(Collection<? extends E> c)方法,一个是从指定位置开始插入的addAll(int index,Collection<? extend E> c),跟addAll()方法类似,在插入之前也需要进行空间检查,如果需要则自动扩容;如果从指定位置插入,也会存在移动元素的情况。addAll()的时间复杂度不仅跟插入元素的多少有关,也跟插入的位置相关

    java
    /**
         * Appends all of the elements in the specified collection to the end of
         * this list, in the order that they are returned by the
         * specified collection's Iterator.  The behavior of this operation is
         * undefined if the specified collection is modified while the operation
         * is in progress.  (This implies that the behavior of this call is
         * undefined if the specified collection is this list, and this
         * list is nonempty.)
         *
         * @param c collection containing elements to be added to this list
         * @return <tt>true</tt> if this list changed as a result of the call
         * @throws NullPointerException if the specified collection is null
         */
        public boolean addAll(Collection<? extends E> c) {
            Object[] a = c.toArray();
            int numNew = a.length;
            ensureCapacityInternal(size + numNew);  // Increments modCount
            System.arraycopy(a, 0, elementData, size, numNew);
            size += numNew;
            return numNew != 0;
        }
    
        /**
         * Inserts all of the elements in the specified collection into this
         * list, starting at the specified position.  Shifts the element
         * currently at that position (if any) and any subsequent elements to
         * the right (increases their indices).  The new elements will appear
         * in the list in the order that they are returned by the
         * specified collection's iterator.
         *
         * @param index index at which to insert the first element from the
         *              specified collection
         * @param c collection containing elements to be added to this list
         * @return <tt>true</tt> if this list changed as a result of the call
         * @throws IndexOutOfBoundsException {@inheritDoc}
         * @throws NullPointerException if the specified collection is null
         */
        public boolean addAll(int index, Collection<? extends E> c) {
            rangeCheckForAdd(index);
    
            Object[] a = c.toArray();
            int numNew = a.length;
            ensureCapacityInternal(size + numNew);  // Increments modCount
    
            int numMoved = size - index;
            if (numMoved > 0)
                System.arraycopy(elementData, index, elementData, index + numNew,
                                 numMoved);
    
            System.arraycopy(a, 0, elementData, index, numNew);
            size += numNew;
            return numNew != 0;
        }
  • remove()

    remove()方法也有两个版本,一个是remove(int index) 删除指定位置的元素,另一个是remove(Object o)删除第一个满足o.equals(elementData[index])的元素。删除操作是add()操作的逆过程,需要将删除点之后的元素向前移动一的位置。需要注意的是为了让GC起作用,必须显示的为最后一个位置赋null

    java
    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; //清除该位置的引用,让GC起作用
        return oldValue;
    }

    关于GC 这里需要额外说明一下,有了垃圾收集器并不意味着一定不会有内存泄漏。对象能否被GC的依据是是否还有引用指向它,上面代码中如果不手动赋null值,除非对应的位置被其他元素覆盖,否则原来的对象就一直不会被回收。

  • trimToSize()

    ArrayList 还给我们提供了将底层数组的容量调整为当前列表保存的实际元素的大小的功能。它可以通过trimToSize()方法来实现

    java
      /**
         * Trims the capacity of this <tt>ArrayList</tt> instance to be the
         * list's current size.  An application can use this operation to minimize
         * the storage of an <tt>ArrayList</tt> instance.
         */
        public void trimToSize() {
            modCount++;
            if (size < elementData.length) {
                elementData = (size == 0)
                  ? EMPTY_ELEMENTDATA
                  : Arrays.copyOf(elementData, size);
            }
        }
  • indexOf(),lastIndexOf()

    获取元素的第一次出现的index:

    java
    /**
         * Returns the index of the first occurrence of the specified element
         * in this list, or -1 if this list does not contain the element.
         * More formally, returns the lowest index <tt>i</tt> such that
         * <tt>(o==null&nbsp;?&nbsp;get(i)==null&nbsp;:&nbsp;o.equals(get(i)))</tt>,
         * or -1 if there is no such index.
         */
        public int indexOf(Object o) {
            if (o == null) {
                for (int i = 0; i < size; i++)
                    if (elementData[i]==null)
                        return i;
            } else {
                for (int i = 0; i < size; i++)
                    if (o.equals(elementData[i]))
                        return i;
            }
            return -1;
        }

    获取元素的最后一次出现的index:

    java
    /**
         * Returns the index of the last occurrence of the specified element
         * in this list, or -1 if this list does not contain the element.
         * More formally, returns the highest index <tt>i</tt> such that
         * <tt>(o==null&nbsp;?&nbsp;get(i)==null&nbsp;:&nbsp;o.equals(get(i)))</tt>,
         * or -1 if there is no such index.
         */
        public int lastIndexOf(Object o) {
            if (o == null) {
                for (int i = size-1; i >= 0; i--)
                    if (elementData[i]==null)
                        return i;
            } else {
                for (int i = size-1; i >= 0; i--)
                    if (o.equals(elementData[i]))
                        return i;
            }
            return -1;
        }

    Fail-Fast机制:

    ArrayList也采用了快速失败的机制,通过记录modCount参数来实现。在面对并发的修改时,迭代器很快就会完全失败,而不是冒着在将来某个不确定时间发生任意不确定的风险。

Vector

和ArrayList类似,但它是线程安全的

LinkedList

基于双向链表实现,只能顺序访问,但是可以快速地在链表中间插入和删除元素。不仅如此,LinkedList还可以用作栈、队列和双向队列。

底层数据结构

LinkedList底层通过双向链表实现,本节将着重讲解插入和删除元素时双向链表的维护过程。双向链表的每个节点用内部类 Node 表示,LinkList 通过firstlast引用只想链表的第一个元素和最后一个元素。注意这里没有所谓的哑元,当链表为空的时候 firstlast 都指向 null

java
	transient int size = 0;

    /**
     * Pointer to first node.
     * Invariant: (first == null && last == null) ||
     *            (first.prev == null && first.item != null)
     */
    transient Node<E> first;

    /**
     * Pointer to last node.
     * Invariant: (first == null && last == null) ||
     *            (last.next == null && last.item != null)
     */
    transient Node<E> last;

其中Node 是私有的内部类

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

构造函数

java
/**
     * Constructs an empty list.
     */
    public LinkedList() {
    }

    /**
     * Constructs a list containing the elements of the specified
     * collection, in the order they are returned by the collection's
     * iterator.
     *
     * @param  c the collection whose elements are to be placed into this list
     * @throws NullPointerException if the specified collection is null
     */
    public LinkedList(Collection<? extends E> c) {
        this();
        addAll(c);
    }

方法剖析

  • getFirst(),getLast()

    获取第一个元素,获取最后一个元素

    java
    /**
         * Returns the first element in this list.
         *
         * @return the first element in this list
         * @throws NoSuchElementException if this list is empty
         */
        public E getFirst() {
            final Node<E> f = first;
            if (f == null)
                throw new NoSuchElementException();
            return f.item;
        }
    
        /**
         * Returns the last element in this list.
         *
         * @return the last element in this list
         * @throws NoSuchElementException if this list is empty
         */
        public E getLast() {
            final Node<E> l = last;
            if (l == null)
                throw new NoSuchElementException();
            return l.item;
        }
  • removeFirst(),removeLast(),remove(e),remove(index)

    remove()方法也有两个版本,一个是删除跟指定元素相等的第一个元素remove(Object o),另一个是删除指定下标处的元素 remove(int index)image-20240829173107465

    删除元素-指的是删除第一次出现的这个元素,如果没有这个元素,则返回false;判断的依据是equals方法,如果equalstrue,则直接unlink这个node;由于linkedList可以存放null元素,故可以删除第一次出现的null的元素

    java
    /**
         * Removes the first occurrence of the specified element from this list,
         * if it is present.  If this list does not contain the element, it is
         * unchanged.  More formally, removes the element with the lowest index
         * {@code i} such that
         * <tt>(o==null&nbsp;?&nbsp;get(i)==null&nbsp;:&nbsp;o.equals(get(i)))</tt>
         * (if such an element exists).  Returns {@code true} if this list
         * contained the specified element (or equivalently, if this list
         * changed as a result of the call).
         *
         * @param o element to be removed from this list, if present
         * @return {@code true} if this list contained the specified element
         */
        public boolean remove(Object o) {
            if (o == null) {
                for (Node<E> x = first; x != null; x = x.next) {
                    if (x.item == null) {
                        unlink(x);
                        return true;
                    }
                }
            } else {
                for (Node<E> x = first; x != null; x = x.next) {
                    if (o.equals(x.item)) {
                        unlink(x);
                        return true;
                    }
                }
            }
            return false;
        }
        
        /**
         * Unlinks non-null node x.
         */
        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) {// 第一个元素
                first = next;
            } else {
                prev.next = next;
                x.prev = null;
            }
    
            if (next == null) {// 最后一个元素
                last = prev;
            } else {
                next.prev = prev;
                x.next = null;
            }
    
            x.item = null; // GC
            size--;
            modCount++;
            return element;
        }

    remove(int index)使用的是下标计数,只需要判断该index是否有元素即可,如果有则直接unlink这个node

    java
    /**
         * Removes the element at the specified position in this list.  Shifts any
         * subsequent elements to the left (subtracts one from their indices).
         * Returns the element that was removed from the list.
         *
         * @param index the index of the element to be removed
         * @return the element previously at the specified position
         * @throws IndexOutOfBoundsException {@inheritDoc}
         */
        public E remove(int index) {
            checkElementIndex(index);
            return unlink(node(index));
        }

    删除head元素

    java
    /**
         * Removes and returns the first element from this list.
         *
         * @return the first element from this list
         * @throws NoSuchElementException if this list is empty
         */
        public E removeFirst() {
            final Node<E> f = first;
            if (f == null)
                throw new NoSuchElementException();
            return unlinkFirst(f);
        }
    
    
        /**
         * Unlinks non-null first node f.
         */
        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;
        }

    删除last元素:

    java
    /**
         * Removes and returns the last element from this list.
         *
         * @return the last element from this list
         * @throws NoSuchElementException if this list is empty
         */
        public E removeLast() {
            final Node<E> l = last;
            if (l == null)
                throw new NoSuchElementException();
            return unlinkLast(l);
        }
        
        /**
         * Unlinks non-null last node l.
         */
        private E unlinkLast(Node<E> l) {
            // assert l == last && l != null;
            final E element = l.item;
            final Node<E> prev = l.prev;
            l.item = null;
            l.prev = null; // help GC
            last = prev;
            if (prev == null)
                first = null;
            else
                prev.next = null;
            size--;
            modCount++;
            return element;
        }
  • add()

    add()方法有两个版本,一个是add(E e),该方法在*LinkedList*的末尾插入元素,因为有last指向链表末尾,在末尾插入元素的花费是常数时间。只需要简单修改几个相关引用即可;另一个是add(int index, E element),该方法是在指定下表处插入元素,需要先通过线性查找找到具体位置,然后修改相关引用完成插入操作。

    java
        /**
         * Appends the specified element to the end of this list.
         *
         * <p>This method is equivalent to {@link #addLast}.
         *
         * @param e element to be appended to this list
         * @return {@code true} (as specified by {@link Collection#add})
         */
        public boolean add(E e) {
            linkLast(e);
            return true;
        }
        
        /**
         * Links e as last element.
         */
        void linkLast(E e) {
            final Node<E> l = last;
            final Node<E> newNode = new Node<>(l, e, null);
            last = newNode;
            if (l == null)
                first = newNode;
            else
                l.next = newNode;
            size++;
            modCount++;
        }

    image-20240829173851830

    add(int index, E element), 当index==size时,等同于add(E e); 如果不是,则分两步: 1.先根据index找到要插入的位置,即node(index)方法;2.修改引用,完成插入操作。

    Java
        /**
         * Inserts the specified element at the specified position in this list.
         * Shifts the element currently at that position (if any) and any
         * subsequent elements to the right (adds one to their indices).
         *
         * @param index index at which the specified element is to be inserted
         * @param element element to be inserted
         * @throws IndexOutOfBoundsException {@inheritDoc}
         */
        public void add(int index, E element) {
            checkPositionIndex(index);
    
            if (index == size)
                linkLast(element);
            else
                linkBefore(element, node(index));
        }

    上面代码中的node(int index)函数有一点小小的trick,因为链表双向的,可以从开始往后找,也可以从结尾往前找,具体朝那个方向找取决于条件index < (size >> 1),也即是index是靠近前端还是后端。从这里也可以看出,linkedList通过index检索元素的效率没有arrayList高。

    java
        /**
         * Returns the (non-null) Node at the specified element index.
         */
        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;
            }
        }# [#](#addall)
  • addAll()

    addAll(index, c) 实现方式并不是直接调用add(index,e)来实现,主要是应为效率问题,另一个是fail-fastmodCount只会增加1次,(集合类中modCount字段的作用

    java
    /**
         * Appends all of the elements in the specified collection to the end of
         * this list, in the order that they are returned by the specified
         * collection's iterator.  The behavior of this operation is undefined if
         * the specified collection is modified while the operation is in
         * progress.  (Note that this will occur if the specified collection is
         * this list, and it's nonempty.)
         *
         * @param c collection containing elements to be added to this list
         * @return {@code true} if this list changed as a result of the call
         * @throws NullPointerException if the specified collection is null
         */
        public boolean addAll(Collection<? extends E> c) {
            return addAll(size, c);
        }
    
        /**
         * Inserts all of the elements in the specified collection into this
         * list, starting at the specified position.  Shifts the element
         * currently at that position (if any) and any subsequent elements to
         * the right (increases their indices).  The new elements will appear
         * in the list in the order that they are returned by the
         * specified collection's iterator.
         *
         * @param index index at which to insert the first element
         *              from the specified collection
         * @param c collection containing elements to be added to this list
         * @return {@code true} if this list changed as a result of the call
         * @throws IndexOutOfBoundsException {@inheritDoc}
         * @throws NullPointerException if the specified collection is null
         */
        public boolean addAll(int index, Collection<? extends E> c) {
            checkPositionIndex(index);
    
            Object[] a = c.toArray();
            int numNew = a.length;
            if (numNew == 0)
                return false;
    
            Node<E> pred, succ;
            if (index == size) {
                succ = null;
                pred = last;
            } else {
                succ = node(index);
                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;
            }
    
            if (succ == null) {
                last = pred;
            } else {
                pred.next = succ;
                succ.prev = pred;
            }
    
            size += numNew;
            modCount++;
            return true;
        }
  • clear()

    为了让GC更快可以回收放置的元素,需要将node 之间的引用关系赋空。

    java
    /**
         * Removes all of the elements from this list.
         * The list will be empty after this call returns.
         */
        public void clear() {
            // Clearing all of the links between nodes is "unnecessary", but:
            // - helps a generational GC if the discarded nodes inhabit
            //   more than one generation
            // - is sure to free memory even if there is a reachable Iterator
            for (Node<E> x = first; x != null; ) {
                Node<E> next = x.next;
                x.item = null;
                x.next = null;
                x.prev = null;
                x = next;
            }
            first = last = null;
            size = 0;
            modCount++;
        }
  • indexOf(),lastIndexOf()

    查找第一次出现的index,如果找不到返回-1

    java
    	/**
         * Returns the index of the first occurrence of the specified element
         * in this list, or -1 if this list does not contain the element.
         * More formally, returns the lowest index {@code i} such that
         * <tt>(o==null&nbsp;?&nbsp;get(i)==null&nbsp;:&nbsp;o.equals(get(i)))</tt>,
         * or -1 if there is no such index.
         *
         * @param o element to search for
         * @return the index of the first occurrence of the specified element in
         *         this list, or -1 if this list does not contain the element
         */
        public int indexOf(Object o) {
            int index = 0;
            if (o == null) {
                for (Node<E> x = first; x != null; x = x.next) {
                    if (x.item == null)
                        return index;
                    index++;
                }
            } else {
                for (Node<E> x = first; x != null; x = x.next) {
                    if (o.equals(x.item))
                        return index;
                    index++;
                }
            }
            return -1;
        }

    查找最后一次出现的index, 如果找不到返回-1

    java
     /**
         * Returns the index of the last occurrence of the specified element
         * in this list, or -1 if this list does not contain the element.
         * More formally, returns the highest index {@code i} such that
         * <tt>(o==null&nbsp;?&nbsp;get(i)==null&nbsp;:&nbsp;o.equals(get(i)))</tt>,
         * or -1 if there is no such index.
         *
         * @param o element to search for
         * @return the index of the last occurrence of the specified element in
         *         this list, or -1 if this list does not contain the element
         */
        public int lastIndexOf(Object o) {
            int index = size;
            if (o == null) {
                for (Node<E> x = last; x != null; x = x.prev) {
                    index--;
                    if (x.item == null)
                        return index;
                }
            } else {
                for (Node<E> x = last; x != null; x = x.prev) {
                    index--;
                    if (o.equals(x.item))
                        return index;
                }
            }
            return -1;
        }
  • Queue方法

    java
       
        /**
         * Retrieves, but does not remove, the head (first element) of this list.
         *
         * @return the head of this list, or {@code null} if this list is empty
         * @since 1.5
         */
        public E peek() {
            final Node<E> f = first;
            return (f == null) ? null : f.item;
        }
    
        /**
         * Retrieves, but does not remove, the head (first element) of this list.
         *
         * @return the head of this list
         * @throws NoSuchElementException if this list is empty
         * @since 1.5
         */
        public E element() {
            return getFirst();
        }
    
        /**
         * Retrieves and removes the head (first element) of this list.
         *
         * @return the head of this list, or {@code null} if this list is empty
         * @since 1.5
         */
        public E poll() {
            final Node<E> f = first;
            return (f == null) ? null : unlinkFirst(f);
        }
    
        /**
         * Retrieves and removes the head (first element) of this list.
         *
         * @return the head of this list
         * @throws NoSuchElementException if this list is empty
         * @since 1.5
         */
        public E remove() {
            return removeFirst();
        }
    
        /**
         * Adds the specified element as the tail (last element) of this list.
         *
         * @param e the element to add
         * @return {@code true} (as specified by {@link Queue#offer})
         * @since 1.5
         */
        public boolean offer(E e) {
            return add(e);
        }
  • Deque方法

    java
        /**
         * Inserts the specified element at the front of this list.
         *
         * @param e the element to insert
         * @return {@code true} (as specified by {@link Deque#offerFirst})
         * @since 1.6
         */
        public boolean offerFirst(E e) {
            addFirst(e);
            return true;
        }
    
        /**
         * Inserts the specified element at the end of this list.
         *
         * @param e the element to insert
         * @return {@code true} (as specified by {@link Deque#offerLast})
         * @since 1.6
         */
        public boolean offerLast(E e) {
            addLast(e);
            return true;
        }
    
        /**
         * Retrieves, but does not remove, the first element of this list,
         * or returns {@code null} if this list is empty.
         *
         * @return the first element of this list, or {@code null}
         *         if this list is empty
         * @since 1.6
         */
        public E peekFirst() {
            final Node<E> f = first;
            return (f == null) ? null : f.item;
         }
    
        /**
         * Retrieves, but does not remove, the last element of this list,
         * or returns {@code null} if this list is empty.
         *
         * @return the last element of this list, or {@code null}
         *         if this list is empty
         * @since 1.6
         */
        public E peekLast() {
            final Node<E> l = last;
            return (l == null) ? null : l.item;
        }
    
        /**
         * Retrieves and removes the first element of this list,
         * or returns {@code null} if this list is empty.
         *
         * @return the first element of this list, or {@code null} if
         *     this list is empty
         * @since 1.6
         */
        public E pollFirst() {
            final Node<E> f = first;
            return (f == null) ? null : unlinkFirst(f);
        }
    
        /**
         * Retrieves and removes the last element of this list,
         * or returns {@code null} if this list is empty.
         *
         * @return the last element of this list, or {@code null} if
         *     this list is empty
         * @since 1.6
         */
        public E pollLast() {
            final Node<E> l = last;
            return (l == null) ? null : unlinkLast(l);
        }
    
        /**
         * Pushes an element onto the stack represented by this list.  In other
         * words, inserts the element at the front of this list.
         *
         * <p>This method is equivalent to {@link #addFirst}.
         *
         * @param e the element to push
         * @since 1.6
         */
        public void push(E e) {
            addFirst(e);
        }
    
        /**
         * Pops an element from the stack represented by this list.  In other
         * words, removes and returns the first element of this list.
         *
         * <p>This method is equivalent to {@link #removeFirst()}.
         *
         * @return the element at the front of this list (which is the top
         *         of the stack represented by this list)
         * @throws NoSuchElementException if this list is empty
         * @since 1.6
         */
        public E pop() {
            return removeFirst();
        }
    
        /**
         * Removes the first occurrence of the specified element in this
         * list (when traversing the list from head to tail).  If the list
         * does not contain the element, it is unchanged.
         *
         * @param o element to be removed from this list, if present
         * @return {@code true} if the list contained the specified element
         * @since 1.6
         */
        public boolean removeFirstOccurrence(Object o) {
            return remove(o);
        }
    
        /**
         * Removes the last occurrence of the specified element in this
         * list (when traversing the list from head to tail).  If the list
         * does not contain the element, it is unchanged.
         *
         * @param o element to be removed from this list, if present
         * @return {@code true} if the list contained the specified element
         * @since 1.6
         */
        public boolean removeLastOccurrence(Object o) {
            if (o == null) {
                for (Node<E> x = last; x != null; x = x.prev) {
                    if (x.item == null) {
                        unlink(x);
                        return true;
                    }
                }
            } else {
                for (Node<E> x = last; x != null; x = x.prev) {
                    if (o.equals(x.item)) {
                        unlink(x);
                        return true;
                    }
                }
            }
            return false;
        }

Queue & Stack

java里有一个叫做Stack 的类,却没有叫做Queue的类(它是个接口名字)。当需要使用栈时,java已不在推荐使用Stack,而是推荐使用更高效的ArrayDeque,既然Queue只是一个接口,当需要使用队列时也就首选ArrayDeque了(次选是LinkList)。

Queue

Queue 接口继承自Collection接口,除了最基本的Collection的方法之外,它还支持额外的insertionextraction,和inspection操作。这里有两组格式,共6个方法,一组是抛出异常的实现;另外一组是返回值的实现(没有则返回null)

Throws exceptionReturns special value
insertadd(e)offer(e)
removeremove()poll()
examine(英*/ɪɡˈzæmɪn/*)
检查,调查;考核,测验
element()peek()

Deque

Deque是“double ended queue”,表示双向的队列,英文读作“deck”。Deque 继承自 Queue接口,除了支持Queue的方法之外,还支持insert, removeexamine操作,由于Deque是双向的,所以可以对队列的头和尾都进行操作,它同时也支持两组格式,一组是抛出异常的实现;另外一组是返回值的实现(没有则返回null)。共12个方法如下

First Element - HeadLast Element -Tail
Throws exceptionSpeial valueThrows exceptionSpecial value
InsertaddFirst(e)offerFirst(e)addLast(e)offerLast(e)
RemoveremoveFirst()pollFirst()removeLast()pollLast()
ExaminegetFirst()peekFirst()getLast()peekLast()

Deque的含义是“double ended queue”,即双端队列,它既可以当作栈使用,也可以当作队列使用。当把 Deque 当作FIFO的 queue 来使用时,元素是从deque 的尾部添加,从头部进行删除的。下表列出了Deque与Queue相对应的接口:

Queue MethodEquivalent Deque Method说明
add(e)addLast(e)向队尾插入元素,失败则抛出异常
offer(e)offerLast(e)向队尾插入元素,失败则返回false
remove()removeFirst()获取并删除队首元素,失败则抛出异常
poll()pollFirst()获取并删除队首元素,失败则返回null
element()getFirst()获取但不删除队首元素,失败则抛出异常
peek()peekFirst()获取但不删除队首元素,失败则返回null

下表列出了Deque 和 Stack 对应的接口:

Stack MethodEquivalent Deque Method说明
push(e)addFrist(e)向栈顶插入元素,失败则抛出异常
offerFirst(e)向栈顶插入元素,失败则返回false
pop()removeFirst()获取并删除栈顶元素,失败则抛出异常
pollFirst()获取并删除栈顶元素,失败则返回null
peek()getFirst()获取但不删除栈顶元素,失败则抛出异常
peekFirst()获取但不删除栈顶元素,失败则返回null

上面两个表共定义了Deque 的12个接口。添加,删除,取值都有两套接口,它们功能相同,区别是对失败情况的处理不同。一套接口遇到失败就会抛出异常,另一套遇到失败会返回特殊值(falsenull)。除非某种实现对容量有很大限制,大多数情况下,添加操作是不会失败的。虽然Deque的接口有12个之多,但无非就是对容器的两端进行操作,或添加,或删除或查看。

ArrqyDequeLinkedListDeque的两个通用实现,由于官方更推荐使用ArrayDeque用作栈和队列,加之上一篇已经讲过LinkedList,本文将着重讲解ArrayDeque的具体实现。

从名字可以看出ArrayDeque底层通过数组实现,为了满足可以同时在数组两端插入或删除元素的需求,该数组还必须是循环的,即循环数组,也就是说数组的任何一点都可能被看作起点或者终点。ArrayDeque是非线程安全的,当多个线程同时使用时,需要程序员手动同步;另外,该容器不允许放入null 元素

image-20240830115311842

上图中我们看到,head 指向首段第一个有效元素,tail 指向尾端第一个可以插入元素的空位。因为是循环数组,所有head 不一定总等于0,tail 也不一定总是比head大。

方法剖析

  • addFirst()

    addFirst(E e) 的作用是在Deque的首端插入元素,也就是在Head 前面插入元素,在空间足够且下标没有越界的情况下,只需要将elements[--head] = e 即可。

    实际需要考虑:1.空间是否够用。2.下标是否越界的问题。上图中,如果head0 之后接着调用addFirst(),虽然空余空间还够用,但head-1,下标越界了。下列代码很好的解决了这俩个问题。

    java
    //addFirst(E e)
    public void addFirst(E e) {
        if (e == null)//不允许放入null
            throw new NullPointerException();
        elements[head = (head - 1) & (elements.length - 1)] = e;//2.下标是否越界
        if (head == tail)//1.空间是否够用
            doubleCapacity();//扩容
    }

    上述代码我们看到,空间问题是在插入之后解决的,因为tail 总是指向下一个可插入的空位,也就意味着elements数组至少有一个空位,所以插入元素的时候不用考虑空间问题。

    下标越界的处理解决起来非常简单,head=(head -1)&(elements.length-1)就可以了,这段代码相当于取余,同时解决的head 为负值的情况。因为elements.length必需是2的指数倍,elements - 1 就是二进制低位全 1 ,跟head -1 相与之后就起到了取模的作用,如果head -1 为负数(其实只可能是-1),则相对于对其取elements.length的补码

    下面再说说扩容函数doubleCapacity(),其逻辑是申请一个更大的数组(原数组的两倍),然后将原数组复制过去。过程如下图所示:image-20240830133822865

    图中我们看到,复制分两次进行,第一次复制 head 右边的元素,第二次复制head 左边的元素。

    java
    //doubleCapacity()
    private void doubleCapacity() {
        assert head == tail;
        int p = head;
        int n = elements.length;
        int r = n - p; // head右边元素的个数
        int newCapacity = n << 1;//原空间的2倍
        if (newCapacity < 0)
            throw new IllegalStateException("Sorry, deque too big");
        Object[] a = new Object[newCapacity];
        System.arraycopy(elements, p, a, 0, r);//复制右半部分,对应上图中绿色部分
        System.arraycopy(elements, 0, a, r, p);//复制左半部分,对应上图中灰色部分
        elements = (E[])a;
        head = 0;
        tail = n;
    }
  • addLast()

    addLast(E e)的作用是在Deque的尾端插入元素,也就是在tail的位置插入元素,由于tail总是指向下一个可以插入的空位,因此只需要elements[tail] = e即可。插入完成后再检查空间,如果空间已经用光,则调用doubleCapactity() 进行扩容。image-20240830134441221

    java

public void addLast(E e) {
if (e == null)//不允许放入null
throw new NullPointerException();
elements[tail] = e;//赋值
if ( (tail = (tail + 1) & (elements.length - 1)) == head)//下标越界处理
doubleCapacity();//扩容
}


下标越界处理方式`addFirst()`中已经讲过,不在赘述。

* pollFirst()

`pollFirst()` 的作用是删除并返回`queue`首端元素,也即是`head`位置处的元素。如果容器不空,只需要直接返回`elements[head]`即可,当然还需要处理下标的问题,由于`arrqyDeque`中不允许放入`null`,当`elements[head]==null`时,意味着容器为空。

```java
public E pollFirst() {
    int h = head;
    E result = elements[head];
    if (result == null)//null值意味着deque为空
        return null;
    elements[h] = null;//let GC work
    head = (head + 1) & (elements.length - 1);//下标越界处理
    return result;
}
  • pollLast()

    pollLast()的作用是删除并返回Deque尾端的元素,也即是tail位置前面的那个元素。

    java
    public E pollLast() {
        int t = (tail - 1) & (elements.length - 1);//tail的上一个位置是最后一个元素
        E result = elements[t];
        if (result == null)//null值意味着deque为空
            return null;
        elements[t] = null;//let GC work
        tail = t;
        return result;
    }
  • peekFirst()

    peekFirst()的作用是返回但不删除*Deque*首端元素,也即是head位置处的元素,直接返回elements[head]即可

    java
    public E peekFirst() {
        return elements[head]; // elements[head] is null if deque empty
    }
  • peekLast()

    peekLast()的作用是返回但不删除Deque尾端元素,也即是tail 位置前面的那个元素。

    java
    public E peekLast() {
        return elements[(tail - 1) & (elements.length - 1)];
    }

PriorityQueue

基于堆结构实现,可以用它来实现优先队列

前面以Java ArrayDeque为例讲解了*StackQueue,其实还有一种特殊的队列叫做PriorityQueue*,即优先队列。优先队列的作用是能保证每次取出的元素都是队列中权值最小的(Java的优先队列每次取最小元素,C++的优先队列每次取最大元素)。这里牵涉到了大小关系,元素大小的评判可以通过元素本身的自然顺序(natural ordering,也可以通过构造时传入的比较器(Comparator,类似于C++的仿函数)。

Java中*PriorityQueue实现了Queue接口,不允许放入null元素;其通过堆实现,具体说是通过完全二叉树(complete binary tree)实现的小顶堆(任意一个非叶子节点的权值,都不大于其左右子节点的权值),也就意味着可以通过数组来作为PriorityQueue*的底层实现。

image-20240830142222465

上图中我们给每个元素按照层序遍历的方式进行了编导,如果你足够细心,会发现父节点和子节点的编号是有联系的,更确切的说父子节点的编号之间有如下关系:

leftNo = parentNo * 2 + 1

rightNo = parentNo * 2 + 2

parentNo = (nodeNo - 1) / 2

通过这上述三个公式,可以轻易计算出某个节点的父节点以及子节点的下标。这也就是为什么可以直接用数组来存储堆的原因。

PriorityQueuepeek()element操作是常数时间,add()offer(),无参数的remove()以及poll()方法的时间复杂度都是log(N)

方法剖析

  • add() 和offer()

    add(E e)offer(E e)的语义相同,都是向优先队列中插入元素,只是Queue接口规定二者对插入失败时的处理不同,前者在插入失败时抛出异常,后者则会返回false。对于PriorityQueue这两个方法其实没什么差别。image-20240830144150090

    新加入的元素可能会破坏小顶堆的性质,因此需要进行必要的调整。

    java

//offer(E e)
public boolean offer(E e) {
if (e == null)//不允许放入null元素
throw new NullPointerException();
modCount++;
int i = size;
if (i >= queue.length)
grow(i + 1);//自动扩容
size = i + 1;
if (i == 0)//队列原来为空,这是插入的第一个元素
queue[0] = e;
else
siftUp(i, e);//调整
return true;
}


上述代码中,扩容函数`grow()`类似于`ArrayList`里的`grow()`函数,就是再申请一个更大的数组,并将原数组的元素复制过去,这里不再赘述。需要注意的是`siftUp(int k, E x)`方法,该方法用于插入元素`x`并维持堆的特性。

```java
//siftUp()
private void siftUp(int k, E x) {
    while (k > 0) {
        int parent = (k - 1) >>> 1;//parentNo = (nodeNo-1)/2
        Object e = queue[parent];
        if (comparator.compare(x, (E) e) >= 0)//调用比较器的比较方法
            break;
        queue[k] = e;
        k = parent;
    }
    queue[k] = x;
}

新加入的元素x可能会破坏小顶堆的性质,因此需要进行调整。调整的过程为: k指定的位置开始,将x逐层与当前点的parent进行比较并交换,直到满足x >= queue[parent]为止。注意这里的比较可以是元素的自然顺序,也可以是依靠比较器的顺序。

  • element()和peek()

    element()peek()的语义完全相同,都是获取但不删除队首元素,也就是队列中权值最小的那个元素,二者唯一的区别是当方法失败时前者抛出异常,后者返回null。根据小顶堆的性质,堆顶那个元素就是全局最小的那个。由于堆用数组表示,根据下标关系。0下标处的那个元素既是堆顶元素。所以直接返回数组0下标处的那个元素即可。

    image-20240830144907693

    代码也就非常简洁:

    java
    //peek()
    public E peek() {
        if (size == 0)
            return null;
        return (E) queue[0];//0下标处的那个元素就是最小的那个
    }
  • remove() 和 poll()

    remove()poll()方法的语义也完全相同,都是获取并删除队首元素,区别是当方法失败时前者抛出异常,后者返回null。由于删除操作会改变队列的结构,为维护小顶堆的性质,需要进行必要的调整。image-20240830145027567

    java
    public E poll() {
        if (size == 0)
            return null;
        int s = --size;
        modCount++;
        E result = (E) queue[0];//0下标处的那个元素就是最小的那个
        E x = (E) queue[s];
        queue[s] = null;
        if (s != 0)
            siftDown(0, x);//调整
        return result;
    }

    上述代码首先记录0下标处的元素,并用最后一个元素替换0下标位置的元素,之后调用siftDown()方法对堆进行调整,最后返回原来0下标处的那个元素(也就是最小的那个元素)。重点是siftDown(int k, E x)方法,该方法的作用是k指定的位置开始,将x逐层向下与当前节点的左右孩子中较小的那个交换,直到x小于或等于左右孩子中的任何一个为止.

    java
    //siftDown()
    private void siftDown(int k, E x) {
        int half = size >>> 1;
        while (k < half) {
        	//首先找到左右孩子中较小的那个,记录到c里,并用child记录其下标
            int child = (k << 1) + 1;//leftNo = parentNo*2+1
            Object c = queue[child];
            int right = child + 1;
            if (right < size &&
                comparator.compare((E) c, (E) queue[right]) > 0)
                c = queue[child = right];
            if (comparator.compare(x, (E) c) <= 0)
                break;
            queue[k] = c;//然后用c取代原来的值
            k = child;
        }
        queue[k] = x;
    }
  • remove(Object o)

    remove(Object o)方法用于删除队列中跟o相等的某一个元素(如果有多个相等,只删除一个),该方法不是Queue接口内的方法,而是Collection接口的方法。由于删除操作会改变队列结构,所以要进行调整;又由于删除元素的位置可能是任意的,所以调整过程比其它函数稍加繁琐。具体来说,remove(Object o)可以分为2种情况: 1. 删除的是最后一个元素。直接删除即可,不需要调整。2. 删除的不是最后一个元素,从删除点开始以最后一个元素为参照调用一次siftDown()即可。此处不再赘述。image-20240830145216302

    具体代码如下:

    java
    //remove(Object o)
    public boolean remove(Object o) {
    	//通过遍历数组的方式找到第一个满足o.equals(queue[i])元素的下标
        int i = indexOf(o);
        if (i == -1)
            return false;
        int s = --size;
        if (s == i) //情况1
            queue[i] = null;
        else {
            E moved = (E) queue[s];
            queue[s] = null;
            siftDown(i, moved);//情况2
            ......
        }
        return true;
    }

Set

HashSet

已经说过HashSet是对HashMap的简单包装,对HashSet的函数调用都会转换成合适的HashMap方法,具体实现可以参考后文HashMap实现

java
//HashSet是对HashMap的简单包装
public class HashSet<E>
{
	......
	private transient HashMap<E,Object> map;//HashSet里面有一个HashMap
    // Dummy value to associate with an Object in the backing Map
    private static final Object PRESENT = new Object();
    public HashSet() {
        map = new HashMap<>();
    }
    ......
    public boolean add(E e) {//简单的方法转换
        return map.put(e, PRESENT)==null;
    }
    ......
}

LinkedHashSet

HashSet类似,LinkedHashSetLinkedHashMap在Java里也有着相同的实现,前者仅仅是对后者做了一层包装,也就是说LinkedHashSet里面有一个LinkedHashMap(适配器模式)。对于LinkHashSet的实现可以参考后文LinkHashMap实现。

java
public class LinkedHashSet<E>
    extends HashSet<E>
    implements Set<E>, Cloneable, java.io.Serializable {
    ......
    // LinkedHashSet里面有一个LinkedHashMap
    public LinkedHashSet(int initialCapacity, float loadFactor) {
        map = new LinkedHashMap<>(initialCapacity, loadFactor);
    }
	......
    public boolean add(E e) {//简单的方法转换
        return map.put(e, PRESENT)==null;
    }
    ......
}

TreeSet

基于红黑树实现,支持有序性操作,例如根据一个范围查找元素的操作。但是查找效率不如HashSet,HashSet查找的时间复杂度为O(1),TreeSet则为O(logN)。与上文类似,TreeSet与TreeMap二者在Java里有着相同的实现,前者仅仅是对后者做了一层包装,也就是说TreeSet里面有一个TreeMap(适配器模式)。对于TreeSet的具体实现可以操作TreeMap

java
// TreeSet是对TreeMap的简单包装
public class TreeSet<E> extends AbstractSet<E>
    implements NavigableSet<E>, Cloneable, java.io.Serializable
{
	......
    private transient NavigableMap<E,Object> m;
    // Dummy value to associate with an Object in the backing Map
    private static final Object PRESENT = new Object();
    public TreeSet() {
        this.m = new TreeMap<E,Object>();// TreeSet里面有一个TreeMap
    }
    ......
    public boolean add(E e) {
        return m.put(e, PRESENT)==null;
    }
    ......
}

Map

TreeMap

TreeMap底层基于红黑树实现,也就意味着containsKey()get()put()remove()都有着log(n)的时间复杂度。

image-20240902155902220

出于性能原因,*TreeMap是非同步的(not synchronized),如果需要在多线程环境使用,需要程序员手动同步;或者通过如下方式将TreeMap*包装成(wrapped)同步的:SortedMap m = Collections.synchronizedSortedMap(new TreeMap(...));

红黑树是一种近似平衡的二叉查找树,它能够确保任何一个节点的左右子树的高度差不会超过二者中较低那个的一倍。具体来说,红黑树是满足如下条件的二叉查找树(binary search tree):

  1. 每个节点要么是红色,要么是黑色
  2. 根节点必须是黑色
  3. 红色节点不能连续(也即是,红色节点的孩子和父亲都不能是红色)。
  4. 对于每个节点,从该点至null(树尾端)的任何路径,都含有相同个数的黑色节点。

在树的结构发生改变时(插入或者删除操作),往往会破坏上述条件3或条件4,需要通过调整使得查找树重新满足红黑树的约束条件。

当查找树的结构发生变化时,红黑树的约束条件可能被破坏,需要通过调整使得查找树重新满足红黑树的约束条件。调整可以分为两类:一类是颜色调整,即改变某个节点的颜色;另一类是结构调整,即改变检索树的结构关系。结构调整过程包含两个基本操作:左旋右旋

左旋

左旋的过程是将x的右子树绕x逆时针旋转,使得x的右子树成为x的父亲,同时修改相关节点的引用。旋转之后,二叉查找树的属性仍然满足。

image-20240902160626833

TreeMap 中的左旋代码如下:

java
//Rotate Left
private void rotateLeft(Entry<K,V> p) {
    if (p != null) {
        Entry<K,V> r = p.right;
        p.right = r.left;
        if (r.left != null)
            r.left.parent = p;
        r.parent = p.parent;
        if (p.parent == null)
            root = r;
        else if (p.parent.left == p)
            p.parent.left = r;
        else
            p.parent.right = r;
        r.left = p;
        p.parent = r;
    }
}

右旋

右旋的过程是将x的左子树绕x顺时针旋转,使得x的左子树成为x的父亲,同时修改相关节点的引用。旋转之后,二叉查找树的属性仍然满足。

image-20240902160740059

TreeMap中右旋代码如下:

java
//Rotate Right
private void rotateRight(Entry<K,V> p) {
    if (p != null) {
        Entry<K,V> l = p.left;
        p.left = l.right;
        if (l.right != null) l.right.parent = p;
        l.parent = p.parent;
        if (p.parent == null)
            root = l;
        else if (p.parent.right == p)
            p.parent.right = l;
        else p.parent.left = l;
        l.right = p;
        p.parent = l;
    }
}

寻找节点后继

对于一棵二叉查找树,给定节点t,其后继(树中比大于t的最小的那个元素)可以通过如下方式找到:

  1. t的右子数不为空,则t的后继是其右子树中最小的那个元素。
  2. t的右孩子为空,则t的后继是其第一个向左走的祖先。

后继节点在红黑树的删除操作中将会用到。

image-20240902161121227

TreeMap中寻找节点后继的代码如下:

java
// 寻找节点后继函数successor()
static <K,V> TreeMap.Entry<K,V> successor(Entry<K,V> t) {
    if (t == null)
        return null;
    else if (t.right != null) {// 1. t的右子树不空,则t的后继是其右子树中最小的那个元素
        Entry<K,V> p = t.right;
        while (p.left != null)
            p = p.left;
        return p;
    } else {// 2. t的右孩子为空,则t的后继是其第一个向左走的祖先
        Entry<K,V> p = t.parent;
        Entry<K,V> ch = t;
        while (p != null && ch == p.right) {
            ch = p;
            p = p.parent;
        }
        return p;
    }
}

方法剖析

  • get()

    get(Object key)方法根据指定的key值返回对应的value,该方法调用了getEntry(Object key)得到相应的entry,然后返回entry.value。因此getEntry()是算法的核心。算法思想是根据key的自然顺序(或者比较器顺序)对二叉查找树进行查找,直到找到满足k.compareTo(p.key) == 0entry

    image-20240902161537843

    具体代码如下:

    java
    //getEntry()方法
    final Entry<K,V> getEntry(Object key) {
        ......
        if (key == null)//不允许key值为null
            throw new NullPointerException();
        Comparable<? super K> k = (Comparable<? super K>) key;//使用元素的自然顺序
        Entry<K,V> p = root;
        while (p != null) {
            int cmp = k.compareTo(p.key);
            if (cmp < 0)//向左找
                p = p.left;
            else if (cmp > 0)//向右找
                p = p.right;
            else
                return p;
        }
        return null;
    }
  • put()

    put(K key, V value)方法是将指定的key, value对添加到map里。该方法首先会对map做一次查找,看是否包含该元组,如果已经包含则直接返回,查找过程类似于getEntry()方法;如果没有找到则会在红黑树中插入新的entry,如果插入之后破坏了红黑树的约束条件,还需要进行调整(旋转,改变某些节点的颜色)。

    java
    public V put(K key, V value) {
    	......
        int cmp;
        Entry<K,V> parent;
        if (key == null)
            throw new NullPointerException();
        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);//创建并插入新的entry
        if (cmp < 0) parent.left = e;
        else parent.right = e;
        fixAfterInsertion(e);//调整
        size++;
        return null;
    }

    上述代码的插入部分并不难理解:首先在红黑树上找到合适的位置,然后创建新的entry并插入(当然,新插入的节点一定是树的叶子)。难点是调整函数fixAfterInsertion(),前面已经说过,调整往往需要1. 改变某些节点的颜色,2.对某些节点进行旋转。

    调整函数fixAfterInsertion()的具体代码如下,其中用到了上文中提到的rotateLeft()rotateRight()函数。通过代码我们能够看到,情况2其实是落在情况3内的。情况4~情况6跟前三种情况是对称的,因此图解中并没有画出后三种情况,读者可以参考代码自行理解

    java
    //红黑树调整函数fixAfterInsertion()
    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);              // 情况1
                    setColor(y, BLACK);                        // 情况1
                    setColor(parentOf(parentOf(x)), RED);      // 情况1
                    x = parentOf(parentOf(x));                 // 情况1
                } else {
                    if (x == rightOf(parentOf(x))) {
                        x = parentOf(x);                       // 情况2
                        rotateLeft(x);                         // 情况2
                    }
                    setColor(parentOf(x), BLACK);              // 情况3
                    setColor(parentOf(parentOf(x)), RED);      // 情况3
                    rotateRight(parentOf(parentOf(x)));        // 情况3
                }
            } else {
                Entry<K,V> y = leftOf(parentOf(parentOf(x)));
                if (colorOf(y) == RED) {
                    setColor(parentOf(x), BLACK);              // 情况4
                    setColor(y, BLACK);                        // 情况4
                    setColor(parentOf(parentOf(x)), RED);      // 情况4
                    x = parentOf(parentOf(x));                 // 情况4
                } else {
                    if (x == leftOf(parentOf(x))) {
                        x = parentOf(x);                       // 情况5
                        rotateRight(x);                        // 情况5
                    }
                    setColor(parentOf(x), BLACK);              // 情况6
                    setColor(parentOf(parentOf(x)), RED);      // 情况6
                    rotateLeft(parentOf(parentOf(x)));         // 情况6
                }
            }
        }
        root.color = BLACK;
    }
  • remove()

    remove(Object key)的作用是删除key值对应的entry,该方法首先通过上文中提到的getEntry(Object key)方法找到key值对应的entry,然后调用deleteEntry(Entry<K,V> entry)删除对应的entry。由于删除操作会改变红黑树的结构,有可能破坏红黑树的约束条件,因此有可能要进行调整。

    getEntry()函数前面已经讲解过,这里重点放deleteEntry()上,该函数删除指定的entry并在红黑树的约束被破坏时进行调用fixAfterDeletion(Entry<K,V> x)进行调整。

    由于红黑树是一棵增强版的二叉查找树,红黑树的删除操作跟普通二叉查找树的删除操作也就非常相似,唯一的区别是红黑树在节点删除之后可能需要进行调整。现在考虑一棵普通二叉查找树的删除过程,可以简单分为两种情况:

    1. 删除点p的左右子树都为空,或者只有一棵子树非空。
    2. 删除点p的左右子树都非空。

    对于上述情况1,处理起来比较简单,直接将p删除(左右子树都为空时),或者用非空子树替代p(只有一棵子树非空时);对于情况2,可以用p的后继s(树中大于x的最小的那个元素)代替p,然后使用情况1删除s(此时s一定满足情况1.可以画画看)。

    基于以上逻辑,红黑树的节点删除函数deleteEntry()代码如下:

    java
    // 红黑树entry删除函数deleteEntry()
    private void deleteEntry(Entry<K,V> p) {
        modCount++;
        size--;
        if (p.left != null && p.right != null) {// 2. 删除点p的左右子树都非空。
            Entry<K,V> s = successor(p);// 后继
            p.key = s.key;
            p.value = s.value;
            p = s;
        }
        Entry<K,V> replacement = (p.left != null ? p.left : p.right);
        if (replacement != null) {// 1. 删除点p只有一棵子树非空。
            replacement.parent = p.parent;
            if (p.parent == null)
                root = replacement;
            else if (p == p.parent.left)
                p.parent.left  = replacement;
            else
                p.parent.right = replacement;
            p.left = p.right = p.parent = null;
            if (p.color == BLACK)
                fixAfterDeletion(replacement);// 调整
        } else if (p.parent == null) {
            root = null;
        } else { // 1. 删除点p的左右子树都为空
            if (p.color == BLACK)
                fixAfterDeletion(p);// 调整
            if (p.parent != null) {
                if (p == p.parent.left)
                    p.parent.left = null;
                else if (p == p.parent.right)
                    p.parent.right = null;
                p.parent = null;
            }
        }
    }

    上述代码中占据大量代码行的,是用来修改父子节点间引用关系的代码,其逻辑并不难理解。下面着重讲解删除后调整函数fixAfterDeletion()。首先请思考一下,删除了哪些点才会导致调整?只有删除点是BLACK的时候,才会触发调整函数,因为删除RED节点不会破坏红黑树的任何约束,而删除BLACK节点会破坏规则4。

    跟上文中讲过的fixAfterInsertion()函数一样,这里也要分成若干种情况。记住,无论有多少情况,具体的调整操作只有两种: 1.改变某些节点的颜色,2.对某些节点进行旋转。treeMap

    上述图解的总体思想是: 将情况1首先转换成情况2,或者转换成情况3和情况4。当然,该图解并不意味着调整过程一定是从情况1开始。通过后续代码我们还会发现几个有趣的规则: a).如果是由情况1之后紧接着进入的情况2,那么情况2之后一定会退出循环(因为x为红色);b).一旦进入情况3和情况4,一定会退出循环(因为x为root)。

    删除后调整函数fixAfterDeletion()的具体代码如下,其中用到了上文中提到的rotateLeft()rotateRight()函数。通过代码我们能够看到,情况3其实是落在情况4内的。情况5~情况8跟前四种情况是对称的,因此图解中并没有画出后四种情况,读者可以参考代码自行理解。

    java
    private void fixAfterDeletion(Entry<K,V> x) {
        while (x != root && colorOf(x) == BLACK) {
            if (x == leftOf(parentOf(x))) {
                Entry<K,V> sib = rightOf(parentOf(x));
                if (colorOf(sib) == RED) {
                    setColor(sib, BLACK);                   // 情况1
                    setColor(parentOf(x), RED);             // 情况1
                    rotateLeft(parentOf(x));                // 情况1
                    sib = rightOf(parentOf(x));             // 情况1
                }
                if (colorOf(leftOf(sib))  == BLACK &&
                    colorOf(rightOf(sib)) == BLACK) {
                    setColor(sib, RED);                     // 情况2
                    x = parentOf(x);                        // 情况2
                } else {
                    if (colorOf(rightOf(sib)) == BLACK) {
                        setColor(leftOf(sib), BLACK);       // 情况3
                        setColor(sib, RED);                 // 情况3
                        rotateRight(sib);                   // 情况3
                        sib = rightOf(parentOf(x));         // 情况3
                    }
                    setColor(sib, colorOf(parentOf(x)));    // 情况4
                    setColor(parentOf(x), BLACK);           // 情况4
                    setColor(rightOf(sib), BLACK);          // 情况4
                    rotateLeft(parentOf(x));                // 情况4
                    x = root;                               // 情况4
                }
            } else { // 跟前四种情况对称
                Entry<K,V> sib = leftOf(parentOf(x));
                if (colorOf(sib) == RED) {
                    setColor(sib, BLACK);                   // 情况5
                    setColor(parentOf(x), RED);             // 情况5
                    rotateRight(parentOf(x));               // 情况5
                    sib = leftOf(parentOf(x));              // 情况5
                }
                if (colorOf(rightOf(sib)) == BLACK &&
                    colorOf(leftOf(sib)) == BLACK) {
                    setColor(sib, RED);                     // 情况6
                    x = parentOf(x);                        // 情况6
                } else {
                    if (colorOf(leftOf(sib)) == BLACK) {
                        setColor(rightOf(sib), BLACK);      // 情况7
                        setColor(sib, RED);                 // 情况7
                        rotateLeft(sib);                    // 情况7
                        sib = leftOf(parentOf(x));          // 情况7
                    }
                    setColor(sib, colorOf(parentOf(x)));    // 情况8
                    setColor(parentOf(x), BLACK);           // 情况8
                    setColor(leftOf(sib), BLACK);           // 情况8
                    rotateRight(parentOf(x));               // 情况8
                    x = root;                               // 情况8
                }
            }
        }
        setColor(x, BLACK);
    }

HashMap

基于哈希表实现

HashSet实现中持有了一个HashMap的引用,二者有着相同的实现,HashSet仅仅是对后者做了已成包装。

HashMap 实现了Map 接口,即允许放入KeyNull 的元素;除该类未实现同步外,其余跟Hashtable大致相同;跟TreeMap不同,该容器不保证匀速顺序,根据需要该容器可能会对元素重新哈希,元素的顺序也会被重新打散,因此不同时间迭代同一个HashMap的顺序可能会不同。根据对冲突的处理方式不同,哈希表有两种实现方式,一种开放地址(open addressing),另一种是冲突链表方式(Separate chaining with linked lists)。Java 7 HashMap采用的是冲突链表方式。

Java 7 HashMap

image-20240830151817358

从上图容易看出,如果选择合适的哈希函数,put()get() 方法可以在常数实现内完成。但在对hashMap进行迭代时,需要遍历整个table以及后面跟的冲突链表。因此对于迭代比较频繁的场景,不宜将hashMap的初始大小设的过大。

有两个参数可以影响*HashMap*的性能: 初始容量(inital capacity)和负载系数(load factor)。初始容量指定了初始table的大小,负载系数用来指定自动扩容的临界值。当entry的数量超过capacity*load_factor时,容器将自动扩容并重新哈希。对于插入元素较多的场景,将初始容量设大可以减少重新哈希的次数。

将对象放入到HashMapHashSet中时,有两个方法需要特别关心: hashCode()equals()hashCode()方法决定了对象会被放到哪个bucket里,当多个对象的哈希值冲突时,equals()方法决定了这些对象是否是“同一个对象”。所以,如果要将自定义的对象放入到HashMapHashSet中,需要**@Override** hashCode()equals()方法。

方法剖析

  • get()

    get(Object key) 方法根据指定的Key 值返回对应的value,该方法调用了getEntry(Object key)得到相应的entry,然后返回entry.getValue()。因此getEntry()是算法的核心。算法思想是首先通过Hash()函数得到对应bucket的下标,然后依次遍历冲突链表。通过key.equals(k)方法来判断是否是要找的那个entryimage-20240830163927725

    上图中**hash(k)&(table.length-1)**等价与 hash(k)%table.length,原因是HashMap要求table.length必须是2的指数,因此table.length-1 就是二进制低位全是1,跟hash(k)相于会将哈希值的高位全抹掉,剩下的就是余数了。

    java
    //getEntry()方法
    final Entry<K,V> getEntry(Object key) {
    	......
    	int hash = (key == null) ? 0 : hash(key);
        for (Entry<K,V> e = table[hash&(table.length-1)];//得到冲突链表
             e != null; e = e.next) {//依次遍历冲突链表中的每个entry
            Object k;
            //依据equals()方法判断是否相等
            if (e.hash == hash &&
                ((k = e.key) == key || (key != null && key.equals(k))))
                return e;
        }
        return null;
    }
  • put()

    put(K key, V value)方法是将指定的key, value对添加到map里。该方法首先会对map做一次查找,看是否包含该元素,如果已经包含则直接返回,查找过程类似于getEntry()方法;如果没有找到,则会通过addEntry(int hash, K key, V value, int bucketIndex)方法插入新的entry,插入方式为头插法image-20240830164913247

    java
    //addEntry()
    void addEntry(int hash, K key, V value, int bucketIndex) {
        if ((size >= threshold) && (null != table[bucketIndex])) {
            resize(2 * table.length);//自动扩容,并重新哈希
            hash = (null != key) ? hash(key) : 0;
            bucketIndex = hash & (table.length-1);//hash%table.length
        }
        //在冲突链表头部插入新的entry
        Entry<K,V> e = table[bucketIndex];
        table[bucketIndex] = new Entry<>(hash, key, value, e);
        size++;
    }
  • remove()

    remove(Object key)的作用是删除key值对应的entry,该方法的具体逻辑是在removeEntryForKey(Object key)里实现的。removeEntryForKey()方法会首先找到key值对应的entry,然后删除该entry(修改链表的相应引用)。查找过程跟getEntry()过程类似。image-20240830165016576

    java
    //removeEntryForKey()
    final Entry<K,V> removeEntryForKey(Object key) {
    	......
    	int hash = (key == null) ? 0 : hash(key);
        int i = indexFor(hash, table.length);//hash&(table.length-1)
        Entry<K,V> prev = table[i];//得到冲突链表
        Entry<K,V> e = prev;
        while (e != null) {//遍历冲突链表
            Entry<K,V> next = e.next;
            Object k;
            if (e.hash == hash &&
                ((k = e.key) == key || (key != null && key.equals(k)))) {//找到要删除的entry
                modCount++; size--;
                if (prev == e) table[i] = next;//删除的是冲突链表的第一个entry
                else prev.next = next;
                return e;
            }
            prev = e; e = next;
        }
        return e;
    }

Java 8 HashMap

java8HashMap 进行了一下修改,最大的不同就是利用的红黑树,所以其有数组+链表+红黑树组成。

根据java7 HashMap 的介绍,我们知道,查找的时候,根据hash值我们能够快速定位到数组的具体下标,但是之后的话,需要顺着链表一个一个比较下去才能找到我们需要的,时间复杂度取决于链表的长度,为O(n)。为了降低这部分的开销,在java8 中,当链表的元素达到了8个时,会将链表转换为红黑树,在这些位置进行查找的时候可以降低的时间复杂度为O(logN)

image-20240830165528441

注意上面是示意图,主要描述结构,不会达到这个状态的,这么多数据的时候早就扩容了。

java 7 中使用Entry 来代表每个HashMap 中的数据节点,Java8 中使用Node,基本没有区别,都是keyvaluehashnext这四个属性,不过,Node 只能用于链表的情况,红黑树的情况需要使用TreeNode。我们根据数组元素中,第一个节点数据类型是Node还是TreeNode来判断该位置下是链表还是红黑树的。

Put过程分析

java
public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}

// 第四个参数 onlyIfAbsent 如果是 true,那么只有在不存在该 key 时才会进行 put 操作
// 第五个参数 evict 我们这里不关心
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict) {
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    // 第一次 put 值的时候,会触发下面的 resize(),类似 java7 的第一次 put 也要初始化数组长度
    // 第一次 resize 和后续的扩容有些不一样,因为这次是数组从 null 初始化到默认的 16 或自定义的初始容量
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    // 找到具体的数组下标,如果此位置没有值,那么直接初始化一下 Node 并放置在这个位置就可以了
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);

    else {// 数组该位置有数据
        Node<K,V> e; K k;
        // 首先,判断该位置的第一个数据和我们要插入的数据,key 是不是"相等",如果是,取出这个节点
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            e = p;
        // 如果该节点是代表红黑树的节点,调用红黑树的插值方法,本文不展开说红黑树
        else if (p instanceof TreeNode)
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        else {
            // 到这里,说明数组该位置上是一个链表
            for (int binCount = 0; ; ++binCount) {
                // 插入到链表的最后面(Java7 是插入到链表的最前面)
                if ((e = p.next) == null) {
                    p.next = newNode(hash, key, value, null);
                    // TREEIFY_THRESHOLD 为 8,所以,如果新插入的值是链表中的第 8 个
                    // 会触发下面的 treeifyBin,也就是将链表转换为红黑树
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        treeifyBin(tab, hash);
                    break;
                }
                // 如果在该链表中找到了"相等"的 key(== 或 equals)
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    // 此时 break,那么 e 为链表中[与要插入的新值的 key "相等"]的 node
                    break;
                p = e;
            }
        }
        // e!=null 说明存在旧值的key与要插入的key"相等"
        // 对于我们分析的put操作,下面这个 if 其实就是进行 "值覆盖",然后返回旧值
        if (e != null) {
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            afterNodeAccess(e);
            return oldValue;
        }
    }
    ++modCount;
    // 如果 HashMap 由于新插入这个值导致 size 已经超过了阈值,需要进行扩容
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict);
    return null;
}

和java7 稍微有点不一样的地方就是,java7是先扩容后插入新值的,Java8先插入值再扩容,不过这个不重要。

数组扩容

resize() 方法用于初始化数组或数组扩容,每次扩容后,容量为原来的2倍,并进行数据迁移。

java
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) // 对应使用 new HashMap(int initialCapacity) 初始化后,第一次 put 的时候
        newCap = oldThr;
    else {// 对应使用 new HashMap() 初始化后,第一次 put 的时候
        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;

    // 用新的数组大小初始化新的数组
    Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
    table = newTab; // 如果是初始化数组,到这里就结束了,返回 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 { 
                    // 这块是处理链表的情况,
                    // 需要将此链表拆成两个链表,放到新的数组中,并且保留原来的先后顺序
                    // loHead、loTail 对应一条链表,hiHead、hiTail 对应另一条链表,代码还是比较简单的
                    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;
                        // 第二条链表的新的位置是 j + oldCap,这个很好理解
                        newTab[j + oldCap] = hiHead;
                    }
                }
            }
        }
    }
    return newTab;
}

get过程分析

相对于put 来说,get相对更加简单。

  • 计算keyHash值,根据hash 值找到对应数组下标:hash&(length -1)

  • 判断数组该位置处的的元素是否刚好就是我们要找的,如果不是,走第三步

  • 判断该元素类型是否是TreeNode,如果是,用红黑树的方法取数据,如果不是,走第四步

  • 遍历链表,知道找到相等(==或equals)的key

    java
    public V get(Object key) {
        Node<K,V> e;
        return (e = getNode(hash(key), key)) == null ? null : e.value;
    }
    final Node<K,V> getNode(int hash, Object key) {
        Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (first = tab[(n - 1) & hash]) != null) {
            // 判断第一个节点是不是就是需要的
            if (first.hash == hash && // always check first node
                ((k = first.key) == key || (key != null && key.equals(k))))
                return first;
            if ((e = first.next) != null) {
                // 判断是否是红黑树
                if (first instanceof TreeNode)
                    return ((TreeNode<K,V>)first).getTreeNode(hash, key);
    
                // 链表遍历
                do {
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        return e;
                } while ((e = e.next) != null);
            }
        }
        return null;
    }

HashTable

和HashMap类似,但它是线程安全的,这意味着同一时刻多个线程可以同时写入HashTable并且不会导致数据不一致。它是遗留类,不应该去使用它。现在可以使用ConcurrentHashMap来支持线程安全,并且ConcurrentHashMap的效率会更高,因为ConcurrentHashMap引入了分段锁

ConcurrentHashMap

LinkedHashMap

使用双向链表来维护元素的顺序,顺序为插入顺序或最近最少使用(LRU)顺序

LinkedHashMap实现Map接口,即允许放入KeyNull 的元素,也允许插入 valuenull 的元素。从名字上可以看出该容器是linkedListHashMap的混合体,也就是说它同时满足HashMapLinkedList的某些特性。可将LinkedHashMap看着采用LinkedList增强的HashMap

image-20240830180238899

事实上LinkedHashMapHashMap的直接子类,二者唯一的区别是LinkedHashMapHashMap的基础上,采用双向链表(doubly-linked list)的形式将所有entry连接起来,这样是为保证元素的迭代顺序相同,上图给出了LinkedHashMap的结构图,主题部分跟HashMap完全一样,多了Header指向双向链表的头部(是一个哑元),该双向链表的迭代顺序就是entry的插入顺序。

除了可以保迭代顺序,这种结构还有一个好处:迭代LinkedHashMap时不需要像HashMap那样遍历整个table,而只需要直接遍历Header指向的双向链表即可,也就是说LinkedHashMap的迭代时间就只跟entry的个数相关,而跟table的大小无关。

有两个参数可以影响LinkedHashMap的性能:初始容量(inital capacity)和负载系数(load factor)。初始容量制定了table的大小,负载系统用来指定自动扩容的临界值,当entry的数量超过capacity*load_factor时,容器将自动扩容并重新哈希。对于插入元素较多的场景,将初始容量设大可以减少重新哈希的次数。

将对象放入到LinkedHashMaplinkHashSet中时,有两个方法需要特别关心:hashCode()equals()hashCode()方法决定了对象会被放到哪个bucket里,当多个对象的哈希值冲突时,equals方法决定了这些对象是否是"同一个对象"。所以,如果要将自定义的对象放入到LinkedHashMapHashSet中,需要@Override hashCode()equals()方法。

通过如下方式可以得到一个跟源Map迭代顺序一样的LinkedHashMap:

java
void foo(Map m) {
    Map copy = new LinkedHashMap(m);
    ...
}

出于性能原因,LinkedHashMap是非同步的(not Synchironized),如果需要在多环境环境使用,需要程序员手动同步;或者通过如下方式将LinkedHashMap包装成(wrapped)同步的:Map m = Collections.synchronizedMap(new LinkedHashMap(...));

方法剖析

  • get()

    get(Object key) 方法根据指定的key值返回对应的value。该方法跟HashMap.get() 方法的流程几乎完全一样,具体实现可以参考前文。

  • put()

    put(K key, V value)方法是将指定的key, value 对添加到map里,该方法首先会对Map做一个查找,看是否包含该元组,如果已经包含则直接返回,查找过程类似与get() 方法;如果没有找到,则会通过addEntry(int hash,K key,V value,int bucketIndex) 方法插入新的entry。

    注意,这里插入有两重含义:

    1. table的角度看,新的entry需要插入到对应的bucket里,当有哈希冲突时,采用头插法将新的entry插入到冲突链表的头部。
    2. header的角度看,新的entry需要插入到双向链表的尾部。

    image-20240902104516562

    addEntry() 代码如下:

    java
    // LinkedHashMap.addEntry()
    void addEntry(int hash, K key, V value, int bucketIndex) {
        if ((size >= threshold) && (null != table[bucketIndex])) {
            resize(2 * table.length);// 自动扩容,并重新哈希
            hash = (null != key) ? hash(key) : 0;
            bucketIndex = hash & (table.length-1);// hash%table.length
        }
        // 1.在冲突链表头部插入新的entry
        HashMap.Entry<K,V> old = table[bucketIndex];
        Entry<K,V> e = new Entry<>(hash, key, value, old);
        table[bucketIndex] = e;
        // 2.在双向链表的尾部插入新的entry
        e.addBefore(header);
        size++;
    }

    上述代码中用到了addBefore() 方法将新entry e 插入到双向链表头引用header的前面,这样e 就成为双向链表中的最后一个元素。addBefore()的代码如下:

    java
    // LinkedHashMap.Entry.addBefor(),将this插入到existingEntry的前面
    private void addBefore(Entry<K,V> existingEntry) {
        after  = existingEntry;
        before = existingEntry.before;
        before.after = this;
        after.before = this;
    }

    上述代码只是简单修改entry的引用而已。

  • remove()

    remove(Object key)的作用是删除key值对应的entry,该方法的具体逻辑是在removeEntryForKey(Object Key)里实现的。removeEntryForKey()方法会首先找到key值对应的entry,然后删除该entry(修改链表的相应引用)。查找过程跟get()方法类似。

    注意,这里的删除也有两重含义:

    1. table 的角度看,需要将该entry从对应的bucket里删除,如果对应的冲突链表不空,需要修改冲突链表的相应引用。
    2. header的角度来看,需要将该entry从双向链表中删除,同时修改链表中前面以及后面元素的相应引用。

    image-20240902154436578

    removeEntryForKey() 对应的代码如下:

    java
    // LinkedHashMap.removeEntryForKey(),删除key值对应的entry
    final Entry<K,V> removeEntryForKey(Object key) {
    	......
    	int hash = (key == null) ? 0 : hash(key);
        int i = indexFor(hash, table.length);// hash&(table.length-1)
        Entry<K,V> prev = table[i];// 得到冲突链表
        Entry<K,V> e = prev;
        while (e != null) {// 遍历冲突链表
            Entry<K,V> next = e.next;
            Object k;
            if (e.hash == hash &&
                ((k = e.key) == key || (key != null && key.equals(k)))) {// 找到要删除的entry
                modCount++; size--;
                // 1. 将e从对应bucket的冲突链表中删除
                if (prev == e) table[i] = next;
                else prev.next = next;
                // 2. 将e从双向链表中删除
                e.before.after = e.after;
                e.after.before = e.before;
                return e;
            }
            prev = e; e = next;
        }
        return e;
    }
  • LinkedhashMap经典用法

    LinkedHashMap除了可以保证迭代顺序外,还有一个非常有用的用法:可以轻松实现一个采用FIFO替换策略的缓存。具体来说,LinkedHashMap 有一个子类方法protected boolean removeEldestEntry(Map.Entry<K,V> eldest),该方法的作用是告诉Map是否要删除“最老”的Entry,所谓最老就是当前Map中最早插入的Entry,如果该方法返回true,最老的那个元素就会被删除。在每次插入新元素的之后LinkedHashMap会自动询问removeEldestEntry()是否要删除最老的元素。这样只需要在子类中重载该方法,当元素个数超过一定数量时让removeEldestEntry()返回true,就能够实现一个固定大小的FIFO策略的缓存。示例代码如下:

    java
    /** 一个固定大小的FIFO替换策略的缓存 */
    class FIFOCache<K, V> extends LinkedHashMap<K, V>{
        private final int cacheSize;
        public FIFOCache(int cacheSize){
            this.cacheSize = cacheSize;
        }
    
        // 当Entry个数超过cacheSize时,删除最老的Entry
        @Override
        protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
           return size() > cacheSize;
        }
    }

WeakHashMap

WeakHashMap,从名字可以看出它是某种 Map。它的特殊之处在于 WeakHashMap 里的entry可能会被GC自动删除,即使程序员没有调用remove()或者clear()方法。

更直观的说,当使用 WeakHashMap 时,即使没有显示的添加或删除任何元素,也可能发生如下情况:

  1. 调用两次size()方法返回不同的值;
  2. 两次调用isEmpty()方法,第一次返回false,第二次返回true
  3. 两次调用containsKey()方法,第一次返回true,第二次返回false,尽管两次使用的是同一个key
  4. 两次调用get()方法,第一次返回一个value,第二次返回null,尽管两次使用的是同一个对象。

遇到这么奇葩的现象,你是不是觉得使用者一定会疯掉? 其实不然,WeakHashMap 的这个特点特别适用于需要缓存的场景。在缓存场景下,由于内存是有限的,不能缓存所有对象;对象缓存命中可以提高系统效率,但缓存MISS也不会造成错误,因为可以通过计算重新得到。

要明白 WeakHashMap 的工作原理,还需要引入一个概念 : 弱引用(WeakReference)。我们都知道Java中内存是通过GC自动管理的,GC会在程序运行过程中自动判断哪些对象是可以被回收的,并在合适的时机进行内存释放。GC判断某个对象是否可被回收的依据是,是否有有效的引用指向该对象。如果没有有效引用指向该对象(基本意味着不存在访问该对象的方式),那么该对象就是可回收的。这里的有效引用 并不包括弱引用。也就是说,虽然弱引用可以用来访问对象,但进行垃圾回收时弱引用并不会被考虑在内,仅有弱引用指向的对象仍然会被GC回收

WeakHashMap 内部是通过弱引用来管理entry的,弱引用的特性对应到 WeakHashMap 上意味着什么呢?将一对key, value放入到 WeakHashMap 里并不能避免该key值被GC回收,除非在 WeakHashMap 之外还有对该key的强引用

关于强引用,弱引用等概念以后再具体讲解,这里只需要知道Java中引用也是分种类的,并且不同种类的引用对GC的影响不同就够了。

WeakHashMap 的存储结构类似与HashMap,这里不在赘述。关于强弱引用的管理方式,将在后续文章中单独讲解。

如果你看过前几篇关于 MapSet 的讲解,一定会问: 既然有 WeakHashMap,是否有 WeekHashSet 呢? 答案是没有:( 。不过Java Collections工具类给出了解决方案,Collections.newSetFromMap(Map<E,Boolean> map)方法可以将任何 Map包装成一个Set。通过如下方式可以快速得到一个 Weak HashSet

java
// 将WeakHashMap包装成一个Set
Set<Object> weakHashSet = Collections.newSetFromMap(
        new WeakHashMap<Object, Boolean>());

不出你所料,newSetFromMap()方法只是对传入的Map做了简单包装:

java
// Collections.newSetFromMap()用于将任何Map包装成一个Set
public static <E> Set<E> newSetFromMap(Map<E, Boolean> map) {
    return new SetFromMap<>(map);
}

private static class SetFromMap<E> extends AbstractSet<E>
    implements Set<E>, Serializable
{
    private final Map<E, Boolean> m;  // The backing map
    private transient Set<E> s;       // Its keySet
    SetFromMap(Map<E, Boolean> map) {
        if (!map.isEmpty())
            throw new IllegalArgumentException("Map is non-empty");
        m = map;
        s = map.keySet();
    }
    public void clear()               {        m.clear(); }
    public int size()                 { return m.size(); }
    public boolean isEmpty()          { return m.isEmpty(); }
    public boolean contains(Object o) { return m.containsKey(o); }
    public boolean remove(Object o)   { return m.remove(o) != null; }
    public boolean add(E e) { return m.put(e, Boolean.TRUE) == null; }
    public Iterator<E> iterator()     { return s.iterator(); }
    public Object[] toArray()         { return s.toArray(); }
    public <T> T[] toArray(T[] a)     { return s.toArray(a); }
    public String toString()          { return s.toString(); }
    public int hashCode()             { return s.hashCode(); }
    public boolean equals(Object o)   { return o == this || s.equals(o); }
    public boolean containsAll(Collection<?> c) {return s.containsAll(c);}
    public boolean removeAll(Collection<?> c)   {return s.removeAll(c);}
    public boolean retainAll(Collection<?> c)   {return s.retainAll(c);}
    // addAll is the only inherited implementation
    ......
}
本站访客数 人次 本站总访问量