Java基础快速入门: List集合、数据结构与源码解析

本文纲要

  1. List集合概述与基本使用
    1.1 List接口特点
    1.2 项目结构
    1.3 基本遍历方式
  2. List特有方法
    2.1 add(int index, E element)
    2.2 remove(int index)remove(Object o)
    2.3 set(int index, E element)
    2.4 get(int index)
  3. 常见数据结构:栈和队列
    3.1 栈(Stack
    3.2 队列(Queue
  4. 数据结构:数组与链表
    4.1 数组
    4.2 链表
  5. ArrayList 底层原理与源码分析
    5.1 ArrayList 底层数据结构
    5.2 空参构造及首次添加元素
    5.3 扩容机制
    5.4 查询与遍历
  6. LinkedList 基本使用
  7. LinkedList 特有方法
  8. LinkedList 源码解析

List集合概述与基本使用

1 )List接口特点

List是单列集合Collection的子接口,代表一个有序、有索引、可重复的元素序列。

  • 有序:指的是存取顺序一致,即按什么顺序存入,就按什么顺序取出。
  • 有索引:可以通过整数索引(从0开始)精确操作每一个元素,包括获取、修改、删除。
  • 可重复:允许存储重复的元素。

常见的List实现类包括ArrayListLinkedList,它们都实现了List接口的全部方法。由于List继承了Collection,所以Collection中的通用方法(如addremovecontains等)在List中同样适用。

2 ) 项目结构

本文涉及的示例代码位于如下包结构中,后续讲解将围绕这些类展开:

mycollection/
└── src/
    └── com/
        └── wb/
            └── mylistdemo1/
                ├── MyListDemo1.java 
                ├── MyListDemo2.java 
                ├── MyLinkedListDemo3.java 
                └── MyLinkedListDemo4.java 

3 ) 基本遍历方式

下面通过 MyListDemo1演示List结合泛型的创建、添加元素以及两种常见的遍历方式:迭代器和增强for循环

// MyListDemo1.java 
package com.wb.mylistdemo1;
 
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
 
public class MyListDemo1 {
    public static void main(String[] args) {
        // 创建List集合,使用多态,实际是ArrayList 
        List<String> list = new ArrayList<>();
 
        // 添加元素 
        list.add("a");
        list.add("b");
        list.add("c");
        list.add("d");
 
        // 方式一:迭代器遍历 
        Iterator<String> it = list.iterator();
        while (it.hasNext()) {
            String s = it.next();
            System.out.println(s);
        }
 
        System.out.println("---------------------");
 
        // 方式二:增强for遍历 
        for (String s : list) {
            System.out.println(s);
        }
    }
}

输出结果均为 a b c d,顺序与插入时一致。

List特有方法

List 接口中定义了一系列与索引相关的方法,这是Set集合所不具备的。包括:

  • void add(int index, E element) — 在指定位置插入元素
  • E remove(int index) — 删除指定索引的元素并返回被删除的元素
  • E set(int index, E element) — 修改指定索引的元素,返回被修改的元素
  • E get(int index) — 返回指定索引处的元素

此外,List中存在两个重载的remove方法,需要注意区分:

  • boolean remove(Object o) — 删除指定的元素,返回是否删除成功
  • E remove(int index) — 删除指定索引的元素,返回被删除的元素

下面的示例将它们拆分为独立的方法进行展示。

1 ) add(int index, E element)

// 方法 extract from MyListDemo2 
private static void method1(List<String> list) {
    // 在索引0处插入"qqq"
    list.add(0, "qqq");
    System.out.println(list);
}

注意事项:原来该位置及其后的元素会依次后移一个索引。

2 ) remove(int index)remove(Object o)

private static void method2(List<String> list) {
    // 按索引删除 
    String removed = list.remove(0);    // 删除0索引的元素,返回被删除的元素 
    System.out.println(removed);
    System.out.println(list);
 
    // 按元素对象删除 
    boolean success = list.remove("bbb"); // 删除指定的元素"bbb",返回是否成功 
    System.out.println("删除bbb成功:" + success);
    System.out.println(list);
}

两个remove方法的参数类型不同,调用时根据参数类型自动匹配。当传入int时调用索引删除,传入Object时调用元素删除。

3 ) set(int index, E element)

private static void method3(List<String> list) {
    // 将索引0处的元素修改为"qqq"
    String oldValue = list.set(0, "qqq");
    System.out.println("被替换的元素:" + oldValue);
    System.out.println(list);
}

注意:被替换的元素在集合中不再存在,返回的是旧值。

4 ) get(int index)

private static void method4(List<String> list) {
    // 获取0索引的元素 
    String s = list.get(0);
    System.out.println(s);
}

get方法常与普通for循环结合,遍历整个集合:

for (int i = 0; i < list.size(); i++) {
    System.out.println(list.get(i));
}

整合后的MyListDemo2完整代码:

package com.wb.mylistdemo1;
 
import java.util.ArrayList;
import java.util.List;
 
public class MyListDemo2 {
    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        list.add("aaa");
        list.add("bbb");
        list.add("ccc");
 
        // 逐一演示各个方法(取消注释即可运行对应方法)
        method1(list);
        method2(list);
        method3(list);
        method4(list);
    }
 
    private static void method1(List<String> list) {
        System.out.println("--- add(index, element) ---");
        list.add(0, "qqq");
        System.out.println(list);
    }
 
    private static void method2(List<String> list) {
        System.out.println("--- remove(index) & remove(object) ---");
        // 按索引删除 
        String removedByIndex = list.remove(0);
        System.out.println("删除索引0的元素: " + removedByIndex);
        System.out.println(list);
        // 按对象删除 
        boolean removedByObject = list.remove("bbb");
        System.out.println("删除元素bbb是否成功: " + removedByObject);
        System.out.println(list);
    }
 
    private static void method3(List<String> list) {
        System.out.println("--- set(index, element) ---");
        String old = list.set(0, "qqq");
        System.out.println("被替换的元素: " + old);
        System.out.println(list);
    }
 
    private static void method4(List<String> list) {
        System.out.println("--- get(index) ---");
        String first = list.get(0);
        System.out.println(first);
    }
}

常见数据结构:栈和队列

数据结构是计算机存储和组织数据的方式,不同的数据结构直接影响程序的运行和存储效率。
下面介绍两种经典模型:栈与队列。

1 ) 栈(Stack)

栈是一种先进后出(FILO,First In Last Out)的结构。数据从一端(栈顶)进入,称为压栈/进栈,也从同一端出去,称为弹栈/出栈

出栈顺序

数据D

数据C

数据B

数据A

压栈顺序

数据A

数据B

数据C

数据D

类比:手枪弹夹,先压入的子弹最后打出。

2 ) 队列(Queue)

队列是一种先进先出(FIFO,First In First Out)的结构。数据从后端进入(入队列),从前端离开(出队列)。

出队列方向

数据D

数据C

数据B

数据A

入队列方向

数据A

数据B

数据C

数据D

类比:排队购票,后来的人排在队尾,队首的人先完成离开。

数据结构:数组与链表

1 ) 数组

数组在内存中是一块连续的空间,通过基地址加索引可以快速定位任意元素,因此查询速度快。但增删元素时需要移动大量数据,效率较低。

  • 删除元素:删除位置后的所有元素依次前移
  • 插入元素:插入位置后的所有元素依次后移

因此,数组是一种 查询快、增删慢 的模型。

2 ) 链表

链表由一个个节点(Node)组成,每个节点独立存储,通过记录相邻节点的地址值形成链状结构。

  • 单向链表:每个节点记录自己的值和下一个节点的地址。
  • 双向链表:每个节点记录自己的值、前一个节点的地址、下一个节点的地址。既能从前向后查找,也能从后向前查找,查询效率更高。

链表在增删时只需修改相邻节点的指向,无需移动大量数据,因此 增删快;但查询时必须从头(或尾)开始遍历,因此 查询慢(相对数组)。

ArrayList底层原理与源码分析

1 ) ArrayList底层数据结构

ArrayList底层是一个动态数组,名为 elementData,类型为 Object[]。其特点为查询快、增删慢。同时维护一个 size 变量,表示当前元素个数,也指向下一次添加操作的索引位置。

2 ) 空参构造及首次添加元素

使用空参构造创建ArrayList时,底层会创建一个长度为0的数组:

// 源码片段(简化理解)
private static final Object[] DEFAULTCAPACITYEMPTYELEMENTDATA = {};
public ArrayList() {
    this.elementData = DEFAULTCAPACITYEMPTYELEMENTDATA;
}

当第一次调用 add(E e) 时,会触发扩容,创建一个默认容量为 10 的新数组,并将元素存入 size 指向的位置,随后 size 自增为1

3 ) 扩容机制

当数组已满(size == elementData.length)时,继续添加元素会触发扩容,流程如下:

  1. 计算新容量:newCapacity = oldCapacity + (oldCapacity >> 1),即扩容至原来的1.5倍
  2. 使用 Arrays.copyOf 将原数组的元素拷贝到新数组中。
  3. size 位置插入新元素,size 自增。

添加元素

数组容量足够?

直接赋值 elementData[size++] = e

计算新容量 1.5倍

Arrays.copyOf 复制到新数组

更新 elementData 引用

4 )查询与遍历

get(int index) 方法直接返回 elementData[index],前提是检查 index 是否小于 size,否则抛出异常。size() 方法返回 size 变量的值,因此普通for循环遍历时直接从0遍历到 size-1即可。

LinkedList基本使用

LinkedList 底层是双向链表,实现了ListDeque接口。它的特点是增删快、查询慢(与ArrayList相反)。由于实现了List接口,ArrayList 中使用的遍历方法在这里完全通用。

// MyLinkedListDemo3.java 
package com.wb.mylistdemo1;
 
import java.util.Iterator;
import java.util.LinkedList;
 
public class MyLinkedListDemo3 {
    public static void main(String[] args) {
        LinkedList<String> list = new LinkedList<>();
        list.add("aaa");
        list.add("bbb");
        list.add("ccc");
 
        // 普通for遍历 
        for (int i = 0; i < list.size(); i++) {
            System.out.println(list.get(i));
        }
        System.out.println("-------------------------");
 
        // 迭代器遍历 
        Iterator<String> it = list.iterator();
        while (it.hasNext()) {
            String s = it.next();
            System.out.println(s);
        }
        System.out.println("--------------------------");
 
        // 增强for遍历 
        for (String s : list) {
            System.out.println(s);
        }
    }
}

LinkedList特有方法

LinkedList 除实现List接口的方法外,还提供了大量针对头尾操作的特有方法:

方法描述
addFirst(E e)在列表开头插入指定元素
addLast(E e)在列表末尾追加指定元素
getFirst()返回此列表的第一个元素
getLast()返回此列表的最后一个元素
removeFirst()删除并返回第一个元素
removeLast()删除并返回最后一个元素

示例代码:

// MyLinkedListDemo4.java 
package com.wb.mylistdemo1;
 
import java.util.LinkedList;
 
public class MyLinkedListDemo4 {
    public static void main(String[] args) {
        LinkedList<String> list = new LinkedList<>();
        list.add("aaa");
        list.add("bbb");
        list.add("ccc");
 
        // 分别演示四组方法,取消注释即可运行 
        // method1(list);  // addFirst 
        // method2(list);  // addLast 
        // method3(list);  // getFirst & getLast 
        // method4(list);  // removeFirst & removeLast 
    }
 
    private static void method1(LinkedList<String> list) {
        // 在开头插入元素 
        list.addFirst("qqq");
        System.out.println(list); // [qqq, aaa, bbb, ccc]
    }
 
    private static void method2(LinkedList<String> list) {
        // 在末尾追加元素 
        list.addLast("www");
        System.out.println(list); // [aaa, bbb, ccc, www]
    }
 
    private static void method3(LinkedList<String> list) {
        // 获取第一个和最后一个元素 
        String first = list.getFirst();
        String last = list.getLast();
        System.out.println(first); // aaa 
        System.out.println(last);  // ccc 
    }
 
    private static void method4(LinkedList<String> list) {
        // 移除第一个和最后一个元素 
        String first = list.removeFirst();
        String last = list.removeLast();
        System.out.println(first); // aaa 
        System.out.println(last);  // ccc 
        System.out.println(list);  // [bbb]
    }
}

注意:getFirst / getLast 在集合为空时会抛出 NoSuchElementException;而 removeFirst / removeLast 同样如此。实际开发中可使用 pollFirst / pollLast 方法,它们在集合为空时返回 null 而非抛异常。

LinkedList源码解析

LinkedList 底层基于双向链表实现,由一个个 Node(节点)对象连接而成。每个节点包含三个部分:

  • item:存储本节点的数据
  • next:指向下一个节点的引用(地址值)
  • prev:指向前一个节点的引用(地址值)

Node

-E item

-Node next

-Node prev

LinkedList 本身维护了两个核心成员变量:

  • Node first:指向链表的头节点
  • Node last:指向链表的尾节点

1 ) 空参构造

// LinkedList 内部 
transient Node<E> first;
transient Node<E> last;
 
public LinkedList() {
    // 没有任何赋值,first 和 last 均为 null 
}

此时,链表为空,first == nulllast == null

2 ) 添加元素(add方法)

调用 add(E e) 默认将元素追加到链表末尾,内部实际调用 linkLast(e)。下面通过添加 "aaa""bbb""ccc" 三个元素的过程来剖析源码。

添加第一个元素 “aaa”

  1. 当前 last == nulll = null
  2. 创建新节点 newNode = new Node<>(l, "aaa", null),此时 prev = nullitem = "aaa"next = null
    last = newNode,尾指针指向新节点
  3. 判断 l == null 成立,因此 first = newNode,头指针也指向该节点

此时链表状态:

first

aaa

last

A.prev

null

A.next

null

添加第二个元素 “bbb”

  1. 当前 last 指向 "aaa" 节点,l = "aaa" 节点
  2. 创建新节点 newNode = new Node<>(l, "bbb", null)prev 指向 "aaa" 节点
    last = newNode,尾指针后移
  3. l != null,执行 l.next = newNode,将原来尾节点的 next 域指向新节点

此时链表状态:

first

aaa

A.next

bbb

B.prev

last

B.next

null

添加第三个元素 “ccc”

  1. 当前 last 指向 "bbb" 节点,l = "bbb" 节点
  2. 创建新节点 newNode = new Node<>(l, "ccc", null)
  3. last = newNode
  4. l.next = newNode

最终链表结构:

first

aaa

A.next

bbb

B.prev

B.next

ccc

C.prev

last

C.next

null

核心源码片段(linkLast):

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;     // 将原尾节点的next指向新节点 
    size++;
}

3 ) 获取元素(get方法)

get(int index) 方法根据索引查找元素,内部调用 node(int index)

node(int index) 方法利用二分思维,判断索引离头还是尾更近,以提升效率:

  • 如果 index < (size >> 1),从 first 开始向后遍历
  • 否则从 last 开始向前遍历
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;
    }
}

例如查询索引为2的元素(在长度为5的链表中),因为 2 < (5 >> 1) (即2.5)成立,会选择从 first 向后遍历3步。若查询索引为3,则从 last 向前遍历。

4 ) 删除元素(remove方法)

LinkedList 既支持按索引删除,也支持按元素删除,还提供了 removeFirst() / removeLast() 等特有方法。删除的核心操作是修改前后节点的指针,并将当前节点的 itemprevnextnull 以便GC回收。

以删除第一个元素为例:

  1. 获取 first 节点 f
  2. 获取下一个节点 next = f.next
  3. first 指向 next
  4. next == null,说明链表已空,last 也置 null;否则 next.prev = null
  5. size-- 并返回被删除的 item
private E unlinkFirst(Node<E> f) {
    final E element = f.item;
    final Node<E> next = f.next;
    f.item = null;
    f.next = null; // help GC 
    first = next;
    if (next == null)
        last = null;
    else 
        next.prev = null;
    size--;
    return element;
}

5 ) 对比 ArrayList

ArrayList:底层数组,查询快(O(1)),增删慢(需要移动元素)
LinkedList:底层双向链表,增删快(只需修改指针),查询慢(需要遍历)

开发者应根据实际场景选择合适的数据结构。

总结

至此,关于 Java 集合中 List 接口及其典型实现类的介绍已全部完成。

通过理解数据结构(数组、链表、栈、队列)以及阅读底层源码,能够更好地掌握集合的特性,为后续高级开发打下扎实的基础。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Wang's Blog

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值