书本在这一章的内容继续延续上一章的程序。我们不去讨论程序的内容,就说说新学到的一个类ArrayList。
public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, Serializable
List 接口的大小可变数组的实现。实现了所有可选列表操作,并允许包括 null 在内的所有元素。除了实现 List 接口外,此类还提供一些方法来操作内部用来存储列表的数组的大小。(此类大致上等同于 Vector 类,除了此类是不同步的。)size、isEmpty、get、set、iterator 和 listIterator 操作都以固定时间运行。add 操作以分摊的固定时间 运行,也就是说,添加 n 个元素需要 O(n) 时间。其他所有操作都以线性时间运行(大体上讲)。与用于 LinkedList 实现的常数因子相比,此实现的常数因子较低。
每个 ArrayList 实例都有一个容量。该容量是指用来存储列表元素的数组的大小。它总是至少等于列表的大小。随着向 ArrayList 中不断添加元素,其容量也自动增长。并未指定增长策略的细节,因为这不只是添加元素会带来分摊固定时间开销那样简单。
在添加大量元素前,应用程序可以使用 ensureCapacity 操作来增加 ArrayList 实例的容量。这可以减少递增式再分配的数量。注意,此实现不是同步的。如果多个线程同时访问一个 ArrayList 实例,而其中至少一个线程从结构上修改了列表,那么它必须 保持外部同步。(结构上的修改是指任何添加或删除一个或多个元素的操作,或者显式调整底层数组的大小;仅仅设置元素的值不是结构上的修改。)这一般通过对自然封装该列表的对象进行同步操作来完成。如果不存在这样的对象,则应该使用 Collections.synchronizedList 方法将该列表“包装”起来。这最好在创建时完成,以防止意外对列表进行不同步的访问:
List list = Collections.synchronizedList(new ArrayList(...));
此类的 iterator 和 listIterator 方法返回的迭代器是快速失败的:在创建迭代器之后,除非通过迭代器自身的 remove 或 add 方法从结构上对列表进行修改,否则在任何时间以任何方式对列表进行修改,迭代器都会抛出 ConcurrentModificationException。因此,面对并发的修改,迭代器很快就会完全失败,而不是冒着在将来某个不确定时间发生任意不确定行为的风险。注意,迭代器的快速失败行为无法得到保证,因为一般来说,不可能对是否出现不同步并发修改做出任何硬性保证。快速失败迭代器会尽最大努力抛出 ConcurrentModificationException。因此,为提高这类迭代器的正确性而编写一个依赖于此异常的程序是错误的做法:迭代器的快速失败行为应该仅用于检测 bug。
此类是 Java Collections Framework 的成员。
看了上述的概要信息,整个人都不好了,很多术语都表示看不懂。我就把懂的列出来吧:
线程不同步是指当在A线程访问一个 ArrayList 实例并从结构上修改了列表(结构上的修改是指任何添加或删除一个或多个元素的操作,或者显式调整底层数组的大小;仅仅设置元素的值不是结构上的修改。)。那么如果在B线程也对同一个ArrayList实例进行访问,访问的结果不一定是A线程修改后的结果。
上图是Java Collection层次结构,里面涉及到Collection、Set、List、SortSet接口(不能实例化)和HashSet、Linked HashSet、TreeSet、LinkedList、Vector、ArrayList具体类的层次关系。
详见这Java Collection和Java - Collection两篇文章。
从上面我们可以得到两者之间的差别如下:
Tables | ArrayList | 数组 |
---|---|---|
是否可以删除元素? | 可以, 调用remove()方法 | 不可以数组类中并没有删除元素的方法。如果在数组中想要删除元素,只能把相应位置的元素置成null(对Object来说)或者使用相应的值进行标记,比如说-1。不过即使是这样,那个元素还是实实在在存在的。 |
是否可以动态改变大小 | 可以,当往ArrayList添加或删除元素时,ArrayList会自动增大或减小其大小。 | 不可以 ,因为数组在声明的时候就需要指定其大小,大小是固定的。 |
是否可以保存primitive主数据类型 | 不可以,虽然是不可以,但是在Java中有自动装箱拆箱技术,可以参考这两篇文章Java中的包装类 装箱和拆箱和等于还是不等于? | 可以 |
存取对象的调用方法 | 使用 . 操作符来调用方法 | 使用 [ ] 操作符 |
我们来从源码上来看看ArrayList是怎么由数组实现的。借用上一章查找源码的方法,我们在 util 这个包中找到了ArrayList的源码(此处以remove()方法为例)。
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
private static final long serialVersionUID = 8683452581122892189L;
/** * The array buffer into which the elements of the ArrayList are stored. * The capacity of the ArrayList is the length of this array buffer. */
private transient Object[] elementData;
/** * The size of the ArrayList (the number of elements it contains). * * @serial */
private int size;
/**此处省略余下代码**/
}
在上述的codes中的elementData就是一个数组,用来存储ArrayList中的元素。容量大小就是ArrayList的大小。
private transient Object[] elementData;
// remove方法
/** * Removes the element at the specified position in this list. * Shifts any subsequent elements to the left (subtracts one from their * indices). * * @param index the index of the element to be removed * @return the element that was removed from the list * @throws IndexOutOfBoundsException {@inheritDoc} */
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;
}
首先调用rangeCheck(index)
检测传进来的index是不是在ArrayList的范围之内。之后获取index位置的Element保存在oldValue中用来最后的函数返回。然后计算需要移动的元素个数,调用System.arraycopy()
进行元素的复制。最后把最后一个元素置为null,总的大小设为size-1.因为删除了一个指定的元素,那么它后面的所有元素都要往左移动一个单位,直至最后一个元素。可用下图进行表示:
Object0 | Object1 | Object2 | Object3 | Object4 | Object5 | Object6 | Object7 | Object8 | Object9 |
---|
当前的ArrayList中有如下10个元素。
Object0 | Object1 | Object2 | Object3 | Object4 | Object5 | Object7 | Object8 | Object9 | null |
---|
调用remove(5)删除第5个元素Object6后ArrayList的元素。最后一个元素置为null,ArrayList的大小设为9
上述就达到了在ArrayList中删除一个元素的目的了。因此,如果ArrayList有n个元素,对于随机位置的remove操作,时间复杂度为O(n)。但是对于列表末尾的remove操作,时间复杂度是 O(1).上述同样适用于add操作。很明显,因为要进行remove和add操作都要进行元素的copy操作,效率比较低。
本例是将ArrayList的add和get封装成我自己一个简单的栈结构的push和pop。
(1)、TestArrayList.java
import java.util.ArrayList;
public class TestArrayList{
public static void main(String[] args)
{
int i = 0;
int size = 5;
String value = "six";
MyStack mystack = new MyStack();
ArrayList<String> arr = new ArrayList<String>();
// 往ArrayList添加5个元素
for(i=0; i<5; i++)
{
String str = new String();
str = String.valueOf(i); // 讲int转换为String
arr.add(str);
}
mystack.ChangeListToStack(arr);
// 打印ArrayList原始的元素顺序
System.out.println("The original ArrayList");
for(String name : arr)
{
System.out.println(" " + name);
}
System.out.println(" ");
// 从Stack中pop出一个元素
value = mystack.StackPop();
System.out.println("The pop value from ArrayList is " + value);
System.out.println(" ");
// 往Stack中push String为"5"的元素
value = "5";
mystack.StackPush(value);
System.out.println("The original ArrayList");
for(String name : arr)
{
System.out.println(" " + name);
}
}
}
(2)、MyStack.java
import java.util.ArrayList;
public class MyStack {
private int StackSize;
private ArrayList<String> ArrList;
public void ChangeListToStack(ArrayList<String> arr)
{
ArrList = arr;
}
public String StackPop()
{
String ret = "";
ret = ArrList.get(0); // 返回起始位置的元素
return ret;
}
public void StackPush(String str)
{
ArrList.add(0, str); // 在list的起始位置插入元素
}
}
运行结果如下:
上面的modCount为Modify Count的简写,作用如下:
ArrayList中已经阐述了。modCount用于记录ArrayList集合的修改次数,初始化为0,,每当集合被修改一次(结构上面的修改,内部update不算),如add、remove等方法,modCount + 1,所以如果modCount不变,则表示集合内容没有被修改。该机制主要是用于实现ArrayList集合的快速失败机制,在Java的集合中,较大一部分集合是存在快速失败机制的,这里就不多说,后面会讲到。所以要保证在遍历过程中不出错误,我们就应该保证在遍历过程中不会对集合产生结构上的修改(当然remove方法除外),出现了异常错误,我们就应该认真检查程序是否出错而不是catch后不做处理。
至于什么是快速失败机制,还不是很懂。希望以后能够学到相关的内容。
此处还没找到详细的对照说明,暂时留着。