1.概述
异常是每个Java开发人员都应该熟悉的重要主题。本文提供了在面试中可能会出现的一些问题的答案。
2.问题
Q1.什么是Exception?
Exception是在程序执行期间发生的异常事件,并中断程序指令的正常流程。
Q2.throw和throws关键字的目的是什么?
throws关键字用于指定一个方法可以在其执行过程中产生异常。它在调用方法时强制执行显式异常处理:
public void simpleMethod() throws Exception {
// ...
}
该throw关键字允许我们抛出一个异常对象中断程序的正常流动。当程序无法满足给定条件时,这是最常用的:
if (task.isTooComplicated()) {
throw new TooComplicatedException("The task is too complicated");
}
Q3.你怎么处理异常?
通过使用try-catch-finally语句:
try {
// ...
} catch (ExceptionType1 ex) {
// ...
} catch (ExceptionType2 ex) {
// ...
} finally {
// ...
}
可能发生异常的代码块包含在try块中。该块也称为“受保护”或“安全”代码。
如果发生异常,则执行与抛出的异常匹配的catch块,否则,将忽略所有catch块。
无论一个异常是否被捕捉到,在finally总是在退出后执行的。
Q4.你怎么能捕捉多个异常?
在代码块中有三种处理多个异常的方法。
第一种是使用可以处理抛出的所有异常类型的catch块:
try {
// ...
} catch (Exception ex) {
// ...
}
您应该记住,建议的做法是使用尽可能准确的异常处理程序。
过于宽泛的异常处理程序可能会使您的代码更容易出错,捕获未预期的异常,并导致程序中出现意外行为。
第二种方法是实现多个catch块:
try {
// ...
} catch (FileNotFoundException ex) {
// ...
} catch (EOFException ex) {
// ...
}
注意,如果异常具有继承关系; 必须先输入子类型,然后再输入父类型。如果我们不这样做,将导致编译错误。
第三是使用多捕获块:
try {
// ...
} catch (FileNotFoundException | EOFException ex) {
// ...
}
此功能首先在Java 7中引入; 减少代码重复并使其更易于维护。
Q5.已检查和未检查的异常有什么区别?
必须在try-catch块中处理已检查的异常,或在throws子句中声明; 而不需要处理或声明未经检查的异常。
已检查和未检查的异常也分别称为编译时和运行时异常。
除Error,RuntimeException及其子类指示的异常外,所有异常都是经过检查的异常。
Q6.异常和错误有什么区别?
异常是表示可以恢复的事件,而错误表示通常无法从中恢复的意外情况。
JVM抛出的所有错误都是Error或其子类之一,更常见的包括但不限于:
- OutOfMemoryError - 当JVM因内存不足而无法分配更多对象时抛出,而垃圾收集器无法提供更多可用内容
- StackOverflowError - 当线程的堆栈空间用完时发生,通常是因为应用程序过于冗长
- ExceptionInInitializerError - 表示在评估静态初始化程序期间发生意外异常
- NoClassDefFoundError - 当类加载器尝试加载类的定义但无法找到它时抛出,通常是因为在类路径中找不到所需的类文件
- UnsupportedClassVersionError - 当JVM尝试读取类文件并确定文件中的版本不受支持时发生,通常是因为该文件是使用较新版本的Java生成的
虽然可以使用try语句处理错误,但这不是推荐的做法,因为无法保证程序在抛出错误后能够可靠地执行任何操作。
Q7.执行以下代码块会抛出什么异常?
Integer[][] ints = { { 1, 2, 3 }, { null }, { 7, 8, 9 } };
System.out.println("value = " + ints[1][1].intValue());
它抛出一个ArrayIndexOutOfBoundsException,因为我们试图访问一个大于数组长度的位置。
Q8.什么是异常链?
在响应另一个异常时抛出异常时发生。这使我们能够发现我们提出的问题的完整历史:
try {
task.readConfigFile();
} catch (FileNotFoundException ex) {
throw new TaskException("Could not perform task", ex);
}
Q9.什么是堆栈跟踪?它与异常有什么关系?
堆栈跟踪提供从应用程序开始到发生异常的点所调用的类和方法的名称。
它是一个非常有用的调试工具,因为它使我们能够确切地确定应用程序中抛出异常的位置以及导致它的原始原因。
Q10.为什么要将自定义异常?
如果Java平台中已存在的异常类型未表示异常类型,或者您需要向客户端代码提供更多信息以更精确地处理它,则应创建自定义异常。
决定是否应该检查或取消选中自定义异常完全取决于业务案例。但是,作为经验法则; 如果可以预期使用您的异常的代码从中恢复,则创建一个已检查的异常,否则将其取消选中。
此外,您应该从最特定的Exception子类继承,该子类与您要抛出的子类密切相关。如果没有这样的类,则选择Exception作为父类。
Q11.异常有哪些优点?
传统的错误检测和处理技术通常会导致异常代码难以维护且难以阅读。但是,异常使我们能够将应用程序的核心逻辑与发生意外情况时的操作细节分开。
此外,由于JVM向后搜索调用堆栈以查找对处理特定异常感兴趣的任何方法; 我们获得了在调用堆栈中传播错误而无需编写其他代码的能力。
此外,由于程序中抛出的所有异常都是对象,因此可以根据其类层次结构对它们进行分组或分类。这允许我们通过在catch块中指定异常的超类来在单个异常处理程序中捕获组异常。
Q12.你可以在lambda表达式的主体中抛出任何异常吗?
使用Java提供的标准功能接口时,只能抛出未经检查的异常,因为标准功能接口在方法签名中没有“throws”子句:
List<Integer> integers = Arrays.asList(3, 9, 7, 0, 10, 20);
integers.forEach(i -> {
if (i == 0) {
throw new IllegalArgumentException("Zero not allowed");
}
System.out.println(Math.PI / i);
});
但是,如果您使用自定义功能接口,则可以抛出已检查的异常:
@FunctionalInterface
public static interface CheckedFunction<T> {
void apply(T t) throws Exception;
}
public void processTasks(
List<Task> taks, CheckedFunction<Task> checkedFunction) {
for (Task task : taks) {
try {
checkedFunction.apply(task);
} catch (Exception e) {
// ...
}
}
}
processTasks(taskList, t -> {
// ...
throw new Exception("Something happened");
});
Q13.覆盖抛出异常的方法时,我们需要遵循哪些规则?
有几条规则规定了如何在继承的上下文中声明异常。
当父类方法不抛出任何异常时,子类方法不能抛出任何已检查的异常,但它可能会抛出任何未检查的异常。
这是一个示例代码来演示这个:
class Parent {
void doSomething() {
// ...
}
}
class Child extends Parent {
void doSomething() throws IllegalArgumentException {
// ...
}
}
下一个示例将无法编译,因为重写方法会抛出未在重写方法中声明的已检查异常:
class Parent {
void doSomething() {
// ...
}
}
class Child extends Parent {
void doSomething() throws IOException {
// Compilation error
}
}
当父类方法抛出一个或多个已检查的异常时,子类方法可以抛出任何未经检查的异常; 所有,没有或已声明的已检查异常的子集,甚至更多这些异常,只要它们具有相同的范围或更窄。
这是一个成功遵循上一个规则的示例代码:
class Parent {
void doSomething() throws IOException, ParseException {
// ...
}
void doSomethingElse() throws IOException {
// ...
}
}
class Child extends Parent {
void doSomething() throws IOException {
// ...
}
void doSomethingElse() throws FileNotFoundException, EOFException {
// ...
}
}
请注意,这两种方法都遵循规则。第一个抛出的异常少于重写方法,第二个抛出的异常少,即使抛出更多; 它们的范围较窄。
但是,如果我们尝试抛出父类方法未声明的已检查异常,或者我们抛出一个范围更广的异常; 我们会得到一个编译错误:
class Parent {
void doSomething() throws FileNotFoundException {
// ...
}
}
class Child extends Parent {
void doSomething() throws IOException {
// Compilation error
}
}
当父类方法具有带有未经检查的异常的throws子句时,子类方法可以抛出任何或未经任选数量的未经检查的异常,即使它们不相关。
这是一个尊重规则的例子:
class Parent {
void doSomething() throws IllegalArgumentException {
// ...
}
}
class Child extends Parent {
void doSomething()
throws ArithmeticException, BufferOverflowException {
// ...
}
}
Q14.下面的代码会编译通过吗?
void doSomething() {
// ...
throw new RuntimeException(new Exception("Chained Exception"));
}
可以。链接异常时,编译器只关心链中的第一个,因为它检测到未经检查的异常,所以我们不需要添加throws子句。
Q15.有没有办法从没有throws子句的方法抛出一个已检查的异常?
可以。我们可以利用编译器执行的类型擦除,并让它认为我们正在抛出一个未经检查的异常,事实上; 我们抛出一个检查过的异常:
public <T extends Throwable> T sneakyThrow(Throwable ex) throws T {
throw (T) ex;
}
public void methodWithoutThrows() {
this.<RuntimeException>sneakyThrow(new Exception("Checked Exception"));
}
3.结论
在本文中,我们探讨了一些可能出现在Java开发人员技术访谈中的关于异常的问题。这不是一个详尽的清单,只应作为进一步研究的开始。
我们祝愿您在即将进行的面试中取得成功。Good Luck!
每日福利
Java知己欢迎大家关注公众号:「Java知己」,关注公众号,回复「1024」你懂得,免费领取 30 本经典编程书籍。关注我,与 10 万程序员一起进步。 每天更新Java知识哦,期待你的到来!
网友评论