禅与计算机 禅与计算机
首页
  • Java基础

    • 聊一聊java一些核心知识点
    • 聊聊java面向对象核心知识点
    • 聊聊Java中的异常
    • 聊聊Java中的常用类String
    • 万字长文带你细聊Java注解本质
    • 来聊聊Java的反射机制
    • 深入解析Java泛型的魅力与机制
    • Java集合框架深度解析与面试指南
    • Java常用集合类HashMap深度解析
    • LinkedHashMap源码到面试题的全解析
    • 深入解析CopyOnWriteArrayList的工作机制
    • Java基础IO总结
    • Java三大IO模型小结
    • Java BIO NIO AIO详解
    • Java进阶NIO之IO多路复用详解
    • Java8流式编程入门
    • 一文速通lambda与函数式编程
    • Java8函数式方法引用最佳实践
  • Java并发编程

    • Java并发编程基础小结
    • 深入理解Java中的final关键字
    • 浅谈Java并发安全发布技术
    • 浅谈Java并发编程中断的哲学
    • Java线程池知识点小结
    • 浅谈Java线程池中拒绝策略与流控的艺术
    • synchronized关键字使用指南
    • 深入源码解析synchronized关键字
    • 详解JUC包下的锁
    • 详解并发编程中的CAS原子类
    • LongAdder源码分析
    • AQS源码解析
    • 深入剖析Java并发编程中的死锁问题
    • Java并发容器总结
    • 详解Java并发编程volatile关键字
    • 并发编程ThreadLocal必知必会
    • CompletableFuture基础实践小结
    • CompletableFuture异步多任务最佳实践
    • 硬核详解FutureTask设计与实现
    • 线程池大小设置的底层逻辑与场景化方案
    • 来聊一个有趣的限流器RateLimiter
  • JVM相关

    • 从零开始掌握 JVM
    • JVM核心知识点小结
    • JVM指令集概览:基础与应用
    • JVM类加载器深度解析
    • JVM方法区深度解析
    • Java内存模型JMM详解
    • Java对象大小的精确计算方法
    • 逃逸分析在Java中的应用与优化
    • 从零开始理解JVM的JIT编译机制
    • G1垃圾回收器:原理详解与调优指南
    • JVM故障排查实战指南
    • JVM内存问题排错最佳实践
    • JVM内存溢出排查指南
    • 简明的Arthas使用教程
    • 简明的Arthas配置及基础运维教程
    • 基于Arthas Idea的JVM故障排查与指令生成
    • 基于arthas量化监控诊断java应用方法论与实践
    • 深入剖析arthas技术原理
  • 深入理解Spring框架

    • Spring 核心知识点全面解析
    • Spring核心功能IOC详解
    • Spring AOP 深度剖析与实践
    • Spring 三级缓存机制深度解析
    • 深入 Spring 源码,剖析设计模式的落地实践
    • 探索 Spring 事务的奥秘
    • 深入解析Spring Bean的生命周期管理
    • 解读 Spring Boot 核心知识点
    • Spring Boot 启动优化实战:1分钟到13秒的排查与优化之路
    • Spring Boot自动装配原理及实践
    • 一文快速上手Sharding-JDBC
    • sharding-jdbc如何实现分页查询
    • 基于DynamicDataSource整合分库分表框架Shardingsphere
  • 计算机组成原理

    • 计算机硬件知识小结
    • CPU核心知识点小结
    • 浅谈CPU流水线的艺术
    • 从Java程序员视角聊聊CPU缓存
    • CPU任务调度和伪共享问题小结
    • CPU MESI缓存一致性协议
    • CPU内存管理机制
    • 内存深度解析
    • 磁盘存储原理
    • 详解计算机启动步骤
    • CPU南北桥架构与发展史
    • CPU中断机制与硬件交互详解
  • 操作系统

    • 如何实现一个高性能服务器
    • Linux文件结构与文件权限
    • Linux常见压缩指令小结
    • Linux核心系统调用详解
    • Linux进程管理
    • Linux线程管理
    • 进程与线程深度解析
    • Linux进程间通信机制
    • 零拷贝技术原理与实践
    • CPU缓存一致性问题深度解析
    • IO任务与CPU调度艺术
  • 计算机网络

    • 网卡通信原理详解
    • 网卡数据包处理指南
    • 基于抓包详解TCP协议
  • 编码最佳实践

    • 浅谈现代软件工程TDD最佳实践
    • 浅谈TDD模式下并发程序设计与实现
    • 面向AI编程新范式Trae后端开发环境搭建与实践
    • 基于提示词工程的Redis签到功能开发实践
    • 基于Vibe Coding的Redis分页查询实现
    • 告别AI无效对话:资深工程师的提示词设计最佳实践
  • 实用技巧与配置

    • Mac常用快捷键与效率插件指南
    • Keynote技术科普短视频制作全攻略
  • 写作

    • 写好技术博客的5大核心原则:从认知科学到AI工具的全流程指南
  • 开发工具

    • IDEA配置详解与高效使用指南
  • Nodejs
  • 博客搭建
  • Redis

    • Redis核心知识小结
    • 解锁Redis发布订阅模式
    • 掌握Redis事务
    • Redis主从复制技术
    • Redis的哨兵模式详解
    • 深度剖析Redisson分布式锁
    • 详解redis单线程设计思路
    • 来聊聊Redis所实现的Reactor模型
    • Redis RDB持久化源码深度解析
    • 来聊聊redis的AOF写入
    • 来聊聊Redis持久化AOF管道通信的设计
    • 来聊聊redis集群数据迁移
    • Redis SDS动态字符串深度解析
    • 高效索引的秘密:redis跳表设计与实现
    • 聊聊redis中的字典设计与实现
  • MySQL

    • MySQL基础知识点小结
    • 解读MySQL 索引基础
    • MySQL 索引进阶指南
    • 解读MySQL Explain关键字
    • 探秘 MySQL 锁:原理与实践
    • 详解MySQL重做日志redolog
    • 详解undoLog在MySQL MVCC中的运用
    • MySQL二进制日志binlog核心知识点
    • MySQL高效插入数据的最佳实践
    • MySQL分页查询优化指南
    • MySQL流式查询的奥秘与应用解析
    • 来聊聊分库分表
    • 来聊聊大厂常用的分布式ID生成方案
  • ElasticSearch

    • 从Lucene到Elasticsearch:进化之路
    • ES 基础使用指南
    • ElasticSearch如何写入一篇文档
    • 深入剖析Elasticsearch文档读取原理
    • 聊聊ElasticSearch性能调优
    • Spring借助Easy-Es操作ES
  • Netty

    • 一文快速了解高性能网络通信框架Netty
    • Netty网络传输简记
    • 来聊聊Netty的ByteBuf
    • 来聊聊Netty消息发送的那些事
    • 解密Netty高性能之谜:NioEventLoop线程池阻塞分析
    • 详解Netty中的责任链Pipeline如何管理ChannelHandler
    • Netty Reactor模型常见知识点小结
    • Netty如何驾驭TCP流式传输?粘包拆包问题全解
    • Netty解码器源码解析
  • 消息队列

    • 一文快速入门消息队列
    • 消息队列RocketMQ入门指南
    • 基于RocketMQ实现分布式事务
    • RocketMQ容器化最佳实践
    • RocketMQ常见问题与深度解析
    • Kafka快速安装与使用指南
  • Nginx

    • Linux下的nginx安装
    • Nginx基础入门总结
    • Nginx核心指令小结
    • Nginx进程结构与核心模块初探
    • Nginx应用进阶HTTP核心模块配置
    • Nginx缓存及HTTPS配置小记
    • nginx高可用实践简记
    • Nginx性能优化
  • 微服务基础

    • 微服务基础知识小结
    • 分布式事务核心概念小结
    • OpenFeign核心知识小结
    • 微服务组件Gateway核心使用小结
    • 分布式事务Seata实践
    • 用 Docker Compose 完成 Seata 的整合部署
  • Nacos

    • Nacos服务注册原理全解析
    • Nacos服务订阅流程全解析
    • Nacos服务变更推送流程全解析
    • 深入解析SpringCloud负载均衡器Loadbalancer
    • Nacos源码环境搭建与调试指南
  • Seata

    • 深度剖析Seata源码
  • Docker部署

    • 一文快速掌握docker的理念和基本使用
    • 使用docker编排容器
    • 基于docker-compose部署微服务基本环境
    • 基于docker容器化部署微服务
    • Gateway全局异常处理及请求响应监控
    • Docker图形化界面工具Portainer最佳实践
  • Go基础

    • 一文带你速通Go语言基础语法
    • 一文快速掌握Go语言切片
    • 来聊聊go语言的hashMap
    • 一文速通go语言类型系统
    • 浅谈Go语言中的面向对象
    • go语言是如何实现协程的
    • 聊聊go语言中的GMP模型
    • 极简的go语言channel入门
    • 聊聊go语言基于epoll的网络并发实现
    • 写给Java开发的Go语言协程实践
  • mini-redis实战

    • 来聊聊我用go手写redis这件事
    • mini-redis如何解析处理客户端请求
    • 实现mini-redis字符串操作
    • 硬核复刻redis底层双向链表核心实现
    • 动手复刻redis之go语言下的字典的设计与落地
    • Go 语言下的 Redis 跳表设计与实现
    • Go 语言版 Redis 有序集合指令复刻探索
  • 项目编排

    • Spring脚手架创建简记
    • Spring脚手架集成分页插件
    • Spring脚手架集成校验框架
    • maven父子模块两种搭建方式简记
    • SpringBoot+Vue3前后端快速整合入门
    • 来聊聊Java项目分层规范
  • 场景设计

    • Java实现文件分片上传
    • 基于时间缓存优化浏览器轮询阻塞问题
    • 基于EasyExcel实现高效导出
    • 10亿数据高效插入MySQL最佳方案
    • 从开源框架中学习那些实用的位运算技巧
  • CI/CD

    • 基于NETAPP实现内网穿透
    • 基于Gitee实现Jenkins自动化部署SpringBoot项目
    • Jenkins离线安装部署教程简记
    • 基于Nexus搭建Maven私服基础入门
    • 基于内网的Jenkins整合gitlab综合方案简记
  • 监控方法论

    • SpringBoot集成Prometheus与Grafana监控
    • Java监控度量Micrometer全解析
    • 从 micrometer计量器角度快速上手promQL
    • 硬核安利一个监控告警开源项目Nightingale
  • Spring AI

    • Spring AI Alibaba深度实战:一文掌握智能体开发全流程
    • Spring AI Alibaba实战:JVM监控诊断Arthas Agent的工程化构建与最佳实践
  • 大模型评测

    • M2.7 真能打!我用两个真实场景测了测,结果有点意外
    • Qoder JetBrains插件评测:祖传代码重构与接口优化实战
关于
收藏
  • 分类
  • 标签
  • 归档
GitHub (opens new window)

sharkchili

计算机禅修者
首页
  • Java基础

    • 聊一聊java一些核心知识点
    • 聊聊java面向对象核心知识点
    • 聊聊Java中的异常
    • 聊聊Java中的常用类String
    • 万字长文带你细聊Java注解本质
    • 来聊聊Java的反射机制
    • 深入解析Java泛型的魅力与机制
    • Java集合框架深度解析与面试指南
    • Java常用集合类HashMap深度解析
    • LinkedHashMap源码到面试题的全解析
    • 深入解析CopyOnWriteArrayList的工作机制
    • Java基础IO总结
    • Java三大IO模型小结
    • Java BIO NIO AIO详解
    • Java进阶NIO之IO多路复用详解
    • Java8流式编程入门
    • 一文速通lambda与函数式编程
    • Java8函数式方法引用最佳实践
  • Java并发编程

    • Java并发编程基础小结
    • 深入理解Java中的final关键字
    • 浅谈Java并发安全发布技术
    • 浅谈Java并发编程中断的哲学
    • Java线程池知识点小结
    • 浅谈Java线程池中拒绝策略与流控的艺术
    • synchronized关键字使用指南
    • 深入源码解析synchronized关键字
    • 详解JUC包下的锁
    • 详解并发编程中的CAS原子类
    • LongAdder源码分析
    • AQS源码解析
    • 深入剖析Java并发编程中的死锁问题
    • Java并发容器总结
    • 详解Java并发编程volatile关键字
    • 并发编程ThreadLocal必知必会
    • CompletableFuture基础实践小结
    • CompletableFuture异步多任务最佳实践
    • 硬核详解FutureTask设计与实现
    • 线程池大小设置的底层逻辑与场景化方案
    • 来聊一个有趣的限流器RateLimiter
  • JVM相关

    • 从零开始掌握 JVM
    • JVM核心知识点小结
    • JVM指令集概览:基础与应用
    • JVM类加载器深度解析
    • JVM方法区深度解析
    • Java内存模型JMM详解
    • Java对象大小的精确计算方法
    • 逃逸分析在Java中的应用与优化
    • 从零开始理解JVM的JIT编译机制
    • G1垃圾回收器:原理详解与调优指南
    • JVM故障排查实战指南
    • JVM内存问题排错最佳实践
    • JVM内存溢出排查指南
    • 简明的Arthas使用教程
    • 简明的Arthas配置及基础运维教程
    • 基于Arthas Idea的JVM故障排查与指令生成
    • 基于arthas量化监控诊断java应用方法论与实践
    • 深入剖析arthas技术原理
  • 深入理解Spring框架

    • Spring 核心知识点全面解析
    • Spring核心功能IOC详解
    • Spring AOP 深度剖析与实践
    • Spring 三级缓存机制深度解析
    • 深入 Spring 源码,剖析设计模式的落地实践
    • 探索 Spring 事务的奥秘
    • 深入解析Spring Bean的生命周期管理
    • 解读 Spring Boot 核心知识点
    • Spring Boot 启动优化实战:1分钟到13秒的排查与优化之路
    • Spring Boot自动装配原理及实践
    • 一文快速上手Sharding-JDBC
    • sharding-jdbc如何实现分页查询
    • 基于DynamicDataSource整合分库分表框架Shardingsphere
  • 计算机组成原理

    • 计算机硬件知识小结
    • CPU核心知识点小结
    • 浅谈CPU流水线的艺术
    • 从Java程序员视角聊聊CPU缓存
    • CPU任务调度和伪共享问题小结
    • CPU MESI缓存一致性协议
    • CPU内存管理机制
    • 内存深度解析
    • 磁盘存储原理
    • 详解计算机启动步骤
    • CPU南北桥架构与发展史
    • CPU中断机制与硬件交互详解
  • 操作系统

    • 如何实现一个高性能服务器
    • Linux文件结构与文件权限
    • Linux常见压缩指令小结
    • Linux核心系统调用详解
    • Linux进程管理
    • Linux线程管理
    • 进程与线程深度解析
    • Linux进程间通信机制
    • 零拷贝技术原理与实践
    • CPU缓存一致性问题深度解析
    • IO任务与CPU调度艺术
  • 计算机网络

    • 网卡通信原理详解
    • 网卡数据包处理指南
    • 基于抓包详解TCP协议
  • 编码最佳实践

    • 浅谈现代软件工程TDD最佳实践
    • 浅谈TDD模式下并发程序设计与实现
    • 面向AI编程新范式Trae后端开发环境搭建与实践
    • 基于提示词工程的Redis签到功能开发实践
    • 基于Vibe Coding的Redis分页查询实现
    • 告别AI无效对话:资深工程师的提示词设计最佳实践
  • 实用技巧与配置

    • Mac常用快捷键与效率插件指南
    • Keynote技术科普短视频制作全攻略
  • 写作

    • 写好技术博客的5大核心原则:从认知科学到AI工具的全流程指南
  • 开发工具

    • IDEA配置详解与高效使用指南
  • Nodejs
  • 博客搭建
  • Redis

    • Redis核心知识小结
    • 解锁Redis发布订阅模式
    • 掌握Redis事务
    • Redis主从复制技术
    • Redis的哨兵模式详解
    • 深度剖析Redisson分布式锁
    • 详解redis单线程设计思路
    • 来聊聊Redis所实现的Reactor模型
    • Redis RDB持久化源码深度解析
    • 来聊聊redis的AOF写入
    • 来聊聊Redis持久化AOF管道通信的设计
    • 来聊聊redis集群数据迁移
    • Redis SDS动态字符串深度解析
    • 高效索引的秘密:redis跳表设计与实现
    • 聊聊redis中的字典设计与实现
  • MySQL

    • MySQL基础知识点小结
    • 解读MySQL 索引基础
    • MySQL 索引进阶指南
    • 解读MySQL Explain关键字
    • 探秘 MySQL 锁:原理与实践
    • 详解MySQL重做日志redolog
    • 详解undoLog在MySQL MVCC中的运用
    • MySQL二进制日志binlog核心知识点
    • MySQL高效插入数据的最佳实践
    • MySQL分页查询优化指南
    • MySQL流式查询的奥秘与应用解析
    • 来聊聊分库分表
    • 来聊聊大厂常用的分布式ID生成方案
  • ElasticSearch

    • 从Lucene到Elasticsearch:进化之路
    • ES 基础使用指南
    • ElasticSearch如何写入一篇文档
    • 深入剖析Elasticsearch文档读取原理
    • 聊聊ElasticSearch性能调优
    • Spring借助Easy-Es操作ES
  • Netty

    • 一文快速了解高性能网络通信框架Netty
    • Netty网络传输简记
    • 来聊聊Netty的ByteBuf
    • 来聊聊Netty消息发送的那些事
    • 解密Netty高性能之谜:NioEventLoop线程池阻塞分析
    • 详解Netty中的责任链Pipeline如何管理ChannelHandler
    • Netty Reactor模型常见知识点小结
    • Netty如何驾驭TCP流式传输?粘包拆包问题全解
    • Netty解码器源码解析
  • 消息队列

    • 一文快速入门消息队列
    • 消息队列RocketMQ入门指南
    • 基于RocketMQ实现分布式事务
    • RocketMQ容器化最佳实践
    • RocketMQ常见问题与深度解析
    • Kafka快速安装与使用指南
  • Nginx

    • Linux下的nginx安装
    • Nginx基础入门总结
    • Nginx核心指令小结
    • Nginx进程结构与核心模块初探
    • Nginx应用进阶HTTP核心模块配置
    • Nginx缓存及HTTPS配置小记
    • nginx高可用实践简记
    • Nginx性能优化
  • 微服务基础

    • 微服务基础知识小结
    • 分布式事务核心概念小结
    • OpenFeign核心知识小结
    • 微服务组件Gateway核心使用小结
    • 分布式事务Seata实践
    • 用 Docker Compose 完成 Seata 的整合部署
  • Nacos

    • Nacos服务注册原理全解析
    • Nacos服务订阅流程全解析
    • Nacos服务变更推送流程全解析
    • 深入解析SpringCloud负载均衡器Loadbalancer
    • Nacos源码环境搭建与调试指南
  • Seata

    • 深度剖析Seata源码
  • Docker部署

    • 一文快速掌握docker的理念和基本使用
    • 使用docker编排容器
    • 基于docker-compose部署微服务基本环境
    • 基于docker容器化部署微服务
    • Gateway全局异常处理及请求响应监控
    • Docker图形化界面工具Portainer最佳实践
  • Go基础

    • 一文带你速通Go语言基础语法
    • 一文快速掌握Go语言切片
    • 来聊聊go语言的hashMap
    • 一文速通go语言类型系统
    • 浅谈Go语言中的面向对象
    • go语言是如何实现协程的
    • 聊聊go语言中的GMP模型
    • 极简的go语言channel入门
    • 聊聊go语言基于epoll的网络并发实现
    • 写给Java开发的Go语言协程实践
  • mini-redis实战

    • 来聊聊我用go手写redis这件事
    • mini-redis如何解析处理客户端请求
    • 实现mini-redis字符串操作
    • 硬核复刻redis底层双向链表核心实现
    • 动手复刻redis之go语言下的字典的设计与落地
    • Go 语言下的 Redis 跳表设计与实现
    • Go 语言版 Redis 有序集合指令复刻探索
  • 项目编排

    • Spring脚手架创建简记
    • Spring脚手架集成分页插件
    • Spring脚手架集成校验框架
    • maven父子模块两种搭建方式简记
    • SpringBoot+Vue3前后端快速整合入门
    • 来聊聊Java项目分层规范
  • 场景设计

    • Java实现文件分片上传
    • 基于时间缓存优化浏览器轮询阻塞问题
    • 基于EasyExcel实现高效导出
    • 10亿数据高效插入MySQL最佳方案
    • 从开源框架中学习那些实用的位运算技巧
  • CI/CD

    • 基于NETAPP实现内网穿透
    • 基于Gitee实现Jenkins自动化部署SpringBoot项目
    • Jenkins离线安装部署教程简记
    • 基于Nexus搭建Maven私服基础入门
    • 基于内网的Jenkins整合gitlab综合方案简记
  • 监控方法论

    • SpringBoot集成Prometheus与Grafana监控
    • Java监控度量Micrometer全解析
    • 从 micrometer计量器角度快速上手promQL
    • 硬核安利一个监控告警开源项目Nightingale
  • Spring AI

    • Spring AI Alibaba深度实战:一文掌握智能体开发全流程
    • Spring AI Alibaba实战:JVM监控诊断Arthas Agent的工程化构建与最佳实践
  • 大模型评测

    • M2.7 真能打!我用两个真实场景测了测,结果有点意外
    • Qoder JetBrains插件评测:祖传代码重构与接口优化实战
关于
收藏
  • 分类
  • 标签
  • 归档
GitHub (opens new window)
  • Java基础

    • 聊一聊java一些核心知识点
    • 聊聊java面向对象核心知识点
    • 聊聊Java中的异常
    • 聊聊Java中的常用类String
    • 万字长文带你细聊Java注解本质
    • 来聊聊Java的反射机制
    • 深入解析 Java 泛型的魅力与机制
    • 来聊聊Java为什么只有值传递
    • 来聊聊大厂常问的SPI工作原理
    • 来聊聊session与token的区别
    • Java集合框架深度解析与面试指南
      • 写在文章开头
      • Java集合的体系概览
      • 详解List集合体系知识点
        • List集合概览
        • ArrayList容量是10,给它添加一个元素会发生什么?
        • 针对动态扩容导致的性能问题,你有什么解决办法嘛?
        • ArrayList和LinkedList性能差异体现在哪
        • ArrayList 和 Vector 的异同
        • ArrayList 与 LinkedList 的区别
        • ArrayList 的扩容机制
      • 详解Map集合
        • Map接口定义概览
        • HashMap(重点)
        • 更多关于HashMap的知识
        • 更多关于LinkedHashMap
        • 更多ConcurrentHashMap
        • 详解Hashtable核心添加操作
      • 详解Set集合
        • Set基本核心概念
        • HashMap 和 Hashtable 的区别(重点)
        • HashMap 和 HashSet有什么区别
        • HashMap 和 TreeMap 有什么区别
        • HashSet实现去重插入的底层工作机制了解嘛?
        • HashSet、LinkedHashSet 和 TreeSet 使用场景
      • 优先队列PriorityQueue
      • Java集合使用以及工具类小结
      • 一道比较偏门的校招笔试题
      • 小结
      • 参考文献
    • Java常用集合类HashMap深度解析
    • 一文带你速通HashMap底层核心数据结构红黑树
    • 深入HashMap底层理解阿里手册的遍历守则
    • LinkedHashMap源码到面试题的全解析
    • 空间预分配思想提升HashMap插入效率
    • 解析Java集合工具类:功能与实践
    • 深入解析CopyOnWriteArrayList的工作机制
    • Java基础IO总结
    • Java三大IO模型小结
    • Java BIO NIO AIO详解
    • Java进阶NIO之IO多路复用详解
    • 聊聊Java关于IO流中的设计模式
    • 为什么流不关闭会导致内存泄漏
    • 聊聊java零拷贝的几种实现
    • Java8流式编程入门
    • Java8流式编程详解
    • 来聊聊java8的数值流
    • 聊聊Java8中的函数式编程
    • 一文速通lambda与函数式编程
    • 基于lambda简化设计模式
    • Java8函数式方法引用最佳实践
    • 使用Java8并行流的注意事项
    • 详解java数值类型核心知识点
    • 将一维数组按指定长度转为二维数组
    • 33个非常实用的JavaScript一行代码
    • 多种数组去重性能对比
    • 防抖与节流函数
    • 比typeof运算符更准确的类型判断
    • new命令原理
    • ES6面向对象
    • ES5面向对象
    • 判断是否为移动端浏览器
    • JS随机打乱数组
    • JS获取和修改url参数
    • 三级目录

  • 并发编程

  • JVM相关

  • 深入理解Spring框架

  • Java核心技术
  • Java基础
sharkchili
2022-12-13
目录

Java集合框架深度解析与面试指南

# 写在文章开头

又到了每年的面试旺季,看到很多读者反馈希望笔者可以根据近几年的面试对这篇文章进行补充迭代,所以笔者结合各大网站的面试题型对这篇文章进行进一步的补充和迭代。

你好,我叫sharkchili,目前还是在一线奋斗的Java开发,经历过很多有意思的项目,也写过很多有意思的文章,是CSDN Java领域的博客专家,也是Java Guide的维护者之一,非常欢迎你关注我的公众号:写代码的SharkChili,实时获取笔者最新的技术推文同时还能和笔者进行深入交流。

# Java集合的体系概览

从Java顶层设计角度分类而言,集合整体可分为两大类型:

第1大类是存放单元素的Collection,从源码的注释即可看出,该接口用于表示一组对象的抽象,该接口下的实现的集合空间或允许或不允许元素重复,JDK不提供此几口的任何直接实现,也就是说,该接口底层有包括List、Set等接口的抽象实现:

The root interface in the collection hierarchy. A collection represents a group of objects, known as its elements. Some collections allow duplicate elements and others do not. Some are ordered and others unordered. The JDK does not provide any direct implementations of this interface: it provides implementations of more specific subinterfaces like Set and List. This interface is typically used to pass collections around and manipulate them where maximum generality is desired.

第2大类则是存放键值对的Map,该类型要求键不可重复,且每个键最多可以到映射到一个值(注意这是从宏观角度说的值,该值可以是一个对象、可以是一个集合):

An object that maps keys to values. A map cannot contain duplicate keys; each key can map to at most one value.

我们针对Collection接口进行展开说明,按照元素存储规则的不同我们又可以分为:

  1. 有序不重复的Set集合体系。
  2. 有序可重复的LIst集合体系。
  3. 支持FIFO顺序的队列类型Queue。

对应的我们给出类图:

同理我们将Map接口进行展开,他的特点就是每一个元素都是由键值对组成,我们可以通过key找到对应的value,类图如下,集合具体详情笔者会在后文阐述这里我们只要有一个粗略的印象即可:

# 详解List集合体系知识点

# List集合概览

List即有序集合,该接口体系下所实现的集合可以精确控制每一个元素插入的位置,用户可以通过整数索引定位和访问元素:

An ordered collection (also known as a sequence). The user of this interface has precise control over where in the list each element is inserted. The user can access elements by their integer index (position in the list), and search for elements in the list.

从底层结构角度,有序集合还可以有两种实现,第一种也就是我们常说的ArrayList ,从ArrayList源码找到的ArrayList底层存储元素的变量elementData,可以看出ArrayList本质上就是对原生数组的封装:

transient Object[] elementData;
1

第2中则是LinkedList即双向链表所实现的有序集合,它由一个个节点构成,节点有双指针,分别指向前驱节点和后继节点。

private static class Node<E> {
        E item;
        // 指向后继节点
        Node<E> next;
        //指向前驱节点
        Node<E> prev;

        Node(Node<E> prev, E element, Node<E> next) {
            this.item = element;
            this.next = next;
            this.prev = prev;
        }
    }
1
2
3
4
5
6
7
8
9
10
11
12
13

Vector底层实现ArrayList一样都是通过空间连续的数组构成,与ArrayList的区别是它在操作时是有上锁的,这意味着多线程场景下它是可以保证线程安全的,但vector现在基本不用了,这里仅仅做个了解:

public class Vector<E>
    extends AbstractList<E>
    implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
   //......
    protected Object[] elementData;
	//......
}
1
2
3
4
5
6
7
8

# ArrayList容量是10,给它添加一个元素会发生什么?

我们不妨看看这样一段代码,可以看到我们将集合容量设置为10,第11次添加元素时,由于ArrayList底层使用的数组已满,为了能够容纳新的元素,它会进行一次动态扩容,即创建一个更大的容器将原有空间的元素拷贝过去:

		 //创建1个容量为10的数组
        ArrayList<Integer> arrayList = new ArrayList<>(10);
        //连续添加10次至空间满
        for (int i = 0; i < 10; i++) {
            arrayList.add(i);
        }
        //再次添加引发动态扩容
        arrayList.add(10);
1
2
3
4
5
6
7
8

我们查看add源码实现细节,可以每次插入前都会调用ensureCapacityInternal来确定当前数组空间是否可以容纳新元素:

public boolean add(E e) {
		//判断本次插入位置是否大于容量
        ensureCapacityInternal(size + 1);
        elementData[size++] = e;
        return true;
    }
1
2
3
4
5
6

查看ensureCapacityInternal的细节可知,一旦感知数组空间不足以容纳新元素时,ArrayList会创建一个新容器大小为原来的1.5倍,然后再将原数组元素拷贝到新容器中:

private void ensureCapacityInternal(int minCapacity) {
		//传入当前元素空间和所需的最小数组空间大小
        ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
    }

    private void ensureExplicitCapacity(int minCapacity) {
        modCount++;

        //当需求空间大于数组
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }

private void grow(int minCapacity) {
        // 创建一个原有容器1.5倍的数组空间
        int oldCapacity = elementData.length;
        int newCapacity = oldCapacity + (oldCapacity >> 1);
     	//......
     	//将原有元素elementData拷贝到新空间去
        elementData = Arrays.copyOf(elementData, newCapacity);
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

# 针对动态扩容导致的性能问题,你有什么解决办法嘛?

我们可以提前调用ensureCapacity顶下最终容量一次性完成动态扩容提高程序执行性能。

public static void main(String[] args) {
        int size = 1000_0000;
        ArrayList<Integer> list = new ArrayList<>(1);
        long start = System.currentTimeMillis();
        for (int i = 0; i < size; i++) {
            list.add(i);
        }
        long end = System.currentTimeMillis();
        System.out.println("无显示扩容,完成时间:" + (end - start));


        //显示扩容显示扩容,避免多次动态扩容的拷贝
        ArrayList<Integer> list2 = new ArrayList<>(size);
        start = System.currentTimeMillis();
        list2.ensureCapacity(size);
        for (int i = 0; i < size; i++) {
            list2.add(i);
        }
        end = System.currentTimeMillis();
        System.out.println("显示扩容,完成时间:" + (end - start));
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

输出结果如下,可以看到在显示指明大小空间的情况下,性能要优于常规插入:

无显示扩容,完成时间:6122
显示扩容,完成时间:761
1
2

# ArrayList和LinkedList性能差异体现在哪

我们给出两种集合的头插法的示例代码:

public static void main(String[] args) {
        int size = 10_0000;
        List<Integer> arrayList = new ArrayList<>();
        List<Integer> linkedList = new LinkedList<>();

        long start = System.currentTimeMillis();
        for (int i = 0; i < size; i++) {
            arrayList.add(0, i);
        }
        long end = System.currentTimeMillis();
        System.out.println("arrayList头插时长:" + (end - start));


        start = System.currentTimeMillis();
        for (int i = 0; i < size; i++) {
            linkedList.add(0, i);
        }
        end = System.currentTimeMillis();
        System.out.println("linkedList 头插时长:" + (end - start));


        start = System.currentTimeMillis();
        for (int i = 0; i < size; i++) {
            ((LinkedList<Integer>) linkedList).addFirst(i);
        }
        end = System.currentTimeMillis();
        System.out.println("linkedList addFirst 耗时:" + (end - start));
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

从性能表现上来看arrayList表现最差,而linkedList 的addFirst 表现最出色。

arrayList头插时长:562
linkedList 头插时长:8
linkedList addFirst 耗时:4
1
2
3

这里我们不妨说一下原因,arrayList性能差原因很明显,每次头部插入都需要挪动整个数组,linkedList的add方法在进行插入时,若是头插法,它会通过node方法定位头节点,然后在使用linkBefore完成头插法。

 public void add(int index, E element) {
      	//......

        if (index == size)
            linkLast(element);
        else
        	//通过node定位到头节点,再进行插入操作
            linkBefore(element, node(index));
    }
1
2
3
4
5
6
7
8
9

而链表的addFirst 就不一样,它直接定位到头节点,进行头插法,正是这一点点性能上的差距造成两者性能表现上微小的差异。

private void linkFirst(E e) {
		//直接定位到头节点,进行头插法
        final Node<E> f = first;
        //创建新节点
        final Node<E> newNode = new Node<>(null, e, f);
        //直接让first 直接引用该节点
        first = newNode;
        //让原有头节点指向当前节点
        if (f == null)
            last = newNode;
        else
            f.prev = newNode;
        //调整元素空间数量    
        size++;
        modCount++;
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

再来看看尾插法:

public static void main(String[] args) {
        int size = 10_0000;
        List<Integer> arrayList = new ArrayList<>();
        List<Integer> linkedList = new LinkedList<>();

        long start = System.currentTimeMillis();
        for (int i = 0; i < size; i++) {
            arrayList.add(i, i);
        }
        long end = System.currentTimeMillis();
        System.out.println("arrayList 尾插时长:" + (end - start));


        start = System.currentTimeMillis();
        for (int i = 0; i < size; i++) {
            linkedList.add(i, i);
        }
        end = System.currentTimeMillis();
        System.out.println("linkedList 尾插时长:" + (end - start));


        start = System.currentTimeMillis();
        for (int i = 0; i < size; i++) {
            ((LinkedList<Integer>) linkedList).addLast(i);
        }
        end = System.currentTimeMillis();
        System.out.println("linkedList 尾插时长:" + (end - start));

    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

输出结果,可以看到还是链表稍快一些,为什么arraylist这里性能也还不错呢?原因也很简单,无需为了插入一个节点维护其他位置。

arrayList 尾插时长:5
linkedList 尾插时长:2
linkedList 尾插时长:3
1
2
3

最后再来看看随机插入,为了公平实验,笔者将list初始化工作都放在计时之外,避免arrayList动态扩容的时间影响最终实验结果:

public static void main(String[] args) {
        int size = 10_0000;
        //填充足够量的数据
        ArrayList<Integer> arrayList = new ArrayList<>();
        for (int i = 0; i < size; i++) {
            arrayList.add(i);
        }
        //随机插入
        long begin = System.currentTimeMillis();
        for (int i = 0; i < size; i++) {
            arrayList.add(RandomUtil.randomInt(0, size), RandomUtil.randomInt());
        }
        long end = System.currentTimeMillis();
        System.out.println("arrayList随机插入耗时:" + (end - begin));

        //填充数据
        LinkedList<Integer> linkedList = new LinkedList<>();
        for (int i = 0; i < size; i++) {
            linkedList.add(i);
        }
        //随机插入
        begin = System.currentTimeMillis();
        for (int i = 0; i < size; i++) {
            linkedList.add(RandomUtil.randomInt(0, size), RandomUtil.randomInt());
        }
        end = System.currentTimeMillis();
        System.out.println("linkedList随机插入耗时:" + (end - begin));

    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

从输出结果来看,随机插入也是arrayList性能较好,原因也很简单,arraylist随机访问速度远远快与linklist:

arrayList随机插入耗时:748
linkedList随机插入耗时:27741
1
2

针对两者的性能差异,笔者也在这里进行一下简单的小结:

  1. 头插法,由于LinkedList节点维护只需管理原有头节点和新节点的关系,无需大费周章的调整整个地址空间,相较于ArrayList,它的表现会相对出色一些。
  2. 尾插法:和头插法类似,除非动态扩容,ArrayList无需进行大量的元素转移,所以大体上两者性能差异不是很大,总的来说linkedList 会稍胜一筹。
  3. 随机插入:ArrayList在进行元素定位时只需O(1)的时间复杂度,相较于LinkedList 需要全集合扫描来说,这些时间开销使得前者性能表现更加出色。

# ArrayList 和 Vector 的异同

这个问题我们可以从以下两个维度分析:

先来说说底层数据结构,两者底层存储都是采用数组,ArrayList存储用的是new Object[initialCapacity];

public ArrayList(int initialCapacity) {
		//给定容量后初始化定长数组存储元素
        if (initialCapacity > 0) {
            this.elementData = new Object[initialCapacity];
        } else if (initialCapacity == 0) {
            this.elementData = EMPTY_ELEMENTDATA;
        } else {
            throw new IllegalArgumentException("Illegal Capacity: "+
                                               initialCapacity);
        }
    }
1
2
3
4
5
6
7
8
9
10
11

Vector底层存储元素用的也是是 new Object[initialCapacity];,即一个对象数组:

public Vector(int initialCapacity, int capacityIncrement) {
        //......
		//基于定长容量初始化数组                                               
        this.elementData = new Object[initialCapacity];
        this.capacityIncrement = capacityIncrement;
    }
1
2
3
4
5
6

从并发安全角度来说,Vector 为线程安全类,ArrayList 线程不安全,如下所示我们使用ArrayList进行多线程插入出现的索引越界问题。

List<Integer> list = new ArrayList<>();


        CountDownLatch countDownLatch = new CountDownLatch(2);
        Thread t1 = new Thread(() -> {
            //插入1000个元素
            for (int i = 0; i < 1000; i++) {
                list.add(i);
            }
            //完成后按下倒计时门闩
            countDownLatch.countDown();
        });

        Thread t2 = new Thread(() -> {
            //插入1000个元素
            for (int i = 0; i < 1000; i++) {
                list.add(i);
            }
            //完成后按下倒计时门闩
            countDownLatch.countDown();
        });

        t1.start();
        t2.start();
        
        countDownLatch.await();
        System.out.println(list.size());
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

因为多线程访问的原因,底层索引不安全操作的自增,导致插入时得到一个错误的索引位置导致ArrayIndexOutOfBoundsException:

对应我们也给出ArrayList可能存在隐患的代码段,可以多线程情况下ensureCapacityInternal的预扩容到元素添加这段操作的size,很可能因为并发计算的线程不安全性导致各种异常:

public boolean add(E e) {
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        elementData[size++] = e;
        return true;
    }
1
2
3
4
5

相较之下,vector的add方法有加synchronized 关键字,保证单位时间内只有一个线程可以操作底层的数组,由此保证了有序集合操作的准确性:

//任何操作都是上锁的,保证一次插入操作互斥和原子性
 public synchronized boolean add(E e) {
        modCount++;
        ensureCapacityHelper(elementCount + 1);
        elementData[elementCount++] = e;
        return true;
    }
1
2
3
4
5
6
7

# ArrayList 与 LinkedList 的区别

从上文中我们基本可以了解两者区别,这里我们就做一个简单的小结:

  1. 底层存储结构:ArrayList 底层使用的是数组,LinkedList 底层使用的是链表
  2. 线程安全性:两者都是线程不安全,因为add方法都没有任何关于线程安全的处理。
  3. 随机访问性:虽然两者都支持随机访问,但是链表随机访问不太高效。
  4. 内存空间占用: ArrayList 的空间浪费主要体现在在 List列表的结尾会预留一定的容量空间,而 LinkedList 的空间花费则体现在它的每一个元素都需要消耗比 ArrayList 更多的空间(因为要存放直接后继和直接前驱以及数据)。

# ArrayList 的扩容机制

Java的ArrayList 底层默认数组大小为10,的动态扩容机制即ArrayList 确保元素正确存放的关键,了解核心逻辑以及如何基于该机制提高元素存储效率也是很重要的,感兴趣的读者可以看看读者编写的这篇博客:

尽管从上面来看两者各有千秋,但比较有趣的是,LinkedList的作者Josh Bloch基本没有用过这个集合:

# 详解Map集合

# Map接口定义概览

Map即映射集,是线上就是将对应的key映射到对应value上,由此构成一个数学上的映射的概念,该适合存储不可重复键值对类型的元素,key不可重复,value可重复,我们可以通过key找到对应的value:

An object that maps keys to values. A map cannot contain duplicate keys; each key can map to at most one value.

# HashMap(重点)

JDK1.8的HashMap默认是由数组+链表/红黑树组成,通过key算得hash寻址从而定位到Map底层数组的索引位置。 在进行put操作时,若冲突时使用拉链法解决冲突,如下面这段代码所示,当相同索引位置存储的是链表时,它会进行for循环定位到相同hash值的索引位置的尾节点进行追加。当链表长度大于8且数组长度大于64的情况下,链表会进行树化变成红黑树,减少元素搜索时间。 注意 : 若长度小于64链表长度大于8只会HashMap底层数组的扩容操作,对此我们给出Map进行put操作时将元素添加到链表结尾的代码段,可以看到当链表元素大于等于8时会尝试调用treeifyBin进行树化操作:

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
	//......
	for (int binCount = 0; ; ++binCount) {
					//遍历找到空位尝试插入
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        //如果链表元素加上本次插入元素大于8( TREEIFY_THRESHOLD )时,则尝试进行树化操作
                        if (binCount >= TREEIFY_THRESHOLD - 1) 
                            treeifyBin(tab, hash);
                        break;
                    }
                   //.......
                }
                //......
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

对应我们步入treeifyBin即可直接印证我们的逻辑,当底层数组空间小于64时只会进行数组扩容,而非针对当前bucket的树化操作:

final void treeifyBin(Node<K,V>[] tab, int hash) {
        int n, index; Node<K,V> e;
        //若当前数组空间小于64则直接会进行数组空间扩容
        if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
            resize();
        else if ((e = tab[index = (n - 1) & hash]) != null) {//反之进行树化操作
            TreeNode<K,V> hd = null, tl = null;
            do {
               //......
            } while ((e = e.next) != null);
            if ((tab[index] = hd) != null)
                hd.treeify(tab);
        }
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# 更多关于HashMap的知识

Java集合hashMap小结:https://blog.csdn.net/shark_chili3007/article/details/123241124 (opens new window)

# 更多关于LinkedHashMap

LinkedHashMap继承自HashMap,他在HashMap基础上增加双向链表,由于LinkedHashMap维护了一个双向链表来记录数据插入的顺序,因此在迭代遍历生成的迭代器的时候,是按照双向链表的路径进行遍历的,所以遍历速度远远快于HashMap,具体可以查阅笔者写的这篇文章:

Java集合LinkedHashMap小结:https://blog.csdn.net/shark_chili3007/article/details/107249595 (opens new window)

# 更多ConcurrentHashMap

Java并发容器小结 (opens new window)

# 详解Hashtable核心添加操作

Hashtable底层也是由数组+链表(主要用于解决冲突)组成的,操作时都会上锁,可以保证线程安全,底层数组是 Hashtable 的主体,在插入发生冲突时,会通过拉链法即将节点追加到同索引位置的节点后面:

public synchronized V put(K key, V value) {
        //......

       //插入元素寻址操作
        Entry<?,?> tab[] = table;
        int hash = key.hashCode();
        int index = (hash & 0x7FFFFFFF) % tab.length;
        //修改操作时,通过遍历对应index位置的链表完成覆盖操作
        @SuppressWarnings("unchecked")
        Entry<K,V> entry = (Entry<K,V>)tab[index];
        for(; entry != null ; entry = entry.next) {
            if ((entry.hash == hash) && entry.key.equals(key)) {
                V old = entry.value;
                entry.value = value;
                return old;
            }
        }
		//发生冲突时,会将节点追加到同索引位置的节点后面
        addEntry(hash, key, value, index);
        return null;
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

# 详解Set集合

# Set基本核心概念

Set集合不可包重复的元素,即如果两个元素在equals方法下判定为相等就只能存储其中一个,这意味着该集合也最多包含一个null的元素,它常用于一些需要进行去重的场景。

A collection that contains no duplicate elements. More formally, sets contain no pair of elements e1 and e2 such that e1.equals(e2), and at most one null element. As implied by its name, this interface models the mathematical set abstraction.

Set有两种我们比较熟悉的实现:

  1. HashSet:HashSet要求数据唯一,但是存储是无序的(底层通过Hash算法实现寻址),所以基于面向对象思想复用原则,Java设计者就通过聚合关系封装HashMap,基于HashMap的key实现了HashSet:
//HashSet底层复用了Map的put方法,value统一使用PRESENT对象
private static final Object PRESENT = new Object();

public boolean add(E e) {
		// 返回null说明当前插入时并没有覆盖相同元素
        return map.put(e, PRESENT)==null;
    }
1
2
3
4
5
6
7
  1. LinkedHashSet:LinkedHashSet即通过聚合封装LinkedHashMap实现的。

  2. TreeSet:TreeSet底层也是TreeMap,一种基于红黑树实现的有序树O(logN)级别的黑平衡树:

A Red-Black tree based NavigableMap implementation. The map is sorted according to the natural ordering of its keys, or by a Comparator provided at map creation time, depending on which constructor is used.

关于红黑树可以参考笔者之前写过的这篇文章:

数据结构与算法之红黑树小结:https://blog.csdn.net/shark_chili3007/article/details/108382322) (opens new window)

对应的从源码中我们也可以看出TreeSet底层是对TreeMap的复用:

public TreeSet() {
        this(new TreeMap<E,Object>());
    }
1
2
3

# HashMap 和 Hashtable 的区别(重点)

针对该问题,笔者建议从以下5个角度进行探讨:

从线程安全角度:HashMap 操作是线程不安全、Hashtable 是线程安全,这一点我们已经在上文中源码进行了相应的介绍,这里就不多做赘述了。

从底层数据结构角度:HashMap 初始情况是数组+链表,特定情况下会变数组+红黑树,Hashtable 则是数组+链表。

从保存数值角度:HashMap 允许null键或null值,而Hashtable则不允许null的value,这一点我们可以直接查看的Hashtable的put方法:

public synchronized V put(K key, V value) {
        // 如果value为空则抛出空指针异常
        if (value == null) {
            throw new NullPointerException();
        }
		//......
    }
1
2
3
4
5
6
7

从初始容量角度考虑:HashMap默认16,对此我们可以通过直接查看源码的定义印证这一点:

static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
1

同时HashMap进行扩容时都是基于当前容量*2,这一点我们可以直接通过resize印证:

  final Node<K,V>[] resize() {
      	//......
        if (oldCap > 0) {
            if (oldCap >= MAXIMUM_CAPACITY) {
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
            //基于原有数组容量*2得到新的数组空间完成map底层数组扩容
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                newThr = oldThr << 1; // double threshold
        }
		 Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
//......
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

Hashtable 默认的初始大小为 11,之后每次扩充,容量变为原来的 2n+1:

//初始化容量为11
 public Hashtable() {
        this(11, 0.75f);
    }

	//扩容方法
   protected void rehash() {
        int oldCapacity = table.length;
        Entry<?,?>[] oldMap = table;

        //扩容的容量基于原有空间的2倍+1
        int newCapacity = (oldCapacity << 1) + 1;
        //......
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

从性能角度考虑:Hashtable 针对冲突总是通过拉链法解决问题,长此以往可能会导致节点查询复杂度退化为O(n)相较于HashMap在达到空间阈值时通过红黑树进行bucket优化来说性能表现会逊色很多。

# HashMap 和 HashSet有什么区别

HashSet 聚合了HashMap ,通俗来说就是将HashMap 的key作为自己的值存储来使用:

//HashSet底层本质是通过内部聚合的map完成元素插入操作
public boolean add(E e) {
        return map.put(e, PRESENT)==null;
    }
1
2
3
4

# HashMap 和 TreeMap 有什么区别

类图如下,TreeMap 底层是有序树,所以对于需要查找最大值或者最小值等场景,TreeMap 相比HashMap更有优势。因为他继承了NavigableMap接口和SortedMap 接口。

如下源码所示,我们需要拿最大值或者最小值可以用这种方式或者最大值或者最小值

 Object o = new Object();
        //创建并添加元素
        TreeMap<Integer, Object> treeMap = new TreeMap<>();
        treeMap.put(3213, o);
        treeMap.put(434, o);
        treeMap.put(432, o);
        treeMap.put(2, o);
        treeMap.put(432, o);
        treeMap.put(31, o);
        //顺序打印
        System.out.println(treeMap);
        //拿到第一个key
        System.out.println(treeMap.firstKey());
        //拿到最后一个key
        System.out.println(treeMap.lastEntry());
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

输出结果:

{2=231, 31=231, 432=231, 434=231, 3213=231}
2
3213=231
1
2
3

# HashSet实现去重插入的底层工作机制了解嘛?

当你把对象加入HashSet时其底层执行步骤为:

  1. HashSet 会先计算对象的hashcode值定位到bucket。
  2. 若bucket存在元素,则与其hashcode 值作比较,如果没有相符的 hashcode,HashSet 会认为对象没有重复出现,直接允许插入了。
  3. 但是如果发现有相同 hashcode 值的对象,这时会调用equals()方法来检查 hashcode 相等的对象是否真的相同。
  4. 如果两者相同,HashSet就会将其直接覆盖返回插入前的值。

对此我们给出HashSet底层的去重的实现,本质上就算map的putVal方法:

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
       //......
       //bucet不存在元素则直接插入
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
            Node<K,V> e; K k;
            //若存在元素,且hash、key、equals相等则覆盖元素并返回旧元素
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
                for (int binCount = 0; ; ++binCount) {
                 	//......
                    //若存在元素,且hash、key、equals相等则覆盖元素并返回旧元素
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            //返回旧有元素的值
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
               	//......
                return oldValue;
            }
        }
       	//......
        return null;
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34

# HashSet、LinkedHashSet 和 TreeSet 使用场景

  1. HashSet:可在不要求元素有序但唯一的场景。
  2. LinkedHashSet:可用于要求元素唯一、插入或者访问有序性的场景,或者FIFO的场景。
  3. TreeSet:要求支持有序性且按照自定义要求进行排序的元素不可重复的场景。

# 优先队列PriorityQueue

关于优先队列的文章,笔者已将该文章投稿给了Javaguide,感兴趣的读者可以参考一下这篇文章:

https://github.com/Snailclimb/JavaGuide/blob/main/docs/java/collection/priorityqueue-source-code.md (opens new window)

# Java集合使用以及工具类小结

Java集合使用以及工具类小结 (opens new window)

# 一道比较偏门的校招笔试题

以下代码分别输出多少?

 List a=new ArrayList<String>();
        a.add(null);
        a.add(null);
        a.add(null);
        System.out.println(a.size());//3
        Map map=new HashMap();
        map.put("a",null);
        map.put("a",null);
        map.put("a",null);
        System.out.println(map.size());//1
1
2
3
4
5
6
7
8
9
10

# 小结

我是sharkchili,CSDN Java 领域博客专家,开源项目—JavaGuide contributor,我想写一些有意思的东西,希望对你有帮助,如果你想实时收到我写的硬核的文章也欢迎你关注我的公众号: 写代码的SharkChili,同时我的公众号也有我精心整理的并发编程、JVM、MySQL数据库个人专栏导航。

# 参考文献

Java集合常见面试题总结(上):https://javaguide.cn/java/collection/java-collection-questions-01.html#arraylist-简介 (opens new window)

ArrayList源码&扩容机制分析:https://javaguide.cn/java/collection/arraylist-source-code.html#arraylist-简介 (opens new window)

编辑 (opens new window)
上次更新: 2026/03/26, 01:05:31
来聊聊session与token的区别
Java常用集合类HashMap深度解析

← 来聊聊session与token的区别 Java常用集合类HashMap深度解析→

最近更新
01
基于EasyExcel实现高效导出
03-25
02
从开源框架中学习那些实用的位运算技巧
03-25
03
浅谈分布式架构设计思想和常见优化手段
03-25
更多文章>
Theme by Vdoing | Copyright © 2025-2026 Evan Xu | MIT License | 桂ICP备2024034950号 | 桂公网安备45142202000030
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式
×
×