Java 集合 笔记

21 阅读13分钟

为什么需要集合

Java需要集合是因为数组存在固定长度、只能存储单一类型元素、缺乏丰富操作方法的局限性。集合框架提供了动态扩容、类型安全(通过泛型)、丰富的算法(如排序查找)和多样化的数据结构(如List、Set、Map),能够更灵活高效地处理各种数据组织和操作需求,大大提升了开发效率和代码可维护性。

Collection 方法分类

01 修改操作

add(E) // 向集合中添加一个指定元素

image.png

  • remove(Object) // 从集合中移除一个指定元素的第一个出现
// 数字集合
Collection<Integer> numbers = new ArrayList<>();
numbers.add(1);
numbers.add(2);
numbers.add(3);
numbers.add(4);
numbers.add(5);
// 删除数字 3
System.out.println(numbers.remove(3));
// 删除数字 6
System.out.println(numbers.remove(6));
// 遍历集合
for (Integer number : numbers) {
    System.out.println(number);
}

image.png

size() // 返回集合中元素的数量

isEmpty() // 判断集合是否为空(无任何元素)

contains(Object) // 检查集合是否包含指定的元素

iterator() // 返回一个用于遍历集合中元素的迭代器

toArray() // 将集合转换为一个包含所有元素的 Object 数组

toArray(T[]) // 将集合转换为指定类型的数组,若数组大小足够则存入,否则创建新数组

03 批量操作

containsAll(Collection<?>) // 判断集合是否包含指定集合中的所有元素

addAll(Collection<? extends E>) // 将指定集合中的所有元素添加到当前集合中

public static void main(String[] args) {
    // 苹果集合
    List<Apple> apples = new ArrayList<>();
    apples.add(new Apple("红富士苹果"));
    apples.add(new Apple("青苹果"));
    apples.add(new Apple("冰糖心苹果"));
    // 香蕉集合
    List<Banana> bananas = new ArrayList<>();
    bananas.add(new Banana("高山香蕉"));
    bananas.add(new Banana("小米香蕉"));
    bananas.add(new Banana("海南香蕉"));
    // 水果集合
    List<Fruit> fruits = new ArrayList<>();
    // 添加批量数据
    fruits.addAll(apples);
    fruits.addAll(bananas);
}

removeAll(Collection<?>) // 移除当前集合中所有也包含在指定集合中的元素

public static void main(String[] args) {
    // 数字集合
    Collection<Integer> numbers = new ArrayList<>();
    numbers.add(1);
    numbers.add(2);
    numbers.add(3);
    numbers.add(4);
    numbers.add(5);
    // 要删除的数字集合
    Collection<Integer> subNumbers = new ArrayList<>();
    subNumbers.add(2);
    subNumbers.add(4);
    subNumbers.add(6);
    // 删除数字集合
    System.out.println(numbers.removeAll(subNumbers));
    // 遍历集合
    for (Integer number : numbers) {
        System.out.println(number);
    }
}

image.png

removeIf(Predicate<? super E>) // 移除满足给定条件的所有元素

public static void main(String[] args) {
    // 数字集合
    Collection<Integer> numbers = new ArrayList<>();
    numbers.add(1);
    numbers.add(2);
    numbers.add(3);
    numbers.add(4);
    numbers.add(5);
    // 按指定规则删除集合中的数据
    boolean result = numbers.removeIf(new Predicate<Integer>() {
        @Override
        public boolean test(Integer integer) {
            // 删除所有偶数
            return integer % 2 == 0;
        }
    });
    // 输出删除结果
    System.out.println(result);
    // 遍历集合
    for (Integer number : numbers) {
        System.out.println(number);
    }
}

image.png

retainAll(Collection<?>) // 仅保留当前集合中也包含在指定集合中的元素

    // 数字集合
    Collection<Integer> numbers = new ArrayList<>();
    numbers.add(1);
    numbers.add(2);
    numbers.add(3);
    numbers.add(4);
    numbers.add(5);
    // 需保留的集合
    Collection<Integer> subNumbers = new ArrayList<>();
    subNumbers.add(2);
    subNumbers.add(4);
    subNumbers.add(6);
    // 保留指定集合中的数据
    boolean result = numbers.retainAll(subNumbers);
    // 输出保留结果
    System.out.println(result);
    // 遍历集合
    for (Integer number : numbers) {
        System.out.println(number);
    }
}

image.png

clear() // 清空集合,移除其中所有元素

spliterator() // 返回一个可分割的迭代器,用于并行遍历

stream() // 返回一个顺序流,用于对集合进行流式处理

parallelStream() // 返回一个并行流,支持并行计算操作

ArrayList

  • ArrayList 是基于数组实现的动态扩容列表,其核心特点是支持随机访问(通过索引直接访问元素,时间复杂度 O(1)),有序存储(插入顺序与存储顺序一致)且允许重复元素和 null 值。其优点是查询效率高(按索引访问快)和尾部添加/删除元素性能好(时间复杂度接近 O(1));有序,可重复,可为null
  • 缺点是在列表中间插入或删除元素效率较低(需要移动后续元素,时间复杂度 O(n)),且频繁扩容可能带来性能开销和内存浪费(默认容量 10,按 1.5 倍增长)。总体来说,ArrayList 适合读多写少、随机访问频繁的场景,不适合频繁在中间位置增删的操作。

ArrayList的创建

  • public ArrayList()

创建一个空的 ArrayList,并使用默认的初始容量。一般是10

  • public ArrayList(int initialCapacity)

创建一个空的 ArrayList,并指定一个初始容量

  • public ArrayList(Collection<? extends E> c)

ArrayList 常用方法分类

01 查询操作

  • get(int index) // 返回列表中指定位置的元素
  • indexOf(Object o) // 返回列表中第一次出现指定元素的索引,未找到则返回-1
  • iterator() // 返回一个按顺序迭代列表中元素的迭代器
  • size() // 返回列表中的元素数量
  • isEmpty() // 如果列表中不包含任何元素,则返回true

02 修改操作

  • add(E) // 将指定元素追加到列表的末尾
  • set(int index, E element) // 用指定元素替换列表中指定位置的元素,并返回被替换的元素
  • remove(int index) // 移除列表中指定位置的元素,并返回被移除的元素

03 批量操作

  • clear() // 移除列表中的所有元素
  • stream() // 返回一个以列表为源的顺序流 创建一个包含指定集合 c中所有元素的 ArrayList

LinkList

  • LinkedList 是基于双向链表实现的有序列表,其核心特点是高效的插入和删除操作(在已知节点位置时时间复杂度为 O(1)),并实现了 List 和 Deque 接口,既可作为列表使用,也可作为栈、队列或双端队列。其优点是内存空间动态分配、无扩容开销,且在列表头部/尾部增删元素性能极高有序,可重复,可为null
  • 缺点是随机访问效率低(必须从头或尾遍历查找,时间复杂度 O(n)),内存开销较大(每个节点需存储前后指针和元素值),且缓存不友好(节点在内存中非连续存储)。总体来说,LinkedList 适合频繁在任意位置插入/删除、队列操作多的场景,不适合随机访问频繁、内存敏感的应用。

LinkedList 常用方法分类

01 查询操作

  • getFirst() // 返回链表的第一个元素,如果链表为空则抛出 NoSuchElementException
  • getLast() // 返回链表的最后一个元素,如果链表为空则抛出 NoSuchElementException
  • size() // 返回链表中包含的元素数量
  • isEmpty() // 判断链表是否为空,若不包含任何元素则返回 true

02 修改操作

  • add(E e) // 将指定元素追加到链表的末尾
  • addFirst(E e) // 将指定元素插入到链表的头部

03 批量操作

  • listIterator() // 返回一个可以从头或尾开始遍历链表的 ListIterator 迭代器,支持双向遍历
  • clear() // 移除链表中的所有元素,使链表变为空
  • stream() // 返回一个以链表为源的顺序流,可用于流式操作处理元素

ArrayList和LinkedList的区别

  • ArrayList和LinkedList在数据结构、操作性能、内存占用及应用场景上存在明显差异:
  • ArrayList基于数组实现,支持随机访问,查询速度快,但在中间插入或删除元素时需移动后续元素,增删速度较慢,内存空间利用率高(仅需存储元素本身),适合查询操作频繁的场景;
  • LinkedList基于双向链表实现,不支持高效随机访问,查询需从头或尾遍历,速度较慢,但插入和删除元素只需调整指针,增删速度快,内存空间占用大(每个节点需额外存储前驱和后继指针),适合增删操作频繁的场景。

image.png

HashMap

  • HashMap 是基于哈希表实现的键值对集合,其核心特点是高效的存储和检索(在理想哈希函数下,增删改查的平均时间复杂度为 O(1)),允许 null 键和 null 值,且元素无序存储。其优点是访问速度快(通过键的哈希值直接定位存储位置),扩容机制相对灵活(默认容量 16,负载因子 0.75,当元素数量达到容量乘以负载因子时自动扩容为原来的 2 倍);
  • 缺点是线程不安全(多线程环境下需额外同步),哈希冲突可能影响性能(冲突较多时会退化为链表或红黑树结构,增加查找时间),且迭代顺序不确定。总体来说,HashMap 适合单线程环境下快速键值查找、无需有序遍历的场景,不适合多线程并发或需要保持插入顺序的应用。

image.png

HashMap 常用方法分类

01 查询操作

  • size() // 返回 HashMap 中键值对的数量
  • isEmpty() // 判断 HashMap 是否为空,若不包含任何键值对则返回 true
  • get(Object key) // 根据指定的键获取对应的值,若不存在该键则返回 null
  • containsKey(Object key) // 判断 HashMap 中是否包含指定的键,包含则返回 true

02 修改操作

  • put(K key, V value) // 将指定的键值对存入 HashMap,若键已存在则覆盖原值
  • remove(Object key) // 根据指定的键移除对应的键值对,若键存在则移除并返回原值,否则返回 null

03 批量操作

  • keySet() // 返回一个包含所有键的 Set 集合,可用于遍历所有键
  • clear() // 移除 HashMap 中的所有键值对,使其变为空

LinkedHashMap

  • LinkedHashMap 是基于哈希表和双向链表实现的键值对集合,继承自 HashMap,其核心特点是能够保持元素的插入顺序或访问顺序(通过双向链表维护顺序),同时具备 HashMap 的高效访问性能。其优点是迭代顺序可预测(默认按插入顺序迭代,也可配置为按访问顺序实现类似 LRU 缓存的效果),性能与 HashMap 相近(在保持顺序的同时,增删改查操作的平均时间复杂度仍为 O(1));
  • 缺点是内存占用更高(每个节点需额外存储前驱和后继指针),在迭代时可能略慢于 HashMap(需遍历链表结构)。总体来说,LinkedHashMap 适合需要保持插入顺序或实现 LRU 缓存淘汰策略的场景,不适合对内存极度敏感或无需顺序保证的应用。
  • linkedHashMap在读取了某个元素后,这个元素就自动排到队尾去,导致再次遍历的元素顺序不同
    • 默认是插入顺序,访问元素不会影响遍历顺序。
    • 只有当构造函数的第三个参数为 true,才开启“访问顺序”模式。
    • 在访问顺序模式下,每次 get()put()已存在的键,都会将该元素移到链表末尾
    • 这个特性使得 LinkedHashMap可以轻松实现 LRU(最近最少使用)缓存,而无需手动维护访问顺序。

image.png

HashMap 方法分类

01 查询操作

  • size() // 返回 HashMap 中键值对的总数量
  • isEmpty() // 判断 HashMap 是否为空(即键值对数量为 0),空则返回 true
  • get(Object key) // 根据指定的键(key)获取对应的值(value),若键不存在则返回 null
  • containsKey(Object key) // 判断 HashMap 中是否包含指定的键(key),包含则返回 true
  • containsValue(Object value) // 判断 HashMap 中是否包含指定的值(value),包含则返回 true

02 修改操作

  • put(K key, V value) // 将指定的键值对(key-value)存入 HashMap,若键已存在则覆盖原有值
  • remove(Object key) // 根据指定的键(key)移除对应的键值对,若键存在则移除并返回原值,否则返回 null

03 批量操作

  • putAll(Map<? extends K, ? extends V> m) // 将另一个 Map 中的所有键值对批量添加到当前 HashMap 中(会覆盖已有键的值)
  • keySet() // 返回一个包含所有键(key)的 Set 集合,可用于遍历所有键
  • entrySet() // 返回一个包含所有键值对(Entry)的 Set 集合,可用于遍历所有键值对
  • clear() // 移除 HashMap 中的所有键值对,使其变为空
  • values() // 返回一个包含所有值(value)的 Collection 集合,可用于遍历所有值

TreeMap

  • TreeMap 是基于红黑树实现的有序键值对集合,其核心特点是按键的自然顺序或自定义比较器顺序自动排序,并提供了一系列范围查询和导航方法。其优点是元素始终有序(便于范围查找、获取子集等操作),键不可重复且不允许null键(但值可为null),且查询、插入、删除操作的时间复杂度稳定在O(log n)
  • 缺点是性能略低于HashMap(树结构操作比哈希表慢),内存开销较大(每个节点需存储父节点、子节点引用和颜色标志),且线程不安全。总体来说,TreeMap 适合需要按键排序、范围查询或有序遍历的场景,不适合对性能要求极高或只需快速存取无需排序的应用。

image.png

TreeMap 常用方法分类

01 查询操作

  • size() // 返回 TreeMap 中键值对的总数量
  • isEmpty() // 判断 TreeMap 是否为空(即键值对数量为 0),空则返回 true
  • get(Object key) // 根据指定的键(key)获取对应的值(value),若键不存在则返回 null
  • containsKey(Object key) // 判断 TreeMap 中是否包含指定的键(key),包含则返回 true

02 修改操作

  • put(K key, V value) // 将指定的键值对(key-value)存入 TreeMap,若键已存在则覆盖原有值(并维持排序)
  • remove(Object key) // 根据指定的键(key)移除对应的键值对,若键存在则移除并返回原值,否则返回 null

03 批量操作

  • keySet() // 返回一个包含所有键(key)的 Set 集合(键按 TreeMap 的排序规则排列),可用于遍历所有键
  • clear() // 移除 TreeMap 中的所有键值对,使其变为空
  • descendingMap() // 返回一个逆序的视图(键按自然顺序或比较器的相反顺序排列),可基于逆序视图进行遍历或操作,descendingMap()给你一个倒着看原 Map 的镜子,镜子里看到的顺序是反的,但镜子里和镜子外的物体是同一个东西,在镜子前移动物体,镜子里也会同步变化。
  1. HashSet ≈ HashMap 的键集合
  2. LinkedHashSet ≈ LinkedHashMap 的键集合
  3. TreeSet ≈ TreeMap 的键集合

核心机制(哈希表、链表、红黑树)完全一样,Set 可以看作“值被隐藏的 Map”。但除了“不可重复”,还有 API 设计、使用场景、性能表现上的细微差别。

线程安全容器

image.png 你可以把 CopyOnWriteArrayList想象成一个拍照更新的公告栏

原来的公告栏(比如 ArrayList)是直接在板上涂改,如果一个人正在看公告,另一个人去修改,看的人就会被打断(线程不安全)。

现在的做法是:

  1. 当有人要修改公告时,不会直接在原版上改
  2. 而是先拍一张照片(复制当前所有内容)
  3. 照片复印件上做修改
  4. 修改完成后,把公告栏上的原版换成这张新照片

关键好处

  • 正在看公告的人不受影响,他们看的还是那张旧照片
  • 看完的人再看时,看到的是更新后的新照片
  • 永远不会出现“看一半被修改打断”的情况

代价是

  • 每次修改都要拍新照片(复制整个数组),如果内容很多就很费内存
  • 刚更新完时,有人可能还在看旧照片,看到的是过时信息

所以这适合公告很少改动,但很多人看的场景(比如公司制度、配置信息)。如果公告天天改(比如股票价格),用这种方式就太浪费了。