当前位置: 首页 > ops >正文

【数据结构】-5- 顺序表 (下)

一、集合框架 

这是 Java 集合框架(Java Collections Framework)的核心继承关系树状图

1. 最顶层:Iterable(接口)
  • 作用:所有 “可迭代” 的集合(如 ListSetQueue)都必须实现它,提供遍历元素的能力(通过 iterator() 方法生成迭代器)。
  • 关键方法:Iterator<T> iterator() → 返回迭代器,用于遍历集合元素。

2. 第二层:Collection(接口)
  • 地位:Java 集合框架的根接口(除 Map 体系外,所有集合都直接 / 间接继承它)。
  • 定义:规范了集合的基础行为(增、删、查、遍历等),是所有单列集合(单个元素存储)的 “通用契约”。
  • 关键方法:
    • add(E e):添加元素
    • remove(Object o):删除元素
    • contains(Object o):判断是否包含元素
    • size():获取元素个数
    • iterator():获取迭代器(继承自 Iterable

3. 第三层:三大子接口(ListQueueSet

Collection 派生出三个核心子接口,代表三种不同的集合特性

(1)List(接口)
  • 特点:有序、可重复(元素按插入顺序排列,允许存重复值)。
  • 典型实现类:
    • ArrayList:基于动态数组,查询快(下标访问 O(1))、增删慢(需移动元素)。
    • LinkedList:基于双向链表,增删快(改变指针 O(1))、查询慢(需遍历链表)。
    • Vector:古老实现(线程安全,但性能差,已被 ArrayList 替代)。
    • Stack:继承 Vector,实现栈结构(后进先出,LIFO)。
(2)Queue(接口)
  • 特点:队列特性(通常 “先进先出,FIFO”,但部分实现支持优先级)。
  • 典型实现类:
    • LinkedList:同时实现 List 和 Queue,作为链表队列使用。
    • PriorityQueue优先队列(元素按优先级排序,不严格遵循插入顺序)。
    • Deque(子接口):双端队列(两端都可增删),LinkedList 也实现了它。
(3)Set(接口)
  • 特点:无序、不可重复(元素无顺序,且 equals 相等的元素只能存一个)。
  • 典型实现类:
    • HashSet:基于哈希表HashMap 实现),增删查快(平均 O(1)),无序。
    • TreeSet:基于红黑树,元素自动排序(需实现 Comparable),查询慢(O(log n))。
    • SortedSet(子接口):规范 “有序 Set” 行为,TreeSet 实现它。

4. 抽象类(AbstractList
  • 作用:简化集合实现,为 List 接口的实现类提供通用逻辑(如部分方法默认实现)。
  • 典型继承:ArrayListLinkedList 都继承它,避免重复写 addremove 等基础逻辑。

⭐ 核心设计思想

  1. 接口分层

    • Iterable 定义 “可遍历” 能力 → Collection 定义 “集合基础行为” → 子接口(List/Queue/Set)细化特性。
    • 解耦 “通用行为” 和 “具体特性”,方便扩展(如新增集合类型只需实现接口)。
  2. 实现类分工

    • 不同实现类(ArrayList/LinkedList/HashSet 等)针对不同场景优化(数组查快、链表增删快、哈希表去重等)。
    • 开发时根据需求选实现类(查多改少用 ArrayList;增删多用 LinkedList;去重用 Set)。
  3. 复用与扩展

    • 抽象类(如 AbstractList)封装通用逻辑,减少实现类代码量。
    • 接口(如 Deque 继承 Queue)扩展功能,让集合支持更多特性(双端操作)。

⭐ 典型应用场景

  • 存有序可重复数据 → 选 List(如 ArrayList 存学生名单,允许重复姓名)。
  • 存无序不可重复数据 → 选 Set(如 HashSet 存用户 ID,避免重复)。
  • 需队列 / 栈行为 → 选 Queue/Stack(如 PriorityQueue 实现任务优先级调度)。

掌握这张图,就能清晰理解 Java 集合的体系脉络,开发时也能更合理地选集合类型~

二、集合和数组区别 

你观察得很敏锐!ArrayList 底层确实是用数组实现的(顺序表本质),但 “集合转数组” 的操作依然有其必要性,核心原因在于 “集合的抽象特性” 与 “数组的具体特性” 之间的差异,具体可以从三个角度理解:

⭐ 接口与实现的分离:集合是 “抽象容器”,数组是 “具体存储”

ArrayList 虽然底层用数组存储,但它对外暴露的是 List 接口的抽象方法(如 addgetsize 等),隐藏了数组的具体实现细节(比如扩容逻辑、下标管理等)。

这种 “抽象” 带来了便利(比如无需手动管理容量),但也限制了对底层数组的直接操作。例如:

  • 你不能直接用 ArrayList 调用数组特有的工具方法(如 Arrays.sort()Arrays.binarySearch());
  • 某些 API 或框架可能只接收 Object[] 类型的参数(而非 List 集合)。

此时,toArray() 的作用就是 “打破抽象”,将集合中 “隐藏的数组元素” 提取出来,转换为可直接操作的数组对象,以便使用数组的特性或兼容外部需求。

⭐ 功能场景的差异:集合擅长 “动态管理”,数组擅长 “静态操作”

ArrayList 作为集合,核心优势是 动态管理数据(自动扩容、简化增删逻辑);而数组的优势是 静态高效操作(如固定长度下的快速遍历、使用 Arrays 工具类的各种功能)。

举两个典型场景:

  1. 需要使用数组工具类
    Arrays 类提供了大量便捷方法(排序、二分查找、填充等),但这些方法的参数必须是数组。例如,要对 ArrayList 中的元素排序,直接操作集合不方便,但转成数组后就很简单:

    List<Integer> list = new ArrayList<>(Arrays.asList(3, 1, 2));
    Integer[] arr = list.toArray(new Integer[0]); // 转数组
    Arrays.sort(arr); // 用数组工具排序
    System.out.println(Arrays.toString(arr)); // [1, 2, 3]
    
  2. 需要固定长度的不可变结构
    集合是 “动态的”(可以随时 add/remove 元素),而有时我们需要一个 “固定不变的快照”(比如传递数据时不希望被修改)。数组的长度固定,转成数组后可以避免误操作导致的元素变化:

    Object[] arr = list.toArray(); // 数组长度固定为3
    // 此时即使修改原集合,数组也不受影响
    list.add("西瓜"); 
    System.out.println(arr.length); // 仍为3(数组长度不变)
    

⭐ 类型系统的限制:集合的泛型与数组的 “具体化”

ArrayList 虽然通过泛型(如 List<String>)保证了元素类型安全,但这种泛型是 编译时检查,运行时会被擦除(类型擦除)。而数组是 “具体化类型”(运行时也能确定类型),两者在类型处理上有差异。

例如,当需要将集合元素传递给一个 “要求具体类型数组” 的方法时(如 void process(String[] strs)),直接传 List<String> 会编译报错,必须通过 toArray(String[]) 转换为具体类型的数组:

process在编译检查时会确定里面的参数是不是string类型数组,但是List<String>里面会被擦除,无法判断是不是确切的String类型,编译会报错。

// 定义一个接收 String[] 参数的方法
public static void process(String[] strs) {for (String s : strs) {System.out.println(s);}
}// 调用方法时,必须将 List<String> 转为 String[]
List<String> list = new ArrayList<>(Arrays.asList("a", "b"));
process(list.toArray(new String[0])); // 正确:传递 String[] 数组

总结

ArrayList 底层用数组实现,但它作为 “集合” 提供的是更高层次的抽象功能(动态管理)。而 toArray() 的作用是 在需要时 “降级” 到数组层面,利用数组的特性(工具类支持、固定长度、具体类型)来满足特定场景需求。

简单说:集合是 “包装好的数组”,方便日常使用;toArray() 是 “拆包装” 的工具,让我们在需要时能直接操作里面的 “数组内核”。

 三、toArray() 

在 Java 集合框架中,toArray() 是 Collection 接口(所有集合类的顶层接口,如 ArrayListLinkedListHashSet 等都实现了它) 定义的核心方法,作用是 将集合中的所有元素,转换为一个「数组」并返回,本质是实现「集合 → 数组」的类型转换。

[1] 核心作用:集合转数组的 “桥梁”

集合(如 ArrayList)是动态存储结构(大小可自动扩容),而数组是静态存储结构(初始化后大小固定)。

当需要用数组的方式操作集合元素(比如兼容某些只接收数组参数的方法、或需要数组的高效随机访问特性)时,toArray() 就是两者之间的关键桥梁。

// 1. 创建一个集合,存入元素
List<String> list = new ArrayList<>();
list.add("苹果");
list.add("香蕉");
list.add("橙子");// 2. 调用 toArray(),将集合转为数组
Object[] fruitArray = list.toArray();// 3. 遍历数组(此时可以用数组的操作方式处理原集合元素)
for (Object fruit : fruitArray) {System.out.println(fruit); // 输出:苹果、香蕉、橙子
}

在代码 Object[] fruitArray = list.toArray(); 中,toArray() 方法括号里没有 new Object[0],是因为调用的是 无参的 toArray() 方法,而不是带参数的 toArray(T[] a) 方法。这两个方法的设计目的和返回值有本质区别:

1. 无参 toArray():直接返回 Object[] 数组

无参的 toArray() 方法是 Collection 接口定义的默认实现,它的行为是:

  • 返回值固定为 Object[] 类型,无论集合的泛型是什么(即使是 List<String>,返回的也是 Object[])。
  • 内部会创建一个新的 Object[] 数组,并将集合中的元素复制进去。

因此,当我们写 list.toArray() 时,不需要传入任何参数,方法会自动创建并返回一个 Object[] 数组,所以代码中直接写成 list.toArray() 即可,无需 new Object[0]

2. 带参 toArray(T[] a):需要传入数组参数

另一个重载方法 toArray(T[] a) 的作用是 返回指定类型的数组(如 String[]Integer[]),而非默认的 Object[]。此时需要传入一个数组参数(如 new String[0]),目的是:

  • 告诉方法 “我需要返回什么类型的数组”(参数的类型决定了返回数组的类型)。
  • 例如 list.toArray(new String[0]) 会返回 String[] 类型,而不是 Object[]

3. 为什么这里不需要 new Object[0]

因为代码的需求是 获取 Object[] 类型的数组,而无参 toArray() 正好直接返回 Object[],完全满足需求。此时如果画蛇添足地传入 new Object[0],写成:

Object[] fruitArray = list.toArray(new Object[0]); // 多余的参数

虽然功能上没问题(返回的也是 Object[]),但参数是多余的 —— 无参方法已经能实现同样的效果,传入参数反而增加了代码的冗余。

[2] 两种常用重载形式(重点区分)

toArray() 有两个核心重载方法,用法和返回值差异很大,实际开发中需根据需求选择:

方法签名返回值类型核心特点适用场景
Object[] toArray()Object[]固定返回 Object 类型数组,无论集合泛型是什么仅需 “遍历元素”,不关心元素具体类型时(但有类型转换风险,需谨慎)
<T> T[] toArray(T[] a)传入的数组类型 T[]可指定返回数组的具体类型(如 String[]Integer[]),避免后续强制类型转换需要明确数组类型(如调用需 String[] 参数的方法),是更常用、更安全的方式

(1)无参 toArray():返回 Object[]
  • 特点:无论原集合的泛型是 StringInteger 还是其他类型,返回的数组元素类型都是 Object(因为数组的编译时类型是 Object[])。
  • 注意:不能直接将返回的 Object[] 强制转为具体类型数组(如 String[]),会触发 ClassCastException(类型转换异常)。

List<String> list = new ArrayList<>();
list.add("a");
// 错误!Object[] 不能直接强转 String[]
String[] arr = (String[]) list.toArray(); // 运行时抛 ClassCastException

(2)带参 toArray(T[] a):返回指定类型数组
  • 特点:传入一个 “指定类型的数组” 作为参数,方法会根据该数组的类型,返回一个同类型的数组(存储集合元素),避免类型转换风险。
  • 内部逻辑:
    1. 如果传入的数组 a 大小 ≥ 集合元素个数:直接将集合元素填入数组 a,多余位置填 null,返回 a 本身;
    2. 如果传入的数组 a 大小 < 集合元素个数:创建一个 “和 a 同类型、大小等于集合元素个数” 的新数组,填入元素后返回新数组。

List<String> list = new ArrayList<>();
list.add("a");
list.add("b");// 方式1:传入一个大小足够的数组(推荐,避免创建新数组)
String[] arr1 = new String[list.size()];
arr1 = list.toArray(arr1); // 返回 String[],可直接使用// 方式2:传入一个空数组(简洁,JDK 1.8+ 常用)
String[] arr2 = list.toArray(new String[0]); // 内部会创建大小为 2 的新 String[]// 遍历数组(无需强制转换,类型安全)
for (String s : arr2) {System.out.println(s); // 输出 a、b
}

[3] 关键注意点(避坑)

(1)无参 toArray() 的类型限制

再次强调:无参方法返回的 Object[] 只能存 Object 类型,不能强转具体类型数组。若需具体类型,必须用带参 toArray(T[] a)

(2)返回数组是 “新数组” 还是 “原集合底层数组”?

  • 对于 ArrayListtoArray() 返回的是复制后的新数组,修改数组元素不会影响原集合(因为数组和集合底层存储的是两个独立的数组)。

    List<Integer> list = new ArrayList<>();
    list.add(10);
    Integer[] arr = list.toArray(new Integer[0]);
    arr[0] = 20; // 修改数组元素
    System.out.println(list.get(0)); // 输出 10(原集合无变化)
    
  • 对于其他集合(如 Vector):逻辑类似,均返回 “元素副本的数组”,避免外部通过数组修改集合内部状态。
(3)集合元素为 null 时的处理

若集合中包含 null 元素,toArray() 会将 null 也存入数组(数组允许 null),遍历数组时需注意判空,避免 NullPointerException

List<String> list = new ArrayList<>();
list.add("a");
list.add(null); // 集合允许存 null
String[] arr = list.toArray(new String[0]);
System.out.println(arr[1]); // 输出 null

四、List 接口常用方法 

一、增(添加元素)

  • boolean add(E e)

    • 功能:在顺序表末尾插入元素(尾插)。
    • 逻辑:检查容量 → 数组末尾赋值 → size++ 。
    • 示例:

      ArrayList<Integer> list = new ArrayList<>();
      list.add(10); // 末尾插入10,size 变为1
      
  • void add(int index, E element)

    • 功能:在指定下标 index 处插入元素,后续元素后移。
    • 逻辑:检查下标合法性 → 扩容(如需) → 从 index 开始后移元素 → 插入新值 → size++ 。
    • 注意:index 范围 [0, size](允许末尾插入,对应 index=size )。
    • 示例:
    • list.add(1, 20); // 在索引1处插入20,原索引1及之后元素后移
      
  • boolean addAll(Collection<? extends E> c)

    • 功能:把另一个集合 c 的元素全部尾插到当前表
    • 逻辑:遍历集合 c ,逐个尾插元素;若插入成功(至少一个),返回 true 。
    • 示例:

      List<Integer> other = Arrays.asList(30, 40);
      list.addAll(other); // 末尾插入30、40,size 增加2
      

二、删(删除元素)

  1. E remove(int index)

    • 功能:删除指定下标 index 处的元素,返回被删元素;后续元素前移。
    • 逻辑:检查下标合法性 → 保存被删元素 → 从 index+1 开始前移元素 → size-- → 返回被删值。
    • 示例:

      int removed = list.remove(1); // 删除索引1的元素,返回该元素
      
  2. boolean remove(Object o)

    • 功能:删除第一个遇到的元素 o(基于 equals 比较),删除成功返回 true 。
    • 逻辑:遍历查找第一个与 o 相等的元素 → 找到则删除(后续元素前移) → 返回是否删除成功。
    • 注意:若存储自定义对象,需重写 equals 保证比较逻辑正确。
    • 示例:

      list.remove(Integer.valueOf(20)); // 删除第一个值为20的元素
      

三、改(修改元素)

  • E set(int index, E element)
    • 功能:将下标 index 处的元素替换为 element,返回原元素。
    • 逻辑:检查下标合法性 → 保存原元素 → 赋值新元素 → 返回原元素。
    • 示例:
    • int old = list.set(1, 25); // 索引1的元素改为25,返回原元素
      

四、查(获取 / 查找元素)

  1. E get(int index)

    • 功能:获取下标 index 处的元素
    • 逻辑:检查下标合法性 → 直接返回数组对应位置的值(时间复杂度 O(1) )。
    • 示例:

      int val = list.get(1); // 获取索引1处的元素值
      
  2. boolean contains(Object o)

    • 功能:判断元素 o 是否存在于表中(基于 equals 遍历比较)。
    • 逻辑:遍历表,用 equals 比较元素;找到则返回 true ,否则 false 。
    • 注意:自定义对象需重写 equals ,否则比较地址而非内容。
    • 示例:

      boolean has = list.contains(25); // 判断25是否在表中
      
  3. int indexOf(Object o)

    • 功能:返回第一个 o 所在的下标(基于 equals 遍历);不存在返回 -1 。
    • 逻辑:遍历表,找到第一个与 o equals 的元素,返回其下标。
    • 示例:

      int idx = list.indexOf(25); // 找25第一次出现的下标
      
  4. int lastIndexOf(Object o)

    • 功能:返回最后一个 o 所在的下标(反向遍历,基于 equals );不存在返回 -1 。
    • 逻辑:从表末尾往前遍历,找到第一个与 o equals 的元素,返回其下标。
    • 示例:

      int lastIdx = list.lastIndexOf(25); // 找25最后一次出现的下标
      

五、工具操作

  1. void clear()

    • 功能:清空顺序表(元素个数置 0,数组引用可保留或重置,不同实现可能有差异)。
    • 逻辑:size = 0 (或额外把数组元素置 null ,帮助 GC 回收)。
    • 示例:

      list.clear(); // 清空后 size=0,元素全被移除
      
  2. List<E> subList(int fromIndex, int toIndex)

    • 功能:截取从 fromIndex(含)到 toIndex(不含)的子表,返回新 List 。
    • 逻辑:创建新 List ,遍历原表对应区间,复制元素;子表与原表共享底层数组(修改子表会影响原表,需注意)。
    • 示例:

      List<Integer> sub = list.subList(1, 3); // 截取索引1、2的元素,生成子表
      

六、方法调用关系 & 核心特点

  • 依赖关系:增删操作常依赖扩容(如 add 时检查容量)元素移动(插入 / 删除时数组拷贝);查找依赖 equals(自定义对象需重写)。
  • 时间复杂度
    • 尾插 / 尾删(不扩容时):O(1) ;
    • 指定位置增删(需移动元素):O(n) ;
    • 查找(遍历):O(n) ;
    • 下标访问(get/set):O(1) 。
  • 适用场景:适合频繁按下标访问、尾插操作;若需大量中间增删,优先选链表(如 LinkedList )。

五、 Java 集合的迭代器(Iterator)遍历机制 

一、迭代器的作用:“指针” 式遍历集合

迭代器(Iterator)是 Java 集合框架中用于安全遍历集合元素的工具,它像一个 “移动的指针”,依次访问集合中的每个元素。

核心优势:

  • 支持在遍历中安全删除元素(避免普通 for 循环遍历删除时的 ConcurrentModificationException);
  • 统一了各类集合的遍历方式(ListSetQueue 都能用迭代器遍历)。

二、代码执行流程

假设 list 是一个 List<Integer>,存储元素 [1, 2, 3],代码:

Iterator<Integer> it = list.iterator(); // 1. 获取迭代器
while (it.hasNext()) {                  // 2. 检查是否有下一个元素System.out.println(it.next() + " "); // 3. 获取并移动到下一个元素
}
1. 获取迭代器:list.iterator()
  • 作用:创建一个与 list 关联的迭代器对象 it,初始时指向集合第一个元素之前(类似指针在数组下标 -1 的位置)。
2. 检查是否有下一个元素:it.hasNext()
  • 作用:判断迭代器当前位置之后是否还有未访问的元素
  • 执行逻辑:
    • 第一次调用:指针在 -1,检查到下标 0 有元素(1),返回 true
    • 第二次调用:指针在 0(已访问 1),检查到下标 1 有元素(2),返回 true
    • 第三次调用:指针在 1(已访问 2),检查到下标 2 有元素(3),返回 true
    • 第四次调用:指针在 2(已访问 3),检查到下标 3 无元素,返回 false,结束循环。
3. 获取并移动指针:it.next()
  • 作用:返回当前指针位置的下一个元素,并将指针向后移动一位(“自动向下执行” 的本质)。
  • 执行逻辑:

    • 第一次 it.next():返回下标 0 的元素 1,指针移动到 0 → 1 之间(指向 1 之后)。
    • 第二次 it.next():返回下标 1 的元素 2,指针移动到 1 → 2 之间。
    • 第三次 it.next():返回下标 2 的元素 3,指针移动到 2 → 3 之间。

三、迭代器的 “指针” 模型(对应图中结构)

图中右侧的 “竖排方框” 代表集合 list 的元素(123),it 是迭代器指针,执行流程对应:

  1. 初始状态:it 指向第一个元素(1之前(无具体位置,逻辑上在 -1)。
  2. 第一次 hasNext():检测到 1 存在 → 进入循环 → next() 返回 1,指针移动到 1 之后。
  3. 第二次 hasNext():检测到 2 存在 → 进入循环 → next() 返回 2,指针移动到 2 之后。
  4. 第三次 hasNext():检测到 3 存在 → 进入循环 → next() 返回 3,指针移动到 3 之后。
  5. 第四次 hasNext():检测到无元素 → 退出循环。

四、迭代器的关键特点

  1. 单向遍历:只能从前往后遍历,无法回退(若需回退,用 ListIterator 子接口)。
  2. 快速失败(fail-fast):遍历中若集合被修改(如 list.add/list.remove),会立即抛出 ConcurrentModificationException,避免脏读。
  3. 与集合状态绑定:迭代器遍历的是创建时集合的 “快照”(但如果是 ArrayList 等基于数组的集合,遍历中修改会触发快速失败)。

五、对比普通 for 循环遍历

虽然 for-each 本质也是迭代器遍历,但手动用 Iterator 的优势在于:

  • 支持遍历中删除元素it.remove(),安全且不会报错);
  • 更灵活(可控制遍历过程,比如跳过某些元素)。

示例:遍历中删除元素(安全写法)

Iterator<Integer> it = list.iterator();
while (it.hasNext()) {Integer num = it.next();if (num == 2) {it.remove(); // 安全删除当前元素}
}

理解迭代器的 “指针移动” 模型后,就能掌握集合遍历的底层逻辑,遇到遍历相关的问题(如并发修改异常)也能快速定位啦~

http://www.xdnf.cn/news/18601.html

相关文章:

  • 【JavaEE】了解synchronized
  • Java 基础学习总结(211)—— Apache Commons ValidationUtils:让参数校验从 “体力活“ 变 “优雅事“
  • 电动车运行原理与最新人工智能驾驶技术在电动车上的应用展望:从基础动力系统到L5级完全自动驾驶的技术深度解析
  • 大语言模型的自动驾驶 LMDrive/DriveVLM-Dual
  • Kubernetes部署Prometheus+Grafana 监控系统NFS存储方案
  • Spark04-MLib library01-机器学习的介绍
  • Spring创建的方式
  • 在 Ubuntu 24.04 或 22.04 LTS 服务器上安装、配置和使用 Fail2ban
  • 【LLM】DeepSeek-V3.1-Think模型相关细节
  • Android - 用Scrcpy 将手机投屏到Windows电脑上
  • MySQL学习记录-基础知识及SQL语句
  • SSRF的学习笔记
  • React useState 全面深入解析
  • 6.2 el-menu
  • Axure RP 9的安装
  • 如何让FastAPI在百万级任务处理中依然游刃有余?
  • Postman参数类型、功能、用途及 后端接口接收详解【接口调试工具】
  • 【C++】函数返回方式详解:传值、传引用与传地址
  • Linux 824 shell:expect
  • 今日科技热点 | AI加速创新,5G与量子计算引领未来
  • PHP - 实例属性访问与静态方法调用的性能差异解析
  • B站视频字幕提取工具
  • mysql 5.7 查询运行时间较长的sql
  • 【计算机408数据结构】第三章:基本数据结构之栈
  • 苍穹外卖项目实战(日记十)-记录实战教程及问题的解决方法-(day3-2)新增菜品功能完整版
  • 启动Flink SQL Client并连接到YARN集群会话
  • 拓展:simulink中将仿真环境离散化
  • K8S的部署与常用管理
  • VS2022的MFC中关联使用控制台并用printf输出调试信息
  • Redis 高可用篇