集合框架是什么?

对容器的学习建议结合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()

参考资料

Java学习篇(三)—— 集合框架的更多相关文章

  1. 【Java学习笔记】<集合框架>定义功能去除ArrayList中的重复元素

    import java.util.ArrayList; import java.util.Iterator; import cn.itcast.p1.bean.Person; public class ...

  2. 【Java学习笔记】<集合框架>对字符串进行长度排序

    package 测试; import java.util.Comparator; public class ComparatorByLength implements Comparator { //定 ...

  3. 【Java学习笔记】<集合框架>TreeSet,Comparable,Comparator

    public class Person implements Comparable{ private String name; private int age; public Person(){ su ...

  4. 已看1.熟练的使用Java语言进行面向对象程序设计,有良好的编程习惯,熟悉常用的Java API,包括集合框架、多线程(并发编程)、I/O(NIO)、Socket、JDBC、XML、反射等。[泛型]\

    1.熟练的使用Java语言进行面向对象程序设计,有良好的编程习惯,熟悉常用的Java API,包括集合框架.多线程(并发编程).I/O(NIO).Socket.JDBC.XML.反射等.[泛型]\1* ...

  5. Java学习笔记之---集合

    Java学习笔记之---集合 (一)集合框架的体系结构 (二)List(列表) (1)特性 1.List中的元素是有序并且可以重复的,成为序列 2.List可以精确的控制每个元素的插入位置,并且可以删 ...

  6. 201671010140. 2016-2017-2 《Java程序设计》java学习第三周

    java学习第三周       不知不觉,学习java已经是第三周了,不同于初见时的无措,慌张,在接触一段时日后,渐渐熟悉了一些,了解到了它的便利之处,也体会到了它的一些难点,本周主攻第四章,< ...

  7. 大数据学习笔记——Java篇之集合框架(ArrayList)

    Java集合框架学习笔记 1. Java集合框架中各接口或子类的继承以及实现关系图: 2. 数组和集合类的区别整理: 数组: 1. 长度是固定的 2. 既可以存放基本数据类型又可以存放引用数据类型 3 ...

  8. JAVA学习第三十四课 (经常使用对象API)—List集合及其子类特点

    整个集合框架中最经常使用的就是List(列表)和Set(集) 一.List集合 && Set的特点 Collection的子接口: 1.List:有序(存入和取出的顺序一致),元素都有 ...

  9. Java 学习 第三篇;面向对象

    1:Java的常用包: 核心类在java 包中:扩展类在javax包中 java.lang 系统默认自动导入 包含String Math System Thread等类 java.util 包含了工具 ...

  10. JAVA学习第三十六课(经常使用对象API)— Set集合:HashSet集合演示

    随着Java学习的深入,感觉大一时搞了一年的ACM,简直是明智之举,Java里非常多数据结构.算法类的东西,理解起来就轻松多了 Set集合下有两大子类开发经常使用 HashSet集合 .TreeSet ...

随机推荐

  1. cxDBTreeList:最简单的节点图标添加方法

    先在窗体上放ImageList关联到cxDBTreeList,在cxDBTreeList的GetNodeImageIndex事件中写如下: procedure cxDBTreeList1GetNode ...

  2. 如何让tcxGrid左边显示序号

    第一步: 设置cxgrid的属性, OptionsView.Indicator = True 第二步: 写OnCustomDrawIndicatorCell方法 procedure TForm1.cx ...

  3. SpringAI vs JBoltAI:Java企业级AI开发的框架之争与实战选型

    「SpringAI vs JBoltAI:Java企业级AI开发的框架之争与实战选型」 一.Java生态的AI困局:工具碎片化与工程化缺失 1. 技术断层:从API调用到全生命周期管理多数企业仍停留在 ...

  4. .net WorkFlow 流程传阅

    WikeFlow官网:www.wikesoft.com WikeFlow学习版演示地址:workflow.wikesoft.com WikeFlow学习版源代码下载:https://gitee.com ...

  5. Redis实现高并发场景下的计数器设计

    大部分互联网公司都需要处理计数器场景,例如风控系统的请求频控.内容平台的播放量统计.电商系统的库存扣减等. 传统方案一般会直接使用RedisUtil.incr(key),这是最简单的方式,但这种方式在 ...

  6. 服务端获取实际IP工具类

    import javax.servlet.http.HttpServletRequest; import java.net.InetAddress; import java.net.UnknownHo ...

  7. Wireshark 的抓包和分析,看这篇就够了!

    原文:Wireshark 的抓包和分析,看这篇就够了!

  8. C#自动属性提供默认值的方法

    编程之路转自:cjavapy.com/article/55/ _  .NET(C#)中,自动属性(Auto-Implemented Properties)提供了一种简洁的方式来实现属性而无需显式定义字 ...

  9. 79.8K star!这款开源自动化神器让技术团队效率飙升,400+集成玩转AI工作流!

    嗨,大家好,我是小华同学,关注我们获得"最新.最全.最优质"开源项目和高效工作学习方法 "n8n 是技术团队自动化领域的瑞士军刀,既能享受可视化搭建的便捷,又能随时插入代 ...

  10. 01Spring-01jdbc 未使用spring代码编写

    目录 pom.xml JdbcDemo1.java pom.xml <?xml version="1.0" encoding="UTF-8"?> & ...