声明:本文为作者原创,请勿装载,如过转载,请注明转载地址
(1) ArrayList是基于数组实现的,是一个动态数组,其容量能自动增长,不像数组一旦初始化长度就不可被改变
(2) ArrayList不是线程安全的,只能用在单线程环境下,多线程环境下可以考虑用Collections.synchronizedList(List l)函数返回一个线程安全的ArrayList类,也可以使用concurrent并发包下的CopyOnWriteArrayList类。
(3) ArrayList实现了Serializable接口,因此它支持序列化,能够通过序列化传输,实现了RandomAccess接口,支持快速随机访问,实际上就是通过下标序号进行快速访问,实现了Cloneable接口,能被克隆。
增删慢:每次删除元素,都需要更改数组的长度,拷贝以及移动元素的位置
查询快:由于数组在内存中是一块连续的空间,因此可以根据索引快速获取某个位置上的元素。
介绍:类的序列化由java.io.Serializable接口的类启用,不实现此接口的类将不会使用任何状态序列化或反序列化。可序列化类的所有子类型都是可序列化的。序列化接口没有方法和字段,仅用于标识可序列化的语义。
序列化:将对象的数据写入到文件中
反序列化:将文件中对象的数据读取出来
//Serializable 源码,接口中并没有方法 public interface Serializable { }一个类实现Cloneable接口来指示Object.clone()方法,该方法对于该类的实例进行字段的复制是合法的,如果不实现该接口,那么调用clone()方法户抛出异常。克隆就是根据已有的数据创造一份新的完全一样的数据拷贝。
// Cloneable 源码,接口中并没有方法 public interface Cloneable { }克隆的前提:被克隆对象所在的类必须实现Cloneable接口,必须重写clone()方法。
public class CloneDemo { public static void main(String[] args) { ArrayList<String> list = new ArrayList<>(); list.add("aaa"); list.add("bbb"); list.add("ccc"); //调用方法进行克隆 Object obj = list.clone(); System.out.println(obj==list); //false System.out.println(obj); //[aaa, bbb, ccc] System.out.println(list); //[aaa, bbb, ccc] } }结果:
Student{name='小明', age=18, skill=Skill{skillName='弹钢琴'}} Student{name='小明', age=18, skill=Skill{skillName='弹钢琴'}} Student{name='小明', age=30, skill=Skill{skillName='画画'}} Student{name='小明', age=18, skill=Skill{skillName='画画'}}存在的问题:基本数据类型可以达到完全复制,但是引用数据类型不可以
原因:在学生对象stu1被拷贝时,其属性skill(引用数据类型)仅仅是拷贝了一份引用,因此当被克隆对象stu1的skill的值发生改变时,克隆对象stu2的属性skill也将跟着改变。
如果属性是基本类型,拷贝的就是基本类型的值;如果属性是内存地址(引用类型),拷贝的就是内存地址。
(1) 对于基本数据类型的成员对象,因为基础数据类型是值传递的,所以是直接将属性值赋值给新的对象。基础类型的拷贝,其中一个对象修改该值,不会影响另外一个。 (2) 对于引用类型,比如数组或者类对象,因为引用类型是引用传递,所以浅拷贝只是把内存地址赋值给了成员变量,它们指向了同一内存空间。改变其中一个,会对另外一个也产生影响。
结果:
Student{name='小明', age=18, skill=Skill{skillName='弹钢琴'}} Student{name='小明', age=18, skill=Skill{skillName='弹钢琴'}} Student{name='小明', age=30, skill=Skill{skillName='画画'}} Student{name='小明', age=18, skill=Skill{skillName='弹钢琴'}}深拷贝,在拷贝引用类型成员变量时,为引用类型的数据成员另辟了一个独立的内存空间,实现真正内容上的拷贝。
对于基本数据类型的成员对象,因为基础数据类型是值传递的,所以是直接将属性值赋值给新的对象。基础类型的拷贝,其中一个对象修改该值,不会影响另外一个(和浅拷贝一样)。 (2) 对于引用类型,比如数组或者类对象,深拷贝会新建一个对象空间,然后拷贝里面的内容,所以它们指向了不同的内存空间。改变其中一个,不会对另外一个也产生影响。 (3) 对于有多层对象的,每个对象都需要实现 Cloneable 并重写 clone() 方法,进而实现了对象的串行层层拷贝。 (4) 深拷贝相比于浅拷贝速度较慢并且花销较大。深拷贝后,不管是基础数据类型还是引用类型的成员变量,修改其值都不会相互造成影响。
RandomAccess接口这个空架子的存在,是为了能够更好地判断集合是否ArrayList或者LinkedList,从而能够更好选择更优的遍历方式,提高性能!
**jdk8:**ArrayList中维护了Object[] elementData,初始容量为0,第一次添加时,将初始elementData的容量为10再次添加时,如果容量足够,则不用扩容直接将新元素赋值到第一个空位上,如果容量不够,会扩容1.5倍
**jdk7:**ArrayList中维护了Object[] elementData,初始容量为10.添加时,如果容量足够,则不用扩容直接将新元素赋值到第一个空位上。如果容量不够,会扩容1.5倍
jdk7 相当于饿汉式,创建对象时,则初始容量为10 jdk8 相当于懒汉式,创建对象时,并没有初始容量为10,而在添加时才去初始容量为10
ArrayList的扩容机制:
private void grow(int minCapacity) { // 获取数组容量 int oldCapacity = elementData.length; // 扩容1.5倍(将数组的容量扩容1.5倍) int newCapacity = oldCapacity + (oldCapacity >> 1); // 如果扩展为1.5倍后还不满足需求,则直接扩展为需求值 if (newCapacity - minCapacity < 0) newCapacity = minCapacity; // 检查扩容后的容量是否大于最大容量 if (newCapacity - MAX_ARRAY_SIZE > 0) newCapacity = hugeCapacity(minCapacity); // 申请一块更大内存容量的数组,将原来数组中的元素挪到这个新数组中,同时将elementData指向这个新数组 // 原来的旧的数组由于没有引用变量指向他,就会被垃圾回收机制回收掉 elementData = Arrays.copyOf(elementData, newCapacity); }总结:在数组的末尾添加一个元素:
(1) 调用ensureCapacityInternal(size + 1);方法,该方法的作用是确保数组的长度加1后内存是充足的,就是说保证能够数组还能存放一个元素。
(2) 调用calculateCapacity(elementData, minCapacity)计算添加一个元素所需的最小容量minCapacity;根据ArrayList创建对象时使用的构造方法分为两种情况:
如果使用空参数构造函数创建ArrayList对象,会在第一次调用add()方法添加元素时,将数组的容量初始化为10,随后每次调用add()方法添加元素时都会比较初始容量10与minCapacity的大小,并返回大的容量。
如果使用带参构造函数创建ArrayList对象,会直接但是minCapacity。
(3) 调用ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));该方法就是ensureCapacityInternal(size + 1)方法的底层,其作用是确保数组的长度加1后内存是充足的,就是说保证能够数组还能存放一个元素。
若添加一个元素所需的最小容量minCapacity大于数组的长度if (minCapacity - elementData.length > 0)那么就需要扩容,此时就会调用扩容方法:grow(int minCapacity)
(4) 调用 grow(int minCapacity)方法,该方法会对数组容量进行扩容。
在扩容时会申请一块更大内存容量的数组,将原来数组中的元素拷贝到这个新的数组中,同时让elementData指向该数组,而原来的数组由于没有新的指针指向它,就会被垃圾回收机制回收。
在扩容时,首先考虑扩容1.5倍(如果扩容的太大,可能会浪费更多的内存,如果扩容的太小,又会导致频繁扩容,申请数组拷贝元素等对添加元素的性能消耗较大,因此1.5倍刚好)。如果扩容1.5倍还是内存不足,就会直接扩容到添加元素所需的最小的容量。同时不能超过数组的最大容量Integer.MAX_VALUE - 8。
先数组的指定位置添加元素:
public void add(int index, E element) { //判断索引是否越界 rangeCheckForAdd(index); //确保数组size+1之后能够存下 下一个数据 ensureCapacityInternal(size + 1); /** 源数组 elementData 从传入形参index处开始复制,复制size-index个元素 目标数组 elementData 从index+1处开始粘贴,粘贴从源数组赋值的元素 **/ System.arraycopy(elementData, index, elementData, index + 1,size - index); //把index处的元素替换成新的元素。 elementData[index] = element; //将size后移 size++; } //判断索引是否越界 private void rangeCheckForAdd(int index) { if (index > size || index < 0) throw new IndexOutOfBoundsException(outOfBoundsMsg(index)); } // 这是一个本地方法,由C语言实现。 public static native void arraycopy(Object src, // 源数组 int srcPos, // 源数组要复制的起始位置 Object dest, // 目标数组(将原数组复制到目标数组) int destPos, // 目标数组起始位置 int length // 复制源数组的长度 );总结:在数组的指定位置添加元素
(1) 调用rangeCheckForAdd(index)方法判断输入的索引是否越界
(2) 调用ensureCapacityInternal(size + 1)方法确保数组的容量充足,至少保证能再装下一个元素。
(3) 调用System.arraycopy(elementData, index, elementData, index + 1,size - index);方法,这是C语言的一个实现方法,这个方法的作用是复制elementData数组中(index,size-index)区间内的元素,粘贴到elementData数组中(index+1,size-index+1)的位置处。
从图中可看出,表现出的结果就是将elementData数组中的元素,从要插入的位置index开始向后移动一个位置,给要插入的元素腾出一个位置,将该元素插入到index位置处。
设置index位置处的元素
public E set(int index, E element) { rangeCheck(index); //记录index位置处的旧值 E oldValue = elementData(index); //将数组index索引处的元素设置为新值element elementData[index] = element; //返回旧值 return oldValue; } //判断索引越界 private void rangeCheck(int index) { if (index >= size) throw new IndexOutOfBoundsException(outOfBoundsMsg(index)); }获取指定索引位置处的元素
public E get(int index) { //判断索引越界 rangeCheck(index); //返回指定索引位置处的元素 return elementData(index); }因为数组的内存是连续的,因此可以根据索引直接获取元素。
总结:
(1) 调用rangeCheckForAdd(index)方法判断输入的索引是否越界
(3) 调用System.arraycopy(elementData, index+1, elementData, index, size - index - 1);该方法的作用是复制elementData数组中(index+1,size-index-1)区间内的元素,粘贴到elementData数组中的(index,size-index-2)位置处。
从图中可看出,表现出的结果就是将elementData数组中的元素,从要删除的元素的后面一个位置开始到末尾的元素结束都向前移动一个位置,最后将size–,将最后一个元素置为null