ArrayList详解

最近在重温java源码,一边看一边总结下,并且分享下自己的心得,共同学习,欢迎指点。这一篇说下ArrayList的源码中的一些注意点,争取把原理讲透彻。虽然这个类很常用,但是还有很多可能你不知到的。

开篇翻译下ArrayList类的注释,虽然有翻译好的现成版本,但是看注释还是程序员必备技能。并且致敬Bloch大神:

注释翻译

/*动态数组(ArrayList)实现了List接口,并且实现了List所有方法,并且可以存储null元素。继承所有属性。这个类提供了可以灵活的操作数组长度size的内部方法,(这个类除了是unsynchronized,其他的大体上和vector差不多)。

size(),isEmpty(),get(),set(),iterator()和listIterator()方法时间复杂度是常量级别(constant time)。add()操作时间复杂度是个可变常量级别(amortized constant time),比如插入第n个元素时候,时间复杂度是n。其他的方法都是线性的时间复杂度..大致的说。(linear time),the constant factor is low compared to that for the LinkedList implementation。

每个ArrayList实例都有一个属性capacity。 这个capacity是存储元素数组的长度,它最小是和list的size一样大。当ArrayList add元素的时候,capacity 自动增长。the details of the growth policy are not specified beyond the fact that adding an element has constant amortized time cost。

一个程序可以在你往一个Arraylist实例中添加非常多元素之前,通过ensureCapacity这个属性来增加ArrayList实例的容量。这样可以减少增加元素带来重新分配数组大小的次数。

注意,ArrayList并不是synchronized。如果多个线程同时竞争一个ArrayList实例,并且至少一个线程修改了list的结构,那么必须在程序里进行synchronized。(修改list结构包括:增加或者删除一个或多个元素,或者明确的说就是resizes the backing array;),常见的典型方法是在list外面synchronizing一个object,如果没有这样的object,这个list最好用{@link Collections#synchronizedList Collections.synchronizedList}方法实例,这是为了防止unsynchronized情况发生创建ArrayList最好的方法。List list =Collections.synchronizedList(New ArrayList(...));

如果list在iterator创建之后以任何方式被修改了,这个iterator会throw一个{@link ConcurrentModificationException}。因此,面对修改,iterator 在不正确的时间做不确定的行为,干净利落的失败要比冒险强,*/


官方标准翻译(鼓励自己翻译下再看官方):http://dlc-cdn.sun.com/jdk/jdk-api-localizations/jdk-api-zh-cn/publish/1.6.0/html/zh_CN/api/java/util/ArrayList.html


ArrayList继承体系



ArrayList实现了RandomAccess标示接口,表明支持随机访问。

RandomAccess用法参见:http://dlc-cdn.sun.com/jdk/jdk-api-localizations/jdk-api-zh-cn/publish/1.6.0/html/zh_CN/api/java/util/RandomAccess.html

ArrayList字段:

private transient Object[] elementData;

有没有发现这个变量被transient修饰?为什么在声明变量的时候加上不序列化修饰符?

不进行序列化当然是不可能的,ArrayLelemenist中维护的elementData,这个属性是用来存储元素的。ArrayList的开头注释中说过,capacity的大小要比实际的size要大,elementData总是要预留一些容量,而这些容量对/序列化来说没有意义的,所以ArrayList实现writeObject和readObject方法,进行自定义的序列化,只将有意义的数据进行序列化。当ObjectOutputStream进行序列化的时候会通过反射调用自定义的序列化方法。

 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 array length
        s.writeInt(elementData.length);

	// 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方法:

/**
     * Constructs an empty list with an initial capacity of ten.
     */
    public ArrayList() {
	this(10);
     }
注意:平常我们初始化ArrayList的时候如果不传值,那么默认的capacity就是10。如果ArrayList在已知的情况下会迅速增长到很大,那么默认给个估值是个很不错的选择,防止不断的扩容。

public boolean add(E e) {
	ensureCapacity(size + 1);  // Increments modCount!!
	elementData[size++] = e;
	return true;
    }
add操作实现比较简单,第一步确保当前capacity的值大于size+1,否则进行  (当前容量*3)/2+1 扩容,扩容的方法是调用Arrays.copyOf

elementData = Arrays.copyOf(elementData, size, Object[].class);
System.arraycopy(elementData, 0, a, 0, size);

有没有发现ArrayList中大量出现Arrays.copyOf方法和System.arraycopy方法,在ArrayList中,扩容,remove等许多方法中都有调用此方法进行数组resize,ArrayList中大部分在做逻辑处理,但是涉及到数组的变更,基本都交由这两个方法进行处理,看源码可以发现Arrays.copyOf(...)的底层实现就是调用的System.arraycopy。

但是这两个方法最大的区别在于Arrays.copyOf方法会实例化一个目标源对象,而system中是个本地方法,不会实例化,如果源或者目标对象为空会抛异常,可以根据具体需求调用不同的方法。

所以下面着重讲解下System.arraycopy(..)

在 System 类提供的设施中,有标准输入、标准输出和错误输出流;对外部定义的属性和环境变量的访问;加载文件和库的方法;还有快速复制数组的一部分的实用方法。

 public static native void arraycopy(Object src,  int  srcPos,
                                        Object dest, int destPos,
                                        int length);

看下这个方法上面的注释:

从指定源数组中复制一个数组,复制从指定的位置开始,到目标数组的指定位置结束。从 src 引用的源数组到 dest 引用的目标数组,数组组件的一个子序列被复制下来。被复制的组件的编号等于 length 参数。源数组中位置在 srcPos 到 srcPos+length-1 之间的组件被分别复制到目标数组中的 destPos 到 destPos+length-1 位置。
如果参数 src 和 dest 引用相同的数组对象,则复制的执行过程就好像首先将 srcPos 到 srcPos+length-1 位置的组件复制到一个带有 length 组件的临时数组,然后再将此临时数组的内容复制到目标数组的 destPos 到 destPos+length-1 位置一样。

该方法是一个本地方法,查找openjdk,经过层层深入和各种类型转换找到适合的类型。调用核心处理方法_Copy_conjoint_jints_atomic。这个方法里面写了copy的整个过程,可以大致的理解为找到src和des的要拷贝的begin的地址,然后循环length更改指针指向。而且还有适应64位系统的代码,用汇编指令进行编写的。

void Copy::conjoint_memory_atomic(void* from, void* to, size_t size) {
  address src = (address) from;
  address dst = (address) to;
  uintptr_t bits = (uintptr_t) src | (uintptr_t) dst | (uintptr_t) size;

  // (Note:  We could improve performance by ignoring the low bits of size,
  // and putting a short cleanup loop after each bulk copy loop.
  // There are plenty of other ways to make this faster also,
  // and it's a slippery slope.  For now, let's keep this code simple
  // since the simplicity helps clarify the atomicity semantics of
  // this operation.  There are also CPU-specific assembly versions
  // which may or may not want to include such optimizations.)

  if (bits % sizeof(jlong) == 0) {
    Copy::conjoint_jlongs_atomic((jlong*) src, (jlong*) dst, size / sizeof(jlong));
  } else if (bits % sizeof(jint) == 0) {
    Copy::conjoint_jints_atomic((jint*) src, (jint*) dst, size / sizeof(jint));
  } else if (bits % sizeof(jshort) == 0) {
    Copy::conjoint_jshorts_atomic((jshort*) src, (jshort*) dst, size / sizeof(jshort));
  } else {
    // Not aligned, so no need to be atomic.
    Copy::conjoint_jbytes((void*) src, (void*) dst, size);
  }

 void _Copy_conjoint_jints_atomic(jint* from, jint* to, size_t count) {
    if (from > to) {
      jint *end = from + count;
      while (from < end)
        *(to++) = *(from++);
    }
    else if (from < to) {
      jint *end = from;
      from += count - 1;
      to   += count - 1;
      while (from >= end)
        *(to--) = *(from--);
    }
  }

ok,copy方法讲的很多了,因为很重要,所以深入了下。如果还不了解鼓励看源码。

add方法讲么实完了,可以考虑下indexof方法怎现的,没有什么特别的,每个人都可以写出来,不多说。顺便提一句,虽然arrayList可以存储null值,但是实际上没有什么意义,如果用null作为特殊分界符,我就不说什么了。

remove方法的实现:

 public E remove(int index) {
	RangeCheck(index);

	modCount++;
	E oldValue = (E) elementData[index];

	int numMoved = size - index - 1;
	if (numMoved > 0)
	    System.arraycopy(elementData, index+1, elementData, index,
			     numMoved);
	elementData[--size] = null; // Let gc do its work

	return oldValue;
    }

又看到了System.arraycopy,没错,核心都在这个,灵活的应用这个方法会带来很多的妙处。已经确保elementData不为空,所以直接调用System.arraycopy方法,进行自我拷贝。不过需要注意的是:elementData[--size] = null一定要注意这点,如果不进行空赋值,OOM隐患随之而来,因为ArrayList维护着这些对象的过期引用。在gc回收器看来,这些对象依然存在着到gc根的路径,但是实际应用过程中已经用不到这些对象。用C++的话来说就是野指针,时间长了很容易导致内存溢出。具体gc可以参考《深入理解java虚拟机》。

其余的方法不一一详举了,基础是System.arraycopy方法,其余逻辑细节很重要。


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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值