天天看点

【搞定Java集合框架】第2篇:ArrayList、Vector、Stack 1、ArrayList 2、Vector3、Stack4、区别

本文大部分内容出自:Java集合详解1:ArrayList,Vector与Stack一文,部分内容为个人原创。

本文目录:

 1、ArrayList

 1.1 ArrayList 概述

1.2  底层数据结构

1.3  增删改查 

1.4  初始容量和扩容方式

1.5  线程安全

 2、Vector

2.1  Vector 简介

2.2  增删改查

2.3、初始容量和扩容方法

2.4  线程安全

3、Stack

4、区别

4.1  ArrayList的优缺点

4.2  ArrayList 和 Vector 的区别

本文将详细地介绍 Java 中的三个集合类:ArrayList、Vector、Stack。

集合是 Java 中非常重要而且基础的内容,因为任何数据都需要存储,集合的作用就是以一定的方式组织、存储数据。

一般对集合的讨论无非包括以下几个方面:

1、底层使用哪种数据结构实现的;

2、增删改查的实现方式;

3、初始容量、扩容方式、扩容时机;

4、线程是否安全;

5、是否允许为空(存储 null)、是否允许重复以及集合中的元素是否有序。

 1、ArrayList

 1.1 ArrayList 概述

  • ArrayList 的类结构
public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable{
	
	private class Itr implements Iterator<E> { // ...}
	 
	private class ListItr extends Itr implements ListIterator<E> { // ...}
	
	private class SubList extends AbstractList<E> implements RandomAccess { // ...}
	
	// ... 省略
}
           
  • 构造函数
// 构造器一:自定义集合大小
public ArrayList(int initialCapacity) {
	super();
	if (initialCapacity < 0)
		throw new IllegalArgumentException("Illegal Capacity: "+ initialCapacity);
	this.elementData = new Object[initialCapacity];
}

// 构造器二:默认集合大小为10
public ArrayList() {
	this(10);
}

// 构造器三:初始化集合为c
public ArrayList(Collection<? extends E> c) {
	elementData = c.toArray();
	size = elementData.length;
	// c.toArray might (incorrectly) not return Object[] (see 6260652)
	if (elementData.getClass() != Object[].class)
		elementData = Arrays.copyOf(elementData, size, Object[].class);
}
           

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

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

在每次添加新的元素时,ArrayList都会检查是否需要进行扩容操作,扩容操作将原数据向新数组的重新拷贝,所以如果我们知道具体业务数据量,在构造ArrayList时可以给ArrayList指定一个初始容量,这样就会减少扩容时数据的拷贝问题。当然在添加大量元素前,应用程序也可以使用ensureCapacity操作来增加ArrayList实例的容量,这可以减少递增式再分配的数量。

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

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

1.2  底层数据结构

ArrayList 的底层是一个 Object 数组,并且由 transient 修饰。

private 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 array length
	s.writeInt(elementData.length);

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

1.3  增删改查 

  • 查询元素

查询元素很简单,因为 ArrayList 底层是数组实现的,所以只需要根据索引index寻址即可。

public E get(int index) {
	rangeCheck(index);

	return elementData(index);
}
           
  • 添加元素

 添加元素有两种常用的方法:一种是直接添加到数组的末尾;另外一种是添加到指定的index索引下。这里就拿第二种为例进行讲解。

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方法来完成数组的复制。 

private void ensureCapacityInternal(int minCapacity) {
	modCount++;
	// 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 + (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);
}
           

这里讲下数组赋值的方法: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位置开始粘贴。

  • 删除元素

同样判断索引是否合法,删除的方式是把被删除元素右边的元素左移,方法同样是使用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; // Let gc do its work

	return oldValue;
}
           
  • 清空数组

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

public void clear() {
	modCount++;

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

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

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

在上面的几个操作中,我们会发现有一个高频出现的变量:modCount,它是在AbstractList中定义的。在一个迭代器初始的时候会赋予它调用这个迭代器的对象的modCount,如果在迭代器遍历的过程中,一旦发现这个对象的modCount和迭代器中存储的modCount不一样那就抛异常。

public abstract class AbstractList<E> extends AbstractCollection<E> implements List<E> {
	// ... 省略
	protected transient int modCount = 0;
}
           

下面是这个的完整解释:

Fail-Fast 机制: 

我们知道 java.util.ArrayList 不是线程安全的,如果出现线程不安全,那么将抛出ConcurrentModificationException,这就是所谓fail-fast策略。

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

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

所以在这里和大家建议,当大家遍历那些非线程安全的数据结构时,尽量使用迭代器

1.4  初始容量和扩容方式

  • 初始容量

ArrayList 的初始容量为:10,可以看它的默认构造函数:

public ArrayList() {
	this(10);
}
           
  • 扩容方式

扩容发生在add元素时,传入当前元素容量加1。

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

这里给出初始化时的数组:

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

精确地扩容: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.5、3、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);
    }
}
           

1.5  线程安全

ArrayList 是线程不安全的。在其迭代器 Itr 中,如果有多线程操作导致 modCount 改变,会执行 fastfail。抛出异常。

private class Itr implements Iterator<E> {
        
	// ...
	int expectedModCount = modCount;

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

 2、Vector

2.1  Vector 简介

  • Vector 的类结构
public class Vector<E>
    extends AbstractList<E>
    implements List<E>, RandomAccess, Cloneable, java.io.Serializable{
	
	protected Object[] elementData;
	
	protected int elementCount;        // 集合中元素的个数
	
	protected int capacityIncrement;   // 扩容增量
	
	// 自己实现的迭代器
	private class Itr implements Iterator<E> {
        // ...
        int expectedModCount = modCount;
	}
	
	final class ListItr extends Itr implements ListIterator<E> {
		// ...
	}
}
           

可以看出来 Vector 的类结构和 ArrayList 很像,毕竟都是实现了 List 接口的。但是 Vector 与 List 最大的不同就是 Vector 是同步的,即线程安全的。它里面的方法都是用 synchronized 关键字修饰的。后面讲解具体操作时会看到。

  • Vector 的构造方法
// 构造函数1:
public Vector(int initialCapacity, int capacityIncrement) {
	super();
	if (initialCapacity < 0)
		throw new IllegalArgumentException("Illegal Capacity: "+
										   initialCapacity);
	this.elementData = new Object[initialCapacity];
	this.capacityIncrement = capacityIncrement;
}

// 构造函数2:
public Vector(int initialCapacity) {
	this(initialCapacity, 0);
}

// 构造函数3:
public Vector() {
	this(10);
}

// 构造函数4:
public Vector(Collection<? extends E> c) {
	elementData = c.toArray();
	elementCount = elementData.length;
	// c.toArray might (incorrectly) not return Object[] (see 6260652)
	if (elementData.getClass() != Object[].class)
		elementData = Arrays.copyOf(elementData, elementCount, Object[].class);
}
           

下面先根据Vector类的结构,给出Vector的几个特点:

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

2、Vector实现List接口,继承AbstractList类,所以我们可以将其看做队列,支持相关的添加、删除、修改、遍历等功能。

3、Vector实现RandmoAccess接口,即提供了随机访问功能,提供提供快速访问功能。在Vector我们可以直接访问元素。

4、Vector 实现了Cloneable接口,支持clone()方法,可以被克隆。

5、Vector底层数组不加transient,序列化时会全部复制。

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

2.2  增删改查

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

2.3、初始容量和扩容方法

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

protected int capacityIncrement;
           

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 static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

private void grow(int minCapacity) {
	// overflow-conscious code
	int oldCapacity = elementData.length;
	// 增量扩容:capacityIncrement大于0增加capacityIncrement,否则增加1倍
	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);
}

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

2.4  线程安全

vector大部分方法都使用了synchronized修饰符,所以他是线层安全的集合类。

3、Stack

  • Stack 的源码

Stack 的源码比较少,直接就将其源码全部贴出来了,如下所示:

package java.util;

public class Stack<E> extends Vector<E> {
    
    // 栈默认是空的
    public Stack() {
    }

    // 入栈
    public E push(E item) {
        
        // 将元素存入栈顶。
        // addElement()的实现在Vector中
        addElement(item);

        return item;
    }

    // 出栈,移除元素
    public synchronized E pop() {
        E       obj;
        int     len = size();

        obj = peek();
        // 删除栈顶元素,removeElementAt()的实现在Vector中
        removeElementAt(len - 1);

        return obj;
    }

    // 出栈,不移除元素
    public synchronized E peek() {
        int     len = size();

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

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

    // 查找
    public synchronized int search(Object o) {
        // 获取元素索引,elementAt()具体实现在Vector中
        int i = lastIndexOf(o);

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

    /** use serialVersionUID from JDK 1.0.2 for interoperability */
    private static final long serialVersionUID = 1224463164541339165L;
}
           

可以看到 Stack 继承自 Vector,所以这里不再累述。

在Java中Stack类表示“后进先出”(LIFO)的对象堆栈。

栈是一种非常常见的数据结构,它采用典型的先进后出的操作方式完成的。每一个栈都包含一个栈顶,每次出栈是将栈顶的数据取出,如下:

【搞定Java集合框架】第2篇:ArrayList、Vector、Stack 1、ArrayList 2、Vector3、Stack4、区别

Stack继承Vector,他对Vector进行了简单的扩展,允许将向量视为堆栈。这个五个操作如下:

1、empty():测试堆栈是否为空。

2、peek():查看堆栈顶部的对象,但不从堆栈中移除它。

3、pop():移除堆栈顶部的对象,并作为此函数的值返回该对象。

4、push(E item):把项压入堆栈顶部。

5、search(Object o):返回对象在堆栈中的位置,以 1 为基数。

4、区别

4.1  ArrayList的优缺点

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

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

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

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

1、删除元素的时候,涉及到一次元素复制,如果要复制的元素很多,那么就会比较耗费性能;

2、插入元素的时候,涉及到一次元素复制,如果要复制的元素很多,那么就会比较耗费性能;

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

4.2  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都完全一样,区别在于:

1、Vector是线程安全的,ArrayList是线程非安全的;

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

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

全篇完!

继续阅读