Java集合-Collection

Java集合-Collection

一、Collection继承关系

Collection继承关系

图片来源

由上图可知Collection有三个子类,分别是Set、List、Queue。
特点:
Set:无序且值唯一
List:有序、值可重复
Queue:先进先出的线性表

二、Collection提供的方法

Collection提供的方法

Collection提供了对集合的通用操作

三、Collection子类

1、Set

无序且值唯一。

Set子类有:

HashSet

底层数据结构是哈希表(实际是hashMap),从构造函数可以看出在创建实例时会创建一个HashMap,该HashMap就是用来实际存储元素的,除此之外在创建HashSet实例时我们可以指定其内部HashMap的容量和加载因子(默认大小为16,加载因子为0.75)

    public HashSet() {
        map = new HashMap<>();
    }

再来看下增删查数据是如何实现的:

  • add操作
    public boolean add(E e) {
       //add是调用HashMap的put操作
        return map.put(e, PRESENT)==null;
    }
  • remove操作
    public boolean remove(Object o) {
        return map.remove(o)==PRESENT;
    }
  • contains操作
public boolean contains(Object o) {
    return map.containsKey(o);
}

HashSet如何来保证元素唯一性? 1.依赖两个方法:hashCode()和equals()。

可选链接

TreeSet

TreeSet是一个非同步的非线程安全的二叉树,底层数据结构是红黑树。(唯一,排序),其add , remove和contains操作的时间复杂度为log(n)

来看下默认构造函数:

    public TreeSet() {
        this(new TreeMap<E,Object>());
    }
    
    private transient NavigableMap<E,Object> m;
    private static final Object PRESENT = new Object();
    TreeSet(NavigableMap<E,Object> m) {
        this.m = m;
    }

可以看出其内部默认是使用TreeMap存储元素的,因为其内部元素是有序的,对于元素的排序有两种方式自然排序和比较器排序,自然排序就是当comparator为空的时候,构建无参构造函数的时候默认的一种排序方式,比较器排序就是在构造函数中传入comparator从而指定排序方式。

        treeSet = new TreeSet<>(new Comparator<String>() {
            @Override
            public int compare(String o1, String o2) {
                return o1.length()-o2.length();
            }
        });

TreeSet保证元素唯一性的是通过比较的返回值是否是0来决定

可选阅读链接

LinkedHashSet

Set接口的哈希表和链接列表实现即保证插入顺序,(FIFO插入有序,唯一)由链表保证元素有序由哈希表保证元素唯一。linkedHashSet是一个非线程安全的集合。如果有多个线程同时访问当前linkedhashset集合容器,并且有一个线程对当前容器中的元素做了修改,那么必须要在外部实现同步

来看下其构造函数

    public LinkedHashSet() {
        super(16, .75f, true);
    }
    
     HashSet(int initialCapacity, float loadFactor, boolean dummy) {
        map = new LinkedHashMap<>(initialCapacity, loadFactor);
    }

LinkedHashSet父类为HashSet,然后在HashSet的构造函数中创建了LinkedHashMap实例,也就是说LinkedHashSet最终是使用LinkedHashMap来存储元素。

Set小结

我们简绍了三种Set在实际使用时可以根据需求选择合适的,同时我们也看到这三种Set的实现最终都是通过Map来存储元素的。

2、List

List链表是一种线性结构,其内部元素有序(插入有序)、不唯一,可以根据索引来查找获取数据。

ArrayList

底层通过数组实现,查找快增删慢,线程不安全。来看下默认构造函数

    transient Object[] elementData;
    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
    public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }

可以看到存储元素的是一个叫做elementData的数组。

  • add操作
    public boolean add(E e) {
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        elementData[size++] = e;
        return true;
    }

我们看到在增加元素前会先调用 ensureCapacityInternal来确保数组elementData有足够的空间,如果空间不足会进行扩容操作。

    private void ensureCapacityInternal(int minCapacity) {
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
        }
        //判断是否需要扩容
        ensureExplicitCapacity(minCapacity);
    }
    
        private void ensureExplicitCapacity(int minCapacity) {
        modCount++;

        // 需要扩容
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }
    
        private void grow(int minCapacity) {
        // 当前数组大小
        int oldCapacity = elementData.length;
        //扩容为原来的1.5倍
        int newCapacity = oldCapacity + (oldCapacity >> 1);     //扩容后还不满足所需最小容量则把容量设置为所需最小容量
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
            //MAX_ARRAY_SIZE的值为Integer.MAX_VALUE - 8表示最大可设置的值
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);
        // 真正扩容操作是通过Arrays.copyOf来完成的
        elementData = Arrays.copyOf(elementData, newCapacity);
    }

    private static int hugeCapacity(int minCapacity) {
        if (minCapacity < 0) // 溢出
            throw new OutOfMemoryError();
            //所需最小容量大于MAX_ARRAY_SIZE则扩容为Integer.MAX_VALUE
        return (minCapacity > MAX_ARRAY_SIZE) ?
            Integer.MAX_VALUE :
            MAX_ARRAY_SIZE;
    }

再来看下在指定位置插入元素的操作

    public void add(int index, E element) {
        if (index > size || index < 0)
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
        //检查是否需要扩容
        ensureCapacityInternal(size + 1);  
        //把插入位置后面所有元素后移一位
        System.arraycopy(elementData, index, elementData, index + 1,
                         size - index);
         //插入元素                
        elementData[index] = element;
        size++;
    }
  • remove操作
    public boolean remove(Object o) {
        if (o == null) {
            for (int index = 0; index < size; index++)
                if (elementData[index] == null) {
                    fastRemove(index);
                    return true;
                }
        } else {
            for (int index = 0; index < size; index++)
                if (o.equals(elementData[index])) {
                    fastRemove(index);
                    return true;
                }
        }
        return false;
    }

可以看到remove中是根据equals来判断元素是否是要删除的,具体移除操作是通过fastRemove来完成。

    private void fastRemove(int index) {
        modCount++;
        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
    }

总体来说ArrayList底层采用数组存储元素在元素增删时通过copy数组来实现元素移动,其增删操作的时间复杂度为O(n)。

可选阅读链接

Vector

底层数组实现,查找快增删慢,线程安全。构造函数

    public Vector() {
        this(10);
    }
    public Vector(int initialCapacity) {
        this(initialCapacity, 0);
    }
    //这里的capacityIncrement是指扩容时增加的容量
    public Vector(int initialCapacity, int capacityIncrement) {
        super();
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal Capacity: "+
                                               initialCapacity);
        this.elementData = new Object[initialCapacity];
        this.capacityIncrement = capacityIncrement;
    }

因为Vector底层也是数组实现,所以在增删数据时会涉及到数组容量的变化,这跟ArrayList类似下面是Vector扩容的核心内容,可以看出其在容量不足时会增加capacityIncrement的容量,如果capacityIncrement<0则直接增加一倍的容量。

    private void grow(int minCapacity) {
        // overflow-conscious code
        int oldCapacity = elementData.length;
        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);
    }

Vector实现跟ArrayList类似最大的不同在于Vector是线程安全的。

可选阅读链接

stack

先进后出的结构,stack中peek函数是查看栈顶元素但并不移除,pop是弹出栈顶元素。

其构造函数是空实现

    public Stack() {
    }
  • push操作
    public E push(E item) {
        addElement(item);
        return item;
    }
    
        public synchronized void addElement(E obj) {
        modCount++;
        //检查是否需要扩容
        ensureCapacityHelper(elementCount + 1);
        //存入数据
        elementData[elementCount++] = obj;
    }
  • pop操作
    public synchronized E pop() {
        E       obj;
        int     len = size();
        obj = peek();
        removeElementAt(len - 1);
        return obj;
    }
        public synchronized void removeElementAt(int index) {
        modCount++;
        if (index >= elementCount) {
            throw new ArrayIndexOutOfBoundsException(index + " >= " +
                                                     elementCount);
        }
        else if (index < 0) {
            throw new ArrayIndexOutOfBoundsException(index);
        }
        int j = elementCount - index - 1;
        if (j > 0) {
           //移动数据
            System.arraycopy(elementData, index + 1, elementData, index, j);
        }
        elementCount--;
        //将删除位置置空
        elementData[elementCount] = null; /* to let gc do its work */
    }
  • peek操作
    public synchronized E peek() {
        int     len = size();
        if (len == 0)
            throw new EmptyStackException();
        return elementAt(len - 1);
    }
    
    public synchronized E elementAt(int index) {
        if (index >= elementCount) {
            throw new ArrayIndexOutOfBoundsException(index + " >= " + elementCount);
        }
        return elementData(index);
    }
    
        E elementData(int index) {
        return (E) elementData[index];
    }

LinkedList

底层双链表实现,查找慢增删快,线程不安全,LinkedList同时实现了List, Deque两个接口也就是说它既可以作为list也可作为deque使用。

既然是双链表则会有节点的概念,我们来看下它的Node,这是LinkedList的一个内部类。

       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;
        }
    }
  • add操作
    public boolean add(E e) {
        linkLast(e);
        return true;
    }
    //在表尾插入一个Node
    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++;
    }
  • add(index,obj)
    public void add(int index, E element) {
        //检查插入位置是否合法
        checkPositionIndex(index);

        if (index == size)
            linkLast(element);
        else
            linkBefore(element, node(index));
    }
    //插入链表
     void linkBefore(E e, Node<E> succ) {
        // assert succ != null;
        final Node<E> pred = succ.prev;
        final Node<E> newNode = new Node<>(pred, e, succ);
        succ.prev = newNode;
        if (pred == null)
            first = newNode;
        else
            pred.next = newNode;
        size++;
        modCount++;
    }
  • remove操作

        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;
        }
        
         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;
            size--;
            modCount++;
            return element;
        }
    

    可以看出LinkedList的数据操作大多都是链表的操作所以其特点是增删快查找慢,在类内部LinkedList维护了first和last两个指针,这也是其能实现deque功能的基础。在作为deque时offer表示在队尾入队一个元素,poll是出队队首一个元素,peek是查看队首元素但并不出队。在作为deque时无法调用list相关接口方法。

3、Queue

队列是一种先进先出的线性结构,不支持随机访问数据。

PriorityQueue

优先队列是基于堆实现的,对内元素是有序的,offer,poll,remove和add等方法提供了O(log(n))的时间复杂度 ,而remove(obj)和contains方法的时间复杂度是O(n),peek时间复杂度为O(1)。排序是通过自然排序和比较器排序实现的,采用哪种排序是通过构造函数确定的,其中自然排序要求元素实现compare函数,比较排序则需要在构造函数中指明排序规则。

默认构造函数

private static final int DEFAULT_INITIAL_CAPACITY = 11;

public PriorityQueue() {
    this(DEFAULT_INITIAL_CAPACITY, null);
}

 public PriorityQueue(int initialCapacity,
                         Comparator<? super E> comparator) {
        //可以看出内部采用数组存储
        if (initialCapacity < 1)
            throw new IllegalArgumentException();
        this.queue = new Object[initialCapacity];
        this.comparator = comparator;
 }
  • offer
    public boolean offer(E e) {
        if (e == 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;
    }
    
    private void siftUp(int k, E x) {
        if (comparator != null)
            siftUpUsingComparator(k, x);
        else
            //这里以分析siftUpComparable为例
            siftUpComparable(k, x);
    }
    
    private void siftUpComparable(int k, E x) {
        Comparable<? super E> key = (Comparable<? super E>) x;
        while (k > 0) {
            //找k位置的父节点的index
            int parent = (k - 1) >>> 1;
            //k位置的父节点
            Object e = queue[parent];
            //调整堆,大于父节点的就不动,小于父节点的就上浮
            if (key.compareTo((E) e) >= 0)
                break;
            queue[k] = e;
            k = parent;
        }
        queue[k] = key;
    }
  • poll操作
    public E poll() {
        if (size == 0)
            return null;
        int s = --size;
        modCount++;
        E result = (E) queue[0];
        E x = (E) queue[s];
        queue[s] = null;
        if (s != 0)
            //调整堆
            siftDown(0, x);
        return result;
    }
    
        private void siftDown(int k, E x) {
        if (comparator != null)
            siftDownUsingComparator(k, x);
        else
            siftDownComparable(k, x);
    }
    
        private void siftDownComparable(int k, E x) {
        Comparable<? super E> key = (Comparable<? super E>)x;
        int half = size >>> 1;
        while (k < half) {
            int child = (k << 1) + 1; // assume left child is least
            Object c = queue[child];
            int right = child + 1;
            if (right < size &&
                ((Comparable<? super E>) c).compareTo((E) queue[right]) > 0)
                c = queue[child = right];
            if (key.compareTo((E) c) <= 0)
                break;
            queue[k] = c;
            k = child;
        }
        queue[k] = key;
    }

可选阅读链接

ArrayDeque

双端队列,底层数组实现。

默认构造函数

    public ArrayDeque() {
        //数组大小默认16
        elements = new Object[16];
    }

因为可以双端操作数据所以其内部采用head和tail来存储头尾元素的index这样就可以快锁找到头尾元素。ArrayDeque还规定elements的size必须是2的整数次幂,当我们设置容量大小不是2的整数次幂时会进行调整

    public ArrayDeque(int numElements) {
        allocateElements(numElements);
    }
    
        private void allocateElements(int numElements) {
        int initialCapacity = MIN_INITIAL_CAPACITY;
        // Find the best power of two to hold elements.
        // Tests "<=" because arrays aren't kept full.
        if (numElements >= initialCapacity) {
            initialCapacity = numElements;
            initialCapacity |= (initialCapacity >>>  1);
            initialCapacity |= (initialCapacity >>>  2);
            initialCapacity |= (initialCapacity >>>  4);
            initialCapacity |= (initialCapacity >>>  8);
            initialCapacity |= (initialCapacity >>> 16);
            initialCapacity++;

            if (initialCapacity < 0)    // Too many elements, must back off
                initialCapacity >>>= 1; // Good luck allocating 2^30 elements
        }
        elements = new Object[initialCapacity];
    }

 allocateElements实现思路如下:

 1.要明确2整数次幂使用二进制的表现形式如下:0...010...0,中间有一个1,其它的都是0。

  2.根据1的形式,计算使输入任意的X,等式成立的Y。X的二进制形式为????????,是一个未知数,这样如何求得Y呢?方法很简单,找到X最高位为1的位置:那么X就是0..001???,这种形式了。那么所求的Y就是0..010...0,其值就是比X最高位为1再高一位为1,其它位为0的值。

  3.X的最高为1的那一位是未知的,如何求更高一位为1的Y呢?直接求是没有办法的,但是可以通过将X最高位为1后面所有位都变成1,再加1进位的方式办到。就是0..001???变成0.001..1,使用这个+1就会变成所要的Y:0.010...0了。

  4.如何保证X最高位为1后面都是1呢?这个就是上面位运算所实现的内容了。假设X是0..01???,左移一位就是0.001??,做或运算就变成了0..011??,是不是很巧妙,出现了两位为1的就移动2位,获得四位为1的值,这样移动到16的时候就涵盖了32位整数的所有范围了。这个时候+1可能发生整数溢出,所以再左移一位保证在整数范围内。

参考

  • addFirst
    public void addFirst(E e) {
        if (e == null)
            throw new NullPointerException();
         //(head - 1) & (elements.length - 1)的作用是确定head的index
        elements[head = (head - 1) & (elements.length - 1)] = e;
        //首尾指向同一位置 扩容至原先两倍大小
        if (head == tail)
            doubleCapacity();
    }
    
       private void doubleCapacity() {
        assert head == tail;
        int p = head;
        int n = elements.length;
        int r = n - p; // number of elements to the right of p
        int newCapacity = n << 1;
        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 = a;
        head = 0;
        tail = n;
    }
  • pollFirst
    public E pollFirst() {
        final Object[] elements = this.elements;
        final int h = head;
        @SuppressWarnings("unchecked")
        E result = (E) elements[h];
        // Element is null if deque empty
        if (result != null) {
            elements[h] = null; // Must null out slot
            head = (h + 1) & (elements.length - 1);
        }
        return result;
    }

在addFirst中(head - 1) & (elements.length - 1)操作主要是确定入队的队首元素的位置,该操作相当于取模操作同时还很好的处理了head-1是-1的情况(head-1是-1时该操作的结果是elements.length - 1)

posted @ 2020-09-20 14:42  Robin132929  阅读(342)  评论(0编辑  收藏  举报