Skip to content

0498 0552 第十四章 集合

14.1 集合

14.1.1 数组

  1. 长度开始时必须指定,而且一旦指定,不能更改
  2. 保存的心须为同一类型的元素
  3. 使用数组进行增加/删除元素的示意代码-比较麻烦

写出 Person 数组扩容示意代码。 Person[] pers = newPerson[1]; //大小是 1 per[0] = new Person();

增加新的 Person 对象? Person[]pers 2=newPerson[pers」ength+1];//新创建数组 for (){}//拷贝 pers 数组的元素到 pers 2 pers2[pers2.length-1] = newPerson();//添加新的对象


14.1.2 集合

  1. 可以动态保存任意多个对象(任意类型),使用比较方便!
  2. 提供了一系列方便的操作对象的方法:add. remove. set. get
  3. 使用集合添加,删除新元素的示意代码-简洁

14.2 集合的框架体系



classDiagram
    class Object {
        +toString() String
        +hashCode() int
        +equals(Object) boolean
    }

    class String {
        +length() int
        +charAt(int) char
        +substring(int, int) String
    }

    class Collection~E~ {
        <<interface>>
        +add(E) boolean
        +remove(Object) boolean
        +size() int
    }

    class List~E~ {
        <<interface>>
        +get(int) E
        +set(int, E) E
        +add(int, E) void
    }

    class Set~E~ {
        <<interface>>
    }

    class Map~K,V~ {
        <<interface>>
        +put(K, V) V
        +get(Object) V
        +remove(Object) V
    }

    class ArrayList~E~ {
        +add(E) boolean
        +get(int) E
        +remove(int) E
    }

    class LinkedList~E~ {
        +addFirst(E) void
        +addLast(E) void
        +getFirst() E
    }

    class HashSet~E~ {
        +add(E) boolean
        +remove(Object) boolean
    }

    class TreeSet~E~ {
        +add(E) boolean
        +first() E
        +last() E
    }

    class HashMap~K,V~ {
        +put(K, V) V
        +get(Object) V
    }

    class TreeMap~K,V~ {
        +put(K, V) V
        +firstKey() K
        +lastKey() K
    }

    Object <|.. String : extends
    Collection <|.. List : extends
    Collection <|.. Set : extends
    List <|.. ArrayList : implements
    List <|.. LinkedList : implements
    Set <|.. HashSet : implements
    Set <|.. TreeSet : implements
    Map <|.. HashMap : implements
    Map <|.. TreeMap : implements

![[14-1-1 单列集合.png]]

![[14-1-2 双列集合.png]]


14.3.2 Collection 接口遍历元素方式 1-使用 Iterator

  1. Iterator 称为迭代器,主要用于遍历 Collection 集合中的元素。
  2. 所有实现了 Collection 接口的集合类都有一个 iterator 方法,用以返回一个实现了 Iterator 接口的对象, 即可以返回一个迭代器。
  3. Iterator 仅用于遍历集合,Iterator 本身并不存放对
Iterator iterator =coll.iteratorO;//得到一个集合的迭代器

//hasNext():判断是否还有下一个元素while(iterator.hasNext()){

//next()作用:1.上的元素返回|下移.将下移以后集合位置|

System.out.print!n(iterator.next());

在调用 iterator.next() 方法之前必须要调用 iterator.hasNext() 进行检测。若不调用,且下一条记录无效,直接调用 it. next () 会抛出 NoSuchElementException 异常。


14.3.3 Collection 接口遍历对象方式 2-for 循环增强

增强 for 循环,可以代替 iteratoi 代器,特点:增强 for 就是简化版的 iterator, 本质一样。只能用于遍历集合或数组

  • 基本语法 for(Object object:col){ System.out.println(object) }

14.4 List 接口和常用方法

14.4.1 List 接口基本介绍

List 接口是 Collection 接口的子接口

  1. List 集合类中元素有序(即添加顺序和取出顺序一致)、且可重复
  2. List 集合中的每个元素都有具对应的顺席索引,即支持索引。
  3. List 容器中的元素都对应一个整数型的序号记载具在容器中的位置,可以根据序号存取容器中的元素。
  4. 常用的有:ArrayList、LinkedList 和 Vector。

14.4.2 List 接口的常用方法

常用方法

  1. 添加元素
  2. boolean add(E e)
    将指定元素追加到列表末尾。
    • 参数: e - 要添加的元素。
    • 返回值: true(通常成功,除非底层实现有限制)。
    • 示例:
      List<String> list = new ArrayList<>();
      list.add("Java"); // 列表变为 ["Java"]
      
  3. void add(int index, E element)
    在指定索引位置插入元素,原有元素后移。
    • 参数: index - 插入位置,element - 要插入的元素。
    • 异常: IndexOutOfBoundsException - 如果 index 超出范围。
    • 示例:
      list.add(0, "Python"); // 列表变为 ["Python", "Java"]
      
  4. boolean addAll(Collection<? extends E> c)
    将指定集合中的所有元素追加到列表末尾。

    • 参数: c - 要添加的集合。
    • 返回值: 如果列表发生变化,返回 true
    • 示例:
      List<String> other = Arrays.asList("C++", "Ruby");
      list.addAll(other); // 列表变为 ["Python", "Java", "C++", "Ruby"]
      
  5. 获取和修改元素

  6. E get(int index)
    返回指定索引处的元素。
    • 参数: index - 要获取的元素索引。
    • 返回值: 索引处的元素。
    • 异常: IndexOutOfBoundsException - 如果 index 超出范围。
    • 示例:
      String element = list.get(1); // 返回 "Java"
      
  7. E set(int index, E element)
    替换指定索引处的元素,并返回被替换的旧元素。

    • 参数: index - 要替换的索引,element - 新元素。
    • 返回值: 原索引处的元素。
    • 异常: IndexOutOfBoundsException - 如果 index 超出范围。
    • 示例:
      String old = list.set(1, "Kotlin"); // 列表变为 ["Python", "Kotlin", "C++", "Ruby"],返回 "Java"
      
  8. 删除元素

  9. E remove(int index)
    删除指定索引处的元素,并返回被删除的元素。
    • 参数: index - 要删除的元素索引。
    • 返回值: 被删除的元素。
    • 异常: IndexOutOfBoundsException - 如果 index 超出范围。
    • 示例:
      String removed = list.remove(2); // 列表变为 ["Python", "Kotlin", "Ruby"],返回 "C++"
      
  10. boolean remove(Object o)
    删除首次出现的指定元素(如果存在)。

    • 参数: o - 要删除的元素。
    • 返回值: 如果元素被移除,返回 true
    • 示例:
      list.remove("Ruby"); // 列表变为 ["Python", "Kotlin"]
      
  11. 查询和搜索

  12. int size()
    返回列表中的元素数量。
    • 返回值: 列表大小。
    • 示例:
      int size = list.size(); // 返回 2
      
  13. boolean isEmpty()
    检查列表是否为空。
    • 返回值: 如果列表为空,返回 true
    • 示例:
      boolean empty = list.isEmpty(); // 返回 false
      
  14. boolean contains(Object o)
    检查列表是否包含指定元素。
    • 参数: o - 要检查的元素。
    • 返回值: 如果包含,返回 true
    • 示例:
      boolean hasKotlin = list.contains("Kotlin"); // 返回 true
      
  15. int indexOf(Object o)
    返回首次出现的指定元素的索引,若不存在返回 -1。
    • 参数: o - 要查找的元素。
    • 返回值: 元素索引或 -1。
    • 示例:
      int index = list.indexOf("Kotlin"); // 返回 1
      
  16. int lastIndexOf(Object o)
    返回最后一次出现的指定元素的索引,若不存在返回 -1。

    • 参数: o - 要查找的元素。
    • 返回值: 元素索引或 -1。
    • 示例:
      list.add("Kotlin");
      int lastIndex = list.lastIndexOf("Kotlin"); // 返回 2
      
  17. 子列表和批量操作

  18. List<E> subList(int fromIndex, int toIndex)
    返回指定范围的子列表(视图,非副本)。
    • 参数: fromIndex - 起始索引(包含),toIndex - 结束索引(不包含)。
    • 返回值: 子列表。
    • 异常: IndexOutOfBoundsException - 如果索引无效。
    • 示例:
      List<String> sub = list.subList(0, 2); // 返回 ["Python", "Kotlin"]
      
  19. void clear()
    清空列表中的所有元素。

    • 示例:
      list.clear(); // 列表变为 []
      
  20. 迭代和转换

  21. Iterator<E> iterator()
    返回一个迭代器,用于遍历列表元素。
    • 示例:
      Iterator<String> it = list.iterator();
      while (it.hasNext()) {
          System.out.println(it.next());
      }
      
  22. ListIterator<E> listIterator()
    返回一个列表迭代器,支持双向遍历和修改。
    • 示例:
      ListIterator<String> lit = list.listIterator();
      while (lit.hasNext()) {
          lit.set(lit.next().toUpperCase()); // 将元素转换为大写
      }
      
  23. Object[] toArray()
    将列表转换为对象数组。
    • 示例:
      Object[] array = list.toArray(); // 返回 Object 数组
      
  24. <T> T[] toArray(T[] a)
    将列表转换为指定类型的数组。
    • 参数: a - 目标数组类型。
    • 示例:
      String[] strArray = list.toArray(new String[0]);
      

特点和注意事项

  • 有序性: List 维护元素的插入顺序,索引从 0 开始。
  • 允许重复: 同一元素可多次出现在列表中。
  • 线程安全: ArrayListLinkedList 默认非线程安全,需使用 Collections.synchronizedListCopyOnWriteArrayList 确保线程安全。
  • 性能差异:
  • ArrayList: 适合随机访问(get / set),插入/删除较慢(尤其在列表开头)。
  • LinkedList: 适合频繁插入/删除(尤其在列表两端),随机访问较慢。
  • 泛型: List<E> 支持泛型,确保类型安全。

示例代码

以下是一个综合示例,展示常用方法的使用:

import java.util.*;

public class ListDemo {
    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        // 添加
        list.add("Java");
        list.add(0, "Python");
        list.addAll(Arrays.asList("C++", "Kotlin"));
        System.out.println("列表: " + list); // [Python, Java, C++, Kotlin]

        // 获取和修改
        System.out.println("索引 1: " + list.get(1)); // Java
        list.set(1, "Ruby");
        System.out.println("修改后: " + list); // [Python, Ruby, C++, Kotlin]

        // 删除
        list.remove(2);
        System.out.println("删除后: " + list); // [Python, Ruby, Kotlin]

        // 查询
        System.out.println("大小: " + list.size()); // 3
        System.out.println("包含 Ruby: " + list.contains("Ruby")); // true
        System.out.println("Ruby 索引: " + list.indexOf("Ruby")); // 1

        // 子列表
        List<String> sub = list.subList(0, 2);
        System.out.println("子列表: " + sub); // [Python, Ruby]
    }
}


如需更深入的讲解(例如特定实现类的性能分析)或更多示例,请进一步说明!

14.4.4 List 的三种遍历方式 ArrayList, LinkedList, Vector

  1. 方式一 :使用 iterator

     Iterator iter = col.iterator();
    
    while(iter.hasNext()){ 
        Object o = iter.nextO;
    }
    

  2. 方式二:使用增强 for for(Object o:col){ }

3) 方式三:使用普通 for

forint i =O; i < list.size();i++){ 
    Object object = list.get(i); 
    System.out.println(object);
}
说明:使用 LinkedList 完成使用方式和 ArrayList —样


14.5 ArrayList 底层结构和源码分析

14.5.1 ArrayList 的注意事项

  1. permits all elements, including null , ArrayList 可以加入 null, 并且多个
  2. ArrayList 是由数组来实现数据存储
  3. ArrayList 基本等同于 Vector ,除了ArrayList 是线程不安全(执行效率高)看源码. 在多线程情况下,不建议使用 ArrayList

14.5.2 ArrayList 的底层操作机制源码分析

  1. ArrayList 中维护了一个 Object 类型的数组 elementData.

    • transient Object[] elementData; //transient 表示瞬间,短暂的,装示该属性不会被序列化
  2. 当创建 ArrayList 对象时,如果使用的是无参构造器,则初始 elementData 容量为 0, 第 1 次添加,则扩容 elementData 为 10, 如需要再次扩容,则扩容 elementData 为 1.5 倍。

  3. 如果使用的是指定大小的构造器,则初始 elementData 容量为指定大小,如果需要扩容,则直接扩容 elementData 为 1.5 倍。

ArrayList 的无参构造器是其最常用的构造方式,源码简单但涉及后续操作的初始化逻辑。以下是详细分析:

1. 无参构造器源码

public ArrayList() {
    this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
- 功能:初始化一个空的 ArrayList,将底层数组 elementData 设置为 DEFAULTCAPACITY_EMPTY_ELEMENTDATA。 - 关键字段: - DEFAULTCAPACITY_EMPTY_ELEMENTDATA:定义为 private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {},是一个共享的空数组常量,用于标识无参构造的初始状态。 - elementDatatransient Object[] 类型,ArrayList 的底层存储数组,初始为空。

2. 初始化逻辑

  • 延迟分配:无参构造器不立即分配容量为 10 的数组,而是将 elementData 指向空数组 DEFAULTCAPACITY_EMPTY_ELEMENTDATA。真正的数组分配在第一次添加元素时触发。
  • 目的:这种延迟分配(lazy initialization)减少了内存浪费。如果 ArrayList 仅声明但不使用,不会占用多余空间。
  • 对比其他构造器
  • 指定容量构造(ArrayList(int initialCapacity))直接分配指定大小的数组。
  • 构造(ArrayList(Collection c))根据输入初始化数组。

3. 第一次添加元素时的行为

无参构造器的真正初始化逻辑在调用 add(E e) 时体现。以下是相关源码:

public boolean add(E e) {
    ensureCapacityInternal(size + 1); // 检查容量,size 初始为 0
    elementData[size++] = e;         // 存入元素,size 增 1
    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); // 默认容量 10
    }
    return minCapacity;
}

private void ensureExplicitCapacity(int minCapacity) {
    modCount++; // 记录结构修改次数
    if (minCapacity - elementData.length > 0) // 需要扩容
        grow(minCapacity);
}

private void grow(int minCapacity) {
    int oldCapacity = elementData.length; // 初始为 0
    int newCapacity = oldCapacity + (oldCapacity >> 1); // 1.5 倍扩容
    if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity; // 至少为 minCapacity
    if (newCapacity - MAX_ARRAY_SIZE > 0)
        newCapacity = hugeCapacity(minCapacity);
    elementData = Arrays.copyOf(elementData, newCapacity); // 复制到新数组
}
- 初始化容量:当 elementDataDEFAULTCAPACITY_EMPTY_ELEMENTDATA 时,calculateCapacity 返回 Math.max(DEFAULT_CAPACITY, minCapacity),其中 DEFAULT_CAPACITY = 10。因此,第一次添加元素时,数组容量扩容至 10。 - 扩容流程: 1. add(E e) 调用 ensureCapacityInternal(size + 1)size 初始为 0,minCapacity = 1。 2. calculateCapacity 检测到 elementDataDEFAULTCAPACITY_EMPTY_ELEMENTDATA,返回 10。 3. grow(10) 创建容量为 10 的新数组,elementData 指向新数组。 4. 元素存储在 elementData[0]size 增至 1。

4. 关键点分析

  • 内存效率:无参构造器通过延迟分配避免初始内存浪费,适合不确定元素数量的场景。
  • 性能影响:第一次添加元素触发扩容,涉及数组分配和复制,略有性能开销。后续添加若不超容量则高效。
  • 线程安全:无参构造器初始化不涉及线程竞争,但 ArrayList 本身非线程安全,需外部同步。
  • 序列化elementDatatransient,序列化时仅保存有效元素(size 个),配合 writeObjectreadObject 优化存储。

5. 使用场景与注意事项

  • 适用场景:无参构造器适合元素数量未知或较小的场景,如临时列表或动态增长的集合。
  • 注意事项
  • 若预知列表会存储大量元素,建议使用 ArrayList(int initialCapacity) 避免频繁扩容。
  • 扩容机制(1.5 倍)可能导致内存碎片,需权衡初始容量。
  • 空数组检查(如 isEmpty())基于 size == 0,与 elementData 是否为空无关。

6. 源码总结

无参构造器通过将 elementData 设置为 DEFAULTCAPACITY_EMPTY_ELEMENTDATA,实现延迟初始化。第一次添加元素时,触发容量为 10 的数组分配,后续扩容按 1.5 倍增长。这种设计平衡了内存效率和灵活性,但需注意扩容开销和线程安全问题。


14.6 Vector 底层结构和源码剖析

14.6.1 Vector 的基本介绍

  1. Vector 类的定义说明

    public class Vector<E> 
    extends AbstractList<E>
    implements List<E>, RandomAccess, Cloneable, Serializable
    

  2. Vector 底层也是一个对象数组,protected Object[] elementData;

  3. Vector 是线程同步的,即线程安全, Vector 类的操作方法带有 synchronized

public synchronized E get(int index) {
    if (index >= elementcount)
    throw new ArraylndexOutOfBoundsException(index);
    return elementData(index); 
}
  1. 在开发中,需要线程同步安全时,考虑使用 Vector
@SuppressWarnings({"all"})
public class Vector_ {
    public static void main(String[] args) {
//无参构造器
//有参数的构造
        Vector vector = new Vector(8);
        for (int i = 0; i < 10; i++) {
            vector.add(i)
        }
        vector.add(100);
        System.out.println("vector=" + vector);
    }
}

14.6.2 Vector 和 ArrayList 的比较

底层结构 版本 线程安全(同步)效率 扩容倍数
ArrayList 可变数组 jdk 1.2 不安全,效率高 如果有参构造 1.5 倍
如果是无参
1. 第一次 10
2. 让第二次开始扩 1.5 倍
Vector 可变数组
Object[]
jdk 1.0 安全,效率高 如果是无参,默认 10
满后,就按 2 倍扩容

如果指定大小,
则每次直接扩容两倍
特性 Vector (无参构造器) ArrayList (无参构造器)
elementData 初始化 分配 new Object[10] 设置为 DEFAULTCAPACITY_EMPTY_ELEMENTDATA(空数组)
初始容量 10 0(延迟到第一次添加时分配 10)
elementCount/size elementCount = 0 size = 0
扩容策略 默认翻倍(capacityIncrement = 0 1.5 倍增长
内存效率 立即分配 10,稍浪费内存 延迟分配,内存效率更高
线程安全 是(synchronized 方法)

14.6.3 底层逻辑

Vector 无参构造器创建时的逻辑

Vector 是一个与 ArrayList 类似的动态数组实现,但它是线程安全的(通过 synchronized 实现),并且在设计上有一些历史遗留差异。以下是 Vector 无参构造器(new Vector<>())创建时的底层逻辑和变量状态。

1. 无参构造器源码

public Vector() {
    this(10);
}
- 逻辑:无参构造器直接调用带参数的构造器 Vector(int initialCapacity),传入默认初始容量 10。 - 有参构造器源码
public Vector(int initialCapacity) {
    this(initialCapacity, 0);
}
- 进一步调用
public Vector(int initialCapacity, int capacityIncrement) {
    super();
    if (initialCapacity < 0)
        throw new IllegalArgumentException("Illegal Capacity: "+ initialCapacity);
    this.elementData = new Object[initialCapacity];
    this.capacityIncrement = capacityIncrement;
}
- 逻辑总结: - 无参构造器最终调用 Vector(int initialCapacity, int capacityIncrement),设置初始容量为 10,容量增量(capacityIncrement)为 0。 - 直接分配一个长度为 10 的 Object[] 数组给 elementData,与 ArrayList 的延迟初始化不同。 - 没有元素,因此 elementCount(类似 ArrayListsize)初始化为 0。

2. 相关变量及值大小

Vector 创建时(调用无参构造器后),以下是关键变量的状态:

  • elementData
  • 类型:protected Object[]
  • 值:new Object[10]
  • 大小:10(长度为 10 的数组,元素全为 null)。
  • 说明:elementDataVector 存储元素的实际数组。创建时立即分配容量为 10 的数组,与 ArrayList 的空数组(DEFAULTCAPACITY_EMPTY_ELEMENTDATA)不同。

  • elementCount

  • 类型:protected int
  • 值:0
  • 说明:elementCount 表示 Vector 中当前存储的元素个数。刚创建时没有元素,因此 elementCount = 0(类似 ArrayListsize)。

  • capacityIncrement

  • 类型:protected int
  • 值:0
  • 说明:capacityIncrement 表示扩容时的容量增量。无参构造器设置为 0,意味着扩容时默认将容量翻倍(通常是当前容量的两倍)。

  • modCount

  • 类型:protected int(继承自 AbstractList
  • 值:0
  • 说明:modCount 用于记录 Vector 的结构修改次数(如添加、删除元素)。创建时没有修改,因此值为 0。

  • 最大容量

  • Vector 没有显式的 MAX_ARRAY_SIZE 常量,但受 JVM 数组长度限制(Integer.MAX_VALUE,约 2^31 - 1)。
  • 说明:创建时未直接使用,但在扩容时会检查数组长度是否超出 JVM 限制。

3. 创建时的逻辑总结

  • 初始化行为
  • 无参构造器通过调用 Vector(10, 0),直接分配一个长度为 10 的 Object[] 数组给 elementData
  • elementCount 初始化为 0,表示没有元素。
  • capacityIncrement 设置为 0,表示后续扩容按两倍增长(除非另行指定)。
  • modCount 保持默认值 0。
  • 内存分配
  • ArrayList 的延迟初始化不同,Vector 在创建时立即分配容量为 10 的数组,内存开销稍大。
  • 这种设计源于 Vector 的历史(Java 1.0 遗留),当时更注重简单性和线程安全。
  • 线程安全
  • Vector 的构造器操作是线程安全的(单线程初始化无竞争),且后续操作通过 synchronized 保证线程安全。
  • 后续影响
  • 由于创建时已有容量 10 的数组,第一次添加元素(add(E e))通常不会触发扩容,除非添加超过 10 个元素。
  • 扩容时,若 capacityIncrement > 0,新容量为 当前容量 + capacityIncrement;否则,新容量为 当前容量 * 2

4. 关键点

  • 为什么立即分配容量 10?
  • Vector 是 Java 1.0 的遗留类,设计时未采用延迟初始化,优先简单性和可预测性。
  • 立即分配容量 10 确保创建后即可存储少量元素,适合早期 Java 应用的简单场景。
  • 与有参构造器的对比
  • 有参构造器 Vector(int initialCapacity)Vector(int initialCapacity, int capacityIncrement) 允许自定义初始容量和扩容增量。
  • 无参构造器是默认配置(容量 10,增量 0)。
  • 性能
  • 创建时分配数组(new Object[10])有轻微内存和时间开销,但通常可忽略。
  • 线程安全导致后续操作(如 add)性能低于 ArrayList,因每次操作需获取锁。
  • 建议
  • 如果不需要线程安全,优先使用 ArrayList,性能更高且内存效率更好。
  • 如果预知存储大量元素,建议使用 Vector(int initialCapacity) 指定初始容量,避免扩容。

6. 值大小总结

在调用 new Vector<>() 后的瞬间: - elementData = new Object[10](长度 10 的数组,元素为 null) - elementCount = 0(元素个数) - capacityIncrement = 0(扩容增量) - modCount = 0(结构修改次数) - 最大容量受 JVM 限制(Integer.MAX_VALUE

7. 注意事项

  • 序列化elementDataprotected,非 transient,创建时的数组会序列化,增加序列化开销(与 ArrayList 不同)。
  • 历史遗留Vector 因线程安全和早期设计,性能和灵活性不如 ArrayList,现代开发中建议优先使用 Collections.synchronizedList(new ArrayList<>()) 替代线程安全的 Vector
  • 扩容:后续扩容逻辑(ensureCapacityHelper)与 ArrayList 不同,Vector 更简单(翻倍或增量),但因同步开销效率较低。

14.7 LinkedList 底层结构

14.7.1 LinkedList 的全面说明

  1. LinkedList 底层实现了双向链表双端队列特点
  2. 可以添加任意元素(元素可以重复), 包括 null
  3. 线程不安全,没有实现同步

14.7.2 LinkedList 的底层操作机制

  1. LinkedList 底层维护了一个双向链表.
  2. LinkedList 中维护了两个属性 firstlast 分别指向首节点和尾节点
  3. 每个节点(Node 对象),里面又维护了 prev.next、item 三个属性,其中通过 prev 指向前一个,通过 next 指向后一个节点。最终实现双向链表.
  4. 所以 LinkedList 的元素的添加和删除,不是通过数组完成的,相对来说效率较高
classDiagram
    class LinkedList {
        -int size
        -Node~E~ first
        -Node~E~ last
        -int modCount
    }
    class Node {
        -E item
        -Node~E~ next
        -Node~E~ prev
    }
    LinkedList o--> "1" Node : first
    LinkedList o--> "1" Node : last
    Node o--> "1" Node : next
    Node o--> "1" Node : prev

    note for LinkedList "创建时:\nsize = 0\nfirst = null\nlast = null\nmodCount = 0"
    note for Node "每个节点包含:\n- item: 元素值\n- next: 下一个节点\n- prev: 前一个节点"


graph TD
    subgraph Empty LinkedList
        L[LinkedList] -->|first| NULL1[null]
        L -->|last| NULL2[null]
        L -->|size| S[0]
        L -->|modCount| M[0]
    end

    subgraph Non-Empty LinkedList
        L2[LinkedList] -->|first| N1[Node1]
        L2 -->|last| N3[Node3]
        L2 -->|size| S2[3]
        L2 -->|modCount| M2[0 or more]
        N1 -->|item| I1[Element1]
        N1 -->|next| N2[Node2]
        N1 -->|prev| NULL3[null]
        N2 -->|item| I2[Element2]
        N2 -->|next| N3
        N2 -->|prev| N1
        N3 -->|item| I3[Element3]
        N3 -->|next| NULL4[null]
        N3 -->|prev| N2
    end

14.7.3 LinkedList 的增删改查案例

@SuppressWarnings({"all"})
public class LinkedListCRUD {
    public static void main(String[] args) {
        LinkedList linkedList = new LinkedList();
        linkedList.add(1);
        linkedList.add(2);
        linkedList.add(3);
        System.out.println("linkedList=" + linkedList);
//演示一个删除结点的
        linkedList.remove(); // 这里默认删除的是第一个结点
//linkedList.remove(2);
        System.out.println("linkedList=" + linkedList);
//修改某个结点对象
        linkedList.set(1, 999);
        System.out.println("linkedList=" + linkedList);
//得到某个结点对象
//get(1) 是得到双向链表的第二个对象
        Object o = linkedList.get(1);
        System.out.println(o);//999
//因为 LinkedList 是 实现了 List 接口, 遍历方式
        System.out.println("===LinkeList 遍历迭代器====");
        Iterator iterator = linkedList.iterator();
        while (iterator.hasNext()) {
            Object next = iterator.next();
            System.out.println("next=" + next);
        }
        System.out.println("===LinkeList 遍历增强 for====");
        for (Object o1 : linkedList) {
            System.out.println("o1=" + o1);
        }
        System.out.println("===LinkeList 遍历普通 for====");
        for (int i = 0; i < linkedList.size(); i++) {
            System.out.println(linkedList.get(i));
        }
        /* 1. LinkedList linkedList = new LinkedList();
        public LinkedList() {}
        2. 这时 linkeList 的属性 first = null last = null
        3. 执行 添加
        public boolean add(E e) {
        linkLast(e);
        return true;
        }
        4.将新的结点,加入到双向链表的最后
        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;
        size++;
        modCount++;
        }
        */
        /*
        4. 执行 removeFirst
        public E remove() {
        return removeFirst();
        }
        5. 执行
        public E removeFirst() {
        final Node<E> f = first;
        if (f == null)
        throw new NoSuchElementException();
        return unlinkFirst(f);
        }
        6. 执行 unlinkFirst, 将 f 指向的双向链表的第一个结点拿掉
        private E unlinkFirst(Node<E> f) {
        // assert f == first && f != null;
        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--;
        modCount++;
        return element;
        }
        */
    }
}

14.8 ArrayList 和 LinkedList 比较

14.8.1 ArrayList 和 LinkedList 的比较

底层结构 增删的效率 改查的效率
ArrayList 可变数组 较低、数组扩容 较高
LinkedList 双向链表 较高、通过链表追加 较低

如何选择 ArrayList 和 LinkedList:

  1. 如果我们改查的操作多,选择 ArrayList
  2. 如果我们增删的操作多,选择 LinkedList
  3. 一般来说,在程序中,80%-90% 都是查询,因此大部分情况下会选择 ArrayList
  4. 在一个项目中,根据业务灵活选择,也可能这样,一个模块使用的是 ArrayList, 另外一介模块是 LinkedList, 也就是说,要根据业务来进行选择

14.9 Set 接口和常用方法

14.9.1 Set 接口基本介绍

  1. 无序(添加和取出的顺序不一致),没有索引 => 不能普通 for
  2. 不允许重复元素,所以最多包含一个 null

14.9.2 Set 接口的常用方法

和 List 接口一样, Set 接口也是 Collection 的子接口,因此,常用方法和 Collection 接口一样.


14.9.3 Set 接口的遍历方式

同 Collection 的遍历方式一样,因为 Set 接口是 Collection 接口的子接口。

  1. 可以使用迭代器
  2. 增强 for
  3. 不能使用索引的方式来获取 (有 getByIndex 的就可以使用,继承 List)

14.9.4 Set 接口的常用方法举例

public class SetMethod {
    public static void main(String[] args) {
//老韩解读
//1. 以 Set 接口的实现类 HashSet 来讲解 Set 接口的方法
//2. set 接口的实现类的对象(Set 接口对象), 不能存放重复的元素, 可以添加一个 null
//3. set 接口对象存放数据是无序(即添加的顺序和取出的顺序不一致)
//4. 注意:取出的顺序的顺序虽然不是添加的顺序,但是他的固定.
        Set set = new HashSet();
        set.add("john");
        set.add("lucy");
        set.add("john");//重复
        set.add("jack");
        set.add("hsp");
        set.add("mary");
        set.add(null);//
        set.add(null);//再次添加 null
        for (int i = 0; i < 10; i ++) {
            System.out.println("set=" + set);
        }
//遍历
//方式 1: 使用迭代器
        System.out.println("=====使用迭代器====");
        Iterator iterator = set.iterator();
        while (iterator.hasNext()) {
            Object obj = iterator.next();
            System.out.println("obj=" + obj);
        }
        set.remove(null);
//方式 2: 增强 for
        System.out.println("=====增强 for====");
        for (Object o : set) {
            System.out.println("o=" + o);
        }
//set 接口对象,不能通过索引来获取
    }
}

14.10 Set 接口实现类-HashSet

14.10.1 HashSet 的全面说明

  1. HashSet 实现了 Set 接口

  2. HashSet 实际上是 HashMap ,看源码.

    public HashSet(){
        map HashMap<>();
    }
    

  3. 可以存放 null 值,但是只能有一个 null

  4. HashSet 不保证元素是有序的,取决于 hash 后,再确定索引的结果.(即,不保证存放元素的顺序和取出顺序一致取出的顺序的顺序虽然不是添加的顺序,但是固定

5. 不能有重复元素 / 对象. 在前面 Set 接口使用已经讲

14.10.2 HashSet 案例说明

public class HashSet01 {
    public static void main(String[] args) {
        HashSet set = new HashSet();

//说明
//1. 在执行 add 方法后,会返回一个 boolean 值
//2. 如果添加成功,返回 true, 否则返回 false
//3. 可以通过 remove 指定删除哪个对象
        System.out.println(set.add("john"));//T
        System.out.println(set.add("lucy"));//T
        System.out.println(set.add("john"));//F
        System.out.println(set.add("jack"));//T
        System.out.println(set.add("Rose"));//T
        set.remove("john");
        System.out.println("set=" + set);//3 个

        set = new HashSet();
        System.out.println("set=" + set);//0
//4 Hashset 不能添加相同的元素/数据?
        set.add("lucy");//添加成功
        set.add("lucy");//加入不了
        set.add(new Dog("tom"));//OK
        set.add(new Dog("tom"));//Ok
        System.out.println("set=" + set);
//在加深一下. 非常经典的面试题.
//看源码,做分析, 先给小伙伴留一个坑,以后讲完源码,你就了然
//去看他的源码,即 add 到底发生了什么?=> 底层机制.
        set.add(new String("hsp"));//ok
        set.add(new String("hsp"));//加入不了.
        System.out.println("set=" + set);
    }
}
class Dog { //定义了 Dog 类
    private String name;
    public Dog(String name) {
        this.name = name;
    }
    @Override
    public String toString() {
        return "Dog{" +
               "name='" + name + '\'' +
               '}';
    }
}

14.10.3 HashSet 底层机制说明

分析 HashSet 底层是 HashMap, HashMap 底层是(数组+链表+红黑树) 分析 HashSet 的添加元素底层是如何实现 (hash();eqiials())

  1. HashSet 底层是 HashMap

  2. 添加一个元素时,先得到 hash 值会转成索引值

  3. 找到存储数据表 table ,看这个索引位置是否已经存放的有元素如果没有,直接加入如果有,调用 equals 比较,如果相同,就放弃添加, 如果不相同,则添加到最后

  4. 在 Java 8 中, 如果一条链表的元素个数到达 TREEIFY THRESHOLD(默认是 8), 井且 table 的大小= MlN TREEIFY CAPACITY(默认 64), 就会独行树化(红黑树)
public class HashSetIncrement {
    public static void main(String[] args) {
        /*
        HashSet 底层是 HashMap, 第一次添加时, table 数组扩容到 16,
        临界值(threshold)是 16*加载因子(loadFactor)是 0.75 = 12
        如果 table 数组使用到了临界值 12,就会扩容到 16 * 2 = 32,
        新的临界值就是 32*0.75 = 24, 依次类推
        */
        HashSet hashSet = new HashSet();
// for(int i = 1; i <= 100; i++) {
// hashSet.add(i);//1,2,3,4,5...100
// }
        /*
        在 Java8 中, 如果一条链表的元素个数到达         TREEIFY_THRESHOLD(默认是 8 ),
        并且 table 的大小 >= MIN_TREEIFY_CAPACITY(默认 64),就会进行树化(红黑树),
        否则仍然采用数组扩容机制
        */
// for(int i = 1; i <= 12; i++) {
// hashSet.add(new A(i));//
// }
        /*
        当我们向 hashset 增加一个元素, -> Node -> 加入 table , 就算是增加了一个 size++
        */
        for (int i = 1; i <= 7; i++) { //在 table 的某一条链表上添加了 7 个 A 对象
            hashSet.add(new A(i));//
        }
        for (int i = 1; i <= 7; i++) { //在 table 的另外一条链表上添加了 7 个 B 对象
            hashSet.add(new B(i));//
        }
    }
}
class B {
    private int n;
    public B(int n) {
        this.n = n;
    }
    @Override
    public int hashCode() {
        return 200;
    }
}
class A {
    private int n;
    public A(int n) {
        this.n = n;
    }
    @Override
    public int hashCode() {
        return 100;
    }
}

14.11 Set 接口实现类-LinkedHashSet

14.11.1 LinkedHashSet 的全面说明

  1. LinkedHashSet 是 HashSet 的子类
  2. LinkedHashSet 底层是一个 LinkedHashMap, 底层维护了一个数组+双向链表
  3. LinkedHashSet 根据元素的 hashCode 值来决定元素的存储位置,同时使用链表维护元素的次序(图), 这使得元素看起来是以插入顺序保存的。
  4. LinkedHashSet 不允许添重复元素
@SuppressWarnings({"all"})
public class LinkedHashSetExercise {
    public static void main(String[] args) {
        LinkedHashSet linkedHashSet = new LinkedHashSet();
        linkedHashSet.add(new Car("奥拓", 1000));//OK
        linkedHashSet.add(new Car("奥迪", 300000));//OK
        linkedHashSet.add(new Car("法拉利", 10000000));//OK
        linkedHashSet.add(new Car("奥迪", 300000));//加入不了
        linkedHashSet.add(new Car("保时捷", 70000000));//OK
        linkedHashSet.add(new Car("奥迪", 300000));//加入不了
        System.out.println("linkedHashSet=" + linkedHashSet);
    }
}
/**
* Car 类(属性:name,price), 如果 name 和 price 一样,
* 则认为是相同元素,就不能添加。 5min
*/
class Car {
    private String name;
    private double price;
    public Car(String name, double price) {
        this.name = name;
        this.price = price;
    }
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public double getPrice() {
        return price;
    }
    public void setPrice(double price) {
        this.price = price;
    }
    @Override
    public String toString() {
        return "\nCar{" +
               "name='" + name + '\'' +
               ", price=" + price +
               '}';
    }
//重写 equals 方法 和 hashCode
//当 name 和 price 相同时, 就返回相同的 hashCode 值, equals 返回 t
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Car car = (Car) o;
        return Double.compare(car.price, price) == 0 &&
               Objects.equals(name, car.name);
    }
    @Override
    public int hashCode() {
        return Objects.hash(name, price);
    }
}

14.12 Map 接口和常用方法

14.12.1 Map 简介

  1. Map 与 Collection 并列存在。用于保存具有映射关系的数据: Key-Value

  2. Map 中的 key 和 value 可以是任何引用类型的数据,会封装到 HashMap$Node 对象中

  3. Map 中的 key 不允许重复,原因和 HashSet 一样,前面分析过源码.

  4. Map 中的 vaule 可以重复

  5. Map 的 key 可以为 null, value 也可以为 null ,注意 key 为 null, 只能有一个,value 为 null ,可以多个.

  6. 常用 String 类作为 Map 的 key

  7. key 和 value 之间存在单向一对一关系,即通过指定的 key 总能找到对应的 value

  8. Map 存放数据的 key-vaEe 示意图, 一对 k-v 是放在一个 HashMap$Node 中的,有因为 Node 实现了 Entry 接口,有些书上也说一对 k-v 就是一个 Entry


14.12.2 Map 接口常用方法

@SuppressWarnings({"all"})
public class MapMethod {
    public static void main(String[] args) {
//演示 map 接口常用方法
        Map map = new HashMap();
        map.put("邓超", new Book("", 100));//OK
        map.put("邓超", "孙俪");//替换-> 一会分析源码
        map.put("王宝强", "马蓉");//OK
        map.put("宋喆", "马蓉");//OK
        map.put("刘令博", null);//OK
        map.put(null, "刘亦菲");//OK
        map.put("鹿晗", "关晓彤");//OK
        map.put("hsp", "hsp 的老婆");
        System.out.println("map=" + map);
// remove:根据键删除映射关系
        map.remove(null);
        System.out.println("map=" + map);
// get:根据键获取值
        Object val = map.get("鹿晗");
        System.out.println("val=" + val);
// size:获取元素个数
        System.out.println("k-v=" + map.size());
// isEmpty:判断个数是否为 0
        System.out.println(map.isEmpty());//F
// clear:清除 k-v
//map.clear();
        System.out.println("map=" + map);
// containsKey:查找键是否存在
        System.out.println("结果=" + map.containsKey("hsp"));//T

    }
}
class Book {
    private String name;
    private int num;
    public Book(String name, int num) {
        this.name = name;
        this.num = num;
    }
}

14.12.3 Map 接口遍历方法

  1. containsKey:查找键是否存在
  2. keyset:获取所有的键
  3. entrySet:获取所有关系k-v
  4. values:获取所有的值
@SuppressWarnings({"all"})
public class MapFor {
    public static void main(String[] args) {
        Map map = new HashMap();
        map.put("邓超", "孙俪");
        map.put("王宝强", "马蓉");
        map.put("宋喆", "马蓉");
        map.put("刘令博", null);
        map.put(null, "刘亦菲");
        map.put("鹿晗", "关晓彤");
//第一组: 先取出 所有的 Key , 通过 Key 取出对应的 Value
        Set keyset = map.keySet();
//(1) 增强 for
        System.out.println("-----第一种方式-------");
        for (Object key : keyset) {
            System.out.println(key + "-" + map.get(key));
        }
//(2) 迭代器
        System.out.println("----第二种方式--------");
        Iterator iterator = keyset.iterator();
        while (iterator.hasNext()) {
            Object key = iterator.next();
            System.out.println(key + "-" + map.get(key));
        }
//第二组: 把所有的 values 取出
        Collection values = map.values();
//这里可以使用所有的 Collections 使用的遍历方法
//(1) 增强 for
        System.out.println("---取出所有的 value 增强 for----");
        for (Object value : values) {
            System.out.println(value);
        }
//(2) 迭代器
        System.out.println("---取出所有的 value 迭代器----");
        Iterator iterator2 = values.iterator();
        while (iterator2.hasNext()) {
            Object value = iterator2.next();
            System.out.println(value);
        }
//第三组: 通过 EntrySet 来获取 k-v
        Set entrySet = map.entrySet();// EntrySet<Map.Entry<K,V>>
//(1) 增强 for
        System.out.println("----使用 EntrySet 的 for 增强(第 3 种)----");
        for (Object entry : entrySet) {
//将 entry 转成 Map.Entry
            Map.Entry m = (Map.Entry) entry;
            System.out.println(m.getKey() + "-" + m.getValue());
        }
//(2) 迭代器
        System.out.println("----使用 EntrySet 的 迭代器(第 4 种)----");
        Iterator iterator3 = entrySet.iterator();
        while (iterator3.hasNext()) {
            Object entry = iterator3.next();
//System.out.println(next.getClass());//HashMap$Node -实现-> Map.Entry (getKey,getValue)
//向下转型 Map.Entry
            Map.Entry m = (Map.Entry) entry;
            System.out.println(m.getKey() + "-" + m.getValue());
        }
    }
}

14.13 Map 接口实现类-HashMap

14.13.1 HashMap 小结

  1. Map接口的常用实现类:HashMap、Hashtable和 Properties。

  2. HashMap是 Map 接口使用频率最高的实现类。

  3. HashMap 是以key-val 对的方式来存储数据(HashMaprNode类型)

  4. key 不能重复,但是值可以重复,允许使用null键和null值。

  5. 如果添加相同的key ,则会覆盖原来的key-val,等同于修改.(key不会替换,val会替换)

  6. 与 HashSet一样,不保证映射的顺序,因为底层是以hash表的方式来存储的.(jdk8的hashMap 底层数组+链表+红黑树)

  7. HashMap没有实现同步,因此是线程不安全的,方法没有做同步互斥的操作,没有synchronized


14.13.2 HashMap 底层机制及源码剖析

HashMap 中,EntrySettable 之间的关系是 逻辑上关联但物理上间接 的。

table 的定义

tableHashMap 的核心数据结构,定义为一个 Node<K,V>[] 数组,用于存储键值对。每个数组元素(称为“桶”)可能包含: - 单个 Node(实现 Map.Entry<K,V> 的对象,包含 hashkeyvaluenext 字段)。 - 链表(通过 Nodenext 字段连接多个节点,处理哈希冲突)。 - 红黑树(当链表过长时,Node 转换为 TreeNode)。

transient Node<K,V>[] table;
  • 作用tableHashMap 的底层存储结构,负责实际保存所有键值对。
  • 存储方式:通过键的哈希值计算索引 (table.length - 1) & hash,将键值对存储到对应桶中。

EntrySet 的定义

EntrySetHashMap 的一个内部类(HashMap$EntrySet),实现了 Set<Map.Entry<K,V>> 接口,表示 HashMap 中所有键值对的集合。

private final class EntrySet extends AbstractSet<Map.Entry<K,V>> {
    public final Iterator<Map.Entry<K,V>> iterator() {
        return new EntryIterator();
    }
    // 其他方法如 size(), contains(), remove() 等
}
  • 作用EntrySet 提供了一种逻辑视图,允许用户通过 map.entrySet() 访问和遍历 HashMap 中的所有键值对。
  • 实现EntrySet 本身不存储数据,而是通过迭代器动态访问 table 中的 NodeTreeNode

EntrySet 和 table 的关系

EntrySettable 的关系可以总结为以下几点:

  1. 逻辑视图与物理存储
  2. tableHashMap物理存储结构,直接存储所有 NodeTreeNode(键值对)。
  3. EntrySetHashMap逻辑视图,通过 map.entrySet() 返回一个 Set<Map.Entry<K,V>>,其内容是对 table 中所有键值对的引用。
  4. EntrySet 的每个元素(Map.Entry)实际上是 table 中的 NodeTreeNode

  5. 动态访问

  6. EntrySet 不独立存储键值对,而是通过迭代器(EntryIterator)遍历 table 数组及其链表或红黑树,动态生成 Map.Entry 对象。
  7. 例如,调用 entrySet().iterator() 时,EntryIterator 会:

    • 遍历 table 的每个桶(table[i])。
    • 如果桶包含链表或红黑树,依次访问每个 NodeTreeNode
    • 将每个节点作为 Map.Entry 返回给用户。
  8. 操作同步

  9. EntrySet 的操作(如通过迭代器删除元素)会直接反映到 table 上,因为 EntrySet 的迭代器直接操作 table 中的节点。
  10. 例如,调用 entrySet().remove(entry) 会从 table 的对应桶中删除相应的 NodeTreeNode

  11. 代码示例

    import java.util.HashMap;
    import java.util.Map;
    import java.util.Set;
    
    public class EntrySetTableDemo {
        public static void main(String[] args) {
            HashMap<String, Integer> map = new HashMap<>();
            map.put("苹果", 10);
            map.put("香蕉", 20);
    
            // 获取 EntrySet
            Set<Map.Entry<String, Integer>> entrySet = map.entrySet();
            System.out.println("EntrySet: " + entrySet);
    
            // 通过 EntrySet 修改值
            for (Map.Entry<String, Integer> entry : entrySet) {
                entry.setValue(entry.getValue() + 100);
            }
    
            System.out.println("修改后的 Map: " + map);
        }
    }
    
    输出:
    EntrySet: [苹果=10, 香蕉=20]
    修改后的 Map: {苹果=110, 香蕉=120}
    

  12. entrySet 返回的 Set 包含 table 中的 Node 对象(作为 Map.Entry)。

  13. 修改 entry.setValue() 直接更改 table 中对应 Nodevalue 字段。

EntrySet 的迭代过程

EntrySet 的迭代器(HashMap$EntryIterator)的工作原理如下: - 初始化:从 table[0] 开始,记录当前桶索引和当前节点。 - 遍历: - 如果当前桶不为空,访问桶中的 NodeTreeNode(通过 next 字段遍历链表,或按红黑树顺序遍历)。 - 移动到下一个非空桶,重复上述过程。 - 返回:将每个 NodeTreeNode 作为 Map.Entry 返回,供用户访问 getKey()getValue()

class EntryIterator extends HashIterator<Map.Entry<K,V>> {
    public final Map.Entry<K,V> next() { return nextNode(); }
}
  • nextNode() 方法遍历 table,确保 EntrySet 能访问所有键值对。

关键点总结

  • 物理与逻辑
  • tableHashMap 的物理存储,保存 NodeTreeNode
  • EntrySet 是逻辑视图,通过引用 table 中的节点提供键值对集合。
  • 依赖关系
  • EntrySet 依赖 table,其内容是对 table 中节点的动态引用。
  • EntrySet 本身不存储数据,所有操作(如遍历、删除、修改)最终作用于 table
  • 效率
  • EntrySet 提供高效遍历方式,直接访问 Node 的键和值,避免通过 key 重新计算哈希查找值的开销。
  • 一致性
  • EntrySet 的修改(如 setValueremove)直接更新 table,确保数据一致。

常见问题

  1. EntrySet 是否复制了 table 的数据?
  2. 没有,EntrySet 只是 table 的视图,迭代器直接访问 table 中的 NodeTreeNode

  3. 为什么使用 EntrySet 而不是直接遍历 table?

  4. table 是内部实现细节,用户无法直接访问。
  5. EntrySet 提供标准化的 Set<Map.Entry> 接口,符合 Java 集合框架,方便遍历和操作。

  6. EntrySet 和 table 的修改如何同步?

  7. EntrySet 的修改(如通过迭代器删除)会直接更新 table 的节点。
  8. 反之,HashMap 的修改(如 putremove)会更新 table,并通过 modCount 确保 EntrySet 迭代器感知结构变化(否则抛出 ConcurrentModificationException)。