
犬豪
2023/04/13阅读:29主题:Pornhub黄
Java中的异常体系

❝Java语言在设计之初就提供了相对完善的异常处理机制,在日常编程中,如何处理好异常是比较考验功底的,掌握好需要两个方面,面试官也常从这两个方面作为抓手出题:
理解Throwable,Exception,Error的设计和分类 理解java语言中操作Throwable的元素和实践
今天就来谈谈第一点,除了掌握Exception和Error的设计理念外,还有比如:
-
掌握那些应用最为广泛的子类 -
以及如何自定义异常等
一、掌握常见的异常类
很多面试官会进一步追问一些细节,比如,你了解哪些Error,Exception或者RuntimeException?面试官并不想听到你说NullPointerException、ClassCastException这些运行时异常,如果你只了解这些说明你掌握的还是太少了。
今天我导了一个类图,并列出来典型例子,可以给你作为参考,至少做到基本心里有数:
这些异常都是比较常见的异常,你是否都遇到过呢?接下来我们一个一个看:
ExceptionInInitializerError
ExceptionInInitializerError:当在静态初始化块中出现异常的时候会抛出该异常。
Demo:
public class QuanHaoTest {
public static void main(String[] args) {
Person.hello();
}
}
class Person {
static String name = "小豪";
static {
int num = 5 / 0;
}
public static void hello() {
System.out.println("hello");
}
}
输出结果:
Exception in thread "main" java.lang.ExceptionInInitializerError
at com.qhao.java.QuanHaoTest.main(QuanHaoTest.java:11)
Caused by: java.lang.ArithmeticException: / by zero
at com.qhao.java.Person.<clinit>(QuanHaoTest.java:19)
... 1 more
几个点说明一下:
1."Exception in thread "main" java.lang.ExceptionInInitializerError"意味着异常出现在主线程,并且是LinkageError的一个子类java.lang.ExceptionInInitializerError,这是JVM类加载失败时才抛出的
,原因是静态初始化代码中出现了诸如IndexOutOfBoundsException这样的RuntimeException
。
2.记住JVM会将所有的静态变量
的初始化按它们在源文件中的出现顺序放到一个静态初始化块中。因此,不要觉得没有看到静态初始块就认为不会出现这个异常
。事实上,你得确保静态变量的正确顺序,比如说,如果 一个变量初始化的时候用到了另一个变量,你得确保这个变量在前面已经初始化过了。
比如看下面这段代码,这段编码编译可以通过,但是运行会报错:
public class StaticParams {
//1.静态初始化,先执行这里
private static StaticParams sp = buildStaticParams();
//3.如果没有报错,第三步走这
private static List<String> LIST_A = getListA();
private static StaticParams buildStaticParams() {
//2.走到这静态变量LIST_A还没有初始化报空指针
LIST_A.add("abc");
return new StaticParams();
}
private static List<String> getListA() {
System.out.println("初始化List");
return new ArrayList<String>();
}
}
public class QuanHaoTest {
public static void main(String[] args) {
StaticParams staticParams = new StaticParams();
}
}
输出:
Exception in thread "main" java.lang.ExceptionInInitializerError
at com.qhao.java.QuanHaoTest.main(QuanHaoTest.java:11)
Caused by: java.lang.NullPointerException
at com.qhao.java.StaticParams.buildStaticParams(StaticParams.java:16)
at com.qhao.java.StaticParams.<clinit>(StaticParams.java:12)
... 1 more
3.对于静态初始化块中出现异常的类,第一次加载会抛出ExceptionInInitializerError异常,以后任何使用到这个类的地方,都会抛出NoClassDefFoundError异常
。这也是生产环境中经常遇到的错误,如果你在遇到NoClassDefFoundError类不存在的异常时,不妨先看看你的日志文件中有没有ExceptionInInitializerError这个异常。
Demo:
public class QuanHaoTestError {
public static void main(String[] args) {
try {
// 第一次使用,加载类
Person.hello();
} catch (Throwable e) {
// 捕获之后再次使用,还会报错
Person.hello();
}
}
}
class Person {
static String name = "小豪";
static {
int num = 5 / 0;
}
public static void hello() {
System.out.println("hello");
}
}
输出:
Exception in thread "main" java.lang.NoClassDefFoundError: Could not initialize class com.qhao.java.Person
at com.qhao.java.QuanHaoTest.main(QuanHaoTest.java:16)
4.记住静态初始化代码块会抛出RuntimeException而不是已检查异常,而后者需要有对应的catch块来进行处理。
需要谨记的是这个异常的一个副作用是NoClassDefFoundError,而Java程序抛出这个异常的位置可能会离java.lang.ExceptionInInitializerError很远,这取决于你的客户端代码何时引用到这个类。因此,在查看类路径解决NoClassDefFoundError异常之前,最好先看看日志有没有出现ExceptionInInitializerError。
UnsatisfiedLinkError
这个异常和JNI有关,先简单了解一下JNI:
❝JNI是Java Native Interface的缩写,通过使用 Java本地接口书写程序,
可以确保代码在不同的平台上方便移植
。从Java1.1开始,JNI标准成为java平台的一部分,它允许Java代码和其他语言写的代码进行交互
。JNI一开始是为了本地已编译语言,尤其是C和C++而设计的,但是它并不妨碍你使用其他编程语言,只要调用约定受支持就可以了。使用java与本地已编译的代码交互,通常会丧失平台可移植性。但是,有些情况下这样做是可以接受的,甚至是必须的。例如,使用一些旧的库,与硬件、操作系统进行交互,或者为了提高程序的性能。JNI标准至少要保证本地代码能工作在任何Java 虚拟机环境。
关于JNI先了解这么多,我们只要知道Java中提供了API:System.loadLibrary("")
可以加载本地的动态链接库,将Java中的native方法和本地代码关联上,其方法参数为不包含扩展名的动态链接库库文件名。
在不同操作系统下,c/c++或其他代码生成的动态链接库也会有差异,在window平台下会编译为dll文件,linux平台下会编译为so文件,在mac os下会编译为dylib文件,关于这一点,你可以在JRE_HOME/jre/lib下查看,这也是为什么JVM跨平台的原因:

再回来看这个异常定义
❝UnsatisfiedLinkError:对于声明为 native 的方法,如果 Java 虚拟机找不到和它对应的本机语言定义,就会抛出该异常。
当调用本地方法时,类加载器会尝试加载定义了该方法的本地库。如果找不到这个库,就会抛出这个错误。 下面是抛出 UnsatisfiedLinkError 的测试用例 :
public class QuanHaoTestError {
public native void call_A_Native_Method();
static {
System.loadLibrary("myNativeLibrary");
}
public static void main(String[] args) {
new QuanHaoTestError().call_A_Native_Method();
}
}
输出:
Exception in thread "main" java.lang.UnsatisfiedLinkError: no myNativeLibrary in java.library.path
at java.lang.ClassLoader.loadLibrary(ClassLoader.java:1867)
at java.lang.Runtime.loadLibrary0(Runtime.java:870)
at java.lang.System.loadLibrary(System.java:1122)
at com.qhao.java.QuanHaoTest.<clinit>(QuanHaoTest.java:13)
NoClassDefFoundError 和 ClassNotFoundException 有什么区别?
这里两个类一起说,因为NoClassDefFoundError和ClassNotFoundException有什么区别,这也是一个经典的入门题目。
当一个类找不到的时候,JVM有时候会抛出ClassNotFoundException异常,而有时候又会抛出NoClassDefFoundError。看两个异常的字面意思,好像都是类找不到,但是JVM为什么要用两个异常去区分类找不到的情况呢?这个两个异常有什么不同的地方呢?
ClassNotFoundException:
ClassNotFoundException是一个运行时异常。从类继承层次上来看,ClassNotFoundException是从Exception继承的,所以ClassNotFoundException是一个检查异常。

当应用程序运行的过程中尝试使用类加载器去加载Class文件的时候,如果没有在classpath中查找到指定的类,就会抛出ClassNotFoundException。一般情况下,当我们使用Class.forName()
或者ClassLoader.loadClass
以及使用ClassLoader.findSystemClass()
在运行时加载类的时候
,如果类没有被找到,那么就会导致JVM抛出ClassNotFoundException。
最简单的,当我们使用JDBC去连接数据库的时候,我们一般会使用Class.forName()的方式去加载JDBC的驱动,如果我们没有将驱动放到应用的classpath下,那么会导致运行时找不到类,所以运行Class.forName()会抛出ClassNotFoundException。
public class QuanHaoTest {
public static void main(String[] args) {
try {
Class.forName("oracle.jdbc.driver.OracleDriver");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
java.lang.ClassNotFoundException: oracle.jdbc.driver.OracleDriver
at java.net.URLClassLoader.findClass(URLClassLoader.java:382)
at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:349)
at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
at java.lang.Class.forName0(Native Method)
at java.lang.Class.forName(Class.java:264)
at com.qhao.java.QuanHaoTest.main(QuanHaoTest.java:11)
NoClassDefFoundError:
NoClassDefFoundError异常,看命名后缀是一个Error
。从类继承层次上看,NoClassDefFoundError是从Error继承
的。和ClassNotFoundException相比,明显的一个区别是,NoClassDefFoundError并不需要应用程序去关心catch的问题。

当JVM在加载一个类的时候,如果这个类在编译时是可用的,但是在运行时找不到这个类的定义的时候
,JVM就会抛出一个NoClassDefFoundError错误。比如当我们在new一个类的实例的时候,如果在运行时类找不到,则会抛出一个NoClassDefFoundError的错误。
public class QuanHaoTest {
public static void main(String[] args) {
TempClass t = new TempClass();
}
}
class TempClass {}
首先这里我们先创建一个TempClass,然后编译以后,将TempClass生产的TempClass.class文件删除,然后执行程序,输出:
Exception in thread "main" java.lang.NoClassDefFoundError: com/qhao/java/TempClass
at com.qhao.java.QuanHaoTest.main(QuanHaoTest.java:11)
Caused by: java.lang.ClassNotFoundException: com.qhao.java.TempClass
at java.net.URLClassLoader.findClass(URLClassLoader.java:382)
at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:349)
at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
... 1 more
总结就是:
ClassNotFoundException | NoClassDefFoundError |
---|---|
从java.lang.Exception继承,是一个Exception类型 | 从Java.lang.Error继承,是一个Error类型 |
当动态加载Class的时候找不到类会抛出该异常 | 当编译成功以后执行过程中Class找不到导致抛出该错误 |
一般在执行Class.forName()、ClassLoader.loadClass()或ClassLoader.findSystemClass()的时候抛出 | 由JVM的运行时系统抛出 |
OutOfMemoryError
内存溢出也是平时开发经常遇到的一个错误,直接演示:
堆溢出,设置堆大小 -Xmx10m -Xms10m :
public class QuanHaoTest {
byte[] buffer = new byte[new Random().nextInt(1024 * 200)];
/**
* -Xmx10m -Xms10m
*
* @param args
*/
public static void main(String[] args) {
List<QuanHaoTest> list = new ArrayList<QuanHaoTest>();
while (true) {
list.add(new QuanHaoTest());
}
}
}
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at com.qhao.java.QuanHaoTest.<init>(QuanHaoTest.java:13)
at com.qhao.java.QuanHaoTest.main(QuanHaoTest.java:23)
方法区溢出,我这里是jdk1.8,所以用的元空间大小参数 -XX:MetaspaceSize=10m -XX:MaxMetaspaceSize=10m:
/**
* Created by 犬豪
*
* @Homepage www.yuque.com/qhao
*/
public class QuanHaoTest {
/**
* -XX:MetaspaceSize=10m -XX:MaxMetaspaceSize=10m
*
* @param args
*/
public static void main(String[] args) {
while (true) {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(OOMObject.class);
enhancer.setUseCache(false);
enhancer.setCallback(new MethodInterceptor() {
@Override
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy)
throws Throwable {
return methodProxy.invoke(o, args);
}
});
enhancer.create();
}
}
static class OOMObject {}
}
Exception in thread "main" java.lang.OutOfMemoryError: Metaspace
at org.springframework.cglib.core.ReflectUtils.defineClass(ReflectUtils.java:557)
at org.springframework.cglib.core.AbstractClassGenerator.generate(AbstractClassGenerator.java:363)
at org.springframework.cglib.proxy.Enhancer.generate(Enhancer.java:585)
at org.springframework.cglib.core.AbstractClassGenerator$ClassLoaderData.get(AbstractClassGenerator.java:131)
at org.springframework.cglib.core.AbstractClassGenerator.create(AbstractClassGenerator.java:319)
at org.springframework.cglib.proxy.Enhancer.createHelper(Enhancer.java:572)
at org.springframework.cglib.proxy.Enhancer.create(Enhancer.java:387)
at com.qhao.java.QuanHaoTest.main(QuanHaoTest.java:35)
StackOverflowError
栈溢出也是我们经常遇到的一个异常,在写递归的时候经常会出现,这里补充一个小知识点:你知道Java最大支持栈深度有多大吗? 其实这里是没有一个固定数值的,主要受两个因素影响:
-
我们可以通过 -Xss 设置 Java 线程堆栈大小, 堆栈大小越大,能够支持越多的方法调用(能够存储更多的栈帧)
-
我们知道JVM中每个栈帧包含五部分,分别包括局部变量表、操作数栈、动态链表(指向运行时常量池的方法引用)、方法返回地址和一些附加信息,其中影响栈帧大小最关键的因素就是局部变量表了。所以 局部变量表内容越多(注意是个数),那么栈帧就越大,栈深度就越小
。
堆栈大小设置-Xss 5m:
public class QuanHaoTest {
public int count = 0;
public void stackOverTest(String s1) {
count++;
stackOverTest(s1);
}
/**
* -Xss 5m
*/
public static void main(String[] args) {
QuanHaoTest quanHaoTest = new QuanHaoTest();
try {
quanHaoTest.stackOverTest("s1");
} catch (Throwable e) {
System.out.println("stack depth: " + quanHaoTest.count);
e.printStackTrace();
}
}
}
stack depth: 1732
java.lang.StackOverflowError
at com.qhao.java.QuanHaoTest.stackOverTest(QuanHaoTest.java:12)
at com.qhao.java.QuanHaoTest.stackOverTest(QuanHaoTest.java:13)
at com.qhao.java.QuanHaoTest.stackOverTest(QuanHaoTest.java:13)
添加成两个参数后:
public class QuanHaoTest {
public int count = 0;
public void stackOverTest(String s1, String s2) {
count++;
stackOverTest(s1, s2);
}
/**
* -Xss 5m
*/
public static void main(String[] args) {
QuanHaoTest quanHaoTest = new QuanHaoTest();
try {
quanHaoTest.stackOverTest("s1", "s2");
} catch (Throwable e) {
System.out.println("stack depth: " + quanHaoTest.count);
e.printStackTrace();
}
}
}
stack depth: 1599
java.lang.StackOverflowError
at com.qhao.java.QuanHaoTest.stackOverTest(QuanHaoTest.java:12)
at com.qhao.java.QuanHaoTest.stackOverTest(QuanHaoTest.java:13)
at com.qhao.java.QuanHaoTest.stackOverTest(QuanHaoTest.java:13)
SecurityException
这个异常通常都是由安全管理器SecurityManager抛出的,以指示安全违规的,除了SecurityManager外其他任何有安全违规的场景都有在用,比如当你自己定义了一个包路径以"java."开头的类的时候:
package java;
public class QuanHaoTest {
public static void main(String[] args) {
System.out.println("hello");
}
}
Error: A JNI error has occurred, please check your installation and try again
Exception in thread "main" java.lang.SecurityException: Prohibited package name: java
at java.lang.ClassLoader.preDefineClass(ClassLoader.java:662)
at java.lang.ClassLoader.defineClass(ClassLoader.java:761)
at java.security.SecureClassLoader.defineClass(SecureClassLoader.java:142)
at java.net.URLClassLoader.defineClass(URLClassLoader.java:468)
at java.net.URLClassLoader.access$100(URLClassLoader.java:74)
at java.net.URLClassLoader$1.run(URLClassLoader.java:369)
at java.net.URLClassLoader$1.run(URLClassLoader.java:363)
at java.security.AccessController.doPrivileged(Native Method)
at java.net.URLClassLoader.findClass(URLClassLoader.java:362)
at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:349)
at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
at sun.launcher.LauncherHelper.checkAndLoadMain(LauncherHelper.java:495)
其他常见的运行时异常
-
IllegalArgumentException:非法参数异常 -
NullPointerException:空指针 -
ClassCastException:类型转换异常 -
ArrayIndexOutOfBoundsException:索引越界 -
ArithmeticException:运算异常
这些非常常见,直接上Demo:
public class QuanHaoTest {
public static void main(String[] args) {
QuanHaoTest nullPointerException = null;
nullPointerException.hello();// 空指针
Object str = "QuanHaoTest";
QuanHaoTest classCastException = (QuanHaoTest)str;//类型转换异常
int[] arrayIndexOutOfBoundsException = new int[3];
int value = arrayIndexOutOfBoundsException[5];//索引越界
int arithmeticException=10/0;//运算异常
}
void hello() {
}
}
二、自定义异常
有的时候我们会根据需要自定义异常:
首先从Exception和Error的设计理念来看,自定义异常一般是不会继承Error的,而是继承Exception,因为自定义的异常肯定是可预料到的,可处理的,一般也不会是虚拟机层面的错误。
另外自定义异常除了保证提供足够的信息外,还有两个点需要考虑:
-
是否需要定义成Checked Exception, 因为这种类型设计的初衷就是为了从异常情况下恢复
,作为异常设计者,我们往往有充足信息进行分类。所以如果是"可恢复"的异常,可以定义成它
。 -
为保证诊断信息足够的同时,要考虑避免包含敏感信息,因为那样可能导致潜在的 安全问题
。如果我们看java的标准库,你可能注意到类似java.net.ConnectException,出错信息是类似"connection refused(connection refused)",而不包含具体的机器名,ip,端口等,一个重要考虑就是信息安全。类似的情况在日志中也有,比如,用户数据一般是不可以输出到日志里面的。
业界有一种争论(甚至可以算上是某种程度的共识),java语言的Checked Exception也许是个设计错误,反对者列举几点:
-
Checked Exception的假设是我们捕获了异常,然后恢复程序。但是,其实我们大多数情况下,根本不可能恢复。Checked exception的使用,已经大大偏离了最初的设计目的。(一般产品业务代码看到最多的自定义异常都是继承RuntimeException) -
Checked Exception不兼容functional编程,如果你写过lambda/stream代码,相信深有体会。
比如,下面这段代码编译是通过的:
public class QuanHaoTest {
public static void main(String[] args) {
print(QuanHaoTest::out);
}
private static String out(String o) {
System.out.println(o);
return "execute success";
}
public static void print(Function<String, String> func) {
String result = func.apply("ok");
System.out.println(result);
}
}
但如果out方法申明了检查异常:

很多开源项目,已经采纳了这种实践,比如Spring,Hibernate等,甚至反映在新的编程语言的设计中,比如Scala等。 当然,很多人觉得没有必要矫枉过正,因为确实有一些异常,比如和环境相关的IO,网络等,其实是存在可回复性的,而且Java已经通过业界的海量实践,证明了其结构高质量软件的能力。我们就不再进一步解读了。
作者介绍
