Java集合学习笔记(二)—— ArrayList源码学习(jdk1.8)

ArrayList是什么

集合==》List接口 ==》List中常用的两个实现类ArrayList、LinkedList;
在这里插入图片描述
ArrayList是集合的一种实现,实现了接口List,List接口继承了Collection接口。Collection是所有集合类的父类。ArrayList就是一个以动态数组形式实现的集合类,从数据结构上来讲,是数组实现的线性表(即顺序表)。所以优势是随机访问元素,但是在list中间插入和移动元素时较慢。特别是插入效率。

ArrayList的继承关系

ArrayList和Collection的关系
ArrayList继承AbstractList抽象父类,实现了List接口(规定了List的操作规范)、RandomAccess(可随机访问)、Cloneable(可拷贝)、Serializable(可序列化)。

数据结构

ArrayList的数据结构是:
ArrayList的数据结构
底层的数据结构就是数组,数组元素类型为Object类型,即可以存放所有类型数据。

我们对ArrayList类的实例的所有的操作底层都是基于数组的。

特点

  • 容量不固定,想放多少放多少(当size大于capacity的时候,自动进行1.5倍扩容;添加的时候,容量加一)
  • 有序的(元素输出顺序与输入顺序一致)
  • 元素可以为 null
  • 元素不唯一(可重复)
  • 不同步,与之对应的是,vector是同步的
  • 可以通过索引直接操作元素,而set不行

ArrayList的线程安全性【未完】

为什么说ArrayList是线程不安全的?
ArrayList 线程安全问题

对ArrayList进行添加元素的操作的时候是分两个步骤进行的,即第一步先在object[size]的位置上存放需要添加的元素;第二步将size的值增加1。由于这个过程在多线程的环境下是不能保证具有原子性的,因此ArrayList在多线程的环境下是线程不安全的。

具体举例说明:在单线程运行的情况下,如果Size = 0,添加一个元素后,此元素在位置 0,而且Size=1;而如果是在多线程情况下,比如有两个线程,线程 A 先将元素存放在位置0。但是此时 CPU 调度线程A暂停,线程 B 得到运行的机会。线程B也向此ArrayList 添加元素,因为此时 Size 仍然等于 0 (注意哦,我们假设的是添加一个元素是要两个步骤哦,而线程A仅仅完成了步骤1),所以线程B也将元素存放在位置0。然后线程A和线程B都继续运行,都增 加 Size 的值。 那好,现在我们来看看 ArrayList 的情况,元素实际上只有一个,存放在位置 0,而Size却等于 2。这就是“线程不安全”了。

如果非要在多线程的环境下使用ArrayList,就需要保证它的线程安全性,通常有两种解决办法:第一,使用synchronized关键字;第二,可以用Collections类中的静态方法synchronizedList();对ArrayList进行调用即可。

主要成员变量

  • capacity:数组容量(长度),默认是10
  • size:当前元素个数

由于要维护数组的size,即动态数组的实现就是扩容时将原数组的元素复制到更大数组中

所以在此之前补充下复制数组元素函数,java中的四种拷贝方式分别是: 循环赋值,System.arraycopy(), Arrays.copyOf()(或者Arrays.copyOfRange)和clone()方法。具体四种方式分析: 学习笔记之 —— java数组的四种拷贝方式

本篇笔记主要涉及System.arraycopy() 和Arrays.copyOf(),其中

System.arraycopy需要考虑自身复制重叠的情况,根据from>to,来判读是向前拷贝还是向后拷贝。
Arrays.copyOf() 源码可见其调用的是 System.arraycopy()

public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
    // 版本号
    private static final long serialVersionUID = 8683452581122892189L;
    // 缺省容量,当ArrayList的构造方法中没有显示指出ArrayList的数组长度时,类内部使用默认缺省时对象数组的容量大小,为10。
    private static final int DEFAULT_CAPACITY = 10;
    // 空对象数组,当ArrayList的构造方法中显示指出ArrayList的数组长度为0时,类内部将EMPTY_ELEMENTDATA 这个空对象数组赋给elemetData数组。
    private static final Object[] EMPTY_ELEMENTDATA = {};
    // 缺省空对象数组,当ArrayList的构造方法中没有显示指出ArrayList的数组长度时,类内部使用默认缺省时对象数组为DEFAULTCAPACITY_EMPTY_ELEMENTDATA。
    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
    // 元素数组,ArrayList的底层数据结构,只是一个对象数组,用于存放实际元素,并且被标记为transient,也就意味着在序列化的时候此字段是不会被序列化的。
    transient Object[] elementData;
    // 实际ArrayList中存放的元素的个数,默认时为0个元素。
    private int size;
    // ArrayList中的对象数组的最大数组容量为Integer.MAX_VALUE – 8。
    private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
}

方法

1.构造方法

  • 初始为空数组public ArrayList()
  • 根据指定容量,创建个对象数组public ArrayList(int initialCapacity)
  • 直接创建和指定集合一样内容的 ArrayList public ArrayList(Collection<? extends E> c)

具体分为三种构造方法:
在这里插入图片描述
1)无参构造方法

	/**
    * Constructs an empty list with an initial capacity of ten.构造一个初始容量为10的空列表。
    */
   public ArrayList() {
       //调用父类中的无参构造方法,父类中的是个空的构造方法
       super();
       //DEFAULTCAPACITY_EMPTY_ELEMENTDATA 为默认容量大小为10的空的Object[]类型元素数据
       this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
   }

2)有参构造函数

 /**
  * 构造一个具有指定初始容量的空列表。
  * @param  initialCapacity  为列表的初始容量
  * @throws IllegalArgumentException 如果指定的初始容量为负
  */
 public ArrayList(int initialCapacity) {
	     super();      
	    
	     if (initialCapacity > 0) { //判断自定义大小的容量情况,> 0正常创建,容量大小为initialCapacity
	         this.elementData = new Object[initialCapacity];
	     } else if (initialCapacity == 0) { //== 0直接赋值EMPT_ELEMENTDATA
	         this.elementData = EMPTY_ELEMENTDATA;
	     } else { //< 0抛出异常
	         throw new IllegalArgumentException("Illegal Capacity: "+
	                                            initialCapacity);
	     }
 }

3)有参构造方法(不常用):指定 collection 列表

public ArrayList(Collection<? extends E> c) {
        elementData = c.toArray(); //c转换为数组,并赋值给elementData元素数组
        if ((size = elementData.length) != 0) { //c中是否有内容            
            if (elementData.getClass() != Object[].class) // 每个集合的toarray()的实现方法不一样,所以需要判断是否为Object[].class类型,
                elementData = Arrays.copyOf(elementData, size, Object[].class); //把elementData改为Object[].class类型
        } else { // c中若无内容,用空数组替换。
            this.elementData = EMPTY_ELEMENTDATA;
        }
    }
}

扩容

简单来说,就是将数组扩容到渴望的数组大小minCapacity(所需的最小容量)

1、首先从ensureCapacityInternal(int minCapacity)开始,确保内部容量:

    private void ensureCapacityInternal(int minCapacity) {
        ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
    }

分别用到calculateCapacity(elementData, minCapacity)ensureExplicitCapacity(int minCapacity) 方法,分别展开:

2、调用calculateCapacity(elementData, minCapacity),即:

    private static int calculateCapacity(Object[] elementData, int minCapacity) {
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { //若数组是无参初始化状态
            return Math.max(DEFAULT_CAPACITY, minCapacity); //从10开始扩容,返回理想最小值和默认值中最大的为新的理想最小值
        }
        return minCapacity;
    }

3、调用ensureExplicitCapacity(int minCapacity),即确保准确的容量:

    private void ensureExplicitCapacity(int minCapacity) {
        modCount++; //已从结构上修改此列表的次数++。结构修改是那些改变列表大小的修改,或者以这样一种方式扰乱列表,使得正在进行的迭代可能产生不正确的结果。

        // overflow-conscious code 溢出的代码
        if (minCapacity - elementData.length > 0)//检测扩容容量minCapacity要大于elementData.length,若大于则需要扩容
            grow(minCapacity); 
    }

4、ensureExplicitCapacity中调用了grow(int minCapacity):开始扩容(最关键的部分):

  1. 每次从 1.5*oldCapacity 扩容;
  2. 若minCapacity大于1.5*oldCapacity,则选minCapacity;
  3. 若扩容超过MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8MAX_ARRAY_SIZE:要分配的数组的最大大小。一些vm在一个数组中保留一些标题词。尝试分配更大的数组可能会导致OutOfMemoryError:请求的数组大小超过VM限制),
    则调用hugeCapacity(int minCapacity)选择,最大限制的容量minCapacity > MAX_ARRAY_SIZE) ? Integer.MAX_VALUE : MAX_ARRAY_SIZE;
  4. 最后elementData = Arrays.copyOf(elementData, newCapacity);复制数组到扩容后的新数组。
    /**
     * 增加容量,以确保它至少可以容纳最小容量参数指定的元素数量。
     *
     * @param minCapacity 所需的最小容量
     */
    private void grow(int minCapacity) {
        // overflow-conscious code
        int oldCapacity = elementData.length; //扩容前elementData的大小
        //扩容1.5倍是因为:(oldCapacity + (oldCapacity >> 1))约是oldCapacity 的1.5倍。>>后约为原先的0.5倍。
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity; //这句话就是适应elementData空数组的时候,length=0,那么oldCapacity=0,newCapacity=0,所以这个判断成立,在这里就是真正的初始化elementData的大小了,前面的工作都是准备工作。
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);
        //minCapacity通常接近于size,所以这是一个优势:
        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.增 add()

add()方法
add()方法
1)boolean add(E); //默认直接在末尾添加元素

   /**
     * 将指定的元素追加到此列表的末尾。
     * @param e 元素添加到此列表中
     * @return boolean (as specified by {@link Collection#add})
     */
    public boolean add(E e) {
        ensureCapacityInternal(size + 1);  // 确定内部容量是否够了,size是数组中数据的个数,添加一个元素size+1。
        
        elementData[size++] = e; //列表的末尾加上e
        return true;
    }

    private void ensureCapacityInternal(int minCapacity) {
        ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
    }

    private void ensureExplicitCapacity(int minCapacity) {
        modCount++;

        // overflow-conscious code
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }

2)void add(int index, E element) //在列表的指定位置插入元素

	/**
	 * 将指定的元素追加到此列表指定位置
	 * @param index
	 * @param element
	 */
    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++;
    }

3)boolean addAll(Collection<? extends E> c) //将集合c的所有元素添加到列表中

	/**
	 * 往指定列表插入集合c的所有元素
	 * @param c
	 * @return
	 */
	public boolean addAll(Collection<? extends E> c) {
        Object[] a = c.toArray(); //c转换为数组,并赋值给object型数组a
        int numNew = a.length;
        ensureCapacityInternal(size + numNew);  // Increments modCount
        System.arraycopy(a, 0, elementData, size, numNew);
        size += numNew;
        return numNew != 0; //集合c中无数据,add失败
    }

4)boolean addAll(int index, Collection<? extends E> c) //从指定的位置开始,将指定 collection 中的所有元素插入到此列表中。

    /**
     * 从指定位置开始,将指定集合中的所有元素插入到此列表中。
     * 将当前位于该位置的元素(如果有的话)和任何后续元素向右移动(增加它们的索引)。新元素将按照指定集合的迭代器返回它们的顺序出现在列表中。
     * 
     * @param index 从指定集合中插入第一个元素的索引
     * @param c collection,包含要添加到此列表的元素
     * @return <tt>true</tt>如果这个列表由于调用而改变
     * @throws IndexOutOfBoundsException {@inheritDoc}
     * @throws NullPointerException 如果指定的集合为空
     */
    public boolean addAll(int index, Collection<? extends E> c) {
        rangeCheckForAdd(index); //判断index是否在(0,size)之间

        Object[] a = c.toArray(); //c转换为数组,并赋值给object型数组a
        int numNew = a.length;
        ensureCapacityInternal(size + numNew);  // Increments modCount

        //ArrayList的大小(它包含的元素的数量)。
        int numMoved = size - index; //需要后移的元素数量
        if (numMoved > 0) //size可能会等于index
            System.arraycopy(elementData, index, elementData, index + numNew, numMoved); //将插入新元素导致的原列表需要改动的元素后移

        System.arraycopy(a, 0, elementData, index, numNew); //复制新数组到原数组中
        size += numNew; //size加上新集合的大小
        return numNew != 0;
    }
    
    /**
     * 由add和addAll使用的rangeCheck的一个版本。
     */
    private void rangeCheckForAdd(int index) {
        if (index > size || index < 0)
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
    }
     
     /*
     * 构造一个IndexOutOfBoundsException详细信息。在错误处理代码的许多可能重构中,这种“大纲”在服务器和客户端vm上执行得最好。
     */
    private String outOfBoundsMsg(int index) {
        return "Index: "+index+", Size: "+size;
    }

3.删remove()

remove()方法在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
1)remove(int index): 通过删除指定位置上的元素

    /**
     * 移除列表中指定位置的元素。
     * 将任何后续元素向左移动(从它们的索引中减去1)。
     *
     * @param index 要删除的元素的索引
     * @return 从列表中删除的元素
     * @throws IndexOutOfBoundsException {@inheritDoc}
     */
	 public E remove(int index) {	       
	       rangeCheck(index); // 检查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; // 清除,将--size上的位置赋值为null,让gc(垃圾回收机制)更快的回收它
		
	       return oldValue;// 返回删除的元素
	   }

  1. remove(Object o):这个方法可以看出来,arrayList是可以存放null值的

    /**
     * 通过元素来删除该元素,就依次遍历,如果有这个元素,就将该元素的索引传给fastRemove(index),使用这个方法来删除该元素
     * arrayList可以存储null值
     *
     * @param o 元素将从此列表中删除(如果存在)
     * @return <tt>true</tt> 如果此列表包含指定的元素
     */
	  public boolean remove(Object o) {
	       if (o == null) {
	           for (int index = 0; index < size; index++)
	               if (elementData[index] == null) {
	                   fastRemove(index);
	                   return true;
	               }
	       } else {
	           for (int index = 0; index < size; index++)
	               if (o.equals(elementData[index])) {
	                   fastRemove(index);
	                   return true;
	               }
	       }
	       return false;
	   }

  1. removeAll(Collection<?> c)

    public boolean removeAll(Collection<?> c) {        
        Objects.requireNonNull(c);//如果C为空,抛出空指针异常
        return batchRemove(c, false); // 批量删除

    }

  1. clear():将elementData中每个元素都赋值为null,等待垃圾回收将这个给回收掉,所以叫clear

    public void clear() {
        modCount++;

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

        size = 0;
    }

  1. batchRemove(Collection<?> c, boolean complement):用于两个方法
  • removeAll():它只清除指定集合中的元素,当complement为false
  • retainAll():用来测试两个集合是否有交集,当complement为true
    /**
	*仅保留此列表中包含在指定集合中的元素。换句话说,从这个列表中删除指定集合中不包含的所有元素。
	*/
    public boolean retainAll(Collection<?> c) {
        Objects.requireNonNull(c);
        return batchRemove(c, true);
    }
	/**
	*这里先假设elementData(ArryList内部维护的一个Object[]数组)和集合c的交集是A:
	* 当 complement == true 时,elementData最终只会存储A
	* 当 complement == false 时,elementData最终删除A。
	* 
	* 在对elementData的元素进行筛选的时候,这里使用了r、w两个游标, 从而避免从新开辟一个新的数组进行存储。
	*/
	private boolean batchRemove(Collection<?> c, boolean complement) {	    	
	        final Object[] elementData = this.elementData; // 将原集合元素数据放入对象数组elementData中    	
	        int r = 0, w = 0; // r 控制循环,w 记录有多少个交集
	        boolean modified = false; //modified 是否已修改
	        try {
	            for (; r < size; r++)     
	                //当调用removeAll(Collection<?> c)时,complement == false,此时elementData数组中存储的是去掉A的部分           
	                if (c.contains(elementData[r]) == complement) 
	                    elementData[w++] = elementData[r];  // 有的话,就给集合A
	        } finally {
	            // 即使c.contains()抛出,也要保持与AbstractCollection的行为兼容性。	            
	            if (r != size) { //出现异常会导致 r !=size , 则将出现异常处后面的数据全部复制覆盖到数组里。	                
	                System.arraycopy(elementData, r, elementData, w,size - r);// 将剩下的元素都赋值给集合A
	                w += size - r;
	            }
	            if (w != size) {
	                // 这里有两个用途,在removeAll()时,w一直为0,就直接跟clear一样,全是null
	                // retainAll():没有一个交集返回true,有交集但不全交也返回true,有两个集合相等的时候,返回false,所以不能根据返回值来确认两个集合是否有交集,而是通过原集合的大小是否发生改变来判断,如果原集合中还有元素,则代表有交集,而元集合没有元素了,说明两个集合没有交集。
	                // clear to let GC do its work
	                for (int i = w; i < size; i++)
	                    elementData[i] = null;
	                modCount += size - w;
	                size = w;
	                modified = true;
	            }
	        }
	        return modified;
	    }
// r == size: 注意到只有 elementData和都为空或者c的元素都不在elementData中的情况下,才会出现 w == size,因为都为空或者没有相同元素,所以不存在删除的情况。

总结:remove函数用户移除指定下标的元素,此时会指定下标到数组末尾的元素向前移动一个单位。,并且会把数组最后一个元素设置为null,这样是为了方便之后将整个数组不被使用时,会被GC,可以作为小的技巧使用。

4.改 set()方法

	public E set(int index, E element) {       
        rangeCheck(index); // 检验索引是否合法
		
        E oldValue = elementData(index); // 旧值        
        elementData[index] = element; // 赋新值        
        return oldValue; // 返回旧值
    }

说明:设定指定下标索引的元素值

5.查 indexOf()方法、get()方法

	/**
	*返回此列表中指定元素的第一个匹配项的索引,如果此列表不包含该元素,则返回-1。
	*/
	public int indexOf(Object o) {        
        if (o == null) { // 查找的元素为空
            for (int i = 0; i < size; i++) // 遍历数组,找到第一个为空的元素,返回下标
                if (elementData[i]==null)
                    return i;            
        } else { // 查找的元素不为空           
            for (int i = 0; i < size; i++)  // 遍历数组,找到第一个和指定元素相等的元素,返回下标
                if (o.equals(elementData[i]))
                    return i;
        }        
        return -1; // 没有找到,返回空
    }

说明:从头开始查找与指定元素相等的元素,注意,是可以查找null元素的,意味着ArrayList中可以存放null元素的。与此函数对应的lastIndexOf,表示从尾部开始查找。

    public E get(int index) {       
        rangeCheck(index);  //检验索引是否合法

        return elementData(index);
    }

说明:get函数会检查索引值是否合法(只检查是否大于size,而没有检查是否小于0),值得注意的是,在get函数中存在element函数,element函数用于返回具体的元素,具体函数如下:

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

说明:返回的值经过了向下转型(Object -> E),这些是对我们应用程序屏蔽的小细节

6.其他方法

size() : 获取集合长度,通过定义在ArrayList中的私有变量size得到

isEmpty():是否为空,通过定义在ArrayList中的私有变量size得到

contains(Object o):是否包含某个元素,通过遍历底层数组elementData,通过equals或==进行判断

clear():集合清空,通过遍历底层数组elementData,设置为null

转化成数组

// 返回ArrayList的Object数组 
 public Object[] toArray() {
        return Arrays.copyOf(elementData, size);
    }


 // 返回ArrayList元素组成的数组
    @SuppressWarnings("unchecked")
    public <T> T[] toArray(T[] a) {
         // 若数组a的大小 < ArrayList的元素个数;
         // 则新建一个T[]数组,数组大小是“ArrayList的元素个数”,并将“ArrayList”全部拷贝到新数组中
        if (a.length < size)
            // Make a new array of a's runtime type, but my contents:
            return (T[]) Arrays.copyOf(elementData, size, a.getClass());
        
        // 若数组a的大小 >= ArrayList的元素个数;  
        // 则将ArrayList的全部元素都拷贝到数组a中
        System.arraycopy(elementData, 0, a, 0, size);
        if (a.length > size)
            a[size] = null;
        return a;
    }

迭代器实现【未完成】

ArrayList为什么要自己实现迭代器
arraylist怎么保证迭代不改变元素 乐观锁

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值