Java集合(三)CopyOnWriteArrayList、Vector、Stack

  本系列文章:
    Java集合(一)集合框架概述
    Java集合(二)List、ArrayList、LinkedList
    Java集合(三)CopyOnWriteArrayList、Vector、Stack
    Java集合(四)Map、HashMap
    Java集合(五)LinkedHashMap、TreeMap
    Java集合(六)Hashtable、ConcurrentHashMap
    Java集合(七)Set、HashSet、LinkedHashSet、TreeSet
    Java集合(八)BlockingQueue、ArrayBlockingQueue、LinkedBlockingQueue、PriorityBlockingQueue、DelayQueue

【CopyOnWriteArrayList】

一、CopyOnWriteArrayList介绍

  在很多应用场景中,读操作可能会远远大于写操作。由于读操作根本不会修改原有的数据,因此对于每次读取都进行加锁其实是一种资源浪费。我们可以允许多个线程同时访问List 的内部数据,毕竟读取操作是安全的。CopyOnWriteArrayList恰恰符合这种要求。
  CopyOnWriteArrayList位于JUC包下,可以说是读写分离、以空间换时间版的ArrayList。其继承关系和ArrayList也是一致的:

	public class CopyOnWriteArrayList<E>
    	implements List<E>, RandomAccess, Cloneable, java.io.Serializable

  CopyOnWriteArrayList是一个线程安全、并且在读操作时无锁的ArrayList,其底层实现也是数组。不止读操作时无锁,写入也不会阻塞读取操作。只有写入和写入之间需要进行同步等待。
  CopyOnWriteArrayList 类的所有可变操作(add、set等)都是通过创建底层数组的新副本来实现的。当List需要被修改的时候,并不修改原有内容,而是对原有数据进行一次复制,副本用来写,原来的数据用来读。写完之后,再将修改完的副本替换原来的数据,这样就可以保证写操作不会影响读操作。

1.1 CopyOnWriteArrayList的特点*

  • 1、线程安全
      CopyOnWriteArrayList所有写操作,都是通过对底层数组进行一次新的复制来实现,是线程安全的。
  • 2、适合读操作大于写操作的场景
      CopyOnWriteArrayList适合使用在读操作远远大于写操作的场景里,比如缓存。CopyOnWriteArrayList不存在扩容的概念,每次写操作都要复制一个副本,在副本的基础上修改后改变原来的引用。CopyOnWriteArrayList中写操作需要大面积复制数组,所以性能差。
  • 3、内存消耗较大
      CopyOnWriteArrayList虽然读多写少的场景,不过要慎用 ,因为当CopyOnWriteArrayList存放的数据比较多时,每次add/set都要重新复制数组,这个代价实在太高。
  • 4、数据一致性
      CopyOnWrite容器只能保证数据的最终一致性,不能保证数据的实时一致性。写和读分别作用在新老不同容器上,在写操作执行过程中,读不会阻塞但读取到的却是老容器的数据。
  • 5、读写分离,效率高
      采用读写分离方式,读的效率非常高。CopyOnWriteArrayList的迭代器是基于创建时的数据快照的,故数组的增删改不会影响到迭代器。
  • 6、支持随机读
      由于CopyOnWriteArrayList底层实现数组,数据存在连续的内存上,因此支持随机读。

1.2 CopyOnWriteArrayList的使用

  • 1、构造方法
      可以构建空对象,也可以根据数组、集合等对象来构建对象。
	//构造一个空数组
	public CopyOnWriteArrayList()
	//将指定集合中的元素copy到数组中
	public CopyOnWriteArrayList(Collection<? extends E> c)
	//将指定数组中的元素copy到数组中
	public CopyOnWriteArrayList(E[] toCopyIn)
  • 2、添加元素
      添加元素的方式和ArrayList类似,默认添加元素在数组尾部,也可以在指定位置添加。
	//将指定的元素列表的结束
	public boolean add(E e)
	//在列表中指定的位置上插入指定的元素
	public void add(int index, E element)
	//追加指定集合的所有元素到这个列表的末尾
	public boolean addAll(Collection<? extends E> c)
	//将指定集合中的所有元素插入到该列表中,从指定位置开始
	public boolean addAll(int index, Collection<? extends E> c)
  • 3、判断是否包含某个元素
	public boolean contains(Object o)
  • 4、获取制定位置的元素
	public E get(int index)
  • 5、返回指定元素的索引
	//返回此列表中指定元素的第一个出现的索引
	public int indexOf(Object o)
	//从指定位置搜索,返回此列表中的第一个出现的指定元素的索引
	public int indexOf(E e, int index)
  • 6、删除元素
	//根据下标删除元素
	public E remove(int index)
	//删除列表中首次出现的元素
	public boolean remove(Object o)
  • 7、清空
	public void clear()	

二、从源码理解CopyOnWriteArrayList

  看CopyOnWriteArrayList源码,可以从两点来把握:

  • 1、动态数组
      CopyOnWriteArrayList内部有个volatile数组。在添加、修改、删除数据时,都会新建一个数组,并将原数组中的元素拷贝到新数组中,然后在新数组中更新,最后再将“volatile数组”引用指向新数组。
      CopyOnWriteArrayList就是通过这种方式实现的动态数组;不过正由于它在添加、修改、删除数据时,都会新建数组,所以涉及到修改数据的操作,CopyOnWriteArrayList效率很低;但只是进行遍历查找的话,效率比较高(与ArrayList相似)。
  • 2、线程安全
      线程安全是通过volatile和互斥锁来实现的。
      CopyOnWriteArrayList的底层实现是volatile数组。volatile可以保证不同线程对共享变量操作的可见性,因此一个线程读取volatile数组时,总能看到其它线程对该volatile数组最后的写入数据,这样就保证了读取到的数据总是最新的。
      CopyOnWriteArrayList通过互斥锁来保护数据。在添加、修改、删除数据时,会先获取互斥锁,修改完毕后,先将volatile数组引用指向新数组,然后再释放互斥锁。这样,就达到了保护数据的目的。

  CopyOnWriteArrayList主要的成员变量:

    //锁
    final transient ReentrantLock lock = new ReentrantLock();
    //数组
    private transient volatile Object[] array;

  CopyOnWriteArrayList在实现线程安全时,用了一把锁。

2.1 创建对象

	//创建空的CopyOnWriteArrayList对象
    public CopyOnWriteArrayList() {
        setArray(new Object[0]);
    }
	//创建含有指定Collection的元素的CopyOnWriteArrayList
    public CopyOnWriteArrayList(Collection<? extends E> c) {
        Object[] elements;
        if (c.getClass() == CopyOnWriteArrayList.class)
            elements = ((CopyOnWriteArrayList<?>)c).getArray();
        else {
            elements = c.toArray();
            if (elements.getClass() != Object[].class)
                elements = Arrays.copyOf(elements, elements.length, Object[].class);
        }
        setArray(elements);
    }
	//创建含有指定数组的元素的CopyOnWriteArrayList
    public CopyOnWriteArrayList(E[] toCopyIn) {
        setArray(Arrays.copyOf(toCopyIn, toCopyIn.length, Object[].class));
    }

    final void setArray(Object[] a) {
        array = a;
    }

2.2 添加元素

  add() 方法在添加集合的时候加了锁,保证了同步,避免了多线程写的时候会copy出多个副本出来。也就是说同时最多只有一个线程可以获取到这把锁,来创建新数组。
  总体而言,添加元素的逻辑是:加锁;创建一个新的数组;将现有数组的元素拷贝过去;在新的数组中增加元素;将引用指向新的数组;释放锁。

  • 1、在数组末尾追加元素
    public boolean add(E e) {
    	//获取该对象的锁
        final ReentrantLock lock = this.lock;
        //加锁,每次只有一个线程可进入临界区
        lock.lock();
        try {
        	//获取原始"volatile数组"中的数据和数据长度
            Object[] elements = getArray();
            int len = elements.length;
            //新建一个数组newElements,并将原始数据拷贝到newElements中
            //newElements数组的长度="原始数组的长度"+1
            Object[] newElements = Arrays.copyOf(elements, len + 1);
            //将"新增加的元素"保存到newElements中
            newElements[len] = e;
            //将"volatile数组"引用指向newElements数组,这样旧数组就被GC回收了
            setArray(newElements);
            return true;
        } finally {
        	//释放锁
            lock.unlock();
        }
    }
  • 2、在数组指定位置添加元素
    public void add(int index, E element) {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            Object[] elements = getArray();
            int len = elements.length;
            if (index > len || index < 0)
                throw new IndexOutOfBoundsException("Index: "+index+
                                                    ", Size: "+len);
            Object[] newElements;
            int numMoved = len - index;
            //如果要插入的位置是最后一位,直接将旧数组复制到新数组中,新数组长度为旧数组长度+1
            if (numMoved == 0)
                newElements = Arrays.copyOf(elements, len + 1);
            //如果要插入的位置不是最后一位,新数组长度为旧数组长度+1,然后将要插入位置前后的数据都复制到新数组中
            else {
                newElements = new Object[len + 1];
                System.arraycopy(elements, 0, newElements, 0, index);
                System.arraycopy(elements, index, newElements, index + 1,
                                 numMoved);
            }
            newElements[index] = element;
            setArray(newElements);
        } finally {
            lock.unlock();
        }
    }

  可以看出,添加1个元素时,新数组的长度是在原有数组长度len的基础上+1。同理,往CopyOnWriteArrayList里添加一个元素数量为n的集合时,新数组的长度是len+n。

2.3 删除元素

  删除元素的逻辑和添加元素的相似:加锁;创建一个新的数组;将现有数组的元素拷贝过去;在新的数组中删除元素;将引用指向新的数组;释放锁。

	//删除索引index处的元素
    public E remove(int index) {
        final ReentrantLock lock = this.lock;
        //加锁
        lock.lock();
        try {
        	//获取原始"volatile数组"中的数据和数据长度
            Object[] elements = getArray();
            int len = elements.length;
            //获取elements数组中的第index个数据
            E oldValue = get(elements, index);
            int numMoved = len - index - 1;
            //如果被删除的是最后一个元素,则直接通过Arrays.copyOf()进行处理,而不需要新建数组
            if (numMoved == 0)
                setArray(Arrays.copyOf(elements, len - 1));
            //否则,新建数组,然后将"volatile数组中被删除元素之外的其它元素"拷贝到新数组中
            //最后,将"volatile数组"引用指向newElements数组,这样旧数组就被GC回收了。
            else {
                Object[] newElements = new Object[len - 1];
                System.arraycopy(elements, 0, newElements, 0, index);
                System.arraycopy(elements, index + 1, newElements, index,
                                 numMoved);
                setArray(newElements);
            }
            //将删除的元素返回
            return oldValue;
        } finally {
        	//释放锁
            lock.unlock();
        }
    }

  和添加元素同理,删除1个元素时,新数组的长度是在原有数组长度len的基础上-1。同理,往CopyOnWriteArrayList里添加一个元素数量为n的集合时,新数组的长度是len-n。

2.4 获取元素

  获取元素是读操作,不修改元素。由于读写分离,因此读数据不需要加锁。

    public E get(int index) {
        return get(getArray(), index);
    }

    private E get(Object[] a, int index) {
        return (E) a[index];
    }

2.5 遍历元素

  即迭代器的实现,CopyOnWriteArrayList中有好几种迭代器,以Iterator为例看下即可。

	//获取迭代器
    public Iterator<E> iterator() {
        return new COWIterator<E>(getArray(), 0);
    }

	static final class COWIterator<E> implements ListIterator<E> {
        //array的快照版本
        private final Object[] snapshot;
        //数组下标
        private int cursor;

        //省略...

        public boolean hasNext() {
            return cursor < snapshot.length;
        }

        public boolean hasPrevious() {
            return cursor > 0;
        }

        @SuppressWarnings("unchecked")
        public E next() {
            if (! hasNext())
                throw new NoSuchElementException();
            return (E) snapshot[cursor++];
        }

        @SuppressWarnings("unchecked")
        public E previous() {
            if (! hasPrevious())
                throw new NoSuchElementException();
            return (E) snapshot[--cursor];
        }

        public int nextIndex() {
            return cursor;
        }

        public int previousIndex() {
            return cursor-1;
        }

        public void remove() {
            throw new UnsupportedOperationException();
        }

        public void set(E e) {
            throw new UnsupportedOperationException();
        }

        public void add(E e) {
            throw new UnsupportedOperationException();
        }

		//省略...
    }

  从上面的迭代器代码,我们能看出CopyOnWriteArrayList遍历的两个特点:

  • 1、支持向前/向后2个方向遍历
      COWIterator中有next()、previous()这样获取后一个元素、前一个元素的方法,所以遍历是可以向前、也可以向后的。CopyOnWriteArrayList的底层实现是数组,可以向前向后遍历也正常。
  • 2、读时不能修改数组中的元素
      COWIterator中的remove、set、add方法都是直接抛出了UnsupportedOperationException,因此在迭代CopyOnWriteArrayList元素时,不能修改元素。
      CopyOnWriteArrayList是读写分离的,原数组用来读,新数组用来写。迭代时不能修改,也保证了数据的安全。

三、相关问题

3.1 CopyOnWriteArrayList的迭代器支持fast-fail吗*

  不支持。因为写时复制的存在,CopyOnWriteArrayList在使用迭代器迭代的时候,实际上迭代的是原数组,而不是更新的数组,所以中间如果发生新增、删除元素是在新数组上发生的,所以不影响旧数组的迭代,但是正因为元素的迭代发生在旧数组上,所以迭代时更新的数据是获取不到最新的。

【Vector】

一、Vector介绍

	public class Vector<E> extends AbstractList<E>
    	implements List<E>, RandomAccess, Cloneable, java.io.Serializable

  Vector与ArrayList很相似,也是基于数组来实现,与ArrayList相比,Vector是线程安全的(在每个方法上几乎都增加了synchronized关键字来实现线程间的同步,也正因为如此,Vector的效率会比较低)。可以简单地将Vector理解为线程安全版的ArrayList。

1.1 Vector的特点*

  • 1、底层实现与ArrayList相似
      Vector底层也是数组,所以也支持随机访问。
  • 2、线程安全
      Vector与ArrayList最大的不同就在于其是线程安全的,元素修改的方法都是用synchronized关键字修饰的。
  • 3、扩容时容量与ArrayList不同
      ArrayList不可以设置扩展的容量,默认1.5倍
      Vector可以设置扩展的容量,如果没有设置,默认2倍
  • 4、初始容量与ArrayList不同
      ArrayList的无参构造方法中初始容量为0(自动扩容时才扩容为10),而Vector的无参构造方法中初始容量为10。可以简单地理解为这两个容器的初始容量都是10。

1.2 Vector的使用

  • 1、构造方法
      在构造Vector对象时,可以指定初始容量initialCapacity和每次的增量capacityIncrement。当capacityIncrement小于等于0时,数组成倍扩容;当capacityIncrement大于0时,数组扩容时就增加相对的数值。
	//构造一个空Vector,其内部数据数组的大小为10 ,标准容量增量为0
	public Vector()
	//构造具有指定初始容量,并且其容量增量等于0的空Vector
	public Vector(int initialCapacity)
	//构造具有指定的初始容量和容量增量的空Vector
	public Vector(int initialCapacity, int capacityIncrement)
  • 2、添加元素
      添加的元素的方式和ArrayList类似,不过每个方法上多了synchronized ,保证线程安全。
	//将指定的元素追加到此Vector的末尾
	public synchronized boolean add(E e)
	//在此Vector中的指定位置插入指定的元素
	public void add(int index, E element)
	//将指定的元素添加到此向量的末尾
	public synchronized void addElement(E obj)
	//在指定的index位置插入指定元素
	public synchronized void insertElementAt(E obj, int index)
  • 3、返回此Vector的当前元素个数
	public synchronized int capacity()
  • 4、清空Vector
	public void clear()
  • 5、判断是否包含某元素
	public boolean contains(Object o)
  • 6、获取元素
	//返回指定位置的元素
	public synchronized E elementAt(int index)
	//返回此Vector的第一个元素
	public synchronized E firstElement()
	//返回此Vector中指定位置的元素
	public synchronized E get(int index)
	//获取Vector中的最后一个元素
	public synchronized E lastElement()
  • 7、比较两个Vector是否相等
	public synchronized boolean equals(Object o)
  • 8、支持for each循环
	public synchronized void forEach(Consumer<? super E> action)
  • 9、检索元素位置
	//返回此Vector中指定元素的第一次出现的索引,如果此向量不包含元素,则返回-1
	public int indexOf(Object o)
	//返回此Vector中指定元素的第一次出现的索引,从 index向前检索,如果未找到
	//该元素,则返回-1
	public synchronized int indexOf(Object o, int index)
	//返回此Vector中指定元素的最后一次出现的索引,如果此向量不包含元素,则返回-1
	public synchronized int lastIndexOf(Object o)
	//返回此Vector中指定元素的最后一次出现的索引,从 index开始 ,如果未找到
	//元素,则返回-1
	public synchronized int lastIndexOf(Object o, int index)
  • 10、Vector是否为空
	public synchronized boolean isEmpty()
  • 11、返回一个迭代器
	public synchronized Iterator< E > iterator()
  • 12、删除元素
	//删除此Vector中指定位置的元素
	public synchronized E remove(int index)
	//删除此Vector中第一个出现的指定元素,如果Vector不包含该元素,则不会更改
	public boolean remove(Object o)
	//删除指定索引处的元素
	public synchronized void removeElementAt(int index)
  • 13、替换元素
	//用指定的元素替换此Vector中指定位置的元素
	public synchronized E set(int index, E element)
	//用指定的元素替换此Vector中指定位置的元素
	public synchronized void setElementAt(E obj, int index)
  • 14、返回此Vector中的元素数
	public synchronized int size()

二、从源码理解Vector

  几个变量:

    //存放元素的数组
    protected Object[] elementData;
    //Vector的实际元素数量
	protected int elementCount;
	//需要扩容时的增量, 如果capacityIncrement小于或等于0,vector的容量
	//需要增长时将会成倍增长
	protected int capacityIncrement;

2.1 创建Vector对象

  在构造方法中,其实要做的就是确定初始容量和增长因子。初始容量默认为10,每次扩容默认增长一倍

  • 1、指定Vector的初始容量和增长容量的构造方法
    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、只指定Vector的初始容量、默认扩容时容量增长一倍的构造方法
    public Vector(int initialCapacity) {
        this(initialCapacity, 0);
    }
  • 3、默认容量为10、扩容时容量增长一倍的构造方法
    public Vector() {
        this(10);
    }

  如调用Vector()方法,此时会生成一个容量为10的Vector:

2.2 自动扩容*

  此处以调用addElement方法向Vector中添加元素为例:

    public synchronized void addElement(E obj) {
    	//modCount用于fail-fast机制
        modCount++;
        //判断在增加一个新的元素后,原数组是否需要扩容
        ensureCapacityHelper(elementCount + 1);
        elementData[elementCount++] = obj;
    }

    private void ensureCapacityHelper(int minCapacity) {
        //判断minCapacity是否大于当前容量,如果大于,就调用grow进行扩容
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }

    private void grow(int minCapacity) {
        // overflow-conscious code
        int oldCapacity = elementData.length;
        //判断capacityIncrement变量是否为0,如果不为0,每次就增加相应的容量;
        //为0的话就扩容一倍
        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 final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

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

  此处的扩容实现过程与ArrayList大致相似,不同的地方在于判断capacityIncrement变量是否为0,如果不为0,每次就增加相应地的容量;为0的话就扩容一倍。而ArrayList是1.5倍扩容。

2.3 添加元素

  添加元素的大致逻辑为:先确定要不要扩容,需要的话就扩容,然后再添加元素。

  • 1、添加元素到数组尾部
    public synchronized boolean add(E e) {
        modCount++;
        //先检验是否需要扩容
        ensureCapacityHelper(elementCount + 1);
        //将元素追加至数组末尾
        elementData[elementCount++] = e;
        return true;
    }

  这个方法的实现,除了加了一个synchronized关键字,别的和ArrayList一模一样,图示:

  如果此时再调用add方法先后添加2、3两个元素后,Vector会变成:

  • 2、添加元素到数组的指定位置
    public void add(int index, E element) {
        insertElementAt(element, index);
    }

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

  假设调用add(1,4)方法,就会在下标索引为1的位置添加元素4。这个过程分为以下几步:

  • 3、在数组尾部添加元素
    public synchronized void addElement(E obj) {
        modCount++;
        //检验是否需要扩容
        ensureCapacityHelper(elementCount + 1);
        //将元素追加至数组末尾
        elementData[elementCount++] = obj;
    }
  • 4、将指定集合的元素添加到数组尾部
    public synchronized boolean addAll(Collection<? extends E> c) {
        modCount++;
        //将集合转换为数组
        Object[] a = c.toArray();
        int numNew = a.length;
        //检验是否需要扩容
        ensureCapacityHelper(elementCount + numNew);
        //接着将元素拷贝到Vector中
        System.arraycopy(a, 0, elementData, elementCount, numNew);
        elementCount += numNew;
        return numNew != 0;
    }
  • 5、将指定集合的元素添加到数组的指定位置
    public synchronized boolean addAll(int index, Collection<? extends E> c) {
        modCount++;
        //检验位置参数是否合法
        if (index < 0 || index > elementCount)
            throw new ArrayIndexOutOfBoundsException(index);
		//将集合元素插入指定数组中
        Object[] a = c.toArray();
        int numNew = a.length;
        //判断是否需要扩容
        ensureCapacityHelper(elementCount + numNew);
		//判断要插入的位置是否小于元素数量,如果小于,则需要将Vector中从插入元素位置后面的元素向后移动
        int numMoved = elementCount - index;
        if (numMoved > 0)
            System.arraycopy(elementData, index, elementData, index + numNew,
                             numMoved);
		//将集合转化成的数组中的元素全部插入到Vector对应的数组中
        System.arraycopy(a, 0, elementData, index, numNew);
        elementCount += numNew;
        return numNew != 0;
    }

  addAll(Collection<? extends E> c)addAll(int index, Collection<? extends E> c)方法的作用是相似的,都是将一个集合中的元素添加到数组中,差异之处是前者是在数组末尾插入,后者是在指定位置插入。接下来以addAll(int index, Collection<? extends E> c)为例,演示一下添加一个集合的过程:

2.4 删除元素

  删除元素的逻辑就是删指定的(一个或多个)元素,然后将空出来的位置置为null,便于GC。

  • 1、删除指定位置的元素
    public synchronized E remove(int index) {
        modCount++;
        //检验位置参数合法性
        if (index >= elementCount)
            throw new ArrayIndexOutOfBoundsException(index);
        //取出该位置元素,用于返回
        E oldValue = elementData(index);
		//判断要删除的元素是不是最后一个位置的元素
        int numMoved = elementCount - index - 1;
        //如果不是最后一个元素,将删除元素位置后面的元素向前移动一位
        if (numMoved > 0)
            System.arraycopy(elementData, index+1, elementData, index,
                             numMoved);
        //数组最后一个元素置为null
        elementData[--elementCount] = null; // Let gc do its work

        return oldValue;
    }
  • 2、删除数组中第一次出现的某个元素
    public boolean remove(Object o) {
        return removeElement(o);
    }

    public synchronized boolean removeElement(Object obj) {
        modCount++;
        //如果能检索出指定元素的位置,就删除该元素
        int i = indexOf(obj);
        if (i >= 0) {
            removeElementAt(i);
            return true;
        }
        return false;
    }

    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--;
        //数组最后一个元素置为null
        elementData[elementCount] = null; /* to let gc do its work */
    }

  Vector删除单个元素和ArrayList删除单个元素的过程相似。remove(Object o)remove(int index)大致思路相似,先找到index,再删除对应位置上的元素,图示:

  • 3、清空数组
    public void clear() {
        removeAllElements();
    }

    public synchronized void removeAllElements() {
        modCount++;
        //所有元素置为null
        for (int i = 0; i < elementCount; i++)
            elementData[i] = null;

        elementCount = 0;
    }

2.5 更新元素

  • 1、替换指定位置的元素,并将被替换的元素返回
    public synchronized E set(int index, E element) {
    	//检验位置参数合法性
        if (index >= elementCount)
            throw new ArrayIndexOutOfBoundsException(index);
		//获取该位置现有元素,替换新元素,返回旧元素
        E oldValue = elementData(index);
        elementData[index] = element;
        return oldValue;
    }
  • 2、替换指定位置的元素
	//与上个方法相比,差异是不再将旧元素返回
    public synchronized void setElementAt(E obj, int index) {
        if (index >= elementCount) {
            throw new ArrayIndexOutOfBoundsException(index + " >= " +
                                                     elementCount);
        }
        elementData[index] = obj;
    }

2.6 获取元素

  Vector中获取元素的方法有三个:获取某个位置、首部和尾部的元素。这也是底层是数组实现的集合类的常见操作。

  • 1、获取数组任意位置上的元素
    public synchronized E get(int index) {
        if (index >= elementCount)
            throw new ArrayIndexOutOfBoundsException(index);

        return elementData(index);
    }

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

        return elementData(index);
    }

    E elementData(int index) {
        return (E) elementData[index];
    }

  get(index)和elementAt(int index)都是获取某位置元素的方法。

  • 2、获取数组第一个元素
    public synchronized E firstElement() {
        if (elementCount == 0) {
            throw new NoSuchElementException();
        }
        //获取第一个元素
        return elementData(0);
    }
  • 3、获取数组最后一个元素
    public synchronized E lastElement() {
        if (elementCount == 0) {
            throw new NoSuchElementException();
        }
        //获取最后一个元素
        return elementData(elementCount - 1);
    }

2.7 检索元素

  Vector中检索元素的方法也是大致分为两大类:从前向后检索和从后向前检索。

  • 1、从头开始检索元素
      从头部开始,从前向后获取元素出现的第一个位置。
    public int indexOf(Object o) {
    	//从头开始检索某个元素
        return indexOf(o, 0);
    }
  • 2、indexOf(Object o, int index)
      从指定位置开始,从前向后获取元素出现的第一个位置。
    public synchronized int indexOf(Object o, int index) {
    	//先判断要检索的元素是否为null
    	//检索null
        if (o == null) {
            for (int i = index ; i < elementCount ; i++)
                if (elementData[i]==null)
                    return i;
        //检索指定元素
        } else {
            for (int i = index ; i < elementCount ; i++)
                if (o.equals(elementData[i]))
                    return i;
        }
        return -1;
    }
  • 3、lastIndexOf(Object o)
      从尾部开始,从后向前获取元素出现的最后一个位置。
    public synchronized int lastIndexOf(Object o) {
    	//从末尾开始,从后向前检索
        return lastIndexOf(o, elementCount-1);
    }
  • 4、lastIndexOf(Object o, int index)
      从指定位置开始,从后向前获取元素出现的最后一个位置。
    public synchronized int lastIndexOf(Object o, int index) {
    	//检索index合法性
        if (index >= elementCount)
            throw new IndexOutOfBoundsException(index + " >= "+ elementCount);
		//判断要检索的元素是否为null
		//检索null
        if (o == null) {
            for (int i = index; i >= 0; i--)
                if (elementData[i]==null)
                    return i;
        //检索指定元素
        } else {
            for (int i = index; i >= 0; i--)
                if (o.equals(elementData[i]))
                    return i;
        }
        return -1;
    }

【Stack】

一、Stack介绍

  Stack是栈,是Vector的“窄化”实现。在栈中,可以操作元素的一段叫栈顶(数组尾部),数组的另一端是栈底(数组首部),图示:
  在栈中可以进行的操作是入栈和出栈。图示:

1.1 Stack的特点*

  Stack的特点,可以简单归结为两点:

  • 1、在底层数组的一端(栈顶)进行存储/取出元素
      普通的数组,一般都可以从两端进行存储和取出元素的操作,但Stack作为Vector的“窄化”实现,严格规定只能在数组的一端进行操作。
  • 2、先进后出(LIFO)
      Stack中存储的元素,都是后进先出(LIFO,最后入栈的元素最先出栈),因为先存入Stack中的元素存入了Stack底部,在取出该元素时自然是最后一个取出的。
  • 3、线程安全
      Stack继承于Vector,所以也是线程安全的。

1.2 Stack的使用

  Stack继承了Vector,所以Vector的方法Stack都有。同时,由于Stack具有先进后出的特点,所以多了一些方法。

  • 1、创建一个空栈
	public Stack()
  • 2、此栈是否为空
	public boolean empty()
  • 3、查看栈顶的对象,而不从栈中删除
	public synchronized E peek()
  • 4、删除并返回此堆栈顶部的对象
	public synchronized E pop()
  • 5、将元素存入到栈的顶部
	public E push(E item)
  • 6、返回一个对象在此栈上的基于栈顶的相对位置
	public synchronized int search(Object o)

  这个方法优点特殊,看个示例:

		Stack<String> stack = new Stack<String>();
        stack.push("A");
        stack.push("B");
        stack.push("C");

        int index = stack.search("A");
        if(index != -1) {
            System.out.println("元素A在Stack中的位置是:" + index);
        }
        else {
            System.out.println("元素A不在Stack中");
        }

  运行结果:

元素A在Stack中的位置是:3

二、从源码理解Stack

2.1 创建对象

    public Stack() {
    	//由于在子类的构造方法中默认有一条隐式的语句super(),所以此处是先调用其父类
    	//无参构造方法,因此Stack的初始化流程可以理解为和Vector的无参构造过程一致
    }

2.2 添加(栈顶)元素

	//将元素放置到栈顶
    public E push(E item) {
    	//调用其父类的addElement方法
        addElement(item);
        return item;
    }

	//该方法的实现与Vector的方法相同,不过是线程安全的,因为方法用synchronized修饰
    public synchronized void addElement(E obj) {
        modCount++;
        ensureCapacityHelper(elementCount + 1);
        elementData[elementCount++] = obj;
    }

  从addElement方法可以看出,栈底是数组的首部,每次向栈中添加元素时,都是在数组后面追加。

2.3 删除(栈顶)元素

    public synchronized E pop() {
        E obj;
        int len = size();
		//peek是获取栈顶元素的方法
        obj = peek();
        //调用其父类Vector中的方法,作用是根据索引删除数组中的一个元素
        removeElementAt(len - 1);

        return obj;
    }
	//Stack父类Vector中的removeElementAt方法,是线程安全的
	public synchronized void removeElementAt(int index)

  删除栈顶元素,其实就是删除数组尾部的元素

2.4 获取(栈顶)元素

    public synchronized E peek() {
        int     len = size();

        if (len == 0)
            throw new EmptyStackException();
        //调用了其父类中的elementAt方法
        return elementAt(len - 1);
    }
	//Stack父类Vector中的方法,是线程安全的
	public synchronized E elementAt(int index)

  获取栈顶元素,其实就是获取数组尾部的元素

2.5 判断Stack是否为空

    public boolean empty() {
    	//调用了父类的size方法
        return size() == 0;
    }
	//Stack父类Vector中的方法,是线程安全的
	public synchronized int size()

2.6 获取元素相对于栈顶的位置*

	//某个元素在栈中相对于栈顶(数组尾部)位置的方法
    public synchronized int search(Object o) {
    	//栈中的栈顶元素是最后添加的,而search方法要返回某个元素相对于栈顶元素
    	//的位置,所以最后要返回size()-i
        int i = lastIndexOf(o);

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

  这个方法比较特殊,图示:

  像上图中的元素“3”,相对于栈顶的位置是1,因此用search检索,返回值就是1。

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值