Java集合详解1:ArrayList,Vector与Stack

本文很是详尽地介绍了Java中的三个集合类
ArrayList,Vector与Stackhtml

”Java集合详解系列“是我在完成Java基础篇的系列博客后准备开始写的新系列。java

Java集合系列专栏地址:https://blog.csdn.net/column/...git

以前的Java基础系列博客首发于个人我的博客:https://h2pl.github.io/程序员

在这个分类中,将会写写Java中的集合。集合是Java中很是重要并且基础的内容,由于任何数据必不可少的就是该数据是如何存储的,集合的做用就是以必定的方式组织、存储数据。github

之因此把这三个集合类放在一块儿讲解,是由于这三个集合类的底层都是数组实现(Stack继承自vector)而且比较经常使用。
后面还会另外讲底层是链表实现的linkedlist和queue;数组

今天咱们来探索一下ArrayList和Vector,以及Stack的源码安全

具体代码在个人GitHub中能够找到微信

https://github.com/h2pl/MyTech网络

喜欢的话麻烦star一下哈数据结构

ArrayList

ArrayList概述

ArrayList是实现List接口的动态数组,所谓动态就是它的大小是可变的。实现了全部可选列表操做,并容许包括 null 在内的全部元素。除了实现 List 接口外,此类还提供一些方法来操做内部用来存储列表的数组的大小。

每一个ArrayList实例都有一个容量,该容量是指用来存储列表元素的数组的大小。默认初始容量为10。随着ArrayList中元素的增长,它的容量也会不断的自动增加。

在每次添加新的元素时,ArrayList都会检查是否须要进行扩容操做,扩容操做带来数据向新数组的从新拷贝,因此若是咱们知道具体业务数据量,在构造ArrayList时能够给ArrayList指定一个初始容量,这样就会减小扩容时数据的拷贝问题。固然在添加大量元素前,应用程序也可使用ensureCapacity操做来增长ArrayList实例的容量,这能够减小递增式再分配的数量。

注意,ArrayList实现不是同步的。若是多个线程同时访问一个ArrayList实例,而其中至少一个线程从结构上修改了列表,那么它必须保持外部同步。因此为了保证同步,最好的办法是在建立时完成,以防止意外对列表进行不一样步的访问:

List list = Collections.synchronizedList(new ArrayList(...));

## 底层数据结构

ArrayList的底层是一个object数组,而且由trasient修饰。

//transient Object[] elementData; //

non-private to simplify nested class access
//ArrayList底层数组不会参与序列化,而是使用另外的序列化方式。

//使用writeobject方法进行序列化,具体为何这么作欢迎查看我以前的关于序列化的文章

//总结一下就是只复制数组中有值的位置,其余未赋值的位置不进行序列化,能够节省空间。

//        private void writeObject(java.io.ObjectOutputStream s)
//        throws java.io.IOException{
//            // Write out element count, and any hidden stuff
//            int expectedModCount = modCount;
//            s.defaultWriteObject();
//
//            // Write out size as capacity for behavioural compatibility with clone()
//            s.writeInt(size);
//
//            // Write out all elements in the proper order.
//            for (int i=0; i<size; i++) {
//                s.writeObject(elementData[i]);
//            }
//
//            if (modCount != expectedModCount) {
//                throw new ConcurrentModificationException();
//            }
//        }

增删改查

//增删改查

添加元素时,首先判断索引是否合法,而后检测是否须要扩容,最后使用System.arraycopy方法来完成数组的复制。

这个方法无非就是使用System.arraycopy()方法将C集合(先准换为数组)里面的数据复制到elementData数组中。这里就稍微介绍下System.arraycopy(),由于下面还将大量用到该方法

。该方法的原型为:

public static void arraycopy(Object src, int srcPos, Object dest, int destPos, int length)。

它的根本目的就是进行数组元素的复制。即从指定源数组中复制一个数组,复制从指定的位置开始,到目标数组的指定位置结束。

将源数组src从srcPos位置开始复制到dest数组中,复制长度为length,数据从dest的destPos位置开始粘贴。

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

删除元素时,一样判断索引是否和法,删除的方式是把被删除元素右边的元素左移,方法一样是使用System.arraycopy进行拷贝。

//        public E remove(int index) {
//            rangeCheck(index);
//
//            modCount++;
//            E oldValue = elementData(index);
//
//            int numMoved = size - index - 1;
//            if (numMoved > 0)
//                System.arraycopy(elementData, index+1, elementData, index,
//                        numMoved);
//            elementData[--size] = null; // clear to let GC do its work
//
//            return oldValue;
//        }

ArrayList提供一个清空数组的办法,方法是将全部元素置为null,这样就可让GC自动回收掉没有被引用的元素了。

//
//        /**
//         * Removes all of the elements from this list.  The list will
//         * be empty after this call returns.
//         */
//        public void clear() {
//            modCount++;
//
//            // clear to let GC do its work
//            for (int i = 0; i < size; i++)
//                elementData[i] = null;
//
//            size = 0;
//        }

修改元素时,只须要检查下标便可进行修改操做。

//        public E set(int index, E element) {
//            rangeCheck(index);
//
//            E oldValue = elementData(index);
//            elementData[index] = element;
//            return oldValue;
//        }
//
//        public E get(int index) {
//            rangeCheck(index);
//
//            return elementData(index);
//        }
//

上述方法都使用了rangeCheck方法,其实就是简单地检查下标而已。

//        private void rangeCheck(int index) {
//            if (index >= size)
//                throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
//        }

modCount

//        protected transient int modCount = 0;

由以上代码能够看出,在一个迭代器初始的时候会赋予它调用这个迭代器的对象的mCount,如何在迭代器遍历的过程当中,一旦发现这个对象的mcount和迭代器中存储的mcount不同那就抛异常

好的,下面是这个的完整解释
Fail-Fast 机制
咱们知道 java.util.ArrayList 不是线程安全的,ArrayList,那么将抛出ConcurrentModificationException,这就是所谓fail-fast策略。

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

在迭代过程当中,判断 modCount 跟 expectedModCount 是否相等,若是不相等就表示已经有其余线程修改了 ArrayList。

因此在这里和你们建议,当你们遍历那些非线程安全的数据结构时,尽可能使用迭代器

初始容量和扩容方式

初始容量是10,下面是扩容方法。
首先先取

//        private static final int DEFAULT_CAPACITY = 10;

扩容发生在add元素时,传入当前元素容量加一
   public boolean add(E e) {
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    elementData[size++] = e;
    return true;
}


这里给出初始化时的数组
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

这说明:若是数组仍是初始数组,那么最小的扩容大小就是size+1和初始容量中较大的一个,初始容量为10。
由于addall方法也会调用该函数,因此此时须要作判断。
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
        若是此时扩容容量大于数组长度吗,执行grow,不然不执行。
    if (minCapacity - elementData.length > 0)
        grow(minCapacity);
}

真正执行扩容的方法grow

扩容方式是让新容量等于旧容量的1.5被。

当新容量大于最大数组容量时,执行大数扩容

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

在这里有一个疑问,为何每次扩容处理会是1.5倍,而不是2.五、三、4倍呢?经过google查找,发现1.5倍的扩容是最好的倍数。由于一次性扩容太大(例如2.5倍)可能会浪费更多的内存(1.5倍最多浪费33%,而2.5被最多会浪费60%,3.5倍则会浪费71%……)。可是一次性扩容过小,须要屡次对数组从新分配内存,对性能消耗比较严重。因此1.5倍刚恰好,既能知足性能需求,也不会形成很大的内存消耗。

处理这个ensureCapacity()这个扩容数组外,ArrayList还给咱们提供了将底层数组的容量调整为当前列表保存的实际元素的大小的功能。它能够经过trimToSize()方法来实现。该方法能够最小化ArrayList实例的存储量。

public void trimToSize() {
    modCount++;
    int oldCapacity = elementData.length;
    if (size < oldCapacity) {
        elementData = Arrays.copyOf(elementData, size);
    }
}

线程安全

ArrayList是线程不安全的。在其迭代器iteator中,若是有多线程操做致使modcount改变,会执行fastfail。抛出异常。

final void checkForComodification() {
        if (modCount != expectedModCount)
            throw new ConcurrentModificationException();
    }

Vector

Vector简介

Vector能够实现可增加的对象数组。与数组同样,它包含可使用整数索引进行访问的组件。不过,Vector的大小是能够增长或者减少的,以便适应建立Vector后进行添加或者删除操做。

Vector实现List接口,继承AbstractList类,因此咱们能够将其看作队列,支持相关的添加、删除、修改、遍历等功能。

Vector实现RandmoAccess接口,即提供了随机访问功能,提供提供快速访问功能。在Vector咱们能够直接访问元素。

Vector 实现了Cloneable接口,支持clone()方法,能够被克隆。

vector底层数组不加transient,序列化时会所有复制

protected Object[] elementData;


//        private void writeObject(java.io.ObjectOutputStream s)
//            throws java.io.IOException {
//            final java.io.ObjectOutputStream.PutField fields = s.putFields();
//            final Object[] data;
//            synchronized (this) {
//                fields.put("capacityIncrement", capacityIncrement);
//                fields.put("elementCount", elementCount);
//                data = elementData.clone();
//            }
//            fields.put("elementData", data);
//            s.writeFields();
//        }

Vector除了iterator外还提供Enumeration枚举方法,不过如今比较过期。

//        public Enumeration<E> elements() {
//            return new Enumeration<E>() {
//                int count = 0;
//
//                public boolean hasMoreElements() {
//                    return count < elementCount;
//                }
//
//                public E nextElement() {
//                    synchronized (Vector.this) {
//                        if (count < elementCount) {
//                            return elementData(count++);
//                        }
//                    }
//                    throw new NoSuchElementException("Vector Enumeration");
//                }
//            };
//        }
//

增删改查

vector的增删改查既提供了本身的实现,也继承了abstractList抽象类的部分方法。
下面的方法是vector本身实现的。

//
//    public synchronized E elementAt(int index) {
//        if (index >= elementCount) {
//            throw new ArrayIndexOutOfBoundsException(index + " >= " + elementCount);
//        }
//
//        return elementData(index);
//    }
//
//

//    public synchronized void setElementAt(E obj, int index) {
//        if (index >= elementCount) {
//            throw new ArrayIndexOutOfBoundsException(index + " >= " +
//                    elementCount);
//        }
//        elementData[index] = 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 */
//    }



//    public synchronized void insertElementAt(E obj, int index) {
//        modCount++;
//        if (index > elementCount) {
//            throw new ArrayIndexOutOfBoundsException(index
//                    + " > " + elementCount);
//        }
//        ensureCapacityHelper(elementCount + 1);
//        System.arraycopy(elementData, index, elementData, index + 1, elementCount - index);
//        elementData[index] = obj;
//        elementCount++;
//    }
//

//    public synchronized void addElement(E obj) {
//        modCount++;
//        ensureCapacityHelper(elementCount + 1);
//        elementData[elementCount++] = obj;
//    }

初始容量和扩容

扩容方式与ArrayList基本同样,可是扩容时不是1.5倍扩容,而是有一个扩容增量。

//    protected int elementCount;

//    protected int capacityIncrement;
//
//
//    }
//    public Vector() {
//        this(10);
//    }

capacityIncrement:向量的大小大于其容量时,容量自动增长的量。若是在建立Vector时,指定了capacityIncrement的大小;则,每次当Vector中动态数组容量增长时>,增长的大小都是capacityIncrement。若是容量的增量小于等于零,则每次须要增大容量时,向量的容量将增大一倍。

//        public synchronized void ensureCapacity(int minCapacity) {
//            if (minCapacity > 0) {
//                modCount++;
//                ensureCapacityHelper(minCapacity);
//            }
//        }
//        private void ensureCapacityHelper(int minCapacity) {
//            // overflow-conscious code
//            if (minCapacity - elementData.length > 0)
//                grow(minCapacity);
//        }
//
//        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大部分方法都使用了synchronized修饰符,因此他是线层安全的集合类。

Stack

在Java中Stack类表示后进先出(LIFO)的对象堆栈。栈是一种很是常见的数据结构,它采用典型的先进后出的操做方式完成的。每个栈都包含一个栈顶,每次出栈是将栈顶的数据取出,以下:

image

Stack经过五个操做对Vector进行扩展,容许将向量视为堆栈。这个五个操做以下:

empty()

测试堆栈是否为空。

peek()

查看堆栈顶部的对象,但不从堆栈中移除它。

pop()

移除堆栈顶部的对象,并做为此函数的值返回该对象。

push(E item)

把项压入堆栈顶部。

search(Object o)

返回对象在堆栈中的位置,以 1 为基数。

Stack继承Vector,他对Vector进行了简单的扩展:

public class Stack<E> extends Vector<E>
Stack的实现很是简单,仅有一个构造方法,五个实现方法(从Vector继承而来的方法不算与其中),同时其实现的源码很是简单

/**
 * 构造函数
 */
public Stack() {
}

/**
 *  push函数:将元素存入栈顶
 */
public E push(E item) {
    // 将元素存入栈顶。
    // addElement()的实如今Vector.java中
    addElement(item);

    return item;
}

/**
 * pop函数:返回栈顶元素,并将其从栈中删除
 */
public synchronized E pop() {
    E    obj;
    int    len = size();

    obj = peek();
    // 删除栈顶元素,removeElementAt()的实如今Vector.java中
    removeElementAt(len - 1);

    return obj;
}

/**
 * peek函数:返回栈顶元素,不执行删除操做
 */
public synchronized E peek() {
    int    len = size();

    if (len == 0)
        throw new EmptyStackException();
    // 返回栈顶元素,elementAt()具体实如今Vector.java中
    return elementAt(len - 1);
}

/**
 * 栈是否为空
 */
public boolean empty() {
    return size() == 0;
}

/**
 *  查找“元素o”在栈中的位置:由栈底向栈顶方向数
 */
public synchronized int search(Object o) {
    // 获取元素索引,elementAt()具体实如今Vector.java中
    int i = lastIndexOf(o);

    if (i >= 0) {
        return size() - i;
    }
    return -1;
}

Stack的源码不少都是基于Vector,因此这里再也不累述

区别

ArrayList的优缺点

从上面的几个过程总结一下ArrayList的优缺点。ArrayList的优势以下:

一、ArrayList底层以数组实现,是一种随机访问模式,再加上它实现了RandomAccess接口,所以查找也就是get的时候很是快

二、ArrayList在顺序添加一个元素的时候很是方便,只是往数组里面添加了一个元素而已

不过ArrayList的缺点也十分明显:

一、删除元素的时候,涉及到一次元素复制,若是要复制的元素不少,那么就会比较耗费性能

二、插入元素的时候,涉及到一次元素复制,若是要复制的元素不少,那么就会比较耗费性能

所以,ArrayList比较适合顺序添加、随机访问的场景。

ArrayList和Vector的区别

ArrayList是线程非安全的,这很明显,由于ArrayList中全部的方法都不是同步的,在并发下必定会出现线程安全问题。那么咱们想要使用ArrayList而且让它线程安全怎么办?一个方法是用Collections.synchronizedList方法把你的ArrayList变成一个线程安全的List,好比:
List<String> synchronizedList = Collections.synchronizedList(list);
synchronizedList.add("aaa");
synchronizedList.add("bbb");
for (int i = 0; i < synchronizedList.size(); i++)
{
    System.out.println(synchronizedList.get(i));
}

另外一个方法就是Vector,它是ArrayList的线程安全版本,其实现90%和ArrayList都彻底同样,区别在于:

一、Vector是线程安全的,ArrayList是线程非安全的

二、Vector能够指定增加因子,若是该增加因子指定了,那么扩容的时候会每次新的数组大小会在原数组的大小基础上加上增加因子;若是不指定增加因子,那么就给原数组大小*2,源代码是这样的:

int newCapacity = oldCapacity + ((capacityIncrement > 0) ?
                                 capacityIncrement : oldCapacity);

微信公众号【Java技术江湖】一位阿里 Java 工程师的技术小站。做者黄小斜,专一 Java 相关技术:SSM、SpringBoot、MySQL、分布式、中间件、集群、Linux、网络、多线程,偶尔讲点Docker、ELK,同时也分享技术干货和学习经验,致力于Java全栈开发!(关注公众号后回复”资料“便可领取 3T 免费技术学习资源以及我我原创的程序员校招指南、Java学习指南等资源)
在这里插入图片描述

相关文章
相关标签/搜索