zoukankan      html  css  js  c++  java
  • 【java集合类】ArrayList和LinkedList源码分析(jdk1.8)

    前言:

      ArrayList底层是依靠数组实现的,而LinkedList的实现是含前驱后继节点的双向列表。平时刷题时会经常使用到这两个集合类,这两者的区别在我眼中主要是ArrayList读取节点平均时间复杂度是O(1)级别的,插入删除节点是O(n);LinkedList读取节点时间复杂度是O(n),插入节点是O(1)。

      本文记录我对jdk1.8下的ArrayList和LinkedList源码中主要内容的学习。

    1、ArrayList

    1.1 主要成员变量

     1     //默认容量
     2     private static final int DEFAULT_CAPACITY = 10;
     3     //空的数组
     4     private static final Object[] EMPTY_ELEMENTDATA = {};
     5 
     6     private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
     7     //数据数组
     8     transient Object[] elementData; // non-private to simplify nested class access
     9     //当前大小
    10     private int size;

      主要成员变量如上,最重要的就是size和elementData,其中elementData的修饰transient一开始很令我费解,查阅资料后豁然开朗,transient是为了序列化ArrayList时不用Java自带的序列化机制,而用ArrayList定义的两个方法(writeObject、readObject),实现自己可控制的序列化操作,防止数组中大量NULL元素被序列化。

    1.2 主要方法

    1.2.1 构造方法

      构造方法源码其实很简单,不过在此提及是为了给后面扩容引出一个思考。

     1     public ArrayList(int initialCapacity) {
     2         if (initialCapacity > 0) {
     3             this.elementData = new Object[initialCapacity];
     4         } else if (initialCapacity == 0) {
     5             this.elementData = EMPTY_ELEMENTDATA;
     6         } else {
     7             throw new IllegalArgumentException("Illegal Capacity: "+
     8                                                initialCapacity);
     9         }
    10     }
    11 
    12    
    13     public ArrayList() {
    14         this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    15     }

      源码如上,一个不带参数的构造器,以及带容量参数的构造器。

    1.2.2 add方法

     1     public boolean add(E e) {
     2         ensureCapacityInternal(size + 1);  // Increments modCount!!
     3         elementData[size++] = e;//加到末尾
     4         return true;
     5     }
     6 
     7     private void ensureCapacityInternal(int minCapacity) {
     8         if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
     9             minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
    10         }
    11 
    12         ensureExplicitCapacity(minCapacity);
    13     }
    14 
    15     //判断是否需要扩容
    16     private void ensureExplicitCapacity(int minCapacity) {
    17         modCount++;
    18 
    19         // overflow-conscious code
    20         if (minCapacity - elementData.length > 0)
    21             grow(minCapacity);
    22     }

      add方法中先用ensureCapacityInternal方法,首先判断是否位第一次add,也就是初始化。如果数组位空,那么DEFAULT_CAPACITY就是为10。然后判断是否需要扩容,如果原size+1比数组的length大就需要扩容。(扩容)后把要加的元素加到末尾即可。

    1.2.3 扩容方法(grow)

     1     private void grow(int minCapacity) {
     2         // overflow-conscious code
     3         int oldCapacity = elementData.length;
     4         int newCapacity = oldCapacity + (oldCapacity >> 1);
     5         if (newCapacity - minCapacity < 0)
     6             newCapacity = minCapacity;
     7         if (newCapacity - MAX_ARRAY_SIZE > 0)
     8             newCapacity = hugeCapacity(minCapacity);
     9         // minCapacity is usually close to size, so this is a win:
    10         elementData = Arrays.copyOf(elementData, newCapacity);
    11     }

      扩容方法如上,hugeCapacity判断minCapacity是否大于ArrayList上限,如果大于就返回ArrayList的容量上限。用Arrays.copyof新生成一个数组,而newCapacity = oldCapacity + (oldCapacity >> 1)则是将容量变为原来的1.5倍。

      因为ArrayList默认初始容量为10,每次扩容将容量变为1.5倍,而如果使用ArrayList时要一次性add100个元素,则会频繁用调用扩容方法,因此可以在初始化ArrayList时使用带参的构造函数,定一个合适的容量值。

    1.2.4 remove方法

     1     public E remove(int index) {
     2         rangeCheck(index);
     3 
     4         modCount++;
     5         E oldValue = elementData(index);
     6 
     7         int numMoved = size - index - 1;
     8         if (numMoved > 0)
     9             System.arraycopy(elementData, index+1, elementData, index,
    10                              numMoved);
    11         elementData[--size] = null; // clear to let GC do its work
    12 
    13         return oldValue;
    14     }
    15 
    16     public boolean remove(Object o) {
    17         if (o == null) {
    18             for (int index = 0; index < size; index++)
    19                 if (elementData[index] == null) {
    20                     fastRemove(index);
    21                     return true;
    22                 }
    23         } else {
    24             for (int index = 0; index < size; index++)
    25                 if (o.equals(elementData[index])) {
    26                     fastRemove(index);
    27                     return true;
    28                 }
    29         }
    30         return false;
    31     }

      remove方法主要有两种,一种是根据下标remove,另一种是根据传入的元素匹配删除第一个遇到的该元素,值得一提的是可以删除null元素(总感觉怪怪的)。

    2、LinkedList

      LinkedList是一个双向链表,可以当(双端)队列用。

    2.1 主要成员变量

     1     transient int size = 0;
     2     
     3     //头节点
     4     transient Node<E> first;
     5 
     6     //尾节点
     7     transient Node<E> last;
     8 
     9     //Node节点
    10     private static class Node<E> {
    11         E item;
    12         Node<E> next;//前驱
    13         Node<E> prev;//后继
    14 
    15         Node(Node<E> prev, E element, Node<E> next) {
    16             this.item = element;
    17             this.next = next;
    18             this.prev = prev;
    19         }
    20     }

      带首尾的双向列表,加一个size变量记录当前节点数量,transient修饰和ArrayList中修饰数组的原因是一样的,同样实现了writeObject和readObject,自己实现把size和每一个节点都序列化和反序列化了。

    2.2 主要方法

    2.2.1 add方法

     1     //add方法添加元素到末尾
     2     public boolean add(E e) {
     3         linkLast(e);
     4         return true;
     5     }
     6     
     7     //添加元素至末尾
     8     void linkLast(E e) {
     9         final Node<E> l = last;
    10         final Node<E> newNode = new Node<>(l, e, null);//新建元素,把前驱节点置为原来的last节点
    11         last = newNode;
    12         if (l == null)//如果尾节点是空(说明头节点也是空的),就把头节点设置成新节点
    13             first = newNode;
    14         else//原来尾节点的后继设置成新节点
    15             l.next = newNode;
    16         size++;
    17         modCount++;
    18     }

      一种add就是上面代码的加到末尾,分析都在注释中了。另一种则是添加到指定index,add的平均时间复杂度为O(n)。

     1     public void add(int index, E element) {
     2         checkPositionIndex(index);
     3 
     4         if (index == size)
     5             linkLast(element);//index是最后一个就直接插到最后
     6         else
     7             linkBefore(element, node(index));
     8     }
     9 
    10     //将节点插入到目标节点前面
    11     void linkBefore(E e, Node<E> succ) {
    12         // assert succ != null;
    13         final Node<E> pred = succ.prev;
    14         final Node<E> newNode = new Node<>(pred, e, succ);//将插入节点的前驱设置成目标节点的前驱
    15         succ.prev = newNode;
    16         if (pred == null)//同linkLast中设置后驱节点为目标节点
    17             first = newNode;
    18         else
    19             pred.next = newNode;
    20         size++;
    21         modCount++;
    22     }

    2.2.2 remove方法

      remove方法和add方法类似,由于是双端队列,因此需要改变删除节点的前驱和后继节点的后继和前驱。在此不再展开描述。

    2.2.3 get方法

      get方法在此不贴源码了,由于是双端队列,因此如果查找的下标大于size的一半,就从后面往前遍历,虽然时间复杂度还是o(n)级别的,不过也算是一个小优化吧。

      本篇简略的对jdk1.8下的ArrayList和LinkedList源码实现进行了分析,期间被几个命名奇怪的方法勾引走了,比如ArrayList的trimToSize,可以将数组多余的(大于size)的部分“删掉”。也学到了不少(emmm,好像没有特别多)东西。本篇博客算是对学习过程的一个记录吧。(才不会说是好久没更新博客了要懒死了QAQ)。

  • 相关阅读:
    Tomcat 之 Aio的安装步骤
    redis的安装问题
    Tomcat开机自启
    常用基本SQL语句
    点击开启此虚拟机时,出现“该虚拟机似乎正在使用中”问题
    sql表连接left join,right join,inner join三者之间的区别
    Lua 中 ipairs 与 pairs 的区别
    Lua 栈【转】【学习笔记】
    Nodejs 环境设置
    nodejs取得当前执行路径
  • 原文地址:https://www.cnblogs.com/zzzdp/p/9279019.html
Copyright © 2011-2022 走看看