回答重点
Exception和Error都是Throwable类的子类(在Java代码中只有继承了Throwable类的实例,才可以被throw和被catch)他们表示程序运行时发生的异常或错误情况
总结来看,Exception表示可以被处理的程序异常,Error表示系统级的不可恢复错误
详细说明:
1)Exception:是允许开发者处理的程序异常。表示程序逻辑或外部环境中的问题,可以通过代码进行恢复或处理。
常见子类有:IOException、SQLException等
Exception又分为Checked Exception(编译期异常/受检异常)和Unchecked Exception(运行时异常/非受检异常)。
- Checked Exception:在编译时必须显式处理(如使用try-catch块或通过throws声明抛出)。
- Unchecked Exception:运行时异常,不需要显式捕获。如空指针异常。全部继承自Runtime Exception
2)Error:也是一种运行时异常,表示系统级的严重错误,通常是JVM层次内系统级别、无法预料的错误。程序也无法通过代码进行处理或恢复。如栈溢出(StackOverFlowError)、内存耗尽(OutOfMemoryError)
Error不应该被程序捕获或处理,因为因为一般出现这种错误时系统无法继续运行,但是不一定立即宕机关闭线程,比如内存耗尽,会多次尝试清理内存。
扩展知识
异常继承体系
异常处理时需要注意的六个点
1)尽量不要捕获类似Exception这样通用的异常,而应该捕获特定的异常
软件工程是一门协作的艺术,在日常的开发中我们有义务使自己的代码更直观、清晰地表达出我们想要表达的信息
但是如果你什么异常都用了Exception,那别的开发同事就不能一眼得知这段代码实际想要捕获的异常,并且这样的代码也会捕获到可能你希望它抛出而不希望捕获的异常
2)不要 "吞" 了异常
如果我们捕获了异常,不把异常抛出,或者没有写到日志里。会出现一种现象:线上出了bug莫名其妙的没人任何的异常信息,会导致后期维护人员找不到出错的原因
这可能让一个简单的bug变得难以诊断,而且很多初级开发者虽然也catch了,但是处理逻辑是e.printStackTrace(),适合用于调试时快速查看异常。在企业开发中不推荐这种方式,一般情况下这样子也没什么大问题,但是这个方法直接将异常堆栈信息输出到标准错误流(System.err)
所以最好是输入到日志里,产品可以自定义一定的格式,将详细的信息输入到日志系统中,适合清晰高效的排查错误
这种方式还有什么缺点?
- 不可控的输出位置:输出到标准错误流(
stderr
),也就是控制台,没有集中到统一的日志存储系统种,无法直接与日志系统或监控工具集成,尤其在复杂的分布式系统中,这种信息很难被收集和定位,导致找不到异常发生的位置。 - 不符合日志系统的规范:企业级应用通常使用日志框架(如 Log4j、SLF4J 等)来记录日志。
e.printStackTrace()
不带日志的上下文信息(如时间戳、线程 ID),会让日志难以统一管理和过滤。 - 可能泄露敏感信息:如果不小心在生产环境使用,堆栈信息中可能包含敏感数据(如文件路径、服务器信息等),会造成安全隐患。
- 日志不可持久化:如果控制台日志未被重定向(如未导入到日志存储系统),容器或服务重启后,这些堆栈信息会被丢失。
- 缺乏上下文:分布式系统中请求往往跨服务或线程传播,堆栈跟踪只能记录本地的调用栈,无法覆盖整个请求链路。
3)不要延迟处理异常
比如有个方法,参数是个name,方法内部调用了好几个其他方法,但是name形参传的值是null值,但是你没有在进入这个方法或者这个方法一开始就处理这个情况,而是在你调用了别的好几个方法后爆出这个空指针
这样的话明明你的出错堆栈信息只需要抛出一点点信息就能定位到这个错误所在的地方,经过了好多方法之后可能就是一坨堆栈信息。
4)只在需要try-catch的地方try-catch,try-catch的范围能小则小
只在必要的代码段使用try-catch,不要不分青红皂白try住一大坨代码,因为try-catch中的代码会影响JVM对代码的优化,例如重排序
- Java 的编译器和 JVM 对正常代码有许多优化技术(如方法内联、分支预测、寄存器分配等)。但
try-catch
块中的代码可能会干扰这些优化。 - 如果大段代码都被包裹在
try-catch
中,JVM 可能会因为异常处理机制而跳过某些优化。- JVM 在
try-catch
中通常会生成额外的异常表,用于捕获和处理异常,而不是直接生成高效的字节码。 - 这些额外的表会对方法的优化和执行性能产生负面影响。
- JVM 在
异常处理的开销
-
字节码层面的异常表
- 在
try-catch
中,编译器会生成异常表,而不是简单的跳转指令。这些表会告诉 JVM 异常发生后跳到哪里执行处理逻辑。 - 这些异常表增加了方法的复杂度,JVM 会在运行时维护这些结构。
- 在
-
热代码路径分析受阻
- JVM 的 JIT(即时编译器)对代码的优化依赖于分析方法的执行路径。异常会被认为是“非热路径”(即非常规执行路径),它们可能导致优化的分支预测失效。
5)不要通过异常来控制程序流程
例如有些null值,能用if-else判断,就不要用异常,异常可能由性能开销,肯定是比一些条件语句低效的,由CPU分支预测的优化等。
而且每实例化一个Exception都会对栈进行快照,相对而言这是一个比较重的操作,如果数量过多开销就不能被忽略了
6)不要再finally代码块中处理返回值或者直接return
在finally中return或者直接处理返回值会让程序发生很多诡异的事情,比如覆盖了try中的return,或者屏蔽的异常
什么?到底由多诡异?我下面举4个例子
行为原因
- 在 Java 中,无论
try
块或catch
块中是否有return
,finally
块总会被执行。 - 如果在
finally
中使用了return
,它会覆盖try
或catch
块中已有的return
值,导致原先的返回值被丢弃。
java">public class FinallyTest {public static int test() {try {return 1; // 1. 这里的返回值会被覆盖} finally {return 2; // 2. finally 中的 return 覆盖了 try 中的返回值}}public static void main(String[] args) {System.out.println(test()); // 输出 2}
}
- 如果
try
或catch
块中的返回值是一个对象(如数组、列表等),在finally
块中对该对象进行修改,修改后的内容会保留,因为返回的是对象的引用。 - 如果返回的是基本类型,
finally
中的操作不会改变已经准备返回的值。
java">public class FinallyTest {public static int test() {int result = 1;try {return result; // 准备返回 result 的值} finally {result = 2; // 修改 result,但不会影响返回值}}public static void main(String[] args) {System.out.println(test()); // 输出 1}
}
java">public class FinallyTest {public static int[] test() {int[] result = {1};try {return result; // 返回 result 的引用} finally {result[0] = 2; // 修改 result 引用指向的内容}}public static void main(String[] args) {System.out.println(test()[0]); // 输出 2}
}
- 如果
try
或catch
中抛出异常,但在finally
中又执行了return
,异常会被吞掉,外部无法捕获到这个异常。 - 这会导致程序出现“隐性错误”,让开发者无法发现问题。
java">public class FinallyTest {public static int test() {try {throw new RuntimeException("Exception in try"); // 抛出异常} finally {return 42; // 异常被吞掉,返回 42}}public static void main(String[] args) {System.out.println(test()); // 输出 42,没有异常信息}
}
感觉学了这么久的Java语法都是假的
什么异常不希望捕获,而是直接抛出?
并非所有的异常都需要捕获,有些异常更适合直接抛出,由调用者处理。
比如:典型的Error,这种异常通常由JVM或操作系统触发,程序本身无法处理,所以用程序捕获无意义啊,可能掩盖问题,导致更严重的后果
再比如:运行时异常(RuntimeException类)
RuntimeException是程序逻辑错误的表现,由调用者自己错误编码导致的。
例如空指针异常、数组越界
这些异常应该抛出给调用者去自定义处理方式,而不是帮忙捕获处理掩盖人家写代码能力差的事实