ArrayList笔记

本文详细解析了ArrayList的实现原理,包括动态数组的扩容机制、元素的添加与删除操作,以及遍历过程中删除元素的注意事项。对比了ArrayList与LinkedList的性能差异,并讨论了线程安全性问题。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一、实现原理

ArrayList底层是通过数组实现的,元素是存在其成员变量的transient Object[] elementData;里面的。在增加元素的时候会自动为我们扩容。也可以称之为动态数组。

常用构造方法:

public ArrayList() 构造方法为我们创建了一个长度为0的空数组。最长用的构造方法。

public ArrayList(int initialCapacity) 为我们创建一个长度为initialCapacity的数组。其实我们在使用过程中如果知道列表的规模的话,最好使用此构造方法。尽量降低列表扩容的次数。

二、新增元素

最常用的方法 add(E e)

    //新增元素
    public boolean add(E e) {
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        elementData[size++] = e;
        return true;
    }
   //检查容量大小 
   private void ensureCapacityInternal(int minCapacity) {
        ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
    }
    //计算需要当前所需容量是否大于默认容量 10
    private static int calculateCapacity(Object[] elementData, int minCapacity) {
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            return Math.max(DEFAULT_CAPACITY, minCapacity);
        }
        return minCapacity;
    }
    //比较当前所需容量是否大于数组长度
    private void ensureExplicitCapacity(int minCapacity) {
        modCount++;

        // 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 + (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);
    }
  1. 首先检查当前数组是否为默认长度为0的空数组,计算本次操作所需容量
  2. 比较本次操作所需容量是否大于数组长度。如果大于的话 则需要对数组扩容。
  3. 计算扩容后的数组长度 等于原来数组长度的1.5倍。
  4. 将原始的数组 拷贝到新建的数组里面。
  5. 列表当前的长度加1,并且将本次添加的元素 放到数组的最后面。

三、删除元素 remove(Object o)

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

  private void fastRemove(int index) {
        modCount++;
        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
    }
  1. 判断当前删除的元素是否为null。因为集合类判断元素是否存在,或者删除元素 都是通过equals来判断的。如果为null的话 则使用==判断。
  2. 找到删除元素所在的下标。
  3. 将数组元素 往前移位。
  4. 将列表的长度建减1 

四、列表遍历时删除元素:

package com.winston.javase.testcollections;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

public class TestIterator {

	public static void main (String[] args) {
		
		testRemove1();
		System.out.println();
		testRemove2();
		
	}
	
	private static void testRemove1 () {
		List<String> lists = new ArrayList<String>();
		lists.add("data1");
		lists.add("data2");
		lists.add("data3");
		lists.add("data4");
		
		System.out.println(lists);
		
		Iterator<String> it = lists.iterator();
			while (it.hasNext()) {
				String tmp = it.next();
				if ("data3".equals(tmp)) {
					lists.remove(tmp);
				}
			}
		System.out.println(lists);
	}
	
	
	private static void testRemove2 () {
		List<String> lists = new ArrayList<String>();
		lists.add("data1");
		lists.add("data2");
		lists.add("data3");
		lists.add("data4");
		
		System.out.println(lists);
		
		Iterator<String> it = lists.iterator();
			while (it.hasNext()) {
				String tmp = it.next();
				if ("data2".equals(tmp)) {
					lists.remove(tmp);
				}
			}
		System.out.println(lists);
	}
	
}

 输出结果:

[data1, data2, data3, data4]
[data1, data2, data4]

[data1, data2, data3, data4]
Exception in thread "main" java.util.ConcurrentModificationException
	at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:909)
	at java.util.ArrayList$Itr.next(ArrayList.java:859)
	at com.winston.javase.testcollections.TestIterator.testRemove2(TestIterator.java:48)
	at com.winston.javase.testcollections.TestIterator.main(TestIterator.java:13)

我们都知道使用Iterator 遍历ArrayList的时候不能删除、修改元素。

testRemove2也按照我们预期的抛出了异常java.util.ConcurrentModificationException。但是为什么,testRemove1()就正常删除了呢。

查看源码:


       //以下仅仅是Itr 的部分代码
       private class Itr implements Iterator<E> {
            //记录下一个元素的下标 从0开始
            int cursor;       // index of next element to return
            int lastRet = -1; // index of last element returned; -1 if no such
            //记录ArrayList被修改的次数。
            int expectedModCount = modCount;
        
             @SuppressWarnings("unchecked")
            public E next() {
                checkForComodification();
                int i = cursor;
                if (i >= size)
                    throw new NoSuchElementException();
                Object[] elementData = ArrayList.this.elementData;
                if (i >= elementData.length)
                    throw new ConcurrentModificationException();
                cursor = i + 1;
                return (E) elementData[lastRet = i];
            }

            public boolean hasNext() {
                return cursor != size;
            }

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

 

根据源码我们可以看到Itr的成员变量expectedModCount被赋值为ArrayList的modCount(该值为记录ArrayList的变更次数。可以从add()remove()等方法中看到ArrayList的每次变更modCount都会自增1),所以当我们遍历过程中 删除元素的话  会导致modCount 加1。即在调用next()方法的时候会调用checkForComodification判断modCount != expectedModCount,所以会抛出异常。

在来看testRemove1()方法不抛出异常的原因。首先看到hasNext()方法。cursor表示即将访问元素的下标。size表示ArrayList的当前长度。因为我们删除的元素是倒数第二个元素。遍历data3后  cursor的值变为 3。删除data3后 ArrayList的长度也变为了 3 。导致调用hasNext()的方法的时候  认为该列表已经没有下一个元素了。所以不会再调用后续的next方法。即不抛出异常。(这样会导致最后一个元素data4未进行判断

那么我们就没有办法在遍历的时候。判处不需要的元素了吗?肯定是有办法的。

方式1、新建一个列表 在遍历时将符合条件的元素添加到新的列表里。

方式2、使用Iterator提供的remove()方法

    public void remove() {
            if (lastRet < 0)
                throw new IllegalStateException();
            checkForComodification();

            try {
                ArrayList.this.remove(lastRet);
                cursor = lastRet;
                lastRet = -1;
                expectedModCount = modCount;
            } catch (IndexOutOfBoundsException ex) {
                throw new ConcurrentModificationException();
            }
        }

我们可以看到该方法也是直接调用的ArrayList的remove方法。但是操作完成后 重新赋值了 expectedModCount = modCount;。所以不会抛出异常。

方式3、在jdk8版本开始支持lambda。使用lists.removeIf(str ->"data2".equals(str));删除。

五、ArrayList和LinkedList比较

LinkedList是基于双向链表结构实现的。其特点就是维护了前后元素的引用。使得添加元素和删除元素的时候比ArrayList快。但是查询指定元素的时候 就相对较慢。

package com.winston.javase.testcollections;

import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;

/**
 * java.util.List 是有序集合也成为 列表 可以重复。
 * 它是Collection的子接口 主要实现类 
 * 			java.util.ArrayList
 * 			java.util.LinkedList 
 * 			java.util.Vector
 * @Author Winston
 * @Version 1.0 2018年9月17日 下午4:11:54
 */
public class TestList {

	public static void main (String[] args) {

		for (int i = 0; i < 20; i++) {
			compareArrayListLinkedList();
		}
		
	}

	private static void compareArrayListLinkedList () {
		List<String> list = new ArrayList<String>();
		List<String> linkedList = new LinkedList<String>();
		linkedList.add("西");
		list.add("西");
		long s1 = System.currentTimeMillis();
		for (int i = 0; i < 100000; i++) {
			//list.add("西");
			linkedList.add(1, "西");
		}
		long s2 = System.currentTimeMillis();
		for (int i = 0; i < 100000; i++) {
			//linkedList.add("西");
			list.add(1, "西");
		}
		long s3 = System.currentTimeMillis();
		
		System.out.println("linkedList:" + (s2 - s1) + "||" + "ArrayList:" + (s3 - s2));

	}
}

输出结果:

linkedList:4||ArrayList:825
linkedList:3||ArrayList:825
linkedList:1||ArrayList:822
linkedList:2||ArrayList:822
linkedList:1||ArrayList:828
linkedList:1||ArrayList:820
linkedList:1||ArrayList:823
linkedList:1||ArrayList:835
linkedList:1||ArrayList:832
linkedList:1||ArrayList:847
linkedList:1||ArrayList:880
linkedList:1||ArrayList:825
linkedList:1||ArrayList:823
linkedList:1||ArrayList:835
linkedList:1||ArrayList:828
linkedList:1||ArrayList:842
linkedList:2||ArrayList:838
linkedList:1||ArrayList:828
linkedList:1||ArrayList:828
linkedList:1||ArrayList:830

从结果中可以看出 LinkedList 在列表第一个元素的位置添加的时候 比ArrayList明显要快很多。因为ArrayList每次会移动列表里面的所有元素。但是在列表最后追加元素的话  这个差距不会这么明显。

六、线程不安全测试:

 

package com.winston.javase.testcollections;

import java.util.ArrayList;
import java.util.List;

/**

 * @Author Winston
 * @Version 1.0 2018年9月17日 下午4:11:54
 */
public class TestList {

	public static void main (String[] args) {

		testThreadSafe();
	}

	static void testThreadSafe () {

		List<String> list1 = new ArrayList<String>();
		Thread t1 = new Thread(new Runnable() {

			@Override
			public void run () {
				for (int i = 0; i < 10; i++) {
					list1.add("A" + i);
				}
			}
		});
		Thread t2 = new Thread(new Runnable() {

			@Override
			public void run () {
				for (int i = 0; i < 10; i++) {
					list1.add("B" + i);
				}
			}
		});
		t1.start();
		t2.start();
		try {
			t1.join();
			t2.join();
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		System.out.println(list1);
		System.out.println(list1.size());
		
}

  结果一

[null, B0, B1, B2, B3, A1, B4, A2, A3, A4, A5, A6, null, B5, A8,null, A9, B7, B8, B9]
19

其中出现为空的原因 在于新增的时候 elementData[size++] = e; 分为两步
elementData[size] = e; ①
size++; ②
当线程A执行 了步骤①还未来得及执行步骤②的时候 此时size为n elementData[n] = A(n) 则
cpu切换到线程B执行。由于此时size还没加1 导致线程B执行的时候 elementData[n] = B(n).
然后线程B 执行size++ 此时 size变为 n+1并且 elementData[n+1]=null。最后CPU再切换回A线程
执行第②步。导致 size变为 n+2 。后面在追加进来的元素 执行步骤①的时候
就将新元素放到了n+2的位置上。所以n+1的位置就永远为null了

结果二

Exception in thread "Thread-0"
    java.lang.ArrayIndexOutOfBoundsException: 10 at
    java.util.ArrayList.add(ArrayList.java:463) at
	com.winston.javase.testcollections.TestList$1.run(TestList.java:83)
	at java.lang.Thread.run(Thread.java:748) 
[A0, A1, B0, A2, A3, A4, A5,B2, A6, B3, null, B4, B5, B6, B7, B8, B9] 17

 if (minCapacity - elementData.length > 0)

如果当前 列表容量为10个 数组目前里面包含 9个元素 下标是 0至8 当A线程和B线程同时加入新元素
当线程A 执行完以上代码 此时10 - 10 刚好等于0 当数组容量刚好达到临界值的时候 不会进行扩容。此时还未将新元素放到数组CPU切换 到线程B,线程B 在以上代码 也刚好达到临界值 不会扩容 然后线程B继续执行 elementData[size++] = e;
此时B线程元素刚好放到数组里面下标为9的位置 并且size变为10
再切换回线程A。此时线程A直接执行elementData[size++] = e 因为此时size 为10 elementData[10]
引发数组下标越界异常

    
 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值