关于作者
程序猿周周
⌨️ 短视频小厂BUG攻城狮
如果文章对你有帮助,记得关注、点赞、收藏,一键三连哦,你的支持将成为我最大的动力
本文是《后端面试小册子》系列的第 1️⃣1️⃣ 篇文章,该系列将整理和梳理笔者作为 Java 后端程序猿在日常工作以及面试中遇到的实际问题,通过这些问题的系统学习,也帮助笔者顺利拿到阿里、字节、华为、快手等多个大厂 Offer,也祝愿大家能够早日斩获自己心仪的 Offer。
PS:《后端面试小册子》已整理成册,目前共十三章节,总计约二十万字,欢迎关注公众号【程序猿周周】获取电子版和更多学习资料(最新系列文章也会在此陆续更新)。公众号后台可以回复关键词「电⼦书」可获得这份面试小册子。文中所有内容都会在 Github 开源,项目地址 csnotes,如文中存在错误,欢迎指出。如果觉得文章还对你有所帮助,赶紧点个免费的 star 支持一下吧!
标题 | 地址 |
---|---|
MySQL数据库面试题总结(2022版) | https://blog.csdn.net/adminpd/article/details/122910606 |
Redis面试题总结(2022版) | https://blog.csdn.net/adminpd/article/details/122934938 |
计算机网络面试题总结(2022版) | https://blog.csdn.net/adminpd/article/details/122973684 |
操作系统面试题总结(2022版) | https://blog.csdn.net/adminpd/article/details/122994599 |
Linux面试题总结(2022版) | https://blog.csdn.net/adminpd/article/details/122994862 |
Spring面试题总结(2022版) | https://blog.csdn.net/adminpd/article/details/123016872 |
Java基础面试题总结(2022版) | https://blog.csdn.net/adminpd/article/details/123080189 |
Java集合面试题总结(2022版) | https://blog.csdn.net/adminpd/article/details/123171501 |
Java并发面试题总结(2022版) | https://blog.csdn.net/adminpd/article/details/123266624 |
Java虚拟机面试题总结(2022版) | https://blog.csdn.net/adminpd/article/details/123412605 |
Java异常面试题总结(2022版) | https://blog.csdn.net/adminpd/article/details/123462676 |
设计模式面试题总结(2022版) | https://blog.csdn.net/adminpd/article/details/123490442 |
Dubbo面试题总结(2022版) | https://blog.csdn.net/adminpd/article/details/123538243 |
Netty面试题总结(2022版) | https://blog.csdn.net/adminpd/article/details/123564362 |
Java 异常是 Java 提供的一种识别及响应错误的一致性机制。
Java 异常机制可以使程序中异常处理代码和正常业务代码分离,保证程序代码更加优雅,并提高程序健壮性。在有效使用异常的情况下,异常能清晰的回答What, Where, Why 这3个问题:异常类型回答了“什么”被抛出,异常堆栈跟踪回答了“在哪”抛出,异常信息回答了“为什么”会抛出。
Java 异常机制中定义了许多异常类,并且定义了基类 java.lang.Throwable
作为所有异常的超类。Java 语言设计者将异常划分为两类:Error 和 Exception,其体系结构大致如下图所示:
Error 是程序中无法处理的错误,表示运行应用程序中出现了严重的错误。此类错误一般表示代码运行时 JVM 出现问题。通常有 Virtual MachineError(虚拟机运行错误)
、NoClassDefFoundError(类定义错误)
、OutOfMemoryError(内存不足错误)
和 StackOverflowError(栈溢出错误)
等。
此类错误发生时,JVM 将终止线程。同时这些错误是不可查的,非代码性错误。因此,当此类错误发生时,应用不应该去处理此类错误,也无法处理。
Exception 是程序本身可以捕获并且可以处理的异常。这种异常又分为两类:运行时异常和编译异常。
1)运行时异常
也称Unchecked(不受检查异常),这类异常包括 RuntimeException 类及其子类,表示 JVM 在运行期间可能出现的异常。
比如说试图使用空值对象的引用(NullPointerException)、数组下标越界(ArrayIndexOutBoundException)。此类异常属于不可查异常,一般是由程序逻辑错误引起的,在程序中可以选择捕获处理,也可以不处理。
2)编译异常
这类异常包括 Exception 中除 RuntimeException 及其子类之外的异常。又称Checked(受检查异常)。
如果程序中出现此类异常,比如 ClassNotFoundException(没有找到指定的类异常)
、IOException(IO流异常)
等,要么通过 throws 语句进行声明抛出,要么通过 try-catch 语句捕获处理,否则不能通过编译。
在程序中,通常不会自定义该类异常,而是直接使用系统提供的异常类。该异常我们必须手动在代码里添加捕获语句来处理该异常。
Java 的所有异常可以分为受检异常(Checked Exception)和非受检异常(Unchecked Exception)。
编译器要求必须处理的异常。正确的程序在运行过程中,经常容易出现的、符合预期的异常情况。一旦发生此类异常,就必须采用某种方式进行处理。除 RuntimeException 及其子类外,其他的 Exception 异常都属于受检异常。编译器会检查此类异常,也就是说当编译器检查到应用中的某处可能会此类异常时,将会提示你处理本异常——要么使用try-catch捕获,要么使用方法签名中用 throws 关键字抛出,否则编译不通过。
编译器不会进行检查并且不要求必须处理的异常,也就说当程序中出现此类异常时,即使我们没有 try-catch 捕获它,也没有使用 throws 抛出该异常,编译也会正常通过。该类异常包括运行时异常(RuntimeException 及其子类)和错误(Error)。
将要被监听代码,即可能抛出异常的代码放在 try 语句块之内,当 try 语句块内发生异常时,异常就被抛出。
catch 用来捕获 try 语句块中发生的异常。
一个 try 语句后可以跟多个 catch 语句以捕获多个异常类型,并对不同类型的异常做出不同的处理。而同一个 catch 也可以用 |
隔开捕获多种类型异常。
它主要用于回收在 try 块里打开的物力资源(如数据库连接、网络连接和磁盘文件)。
只有 finally 块中语句执行完成之后,才会回来执行 try 或者 catch 块中的 return 或 throw 语句,如果 finally 中使用了 return 或 throw 等终止方法的语句,则就不会跳回执行,直接停止。
try {
// ...
} catch (IOException e) {
// ...
} finally {
if (null != reader) {
try {
reader.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
上述代码中,finally 块的 close 方法也可能抛出 IOException, 从而覆盖了原始异常。JAVA 7 提供了更优雅的方式来实现资源的自动释放,但需要自动释放的资源需要实现 Closeable
或 AutoCloseable
接口。
同时还需将要关闭的外部资源在 try() 中创建,catch()捕获处理异常。其实 try-with-resource 机制是一种语法糖,其底层实现原理仍然是 try-catch-finally 写法,不过在 catch 代码块中加入一个 addSuppressed() 方法,即异常抑制方法。如果业务处理和关闭连接都出现了异常,业务处理的异常会抑制关闭连接的异常,只抛出处理中的异常,仍然可以通过 getSuppressed() 方法获得关闭连接的异常。
try(FileInputStream fis = new FileInputStream(new File("./test.txt"))){
// do something
}catch(IOExpection e){
// ...
}
在一个方法中如果发生异常,这个方法会创建一个异常对象,并转交给 JVM,该异常对象包含异常名称,异常描述以及异常发生时应用程序的状态。创建异常对象并转交给 JVM 的过程称为抛出异常。可能有一系列的方法调用,最终才进入抛出异常的方法,这一系列方法调用的有序列表叫做调用栈。
JVM 会顺着调用栈去查找看是否有可以处理异常的代码,如果有,则调用异常处理代码。当 JVM 发现可以处理异常的代码时,会把发生的异常传递给它。如果 JVM 没有找到可以处理该异常的代码块,JVM 就会将该异常转交给默认的异常处理器(默认处理器为 JVM 的一部分),默认异常处理器打印出异常信息并终止应用程序。
throw 关键字用在方法内部,而 throws 关键字用在方法声明上。
throw 只能抛出一种异常,用来抛出方法或代码块中的异常,受查异常和非受查异常都可以被抛出。
throws 可以抛出多个异常,用来标识该方法可能抛出的异常列表。
引起该异常的原因是 JVM 或 ClassLoader 尝试加载某类时在内存中找不到该类的定义,该动作发生在运行期间,即编译时该类存在,但是在运行时却找不到了,可能是变异后被删除了等原因导致。
当使用 Class.forName、ClassLoader.loadClass 或 ClassLoader.findSystemClass 动态加载类到内存的时候,通过传入的类路径参数没有找到该类,就会抛出该异常;另一种抛出该异常的可能原因是某个类已经由一个类加载器加载至内存中,另一个加载器又尝试去加载它。
try {
throw new ExampleB("Example B")
} catch(ExampleA e){
System.out.println("Example A");
} catch(Exception e){
System.out.println("Exception");
}
根据里式代换原则,能使用父类型的地方一定能使用子类型。故上面一段代码会输出 Example A
,即 catch 可以捕获声明异常以及所有子类,这也是我们不推荐直接捕获 Exception 类的原因。
节选自《阿里巴巴Java开发手册》。
1)【强制】Java 类库中定义的可以通过预检查方式规避的 RuntimeException 异常不应该通过 catch 的方式来处理,比如 NullPointerException、IndexOutOfBoundsException 等。
说明:无法通过预检查的异常除外,比如,在解析字符串形式的数字时,可能存在数字格式错误,不得不通过catch NumberFormatException来实现。 正例:if (obj != null) {…} 反例:try { obj.method(); } catch (NullPointerException e) {…}
2)【强制】异常不要用来做流程控制,条件控制。
说明:异常设计的初衷是解决程序运行中的各种意外情况,且异常的处理效率比条件判断方式要低很多。
3)【强制】使用 catch 时请分清稳定代码和非稳定代码,稳定代码指的是无论如何不会出错的代码。对于非稳定代码的catch尽可能进行区分异常类型,再做对应的异常处理。
说明:对大段代码进行 try-catch,使程序无法根据不同的异常做出正确的应激反应,也不利于定位问题,这是一种不负责任的表现。 正例:用户注册的场景中,如果用户输入非法字符,或用户名称已存在,或用户输入密码过于简单,在程序上作出分门别类的判断,并提示给用户。
4)【强制】捕获异常是为了处理它,不要捕获了却什么都不处理而抛弃之,如果不想处理它,请将该异常抛给它的调用者。最外层的业务使用者,必须处理异常,将其转化为用户可以理解的内容。
5)【强制】有try块放到了事务代码中,catch异常后,如果需要回滚事务,一定要注意手动回滚事务。
6)【强制】finally 块必须对资源对象、流对象进行关闭,有异常也要做try-catch。 说明:如果JDK7及以上,可以使用try-with-resources方式。
7)【强制】不要在 finally 块中使用return。 说明:try块中的return语句执行成功后,并不马上返回,而是继续执行finally块中的语句,如果此处存在return语句,则在此直接返回,无情丢弃掉try块中的返回点。 反例:
private int x = 0;
public int checkReturn() {
try {
// x等于1,此处不返回
return ++x;
} finally {
// 返回的结果是2
return ++x;
}
}
8)【强制】捕获异常与抛异常,必须是完全匹配,或者捕获异常是抛异常的父类。 说明:如果预期对方抛的是绣球,实际接到的是铅球,就会产生意外情况。
9)【强制】在调用 RPC、二方包、或动态生成类的相关方法时,捕捉异常必须使用 Throwable 类来进行拦截。
说明:通过反射机制来调用方法,如果找不到方法,抛出 NoSuchMethodException。什么情况会抛出 NoSuchMethodError 呢?二方包在类冲突时,仲裁机制可能导致引入非预期的版本使类的方法签名不匹配,或者在字节码修改框架(比如:ASM)动态创建或修改类时,修改了相应的方法签名。这些情况,即使代码编译期是正确的,但在代码运行期时,会抛出 NoSuchMethodError。
10)【推荐】方法的返回值可以为 null,不强制返回空集合,或者空对象等,必须添加注释充分说明什么情况下会返回 null 值。
说明:本手册明确防止 NPE 是调用者的责任。即使被调用方法返回空集合或者空对象,对调用者来说,也并非高枕无忧,必须考虑到远程调用失败、序列化失败、运行时异常等场景返回 null 的情况。
11)【推荐】防止 NPE 是程序员的基本修养,注意 NPE 产生的场景: 1) 返回类型为基本数据类型,return 包装数据类型的对象时,自动拆箱有可能产生 NPE。2) 数据库的查询结果可能为 null。3)集合里的元素即使 isNotEmpty,取出的数据元素也可能为 null。4)远程调用返回对象时,一律要求进行空指针判断,防止 NPE。5)对于 Session 中获取的数据,建议进行 NPE 检查,避免空指针。6)级联调用如 obj.getA().getB().getC();
一连串调用,易产生NPE。
推荐使用 JDK8 的 Optional 类来防止 NPE 问题。
12)【推荐】定义时区分 unchecked / checked 异常,避免直接抛出 new RuntimeException(),更不允许抛出 Exception 或者 Throwable,应使用有业务含义的自定义异常。推荐业界已定义过的自定义异常,如:DAOException / ServiceException 等。
13)【参考】对于公司外的 http/api 开放接口必须使用”错误码”形式;而应用内部推荐异常抛出;跨应用间 RPC 调用优先考虑使用 Result 方式,封装 isSuccess() 方法、“错误码”、“错误简短信息”。
说明:关于 RPC 方法返回方式使用 Result 方式的理由: 1)使用抛异常返回方式,调用方如果没有捕获到就会产生运行时错误。 2)如果不加栈信息,只是自定义异常,加入自己的理解的 error message,对于调用端解决问题的帮助不会太多。如果加了栈信息,在频繁调用出错的情况下,数据序列化和传输的性能损耗也是问题。
14)【参考】避免出现重复的代码(Don’t Repeat Yourself),即 DRY 原则。
说明:随意复制和粘贴代码,必然会导致代码的重复,在以后需要修改时,需要修改所有的副本,容易遗漏。必要时抽取共性方法,或者抽象公共类,甚至是组件化。