Java 集合框架中的 ArrayList 和 LinkedList:实现、性能差异和适用场景

深入剖析Java ArrayList与LinkedList

本文将深入分析Java中两个重要的数据结构:ArrayListLinkedList,通过查看源码,我们将探讨它们的内部实现、扩容机制、迭代器实现以及它们之间的区别。我们将以JDK源码为基础进行分析,帮助您更好地理解这两种数据结构的实现细节和使用场景。

ArrayList源码分析

ArrayList的构造方法与扩容机制深入解析

ArrayList提供了以下几个构造方法:
1.无参构造函数:创建一个初始容量为10的空列表。
当我们使用无参构造函数创建一个 ArrayList 对象时,底层的数组实际上是在第一次添加元素时才会被创建,且默认大小为 0。当添加第一个元素时,ArrayList 会将数组大小设置为默认容量 10,然后将元素添加到数组中。如果再添加更多元素,当数组大小不足以容纳这些元素时,ArrayList 会动态地进行扩容,每次扩容后数组大小会增加 50%。

public ArrayList() {
    this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}

2.指定初始容量的构造函数:创建一个指定初始容量的空列表。

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

3.基于已有集合的构造函数:创建一个包含指定集合元素的列表。
当我们使用 ArrayList(Collection c) 构造函数创建对象时,底层会使用传入集合的大小作为数组的容量。

public ArrayList(Collection<? extends E> c) {
    elementData = c.toArray();
    if ((size = elementData.length) != 0) {
        // c.toArray可能返回不是Object[]类型的数组
        if (elementData.getClass() != Object[].class)
            elementData = Arrays.copyOf(elementData, size, Object[].class);
    } else {
        // 如果集合为空,初始化为空数组
        this.elementData = EMPTY_ELEMENTDATA;
    }
}

4.add(Object o) 方法的扩容
在 ArrayList 中,每次添加元素时,都会检查当前数组容量是否足够。如果不足以容纳新元素,则会进行扩容。首次扩容时,容量会增加到 10;之后,每次扩容都会增加为原来容量的 1.5 倍。

具体的扩容方式是:先将当前数组大小右移 1 位,得到数组大小一半的数,再将这个数与当前数组相加,得到扩容后的大小。

ArrayList的扩容机制在grow()方法中实现,当添加元素时,如果当前数组容量不足以容纳新元素,则会触发扩容。
扩容后的容量计算主要为如下:

int newCapacity = oldCapacity + (oldCapacity >> 1);

这表示新容量将是旧容量的1.5倍。
以下为grow(int minCapacity)方法完整代码

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

grow() 方法在 ensureCapacityInternal() 方法中被调用,以确保在添加新元素时有足够的空间。

private void ensureCapacityInternal(int minCapacity) {
    ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}

private static int calculateCapacity(Object[] elementData, int minCapacity) {
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        return Math.max(DEFAULT_CAPACITY, minCapacity);
    }
    return minCapacity;
}

private void ensureExplicitCapacity(int minCapacity) {
    modCount++;

    if (minCapacity - elementData.length > 0) {
        grow(minCapacity);
    }
}

5.addAll(Collection c) 方法的扩容
ArrayListaddAll(Collection c) 方法中,扩容逻辑与 add(Object o) 类似。如果添加的集合中没有元素,扩容后的容量为 Math.max(10, 实际元素个数);如果已有元素,扩容后的容量为 Math.max(原容量 1.5 倍, 实际元素个数)

public boolean addAll(Collection<? extends E> c) {
    Object[] a = c.toArray();
    int numNew = a.length;
    ensureCapacityInternal(size + numNew);
    System.arraycopy(a, 0, elementData, size, numNew);
    size += numNew;
    return numNew != 0;
}

通过分析 ArrayList 的源码,我们可以看到其扩容机制的实现细节。这种动态扩容方式使得 ArrayList 在存储元素时更加灵活,但也可能在扩容过程中产生一定的性能开销。因此,在使用 ArrayList 时,合理预估初始容量可以提高性能,避免频繁的扩容操作。

迭代器实现

Fail-Fast 与 Fail-Safe 原理简介

在深入了解 Java 集合中的迭代器之前,我们先介绍一下 Fail-Fast 和 Fail-Safe 的概念,这将有助于理解不同迭代器的行为特点。

Fail-Fast

Fail-Fast 机制是指,在迭代集合的过程中,如果检测到集合被修改,就会立即抛出 ConcurrentModificationException 异常,终止迭代。这种机制通过对集合的修改次数进行检测来发现集合是否被修改的。具体来说,当使用 Fail-Fast 迭代器遍历一个集合时,迭代器会记录下集合的修改次数 modCount 的值。每当集合发生修改操作时,modCount 的值就会增加。在每一次迭代操作之前,迭代器都会检查当前的 modCount 值是否等于迭代开始时的 modCount 值,如果不相等,就说明在迭代过程中集合被修改了,就会抛出 ConcurrentModificationException 异常,终止迭代。Fail-Fast 迭代器基于迭代器遍历过程中对集合内容的检查来实现,它假设在迭代期间其他线程不会修改集合。一旦其他线程对集合进行了修改,就会抛出异常,提醒程序员集合在多个线程之间被修改,从而保障了遍历的安全性和一致性。

Fail-Safe

Fail-Safe 机制与 Fail-Fast 不同,其迭代器不会在迭代过程中抛出 ConcurrentModificationException 异常,而是在遍历开始时创建集合的一个副本,并在副本上进行遍历。由于是在副本上进行操作,因此即使其他线程修改了原集合,也不会影响迭代器的遍历结果。Fail-Safe 迭代器的优点是能够避免遍历出不一致的结果,同时也不会抛出异常导致程序终止。但是它的缺点是需要在内存中存储一个集合的副本,如果集合比较大,会占用较多的内存资源。

现在我们已经了解了 Fail-Fast 和 Fail-Safe 的原理,接下来将结合 Java 源码,深入探讨 ArrayList 和 LinkedList 的迭代器实现。

ArrayList迭代器实现

ArrayList实现了Fail-Fast迭代器,通过内部类Itr来实现:

private class Itr implements Iterator<E> {
    int cursor;       // 下一个元素的索引
    int lastRet = -1; // 上一个返回元素的索引
    int expectedModCount = modCount;

    public boolean hasNext() {
        return cursor != size;
    }

    @SuppressWarnings("unchecked")
    public E next() {
        checkForComodification();
        int i = cursor;
        if (i >= size)
            throw new NoSuchElementException();
        Object[] elementData = ArrayList.this.elementData;
        if (i >= elementData.length)
            throw new ConcurrentModificationException();
        cursor = i + 1;
        return (E) elementData[lastRet = i];
    }
	public void remove() {
   	 if (lastRet < 0)
   	     throw new IllegalStateException();
  	  checkForComodification();
	
   	 try {
    	    ArrayList.this.remove(lastRet);
     	   cursor = lastRet;
    	    lastRet = -1;
    	    expectedModCount = modCount;
 	   } catch (IndexOutOfBoundsException ex) {
  	      throw new ConcurrentModificationException();
 	   }
	}

	final void checkForComodification() {
  	  if (modCount != expectedModCount)
    	    throw new ConcurrentModificationException();
	}
}

Itr类中,checkForComodification()方法用于检查modCount(修改计数)是否等于expectedModCount,以检测集合是否在迭代过程中被修改。如果检测到修改,将抛出ConcurrentModificationException异常。

LinkedList源码分析

LinkedList是基于双向链表实现的列表。它不需要连续内存空间,因此在插入和删除操作上具有更高的性能。

构造方法与内部节点实现

LinkedList提供了以下几个构造方法:

  1. 无参构造函数:创建一个空的链表。
    public LinkedList() {
    }
    

2.基于已有集合的构造函数:创建一个包含指定集合元素的列表。

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

LinkedList的内部节点由内部类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包含一个元素和指向前一个和后一个节点的指针。

迭代器实现

LinkedList实现了Fail-Fast迭代器,通过内部类ListItr来实现:

private class ListItr implements ListIterator<E> {
    private Node<E> lastReturned;
    private Node<E> next;
    private int nextIndex;
    private int expectedModCount = modCount;

    // ...
}

ArrayList的迭代器类似,LinkedList的迭代器也使用checkForComodification()方法来检测集合是否在迭代过程中被修改。如果检测到修改,将抛出ConcurrentModificationException异常。

对比与总结

1.删除操作

在删除元素时,ArrayList和LinkedList的性能差异较大。

对于ArrayList,删除元素会触发数组元素的移动。具体来说,当删除一个元素时,需要将其后面的所有元素向前移动一个位置。因此,删除元素的时间复杂度为O(n),其中n为需要移动的元素个数。
对于LinkedList,删除元素只需要改变前后节点的指针即可。因此,在已知节点位置的情况下,删除元素的时间复杂度为O(1)

2.性能对比

根据上述源码分析和操作特性,我们可以总结出以下性能对比:

随机访问:ArrayList具有较好的随机访问性能,时间复杂度为O(1);LinkedList在随机访问时需要沿着链表遍历,时间复杂度为O(n)。
插入/删除:ArrayList在尾部插入/删除性能较好,时间复杂度为O(1);在其他部分插入/删除时,需要移动大量元素,时间复杂度为O(n)。LinkedList在已知节点位置的情况下,插入/删除操作的时间复杂度为O(1)。
内存消耗:ArrayList使用连续的内存空间,当数组容量不足时需要扩容,可能导致额外的内存消耗。LinkedList使用非连续内存空间,额外存储了前后节点的指针,因此每个元素的内存开销相对较大。

3.使用场景

根据上述分析,我们可以得出以下适用场景:

如果需要频繁访问、查找元素,且插入/删除操作较少,可以选择ArrayList。
如果需要频繁插入/删除元素,特别是在列表的中间位置进行操作,可以选择LinkedList。
然而,在实际应用中,我们还需要考虑其他因素,例如内存限制、数据量大小等。因此,在选择合适的数据结构时,需要综合考虑多种因素,权衡性能和资源消耗。

根据源码分析,我们可以总结出ArrayListLinkedList的以下区别:

1.ArrayList基于数组实现,需要连续内存空间;LinkedList基于双向链表实现,无需连续内存空间。
2,ArrayList具有较好的随机访问性能;LinkedList在随机访问时需要沿着链表遍历,性能较差。
3.ArrayList在尾部插入、删除性能较好,其它部分插入、删除性能较差;LinkedList在插入、删除操作上性能较好,尤其是在链表中间部分。
4. ArrayList需要扩容,而LinkedList不需要。

ArrayList可能存在内存浪费问题,当数组容量远大于实际元素数量时;LinkedList由于每个节点都需要额外的前后指针,其内存占用也相对较高。
综合考虑,我们可以在不同的使用场景中选择合适的数据结构:

当需要频繁访问列表中的元素时,建议使用ArrayList。
当需要频繁进行插入、删除操作时,尤其是在列表中间部分,建议使用LinkedList。
当内存空间紧张时,可以根据具体情况权衡ArrayList和LinkedList的优缺点。
通过以上源码分析和对比,希望能够帮助您更好地理解ArrayList和LinkedList的内部实现细节和使用场景,从而在实际应用中做出明智的选择。

其他Java集合实现

除了ArrayList和LinkedList,Java还提供了其他集合实现,如Vector和CopyOnWriteArrayList等。这些集合具有不同的性能特点和使用场景,例如:

Vector:与ArrayList类似,但实现了线程安全。由于线程安全的开销,其性能通常低于ArrayList。
CopyOnWriteArrayList:线程安全的ArrayList实现,适用于读操作远多于写操作的场景。在写操作时,会复制整个数组,因此写操作的性能开销较大。

在选择集合实现时,除了考虑基本的性能特点和使用场景外,还需要根据实际需求考虑线程安全、并发性能等因素。

Java集合与线程安全

Java集合中的大多数实现都不是线程安全的,例如ArrayList、LinkedList、HashMap等。在多线程环境下使用这些集合时,需要注意同步控制。通常有以下几种方法来保证线程安全:

使用线程安全的集合实现:如Vector、CopyOnWriteArrayList、ConcurrentHashMap等。这些集合实现了线程安全,但可能存在性能开销。
使用同步封装器:Collections类提供了一些方法,如Collections.synchronizedList()、Collections.synchronizedMap()等,可以将非线程安全的集合封装为线程安全的集合。但这些方法实现的同步控制粒度较大,可能导致性能下降。
手动同步:在访问集合时,使用synchronized关键字或其他同步工具(如ReentrantLock)进行同步控制。这种方法需要用户自己管理同步逻辑,容易出错,但可以实现更细粒度的同步控制,提高性能。

Java集合与垃圾回收

在使用Java集合时,需要注意垃圾回收的影响。在集合中存储大量对象时,可能会导致垃圾回收器的负担加重,影响应用性能。为了避免这种问题,可以采取以下策略:

及时清理无用对象:在使用集合存储对象时,确保不再使用的对象被及时清理,避免内存泄漏。例如,在集合中存储监听器对象时,需要在不再需要监听时将其从集合中移除。
使用弱引用:在某些场景下,可以使用弱引用(WeakReference)来替代强引用,让垃圾回收器在需要时回收对象。例如,可以使用WeakHashMap来实现缓存,当内存不足时,垃圾回收器可以自动清理缓存中的对象。
总结
本文深入分析了Java中ArrayList和LinkedList的源码实现、性能特点和使用场景。同时,讨论了线程安全、垃圾回收等与集合相关的问题。希望通过这篇博客,帮助大家更好地理解Java集合的实现原理和使用方法,为实际开发提供参考。

你可能感兴趣的:(Java,集合框架源码揭秘,java,数据结构,开发语言)