前言一、基本结构
ArrayListlinkedList 二、增删改查(CRUD)
1.ArrayList
增删查改 2.linkedList
增删改查 三、基于底层数据结构的总结四、ArrayList与Vector的比较五、ArrayList与Array的区别
前言
ArrayList与linkedList的区别常常被人提及,清楚了解二者的区别有利于夯实自己的开发基本功,在合适的场合选择合适的数据结构,能够帮助自己写出更加优质的代码,本文结合二者的源码,对它们进行组成分析和区别分析。
一、基本结构 ArrayListArrayList基于动态数据(顺序表)进行实现,且默认存储的容量为10
linkedList基于双向链表的数据结构进行实现,具有头尾指针,单个结点具有prev指针和next指针
private void add(E e, Object[] elementData, int s) { if (s == elementData.length) elementData = grow(); elementData[s] = e; size = s + 1; } public boolean add(E e) { modCount++; add(e, elementData, size); return true; }
private Object[] grow(int minCapacity) { int oldCapacity = elementData.length; if (oldCapacity > 0 || elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { int newCapacity = ArraysSupport.newLength(oldCapacity, minCapacity - oldCapacity, oldCapacity >> 1 ); return elementData = Arrays.copyOf(elementData, newCapacity); } else { return elementData = new Object[Math.max(DEFAULT_CAPACITY, minCapacity)]; } } private Object[] grow() { return grow(size + 1); }
众所周知,一个数组如果往里面添加值超过了边界,那么便要对这个数组进行扩容;那么我们假设有一个ArrayList,现在里面的元素已经满了,我们再试图往里面添加元素,这时候便要调用add()函数了,那么依照我上文添加的源码,add函数便会去调用grow()函数,那么我们便对grow函数进行一番分析。
那么我们看到,当调用add函数的时候,可能存在两种情况:
1.原有列表不存在元素(未初始化),直接生成一个新的顺序表,大小取默认大小和添加元素大小的最大值
2.原有列表存在元素,这时候需要完成两项工作
对原有的动态数组进行扩容将原有的动态数组的元素添加至新的动态数组
第二步无非就是O(n)级的复制,这里我们重点关注扩容那一步发生了什么,也就是调用ArraysSupport.newLength()那一步发生了什么,来看看源码;
public static int newLength(int oldLength, int minGrowth, int prefGrowth) { // assert oldLength >= 0 // assert minGrowth > 0 int newLength = Math.max(minGrowth, prefGrowth) + oldLength; if (newLength - MAX_ARRAY_LENGTH <= 0) { return newLength; } return hugeLength(oldLength, minGrowth); } private static int hugeLength(int oldLength, int minGrowth) { int minLength = oldLength + minGrowth; if (minLength < 0) { // overflow throw new OutOfMemoryError("Required array length too large"); } if (minLength <= MAX_ARRAY_LENGTH) { return MAX_ARRAY_LENGTH; } return Integer.MAX_VALUE; }
看到这里我们豁然开朗,也感慨Java语言设计者的良苦用心,联系上文grow()函数中传入的两个参数
minCapacity - oldCapacityoldCapacity >> 1//右移得到原来长度的一半长度
我们可以看到,这里生成新长度也分为两种情况
先取新增长度为添加入元素之后的值和原有动态数组的长度一半的最大值若新增长度小于可分配的长度的值,则直接扩容新增长度大小若新增长度大于可分配的长度的值,则取最小分配长度minGrowth,比方说,若此时往数组里只增加1,但是按照第一种分配方式,将原动态数组扩容一半超出内存分配空间,则选择只minLength为1,结果就是将动态数组扩容至最大长度
那么什么情况是动态数组长度的最大值呢,我们这里可以看到是Integer.MAX_VALUE,也就是Integer长度的上界,至于原因是为什么呢,笔者的理解是动态数组是采用Integer作为下表采集的,若超出,则会发生不可预知的错误。
如果要改大,可以将Integer改为其他数据类型,重新生成自己的数据结构,当然这涉及到很多应用上的问题,这里笔者就不赘述了。
public E remove(int index) { Objects.checkIndex(index, size); final Object[] es = elementData; @SuppressWarnings("unchecked") E oldValue = (E) es[index]; fastRemove(es, index); return oldValue; } private void fastRemove(Object[] es, int i) { modCount++; final int newSize; if ((newSize = size - 1) > i) System.arraycopy(es, i + 1, es, i, newSize - i); es[size = newSize] = null; }
这里采用的是覆盖删除的方式,即把要删除的i结点右面的元素利用Copy的方式统一完成移动,时间复杂度为O(n);
查查是比较简单的部分了,因为 ArrayList是基于索引的数据接口,它的底层是类似顺序表的动态数组。它可以以O(1)时间复杂度对元素进行随机访问。
改其实改和查一样,基于顺序表的结构这俩个 *** 作都十分简单,优化也没有大的优化方向了,直接看源码吧
public E set(int index, E element) { Objects.checkIndex(index, size); checkForComodification(); E oldValue = root.elementData(offset + index); root.elementData[offset + index] = element; return oldValue; }
我们看到这里先对输入数据进行了合法性检验,而后利用checkForComodification()保证修改和查询两个线程的一致性,最后进行修改。
2.linkedList 增public boolean add(E e) { linkLast(e); return true; } void linkLast(E e) { final Nodel = last; final Node newNode = new Node<>(l, e, null); last = newNode; if (l == null) first = newNode; else l.next = newNode; size++; modCount++; }
这里插入因为具有head、tail指针的缘故,所以插入始终保持o(1)的复杂度;
删public boolean remove(Object o) { if (o == null) { for (Nodex = first; x != null; x = x.next) { if (x.item == null) { unlink(x); return true; } } } else { for (Node x = first; x != null; x = x.next) { if (o.equals(x.item)) { unlink(x); return true; } } } return false; } E unlink(Node x) { // assert x != null; final E element = x.item; final Node next = x.next; final Node prev = x.prev; if (prev == null) { first = next; } else { prev.next = next; x.prev = null; } if (next == null) { last = prev; } else { next.prev = prev; x.next = null; } x.item = null; size--; modCount++; return element; }
删除的源码因为要用到equals方法,所以对空指针进行了校验,同时保证是否存在空指针的元素,若存在,则对其进行删除;删除则是采用双端链表最基础的删除方式
改public void set(E e) { if (lastReturned == null) throw new IllegalStateException(); checkForComodification(); lastReturned.item = e; }
因为其实linkedList没有下标这个概念,所以其实set方法用的也不多,这里用到的lastReturned,是实现迭代器的一部分,可以理解为上一次访问到的位置,这里主要应用于提高访问效率
查public boolean contains(Object o) { return indexOf(o) >= 0; } public int indexOf(Object o) { int index = 0; if (o == null) { for (Nodex = first; x != null; x = x.next) { if (x.item == null) return index; index++; } } else { for (Node x = first; x != null; x = x.next) { if (o.equals(x.item)) return index; index++; } } return -1; }
查找便是从头节点一直查找到尾节点,引入空指针校验,时间复杂度为O(n)
三、基于底层数据结构的总结综上,我们可以做出如下总结:
随机访问效率:ArrayList 比 linkedList 在随机访问的时候效率要高,因为 linkedList 是线性的数据存储方式,所以需要移动指针从前往后依次查找。增加和删除效率:在非首尾的增加和删除 *** 作,linkedList 要比 ArrayList 效率要高,因为 ArrayList 增删 *** 作要影响数组内的其他数据的下标。综合来说,在需要频繁读取集合中的元素时,更推荐使用 ArrayList,而在插入和删除 *** 作较多时,更推荐使用 linkedList。 四、ArrayList与Vector的比较
线程安全:Vector 使用了 Synchronized 来实现线程同步,是线程安全的,而 ArrayList 是非线程安全的。补充:linkedList同样是非线程安全,只可以在单线程环境下使用。
性能:ArrayList 在性能方面要优于 Vector。
扩容:ArrayList 和 Vector 都会根据实际的需要动态的调整容量,只不过在 Vector 扩容每次会增加 1 倍,而 ArrayList 只会增加 50%。
而ArrayList的扩容机制我已经在上文的add()方法中书写得比较完整了,如果还有不清楚的uu可以去上文查看或留言询问。
五、ArrayList与Array的区别Array 可以存储基本数据类型和对象,ArrayList 只能存储对象。
Array 是指定固定大小的,而 ArrayList 大小是自动扩展的。
Array 内置方法没有 ArrayList 多,比如 addAll、removeAll、iteration 等方法只有 ArrayList 有。
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)