ArrayList学习(JDK1.8)

对于集合,关注的点主要有四点:

1、是否允许空

2、是否允许重复数据

3、是否有序,有序的意思是读取数据的顺序和存放数据的顺序是否一致

4、是否线程安全 

ArrayList

ArrayList是最常见以及每个Java开发者最熟悉的集合类了,顾名思义,ArrayList就是一个以数组形式实现的集合,以一张表格来看一下ArrayList里面有哪些基本的元素:


先对于一个集合关注的四个点以表格形式做一个解答:


添加元素

public boolean add(E e) {
        ensureCapacityInternal(size + 1);  // Increments modCount!!判断是否扩容
        elementData[size++] = e; //把添加的元素顺序放入数组中。这里size的大小为实际存储元素个数的大小,而不是容量的大小
        return true;
    }
我们进入ensureCapacityInternal(size+1)这个方法看一下。
private void ensureCapacityInternal(int minCapacity) {
	        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
	        	//如果第一次添加元素,就为minCapacity赋值为默认容量DEFAULT_CAPACITY
	            minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
	        }
	        //然后进入此方法,判断是不是需要扩容
	        ensureExplicitCapacity(minCapacity);
	    }
而默认容量是10.
 private static final int DEFAULT_CAPACITY = 10;
所以,从这可以ArrayList并不是一开始new的时候就为ArrayLsit设置容量的,而是在第一次添加元素的时候才设置容量的,设置的容量大小默认为10。
我们看一下ensureExplicitCapacity(minCapacity)这个方法。
private void ensureExplicitCapacity(int minCapacity) {
			 modCount++;//有这个变量表明ArrayList不是线程安全的

		     // overflow-conscious code
		     //判断是否需要扩容,如果需要,进入确认扩容大小的方法grow(minCapacity)
		     if (minCapacity - elementData.length > 0)
		    	 grow(minCapacity);
		 }
我们进入grow(minCapacity)看一下。
private void grow(int minCapacity) {
	        // overflow-conscious code
	        int oldCapacity = elementData.length;//原始容量
	        // 扩容为: 原始大小 + 0.5*原始大小,即1.5倍
	        int newCapacity = oldCapacity + (oldCapacity >> 1); 
	        //如果扩容后为原来的1.5倍,还小于元素数量(minCapacity这个值是一路传进来的,为:实际元素数量+1)
	        //就把元素数量,就把元素数量+1的值赋值为最新容量newCapacity
	        if (newCapacity - minCapacity < 0)
	            newCapacity = minCapacity;
	        //下面这个条件基本不可能执行,
	        //因为:private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
	        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);
	    }
下面这两个方法就是实际执行申请数组(旧数组容量为n,而新数组申请的容量就为1.5n,然后把旧数组复制到新数组中的部分了),下图为旧数组和复制的新数组图示。

 @SuppressWarnings("unchecked")
    public static <T> T[] copyOf(T[] original, int newLength) {
        return (T[]) copyOf(original, newLength, original.getClass());
    }
public static <T,U> T[] copyOf(U[] original, int newLength, Class<? extends T[]> newType) {
        @SuppressWarnings("unchecked")
        T[] copy = ((Object)newType == (Object)Object[].class)
            ? (T[]) new Object[newLength]
            : (T[]) Array.newInstance(newType.getComponentType(), newLength);
        System.arraycopy(original, 0, copy, 0, //此方法为native方法
                         Math.min(original.length, newLength));
        return copy; //返回复制好的新数组
    }

这就是使用add()方法添加元素的图解,从图中和源代码中可以看到都是顺序添加的。



多说一句,我这么画图有一定的误导性。elementData中存储的应该是堆内存中元素的引用,而不是实际的元素,这么画给人一种感觉就是说elementData数组里面存放的就是实际的元素,这是不太严谨的。不过这么画主要是为了方便起见,只要知道这个问题就好了。

当arraylist容量不够的时候,那我们就需要扩容了,实际和我们第一次添加元素的时候是一样,我们第一次添加元素也需要扩容,所以当arraylist装满10个元素的时候,看源码就需要扩容至15 = 10+10*0.5。

删除元素。

public E remove(int index) {
        rangeCheck(index);//这个是判断index是否合法,没什么好看的

        modCount++;
        E oldValue = elementData(index);//取到要删除位置上的元素

        int numMoved = size - index - 1;  //要复制的元素数量
        if (numMoved > 0)
        	//index+1为原数组的起始位置,index为目标数组的起始位置,nuMoved为复制数组元素的数量
            System.arraycopy(elementData, index+1, elementData, index,
                             numMoved);
       //将最后一个元素置为null
        elementData[--size] = null; // clear to let GC do its work
        return oldValue;
    }


所以看出,arraylist删除元素和普通的数组删除元素没什么两样,都是把删除位置后面的元素统一往前挪,然后把最后一位元素置为null,所以删除元素也是很费时间的。

插入元素

public void add(int index, E element) {
	        rangeCheckForAdd(index);//检查参数
	       //同理,判断插入后是否需要扩容
	        ensureCapacityInternal(size + 1);  // Increments modCount!!
	        //先把index(包括)和后面的元素整体往后移一个,
	        System.arraycopy(elementData, index, elementData, index + 1,
	                         size - index);
	        //然后设置index位置上位插入的元素
	        elementData[index] = element;
	        size++;
	    }

可以看出,插入元素和数组插入操作是一样的。

其中333为要插入的位置,000为要插入的数据。



我们再看一下clone()方法。

这是我写的测试代码

private static  class Stu{
		public int a = 10;
	}	
	public static void main(String[] args) throws InterruptedException {
		ArrayList<Stu> list = new ArrayList<Stu>();
		Stu s = new Stu();
		s.a = 10;
		list.add(s);		
		ArrayList<Stu> li = (ArrayList<Stu>) list.clone();
		//为true,根据API和源码可以看出其实是复制了一个数组,但是其中存储的对象还是同一个,元素并没有被复制,还是指向同一个对象
		System.out.println(li.get(0) == list.get(0));
		System.out.println(li.size());
		list.remove(0);		
		System.out.println(li.size());
		System.out.println(list.size());						
	}

public Object clone()
返回此 ArrayList 实例的浅表副本。(不复制这些元素本身。) 

public Object clone() {
	        try {
	            ArrayList<?> v = (ArrayList<?>) super.clone();
	            //复制了数组,数组中的元素并没有复制,相应指向的还是同一个元素
	            v.elementData = Arrays.copyOf(elementData, size);
	            v.modCount = 0;
	            return v;
	        } catch (CloneNotSupportedException e) {
	            // this shouldn't happen, since we are Cloneable
	            throw new InternalError(e);
	        }
	    }

ArrayList的优缺点

从上面的几个过程总结一下ArrayList的优缺点。ArrayList的优点如下:

1、ArrayList底层以数组实现,是一种随机访问模式,再加上它实现了RandomAccess接口,因此查找也就是get的时候非常快

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

不过ArrayList的缺点也十分明显:

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

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

因此,ArrayList比较适合顺序添加、随机访问的场景。

ArrayList和Vector的区别

ArrayList是线程非安全的,这很明显,因为ArrayList中所有的方法都不是同步的,在并发下一定会出现线程安全问题。那么我们想要使用ArrayList并且让它线程安全怎么办?一个方法是用Collections.synchronizedList方法把你的ArrayList变成一个线程安全的List,比如:

1
2
3
4
5
6
7
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,源代码是这样的:

1
2
int newCapacity = oldCapacity + ((capacityIncrement > 0 ) ?
                                  capacityIncrement : oldCapacity);

为什么ArrayList的elementData是用transient修饰的?

最后一个问题,我们看一下ArrayList中的数组,是这么定义的:

1
private transient Object[] elementData;

不知道大家有没有想过,为什么elementData是使用transient修饰的呢?关于这个问题,说说我的看法。我们看一下ArrayList的定义:

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

看到ArrayList实现了Serializable接口,这意味着ArrayList是可以被序列化的,用transient修饰elementData意味着我不希望elementData数组被序列化。这是为什么?因为序列化ArrayList的时候,ArrayList里面的elementData未必是满的,比方说elementData有10的大小,但是我只用了其中的3个,那么是否有必要序列化整个elementData呢?显然没有这个必要,因此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();
	        }
	    }
	

每次序列化的时候调用这个方法,先调用defaultWriteObject()方法序列化ArrayList中的非transient元素,elementData不去序列化它,然后遍历elementData,只序列化那些有的元素,这样:

1、加快了序列化的速度

2、减小了序列化之后的文件大小

参考:http://www.importnew.com/25008.html






  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值