源码级别说明ArrayList和LinkedList的区别

源码级别说明ArrayList和LinkedList的区别

首先二者都是实现了List接口的不同实现,并且都不是线程安全的

区别一 : 数据结构方面

  • ArrayList底层使用的是动态数组来存储元素,初始化大小DEFAULT_CAPACITY = 10;
  • LinkedList底层使用的是双向链表来存储元素

区别二 : 使用场景

  • ArrayList更适用于随即查找
  • LinkedList更适合删除和添加
  • 再因为LinkedList在实现了List接口的基础上还实现了Deque接口,所以LinkedList还被用来做双端队列

区别三 : get方法

  • ArrayList的get方法

    • ArrayList arrayList = new ArrayList();
      arrayList.get(1);
      
  • LinkedList的get方法

    • LinkedList linkedList = new LinkedList();
      linkedList.get(1);
      
    • 看起来和ArrayList的get方法一样都是使用下标直接获取,但是因为LinkedList底层使用的是双向链表来存储元素,所以他需要先进行遍历,才能找到对应下标,源码如下

    • //测试代码
      LinkedList linkedList = new LinkedList();
      linkedList.add(1);
      linkedList.add(2);
      linkedList.add(3);
      linkedList.add(4);
      ->断点 System.out.println(linkedList.get(1));
      //LinkedListt的get方法
      public E get(int index) {
          //检查LinkedList中是否有该下标,没有的话就抛IndexOutOfBoundsException异常
          checkElementIndex(index);
          return node(index).item;
      }
      //node(index).item;
      Node<E> node(int index) {
          // assert isElementIndex(index);保证index是存在的
          //size右移一位,就是size/2
          //判断index和size/2的原因是
          //index<size>>1,就从前往后遍历
          if (index < (size >> 1)) {
              Node<E> x = first;
              for (int i = 0; i < index; i++)
                  x = x.next;
              return x;
          } 
          //index>size>>1,就从后往前遍历
          else {
              Node<E> x = last;
              for (int i = size - 1; i > index; i--)
                  x = x.prev;
              return x;
          }
      }
      
  • LinkedList是有两个查询方法是不需要遍历的,直接定位到就可以获取

    • linkedList.getFirst();
      linkedList.getLast();
      
    • 原因如源码所示

      • //LinkedList自身的属性有first和last,一直持续的记录first和last这两个属性
        //所以不需要遍历就可以直接定位到
        public E getFirst() {
            final Node<E> f = first;
            if (f == null)
                throw new NoSuchElementException();
            return f.item;
        }
        transient Node<E> first;
        public E getLast() {
            final Node<E> l = last;
            if (l == null)
                throw new NoSuchElementException();
            return l.item;
        }
        transient Node<E> last;
        

区别四 : 添加操作

  • ArrayList的添加操作

    • 1 . arrayList.add(element),直接添加到数组的最后一个位置

    • public boolean add(E e) {
          ensureCapacityInternal(size + 1);  // Increments modCount!!
          //直接添加到数组的最后一个位置
          elementData[size++] = e;
          return true;
      }
      
    • 2 . arrayList.add(index,element),添加指定位置

    • public void add(int index, E element) {
          //检查这个index,如果index > size || index < 0
          //throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
          rangeCheckForAdd(index);
          ensureCapacityInternal(size + 1);  // Increments modCount!!
          //数组移动
          System.arraycopy(elementData, index, elementData, index + 1,
                           size - index);
          elementData[index] = element;
          size++;
      }
      
  • LinkedList的添加操作

    • 1 . LinkedList.add(element),直接添加到数组的最后一个位置

    • public boolean add(E e) {
          linkLast(e);
          return true;
      }
      void linkLast(E e) {
          final Node<E> l = last;
          final Node<E> newNode = new Node<>(l, e, null);
          //直接添加到最后一个节点
          last = newNode;
          if (l == null)
              //如果last为空,就直接为头节点
              first = newNode;
          else
              //不为空,就插入到最后
              l.next = newNode;
          size++;
          modCount++;
      }
      
    • 2 . LinkedList.add(index,element),添加指定位置

    • public void add(int index, E element) {
          //检查这个index,如果index > size || index < 0
          //throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
          checkPositionIndex(index);
          //如果index就是Linked的大小,那么就说明是插入到最后一个节点,就直接调用linkLast方法
          if (index == size)
              linkLast(element);
          else
              //如果不是size,那么就说明需要插入到last节点的前面的节点,就调用linkBefore方法
              linkBefore(element, node(index));
      }
      void linkBefore(E e, Node<E> succ) {
          // assert succ != null;
          //取通过下标确定出的节点的前驱节点
          final Node<E> pred = succ.prev;
          final Node<E> newNode = new Node<>(pred, e, succ);
          succ.prev = newNode;
          if (pred == null)
              //如果pred为空,那么就说明pred就是头节点,那就直接为头节点
              first = newNode;
          else
              //如果pred不为空,就直接直接插入到next
              pred.next = newNode;
          size++;
          modCount++;
      }
      

区别五 : 扩容操作

LinkedList是不存在扩容的,主要是ArrayList的扩容操作

  • ArrayList的扩容

    • 例 :

    • //初始化大小为2
      ArrayList arrayList = new ArrayList(2);
      arrayList.add(1);
      arrayList.add(2);
      //尝试去插入第三个元素
      System.out.println(arrayList.add(3));
      
    • 插入方法

    • public boolean add(E e) {
          ensureCapacityInternal(size + 1);  // Increments modCount!!
          elementData[size++] = e;
          return true;
      }
      
    • ensureCapacityInternal()方法,就是确保可以放的下这个元素,就是检查是否需要扩容

    • private void ensureExplicitCapacity(int minCapacity) {
          modCount++;
          // 判断最小容量减去数组长度是否大于0,大于就说明需要扩容
          if (minCapacity - elementData.length > 0)
              //扩容方法
              grow(minCapacity);
      }
      
    • grow(minCapacity)方法

    • private void grow(int minCapacity) {
          // overflow-conscious code
          int oldCapacity = elementData.length;
          //新长度为旧长度+旧长度/2
          int newCapacity = oldCapacity + (oldCapacity >> 1);
          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);
      }
      
    • private static int hugeCapacity(int minCapacity) {
          if (minCapacity < 0) // overflow
              throw new OutOfMemoryError();
          //如果扩容完的最小容量已经超过了数组长度最大值,就直接将其设置为整数的最大值
          return (minCapacity > MAX_ARRAY_SIZE) ?
              Integer.MAX_VALUE :
              MAX_ARRAY_SIZE;
      }
      

区别六 : 删除操作

  • ArrayList的删除

    • public E remove(int index) {
          //检查这个index是否正确
          rangeCheck(index);
          modCount++;
          //取出index的值
          E oldValue = elementData(index);
      	//判断要删除的那个元素是否是否是唯一的那个元素
          int numMoved = size - index - 1;
          //不是就删除,并且复制移动数组
          if (numMoved > 0)
              System.arraycopy(elementData, index+1, elementData, index,
                               numMoved);
          //是的话,就将数组置为空,等待GC
          elementData[--size] = null; // clear to let GC do its work
      	//返回删除的值
          return oldValue;
      }
      
  • LinkedList的删除

    • //其实就是普通的链表删除操作,断开连接
      E unlink(Node<E> x) {
          // assert x != null;
          final E element = x.item;
          final Node<E> next = x.next;
          final Node<E> 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;
      }
      
      

总结

如上所述,即为ArrayList和LinkedList的源码层面的区别分析,那么到底在实际开发中具体使用什么呢?

  • ArrayList比较适合随机查找的原因是因为底层是数组,可以直接通过index确定到具体的element,时间复杂度为0(1)
  • ArrayList的增删改比较耗时原因是不仅是因为需要扩容,而且还有System.arraycopy()和Arrays.copyOf()这两个方法的存在,就是在对数组进行改动的时候,需要将数组进行复制移动,时间复杂度为0(n)
  • LinkedList比较适合增删改的原因就是只是单纯的链表增删改,时间复杂度为0(1),如果是指定下标增删改的话就是0(n),因为需要遍历嘛
  • LindedList查找比较耗时就是因为需要遍历链表,其实他底层还使用了二分法进行遍历优化,但是还是比较慢,时间复杂度为O(n),但是如果是查第一个或者最后一个,因为有getFirst()和getLast()这两个方法t的存在,时间复杂度为0(1)

补充知识

我们会在ArrayList和LinkedList的源码中看到很多的modCount++,这是什么呢?

modCount字面意思就是modify count 修改次数

那为什么要记录修改次数的呢?

  • 我们知道ArrayList和LinkedList不是线程安全的,因此如果在使用迭代器的过程中有其他线程修改了list,那么将抛出ConcurrentModificationException,这就是所谓fail-fast策略。这一策略在源码中的实现是通过 modCount 域,modCount 顾名思义就是修改次数,对HashMap 内容的修改都将增加这个值,那么在迭代器初始化过程中会将这个值赋给迭代器的 expectedModCount。在迭代过程中,判断 modCount 跟 expectedModCount 是否相等,如果不相等就表示已经有其他线程修改了 Map:注意到 modCount 声明为 volatile,保证线程之间修改的可见性。

注意!!!,在JDK5和JDK6的时候,也许是正确的,因为在JDK5和JDK6中变量modCount确实声明为volatile。但在JDK7和JDK8中,已经没有这样声明了!!!!!

  • JDK7和JDK8中,说的是此异常并不总是表示对象已被其他线程同时修改。如果单个线程发出一系列违反对象约定的方法调用,则该对象可能会抛出此异常。 例如,如果线程使用有fail-fast机制的迭代器在集合上迭代时修改了集合,迭代器将抛出此异常。,就是说ConcurrentModificationException这个异常不一定是因为迭代器在集合上迭代时修改了集合,还有可能是因为违反对象约定的方法调用等等

写在最后

以上就是我自己通过阅读源码和构造例子对ArrayList和LinkedList区别的源码级别的说明,希望对大家有帮助

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值