集合类中线程安全的就只有,vector,hashtable,concurrentHashmap
ArrayList:
ArrayList
实现于 List
、RandomAccess
接口。可以插入空数据,也支持随机访问。
ArrayList的初始容量为10,这里我们主要了解一下ArrayList的扩容机制
对于一个普通的数组,我们一个一个添加的时候,size就会每加一个就扩容一次
添加到末尾
public boolean add(E e) { ensureCapacityInternal(size + 1); // 在size=10的时候就会开始扩充了,第一次扩充到15,就不用每次加一这样扩容了 //这里看到ArrayList添加元素的实质就相当于为数组赋值 elementData[size++] = e; return true;
}
在任意位置插入
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++; }
扩容校验:
1.得到最小扩容量
private void ensureCapacityInternal(int minCapacity) { if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { // 获取默认的容量和传入参数的较大值 minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity); } ensureExplicitCapacity(minCapacity); }
2.判断是否需要扩容
private void ensureExplicitCapacity(int minCapacity) { modCount++;//版本号增加 if (minCapacity - elementData.length > 0) //调用grow方法进行扩容,调用此方法代表已经开始扩容了 grow(minCapacity); }
private void grow(int minCapacity) { // oldCapacity为旧容量,newCapacity为新容量 int oldCapacity = elementData.length; //将oldCapacity 右移一位,其效果相当于oldCapacity /2, //我们知道位运算的速度远远快于整除运算,整句运算式的结果就是将新容量更新为旧容量的1.5倍, int newCapacity = oldCapacity + (oldCapacity >> 1); //然后检查新容量是否大于最小需要容量,若还是小于最小需要容量,那么就把最小需要容量当作数组的新容量, if (newCapacity - minCapacity < 0) newCapacity = minCapacity;//没有超过的话就按定义的最大容量 //再检查新容量是否超出了ArrayList所定义的最大容量, //若超出了,则调用hugeCapacity()来比较minCapacity和 MAX_ARRAY_SIZE, //如果minCapacity大于MAX_ARRAY_SIZE,则新容量则为Interger.MAX_VALUE,否则,新容量大小则为 MAX_ARRAY_SIZE。 if (newCapacity - MAX_ARRAY_SIZE > 0) newCapacity = hugeCapacity(minCapacity); elementData = Arrays.copyOf(elementData, newCapacity); }
对于扩容的总结:
每次都会扩容到1.5倍,不会每次添加都会扩容了,效率会增加很多,然后在大量的数据的时候多用ensureCapacity这个方法,这个方法是用来给用户用的,防止反复的扩容可以增加效率。
ArrayList序列化问题:
由于 ArrayList 是基于动态数组实现的,所以并不是所有的空间都被使用。因此使用了 transient
修饰,可以防止被自动序列化。
transient Object[] elementData;
elementData定义为transient的优势,自己根据size序列化真实的元素,而不是根据数组的长度序列化元素,减少了空间占用。
private void writeObject(java.io.ObjectOutputStream s) throws java.io.IOException{ // 防止序列化期间有修改 int expectedModCount = modCount; // 写出非transient非static属性(会写出size属性) s.defaultWriteObject(); // 写出元素个数 s.writeInt(size); // 依次写出元素 for (int i=0; i<size; i++) { s.writeObject(elementData[i]); } // 如果有修改,抛出异常 if (modCount != expectedModCount) { throw new ConcurrentModificationException(); } } private void readObject(java.io.ObjectInputStream s) throws java.io.IOException, ClassNotFoundException { // 声明为空数组 elementData = EMPTY_ELEMENTDATA; // 读入非transient非static属性(会读取size属性) s.defaultReadObject(); // 读入元素个数,没什么用,只是因为写出的时候写了size属性,读的时候也要按顺序来读 s.readInt(); if (size > 0) { // 计算容量 int capacity = calculateCapacity(elementData, size); SharedSecrets.getJavaOISAccess().checkArray(s, Object[].class, capacity); // 检查是否需要扩容 ensureCapacityInternal(size); Object[] a = elementData; // 依次读取元素到数组中 for (int i=0; i<size; i++) { a[i] = s.readObject(); } } }
关于ArrayList的克隆的问题:
其中的源码:
public Object clone() { try { ArrayList<?> v = (ArrayList<?>) super.clone(); //Arrays.copyOf功能是实现数组的复制,返回复制后的数组。参数是被复制的数组和复制的长度 v.elementData = Arrays.copyOf(elementData, size); v.modCount = 0; return v; } catch (CloneNotSupportedException e) { // 这不应该发生,因为我们是可以克隆的 throw new InternalError(e); } }
用一个例子理解ArrayList的复制:
public static void main(String[] args) { ArrayList<Person> a=new ArrayList<>(); Person b=new Person("lijun",21); a.add(b); ArrayList<Person> c=(ArrayList<Person>) a.clone(); System.out.println(a==c); System.out.println(a.toArray(new Person[0])==c.toArray(new Person[0])); System.out.println(a.get(0)==c.get(0)); }
false
false
true
其中基本上可以算是深复制了,已经重新生成了ArrayLIst了,但是ArrayList里面的数组是深复制,因为copyOf重新生成了一个新的数组,但是数组指向的仍然是原来数组里面的对象。
public static void main(String[] args) { ArrayList<Person> a=new ArrayList<>(); Person b=new Person("lijun",21); ArrayList<Person> c=(ArrayList<Person>) a.clone(); c.get(0).setName("yang"); System.out.println(a.get(0)); }
输出:yang,所以进一步验证了我上面的结论。
ArrayList里面有几个求并交差的函数:
// 并集 // list1.addAll(list2); // 交集 //list1.retainAll(list2); // 差集 // list1.removeAll(list2);
batchremove源码:
https://blog.csdn.net/weixin_40841731/article/details/85263889
关于ArrayList的几个点:
1.ArrayList中,默认的大小是10,当你使用new ArrayList();时并不会立即为数组分配大小为10的空间,而是等插入第一个元素时才会真正分配,这样做是为了节约内存空间。
2.维护内部数组时,使用的是Arrays.copyOf()方法和System.arraycopy()方法。
3.在扩容时,默认的扩容因子是1.5,每次需要扩容时,会将原数组大小的1.5倍和实际需要的数组空间进行比较,从中取最大值作为数组大小。然后新建一个数组,把原数组中的所有元素复制到新数组中去。所以扩容其实是最耗费时间的操作,不仅仅需要重新分配空间,而且需要重新赋值。
4.多用ensureCapacity,防止频繁扩容。
5.modCount标识,使用迭代器的不要尝试修改,会报错。
vector:
基本上就上在ArrayList的所有方法上加了Synchronized关键字
LinkedList:
LinkedList是一个实现了List接口和Deque接口的双端链表。 LinkedList底层的链表结构使它支持高效的插入和删除操作,另外它实现了Deque接口,使得LinkedList类也具有队列的特性;
LinkedList主要的优点还是插入删除很方便(仅仅就是移动指针)。
另外可以很容易的实现队列,栈这些数据结构。
对于查询,有利用到双向链表的特性:
public E get(int index) { checkElementIndex(index); return node(index).item; } Node<E> node(int index) { // assert isElementIndex(index); if (index < (size >> 1)) { Node<E> x = first; for (int i = 0; i < index; i++) x = x.next; return x; } else { Node<E> x = last; for (int i = size - 1; i > index; i--) x = x.prev; return x; } }
上述代码,利用了双向链表的特性,如果index
离链表头比较近,就从节点头部遍历。否则就从节点尾部开始遍历。使用空间(双向链表)来换取时间。
node()
会以O(n/2)
的性能去获取一个结点:如果索引值大于链表大小的一半,那么将从尾结点开始遍历