zoukankan      html  css  js  c++  java
  • 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)

  • 相关阅读:
    程序员 你中毒了吗?
    Win8 下安装 Live Writer 发布博客
    Rational Rose 2003 下载及破解方法(转载)
    如何在dos 下使用csc.exe命令?
    as 与 is
    【转载】关于工资的三个秘密
    C#反射(1)<转>
    C#常用字符串格式
    微软企业库EntLib5.0使用过程中常见的异常
    关于window7 AERO 声音 IIS 无线网络失效的解决办法
  • 原文地址:https://www.cnblogs.com/Robin132929/p/13700289.html
Copyright © 2011-2022 走看看