禅与计算机 禅与计算机
首页
  • 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并发编程基础小结
    • 深入理解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技术原理
    • 探索JVM的隐秘角落:元空间详解
  • 深入理解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技术科普短视频制作全攻略
  • 元认知

    • 摩擦感:AI时代的写作自省
    • 从断墨寻径浅谈程序员的元学习能力
    • 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

    • 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并发编程基础小结
    • 深入理解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技术原理
    • 探索JVM的隐秘角落:元空间详解
  • 深入理解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技术科普短视频制作全攻略
  • 元认知

    • 摩擦感:AI时代的写作自省
    • 从断墨寻径浅谈程序员的元学习能力
    • 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

    • 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异常机制:从核心概念到最佳实践与底层原理
        • 什么是异常
        • 受检异常、非受检异常与Error的区别
        • 受检异常和非受检异常的区别
        • 受检异常:编译期强制处理
        • 非受检异常:运行时才暴露
        • 非受检异常的 throws 声明规则
        • throw与throws的区别
        • throw 用法
        • throws 用法
        • 核心区别总结
        • 异常常用方法和使用示例
        • 自定义异常最佳实践
        • try-catch运行原理
        • 异常抛出与捕获流程
        • 异常表(Exception Table)底层机制
        • 性能影响
      • Effective Java中关于异常的最佳实践
        • 在正确的场景使用异常
        • 受检异常和运行时异常的使用技巧
        • 避免非必要的受检异常
        • 优先使用标准异常
        • 合理抛出异常信息
        • 尽可能给出详尽异常信息
        • 保持异常原子性
        • 不要忽略异常
      • 异常使用注意事项
        • 多异常捕获处理技巧
        • finally 关键字详解
        • finally 不执行的场景
        • 不要在 finally 中使用 return
        • 资源释放的最佳实践
        • 异常不处理就抛出
        • 规范异常日志打印
        • 避免频繁抛出和捕获异常
        • 尽可能在for循环外捕获异常
      • 常见面试题解析
        • 为什么不建议使用异常控制业务流程
        • ClassNotFoundException和NoClassDefFoundError的区别
        • 受检异常和非受检异常的区别
      • 小结
      • 参考
    • 聊聊Java中的常用类String
    • 万字长文带你细聊Java注解本质
    • 来聊聊Java的反射机制
    • 深入解析 Java 泛型的魅力与机制
    • 来聊聊Java为什么只有值传递
    • 来聊聊大厂常问的SPI工作原理
    • 来聊聊session与token的区别
    • 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
2026-04-09
目录

Java异常:从原理到实践

# 写在文章开头

日常开发中频繁接触异常,但多数开发者对其底层机制和性能影响缺乏深入理解。对于 Java 异常技术,如果使用得当,异常可以提高程序的可读性、可维护性和健壮性,使用不当则会适得其反。

所以,本文将从异常的核心概念、最佳实践到底层原理,由浅入深地进行全面梳理和分析,希望对你有所帮助。

你好,我是 SharkChili ,Java Guide 核心维护者之一,对 Redis、Nightingale 等知名开源项目有深度源码研究经验。熟悉 Java、Go 等多语言技术栈,现任某知名黑厂高级研发。

🌟 开源项目贡献

  • mini-redis:教学级 Redis 精简实现,助力分布式缓存原理学习
    🔗 https://github.com/shark-ctrl/mini-redis (opens new window)(欢迎 Star & Contribute)

📚 公众号价值 分享企业级架构设计、性能优化、源码解析等核心技术干货,涵盖分布式系统、微服务治理、大数据处理等实战领域,并探索面向AI的vibe coding等现代开发范式。

👥 加入技术社群 关注公众号,回复 【加群】 获取联系方式,与众多技术爱好者交流分布式架构、微服务等前沿技术!

# 深度解析Java异常机制:从核心概念到最佳实践与底层原理

# 什么是异常

在Java中,"异常"通常指程序执行过程中发生的非正常情况。从异常机制角度,Java异常体系基于Throwable类,分为两大类:

  1. Exception:程序可以捕获并处理的异常
  2. Error:严重的系统级错误,一般不应捕获

本文重点讨论Java异常机制,尤其是Exception体系。

引用guide哥的图片,Java异常类体系结构如下图所示,整体来说分为三大类:

  1. checked exceptions:一些三方包方法签名上常会打上这些异常,告知调用者可能需要处理这些异常,需在代码中显式捕获或声明抛出。
  2. unchecked exceptions:运行时异常,程序运行期间才可能发生,编译器不强制要求处理。
  3. errors:一些比较严重的异常,它表示合理的应用程序不应尝试捕获的严重问题。

# 受检异常、非受检异常与Error的区别

前文提到,Java 异常体系基于 Throwable 类,整体分为三大类,下面分别说明。

Exception及其子类的异常对象通常是可以进行捕获和处理的。例如下面这段算术异常,我们就可以手动捕获并处理:

try {
    for (int i = 0; i < 30_0000; i++) {
        int num = 10 / 0;
        System.out.println("Fnum:" + num);
    }
} catch (Exception e) {
    System.out.println("outerTryCatch 报错");
}
1
2
3
4
5
6
7
8

从 Exception 的子类来看,又分为受检异常和非受检异常:

  1. 受检异常(checked):方法在调用时需要对可能抛出的潜在风险进行处理,要么捕获,要么向上抛出声明。
  2. 非受检异常(unchecked):运行时才可能知晓的异常,该异常可由开发人员结合业务场景确定是否捕获。

关于受检异常和非受检异常,后文将给出具体示例。

与Exception不同,Error表示严重的系统级错误,如OOM(内存溢出)、VirtualMachineError(虚拟机错误)、NoClassDefFoundError(类定义错误)等。这类错误通常导致应用程序崩溃,一般情况下不应捕获,而是需要定位并修复根本问题。

# 受检异常和非受检异常的区别

从编译期检查的角度,两者的核心区别如下:

维度 受检异常 非受检异常
编译期检查 强制处理,否则编译失败 不强制处理,编译通过
继承关系 继承 Exception(非 RuntimeException) 继承 RuntimeException
典型场景 外部资源操作、可恢复的业务异常 编程错误、不可恢复的运行时错误

# 受检异常:编译期强制处理

受检异常是指在方法签名中通过 throws 声明的异常,调用方必须在编译期显式处理(捕获或继续声明抛出)。这类异常通常表示可预见且可恢复的异常情况,例如:

  • 文件操作:FileNotFoundException
  • 网络通信:SocketTimeoutException
  • 数据库访问:SQLException

以 FileInputStream 为例,其构造方法声明了 FileNotFoundException:

public FileInputStream(String name) throws FileNotFoundException {
    this(name != null ? new File(name) : null);
}
1
2
3

调用时若不处理该异常,编译器将报错:

正确的处理方式如下:

// 方式一:try-catch 捕获处理
try {
    FileInputStream fis = new FileInputStream("config.txt");
    // 使用文件流...
} catch (FileNotFoundException e) {
    logger.error("配置文件不存在", e);
}

// 方式二:继续声明抛出
public void loadConfig() throws FileNotFoundException {
    FileInputStream fis = new FileInputStream("config.txt");
}
1
2
3
4
5
6
7
8
9
10
11
12

# 非受检异常:运行时才暴露

非受检异常是指 RuntimeException 及其子类,编译器不强制要求处理。这类异常通常表示编程错误或系统级问题,例如:

  • NullPointerException:空指针访问
  • IllegalArgumentException:非法参数
  • ArithmeticException:算术运算异常
  • ArrayIndexOutOfBoundsException:数组越界

以下代码编译期正常通过,但运行时抛出异常:

public class ArithmeticDemo {
    public static void main(String[] args) {
        int num = 10 / 0;  // 编译通过,运行时抛出 ArithmeticException
        System.out.println("结果:" + num);
    }
}
1
2
3
4
5
6

# 非受检异常的 throws 声明规则

需要特别说明的是,抛出 RuntimeException 或其子类时,方法签名上无需 throws 声明。这是 Java 语言规范的明确约定:

class Calculator {
    /**
     * 执行除法运算,对非法参数抛出运行时异常
     */
    int divide(int a, int b) {
        if (b < 0) {
            throw new IllegalArgumentException("除数不能为负数: " + b);
        }
        if (b == 0) {
            throw new ArithmeticException("除数不能为零");
        }
        return a / b;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

设计原理:根据《Effective Java》第71条,RuntimeException 代表编程错误或不可恢复的运行时条件,调用方通常无法有效恢复。因此,设计者选择不强制编译期处理,由开发人员根据实际场景决定是否捕获。

# throw与throws的区别

throw 和 throws 是 Java 异常处理机制中的两个关键字,虽然名称相似,但用途和语法完全不同:

对比维度 throw throws
位置 方法体内 方法签名上
作用 抛出异常对象 声明方法可能抛出的异常类型
后面跟什么 单个异常对象(new XxxException()) 一个或多个异常类名
执行时机 执行到该语句时立即抛出 编译期检查,告知调用者需处理
适用异常类型 受检异常和非受检异常均可 主要用于受检异常

# throw 用法

throw 用于在方法体内主动抛出异常对象:

public void setAge(int age) {
    if (age < 0) {
        throw new IllegalArgumentException("年龄不能为负数: " + age);
    }
    this.age = age;
}
1
2
3
4
5
6

# throws 用法

throws 用于在方法签名上声明可能抛出的受检异常:

// 声明单个异常
public void readFile(String path) throws FileNotFoundException {
    FileInputStream fis = new FileInputStream(path);
}

// 声明多个异常
public void loadData(String path) throws FileNotFoundException, IOException {
    FileInputStream fis = new FileInputStream(path);
    // 读取数据...
}
1
2
3
4
5
6
7
8
9
10

# 核心区别总结

  1. throw:是一个动作,用于在代码中主动抛出异常对象
  2. throws:是一个声明,用于告知调用者该方法可能抛出哪些异常,由调用者决定如何处理

注意:throws 声明的非受检异常(RuntimeException 及其子类)不会强制调用者处理,仅作为文档提示。

# 异常常用方法和使用示例

Throwable 类提供了多种获取异常信息的方法,开发者可根据日志需求选择:

方法 返回内容 是否含堆栈 适用场景
getMessage() 异常的简要描述 ❌ 仅需错误原因
toString() 异常类名 + 详细消息 ❌ 需要异常类型信息
getLocalizedMessage() 本地化的异常描述 ❌ 国际化场景
printStackTrace() 完整堆栈追踪 ✅ 调试和问题定位

以下面代码为例,对比各方法输出:

try {
    int result = 10 / 0;
} catch (ArithmeticException e) {
    System.out.println("getMessage: " + e.getMessage());
    System.out.println("toString: " + e.toString());
    System.out.println("getLocalizedMessage: " + e.getLocalizedMessage());
    System.out.println("printStackTrace:");
    e.printStackTrace();
}
1
2
3
4
5
6
7
8
9

输出结果:

getMessage: / by zero
toString: java.lang.ArithmeticException: / by zero
getLocalizedMessage: / by zero
printStackTrace:
java.lang.ArithmeticException: / by zero
    at com.example.Main.main(Main.java:5)
1
2
3
4
5
6

实践建议:生产环境推荐使用日志框架(如 SLF4J)记录异常,将异常对象作为最后一个参数传入,自动打印完整堆栈:logger.error("操作失败,参数: {}", param, e);

# 自定义异常最佳实践

上文介绍了受检异常的基本用法,这里补充自定义异常的设计建议:

推荐做法:提供包含详细信息的构造方法,而非仅重写 getMessage():

// 推荐:提供丰富的构造方法
public class BusinessException extends Exception {
    private final String errorCode;
    
    public BusinessException(String errorCode, String message) {
        super(message);
        this.errorCode = errorCode;
    }
    
    public BusinessException(String errorCode, String message, Throwable cause) {
        super(message, cause);
        this.errorCode = errorCode;
    }
    
    public String getErrorCode() {
        return errorCode;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

使用示例:

public void withdraw(Long userId, BigDecimal amount) throws BusinessException {
    BigDecimal balance = accountService.getBalance(userId);
    if (balance.compareTo(amount) < 0) {
        throw new BusinessException("INSUFFICIENT_BALANCE", 
            "余额不足,当前余额: " + balance + ", 提现金额: " + amount);
    }
    // 执行提现逻辑...
}
1
2
3
4
5
6
7
8

这样设计的好处:

  1. 异常信息包含业务上下文(错误码、具体数值)
  2. 支持异常链传递(cause 参数)
  3. 便于上层统一处理和国际化

了解了异常的基本使用后,我们来看看 try-catch 在 JVM 层面是如何工作的。

# try-catch运行原理

从 JVM 层面理解异常处理机制,有助于编写更高效的代码。

# 异常抛出与捕获流程

当 try 块中抛出异常时,JVM 的处理流程如下:

  1. 创建异常对象:执行 throw 语句时,JVM 在堆上创建对应的异常对象
  2. 遍历异常表:JVM 查询当前方法的异常表,寻找匹配的 catch 块
  3. 栈帧展开:若当前方法无匹配项,JVM 沿调用栈向上查找,直到找到匹配的 catch 块或程序终止

# 异常表(Exception Table)底层机制

编译后的字节码中包含一张异常表,记录每个 catch 块的捕获范围和异常类型。以下面的代码为例:

public class ExceptionDemo {
    public static void main(String[] args) {
        int a = 10, b = 0;
        try {
            int result = a / b;               // try 块
        } catch (ArithmeticException e) {
            System.out.println("除数不能为零");  // catch 块
        } finally {
            System.out.println("清理资源");     // finally 块
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12

使用 javap -v ExceptionDemo 查看字节码,可以看到如下异常表:

Exception table:
    from    to  target type
        5     9    20   Class java/lang/ArithmeticException
        5     9    40   any
       20    29    40   any
       40    42    40   any
1
2
3
4
5
6

异常表字段含义:

字段 含义
from 监控范围的起始字节码偏移量
to 监控范围的结束偏移量(不包含)
target 异常处理代码的起始偏移量
type 捕获的异常类型(any 表示任意异常)

逐行解读异常表:

行号 监控范围 目标位置 类型 含义
1 5-9 20 ArithmeticException try 块内抛出 ArithmeticException,跳转到 catch 块(偏移量 20)
2 5-9 40 any try 块内抛出其他异常,跳转到 finally 块(偏移量 40)
3 20-29 40 any catch 块内抛出异常,跳转到 finally 块
4 40-42 40 any finally 块自身异常的处理(防止无限递归)

从这个异常表可以看出 finally 的实现本质:JVM 为 try 块和 catch 块都生成了指向 finally 的异常表条目,确保无论是否发生异常、是否捕获异常,finally 块都会执行。

# 性能影响

理解异常表机制后,可以明确异常处理的性能开销来源:

  1. 创建异常对象:需填充堆栈轨迹,涉及 StackTraceElement 数组创建
  2. 遍历异常表:虽然是 O(n) 操作,但需在异常发生时执行
  3. 栈帧展开:沿调用栈查找匹配项,调用层次越深开销越大

因此,异常机制不适合用于正常的控制流,应仅用于真正的异常情况。

# Effective Java中关于异常的最佳实践

# 在正确的场景使用异常

明确java异常基本概念之后,我们再来聊聊异常的一些最佳实践。按照《 Effective Java》一书中的说法,设计者明确说明异常机制应用于异常情况,而不是用于普通的控制流。

例如:开发人员采用循环+指针递进的方式进行迭代,这完全是一种基于错误的推理得出的错误优化的尝试。

int[] arr = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
int i = 0;

try {
    while (true) {
        int num = arr[i++];
        //do something
    }
} catch (ArrayIndexOutOfBoundsException e) {
    //do something
}
1
2
3
4
5
6
7
8
9
10
11

其推理逻辑为:因为JVM虚拟机在获取元素时会对数组进行越界测试,所以对于循环终止条件测试就是多余的,应该在代码层面上避免。例如下面这段经典的for-each循环,JVM底层会将其编译为for-range循环:

// foreach 底层实际编译为:
// for (int i = 0; i < arr.length; i++) { int i1 = arr[i]; ... }
for (int i1 : arr) {
    //do something
}
1
2
3
4
5

对应for-each编译优化可通过解析字节码印证:

L1
    ALOAD 1          // 加载数组 arr
    ASTORE 2
    ALOAD 2
    ARRAYLENGTH      // 获取数组长度
    ISTORE 3         // 存储长度到变量3
    ICONST_0
    ISTORE 4         // int i = 0 (计数器)

L2
ILOAD 4          // 加载 i
ILOAD 3          // 加载长度
IF_ICMPGE L3     // if i >= length 跳转到 L3 (结束循环)

ALOAD 2          // 加载数组
ILOAD 4          // 加载 i
IALOAD           // arr[i]
ISTORE 5         // int i1 = arr[i]

...              // 循环体代码

IINC 4 1         // i++
GOTO L2          // 跳回 L2 继续循环
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

而指针递进+while迭代的代码设计就是考虑到JVM底层存在数组访问越界的检测,所以采用了激进的异常处理控制流的运转。

指针递进+while迭代的反面案例,展示了 try-catch 包裹的数组访问循环

这其中是一种过度 "优化" 的设计,它存在以下几个弊端:

  1. 抑制JIT优化:将代码放在try-catch块内,抑制了JVM某些可能的优化操作,使得程序执行性能下降
  2. 低效的迭代效率:异常机制是针对异常情况设计,JVM设计者没有什么动力将其优化得像显示条件测试(i < arr.length)一样高效,这一点笔者也会在后续篇幅中给出实验结果。
  3. 冗余操作:采用标准语法的数组迭代未必会导致多余的检查,许多JVM实现会将终止检查操作优化掉。

所以,最佳实践是正确识别异常使用的场景,即对于逻辑流的控制优先采用合理的API处理,而非捕获异常手动控制流的运转:

 List<User> list = Arrays.asList(new User("name", 18),
                new User("name2", 19));
        for (User user : list) {
            //do something
        }
1
2
3
4
5

为了更直观地说明异常控制流程的性能问题,我们再看一个 Stack 出栈的性能对比实验:用 try-catch 控制循环 vs 使用标准 for 循环控制:

public static void stackPopByCatch() {
    long start = System.currentTimeMillis();
    try {
        Stack stack = new Stack();
        for (int i = 0; i < 1000_0000; i++) {
            stack.push(i);
        }
        while (true) {
            try {
                stack.pop();
            } catch (Exception e) {
                System.out.println("出栈结束");
                break;
            }
        }
    } catch (Exception e) {
    }
    long end = System.currentTimeMillis();
    System.out.println("使用try进行异常捕获,执行时间:" + (end - start));

    start = System.currentTimeMillis();
    Stack stack2 = new Stack();
    for (int i = 0; i < 1000_0000; i++) {
        stack2.push(i);
    }
    int size = stack2.size();
    for (int i = 0; i < size; i++) {
        stack2.pop();
    }
    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
22
23
24
25
26
27
28
29
30
31
32

输出结果,可以看到用 try 块控制业务流程性能相差近一倍:

出栈结束
使用try进行异常捕获,执行时间:2613
使用逻辑进行出栈操作,执行时间:1481
1
2
3

补充说明:上述性能测试仅使用 System.currentTimeMillis() 粗略计时,结果受 JVM 预热、系统负载等因素影响。如需更精确的基准测试数据,建议使用 JMH(Java Microbenchmark Harness)框架进行测量。

# 受检异常和运行时异常的使用技巧

异常体系存在check exception和runtime exception以及error三大体系,正确的理解并在合适的场景使用这些异常有助于我们编写出健壮且易于维护的程序。

我们更多的是的讨论受检异常(check exception)和非受检异常(runtime exception),就笔者个人经验而言,二者并没有很明显的区分,使用建议如下:如果调用者能够从异常中恢复,就使用受检异常。

例如下面这段代码,即开发者明确知晓网络的不可靠性,在编写创建连接的方法时抛出超时异常,外部调用者可以利用方法签名进行重连:

public static void main(String[] args) {
        int retryCount = 0;
        while (retryCount < MAX_RETRY_COUNT) {
            retryCount++;
            try {
                Socket socket = createConnection(HOST, PORT);
                System.out.println("连接成功!");
                // 使用socket进行通信...
                break;
            } catch (SocketTimeoutException e) { //捕获超时的受检异常,进行重试
                System.err.println("第 " + retryCount + " 次连接超时");
                if (retryCount >= MAX_RETRY_COUNT) {
                    System.err.println("达到最大重试次数,放弃连接");
                }
            }
        }
    }

    /**
     * 创建连接,超时抛出异常
     */
    public static Socket createConnection(String host, int port) throws SocketTimeoutException {
        Socket socket = new Socket();
        try {
            socket.connect(new InetSocketAddress(host, port), CONNECT_TIMEOUT);
            return socket;
        } catch (SocketTimeoutException e) {
            throw e;
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
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

对于error和runtime exception而言,二者在某些行为上是相同的:它们都不需要,也不应该捕获。如果程序抛出运行时异常亦或者错误,这就意味代码实现逻辑或者程序在运行时出现了不可恢复的错误,继续执行只会弊大于利,应抛出适当的错误信息并停止。

# 避免非必要的受检异常

受检异常是针对可以感知,且强制程序员处理的问题,适时的使用可以有效提升程序的健壮性。但是过度的使用受检异常,会成为某些API操作的阻碍。最典型的案例就是JDK 8中针对受检异常的方法,无法在流操作中使用:

public static void main(String[] args) {
        List<User> list = Arrays.asList(new User("Alice", 25), new User("Bob", 30));

        List<Map<String, Object>> mapList = list.stream()
                .map(user -> convertToMap(user)) //抛出受检异常,无法通过编译
                .collect(Collectors.toList());

        //do something
    }

    public static Map<String, Object> convertToMap(User user) throws Exception {
        if (user == null) {
            throw new Exception("User cannot be null");
        }
        //......

        Map<String, Object> map = new HashMap<>();
        map.put("name", user.getName());
        map.put("age", user.getAge());
        return map;
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

所以,对于受检异常的使用,我们建议应在满足以下两个条件的情况下使用: 1. 异常条件无法通过正确的使用API防止,例如某些远程调用无法用optional进行封装判空 2. 一旦出现异常,程序员可以采用有效动作恢复状态

如果无法满足上述两个条件,我们就应该使用非受检异常。

如果方法仅存在一个受检异常能够准确处理的场景下,我们要学会尽可能避免使用受检异常,确保顺畅使用流操作。

上述的流操作,结合业务推理可看出,是针对有效用户数据的类型转换,针对非合法的空对象抛出异常让上层着手处理。实际上针对该逻辑,我们完全可以通过返回Optional实例让上层调用者感知转换是否成功。

修改后的代码如下所示,大体思路为:

  1. 将受检异常转为Optional受检状态,若转换成功则存入map实例,反之存入null
  2. 上层调用者不再处理受检异常,取而代之使用optional检测状态
  3. 通过filter过滤生成有效实例

这样做不仅避免了处理异常的开销,还保证了流操作的可读性和可维护性:

List<Map<String, Object>> mapList = list.stream()
    .map(user -> convertToMap(user).orElse(null)) // 利用optional感知实例转换有效性
    .filter(Objects::nonNull) //有效过滤
    .collect(Collectors.toList());

public static Optional<Map<String, Object>> convertToMap(User user) {
    if (user == null) {
        return Optional.empty();
    }
    if (user.getName() == null || user.getName().trim().isEmpty()) {
        return Optional.empty();
    }
    if (user.getAge() < 0 || user.getAge() > 150) {
        return Optional.empty();
    }

    Map<String, Object> map = new HashMap<>();
    map.put("name", user.getName());
    map.put("age", user.getAge());
    return Optional.of(map);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

# 优先使用标准异常

计算机程序设计一直是一门讲究复用的学科,异常使用亦是同理,Java类库内置了许多高度抽象的标准异常,可以满足大多数API抛出异常的需求。

下面笔者就结合一些实际的场景给出相应异常使用实践,针对参数合法性校验,推荐采用IllegalArgumentException向上传递:

public static void validateUser(User user) {

    if (user.getName() == null || user.getName().trim().isEmpty()) {
        throw new IllegalArgumentException("用户名不能为空");
    }
    if (user.getAge() == null) {
        throw new IllegalArgumentException("年龄不能为空");
    }
}
1
2
3
4
5
6
7
8
9

针对非法的状态示例,例如:对象尚未初始化、对象状态异常不符合业务操作要求,应统一抛出IllegalStateException:

public static void checkUserState(User user) {
    if (user.isDeleted()) {
        throw new IllegalStateException("用户已注销,无法执行此操作");
    }

    updateUser(user);
}
1
2
3
4
5
6
7

还有一些可复用的异常是ConcurrentModificationException,该异常主要是针对以下两种场景:

  1. 对象被设计为供单线程使用,不允许并发修改
  2. 存在同步操作机制

该异常最典型的应用就是ArrayList,该容器是典型的单线程操作容器,其底层通过在进行读写操作时都会采用一个变量modCount来感知是否存在并发操作,例如:当我们进行迭代操作时,forEach函数的执行逻辑为:

  1. 创建一个不可变的变量expectedModCount拷贝modCount当前的值,作为后续的校验快照
  2. 比对实例变量modCount与expectedModCount是否一致,若一致则说明遍历期间,容器没有被修改,若不一致则进入步骤3
  3. 说明当前容器被其他线程修改,存在一致性问题,抛出ConcurrentModificationException

注意ConcurrentModificationException充其量只是一个提示,它无法有效的阻断并发修改操作:

因为ConcurrentModificationException在日常工程实践中比较少用到,所以笔者索性从JDK源代码ArrayList的forEach中作为最佳实践的案例,逻辑和上述说明一致:

  1. 创建不可变参数expectedModCount拷贝全局示例modCount快照
  2. 比对二者差异,感知是否存在并发修改
  3. 出现不一致,则跳出循环抛出ConcurrentModificationException
@Override
    public void forEach(Consumer<? super E> action) {
        Objects.requireNonNull(action);
        //拷贝modCount快照
        final int expectedModCount = modCount;
        @SuppressWarnings("unchecked")
        final E[] elementData = (E[]) this.elementData;
        final int size = this.size;
        //比对快照和全局变量是否不一致,以确认是否存在并发修改操作
        for (int i=0; modCount == expectedModCount && i < size; i++) {
            action.accept(elementData[i]);
        }
        if (modCount != expectedModCount) {
            throw new ConcurrentModificationException();
        }
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

基于ConcurrentModificationException,我们再给出一些不错的实践。上文说到ConcurrentModificationException无法杜绝并发修改。所以,对于需要阻断并发操作亦或者说要阻断非法操作的,可参考Arrays.asList方法底层的add方法的实现。针对所有非法写操作直接在逻辑层面抛出UnsupportedOperationException:

public void add(int index, E element) {
        throw new UnsupportedOperationException();
    }
1
2
3

其他方面例如空校验异常,则直接采用NullPointerException,这里就不多做赘述:

if (key == null)
    throw new NullPointerException();
1
2

标准异常使用场景总结:

场景 异常类型
参数合法性校验 IllegalArgumentException
状态合法性校验 IllegalStateException
并发修改检测 ConcurrentModificationException
阻断非法操作 UnsupportedOperationException
空值校验 NullPointerException

# 合理抛出异常信息

异常应结合当前方法抽象维度进行转译,并抛出正确的异常信息,确保调用者能够理解异常。

举个不是很规范的例子:我们编写一个访问列表操作的getElement方法,在执行元素返回操作时,底层的数据结构可能会返回越界异常。默认情况下会抛出IndexOutOfBoundsException:

 public static String getElement(List<String> list, int index) {
        return list.get(index);//可能抛出IndexOutOfBoundsException
    }
1
2
3

对于我们当前方法的抽象维度即元素读取操作,所以我们应针对该异常做一个特殊的语义上的转换。例如:我们用链表实现一个栈,在pop操作时,存在NoSuchElementException的风险。考虑到该异常不符合当前API层面即pop操作的语义抽象,最佳实践则是将其转译为自定义StackEmptyException,让上层明白该异常原因是栈为空:

 public E pop() throws StackEmptyException {
            try {
                return list.removeFirst();
            } catch (NoSuchElementException e) {//将链表的NoSuchElementException异常转换为自定义的StackEmptyException异常
                throw new StackEmptyException("Stack is empty");
            }
        }
1
2
3
4
5
6
7

# 尽可能给出详尽异常信息

抛出异常的目的,是为了了解程序的情况,并采用合适的策略解决问题。对于异常的故障信息,我们应详细提供导致该异常的参数和值。例如:JDK底层对于列表越界访问异常,就会在IndexOutOfBoundsException构造函数基础之上给出非法访问的索引和当前列表大小信息:

 private void rangeCheck(int index) {
            if (index < 0 || index >= this.size)
                throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
        }

     
        private String outOfBoundsMsg(int index) {
            return "Index: "+index+", Size: "+this.size;
        }
1
2
3
4
5
6
7
8
9

需要特别注意:异常的堆栈轨迹信息可能会被很多人看到,因此不要将密码、密钥等重要的信息包含在异常详细信息中。

# 保持异常原子性

抛出异常时,应确保当前逻辑状态的原子性,即异常前后状态是一致的。例如:ArrayBlockingQueue要求添加的元素是非空的,所以为了保证处理异常后,队列内部原子性,会在添加操作前检查元素状态,提前抛出异常,避免队列状态原子性遭到破坏:

 public boolean offer(E e) {
        //fail-fast提前感知异常并抛出,保证异常操作前后的原子性
        checkNotNull(e);
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            // 添加元素逻辑...
            return true;
        } finally {
            lock.unlock();
        }
    }

 private static void checkNotNull(Object v) {
    if (v == null)
        throw new NullPointerException();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

所以,对于异常原子性问题,核心目的就是要做到抛出异常前后状态一致。对于这种异常原子性的处理,常见的方案有4种: 1.快速失败: 提前检查感知并抛出,不修改当前状态,参考上文ArrayBlockingQueue的offer操作。

2.写时复制(copy on write) 拷贝当前处理对象信息,若出现异常直接抛出,不破坏当前对象的状态,参考CopyOnWriteArrayList的add方法,其内部会通过拷贝原数组的方式执行修改操作,只有明确操作成功的情况下才会原子覆盖旧有对象:

 public boolean add(E e) {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            Object[] elements = getArray();
            int len = elements.length;
            //基于原数组拷贝一个新的数组,执行修改操作
            Object[] newElements = Arrays.copyOf(elements, len + 1);
            newElements[len] = e;
            //明确操作成功后原子覆盖
            setArray(newElements);
            return true;
        } finally {
            lock.unlock();
        }
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

3. 手动回滚: 这种做法相对少见,也不太推荐,即在逻辑执行发生异常后将修改的状态还原。例如下面这段这段针对user对象的操作,在业务节点执行过程中出现异常,开发者手动将状态还原并返回:

/**
     * 更新用户信息,异常时回滚修改
     * @param user 待更新的用户对象
     * @param newName 新用户名
     * @param newAge 新年龄
     */
    public static void updateUserWithRollback(User user, String newName, Integer newAge) {
        // 保存原始值,用于异常时回滚
        String oldName = user.getName();
        Integer oldAge = user.getAge();

        try {
            // 修改用户信息
            user.setName(newName);
            user.setAge(newAge);

            // 执行后续业务逻辑
            doSomething();

        } catch (Exception e) {
            // 发生异常,回滚到原始值
            user.setName(oldName);
            user.setAge(oldAge);
            // 重新抛出异常,让上层处理
            throw e;
        }
    }
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

4. 不可变对象: 通过对象不可变性,确保异常处理状态前后的一致性,参考String的replace方法。因为String设计的不可变性,对于替换操作都是采用创建新对象的方式完成修改:

public String replace(char oldChar, char newChar) {
        if (oldChar != newChar) {
            int len = value.length;
            //......
            if (i < len) {
            		//基于原字符长度创建新数组
                char buf[] = new char[len];
                //拷贝原字符串信息
                for (int j = 0; j < i; j++) {
                    buf[j] = val[j];
                }
                //执行替换操作,并返回修改后的全新对象
                while (i < len) {
                    char c = val[i];
                    buf[i] = (c == oldChar) ? newChar : c;
                    i++;
                }
                return new String(buf, true);
            }
        }
        return this;
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

# 不要忽略异常

这一点其实是众所周知的一些定论了,受检异常势必是要求调用者手动进行相应兜底处理。唯一需要补充说明的是,如果明确要忽略受检异常处理,我们需要怎么做?

按照代码整洁之道的说法:

声明的变量、函数或者类的名称在阅读时,应能够交待所有的问题,它能够告诉你为什么存在、做什么事、该怎么用。如果一个名称需要注释补充说明,那就不算名副其实。

所以,对于可忽略的异常,我们也应该在变量声明时体现可忽略,同时在逻辑代码块中注释说明这样做为什么是合适的。例如:我们通过TimeUnit封装了一个线程休眠方法yield。按照TimeUnit设计者的考量,线程会因为中断操作而结束休眠,所以会抛出InterruptedException受检异常。

但是我们的场景仅仅是基于sleep操作让出CPU时间片,且所有中断操作都被重写覆盖,不存在这种情况。在明确异常无需处理的情况下,catch块标注异常可忽略,并通过注释说明原因,提升程序可读性和可维护性:

public static void yield(long time, TimeUnit unit) {
        try {
            unit.sleep(time);
        } catch (InterruptedException ignored) {
            //所有线程中断操作都被阻断,不存在中断异常
        }
    }
1
2
3
4
5
6
7

# 异常使用注意事项

# 多异常捕获处理技巧

多异常捕获应遵循以下原则:

  1. 有几个异常就处理几个异常,无法处理则向上抛出
  2. 父类 Exception 放在最下方
  3. 使用 | 运算符合并相同处理逻辑的异常

假设方法抛出多个异常,其中 UnknownHostException 是用户配置问题无法处理,应向上抛出:

private int calculate(int number, int divNum) 
        throws ArithmeticException, FileNotFoundException, UnknownHostException, IOException {
    if (divNum == 0) {
        throw new ArithmeticException();
    }
    return number / divNum;
}
1
2
3
4
5
6
7

使用 | 运算符合并相同处理逻辑的异常,使代码更简洁:

int number = 20;
int result = 0;
try {
    result = calculate(number, 0);
} catch (ArithmeticException | FileNotFoundException e) {
    logger.error("计算出错或文件不存在,参数:{}, 错误:{}", number, e.getMessage(), e);
} catch (IOException e) {
    logger.error("IO异常,参数:{}, 错误:{}", number, e.getMessage(), e);
} catch (Exception e) {
    logger.error("未知异常,参数:{}, 错误:{}", number, e.getMessage(), e);
}
1
2
3
4
5
6
7
8
9
10
11

注意:使用 | 运算符后,异常变量 e 会变为 final,无法重新赋值。

# finally 关键字详解

finally 块无论是否发生异常,在 try 块结束后必定执行。

# finally 不执行的场景

以下情况 finally 不会执行:

  1. 调用 System.exit(0) 或 Runtime.halt()
  2. JVM 崩溃(如内存不足)
  3. 守护线程结束时
public void finallyNotRun() {
    try {
        System.out.println("try block");
        System.exit(0);  // finally 不会执行
    } finally {
        System.out.println("finally block");
    }
}
1
2
3
4
5
6
7
8

# 不要在 finally 中使用 return

finally 中的 return 会覆盖 try/catch 的返回值:

public int func() {
    try {
        return 1;  // 被覆盖
    } finally {
        return 2;  // 实际返回值
    }
}
1
2
3
4
5
6
7

# 资源释放的最佳实践

传统方式需要在 finally 中手动释放资源:

Scanner scanner = null;
try {
    scanner = new Scanner(new File("read.txt"));
    // 使用 scanner...
} finally {
    if (scanner != null) {
        scanner.close();
    }
}
1
2
3
4
5
6
7
8
9

推荐使用 try-with-resources 语法,代码更简洁:

try (Scanner scanner = new Scanner(new File("read.txt"))) {
    // 使用 scanner...
} catch (FileNotFoundException e) {
    logger.error("文件不存在", e);
}
1
2
3
4
5

try-with-resources 适用于所有实现了 AutoCloseable 或 Closeable 接口的类。

# 异常不处理就抛出

捕获异常是为了处理它,不应捕获后什么都不做。如果当前层无法处理,应向上抛出:

public class MyIllegalArgumentException extends Exception {
    public MyIllegalArgumentException(String msg) {
        super(msg);
    }
}

private void check(String str) throws MyIllegalArgumentException {
    if (str == null || str.isEmpty()) {
        throw new MyIllegalArgumentException("字符串不可为空");
    }
}
1
2
3
4
5
6
7
8
9
10
11

最外层调用者必须处理异常,转化为用户可理解的提示。

# 规范异常日志打印

日志打印应遵循以下原则:

  1. 不要使用 JSON 工具序列化对象,某些 get 方法可能会抛出异常
  2. 记录参数、错误信息、堆栈信息
public void logShowTest() {
    Map inputParam = new JSONObject().fluentPut("key", "value");
    try {
        logShow(inputParam);
    } catch (ArithmeticException e) {
        logger.error("inputParam:{} ,errorMessage:{}", inputParam.toString(), e.getMessage(), e);
    }
}

private int logShow(Map inputParam) throws ArithmeticException {
    int zero = 0;
    if (zero == 0) {
        throw new ArithmeticException();
    }
    return 19 / zero;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

输出结果:

[main] ERROR com.guide.exception.ExceptionTest - inputParam:{"key":"value"} ,errorMessage:null
com.guide.exception.ArithmeticException
    at com.guide.exception.ExceptionTest.logShow(ExceptionTest.java:166)
1
2
3

# 避免频繁抛出和捕获异常

如下代码所示,可以看到频繁抛出和捕获异常是非常耗时的,所以我们不建议使用异常来作为处理逻辑,我们完全可以和前端协商好错误码从而避免没必要的性能开销:

private int testTimes;

public ExceptionTest(int testTimes) {
    this.testTimes = testTimes;
}

public void newObject() {
    long l = System.currentTimeMillis();
    for (int i = 0; i < testTimes; i++) {
        new Object();
    }
    System.out.println("建立对象:" + (System.currentTimeMillis() - l));
}

public void newException() {
    long l = System.currentTimeMillis();
    for (int i = 0; i < testTimes; i++) {
        new Exception();
    }
    System.out.println("建立异常对象:" + (System.currentTimeMillis() - l));
}

public void catchException() {
    long l = System.currentTimeMillis();
    for (int i = 0; i < testTimes; i++) {
        try {
            throw new Exception();
        } catch (Exception e) {
        }
    }
    System.out.println("建立、抛出并接住异常对象:" + (System.currentTimeMillis() - l));
}

public void catchObj() {
    long l = System.currentTimeMillis();
    for (int i = 0; i < testTimes; i++) {
        try {
            new Object();
        } catch (Exception e) {
        }
    }
    System.out.println("建立,普通对象并catch:" + (System.currentTimeMillis() - l));
}

public static void main(String[] args) {
    ExceptionTest test = new ExceptionTest(100_0000);
    test.newObject();
    test.newException();
    test.catchException();
    test.catchObj();
}
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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51

输出结果:

建立对象:3
建立异常对象:484
建立、抛出并接住异常对象:539
建立,普通对象并catch:3
1
2
3
4

# 尽可能在for循环外捕获异常

上文提到 try-catch 时会捕获并创建异常对象,所以如果在 for 循环内频繁捕获异常会创建大量的异常对象:

public static void main(String[] args) {
    outerTryCatch();
    innerTryCatch();
}

// for 外部捕获异常
private static void outerTryCatch() {
    long memory = Runtime.getRuntime().freeMemory();
    try {
        for (int i = 0; i < 30_0000; i++) {
            int num = 10 / 0;
            System.out.println("Fnum:" + num);
        }
    } catch (Exception e) {
    }
    long useMemory = (memory - Runtime.getRuntime().freeMemory()) / 1024 / 1024;
    System.out.println("cost memory:" + useMemory + "M");
}

// for 内部捕获异常
private static void innerTryCatch() {
    long memory = Runtime.getRuntime().freeMemory();
    for (int i = 0; i < 30_0000; i++) {
        try {
            int num = 10 / 0;
            System.out.println("num:" + num);
        } catch (Exception e) {
        }
    }
    long useMemory = (memory - Runtime.getRuntime().freeMemory()) / 1024 / 1024;
    System.out.println("cost memory:" + useMemory + "M");
}
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

输出结果如下,可以看到 for 循环内部捕获异常消耗了 22M 的堆内存。原因很简单,for 外部捕获异常时,会直接终止 for 循环,而在 for 循环内部捕获异常仅结束本次循环,所以如果 for 循环频繁报错,那么在内部捕获异常会创建大量的异常对象。

cost memory:0M
cost memory:22M
1
2

# 常见面试题解析

# 为什么不建议使用异常控制业务流程

  1. 《Effective Java》指出,创建异常并抛出的代价昂贵,涉及堆栈轨迹填充、异常对象创建、异常捕获等操作
  2. 异常旨在处理非正常情况,而非用于控制业务流程

# ClassNotFoundException和NoClassDefFoundError的区别

对比项 ClassNotFoundException NoClassDefFoundError
类型 受检异常 错误
触发场景 运行时加载类找不到 类文件存在但加载/解析失败
典型案例 Class.forName("com.mysql.cj.jdbc.Driver") 编译时存在,运行时 class 文件丢失

# 受检异常和非受检异常的区别

对比项 受检异常 非受检异常
编译期检查 强制处理 不强制
代表类型 IOException、SQLException NullPointerException、IllegalArgumentException
使用场景 可恢复的异常 代码错误导致的异常

📝 选择题练习

36. 以下哪个是检查型异常(Checked Exception)?

A. NullPointerException
B. ArrayIndexOutOfBoundsException
C. IOException
D. RuntimeException
1
2
3
4

答案:C ✅


37. 以下关于throw和throws的说法,正确的是?

A. throw用于抛出异常对象,throws用于声明方法可能抛出的异常
B. throw用于声明异常,throws用于抛出异常对象
C. throw和throws的作用完全相同
D. throws可以单独使用,不需要throw配合
1
2
3
4

答案:A ✅


38. 下面代码的输出是什么?

public class Test {
    public static void main(String[] args) {
        try {
            System.out.print("A ");
            int x = 1 / 0;
            System.out.print("B ");
        } catch (ArithmeticException e) {
            System.out.print("C ");
        } finally {
            System.out.print("D ");
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
A. A B C D
B. A B D
C. A C D
D. A C
1
2
3
4

答案:C ✅

解析:输出A → 执行1/0抛出异常 → B不输出 → 捕获异常输出C → finally执行输出D


39. 以下关于finally块的说法,正确的是?

A. finally块总是会执行
B. finally块在return之前执行
C. 如果finally块中有return语句,会覆盖try中的return
D. 以上全部正确
1
2
3
4

答案:B、C ✅

注意:A选项错误,调用 System.exit(0) 或 Runtime.halt() 时finally不会执行。因此D选项也错误。


40. 以下关于自定义异常的说法,正确的是?

A. 自定义异常必须直接继承Exception类
B. 自定义异常可以继承Exception类或RuntimeException类
C. 自定义异常必须重写父类的getMessage方法
D. 自定义异常不能有带参数的构造方法
1
2
3
4

答案:B ✅


# 小结

本文系统梳理了 Java 异常机制的核心知识点:

  • 异常体系:Throwable 分为 Exception(可处理)和 Error(系统级错误)
  • 受检 vs 非受检:checked 异常需显式处理,unchecked 异常运行时发生
  • 处理语法:try-catch-finally、throw、throws 的使用场景和注意事项
  • 最佳实践:避免用异常控制流程、规范日志打印、注意性能影响

掌握异常处理是编写健壮 Java 程序的基础,建议结合《Effective Java》和阿里巴巴 Java 开发手册深入理解。

你好,我是 SharkChili ,Java Guide 核心维护者之一,对 Redis、Nightingale 等知名开源项目有深度源码研究经验。熟悉 Java、Go 等多语言技术栈,现任某知名黑厂高级研发。

🌟 开源项目贡献

  • mini-redis:教学级 Redis 精简实现,助力分布式缓存原理学习
    🔗 https://github.com/shark-ctrl/mini-redis (opens new window)(欢迎 Star & Contribute)

📚 公众号价值 分享企业级架构设计、性能优化、源码解析等核心技术干货,涵盖分布式系统、微服务治理、大数据处理等实战领域,并探索面向AI的vibe coding等现代开发范式。

👥 加入技术社群 关注公众号,回复 【加群】 获取联系方式,与众多技术爱好者交流分布式架构、微服务等前沿技术!

# 参考

Java基础常见面试题总结(下):https://javaguide.cn/java/basis/java-basic-questions-03.html#项目相关 (opens new window)

《Effective Java中文版(第3版)》

阿里巴巴Java开发手册:https://book.douban.com/subject/27605355/ (opens new window)

Java核心技术·卷 I(原书第11版):https://book.douban.com/subject/34898994/ (opens new window)

Java 基础 - 异常机制详解:https://www.pdai.tech/md/java/basic/java-basic-x-exception.html#异常是否耗时为什么会耗时 (opens new window)

面试官问我 ,try catch 应该在 for 循环里面还是外面?:https://mp.weixin.qq.com/s/TQgBSYAhs_kUaScw7occDg (opens new window)

《代码整洁之道》

编辑 (opens new window)
上次更新: 2026/04/10, 15:57:03
Java面向对象知识点大总结,建议收藏
聊聊Java中的常用类String

← Java面向对象知识点大总结,建议收藏 聊聊Java中的常用类String→

最近更新
01
AI时代专注力培养
04-11
02
DelayQueue源码解析
04-10
03
PriorityQueue源码分
04-10
更多文章>
Theme by Vdoing | Copyright © 2025-2026 Evan Xu | MIT License | 桂ICP备2024034950号 | 桂公网安备45142202000030
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式
×
×