文章目录
ArrayList的扩容机制
版本区别: jdk1.7之前就是初始化一个初始长度为10的数组, 1.8 就是初始化一个初始长度为0的数组.
初始容量的选择:ArrayList()无参构造会使用长度为零的数组
ArrayList(int initCapacity)带参构造会使用长度为指定参数的数组
public ArrayList(Collection<? extends E> c) 会使用容器c的大小作为数组长度
add(Object o) 方法首次扩容为10,再次扩容为上次容量的1.5倍
1. 首次容量为0;
2. 调用add方法,加入第一个元素时数组扩容为10.
3. 继续调用add方法,当加入的元素超过10时,重新定义一个原数组1.5倍长的数组, 调用System.copy本地方法 , 将原数组中的数据拷贝到新数组.
//不是 扩容后=扩容前*1.5
//而是 扩容后=扩容前+扩容前>>1 ,采用移位运算,右移一位,相当于除以2,防止出现小数
4. 把ArrayList的指针指向新数组.
addAll(Collection c)没有元素时,扩容为Math.max(10,实际元素个数),有元素时为Math.max(原容量的1.5倍,实际元素个数)
//原始数组已经有10个元素
//使用addAll(List.of(1,2,3)),扩容为15 原因:Math.MAX(下次扩容的长度:15,实际长度:13)
//使用addAll(List.of(1,2,3,4,5,6)),扩容为16 原因:Math.MAX(下次扩容的长度:15,实际长度:16)
Iterator的FailFast和FailSafe
FailFast:
一旦发现遍历的同时其他人来修改,则立即抛出异常------>ArrayList
FailSafe:
遍历的同时其他人来修改,则牺牲一致性来保证遍历的完成,即只能遍历出修改之前的数组元素---->CopyOnWriteArrayList
ArrayList和LinkedList的比较
// ArrayList:
// 基于数组,需要连续内存
// 随机访问快(需根据下标访问)
// 尾部插入与删除性能可以,其他部分插入删除都会移动数据,性能低,其他部位插入是直接复制一份新的数组,将待插入元素索引后面的元素后移
// 可以利用CPU缓存,局部性原理
// LinkedList:
// 基于双向链表,无需连续存储
// 随机访问慢(因为链表的特性,每个节点只存储相邻节点的数据,所有需要从第一个元素开始沿着链表查找)
// 头尾插入和删除性能都高
// 占用内存多
// 两着都是线程不安全的, 需要线程安全的情况下需要使用Vector或者CopyOnWriteList
关于查询:
如果都是根据下标查找,那么ArrayList的查询效率高于LinkedList, 因为链表嘛, 不像redis底层的跳表一样, 可以使用多级节点查找
如果根据查找的内容来查询,他们两个效率是一样的,时间复杂度都是O(n),应该使用HashMap或者TreeMap这样的数据结构来实现根据内容查询
关于插入
头部插入,LinkedList速度快于ArrayList,因为ArrayList要复制一份新的数组,新插入元素在索引0,其他原来的元素索引后移一位
尾部插入,ArrayList的速度略快与LinkedList,ArrayList不涉及数组的复制移动,索引一定位直接插入
中间插入,ArrayList的速度远高于LinkedList,因为LinkedList要调用next()方法遍历查找要插入的位置,查找到之后才能进行插入。
关于内存的局部性原理
读取数组某个元素时,因为数组是连续的,CPU会假设读取时会访问到相邻的其他元素,CPU缓存会一并读取此元素相邻的其他元素,
读取链表的某个元素时,因为链表是不连续的,CPU缓存读取某节点元素时会读取到与次节点不相邻的元素,造成缓存空间的浪费,导致下次读取很可能覆盖掉之前已经缓存的数据
关于占用内存
LinkedList底层是由一个个Node对象组成的,每个Node对象由元素,上一个指针,下一个指针组成,占用内存很大
public class LinkedList<E>
extends AbstractSequentialList<E>
implements List<E>, Deque<E>, Cloneable, java.io.Serializable
{
transient int size = 0;
transient Node<E> first;
transient Node<E> last;
}
private static class Node<E> {
E item;
Node<E> next;
Node<E> prev;
Node(Node<E> prev, E element, Node<E> next) {
this.item = element;java
this.next = next;
this.prev = prev;
}
}
如何复制某个ArrayList到另一个ArrayList中去?写出你的代码?
回答
使用clone()方法,比如ArrayList newArray = oldArray.clone();
使用ArrayList构造方法,比如:ArrayList myObject = new ArrayList(myTempObject);
使用Collections的copy方法。java
clone()和构造方法都是浅拷贝,
Collections.copy(List target, List src)是深拷贝, 拷贝后的数组长度必须大于或者等于源数组.
Collection.copy()注意事项
ArrayList<Integer> arrayList=new ArrayList<Integer>(5);
arrayList.add(1);java
arrayList.add(2);
// 直接在构造方法里面声明长度并不会初始化长度,仍然为0,只是声明的数组容量,而不是实际长度
// ArrayList<Integer> list=new ArrayList<>(5);
// Arrays.asList(new Integer[arrayList.size()]) 会初始化全为null的一个数组,长度为arrayList的长度,再转为list
List<Integer> list = Arrays.asList(new Integer[arrayList.size()]);
Collections.copy(list,arrayList);
浅拷贝与深拷贝
基本类型 :
整型: byte, short, int ,long
浮点型: float,double
字符型: char
布尔型: boolean
引用数据类型:
类,接口,数组
- 浅拷贝就是创建一个新对象, 这个对象有着原始对象属性值的一份精确拷贝. 如果属性是基本类型, 拷贝的就是基本类型的值, 如果属性是引用类型, 拷贝的就是内存地址.
- 深拷贝就是将一个对象从内存中完整的拷贝一份出来, 从堆内存中开辟了一个新的区域存放新对象.
- 浅拷贝之后虽然两个对象的引用不同了,但指向的还是同一片内存空间, 一旦其中一个改变了内存中的数据, 另一个也会随之改变.
- 深拷贝之后, 新对象与就对象完全是两个不同的对象, 每个对象各自使用自己的内存空间,互不影响.
怎么确保一个集合不能被修改?
可以使用 Collections. unmodifiableCollection(Collection c) 方法来创建一个只读集合,这样改变集合的任何操作都会抛出
Java. lang. UnsupportedOperationException 异常
ArrayList频繁扩容导致添加性能急剧下降,如何处理?
使用ArrayList时,可以 new ArrayList(大小)构造方法来指定集合的大小,以减少扩容的次数,提高写入效率。
ArrayList线程安全吗
不安全, ArrayList和LinkedList都是不安全的.
当开启多个线程操作List集合,向ArrayList中增加元素,同时去除元素,可能导致有些元素为null, 或者导致数组下标越界异常
-
一般使用Vector这个线程安全的数组容器,它给所有方法都加上了锁
-
或者使用Collections.synchronizedList将ArrayList包装成线程安全的数组容器
-
Vector vector=new Vector(); List<Object> objects = Collections.synchronizedList(new ArrayList<>());
为什么ArrayList线程不安全,我们还使用它?
因为大多数的场景中,查询操作使用频率高,增删操作的使用频率低。 如果涉及频繁的增删,可以使用LinkedList,实际开发过程中还是ArrayList使用最多的。不存在一个集合既查询效率高,又增删效率高,还线程安全的,因为数据结构的特性就是优劣共存的,想找个平衡点很难,牺牲了性能,那就安全,牺牲了安全那就快速。