Java-ArrayList & LinkedList的源码对比分析

ArrayList和LinkedList都继承了AbstractList抽象类,上层实现了List接口,又根据自我定位,实现不同的功能

二、ArrayList的实现

ArrayList是基于数组实现的,在底层维护了一个 Object 类的数组用来存放元素

2.1 ArrayList实现类

ArrayList的类定义:

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
  1. 实现 List 接口,继承了 AbstractList 抽象类,这个是很好理解的,ArrayList作为List集合类家族的一部分,可以更好的将公用方法抽象给上层
  2. 实现Cloneable和Serializable接口 ,可以进行克隆和序列化操作
  3. 实现RandomAccess接口,这个接口会相对陌生点,点进去官方给的解释是,RandomAccess是一个标记接口,是一个空接口,仅起到标记的作用(类似Serializable接口)
    public interface RandomAccess {
    }
    

List实现这个接口表明这个类能实现 快速随机访问。该接口的主要目的是允许通用算法更改其行为,以便在应用于随机访问或顺序访问列表时提供良好的性能

而后文档里给了说明,如果实现了RandomAccess接口,对List做查询操作时使用for循环的方式,否则使用迭代器的方式。这样做的原因做了RandomAccess接口标记的LIst实现类底层数据结构使用for循环效率更高(比如ArrayList),没有RandomAccess接口标记的使用迭代器效率更高(比如LinkedList,下文可以看到LinkedList没有实现RandomAccess接口)

举个简单的小例子,比如Collections类里的二分查找方法,就是用是否标记了RandomAccess接口来区分用哪种方法实现的:

public static <T> int binarySearch(List<? extends Comparable<? super T>> list, T key) {
  if (list instanceof RandomAccess || list.size()<BINARYSEARCH_THRESHOLD)
    return Collections.indexedBinarySearch(list, key);  // for循环方法
  else
    return Collections.iteratorBinarySearch(list, key); // Iterator循环方法
}
2.2 ArrayList构造函数

ArrayList实现了三种构造函数用于不同情形下的对象创建

  1. 无参构造函数
    public ArrayList() {
      this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }
    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
    
    构造了一个空数组,在首次添加元素的时候才给数据设置大小
  2. 给定初始容量的构造函数
    public ArrayList(int initialCapacity) {
      if (initialCapacity > 0) {
         // initialCapacity大于0,数组的大小为initialCapacity值
        this.elementData = new Object[initialCapacity];
      } else if (initialCapacity == 0) {
        // initialCapacity等于0,生成默认空数组EMPTY_ELEMENTDATA
        this.elementData = EMPTY_ELEMENTDATA;
      } else {
        // 其他情况,报非法参数异常-》数组的大小必须大于等于0
        throw new IllegalArgumentException("Illegal Capacity: "+
                                           initialCapacity);
      }
    }
    
    通过参数 initialCapacity 可设置List数组的初始大小,这样做的好处是当 ArrayList 新增元素时,如果所存储的元素已经超过其已有大小,它会计算元素大小后再进行动态扩容,数组的扩容会导致整个数组进行一次内存复制。因此,我们在初始化 ArrayList 时,可以通过第一个构造函数合理指定数组初始大小,这样有助于减少数组的扩容次数,从而提高系统性能
  3. 传入Collection对象转化成ArrayList
    public ArrayList(Collection<? extends E> c) {
      Object[] a = c.toArray();
      if ((size = a.length) != 0) {
        if (c.getClass() == ArrayList.class) {
          elementData = a;
        } else {
          elementData = Arrays.copyOf(a, size, Object[].class);
        }
      } else {
        // replace with empty array.
        elementData = EMPTY_ELEMENTDATA;
      }
    }
    

将 Collection 转化为数组并赋值给 elementData,把 elementData 中元素的个数赋值给 size。 如果 size 不为零,则判断 elementData 的 class 类型是否为 Object[],不是的话则做一次转换。 如果 size 为零,则把 EMPTY_ELEMENTDATA 赋值给 elementData,相当于new ArrayList(0)

2.3 ArrayList属性

ArrayList主要包含了三个属性:

  1. elementData :底层数组,用来存储数据
  2. DEFAULT_CAPACITY :数组的默认初始化容量 10
  3. size :当前的数组大小,用来表示当前数组包含了多少个元素
    transient Object[] elementData; // non-private to simplify nested class access
    private static final int DEFAULT_CAPACITY = 10;
    private int size;
    
2.4 ArrayList新增元素

ArrayList提供了两种添加元素的方法,第一种:直接将元素添加到数组尾部;第二种:将元素添加到指定位置

/**
 * 直接将元素添加到数组尾部
 */
public boolean add(E e) {
  // 1.判断当前数组容量是否够用,不够的话进行数组扩容操作
  ensureCapacityInternal(size + 1);  // Increments modCount!!
  // 2.将元素添加到数组尾部,size加1
  elementData[size++] = e;
  return true;
}/**
 * 将元素添加到指定位置
 */
public void add(int index, E element) {
  // 1.判断指定位置是否在数组包含范围内
  rangeCheckForAdd(index);
  // 2.判断当前数组容量是否够用,不够的话进行数组扩容操作
  ensureCapacityInternal(size + 1);  // Increments modCount!!
  // 3.将指定位置开始的元素向后挪动一位
  System.arraycopy(elementData, index, elementData, index + 1,
                   size - index);
  // 4.将元素添加到指定位置上
  elementData[index] = element;
  size++;
}

从代码里可以看出,两种添加方式都执行了 ensureCapacityInternal 方法,判断数组的容量情况,具体看下这个方法是如何实现的呢:

判断当前数组是否是空数组EMPTY_ELEMENTDATA,如果是的话,判断默认值(10)和传入的容量(size+1)谁大,就返回哪个,如果不是空数组的话,直接返回(size+1),这一步是对初始化为空数组的情况进行特殊处理

判断传入容量(size+1)是否大于当前的数组大小,如果是的话进行扩容操作(grow)

扩容操作是将容量扩充到原始容量的1.5倍大小(oldCapacity + (oldCapacity >> 1)),如果还是不够,就将容量给定为当前传入的容量(size+1)

这里需要判断下是否有内存溢出的问题,当容量达到允许的最大值(MAX_ARRAY_SIZE)的时候,将数组容量给定为int最大值

扩容后需要将原数组重新分配到新的内存地址中

/**
 * 计算数组容量,从传入的容量和默认容量(10)里面去较大的
 */
private static int calculateCapacity(Object[] elementData, int minCapacity) {
  if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
    return Math.max(DEFAULT_CAPACITY, minCapacity);
  }
  return minCapacity;
}/**
 * 计算数组容量,从传入的容量和默认容量(10)里面去较大的
 */
private void ensureCapacityInternal(int minCapacity) {
  ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}/**
 * 计算数组容量,如果传入容量比当前数组已有元素数量小,需要进行扩容操作
 */
private void ensureExplicitCapacity(int minCapacity) {
  modCount++;// overflow-conscious code
  if (minCapacity - elementData.length > 0)
    grow(minCapacity);
}/**
 * 数组扩容
 */
private void grow(int minCapacity) {
  // 计算扩容后的数组容量
  // overflow-conscious code
  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);
  // 将原数组的元素复制到新数组上
  // 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;
}/**
 * 数据允许的最大容量
 */
 private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

从代码里可以看到,添加元素到任意位置,会导致在该位置后的所有元素都需要重新排列,而将元素添加到数组的末尾,在没有发生扩容的前提下,是不会有元素复制排序过程的。所以直接将元素添加的数组的末尾效率会更高。

2.5 ArrayList查找元素

ArrayList在数据查找上的效率很高,只需要O(1)的时间复杂度就能获取数据,传入需要元素的下标index,返回数据中的对应元素即可

public E get(int index) {
  // 检查index是否越界,
  rangeCheck(index);
  // 返回数组对应下标元素值
  return elementData(index);
}private void rangeCheck(int index) {
  if (index >= size)
    throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}@SuppressWarnings("unchecked")
E elementData(int index) {
  return (E) elementData[index];
}
2.6 ArrayList删除元素

ArrayList删除元素的代码逻辑和添加元素到任意位置的方法类似,在每一次有效的删除元素操作之后,都要进行数组的重组,并且删除的元素位置越靠前,数组重组的开销就越大

  1. 判断index是否合法(有没有下标越界)
  2. 获取要删除的元素
  3. 将要删除数据下标往后的元素向前移动一位
  4. 将最后一位置零
  5. 返回被删除的元素
public E remove(int index) {
  rangeCheck(index);
​
  modCount++;
  E oldValue = elementData(index);int numMoved = size - index - 1;
  if (numMoved > 0)
    System.arraycopy(elementData, index+1, elementData, index,
                     numMoved);
  elementData[--size] = null; // clear to let GC do its workreturn oldValue;
}

三、LinkedList的实现

虽然LinkedList和ArrayList都同属于List集合下的,但在实现上却有很大的区别。LinkedList是基于双向链表实现的,LinkedList 定义了一个 Node 结构用来存储数据,Node里维护了三个属性:

  1. item:用来存储元素内容
  2. next:指向下一个节点地址
  3. prev:指向上一个节点地址
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;
  }
}
3.1 LinkedList实现类
  1. 实现了 List 接口,有了List类型的特点
  2. 实现了 Deque 接口,有了Queue类型的特点
  3. 实现了Cloneable 和 Serializable 接口,可以实现克隆和序列化

⚠️ 由于 LinkedList 存储数据的内存地址是不连续的,而是通过指针来定位不连续地址,因此,LinkedList 不支持随机快速访问,LinkedList 也就不能实现 RandomAccess 接口

public class LinkedList<E>
    extends AbstractSequentialList<E>
    implements List<E>, Deque<E>, Cloneable, java.io.Serializable
{
3.2 LinkedList构造函数

由于LinkedList是双向链表,不像ArrayList那样可以提前设定容量大小,所以LinkedList只有两个构造函数,一个是空构造器,一个是将传入的Collection数据全部存入

public LinkedList() {
}
public LinkedList(Collection<? extends E> c) {
  this();
  addAll(c);
}
3.3 LinkedList属性

LinkedList同样也包含了三个属性:

  1. size :记录前LinkedList存储的元素节点个数
  2. first :双向链表的头节点
  3. last :双向链表的尾节点

在JDK1.7以后使用了first/last节点来表示双向链表,而不是之前的使用head节点,这样的好处有:

  1. first/last 属性能更清晰地表达链表的链头和链尾概念
  2. first/last 方式可以在初始化 LinkedList 的时候节省 new 一个 Entry
  3. first/last 方式最重要的性能优化是链头和链尾的插入删除操作更加快捷了

除此之外可以看到这三个属性都被 transient 修饰了,原因很简单,我们在序列化的时候不会只对头尾进行序列化,所以 LinkedList 也是自行实现 readObject 和 writeObject 进行序列化与反序列化

transient int size = 0;/**
     * Pointer to first node.
     * Invariant: (first == null && last == null) ||
     *            (first.prev == null && first.item != null)
     */
transient Node<E> first;/**
     * Pointer to last node.
     * Invariant: (first == null && last == null) ||
     *            (last.next == null && last.item != null)
     */
transient Node<E> last;
3.4 LinkedList新增元素

LinkedList提供了多种插入数据的方式:

默认的add(E element)是将数据添加到链表的尾部,首先创建一个新的节点对象,将之前的last指针指向对象的next节点指向新对象,将last指针指向新对象,完成尾插

public boolean add(E e) {
  linkLast(e);
  return true;
}/**
 * Links e as last element.
 */
void linkLast(E e) {
  // 将原先的last指针指向的对象放到临时变量里
  final Node<E> l = last;
  // 创建一个新的节点对象,传入l节点作为新节点的前置节点
  final Node<E> newNode = new Node<>(l, e, null);
  // last指针指向新对象
  last = newNode;
  // 判断下,如果之前的节点为空,证明是空链表,这是第一次插入,将first指针指向新节点
  if (l == null)
    first = newNode;
  else
    // 原last指针指向的节点指向新的节点(新节点挂链到原链表上了)
    l.next = newNode;
  size++;
  modCount++;
}

addLast(E e)和默认的add相同,实现里就一行代码,调用了linkLast方法

addFirst(E e)和addLast正好相反,是从头部插入数据

/**
 * Links e as first element.
 */
private void linkFirst(E e) {
  final Node<E> f = first;
  final Node<E> newNode = new Node<>(null, e, f);
  first = newNode;
  if (f == null)
    last = newNode;
  else
    f.prev = newNode;
  // 链表个数+1
  size++;
  modCount++;
}

add(int index, E element)方法是将元素插入到指定的位置上:

  1. 首先,判断index是否在合法范围内
  2. 遍历链表找到index所在的节点,遍历的时候会判断index是在链表的前半部分还是后半部分来决定从first开始还是last开始
  3. 将新节点插入到index节点的前一个位置
  4. 将元素添加到任意两个元素的中间位置,添加元素操作只会改变前后元素的前后指针,指针将会指向添加的新元素,所以相比 ArrayList 的添加操作来说,LinkedList 的性能优势明显。
/**
 * 指定位置的插入
 */
public void add(int index, E element) {
  // 校验index是否合法
  checkPositionIndex(index);
  // 如果index刚好等于size,则直接尾插
  if (index == size)
    linkLast(element);
  else
    linkBefore(element, node(index));
}/**
 * 校验index是否合法
 */
private boolean isPositionIndex(int index) {
  return index >= 0 && index <= size;
}
private void checkPositionIndex(int index) {
  if (!isPositionIndex(index))
    throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}/**
 * 将新节点插入到当前节点(succ)的前一个位置上
 */
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)
    first = newNode;
  else
    pred.next = newNode;
  size++;
  modCount++;
}
3.5 LinkedList查找元素

LinkedList查找元素和将新节点插入指定位置的逻辑是一样的,实际上将元素插入指定位置就是先查找到元素,再进行插入操作。同样,这里会先对index进行合法性校验,再通过index计算是从前向向后还是从后向前对链表进行遍历获取到对应数据

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;
  }
}
3.6 LinkedList删除元素

LinkedList提供了两种删除数据的方法,第一种是remove(int index),删除指定下标的元素,同查找元素逻辑一样,先遍历查找到index对应的下标节点,然后将这个节点的前置节点(prev)的next关联到这个节点的后置节点(next),将当前节点对应的属性值释放(赋值为null,便于JVM回收),这里要注意对边界值的处理

/**
 * 删除指定下标的元素
 */    
public E remove(int index) {
  checkElementIndex(index);
  return unlink(node(index));
}/**
 * 删除指定下标的元素
 */ 
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;
  // 处理完后链表个数-1
  size--;
  modCount++;
  return element;
}

另一个删除方法是remove(Object o),删除指定的元素,由于无法用index定位,所以这种删除方式只能每次动first开始遍历,将遇到的所有存的是指定元素值的节点全部删除,这里会对null进行特殊处理,如果入参为null的话就删除所有元素值为null的节点

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;
}

此外,LinkedList还提供了peek()和poll()系列方法,用来实现Queue的特性,这里就不展开论述了

四、总结

对比项ArrayListLinkedList
实现接口实现 List 接口,具有List类型特点实现List 和 Deque接口,兼有List和Queue类型特点
克隆和序列化可以实现 ✅可以实现 ✅
随机快速访问支持 ✅不支持 ❌
元素遍历for循环效率更高迭代器效率更高
数据结构数组 (Object[]双向链表 (Node)
扩容扩容1.5倍链表无需扩容
内存占用连续的内存空间不连续的内存空间,额外增加prev和next属性空间

写在最后 欢迎关注微信公众号【小肖爱吃肉】和你一起记录生活的小美好
在这里插入图片描述

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: 答:hashmap、hashtable、arraylistlinkedlist都是Java源码中常用的数据结构。hashmap是一种散列表,它使用键值对来存储数据;hashtable是一种哈希表,它使用散列函数来存储数据;arraylist是一种动态数组,它可以动态增加和删除数据;linkedlist是一种链表,它使用节点来存储数据。 ### 回答2: 1. HashMap(哈希映射):是Java中最常用的集合类之一。它是基于哈希表实现的,用于存储键值对。HashMap可以根据键快速地查找对应的值,内部使用哈希函数来将键计算为哈希码,并将其存储在数组中。当需要根据键查找值时,HashMap会将键的哈希码通过哈希函数再次计算得到数组索引,然后在该位置的链表中查找对应的值。 2. Hashtable(哈希表):也是一种存储键值对的集合类,在Java中已被HashMap取代,但仍然保留供向后兼容使用。Hashtable与HashMap的原理相似,都是基于哈希表实现的。与HashMap不同的是,Hashtable是线程安全的,即在多线程环境下,不需要额外的同步操作就可以安全地使用Hashtable。然而,由于线程安全的机制增加了额外开销,推荐在单线程环境下使用HashMap。 3. ArrayList(数组列表):是Java中最常用的动态数组实现。底层使用数组来存储元素,具有自动扩容的功能。ArrayList可以按索引访问元素,并支持快速插入和删除元素,通过扩容和拷贝数组的方式实现。ArrayList允许存储任意类型的对象,可以动态地增加和减少集合的大小。 4. LinkedList(链表):也是Java中的一种集合类,LinkedList底层实现了双向链表的数据结构。相比于ArrayListLinkedList在插入和删除元素时效率更高,因为它不需要像ArrayList一样进行数组的扩容和拷贝操作。LinkedList可以在链表的任意位置进行插入和删除操作,但访问指定位置的元素需要遍历整个链表,效率较低。 总结:以上四种集合类都是Java中用于存储和操作数据的类库。HashMap和Hashtable都是基于哈希表实现的键值对集合,其中Hashtable是线程安全的,HashMap性能更好。ArrayList是基于数组实现的动态数组,支持快速访问和增删操作。LinkedList是基于链表实现的集合,插入和删除元素效率较高,但访问元素的效率较低。 ### 回答3: HashMap是Java中的一种数据结构,它使用了哈希表来存储键值对。其中的键是唯一的,通过哈希函数计算得到对应的存储位置,提高了查找和插入的效率。HashMap允许键和值都为空,但是不保证存储顺序。 Hashtable也是一种键值对的映射结构,它与HashMap类似,但是Hashtable是线程安全的,即在多线程环境下,多个线程同时访问Hashtable的读写操作,不会导致数据不一致的情况发生。Hashtable的键和值都不允许为空。 ArrayList是一种动态数组,它使用数组来存储一组元素。与普通的数组相比,ArrayList的大小可以动态调整,可以根据实际需求进行增删操作。ArrayList适用于频繁访问、查询和插入元素,但不适用于频繁删除元素的场景。 LinkedList是一种双向链表,它通过节点之间的引用关系连接了一组元素。与ArrayList相比,LinkedList的插入和删除操作效率更高,但是随机访问元素的效率较低。LinkedList适用于频繁插入和删除元素的场景。 综上所述,HashMap和Hashtable都是用于存储键值对的数据结构,其中HashMap在非多线程环境下性能更好,Hashtable在多线程环境下安全可靠;ArrayList是动态数组,适用于频繁访问和查询元素;LinkedList是双向链表,适用于频繁插入和删除元素。根据实际需求选择合适的数据结构可以提高程序的效率。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值