前言

对于Java程序员,可以说对于ArrayListLinkedList可谓是十分熟悉了

对于ArrayList和LinkedList,他们都是List接口的一个实现类,并且我们知道他们的实现方式各不相同,例如ArrayList底层实现是一个数组,而LinkedList底层实现是链表,对于数组来说,插入慢但是查询快,而对于链表来说查询慢,插入快

今天我们就来分析分析他们的源码

ArrayList

我们先看一看ArrayList的类图。他继承于AbstractList,而这个类是List类的的骨架,可以说这个类是List类的基石

成员属性

/**
序列化ID
**/
private static final long serialVersionUID = 8683452581122892189L;
/**
默认容量
**/
private static final int DEFAULT_CAPACITY = 10;
/**
如果传入的容量为0,那么将使用这个空元素数组
**/
private static final Object[] EMPTY_ELEMENTDATA = {};
/**
默认空元素数组,长度为0,传入元素时会初始化为默认元素大小
**/
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
/**
真正存储数据的数组
**/
transient Object[] elementData;
/**
列表中元素的个数
**/
private int size;

这里需要主要关注的成员属性为sizeelementData,一个是元素个数,一个是真正存储数据的数组

构造函数

/**
指定数组长度构造
**/
public ArrayList(int initialCapacity) {
    if (initialCapacity > 0) {
        this.elementData = new Object[initialCapacity];
    } else if (initialCapacity == 0) {
        this.elementData = EMPTY_ELEMENTDATA;
    } else {
        throw new IllegalArgumentException("Illegal Capacity: " 
                                           initialCapacity);
    }
}
/**
空参构造
**/
public ArrayList() {
    this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
/**
传入一个集合,将该集合的元素初始化到ArrayList种
**/
public ArrayList(Collection<? extends E> c) {
    elementData = c.toArray();
    if ((size = elementData.length) != 0) {
        // c.toArray might (incorrectly) not return Object[] (see 6260652)
        if (elementData.getClass() != Object[].class)
            elementData = Arrays.copyOf(elementData, size, Object[].class);
    } else {
        // replace with empty array.
        this.elementData = EMPTY_ELEMENTDATA;
    }
}

扩容机制

我们知道ArrayList是一个动态数组,但是他的底层实现是一个数组,那他是怎样保证动态的呢?

ArrayList每次添加元素之前,都会检查添加元素后的元素个数是否超过数组长度,如果超出,那么就会进行扩容,而数组扩容通过一个公开的方法实现,我们也可以手动进行扩容

    public void ensureCapacity(int minCapacity) {
        //判断数组是否等于默认空数组,不等于则给minExpand赋值为10
        int minExpand = (elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA)
            ? 0 : DEFAULT_CAPACITY;
        //判断参数是否大于minExpand大于的时候才会去扩容
        if (minCapacity > minExpand) {           
            ensureExplicitCapacity(minCapacity);
        }
    }
    private void ensureExplicitCapacity(int minCapacity) {
        modCount  ;//记录数组修改次数   1
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }
    //真正扩容的方法
    private void grow(int minCapacity) {
        //获取原来的容量
        int oldCapacity = elementData.length;
        //计算新容量,新容量大小 = 旧容量大小的1.5倍
        int newCapacity = oldCapacity   (oldCapacity >> 1);
        //如果需要的容量比新容量还小就用需要的容量
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
        //如果新容量大于最大容量就用最大的容量
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);
        //数据拷贝
        elementData = Arrays.copyOf(elementData, newCapacity);
    }

我们可以发现每次扩容,ArrayList都会扩容1.5倍,通过位运算完成计算扩容大小的,我们知道,扩容之后进行数据迁移这个操作是很费时间的,比如我们有10w条数据,这样的话,进行数据迁移的时候,我们会耗费很长时间,所以我们建议再初始化的时候就定义一个容量,这样可以让ArrayList的效率提高很多

add方法

//添加元素到尾部
public boolean add(E e) {
    //检查是否需要扩容
    ensureCapacityInternal(size   1); 
    //将元素添加到数组末尾,并把size  
    elementData[size  ] = e;
    return true;
}
//添加元素到指定索引
public void add(int index, E element) {
    //检查index是否越界
    rangeCheckForAdd(index);
    //检查是否需要扩容
    ensureCapacityInternal(size   1);
    //将index以及之后的元素向后挪动一位
    //这样index就能添加元素了
    System.arraycopy(elementData, index, elementData, index   1,
                     size - index);
    //添加元素到index的位置
    elementData[index] = element;
    size  ;
}
//将集合c的元素添加到list中
public boolean addAll(Collection<? extends E> c) {
    //把c转换为数组
    Object[] a = c.toArray();
    //拿到c的长度
    int numNew = a.length;
    //检查是否需要扩容
    ensureCapacityInternal(size   numNew);  // Increments modCount
    //将a数组拷贝到原来数组的末尾
    System.arraycopy(a, 0, elementData, size, numNew);
    size  = numNew;
    return numNew != 0;
}
//将集合c的元素从index位置开始添加到list中
public boolean addAll(int index, Collection<? extends E> c) {
    //检查index是否越界
    rangeCheckForAdd(index);
    //将c转换为数组
    Object[] a = c.toArray();
    //获取数组a的长度
    int numNew = a.length;
    //检查是否需要扩容
    ensureCapacityInternal(size   numNew);  // Increments modCount
    //计算需要移动的长度
    int numMoved = size - index;
    if (numMoved > 0)
        //向后移动
        System.arraycopy(elementData, index, elementData, index   numNew,
                         numMoved);
    将数组a拷贝到原数组中
    System.arraycopy(a, 0, elementData, index, numNew);
    size  = numNew;
    return numNew != 0;
}

get方法

public E get(int index) {
    //检查index是否越界
    rangeCheck(index);
    //返回对应数组中指定索引的元素
    return elementData(index);
}

remove方法

//删除指定索引的元素
public E remove(int index) {
    //判断index是否越界
    rangeCheck(index);
    //将数组修改次数 1
    modCount  ;
    //拿到对应索引的元素
    E oldValue = elementData(index);
    计算移动位置
    int numMoved = size - index - 1;
    if (numMoved > 0)
        //移动数组
        System.arraycopy(elementData, index 1, elementData, index,
                         numMoved);
    //size-1,并把尾部元素置为null,这里把尾部置为null主要是为了让GC起作用
    elementData[--size] = null;

    return oldValue;
}
//删除第一个满足o.equals(elementData[index]的元素
public boolean remove(Object o) {
    if (o == null) {
        //如果o为null使用==进行判断
        //从索引为0的元素开始寻找
        for (int index = 0; index < size; index  )
            if (elementData[index] == null) {
                fastRemove(index);
                return true;
            }
    } else {
        //否则使用equals判断
        //从索引为0的元素开始寻找
        for (int index = 0; index < size; index  )
            if (o.equals(elementData[index])) {
                fastRemove(index);
                return true;
            }
    }
    return false;
}

小结

  • 如果我们的数据量很大,我们可以给List估算一个容量,然后进行初始化,否则会对效率有影响,毕竟一直进行数据迁移。
  • 最开始Arraylist的容量为10,每次扩容为原先容量的1.5倍,但是如果我们已经扩容的数组,是不能进行缩容的,例如我们添加了10个元素,这个时候已经扩容了,但是我们删除最后一个元素,他是不会进行缩容的
  • 我们并没有看到他方法上有synchronized关键字,所以他也不是线程安全的,我们可以使用Vector或者使用Collections.synchronizedList(list)

LinkedList

对于LinkedList,它同样继承与AbstractList,在前面已经说过了,AbstractList是List的骨架,我们还可以看到它实现了Deque,所以LinkedList既可以看做一个链表也可以看做是一个队列,同样也可以看做一个栈,所以LinkedList比较全能

Node类

我们知道LinkedList是一个双向链表,那么肯定有一个个的Node,所以我们先来看一看Node类

private static class Node<E> {
    E item;
    Node<E> next;
    Node<E> prev;

    Node(Node<E> prev, E element, Node<E> next) {
        this.item = element;
        this.next = next;
        this.prev = prev;
    }
}

这部分代码比较简单就简单说一下,Node节点有三个成员属性,分别是value,前驱指针,后继指针,以及一个全参构造方法

成员属性

/**
元素个数
 */
transient int size = 0;
/**
指向链表头结点
 */
transient Node<E> first;
/**
指向链表尾节点
 */
transient Node<E> last;
/**
序列化ID
 */
private static final long serialVersionUID = 876323262645176354L;

构造函数

//空参构造,没什么好说的
public LinkedList() {
}
//将集合c的元素添加到链表中
public LinkedList(Collection<? extends E> c) {
    //调用空参构造
    this();
    //调用addAll()这个方法在下面讲述
    addAll(c);
}

添加

public boolean add(E e) {
    linkLast(e);//添加元素到末尾
    return true;
}
//将元素添加到指定index位置
public void add(int index, E element) {
    //检查索引是否大于size或者小于0
    checkPositionIndex(index);
    //如果索引位置和size相等添加到末尾
    if (index == size)
        linkLast(element);
    else
        //添加到指定位置
        linkBefore(element, node(index));
}
public void addFirst(E e) {
    linkFirst(e);//添加元素到头部
}
public void addLast(E e) {
    linkLast(e);//添加元素到末尾
}
//添加到头部
private void linkFirst(E e) {
     //拿到头指针
    final Node<E> f = first;
    //new一个Node节点,值为e,next为头指针,pre为null
    final Node<E> newNode = new Node<>(null, e, f);
    //将头指针替换为新的
    first = newNode;
    //将f的pre修改为现在的头指针
    if (f == null)
        last = newNode;
    else
        f.prev = newNode;
    size  ;
    modCount  ;
}
//添加到末尾
void linkLast(E e) {
    //拿到尾指针
    final Node<E> l = last;
    //new一个Node节点,值为e,next为null,pre为尾指针
    final Node<E> newNode = new Node<>(l, e, null);
    //替换尾指针
    last = newNode;
    //将l的next修改为现在的尾指针
    if (l == null)
        first = newNode;
    else
        l.next = newNode;
    size  ;
    modCount  ;
}
//将集合c的元素添加到list中
public boolean addAll(Collection<? extends E> c) {
    //从尾部开始添加
    return addAll(size, c);
}
//真正的addAll方法
public boolean addAll(int index, Collection<? extends E> c) {
    //检查index正确
    checkPositionIndex(index);
    //先把c转换为数组
    Object[] a = c.toArray();
    //拿到c的长度
    int numNew = a.length;
    if (numNew == 0)
        return false;
    //定义两个指针,一个前驱一个后继
    Node<E> pred, succ;
    if (index == size) {
        succ = null;
        pred = last;
    } else {
        succ = node(index);
        pred = succ.prev;
    }
    //遍历数组a
    for (Object o : a) {
        @SuppressWarnings("unchecked") E e = (E) o;
        //new一个Node节点,值为e,前驱为pred,后继为null
        Node<E> newNode = new Node<>(pred, e, null);
        //pred为null,证明前驱为null,当前节点为链表的头结点
        if (pred == null)
            first = newNode;
        else
            //前驱节点后继指针指向头结点
            pred.next = newNode;
        //前驱节点后移
        pred = newNode;
    }
    //succ为null证明index索引位于链表最后
    if (succ == null) {
        last = pred;
    } else {
        //pred的后继节点为succ
        pred.next = succ;
        //succ的前驱节点为pred
        succ.prev = pred;
    }

    size  = numNew;
    modCount  ;
    return true;
}

获取

//这个方法是Node类的方法,我们可以看到上面的addAll方法也使用了这个方法
//这个方法作用是返回指定索引的非空节点
Node<E> node(int 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;
    }
}
//获取index位置的元素
public E get(int index) {
    //检查索引是否正常
    checkElementIndex(index);
    return node(index).item;
}
//获取头部元素
public E getFirst() {
    final Node<E> f = first;
    if (f == null)
        throw new NoSuchElementException();
    return f.item;
}
//获取尾部元素
public E getLast() {
    final Node<E> l = last;
    if (l == null)
        throw new NoSuchElementException();
    return l.item;
}

删除

//删除index位置的元素
public E remove(int index) {
    //检查索引是否大于size或者小于0
    checkElementIndex(index);
    //删除index位置的元素
    return unlink(node(index));
}
//删除头部元素,这个方法是Node类的方法
private E unlinkFirst(Node<E> f) {
    //拿到头节点的值
    final E element = f.item;
    //拿到头结点的next值
    final Node<E> next = f.next;
    //将头结点的值置为null(帮助GC)
    f.item = null;
    //将头结点的next置为null(帮助GC)
    f.next = null;
    //将头结点修改为next
    first = next;
    if (next == null)
        //此时链表没有元素了
        last = null;
    else
        next.prev = null;
    size--;
    modCount  ;
    return element;
}
//删除尾部元素,这个方法是Node类的方法
private E unlinkLast(Node<E> l) {
    //拿到尾节点的值
    final E element = l.item;
    //拿到尾节点的前驱
    final Node<E> prev = l.prev;
    //将尾结点的值置为null(帮助GC)
    l.item = null;
    //将尾结点的next置为null(帮助GC)
    l.prev = null;
    //将尾指针修改为prev
    last = prev;
    if (prev == null)
        //此时链表没有元素了
        first = null;
    else
        prev.next = null;
    size--;
    modCount  ;
    return element;
}

小结

  • 虽然说链表的删除的效率为O(1),但是在LinkedList中我们需要先利用node方法查询到指定节点的位置,然后再去删除,所以千万不要误认为LinkedList的remove方法是O(1)
  • LinkedList删除头部和尾部的元素效率为O(1)

到此这篇关于Java集合ArrayList与LinkedList详解的文章就介绍到这了,更多相关Java  ArrayList与LinkedList内容请搜索Devmax以前的文章或继续浏览下面的相关文章希望大家以后多多支持Devmax!

Java集合ArrayList与LinkedList详解的更多相关文章

  1. android – 如何在ViewPager中使用cursorLoader?

    解决方法我无法评论,所以我正在写一个答案..您有一个实现LoaderCallbacks的活动.加载数据时,您的活动会收到onLoadFinished回调.在此方法中,您有一个应该在ViewPager中显示的Cursor.要显示Cursor中的数据,请在适配器上调用swapCursor方法.因此,每次加载数据时都不要创建适配器.创建一次,然后只需调用swapCursor即可.此外,每次都找不到ViewPager–findViewById是一个繁重的操作,它应该在创建视图层次结构后执行.所以,你的onLoad

  2. android – 在onRestoreInstanceState中发生ClassCastException

    ClassCastException随机发生以恢复onRestoreInstanceState()中的Vector.通常恢复向量很好,但有时会发生异常.我认为它发生在活动进入后台并被破坏但我不确定.有任何想法吗?谢谢.添加:我忘了附上异常日志.那是解决方法我使用下一个代码来恢复Vector:它会阻止java.lang.classCastException并保存元素顺序.要恢复Stack,您可以使用

  3. android – 如何在活动中调用非活动类的方法

    我有一个Activity和非Activity类.如何从非Activity类调用Activity类中的方法解决方法只需在DateClass中创建一个回调接口.

  4. 是否有可能在android中创建一个Bitmap数组

    我想创建一个位图数组.可能吗?如果是,那是声明Bitmap数组的方法.以及如何初始化它?谢谢解决方法你可以使用Arraylist:或者只是一个位图数组,如:不过要小心你的图像大小.如果你试图存储很多大图像,你可能会遇到一些麻烦.

  5. Realm.io Android从表中获取最后20个项目的最佳方法

    在一个表中说100项,这是获得最后20个对象的最佳方法.我能想到的一种方法是加载所有对象,反转数组,创建一个新数组并从结果循环20次,填充新数组并返回它.如下所示:有没有更好的方法在Android中使用realm.io做到这一点?更新到目前为止这是如何处理的..解决方法另请注意,Realm表是无序的.将它们视为放置数据的包.这意味着如果您想要插入最后20个项目,则需要添加一个字段以包含插入时间.这样做还可以让您非常有效地实现您想要的结果:

  6. 如何在Android中处理ConcurrentModificationException

    我试图从ArrayList中删除项目.有时它会弹出一个异常,“java.util.ConcurrentModificationException”.首先我尝试通过array_list_name.remove(i)删除它们,但它失败了,有些人被要求使用Iterator.所以我目前的代码如下.我在视图中的onDraw()函数中调用“array_list_name”.我的观点是SurfaceView.任

  7. android – 用于osmdroid错误的RoadManager

    我在这里遵循https://code.google.com/p/osmbonuspack/wiki/Tutorial_1教程,但我遇到一个错误,它没有正确显示正确的路由.它只显示从A点到B点的直线.我想要实现的是从这些点显示正确的路线.我猜错误是它无法识别任何节点.一个类似的问题也被问到,如果我没有很好地解释我的问题,我假设我有同样的问题.类似的问题可以在这里找到:OSMDroidRoutingp

  8. android – 自动值 – 包裹 – 适配器可以应对另一个自动值类的Typed Set吗?

    我正在调查自动值和它的扩展,即我的Android应用程序中的auto-value-parcel和auto-value-parcel-adapter.我有这些模型类:–和我的类型适配器是我的问题出现的地方我需要传递给readTypedArray的CREATOR在Autovalue_Amoeba中声明.我的错误在哪里?对自动价值包裹的误解?解决方法Autovalue:Parcel扩展无法处理集合,但如果将属性转换为List,则无需自定义适配器即可开箱即用.如果你想把它当成一个集合你就可以做到这一点.请记住,您

  9. android – 保存ArrayList内容的最佳方法是什么?

    我想保存一个ArrayList,以便它是持久的.内容可以改变.在android中接近这个的最佳方法是什么?.不知道“东西”是什么,我们不能轻易回答这个问题.您的一般选择是使用数据库或持久化到文件.一般来说,sqlite数据库具有优势,因为它使用事务并且通常比仅编写自己的文件更健壮.但是,您的ArrayList可能存储的内容在数据库中可能无法正常工作.当然,您的ArrayList可能存储的内容甚至可能根本不能保留.

  10. android – 在ActivityGroup中暂停和恢复子活动

    我正在尝试创建一个自定义的ActivityGroup.除了组子活动的活动生命周期方法之外,我正在使一切正常工作.当我们的子活动进入/退出焦点时如何调用onResume/onPause方法?我知道tabactivity这样做但我在查看代码时无法找到.提前致谢!

随机推荐

  1. 基于EJB技术的商务预订系统的开发

    用EJB结构开发的应用程序是可伸缩的、事务型的、多用户安全的。总的来说,EJB是一个组件事务监控的标准服务器端的组件模型。基于EJB技术的系统结构模型EJB结构是一个服务端组件结构,是一个层次性结构,其结构模型如图1所示。图2:商务预订系统的构架EntityBean是为了现实世界的对象建造的模型,这些对象通常是数据库的一些持久记录。

  2. Java利用POI实现导入导出Excel表格

    这篇文章主要为大家详细介绍了Java利用POI实现导入导出Excel表格,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下

  3. Mybatis分页插件PageHelper手写实现示例

    这篇文章主要为大家介绍了Mybatis分页插件PageHelper手写实现示例,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪

  4. (jsp/html)网页上嵌入播放器(常用播放器代码整理)

    网页上嵌入播放器,只要在HTML上添加以上代码就OK了,下面整理了一些常用的播放器代码,总有一款适合你,感兴趣的朋友可以参考下哈,希望对你有所帮助

  5. Java 阻塞队列BlockingQueue详解

    本文详细介绍了BlockingQueue家庭中的所有成员,包括他们各自的功能以及常见使用场景,通过实例代码介绍了Java 阻塞队列BlockingQueue的相关知识,需要的朋友可以参考下

  6. Java异常Exception详细讲解

    异常就是不正常,比如当我们身体出现了异常我们会根据身体情况选择喝开水、吃药、看病、等 异常处理方法。 java异常处理机制是我们java语言使用异常处理机制为程序提供了错误处理的能力,程序出现的错误,程序可以安全的退出,以保证程序正常的运行等

  7. Java Bean 作用域及它的几种类型介绍

    这篇文章主要介绍了Java Bean作用域及它的几种类型介绍,Spring框架作为一个管理Bean的IoC容器,那么Bean自然是Spring中的重要资源了,那Bean的作用域又是什么,接下来我们一起进入文章详细学习吧

  8. 面试突击之跨域问题的解决方案详解

    跨域问题本质是浏览器的一种保护机制,它的初衷是为了保证用户的安全,防止恶意网站窃取数据。那怎么解决这个问题呢?接下来我们一起来看

  9. Mybatis-Plus接口BaseMapper与Services使用详解

    这篇文章主要为大家介绍了Mybatis-Plus接口BaseMapper与Services使用详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪

  10. mybatis-plus雪花算法增强idworker的实现

    今天聊聊在mybatis-plus中引入分布式ID生成框架idworker,进一步增强实现生成分布式唯一ID,具有一定的参考价值,感兴趣的小伙伴们可以参考一下

返回
顶部