Java学习篇(三)—— 集合框架

集合框架是什么?

对容器的学习建议结合leecode,了解每一个容器的增删改查操作。

数据结构里学习了几种数据结构类型:数组、链表、栈、队列、树、哈希表、堆。在C++中,C++ STL提供了数组vector,栈stack,队列queue,哈希表unordered_map等容器,分为序列式容器和关联式容器,依赖于模板可以实现非常自由的容器构建。这些容器中,一些是依赖于其它容器所构建的,称为容器适配器,是对其它容器的封装;另一些基础容器则被称为底层基础容器,主要包括:vector, deque, list, map, set, unordered_map等。

Java集合框架,也叫作容器,主要是由两大接口派生而来:一个是Collection接口,主要用于存放单一元素;另一个是Map 接口,主要用于存放键值对。对于Collection接口,下面又有三个主要的子接口:ListSetQueue

具体的使用方式不赘述,参考资料和文档里会有,下面主要记录一些问题。

Collection 接口

List

  • ArrayList:JDK1.2 引入,动态数组容器,具有可扩容、不定大小初始化、增删改查、泛型的优点。

  • vector:也是动态数组,JDK1.0 引入,所有方法都被synchronized修饰,是线程安全的,Java 官方文档中早已不推荐使用。

  • LinkedList:双向链表。

Java的链表如何实现?

C++的链表的数据类型是:

class List{
private:
  struct ListNode{
    int val;
    ListNode* next;
    ListNode(int val):val(val), next(nullptr){}
  };
  ListNode head;
public:
  List():head(nullptr){
  };
};

为了使得每一个结点可以长期存在,所以需要在堆上构建对象,next为指针类型。而Java默认除了基础数据类型全都是引用类型,所以也就不需要指针的存在,类似的,我们就可以构建Java的链表:

public class ListNode {
    int val;
    ListNode next;

    // 构造函数
    ListNode(int val) {
        this.val = val;
        this.next = null;
    }
}

public class LinkedListExample {
    public static void main(String[] args) {
        // 构建链表:1 -> 2 -> 3 -> 4 -> null
        ListNode head = new ListNode(1);
        head.next = new ListNode(2);
        head.next.next = new ListNode(3);
        head.next.next.next = new ListNode(4);

        // 打印链表
        ListNode current = head;
        while (current != null) {
            System.out.print(current.val + " -> ");
            current = current.next;
        }
        System.out.println("null");
    }
}

注:C++的类和结构体要;

ArrayList的插入和删除的时间复杂度

这个和C++的vector时间复杂度一致,在最后一个位置就是O(1),其它位置是O(n)。

ArrayList为什么是非线程安全的?

Java中的 ArrayList 是非线程安全的,意味着如果多个线程同时读写同一个 ArrayList,可能会发生数据竞争(race condition)、数据不一致或甚至程序崩溃(如抛出异常),C++的vector也存在这个问题。

import java.util.ArrayList;

public class UnsafeArrayListExample {
    public static void main(String[] args) throws InterruptedException {
        ArrayList<Integer> list = new ArrayList<>();

        Runnable task = () -> {
            for (int i = 0; i < 1000; i++) {
                list.add(i);  // 非线程安全操作
            }
        };

        Thread t1 = new Thread(task);
        Thread t2 = new Thread(task);

        t1.start();
        t2.start();

        t1.join();
        t2.join();

        System.out.println("List size: " + list.size()); // 理论值应为 2000
    }
}

会出现多个线程同时对数组进行扩容或插入时,修改了同一位置的数据,size没来得及更新或冲突。

Set

和C++一致,无序的set使用哈希表(数组+链表+拉链法)实现,有序的set使用红黑树(平衡二叉搜索树)。

  • HashSet(无序,唯一): 基于HashMap实现的,底层采用HashMap来保存元素。

  • LinkedHashSet:HashSet的子类,并且其内部是通过LinkedHashMap来实现的。

  • TreeSet(有序,唯一): 红黑树(自平衡的排序二叉树),类似于unordered_set

特性 HashSet LinkedHashSet TreeSet
🧱 底层结构 哈希表(HashMap 哈希表 + 双向链表 红黑树(TreeMap
📌 元素顺序 无序 插入顺序 自动排序(升序或指定 Comparator)
🕒 时间复杂度 增删查平均 O(1) 增删查平均 O(1),多维护链表指针 增删查 O(logN)
💡 是否有序 是(保持插入顺序) 是(按排序规则)
🧠 使用场景 快速查重 需要维持插入顺序 需要自动排序的集合

Queue

  • PriorityQueue: Object[] 数组来实现小顶堆,这个和C++有一点区别,C++的优先队列是适配器,可以用vectorstack来实现,准确一点,能支持底层操作比如push_backpop_back这些操作的都可以。

  • DelayQueue:延迟队列,用于实现延时任务比如订单下单 15 分钟未支付直接取消,底层是一个基于 PriorityQueue 实现的一个无界队列,是线程安全的。

  • ArrayDeque: 可扩容动态双向数组。

Map 接口

  • HashMap:JDK1.8之前HashMap 由数组+链表组成的。JDK1.8 以后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为 8)(将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树)时,将链表转化为红黑树,以减少搜索时间。

  • LinkedHashMap:它的底层仍然是基于拉链式散列结构即由数组和链表或红黑树组成。另外,LinkedHashMap在上面结构的基础上,增加了一条双向链表,使得上面的结构可以保持键值对的插入顺序。同时通过对链表进行相应的操作,实现了访问顺序相关逻辑。

  • Hashtable:数组+链表组成的,数组是Hashtable的主体,链表则是主要为了解决哈希冲突而存在的。

  • TreeMap:红黑树(自平衡的排序二叉树)。

fail-fast和fail-safe机制

fail-fastfail-safe是Java集合类(尤其是在多线程环境中)对并发修改的处理策略,主要体现于集合类(如 List、Map、Set)在迭代期间如何处理结构修改(modification)。

特性 fail-fast fail-safe
代表类 ArrayList, HashMap, HashSet, Vector(等) ConcurrentHashMap, CopyOnWriteArrayList
修改检测方式 使用 modCount 结构变更计数 使用副本(复制)机制分段锁
是否抛异常 是 (ConcurrentModificationException) 否(安全,不抛异常)
是否实时反映 是,原始集合本身 否,修改的是副本
性能 高,非线程安全 相对较低,但线程安全

fail-fast

快速失败的思想即针对可能发生的异常进行提前表明故障并停止运行,通过尽早的发现和停止错误,降低故障系统级联的风险。通过维护一个modCount记录修改的次数,迭代期间通过比对预期修改次数expectedModCountmodCount是否一致来判断是否存在并发操作,从而实现快速失败,由此保证在避免在异常时执行非必要的复杂代码。

下面演示一段可以引发ConcurrentModificationException的代码:

List<String> list = new ArrayList<>();
list.add("A");
list.add("B");

for (String s : list) {
    if (s.equals("A")) {
        list.remove(s); // 会抛 ConcurrentModificationException
    }
}

报错原因:

  • ArrayList 内部有一个 modCount 字段记录结构修改次数;

  • 迭代器(Iterator)初始化时记录 expectedModCount;

  • 如果在迭代时集合结构被修改,modCount != expectedModCount,抛出异常。

也就是说,Java不可以像C++那样一边迭代一边更改数组,设计初衷就是为了快速暴露潜在的并发bug。解决方法就是:1. 不使用这种迭代循环,使用普通的for循环。 2. 换用线程安全且fail-safe的集合。

fail-safe

而fail-safe也就是安全失败的含义,它旨在即使面对意外情况也能恢复并继续运行,这使得它特别适用于不确定或者不稳定的环境。通过写时复制的思想保证在进行修改操作时复制出一份快照,基于这份快照完成添加或者删除操作后,将CopyOnWriteArrayList底层的数组引用指向这个新的数组空间,由此避免迭代时被并发修改所干扰所导致并发操作安全问题。

下面的代码,和上面的区别就是使用了CopyOnWriteArrayList:

CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
list.add("A");
list.add("B");

for (String s : list) {
    if (s.equals("A")) {
        list.remove(s); // 不会抛异常
    }
}

为什么这样就可以避免fail-fast:

  • CopyOnWriteArrayList 在写操作时复制整个数组,操作副本;

  • ConcurrentHashMap 通过分段锁和 bucket-level 控制来实现线程安全;

  • 迭代器基于旧副本,不会被并发修改破坏。

所以很显然,这样会很慢,这个效率完全不如自己控制线程同步。

排序

C++ STL不仅有容器,还包含算法、迭代器,其中算法部分包含了sort函数,想要自定义sort,可以通过传入自定义的lambda函数实现,比较函数来决定哪一个元素应该排在前面。Java则通过重写Comparable接口和 Comparator接口,它们都是 Java 中用于排序的接口,在实现类对象之间比较大小、排序等方面发挥了重要作用。

容器操作方法对照表

C++ STL操作

容器类型 插入(尾) 插入(头) 插入(中间/指定位置) 删除(尾) 删除(头) 删除(指定位置) 查找/访问
vector<T> push_back() insert(pos, val) pop_back() erase(pos) [] / at()
deque<T> push_back() push_front() insert(pos, val) pop_back() pop_front() erase(pos) [] / at()
list<T> push_back() push_front() insert(pos, val) pop_back() pop_front() erase(pos) [],用迭代器遍历
forward_list<T> ❌(无尾插) push_front() insert_after(pos, val) ❌(无尾删) ❌(无 pop_front) erase_after(pos) 用迭代器
stack<T> push() pop() top()
queue<T> push() ❌(无 pop_back) pop() front() / back()
priority_queue<T> push() pop() top()
set<T> insert(val) - - erase(val) - erase(it) find(val)
unordered_set<T> insert(val) - - erase(val) - erase(it) find(val)
map<K,V> insert({k,v}) - - erase(k) - erase(it) operator[] / find()
unordered_map<K,V> insert({k,v}) - - erase(k) - erase(it) operator[] / find()

Java容器操作

容器类型 插入(尾) 插入(头) 插入(中间/指定位置) 删除(尾) 删除(头) 删除(指定位置/值) 查找/访问方式
ArrayList<E> add(e) add(index, e) remove(size-1) remove(index) get(index) / set()
LinkedList<E> addLast(e) / add(e) addFirst(e) add(index, e) removeLast() removeFirst() remove(index) / remove(obj) get(index)
Vector<E> add(e) / addElement(e) add(index, e) remove(size-1) remove(index) get(index) / elementAt()
Stack<E> push(e) pop() peek()
Queue<E> (接口) offer(e) poll() peek()
Deque<E> (接口) offerLast(e) / addLast(e) offerFirst(e) / addFirst(e) pollLast() / removeLast() pollFirst() / removeFirst() peekFirst() / peekLast()
PriorityQueue<E> offer(e) poll() peek()
HashSet<E> add(e) - - remove(e) - remove(e) contains(e)
LinkedHashSet<E> add(e) -(保序) - remove(e) - remove(e) contains(e)
TreeSet<E> add(e) - - remove(e) - remove(e) contains(e) / ceiling()
HashMap<K,V> put(k,v) - - remove(k) - remove(k) get(k) / containsKey()
LinkedHashMap<K,V> 同上(按插入顺序) - - remove(k) - remove(k) get(k)
TreeMap<K,V> put(k,v) - - remove(k) - remove(k) get(k) / ceilingKey()

参考资料

posted @ 2025-06-02 18:12  ZCry  阅读(21)  评论(0)    收藏  举报