文章目录
ArrayList
1,甚至可以存放null
2.基于数组是实现数组存储
3.无线程安全控制,即线程不安全,基本等同于Vector ,在多线程情况下不建议ArrayList
数组扩容效率问题
当需要加入的数据量特别多,如果是无参的话,数组的容量是逐渐增加的,那么就会触发很多次的扩容,因为扩容的时候会使用到数组拷贝,这个过程很耗费性能,会导致ArrayList效率下降
//这里是测试当添加很多数据时,ArrayList性能急剧下降的问题
public class AddManyElementProblem {
public static void main(String[] args) {
add1();
}
// 如果指定了容量
public static void add1(){
//创建集合对象
List<String> list = new ArrayList<String>();
//添加元素
list.add("hello");
list.add("PHP");
list.add("Java");
long startTime = System.currentTimeMillis();
//需求:还需要添加10W条数据
for (int i = 0; i < 100000; i++) {
// 优化效果图
// 注意:这种优化方式只针对特定的场景,如果添加的元素是少量的、未知的,不推荐使用
// 4.3 ArrayList插入或删除元素一定比LinkedList慢么?
// 根据索引删除
// 案例:ArrayList和LinkedList对比
list.add(i+"");
}
long endTime = System.currentTimeMillis();
System.out.println("未指定容量: "+ (endTime - startTime));
//创建集合的时候指定足够大的容量
List<String> list1 = new ArrayList<String>(100000);
startTime = System.currentTimeMillis();
for (int i = 0; i < 100000; i++) {
list1.add(i+"");
}
endTime = System.currentTimeMillis();
System.out.println("指定容量: "+ (endTime - startTime));
}
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-t1Kgj232-1658498591138)(images.images/image-20220722213420918.png)]
底层和源码分析
1.维护了一个Objec的数组,transient Object[] elementData
2 当创建对象时,若使用无参构造,则初始容量为0,此时第一次添加就需要扩容为10,如需再次扩容为1.5倍
3.如果使用指定大小构造器,扩容同样1.5
添加方法:
首先确定索引是否范围合理,然后确定容量是否满足,这里的原理是:在极端情况下,此时容量=长度=size,那么当前所需要的最小长度是size+1,假如size+1大于当前长度,就应该扩容。如果不需要扩容,那么直接赋值即可
public void add(int index, E element) {
rangeCheckForAdd(index);//检测索引范围是否正确
ensureCapacityInternal(size + 1); //这里是判断是否需要扩容,原理是:在极端情况下,此时容量=长度=size,那么当前所需要的最小长度是size+1,假如size+1大于当前长度,就应该扩容
System.arraycopy(elementData, index, elementData, index + 1,
size - index);
elementData[index] = element;
size++;
}
modCount记录修改次数,minCapacity-elementData.length若大于零,代表当前长度不够存储
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BEIIKpXh-1658498591138)(images.images/image-20220601104236172.png)]
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1);//原先数组1.5倍,但是第一次是0
if (newCapacity - minCapacity < 0)//如果,
newCapacity = minCapacity;
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);
}
扩容
无参构造创建时容量为0,第一次add时候会扩容,否则他内部数组就是一个空数组,然后每次数组满之后,再次添加,会触发扩容机制,扩容1.5倍
有参构造,创建时容量为传入的值,数组满之后,再次添加,会触发扩容机制,扩容1.5倍。
注意:扩容的时机都是内部数组满了之后,再次add才会扩容
添加流程
ensureCapacityInternal:判断是否扩容
public boolean add(E e) {
ensureCapacityInternal(size + 1); //这里判断是否要扩容,size+1代表着极端情况下最小的容量,因为极端情况下,当前数组满了,所有需要的最小容量是size+1
elementData[size++] = e;
return true;
}
private void ensureCapacityInternal(int minCapacity) {
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
3.确定minCapacity,若使用的是无参构造,那么初始时容量为0,当第一次添加的时候,这里就会返回10。还有一种情况,就是有参构造,但是传值小于DEFAULT_CAPACITY,也会返回DEFAULT_CAPACITY,10。这样做的目的时防止当容量小时,添加元素会触发多次扩容。例如如果传入2,那么在添加了两个元素,就要扩容,然后此时2*1.5=3,添加第四个元素还需要扩容,会触发多次扩容,影响性能。
private static int calculateCapacity(Object[] elementData, int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
return Math.max(DEFAULT_CAPACITY, minCapacity);
}
return minCapacity;
}
4.确定修改次数,再查看当前所需要的minCapacity和数组长度的差,若需要扩容再扩容
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// overflow-conscious code
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1);//每次1.5倍
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;//第一次调用这个方法,因为oldCapacity为0
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// minCapacity is usually close to size, so this is a win:若超越了最大的容量进入这里
//将前newCapacity个拷贝至elementData
elementData = Arrays.copyOf(elementData, newCapacity);
}
有参构造
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到另一个ArrayList中去?
使用clone()方法
使用ArrayList构造方法
使用addAll方法
线程安全问题
当然不是线程安全的,线程安全版本的数组容器是Vector。 Vector的实现很简单,就是把所有的⽅法统统加上synchronized就完事了。 你也可以不使⽤Vector,⽤Collections.synchronizedList把⼀个普通ArrayList包装成⼀个线程安全版本 的数组容器也可以,原理同Vector是⼀样的,就是给所有的⽅法套上⼀层synchronized。
ArrayList插入或删除元素一定比LinkedList慢么
结果:
在数据量大的情况下,因为ArrayList底层数组, LinkedList底层双向链表。ArrayList增删时,越靠前头部,增删效率越低,因为ArrayList增删的时候是需要拷贝数组的。而LinkedList当增删、查找效率都不是很高,特别是对象处于链表中部位置
所以当插入删除元素在中间/或者随机查找的时候,数据量大的情况下,ArrayList可能会比LinkedList快。
public void add(int index, E element) {
rangeCheckForAdd(index);
ensureCapacityInternal(size + 1); // Increments modCount!!
System.arraycopy(elementData, index, elementData, index + 1,
size - index;//把索引位置的元素后移一格
elementData[index] = element;
size++;
}
public E remove(int index) {
rangeCheck(index);
modCount++;
E oldValue = elementData(index);
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
return oldValue;
}
ArrayList适合做队列吗
队列一般都是FIFO的,先进先出,如果用ArrayList,那么就需要在数组头部删除,尾部添加,反过来也行,但是都会有一个操作涉及数组拷贝,比较耗费性能
ArrayList的遍历和LinkedList遍历性能⽐较如何
论遍历ArrayList要⽐LinkedList快得多,ArrayList遍历最⼤的优势在于内存的连续性,CPU的内部缓存 结构会缓存连续的内存⽚段,可以⼤幅降低读取内存的性能开销
ArrayList常⽤的⽅法总结
- boolean add(E e)
将指定的元素添加到此列表的尾部。
- void add(int index, E element)
将指定的元素插⼊此列表中的指定位置
- boolean addAll(Collection c)
按照指定 collection 的迭代器所返回的元素顺序,将该 collection 中的所有元素添加到此列表的尾部。
- boolean addAll(int index, Collection c)
从指定的位置开始,将指定 collection 中的所有元素插⼊到此列表中。
- void clear()
移除此列表中的所有元素。
- Object clone()
返回此 ArrayList 实例的浅表副本。
- boolean contains(Object o)
如果此列表中包含指定的元素,则返回 true。
- void ensureCapacity(int minCapacity)
如有必要,增加此 ArrayList 实例的容量,以确保它⾄少能够容纳最⼩容量参数所指定的元素数。
- E get(int index)
返回此列表中指定位置上的元素。
- int indexOf(Object o)
返回此列表中⾸次出现的指定元素的索引,或如果此列表不包含元素,则返回 -1。
- boolean isEmpty()
如果此列表中没有元素,则返回 true int lastIndexOf(Object o) 返回此列表中最后⼀次出现的指定元素的索引,或如果此列表不包含索引,则返回 -1。
- E remove(int index)
移除此列表中指定位置上的元素。
- boolean remove(Object o)
移除此列表中⾸次出现的指定元素(如果存在)。
- protected void removeRange(int fromIndex, int toIndex)
移除列表中索引在 fromIndex(包括)和 toIndex(不包括)之间的所有元素。
- E set(int index, E element)
⽤指定的元素替代此列表中指定位置上的元素。 int size() 返回此列表中的元素数。
- Object[] toArray()
按适当顺序(从第⼀个到最后⼀个元素)返回包含此列表中所有元素的数组。
- T[] toArray(T[] a)
按适当顺序(从第⼀个到最后⼀个元素)返回包含此列表中所有元素的数组;返回数组的运⾏时类型是 指定数组的运⾏时类型。
- void trimToSize()
将此 ArrayList 实例的容量调整为列表的当前⼤⼩。