数据结构系列之集合

 集合是一种应用广泛的数据集合容器。是一种可以自动增长的添加重复元素的顺序存储的线性结构类型。先看一下List继承接口Conllection关系结构关系图:

 Conllection接口是list 的最顶层接口,list 本身也是一个接口。

起源

除了 Vector ,Stack 是1.0版本,Queue是1.5版本外,其余均是在1.2版本添加的。

使用list 时 通常会联想到另外的一种数据结构,那就是数组array。它们使用起来真的相似,而且区别仅是使用数组有一个容量限制,而集合容量却没有限制。实际上list的子类 Arraylist,Vector 的内部实现就是使用的数组。LinkedList的内部使用的双向链表。

先看ArrayList,这是使用最频繁的List子类了。前面说了它的内部实现是数组,但是在使用的时候却不用担心容量大小的问题,那么它内部肯定存在某种扩容机制了。看list的源码就可以得到答案。

首先看的是它的添加元素的方法:

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

 ensureCapacityInternal方法里面就是处理了数组容量的问题,它最终会到这里:

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

 传入的参数就是前面的 size+1 了 ,不管当前的容量是否足够,先扩大为原容量的1.5倍再说。简单粗暴哦~。后面再根据新容量与原容量来决定是否使用新的容量大小。最后复制这个数组。完成数组容量处理,添加元素,方法结束。还有就是list的默认容量大小是10。

/**
 * Default initial capacity.
 */
private static final int DEFAULT_CAPACITY = 10;

接下来看下list 的删除方法,list是一个接口,里边的方法都由它的子类取实现,这看ArrayList的remove 方法。arraylist 的remove实现很普通,但是有一点要注意的:

扫描二维码关注公众号,回复: 9610672 查看本文章
 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
    }

 可以看到,当删除一个元素后,整个list的size 会减少1的。如果不注意这点的话,处理这样一个需求:需要删除某个集合内的某个元素,而恰好这个元素又是连续的重复元素,而你做法是循环遍历,判断值是否相等,然后删除。到最后就会发现没有完全删除掉。根本原因就是这个,你在List里删除了一个元素后,整个List会往前推1位。而遍历却是往后走的,所以造成了删除不完全的情况。

LinkedList 

前面有提到过LinkedList的内部使用了双向链表,而且链表之前有提到过大概。看下LinsedList内部的链表节点类:

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

每个节点都拥有数据域,前驱指针域,后继指针域。接下看它的添加的方法。

/**
 * 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++;
}

LinkedList 的添加方法最终是调用linkLast 方法,从名字就可以得知,这就是链表里的尾插法。直接在末尾处添加元素,然后长度加1。

了解链表结构的话看linkedlist的增加,删除方法逻辑都不会太难理解,这里不一一介绍。看linkedlist的查找元素方法 get。

直接点get 方法:

 public E get(int index) {
        checkElementIndex(index);  //------> 检测元素是否越界。
        return node(index).item;
    }

接着进入到node(index)方法里边 ,

 /**
     * 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;
        }
    }

emmmm 这个方法也是比较好理解的。先判断了当前要获取的元素的位置在整条链表的什么位置,是靠近头部,还是靠近尾部。以此决定是从头部遍历查找还是尾部查找。最后就是遍历寻址,返回对应index的那个节点出去了。

vector

vector跟arraylist一样,内部使用数组结构实现,甚至用法上都没有区别。特点就是vector是线程安全的,而arraylist是线程不安全。进入到vector的源码会发现很多方法都使用  synchronized 关键字来修饰。比如,添加方法:

 public synchronized boolean add(E e) {
        modCount++;
        ensureCapacityHelper(elementCount + 1);
        elementData[elementCount++] = e;
        return true;
    }

可以看到跟arraylist 的添加方法几乎是一样的只不过添加了synchronized修饰。还有vector的扩容手段跟arraylist也是不同的。vector有一个专门用于扩容的属性: capacityIncrement

 /**
     * The amount by which the capacity of the vector is automatically
     * incremented when its size becomes greater than its capacity.  If
     * the capacity increment is less than or equal to zero, the capacity
     * of the vector is doubled each time it needs to grow.
     *
     * @serial
     */
    protected int capacityIncrement;

什么意思呢?哈哈英文不是很好的去翻译翻译喽。大概意思就是这个int属性表示当vector添加元素后的容量大于它自己当前的容量时自动增长的量。如果它小于或者等于0时,vector增长的量是原容量的2倍。由代码也可以看出来:

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 的初始容量也是10。虽说 capacityIncrement 加入了扩容的计算。但实际上capacityIncrement的值在初始化为0后就再也没有被赋值过了。因此vector都是以2倍的大小扩容。

总结

AraayList

数组实现,线程不安全,增删慢,查找快。单线程下查找操作频繁推荐使用

LinkedList

链表实现,线程不安全,增删快速,查找慢。大量插入,删除操作下推荐使用;

Vector

数组实现,线程不安全,增删慢,查找快。不涉及到多线程情况下,能用Vector的地方就能用ArrayList代替;多线程情况下查找操作频繁推荐使用vector。

发布了46 篇原创文章 · 获赞 15 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/FooTyzZ/article/details/89361649