《Java集合类1》ArrayList,Vector与Stack

:这三个集合类的底层都是数组实现(Stack继承自Vector)并且较为常用。
在这里插入图片描述

一般在这几方面讨论集合类:
1底层数据结构
2增删改查方式
3初始容量,扩容方式,扩容实际
4线程安全与否
5是否允许空,是否允许重复,是否有序

ArrayList

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

每个ArrayList实例都有一个容量,该容量是指用来存储列表元素的数组的大小。默认初始容量为10。随着ArrayList中元素的增加,它的容量也会不断的自动增长。
在这里插入图片描述
在每次添加 add 新的元素时,ArrayList都会检查是否需要进行扩容操作,扩容操作带来数据向新数组的重新拷贝,所以如果我们知道具体业务数据量,在构造ArrayList时可以给ArrayList指定一个初始容量,这样就会减少扩容时数据的拷贝问题。当然在添加大量元素前,应用程序也可以使用ensureCapacity操作来增加ArrayList实例的容量,这可以减少递增式再分配的数量。
【查看源码】:
在这里插入图片描述
ArrayList 中ensureCapacity方法的使用与优化

package List;
//ArrayList 中ensureCapacity方法的使用与优化

//对于ArrayList 中的一个方法ensureCapacity(int n),这个方法可以对ArrayList底层的
// 数组进行扩容,显式的调用这个函数。
//  (由源码可知,系统的默认扩容是:扩容为原数组长度的1.5倍)
//    如果参数n大于底层数组长度的1.5倍(默认扩容),那么这个数组的容量就会被扩容到这个参数值n;
//    如果参数n小于底层数组长度的1.5倍(默认扩容),那么这个容量就会被扩容到底层数组长度的1.5倍。

import java.util.ArrayList;

//总之,就是这个函数可以对底层数组进行扩容,在适当的时机,很好的利用这个函数,
//将会使我们写出来的程序性能得到提升。
public class ListDemo1 {
    public static void main(String[] args) {
        final int N=1000000;
        Object obj=new Object();
        ArrayList list1=new ArrayList();
        long start=System.currentTimeMillis();
        for(int i=0;i<N;i++){
            list1.add(obj);
        }
        System.out.println(System.currentTimeMillis()-start);

        ArrayList list2=new ArrayList();
        long start2=System.currentTimeMillis();
        list2.ensureCapacity(N);//显式的对底层数组进行扩容
        for(int i=0;i<N;i++){
            list2.add(obj);
        }
        System.out.println(System.currentTimeMillis()-start2);
    }
}

//运行结果:36
//         17
//第2段的效率显然比第1段高很多。
// 原因:因为第1段代码如果没有一次性扩到想要的最大容量
// 的话,它就会在添加元素的过程中,一点一点的进行扩容,
//要知道对数组的扩容是要进行数组拷贝的,这就会浪费大量的时间。
// 如果已经预知容器可能会装多少元素,最好显式的调用ensureCapacity这个方法,一次性扩容到位。

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

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

2、ArrayList的继承关系
ArrayList继承AbstractList抽象父类,实现了List接口(规定了List的操作规范)、RandomAccess(可随机访问)、Cloneable(可拷贝)、Serializable(可序列化)。
在这里插入图片描述
3、底层数据结构
ArrayList的底层是一个Object数组,并且由transient修饰

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

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

4、增删改查

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位置开始粘贴。

//   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方法进行复制。

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

set() :修改元素时,只需要检查下标即可进行修改操作。

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

get() :获取元素时,也是只需要检查下标即可进行获取操作。

//   public E get(int index) {
//       rangeCheck(index);
//
//       return elementData(index);
//   }

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

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

5、modCount

//        protected transient int modCount = 0;

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

下面是这个的完整解释 Fail-Fast 机制 因为 java.util.ArrayList
不是线程安全的,ArrayList,那么将抛出ConcurrentModificationException,这就是所谓fail-fast策略。
这一策略在源码中的实现是通过 modCount 域,modCount 顾名思义就是修改次数,对ArrayList
内容的修改都将增加这个值,那么在迭代器初始化过程中会将这个值赋给迭代器的 expectedModCount。 在迭代过程中,判断
modCount 跟 expectedModCount 是否相等,如果不相等就表示已经有其他线程修改了 ArrayList。

所以,当我们遍历那些非线程安全的数据结构时,尽量使用迭代器

6、初始容量and扩容方式

初始容量是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.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);
    }
}

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

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

Vector

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

    Vector实现 List接口,继承 AbstractList类,所以我们可以将其看做队列,支持相关的添加、删除、修改、遍历等功能。
    Vector实现RandomAccess接口,即提供了随机访问功能,提供提供快速访问功能。在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();
//        }

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 addElement(E obj) {
    modCount++;
    ensureCapacityHelper(elementCount + 1);
    elementData[elementCount++] = obj;
}
//在指定位置插入**********************************************************
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++;
}

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

 /**
 * Constructs an empty vector with the specified initial capacity and
 * with its capacity increment equal to zero.
 *
 * @param   initialCapacity   the initial capacity of the vector
 * @throws IllegalArgumentException if the specified initial capacity
 *         is negative
 */
public Vector(int initialCapacity) {
    this(initialCapacity, 0);
}

/**
 * Constructs an empty vector so that its internal data array
 * has size {@code 10} and its standard capacity increment is
 * zero.
 */
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);
//        }

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

Stack

栈是我们常用的数据结构之一。在Java中Stack类表示先进后出(FILo)的对象堆栈。每一个栈都包含一个栈顶,每次出栈是将栈顶的数据取出。

通过继承了Vector类,Stack类可以很容易的实现他本身的功能。因为大部分的功能在Vector里面已经提供支持了。

Stack通过五个操作对Vector进行扩展:这个五个操作如下:

empty():判断堆栈是否为空。
peek():查看堆栈顶部的元素,但不取走。
pop():移除堆栈顶部的元素,并作为函数的值返回该元素。
push(E item):将item压入栈顶部。
search(Object o):返回元素对象o在堆栈中的位置,以1为基数。

public class Stack extends Vector
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) {
    // 获取元素索引,lastIndexOf()具体实现在Vector.java中
    int i = lastIndexOf(o);

    if (i >= 0) {
        return size() - i;
    }
    return -1;
}
   Stack继承自Vector,所以也是线程安全的

ArrayList和Vector之间的区别

ArrayList的优缺点

   【ArrayList优点】:
           1. ArrayList底层以数组实现,是一种随机访问模式,再加上它实现了
               RandomAccess接口,所以查找get的时候也很快。
           2. ArrayList在顺序添加一个元素的时候非常方便,只是往数组中添加
               一个元素而已。

    【ArrayList缺点】:
           1. 删除元素的时候,涉及到一次元素复制,如果要复制的元素很多,  
               则就会比较耗费性能。
           2. 插入元素的时候,涉及到一次元素复制,如果要复制的元素很多,
               则就会比较耗费性能。
             因此,ArrayList比较适合顺序添加、随机访问的场景。           

ArrayList和Vector的区别

    ArrayList不是线程安全的,因为ArrayList中所有的方法都不是同步的,在
    并发下一定会出现线程安全问题。若我们想要使用ArrayList并且让它线程
    安全那就这样做:
        第一个方法是使用Collections.synchronizedList方法把我们的ArrayList变成一个线程安全的List,Eg:
public static void main(String[] args) {
    List<String> list=Collections.synchronizedList(new ArrayList<>());
    list.add("aaa");
    list.add("bbb");
    for(int i=0;i<list.size();i++){
        System.out.println(list.get(i));
    
    第二方法是Vector,它是ArrayList的线程安全版本,Vector的实现90%和ArrayList都是一样的,区别在于:
    Vector是线程安全的,ArrayList不是线程安全的。
    Vector可以指定增长因子,若该增长因子指定了,那么扩容的时候会使每次新的数组大小会在原数组的大小基础上加上增长因子;如果不指定增长因子,那么就给原数组大小*2,源代码如下:
int newCapacity = oldCapacity + ((capacityIncrement > 0) ?
                                 capacityIncrement : oldCapacity);

—————————————OVER—————_————————————

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章