JDK集合类源码 || 一、ArrayList,Vector,Stack


前言

这一系列是对程序员江湖博主LXF - Java集合类专题的整理。因为原文虽然讲解的很全面透彻,但是组织结构不清晰,而且错别字较多,所以我重新整理,便于自己阅读理解记忆。

原文:https://h2pl.github.io/2018/05/08/collection1/

本文详尽地介绍了Java中的三个集合类:ArrayList、Vector、Stack。集合是Java中非常重要而且基础的内容,因为任何数据必不可少地需要存储,集合的作用就是以一定的方式组织、存储数据。之所以把这三个集合类放在一起讲解,是因为这三个集合类的底层数据结构都是数组,并且比较常用。

一般从以下5个方面讨论集合类:

1、底层数据结构
2、增删改查方式
3、初始容量,扩容方式,扩容时机
4、是否线程安全
5、是否允许null,是否允许重复,是否有序

一、ArrayList

1、概述

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

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

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

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

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

2、底层数据结构

ArrayList的底层数据结构是一个object数组elementData,并且被trasient修饰,

transient Object[] elementData; // non-private to simplify nested class access

trasient:java语言的关键字,变量修饰符,如果用transient声明一个实例变量,当对象存储时,它的值不需要维持。换句话说,用transient关键字修饰的成员变量不参与序列化过程。

所以,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();
       }
   }

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

3、增删改查

添加元素调用add方法。首先判断索引是否合法,然后检测是否需要扩容,最后使用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位置开始粘贴。

add方法的源码如下,

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

删除元素调用remove方法。同样首先判断索引是否合法,如果合法就执行删除操作。删除的方式是,使用System.arraycopy进行复制,把被删除元素右边的元素左移。remove方法的源码如下,

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

清空数组调用clear方法。该方法将所有元素置为null,这样就可以让GC自动回收掉没有被引用的元素了。clear方法的源码如下,

public void clear() {
    modCount++;

    // clear to let GC do its work
    for (int i = 0; i < size; i++)
        elementData[i] = null;

    size = 0;
}

修改元素调用set方法。首先检查下标是否合法,如果合法即可进行修改操作。set方法的源码如下,

public E set(int index, E element) {
    rangeCheck(index);

    E oldValue = elementData(index);
    elementData[index] = element;
    return oldValue;
}

查找元素调用get方法。首先检查下标是否合法,如果合法则返回该下标处的元素。get方法的源码如下,

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

    return elementData(index);
}

上述方法都使用了rangeCheck方法,其实该方法仅仅是简单地检查下标而已。rangeCheck方法的源码如下,

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

4、modCount

由以上代码可以看出,在一个迭代器初始化时,会赋予它调用该迭代器的对象的mCount。Q:如何在迭代器遍历的过程中,一旦发现这个对象的mcount和迭代器中存储的mcount不一样那就抛异常呢?

A:Fail-Fast 机制

java.util.ArrayList不是线程安全的,一旦出现并发修改,那么将抛出ConcurrentModificationException异常,这就是所谓fail-fast策略。

这一策略在源码中的实现是通过 modCount 域。modCount 顾名思义就是修改次数,一旦在结构上修改了 ArrayList 的内容,modCount++。那么在迭代器初始化过程中将 modCount 赋给迭代器的 expectedModCount。在迭代过程中,判断 modCount 跟 expectedModCount 是否相等,如果不相等就表示已经有其他线程修改了 ArrayList。

所以建议,遍历非线程安全的数据结构时,尽量使用迭代器。

5、初始容量和扩容方式

初始容量是10,下面是扩容方法,

private static final int DEFAULT_CAPACITY = 10;

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

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

/ /这说明:如果数组还是初始数组,那么最小的扩容大小就是minCapacity=(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倍。当新容量大于最大数组容量时,执行大数扩容。grow方法的源码如下,

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

二、Vector

1、概述

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

2、增删改查

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

elementAt 方法:返回指定索引处的元素。

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

    return elementData(index);
}

setElementAt 方法:设置指定索引处的元素。

public synchronized void setElementAt(E obj, int index) {
    if (index >= elementCount) {
        throw new ArrayIndexOutOfBoundsException(index + " >= " + elementCount);
    }
    elementData[index] = obj;
}

removeElementAt 方法:删除指定索引处的元素。

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

insertElementAt 方法:在指定索引处插入元素。

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

addElement 方法:在底层数组尾部添加元素。

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

3、初始容量和扩容

Vector 的扩容方式与 ArrayList 的扩容方式基本一样,但是扩容时不是采用1.5倍扩容,而是有一个扩容增量。在JDK中的源码为:

protected int elementCount;

protected int capacityIncrement;

public Vector() {
    this(10);
}

capacityIncrement:向量的大小 > elementCount时,容量的增量。如果在创建Vector时,指定了capacityIncrement的大小,那么,每次扩容时,增加的大小都是capacityIncrement。如果扩容的增量capacityIncrement <= 0 ,则每次需要扩容时,向量的容量将增大一倍。

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

4、线程安全

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

三、Stack

在Java中,Stack 类表示后进先出(LIFO)的栈。栈是一种非常常见的数据结构,它采用典型的先进后出的操作方式完成。每一个栈都包含一个栈顶,每次出栈是将栈顶的数据取出,如下图:
在这里插入图片描述
Stack 通过五个操作对 Vector 进行扩展,允许将向量视为堆栈。这个五个操作如下:

empty() 判断栈是否为空

peek() 查看栈顶的对象,但不从栈移除它

pop() 移除栈顶的对象,并返回该对象

push(E item) 将对象压入栈顶,并返回该对象

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

Stack 继承自 Vector,对Vector进行了简单的扩展:public class Stack extends Vector {…}

Stack 的实现非常简单,仅有一个构造方法 + 五个实现方法(从Vector继承而来的方法不算在其中)。Stack的源码很多都是基于Vector,所以这里不再累述。在JDK中的源码为:

// 构造函数
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;
}

四、区别

1、ArrayList的优缺点

ArrayList 的优点如下:

1、ArrayList 底层数据结构是数组,是一种随机访问模式,再加上它实现了RandomAccess接口,所以使用get方法查找的时候非常快;

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

ArrayList 的缺点如下:

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

总结:ArrayList 比较适合随机访问、顺序添加的场景。

2、ArrayList和Vector的区别

ArrayList 是线程非安全的,因为ArrayList中所有的方法都不是同步的,在并发情况下一定会出现线程安全问题。

Q:那么,如果想要使用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是线程非安全的。因为Vector的所有方法都使用了Synchronized关键字修饰;
2、Vector可以指定增长因子。如果指定增长因子,那么扩容时,每次新的数组大小会在原数组的大小基础上加上增长因子;如果不指定增长因子,那么就给原数组大小*2(扩容一倍),源代码是这样的:

int newCapacity = oldCapacity + ((capacityIncrement > 0) ? capacityIncrement : oldCapacity);
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值