ArrayList与LinkedList源码解析

来源:互联网 发布:表达知错悔恨的古诗 编辑:程序博客网 时间:2024/06/05 08:05

                                      ArrayList与LinkedList源码解析

     相信使用java的同仁对这个都不陌生, 在java中我们最长使用的数据结构就是List与Map了, 而今天我们就来看看List中最常用的两种实现。

     1. 首先我们来看ArrayList集合, 他的类层级图如下:

                       

       1). 首先我们需要了解ArrayList的数据结构模型为:

                    struct  ArrayList {

            static final int DEFAULT_CAPACITY = 10;
                           static finalObject[]EMPTY_ELEMENTDATA={};
            static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
                           Object[]elementData;     

                           int size;

                     }

            从这个结构中我们可以知道ArrayList的结构是以数组的形式进行操作的。

        2). 然后我们一般就需要关心数据结构的增删查改操作了, 数据结构主要的操作就是增删查改。

            (1). 首先看一下ArrayList的add操作:

                   

    public boolean add(E e) {        ensureCapacityInternal(size + 1);  // Increments modCount!!        elementData[size++] = e;        return true;    }
                    这段代码的增加操作和普通的数据操作一样, 关键在于ensureCapacityinternal()这个方法, 该方法会在ArrayList长度不够时进行对应的扩容,具体源代码如下:

          

private void grow(int minCapacity) {        // overflow-conscious code        int oldCapacity = elementData.length;        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);    }

                   在我们进入ensureCapacityInternal()方法去时,我们最后发现grow()才是真正进行操作的方法从这上面来看, 我们到一般情况下扩容的时候计算方法为增加原有数组的1/2的长度。  而且我们从第二个If中我们可以知道ArrayList的限制为Integer的最大值2147483647,当然我们的对象不可能这么多, 不然服务器不爆都难。然后使用Arrays.copyOf来进行扩容。

            (2).其他的删改查都很简单, 这里就不进行介绍了。

      2. LinkedList的类层次结构图:

                        

        

                          

               从这个图中我们知道,ArrayList和LinkedList都继承了最左边这个分支类层级, 他们中有些通用的方法都来自这个实现结构。

          1). 首先来了解LinkedList的数据结构:

                   struct   Node<E> {

             E item;
             Node<E> next;
             Node<E> prev;

                   }

                   struct   LinkedList {

            transient int size = 0;      
            transient Node<E> first;
            transient Node<E> last;

                   }

         从这个结构中我们可以知道, LinkedList使用的是双向链表来进行数据存储的。

           2). 现在我们来看看它的相关操作

                    (1).  add操作

      

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)        first = newNode;    else        l.next = newNode;    size++;    modCount++;}
                     这段代码就是LinkedList的操作, 增加一个新节点, 就是追加该节点到链表的最后。 从代码来看, LinkedList的长度是没有限制的。对比ArrayList与LinkedList的add方法发现: 当使用ArrayList新增数据时, 我们要进行对应的数据移动工作, 它会先创建一个扩容后的数组, 将原数组数据拷贝到新数据中; 而我们在使用LinkedList的时候,则是直接在链表尾部追加新元素。 很明显, LinkedList的add操作比ArrayList的操作要便利很多, 性能更加。所以当我们是List时, 如果该List有频繁的增删改时, 推荐使用LinkedList操作。


                     (2). get操作

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;        }    }
                              这就是它的get操作, 从该操作来看, 它先看当前索引是靠近头部还是尾部, 然后进行链表的遍历进行查找。  从LinkedList和ArrayList的get中我们可以知道,由于ArrayList采用数据方式存储数据, 该种存储是连续的,所以查找数据时不需要遍历数组就可以根据下标获得对应数据, 而LinkedList则是使用链表, 该种方式存储数据不是连续的, 当我们需要查找数据时, 需要对整个链表进行遍历; 可以知道, 当数据靠近链表中部时, 我们最快查找也需要size/2次才能查到。 所以当我们在使用List进行数据存储,更多的操作是查找的时候, 推荐使用ArrayList。
                      (3). remove操作:

public boolean remove(Object o) {        if (o == null) {            for (Node<E> x = first; x != null; x = x.next) {                if (x.item == null) {                    unlink(x);                    return true;                }            }        } else {            for (Node<E> x = first; x != null; x = x.next) {                if (o.equals(x.item)) {                    unlink(x);                    return true;                }            }        }        return false;    }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;    }
                         上面的代码时根据对象值进行删除的, 因为根据索引删除的基本就是unlink在操作, 所以没有贴出代码, 从上面可以看到当使用对象值进行删除时, 所有的值相同的对象都会被删除; 而我们使用索引删除的时候, 只是删除对应索引位置的对象。在这点上, ArrayList和LinkedList的做法也是一样的。

                    (3). set操作: 我们想替换一个对象的时候使用的是set方法进行操作。

public E set(int index, E element) {        checkElementIndex(index);        Node<E> x = node(index);        E oldVal = x.item;        x.item = element;        return oldVal;    }

                           set操作通过node方式找到对应的节点, 然后替换调值即可。

                     (4). contains操作:

public boolean contains(Object o) {        return indexOf(o) != -1;    }public int indexOf(Object o) {        int index = 0;        if (o == null) {            for (Node<E> x = first; x != null; x = x.next) {                if (x.item == null)                    return index;                index++;            }        } else {            for (Node<E> x = first; x != null; x = x.next) {                if (o.equals(x.item))                    return index;                index++;            }        }        return -1;    }

                                为什么在这里要讲contains方法, 当我们在使用List并且想去重的时候, 我们就可以使用这个方法, 但是我们观察一下当前这个方法, 它会找到第一个值相等的元素并且返回对应的索引下标, 而在它进行对比是,它调用了当前对象的equals方法, 这个方法是关键。 所以在我们需要使用List的contains的时候, 我们需要重写当前对象对应类的equals方法, 让对应的比较按我们的需求进行。

总结:       在介绍对应源码时, 主要介绍了LinkedList, 这里并不是说它更重要, 而是因为ArrayList的是采用数组进行保存的, 对于数据的操作相比开发人员都很清楚, 所以就集中精力分析了下LinkedList。 从这些分析中知道:

        1. ArrayList采用的是数组方式进行数据保存, 而LinkedList采用的是双向链表进行保存。

        2. 从它们的操作实现中, 我们可以知道在查询比较多的情况下使用ArrayList, 增删改频繁的时候使用LinkedList是比较不错的选择。

        3. 在我们使用pojo时, 最好别忘记了重写equals方法, 防止以后比较会使用到。

0 0