Java中最常见的数据结构莫过于ArrayList与HashMap了
首先看看ArrayList类:
对于数据我们做的最多的 无非为 增删查 三项了,那么我们首先看看ArrayList增删查方法。
增 add方法:
add(E e)方法在末尾加入一个元素e
add(int index,E e)方法在指定位置加入一个元素。
删 remove方法:
remove(int index) 指定位置的删除
remove(E e)指定元素的删除
查 get方法
get(int index)查找指定位置的元素
ArrayList 从名字我们能看出 底层的基本数据结构是由数组实现的,而数组的特性是随机查找的性能好,对于增加和删除操作效率较低,所以ArrayList 似乎是更适合用于数据查找的一种结构;为了应对改善这种性能缺陷我们有了另一个类 LinkedList
依然来看 增删查 三种方法
看方法似乎是一样的,那么ArrayList与LinkedList实现这些方法的区别在哪呢?
他们的区别也就是性能差异的地方了 首先看 get方法
左边为ArrayList的实现,右边为LinkedList的实现
可以看到 ArrayList的指定位置查找直接用数组的随机查找能力返回了结果,而LinkedList 进入了node(index)来查找结果
node(index)中对链表进行了一次遍历,来查找目标位置的元素,当next的次数到达index 指定的位置时返回结果
时间复杂度数组的随机查找为 O(1) 而链表实现的时间复杂度为遍历的O(n)
在看添加方法
可以看到 每次add操作 ArrayList 都会对当前elementData数组的长度进行操作
而LinkedList则只对当前节点的前驱和后继进行了处理
我们知道数组一旦分配了空间长度就不能变了,但是ArrayList创建的时候我们并不知道需要的长度是多少,而底层又是不能改变长度数组结构,这里是怎么处理的呢?
数据长度的变化由add/remove 引起所以我们由add方法入手
可以看见在加入新元素时 首先进行了一次ensureCapacityInternal(size+1)操作
直觉这里是对底层数组的扩容操作 进去看一下
ensureCapacityInternal中
首先判断了 当前数组是否是默认空数组,如果是比较新的容量与默认容量的大小来决定目标容量minCapacity。
接着进入ensureExplicitCapacity
判断了一下 目标容量minCapacity是否比当前已有容量大,如果需要扩容执行grow方法
进入grow方法中
旧容量oldCapacity 为现有元素的长度
新容量newCapacity = oldCapacity+(oldCapacity>>1);
即1.5倍原有容量
最终使用Arrays.copyOf(sourceArray,capacity);创建了新的数组
在创建新的数组后,使用System.arraycopy方法将原数组的数据复制到新的数组当中,一路返回到add(index,e)中
此时完成了对数组的扩容,现在可以把新元素加入到数组中了,然后对记录元素数量的size 进行+1操作,完成添加流程
再看remove方法
可以看到,remove方法中并没有对已有数组的容量进行操作...
难道 我扩容到一个巨大的数组后再删除 ,这些空间就不会释放了吗?
绝望之时我看到了这个方法。
看到 这里创建了一个与真实元素数量相同的数组来代替原有数据数组,去除了申请的那些预留空间 debug一下看看情况
trimToSize之前 elementData.length=15
trimToSize之后 elementData.length=1;
最后我们看一下线程安全的ArrayList
首先是 Vector类 几乎不会使用
对方法进行加锁处理也就是对当前对象加了锁,该对象的其他方法也会受到限制。
CopyOnWriteArrayList
可以看到我们锁住的对象是一个空的object
这个要比vector好,当前list对象的其他方法不会受到限制