Java集合 - List介绍及源码解析

简介: Java集合 - List介绍及源码解析(源码版本为 JDK 8)集合类在java.util包中,类型大体可以分为3种:Set、List、Map。JAVA 集合关系(简图)#集合.jpg(图片来源网络)List集合和Set集合都是继承Collection接口,是List和Set的最上级接口,包含如下方法:Collection接口.pngList 集合#List是一个有序集合(也称为序列),你可以控制每个元素被插入的位置,和根据索引访问列表中元素。

Java集合 - List介绍及源码解析
(源码版本为 JDK 8)

集合类在java.util包中,类型大体可以分为3种:Set、List、Map。

JAVA 集合关系(简图)#
集合.jpg

(图片来源网络)

List集合和Set集合都是继承Collection接口,是List和Set的最上级接口,包含如下方法:

Collection接口.png

List 集合#
List是一个有序集合(也称为序列),你可以控制每个元素被插入的位置,和根据索引访问列表中元素。List集合元素可以重复,也可以存入 null 元素。

List集合是可以根据索引来操纵集合,所以List接口在Collection接口基础增加了一些根据索引操纵集合的接口方法。

List接口.png

集合接口的实现类#
List 集合有两个常用实现,ArrayList和LinkedList,内部采用不同数据结构来实现,不同场景下有不同的使用选择。

ArrayList

ArrayList类-1.png

ArrayList类除了继承和实现集合接口外,还实现了RandomAccess, Cloneable接口。说明ArrayList支持克隆和快速随机访问。

ArrayList 的内部数据结构是数组。

ArrayList内部数据结构-数组.png

默认初始化容量为10

默认初始化容量.png

从查找,增加,删除,修改元素方法看ArrayList集合

查找元素方法:get,indexOf

Copy
// 直接根据索引查找元素,效率较高
public E get(int index) {

rangeCheck(index);
return elementData(index); // 根据索引直接返回数组中元素

}

// 根据元素查索引位置,元素不存在返回 -1 ,使用了循环遍历查找元素,查找效率取决于集合大小,元素所处的位置。
public int indexOf(Object o) {

if (o == null) {
    for (int i = 0; i < size; i++)
        if (elementData[i]==null)
            return i;
} else {
    for (int i = 0; i < size; i++)
        if (o.equals(elementData[i]))
            return i;
}
return -1;

}
增加元素方法:add

Copy
// 增加元素,存在扩容,数据拷贝等问题,效率会变低,如果要向集合中大量的添加元素可以通过构造方法指定较大的初始容量。
public boolean add(E e) {

ensureCapacityInternal(size + 1);  // 增加 modCount!!
elementData[size++] = e;
return true;

}

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

}

// 扩容
private void grow(int minCapacity) {

// overflow-conscious code
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1); // 容量变为原来的1.5倍
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);

}
删除元素方法:remove

Copy
// 根据索引删除元素,如果从开头和中间位置删除元素,删除位置后面的元素会向前移动,效率会比删除末尾元素低。
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; // 赋值为null 明确的让垃圾回收,--size 删除元素后修改集合长度

return oldValue;

}
Copy
// 根据集合元素删除,先循环找出要删除的元素位置索引,然后再根据索引删除。和根据索引删除方法比较,多了一步通过循环查找元素索引位置的过程。
public boolean remove(Object o) {

if (o == null) {
    for (int index = 0; index < size; index++)
        if (elementData[index] == null) {
            fastRemove(index);
            return true;
        }
} else {
    for (int index = 0; index < size; index++)
        if (o.equals(elementData[index])) {
            fastRemove(index);
            return true;
        }
}
return false;

}

// 删除集合元素
private void fastRemove(int index) {

modCount++;
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 work

}
修改元素方法:set

Copy
// 根据索引修改元素,直接索引指向新的元素值。
public E set(int index, E element) {

rangeCheck(index);

E oldValue = elementData(index);
elementData[index] = element;
return oldValue;

}

通过上面代码可以发现,ArrayList 集合检索元素效率较高,比较适合,而对于增删效率较低。

LinkedList

LinkedList集合.png

LinkedList 类还实现了Deque 接口(Deque 代表算端队列,与 List 接口不同,此接口不支持通过索引访问元素),所以LinkedList 是一个List集合也是一个双端队列。

LinkedList类-1.png

Copy
private static class Node {

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

}

LinkedList 是一个链表数据结构,其中维护了一个内部类Node做为链表中的节点,first 是指向首节点,last 是指向尾节点。每个Node节点都记录上一个节点、下一个节点的引用,和当前节点所存储的元素。

链表结构如图:

双向链表结构图.png

(图片来源网络)

从查找,增加,删除,修改元素部分方法看LinkedList集合适合哪些操作(从底层数据结构就能够发现)

查找元素方法:get

Copy
// 根据索引查找元素,由于链表没有索引,所以需要从头部或尾部遍历查找。ArrayList 和 LinkedList底层数据结构不同导致的 ArrayList集合中查找元素效率更高,因为ArrayList底层是数组,可以直接根据index索引获取元素。
public E get(int index) {

checkElementIndex(index);
return node(index).item;

}

Node node(int index) {

// 如果要找的元素位置小于集合长度的1/2,就从前向后遍历,否则从后向前遍历,由此可知越向中间效率越低
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;
}

}
增加元素方法:add

由于底层数据结构不同,LinkedList增加元素效率要比ArrayList效率高,ArrayList存在扩容和拷贝等操作。

Copy
// 向尾部添加元素
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; // 将新节点指向尾节点(last)
if (l == null) 
    first = newNode;//  如果newNode是集合中唯一元素(初始是空集合),那么也将newNode指向首节点(first)
else
    l.next = newNode; // 原last节点下一个元素的引用指向新的节点(newNode)
size++;
modCount++;

}

// 在指定位置添加元素,index 位置越靠近中间插入效率越低,随着集合长度增大而增大
public void add(int index, E element) {

checkPositionIndex(index);

if (index == size) // index==size 说明集合为空或者在集合末尾添加元素
    linkLast(element);
else
    linkBefore(element, node(index));

}

// 链表和数组不同,不能直接根据索引获得元素,链表需要从头或尾部循环遍历到指定位置获得元素
Node node(int index) {

// 如果要找的元素位置小于集合长度的1/2,就从前向后遍历,否则从后向前遍历,所以向中间效率越低
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;
}

}

// 在 succ节点之前插入元素
void linkBefore(E e, Node succ) {

final Node<E> pred = succ.prev; 
final Node<E> newNode = new Node<>(pred, e, succ);// 创建一个新节点
succ.prev = newNode; // 
if (pred == null)
   // 说明 succ 是首节点
    first = newNode;
else
    pred.next = newNode;
size++;
modCount++;

}
修改元素方法:set

LinkedList修改元素时需要先遍历找到元素,ArrayList可以直接根据索引获得元素,所以LinkedList效率较低,当元素越靠近中间位置越明显。

Copy
public E set(int index, E element) {

checkElementIndex(index);
Node<E> x = node(index);
E oldVal = x.item;
x.item = element;
return oldVal;

}

// 根据索引遍历出元素节点
Node node(int index) {

// 如果要找的元素位置小于集合长度的1/2,就从前向后遍历,否则从后向前遍历,所以向中间效率越低

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

}
删除元素方法:remove

和ArrayList相比不存在移动拷贝情况,所以LinkedList删除元素效率比ArrayList高

Copy
public E remove(int index) {

checkElementIndex(index);
return unlink(node(index));

}

// 根据索引遍历查找出目标节点
Node 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;
}

}

E unlink(Node 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;

}
LinkedList 实现了Deque接口,是一个双端队列,所以LinkedList又包含如下常用方法:

Deque接口部分方法.png

源码中“有趣”的设计#
方法重复定义

源码中Collection接口中的多个方法在List接口中又重复定义了一次,既然List 已经继承了Collection接口,为什么重复定义,历史原因?先有List后有Collection?

Collection接口-1.png

List接口-1.png

重复实现接口

AbstractList 已经实现List接口,ArrayList继承 AbstractList,然而ArrayList源码又实现了 List接口。

ArrayList类.png
AbstractListl类.png

网上搜了下答案:
重复实现接口.png

意思是他问过这块的开发者,这是一个错误。很久以前认为有价值的。

不知道这个答案是否正确?

https://stackoverflow.com/questions/2165204/why-does-linkedhashsete-extend-hashsete-and-implement-sete

小结#
List集合和Set集合都是继承Collection接口,Collection是List和Set的最上级接口,List接口下有两个常用的实现类,分别为ArrayList和LinkedList,而LinkedList又实现Deque接口,所以LinkedList即是List集合也是一个双端队列。

ArrayList是基于数组数据结构而实现的,而LinkedList是基于链表数据结构实现的,从数据结构特点和源码实现上来看,ArrayList可以根据索引快速获取到元素,而增加元素时需要数组的扩容和拷贝,删除元素时需要数组的移动拷贝,因此ArrayList集合对查找和修改元素效率较好,对增删效率略低。

LinkedList的链表数据结构不能根据索引直接快速获取元素节点,必须从头部,或者尾部遍历到索引位置(如果索引值小于集合长度的1/2时就从头部开始遍历,否则从尾部开始遍历,因此索引值处于中间时遍历效率会比位于两端要差。)而增加或删除元素时只需要将上下节点重新指向新的节点对象引用即可,不存在扩容,移动等情况,因此LinkedList和ArrayList相比更适合增加和删除元素操作,对查找操作效率较低。

转载请注明出处: https://www.cnblogs.com/newobjectcc/p/10789188.html#

相关文章
|
12月前
|
Java 开发者
重学Java基础篇—Java类加载顺序深度解析
本文全面解析Java类的生命周期与加载顺序,涵盖从加载到卸载的七个阶段,并深入探讨初始化阶段的执行规则。通过单类、继承体系的实例分析,明确静态与实例初始化的顺序。同时,列举六种触发初始化的场景及特殊场景处理(如接口初始化)。提供类加载完整流程图与记忆口诀,助于理解复杂初始化逻辑。此外,针对空指针异常等问题提出排查方案,并给出最佳实践建议,帮助开发者优化程序设计、定位BUG及理解框架机制。最后扩展讲解类加载器层次与双亲委派机制,为深入研究奠定基础。
450 0
|
12月前
|
存储 设计模式 Java
重学Java基础篇—ThreadLocal深度解析与最佳实践
ThreadLocal 是一种实现线程隔离的机制,为每个线程创建独立变量副本,适用于数据库连接管理、用户会话信息存储等场景。
424 5
|
12月前
|
存储 监控 安全
重学Java基础篇—类的生命周期深度解析
本文全面解析了Java类的生命周期,涵盖加载、验证、准备、解析、初始化、使用及卸载七个关键阶段。通过分阶段执行机制详解(如加载阶段的触发条件与技术实现),结合方法调用机制、内存回收保护等使用阶段特性,以及卸载条件和特殊场景处理,帮助开发者深入理解JVM运作原理。同时,文章探讨了性能优化建议、典型异常处理及新一代JVM特性(如元空间与模块化系统)。总结中强调安全优先、延迟加载与动态扩展的设计思想,并提供开发建议与进阶方向,助力解决性能调优、内存泄漏排查及框架设计等问题。
507 5
|
12月前
|
机器学习/深度学习 人工智能 Java
Java机器学习实战:基于DJL框架的手写数字识别全解析
在人工智能蓬勃发展的今天,Python凭借丰富的生态库(如TensorFlow、PyTorch)成为AI开发的首选语言。但Java作为企业级应用的基石,其在生产环境部署、性能优化和工程化方面的优势不容忽视。DJL(Deep Java Library)的出现完美填补了Java在深度学习领域的空白,它提供了一套统一的API,允许开发者无缝对接主流深度学习框架,将AI模型高效部署到Java生态中。本文将通过手写数字识别的完整流程,深入解析DJL框架的核心机制与应用实践。
720 3
|
12月前
|
前端开发 数据安全/隐私保护 CDN
二次元聚合短视频解析去水印系统源码
二次元聚合短视频解析去水印系统源码
470 4
|
12月前
|
安全 IDE Java
重学Java基础篇—Java Object类常用方法深度解析
Java中,Object类作为所有类的超类,提供了多个核心方法以支持对象的基本行为。其中,`toString()`用于对象的字符串表示,重写时应包含关键信息;`equals()`与`hashCode()`需成对重写,确保对象等价判断的一致性;`getClass()`用于运行时类型识别;`clone()`实现对象复制,需区分浅拷贝与深拷贝;`wait()/notify()`支持线程协作。此外,`finalize()`已过时,建议使用更安全的资源管理方式。合理运用这些方法,并遵循最佳实践,可提升代码质量与健壮性。
356 1
|
12月前
|
JavaScript 算法 前端开发
JS数组操作方法全景图,全网最全构建完整知识网络!js数组操作方法全集(实现筛选转换、随机排序洗牌算法、复杂数据处理统计等情景详解,附大量源码和易错点解析)
这些方法提供了对数组的全面操作,包括搜索、遍历、转换和聚合等。通过分为原地操作方法、非原地操作方法和其他方法便于您理解和记忆,并熟悉他们各自的使用方法与使用范围。详细的案例与进阶使用,方便您理解数组操作的底层原理。链式调用的几个案例,让您玩转数组操作。 只有锻炼思维才能可持续地解决问题,只有思维才是真正值得学习和分享的核心要素。如果这篇博客能给您带来一点帮助,麻烦您点个赞支持一下,还可以收藏起来以备不时之需,有疑问和错误欢迎在评论区指出~
|
12月前
|
负载均衡 JavaScript 前端开发
分片上传技术全解析:原理、优势与应用(含简单实现源码)
分片上传通过将大文件分割成多个小的片段或块,然后并行或顺序地上传这些片段,从而提高上传效率和可靠性,特别适用于大文件的上传场景,尤其是在网络环境不佳时,分片上传能有效提高上传体验。 博客不应该只有代码和解决方案,重点应该在于给出解决方案的同时分享思维模式,只有思维才能可持续地解决问题,只有思维才是真正值得学习和分享的核心要素。如果这篇博客能给您带来一点帮助,麻烦您点个赞支持一下,还可以收藏起来以备不时之需,有疑问和错误欢迎在评论区指出~
|
安全 Java
java线程之List集合并发安全问题及解决方案
java线程之List集合并发安全问题及解决方案
1638 1
|
运维 关系型数据库 Java
PolarDB产品使用问题之使用List或Range分区表时,Java代码是否需要进行改动
PolarDB产品使用合集涵盖了从创建与管理、数据管理、性能优化与诊断、安全与合规到生态与集成、运维与支持等全方位的功能和服务,旨在帮助企业轻松构建高可用、高性能且易于管理的数据库环境,满足不同业务场景的需求。用户可以通过阿里云控制台、API、SDK等方式便捷地使用这些功能,实现数据库的高效运维与持续优化。

推荐镜像

更多
  • DNS