第一次写了这么多的文字,也是帮助自己记录一下学习的过程。如果有什么错误的地方请各位指出,谢谢!
最近准备跳槽(哈哈),顺手记录一下ArrayList。
重要的成员变量:
//默认初始容量为10
private static final int DEFAULT_CAPACITY = 10;
//用于空实例的共享空数组实例
private static final Object[] EMPTY_ELEMENTDATA = {};
//默认大小的空数组实例。
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
//ArrayList 的元素存储在其中的数组缓冲区。
transient Object[] elementData;
//它实际包含的元素的个数
private int size;
//ArrayList的最长的长度,因为有些VM会在数组中保留一些头字所以预留了
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
DEFAULTCAPACITY_EMPTY_ELEMENTDATA和EMPTY_ELEMENTDATA区别:
调用ArrayList的构造时,无参构造elementData =DEFAULTCAPACITY_EMPTY_ELEMENTDATA,有参构造传0时,elementData = EMPTY_ELEMENTDATA。DEFAULTCAPACITY_EMPTY_ELEMENTDATA第一次add扩容时为10,EMPTY_ELEMENTDATA第一次add为1。这两个是用来共享给空数组的,避免了创建了很多ArrayList,而每个ArrayList中都有空数组的问题,提高了性能。
构造方法:
public ArrayList(int initialCapacity) {
if (initialCapacity > 0) {
this.elementData = new Object[initialCapacity];
} else if (initialCapacity == 0) {
this.elementData = EMPTY_ELEMENTDATA;
}
}
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
构造方法解析:
- 无参构造:会将默认容量的共享空数组的引用赋值给elementData,add时,判断elementData的引用是否是默认容量共享空数组,如果是,就会将elementData扩展为长度为默认容量(10)的数组,然后才添加元素。
- 有参构造:
- initialCapacity大于0:直接新建一个长度为initialCapacity的数组,并将引用赋值给elementData,这里就相当于直接进行了扩展了。初始化完成elementData的长度就已经是initialCapacity了,直到add到超出了该长度时才会进行扩展。
- initialCapacity等于0:因为initialCapacity等于0,所以elementData的数组应该为空数组,这里就直接将EMPTY_ELEMENTDATA的引用赋值给elementData,扩展时也会根据实际需要的大小进行在扩展。
方法
add:add方法分为确保容量、赋值、增加实际元素数量(size)
- 确保容量:添加元素前必须确保elementData中有足够的空间来存储元素。首先得到添加元素需要的最小容量=size+1,然后判断elementData是否是默认容量的空数组,如果是那么说明elementData应该再第一次扩展时将长度扩展为默认容量(10),然后取默认容量和需要的最小容量的较大值作为elementData应该有的长度,然后这个长度和elementData的实际长度进行比较,如果这个长度大于实际长度就会对elementData进行扩展。
- 将e(需要添加的元素)添加到size+1的位置上面。
扩容机制:
从下面这行代码可以看出容量计算方式为:原容量 + 原容量/2
所以如果第一次为0,则计算得出1,
为1,则计算得出2,
为2,则计算得出3,
为3,则计算得出4,
为4,则计算得出6,
…
为10,则计算得出15,
为15,则计算得出22
int newCapacity = oldCapacity + (oldCapacity >> 1);
//将指定的元素添加到此列表的末尾
public boolean add(E e) {
ensureCapacityInternal(size + 1);
elementData[size++] = e;
return true;
}
//在指定下标插入元素
public void add(int index, E element) {
//index不能大于elementData中实际的数据的数量,也不能小于0
rangeCheckForAdd(index);
//确保内部容量,如果需要会进行扩容
ensureCapacityInternal(size + 1);
//对elementData进行复制,因为数组的length一定大于size,所以不会
//出现下标越界
System.arraycopy(elementData, index, elementData, index + 1,
size - index);
//进行赋值,并且size+1
elementData[index] = element;
size++;
}
//以下两个方法的步骤:1.确保内部容量 2.数组复制 3.增加size
//在指定下标依次插入c的所有元素
public boolean addAll(int index, Collection<? extends E> c)
//在后面依次插入c的所有元素
public boolean addAll(Collection<? extends E> c)
其他方法:
//清空所有。循环遍历size次,将所有下标对应的元素设置为null,并将size设为0,并不会修改elementData的长度
public void clear() {
for (int i = 0; i < size; i++)
elementData[i] = null;
size = 0;
}
//返回此ArrayList实例的浅表副本。 浅拷贝,拷贝对象引用
public Object clone()
//如果此列表包含指定的元素,则返回true 。调用indexOf方法。大于0说明存在指定的元素
public boolean contains(Object o) {
return indexOf(o) >= 0;
}
//返回指定元素在arrayList中的下标,如果o为null就返回第一个null的下标,如果不为null,则返回第一个equals相等的元素的下标。都没有返回-1
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;
}
//forEach方法:遍历。
arrayList.forEach( it -> {
System.out.println("it = " + it);
});
//获取指定下标的元素
public E get(int index) {
//检验下标,不能大于等于size
rangeCheck(index);
//elementData(index)是取出(E) elementData[index]返回
return elementData(index);
}
//返回elementData的元素个数是否为0
public boolean isEmpty() {
return size == 0;
}
//和indexOf一样只是遍历的时候从size开始遍历
public int lastIndexOf(Object o)
//删除指定下标的元素
public E remove(int index) {
rangeCheck(index);//检查不能大于等于size
E oldValue = elementData(index);//先取出需要删除的元素
//将index后面的元素向前移动一个位置
int numMoved = size - index - 1;
if (numMoved > 0){//如果需要移动的位置大于0就复制数组
System.arraycopy(elementData, index+1,
elementData, index,numMoved);
}
// 将原来的最后一个元素的下标对应设置为null,并size--
elementData[--size] = null;
return oldValue;//返回删除了的元素
}
//正向遍历,如果equals相等就删除第一个
public boolean remove(Object o)
//设置指定下标的元素为element
public E set(int index, E element)={elementData[index] = element}
size();//获取elementData中的元素的总个数
//排序,返回正数升序,负数降序
sort((o1, o2) -> {
return o1-o2;
});
常见面试题
ArrayList是什么
ArrayList就是有序的数组列表,不能存放基本类型,它的主要底层实现是数组。
ArrayList与LinkedList的区别?
1、ArrayList的查找和访问元素的速度较快,但新增,删除的速度较慢,因为需要确保容量足够,所以有可能需要扩容有,操作还会移动元素位置,所以速度较慢。LinkedList的查找和访问元素的速度较慢,但是他的新增,删除的速度较快,因为是使用链表的形式。
2、ArrayList需要分配一块连续的内存空间,LinkedList不需要连续的内存空间
3、两个线程都不安全
ArrayList(int initialCapacity)会不会初始化数组⼤⼩?
当initialCapacity大于0时会直接初始化elementData的大小
当initialCapacity等于0时是使用的共享空数组,避免存在很多的空数组
当调用无参构造时,elementData使用的是默认容量的共享空数组。
两个空数组的区别在于:默认容量的共享空数组第一次扩容时elementData的长度为10,而共享空数组第一次扩容时根据 原长度 + 原长度/2 计算出的长度进行扩展。
ArrayList的遍历为什么快?
因为底层是数组,数组的内存空间是连续的,CPU读取时会缓存那一块内存片段,而数据在内存中都是连续的,所以遍历快。
ArrayList容量的增加机制
如果使用的无参构造创建,第一次add时扩展长度为10(默认容量)。如果有参构造传递的初始容量大于0,就会直接给elementData分配空间,创建这个初始容量长度的数组。如果等于0,那么在add扩展时就会从0开始计算需要扩展的新的容量。当有参构造参数为0或者其他方式第二次扩展时,得到的新的容量 = 旧的容量 + 旧的容量/2。ArrayList类中说了了MAX_ARRAY_SIZE=Integer.MAX_VALUE - 8,vm可能会保留一些头字,所以将最大的长度设置为此值。如果最小需要的容量大于了MAX_ARRAY_SIZE则新的容量为Integer.MAX_VALUE(可能会导致 OutOfMemoryError),否则新的容量就是MAX_ARRAY_SIZE。
ArrayList的大小是如何自动增加的?
在进行add操作时会先检查elementData的容量来判断是否需要进行扩展以加入这个数据。如果容量不足,就会新建一个容量更大的数组。然后将原数组的数据拷贝到新数组中。