ArrayList
ArrayList实现了List接口 , 它是有序且可以重复的 , 允许存放所有所有元素 , 包括null , 除了实现List接口之外这个类还提供了一些方法来操作内部存储列表数组的大小 , 这个类大致相当于Vector , 只是它不是同步的 , 同时ArrayList还实现了RandomAccess, Cloneable, java.io.Serializable
RandomAccess
Random是随机的意思,Access是访问的意思,合起来就是随机访问的意思。RandomAccess是一个标记接口 , 用以标记实现的List集合具备快速随机访问的能力 , 那么什么是随机访问的能力呢?其实很简单,随机访问就是随机访问List中的任何一个元素。
所有的List实现都支持随机访问的,只是基于基本结构的不同,实现的速度不同罢了,这里的快速随机访问,那么就不是所有List集合都支持了
ArrayList基于数组实现,天然带下标 ,可以实现常量级的随机访问,复杂度为O(1)
LinkedList基于链表实现,随机访问需要依靠遍历实现,复杂度为O(n)
通过代码的实现也能看出来 , ArrayList实现了RandomAccess接口 , 而LinkedList没有实现
Cloneable
支持拷贝:实现Cloneable接口,重写clone方法、方法内容默认调用父类的clone方法。
浅拷贝(shallowCopy)
如果是基础类型和String类型的变量拷贝之后是独立的,不会随着源变量变动而变 , 但是如果修改了任意一个变量的值 , 对另一个都是有影响的
引用类型拷贝的是引用地址,拷贝前后的变量引用同一个堆中的对象
深拷贝(deepCopy)
基本类型是复制的变量名和值,值变了不影响源变
用类型复制的是变量名和值(引用地址),对象变了,会影响源变量(引用地址是一样的)
String:是不可变对象,重新赋值时,会在常量表新生成字符串(如果已有,直接取他的引用地址),将新字符串的引用地址赋值给栈中的新变量,因此源变量不会受影响
也可以理解为浅拷贝是修改堆内存中的同一个值 , 而深拷贝是修改堆内存中的不同的值
java.io.Serializable
序列化 : 把Java对象转换为字节序列(二进制)的过程
序列化作用 : 在传递和保存对象时.保证对象的完整性和可传递性。对象转换为有序字节流,以便在网络上传输或者保存在本地文件中
反序列化 : 把字节序列(二进制)恢复为Java对象的过程
反序列化作用 : 根据字节流中保存的对象状态及描述信息,通过反序列化重建对象
json/xml的数据传递
在数据传输(也可称为网络传输)前,先通过序列化工具类将Java对象序列化为json/xml文件。
在数据传输(也可称为网络传输)后,再将json/xml文件反序列化为对应语言的对象
序列化优点
将对象转为字节流存储到硬盘上,当JVM停机的话,字节流还会在硬盘上默默等待,等待下一次JVM的启动,把序列化的对象,通过反序列化为原来的对象,并且序列化的二进制序列能够减少存储空间(永久性保存对象)。
序列化成字节流形式的对象可以进行网络传输(二进制形式),方便了网络传输。
通过序列化可以在进程间传递对象。
ArrayList属性
// 默认初始容量 private static final int DEFAULT_CAPACITY = 10; // 用于空实例的共享空数组实例。 private static final Object[] EMPTY_ELEMENTDATA = {}; // 用于默认大小的空实例的共享空数组实例 private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {}; // 当前数据对象存放地方,当前对象不参与序列化 transient Object[] elementData // 当前ArrayList的长度 private int size; // 数组最大容量 private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
ArryList构造函数
无参构造
/** * Constructs an empty list with an initial capacity of ten. */ public ArrayList() { this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA; }
如果使用无参构造创建ArrayList对象 , 此时我们创建的ArrayList对象中的elementData中的长度是1,size是0 , 当第一次添加元素的时候 , elementData才会变成默认长度10
有参构造
/** * Constructs an empty list with the specified initial capacity. * * @param initialCapacity the initial capacity of the list * @throws IllegalArgumentException if the specified initial capacity * is negative */ public ArrayList(int initialCapacity) { if (initialCapacity > 0) { this.elementData = new Object[initialCapacity]; } else if (initialCapacity == 0) { this.elementData = EMPTY_ELEMENTDATA; } else { throw new IllegalArgumentException("Illegal Capacity: "+ initialCapacity); } }
如果传入参数,则代表指定ArrayList的初始数组长度,传入参数如果是大于等于0,则使用用户的参数初始化,如果用户传入的参数小于0,则抛出异常
/** * Constructs a list containing the elements of the specified * collection, in the order they are returned by the collection's * iterator. * * @param c the collection whose elements are to be placed into this list * @throws NullPointerException if the specified collection is null */ public ArrayList(Collection<? extends E> c) { elementData = c.toArray(); if ((size = elementData.length) != 0) { // c.toArray might (incorrectly) not return Object[] (see 6260652) if (elementData.getClass() != Object[].class) elementData = Arrays.copyOf(elementData, size, Object[].class); } else { // replace with empty array. this.elementData = EMPTY_ELEMENTDATA; } }
将collection对象转换成数组,然后将数组的地址赋给elementData。
更新size的值,同时判断size的大小,如果是size等于0,直接将空对象EMPTY_ELEMENTDATA的地址赋给elementData
如果size的值大于0,则执行Arrays.copy方法,把collection对象的内容(可以理解为深拷贝)copy到elementData中。
注意:this.elementData = arg0.toArray(); 这里执行的简单赋值是浅拷贝,所以要执行Arrays.copy 做深拷贝
add(E e)方法源码
这种情况下默认使用尾插法 , 时间复杂度O(1) , 如果需要扩容 , 则使用Arrays.copy()进行深拷贝 , 以此来生成新的数组 , 然后把旧数组的值赋值给新数组 , 并且如果此时数组的长度+1小于默认容量 , 那么就会初始化一个容量为10的数组 , 并不是在new ArrayList<>()的时候就会生成默认容量的数组
/** * Appends the specified element to the end of this list. * * @param e element to be appended to this list * @return <tt>true</tt> (as specified by {@link Collection#add}) */ public boolean add(E e) { // 判断是否需要扩容 ensureCapacityInternal(size + 1); // Increments modCount!! elementData[size++] = e; return true; } private void ensureCapacityInternal(int minCapacity) { if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { // 从默认容量和当前ArrayList的size + 1选一个最大的最为扩容阈值 minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity); } ensureExplicitCapacity(minCapacity); } private void ensureExplicitCapacity(int minCapacity) { // 将修改次数(modCount)自增1 modCount++; // overflow-conscious code // 判断是否需要扩容 , 如果当前容量减去数组容量大于0 , 表示需要扩容 if (minCapacity - elementData.length > 0) grow(minCapacity); } /** * Increases the capacity to ensure that it can hold at least the * number of elements specified by the minimum capacity argument. * * @param minCapacity the desired minimum capacity */ private void grow(int minCapacity) { // overflow-conscious code // 旧的容量 int oldCapacity = elementData.length; // 新容量等于旧容量 + 旧容量左移1位 int newCapacity = oldCapacity + (oldCapacity >> 1); if (newCapacity - minCapacity < 0) // 如果新容量 - 需要扩容阈值 < 0 那么新容量就使用扩容阈值 newCapacity = minCapacity; if (newCapacity - MAX_ARRAY_SIZE > 0) // 如果新容量 - 数组最大长度 > 0 , 那么就需要另外判断 newCapacity = hugeCapacity(minCapacity); // minCapacity is usually close to size, so this is a win: 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; }
旧数组值赋值给新数组之后可能会出现三种情况 :
如果旧数组之前经过多次年轻代的GC(yongGC)没有被清理掉的话 ,旧数组就会进入老年代 , 那么生成新数组之后 ,旧数组会被老年代GC掉
如果旧数组经过多次yongGC , 但是没有符合进入老年代的条件及每次yongGC都会在Survivor 0 ,Survivor 1进行移动 , 每移动一次同时记录次数, 达到15次 , 才会被放入老年代 ,这个时候就会被yongGC
如果新数组分配的和旧数组是同一块地址 ,这个时候不需要进行回收
add(int index, E element)源码
如果使用的这个add方法 , 那么将会在数组指定位置插入一个新元素 , 其它元素向后移动 ,此时时间复杂度为O(N) , 扩容机制和add(E e)方法是相同的
public void add(int index, E element) { // 如果index大于当前ArrayList的size , 或者小于0 , 则抛异常 rangeCheckForAdd(index); // 判断是否需要扩容 ensureCapacityInternal(size + 1); // Increments modCount!! // 使用System.arraycopy 将需要插入的位置(index)后面的元素统统往后移动一位 System.arraycopy(elementData, index, elementData, index + 1, size - index); // 将新的数据内容存放到数组的指定位置(index)上 elementData[index] = element; size++; }
get(int index)源码
数组的查询是通过一个公式来计算的 :a[n] = 起始地址 + (n * 字节数) , n表示的就是下标 , 比如我们此时要寻找下边为2的元素 , 那么通过公式计算 , 100 + (2*4) , 那么找到的就是地址为108的元素 , 也就是说5 , 因为它有一个公式 , 经过计算 , 可以很快的找到这个元素的地址 , 所以它的查询的时间复杂度是o(1) , 因此使用get方法随机访问元素的时候 , 时间复杂度为O(1)
public E get(int index) { // 检查下标是否越界 rangeCheck(index); return elementData(index); } E elementData(int index) { // 返回下标对应的元素 return (E) elementData[index]; }
contains方法
调用indexOf方法,遍历数组中的每一个元素作对比,如果找到对于的元素,则返回true,没有找到则返回false
public boolean contains(Object o) { return indexOf(o) >= 0; } 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; }
remove(int index)源码
根据下标删除的情况下时间复杂度分为几种情况考虑 : 如果使用尾删法及删除最后一个元素 , 那个时间复杂度为O(1) , 不涉及到其他元素的移动 , 否则时间复杂度为O(N) , 将会涉及到其他元素的移动
public E remove(int index) { // 检查下标是否越界 rangeCheck(index); // 修改次数 + 1 modCount++; // 根据下标获取需要修改的值 E oldValue = elementData(index); // 判断是否需要移动数组元素 int numMoved = size - index - 1; if (numMoved > 0) // 使用System.arraycopy 将需要插入的位置(index)后面的元素统统往前移动一位 System.arraycopy(elementData, index+1, elementData, index, numMoved); // 不需要移动元素的话 , 把最后一位置为空 elementData[--size] = null; // clear to let GC do its work return oldValue; }
remove(Object o)源码
如果使用这种方法进行删除元素 , 那么时间复杂度为O(N) , 涉及到元素的比阿尼寻找以及移动
public boolean remove(Object o) { // 循环遍历所有对象,得到对象所在索引位置,然后调用fastRemove方法,执行remove操作 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 }
clear()源码
添加操作次数(modCount),将数组内的元素都置空,等待垃圾收集器收集,不减小数组容量。
public void clear() { modCount++; // clear to let GC do its work for (int i = 0; i < size; i++) elementData[i] = null; size = 0; }
subList(int fromIndex, int toIndex)源码
我们看到代码中是创建了一个ArrayList 类里面的一个内部类SubList对象,传入的值中第一个参数时this参数,其实可以理解为返回当前list的部分视图,真实指向的存放数据内容的地方还是同一个地方,如果修改了sublist返回的内容的话,那么原来的list也会变动。
public List<E> subList(int fromIndex, int toIndex) { subListRangeCheck(fromIndex, toIndex, size); return new SubList(this, 0, fromIndex, toIndex); }
iterator()源码
interator方法返回的是一个内部类,由于内部类的创建默认含有外部的this指针,所以这个内部类可以调用到外部类的属性
public Iterator<E> iterator() { return new Itr(); }
一般我们调用interator之后会进行集合的遍历 , 这里使用next做遍历的时候有个需要注意的地方,就是调用next的时候,可能会引发ConcurrentModificationException,当修改次数,与期望的修改次数(调用iterator方法时候的修改次数)不一致的时候,会发生该异常,详细我们看一下代码实现
@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]; } final void checkForComodification() { if (modCount != expectedModCount) throw new ConcurrentModificationException(); }
expectedModCount这个值是在用户调用ArrayList的iterator方法时候确定的,但是在这之后用户add,或者remove了ArrayList的元素,那么modCount就会改变,那么这个值就会不相等,将会引发ConcurrentModificationException异常,这个是在多线程使用情况下,比较常见的一个异常。
System.arraycopy 方法
参数 说明
src 原数组
srcPos 原数组
dest 目标数组
destPos 目标数组的起始位置
length 要复制的数组元素的数目
Arrays.copyOf方法
参数
说明
original 要复制的数组
newLength 要返回副本的长度
newType 要返回副本的类型
其实Arrays.copyOf底层也是调用System.arraycopy实现的源码如下:
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, Math.min(original.length, newLength)); return copy; }
小结
ArrayList底层是基于动态数组实现的 , 可以进行动态扩容
ArrayList自己实现了序列化和反序列化方法 , 因为它实现了writeObject和readObject方法
ArrayList在编码时如果知道数据比较多 , 那么可以提前传入容量值 , 避免进行频繁扩容
ArrayList使用尾插法的时间复杂度为O(1) , 指定位置插入时间复杂度为O(N)
ArrayList支持随机访问 , 其时间复杂度为O(1)
ArrayList使用尾删法时 , 时间复杂度为O(1) , 并且会把最后一个元素置位null , 其它删除时间复杂度为O(N) , 因为会涉及到元素的移动以及元素的遍历
ArrsyList是线程不安全的