写点什么

Java 异常面试题

  • 2022 年 4 月 26 日
  • 本文字数:8100 字

    阅读完需:约 27 分钟

}


}

[](()捕获异常

在一个 try-catch 语句块中可以捕获多个异常类型,并对不同类型的异常做出不同的处理


private static void readFile(String filePath) {


try {


// code


} catch (FileNotFoundException e) {


// handle FileNotFoundException


} catch (IOException e){


// handle IOException


}


}


同一个 catch 也可以捕获多种类型异常,用 | 隔开


private static void readFile(String filePath) {


try {


// code


} catch (FileNotFoundException | UnknownHostException e) {


// handle FileNotFoundException or UnknownHostException


} catch (IOException e){


// handle IOException


}


}

[](()自定义异常

习惯上,定义一个异常类应包含两个构造函数,一个无参构造函数和一个带有详细描述信息的构造函数(Throwable 的 toString 方法会打印这些详细信息,调试时很有用)


public class MyException extends Exception {


public MyException(){ }


public MyException(String msg){


super(msg);


}


// ...


}

[](()try-catch-finally

当方法中发生异常,异常处之后的代码不会再执行,如果之前获取了一些本地资源需要释放,则需要在方法正常结束时和 catch 语句中都调用释放本地资源的代码,显得代码比较繁琐,finally 语句可以解决这个问题。


private static void readFile(String filePath) throws MyException {


File file = new File(filePath);


String result;


BufferedReader reader = null;


try {


reader = new BufferedReader(new FileReader(file));


while((result = reader.readLine())!=null) {


System.out.println(result);


}


} catch (IOException e) {


System.out.println("readFile method catch block.");


MyException ex = new MyException("read file failed.");


ex.initCause(e);


throw ex;


} finally {


System.out.println("readFile method finally block.");


if (null != reader) {


try {


reader.close();


} catch (IOException e) {


e.printStackTrace();


}


}


}


}


调用该方法时,读取文件时若发生异常,代码会进入 catch 代码块,之后进入 finally 代码块;若读取文件时未发生异常,则会跳过 catch 代码块直接进入 finally 代码块。所以无论代码中是否发生异常,fianlly 中的代码都会执行。


若 catch 代码块中包含 return 语句,finally 中的代码还会执行吗?将以上代码中的 catch 子句修改如下:


catch (IOException e) {


System.out.println("readFile method catch block.");


return;


}


调用 readFile 方法,观察当 catch 子句中调用 return 语句时,finally 子句是否执行


readFile method catch block.


readFile method finally block.


可见,即使 catch 中包含了 return 语句,finally 子句依然会执行。若 finally 中也包含 return 语句,finally 中的 return 会覆盖前面的 return.

[](()try-with-resource

上面例子中,finally 中的 close 方法也可能抛出 IOException, 从而覆盖了原始异常。JAVA 7 提供了更优雅的方式来实现资源的自动释放,自动释放的资源需要是实现了 AutoCloseable 接口的类。


private static void tryWithResourceTest(){


try (Scanner scanner = new Scanner(new FileInputStream("c:/abc"),"UTF-8")){


// code


} catch (IOException e){


// handle exception


}


}


try 代码块退出时,会自动调用 scanner.close 方法,和把 scanner.close 方法放在 finally 代码块中不同的是,若 scanner.close 抛出异常,则会被抑制,抛出的仍然为原始异常。被抑制的异常会由 addSusppressed 方法添加到原来的异常,如果想要获取被抑制的异常列表,可以调用 getSuppressed 方法来获取。


[](()Java 异常常见面试题



[](()1. Error 和 Exception 区别是什么?

Error 类型的错误通常为虚拟机相关错误,如系统崩溃,内存不足,堆栈溢出等,编译器不会对这类错误进行检测,JAVA 应用程序也不应对这类错误进行捕获,一旦这类错误发生,通常应用程序会被终止,仅靠应用程序本身无法恢复;


Exception 类的错误是可以在应用程序中进行捕获并处理的,通常遇到这种错误,应对其进行处理,使应用程序可以继续正常运行。

[](()2. 运行时异常和一般异常(受检异常)区别是什么?

运行时异常包括 RuntimeException 类及其子类,表示 JVM 在运行期间可能出现的异常。 Java 编译器不会检查运行时异常。


受检异常是 Exception 中除 RuntimeException 及其子类之外的异常。 Java 编译器会检查受检异常。


RuntimeException 异常和受检异常之间的区别:是否强制要求调用者必须处理此异常,如果强制要求调用者必须进行处理,那么就使用受检异常,否则就选择非受检异常(RuntimeException)。一般来讲,如果没有特殊的要求,我们建议使用 RuntimeException 异常。

[](()3. JVM 是如何处理异常的?

在一个方法中如果发生异常,这个方法会创建一个异常对象,并转交给 JVM,该异常对象包含异常名称,异常描述以及异常发生时应用程序的状态。创建异常对象并转交给 JVM 的过程称为抛出异常。可能有一系列的方法调用,最终才进入抛出异常的方法,这一系列方法调用的有序列表叫做调用栈。


JVM 会顺着调用栈去查找看是否有可以处理异常的代码,如果有,则调用异常处理代码。当 JVM 发现可以处理异常的代码时,会把发生的异常传递给它。如果 JVM 没有找到可以处理该异常的代码块,JVM 就会将该异常转交给默认的异常处理器(默认处理器为 JVM 的一部分),默认异常处理器打印出异常信息并终止应用程序。

[](()4. throw 和 throws 的区别是什么?

Java 中的异常处理除了包括捕获异常和处理异常之外,还包括声明异常和拋出异常,可以通过 throws 关键字在方 《一线大厂 Java 面试题解析+后端开发学习笔记+最新架构讲解视频+实战项目源码讲义》无偿开源 威信搜索公众号【编程进阶路】 法上声明该方法要拋出的异常,或者在方法内部通过 throw 拋出异常对象。


throws 关键字和 throw 关键字在使用上的几点区别如下


  • throw 关键字用在方法内部,只能用于抛出一种异常,用来抛出方法或代码块中的异常,受查异常和非受查异常都可以被抛出。

  • throws 关键字用在方法声明上,可以抛出多个异常,用来标识该方法可能抛出的异常列表。一个方法用 throws 标识了可能抛出的异常列表,调用该方法的方法中必须包含可处理异常的代码,否则也要在方法签名中用 throws 关键字声明相应的异常。

[](()5. final、finally、finalize 有什么区别?

  • final 可以修饰类、变量、方法,修饰类表示该类不能被继承、修饰方法表示该方法不能被重写、修饰变量表示该变量是一个常量不能被重新赋值。

  • finally 一般作用在 try-catch 代码块中,在处理异常的时候,通常我们将一定要执行的代码方法 finally 代码块中,表示不管是否出现异常,该代码块都会执行,一般用来存放一些关闭资源的代码。

  • finalize 是一个方法,属于 Object 类的一个方法,而 Object 类是所有类的父类,Java 中允许使用 finalize()方法在垃圾收集器将对象从内存中清除出去之前做必要的清理工作。

[](()6. NoClassDefFoundError 和 ClassNotFoundException 区别?

NoClassDefFoundError 是一个 Error 类型的异常,是由 JVM 引起的,不应该尝试捕获这个异常。


引起该异常的原因是 JVM 或 ClassLoader 尝试加载某类时在内存中找不到该类的定义,该动作发生在运行期间,即编译时该类存在,但是在运行时却找不到了,可能是变异后被删除了等原因导致;


ClassNotFoundException 是一个受查异常,需要显式地使用 try-catch 对其进行捕获和处理,或在方法签名中用 throws 关键字进行声明。当使用 Class.forName, ClassLoader.loadClass 或 ClassLoader.findSystemClass 动态加载类到内存的时候,通过传入的类路径参数没有找到该类,就会抛出该异常;另一种抛出该异常的可能原因是某个类已经由一个类加载器加载至内存中,另一个加载器又尝试去加载它。

[](()7. try-catch-finally 中哪个部分可以省略?

答:catch 可以省略


原因


更为严格的说法其实是:try 只适合处理运行时异常,try+catch 适合处理运行时异常+普通异常。也就是说,如果你只用 try 去处理普通异常却不加以 catch 处理,编译是通不过的,因为编译器硬性规定,普通异常如果选择捕获,则必须用 catch 显示声明以便进一步处理。而运行时异常在编译时没有如此规定,所以 catch 可以省略,你加上 catch 编译器也觉得无可厚非。


理论上,编译器看任何代码都不顺眼,都觉得可能有潜在的问题,所以你即使对所有代码加上 try,代码在运行期时也只不过是在正常运行的基础上加一层皮。但是你一旦对一段代码加上 try,就等于显示地承诺编译器,对这段代码可能抛出的异常进行捕获而非向上抛出处理。如果是普通异常,编译器要求必须用 catch 捕获以便进一步处理;如果运行时异常,捕获然后丢弃并且+finally 扫尾处理,或者加上 catch 捕获以便进一步处理。


至于加上 finally,则是在不管有没捕获异常,都要进行的“扫尾”处理。

[](()8. try-catch-finally 中,如果 catch 中 return 了,finally 还会执行吗?

答:会执行,在 return 前执行。


注意:在 finally 中改变返回值的做法是不好的,因为如果存在 finally 代码块,try 中的 return 语句不会立马返回调用者,而是记录下返回值待 finally 代码块执行完毕之后再向调用者返回其值,然后如果在 finally 中修改了返回值,就会返回修改后的值。显然,在 finally 中返回或者修改返回值会对程序造成很大的困扰,C#中直接用编译错误的方式来阻止程序员干这种龌龊的事情,Java 中也可以通过提升编译器的语法检查级别来产生警告或错误。


代码示例 1:


public static int getInt() {


int a = 10;


try {


System.out.println(a / 0);


a = 20;


} catch (ArithmeticException e) {


a = 30;


return a;


/*


  • return a 在程序执行到这一步的时候,这里不是 return a 而是 return 30;这个返回路径就形成了

  • 但是呢,它发现后面还有 finally,所以继续执行 finally 的内容,a=40

  • 再次回到以前的路径,继续走 return 30,形成返回路径之后,这里的 a 就不是 a 变量了,而是常量 30


*/


} finally {


a = 40;


}


return a;


}


执行结果:30


代码示例 2:


public static int getInt() {


int a = 10;


try {


System.out.println(a / 0);


a = 20;


} catch (ArithmeticException e) {


a = 30;


return a;


} finally {


a = 40;


//如果这样,就又重新形成了一条返回路径,由于只能通过 1 个 return 返回,所以这里直接返回 40


return a;


}


}


执行结果:40

[](()9. 类 ExampleA 继承 Exception,类 ExampleB 继承 ExampleA。

有如下代码片断:


try {


throw new ExampleB("b")


} catch(ExampleA e){


System.out.println("ExampleA");


} catch(Exception e){


System.out.println("Exception");


}


请问执行此段代码的输出是什么?



输出:ExampleA。(根据里氏代换原则[能使用父类型的地方一定能使用子类型],抓取 ExampleA 类型异常的 catch 块能够抓住 try 块中抛出的 ExampleB 类型的异常)


面试题 - 说出下面代码的运行结果。(此题的出处是《Java 编程思想》一书)


class Annoyance extends Exception {


}


class Sneeze extends Annoyance {


}


class Human {


public static void main(String[] args)


throws Exception {


try {


try {


throw new Sneeze();


} catch ( Annoyance a ) {


System.out.println("Caught Annoyance");


throw a;


}


} catch ( Sneeze s ) {


System.out.println("Caught Sneeze");


return ;


} finally {


System.out.println("Hello World!");


}


}


}


结果


Caught Annoyance


Caught Sneeze


Hello World!

[](()10. 常见的 RuntimeException 有哪些?

  • ClassCastException(类转换异常)

  • IndexOutOfBoundsException(数组越界)

  • NullPointerException(空指针)

  • ArrayStoreException(数据存储异常,操作数组时类型不一致)

  • 还有 IO 操作的 BufferOverflowException 异常

[](()11. Java 常见异常有哪些

java.lang.IllegalAccessError:违法访问错误。当一个应用试图访问、修改某个类的域(Field)或者调用其方法,但是又违反域或方法的可见性声明,则抛出该异常。


java.lang.InstantiationError:实例化错误。当一个应用试图通过 Java 的 new 操作符构造一个抽象类或者接口时抛出该异常.


java.lang.OutOfMemoryError:内存不足错误。当可用内存不足以让 Java 虚拟机分配给一个对象时抛出该错误。


java.lang.StackOverflowError:堆栈溢出错误。当一个应用递归调用的层次太深而导致堆栈溢出或者陷入死循环时抛出该错误。


java.lang.ClassCastException:类造型异常。假设有类 A 和 B(A 不是 B 的父类或子类),O 是 A 的实例,那么当强制将 O 构造为类 B 的实例时抛出该异常。该异常经常被称为强制类型转换异常。


java.lang.ClassNotFoundException:找不到类异常。当应用试图根据字符串形式的类名构造类,而在遍历 CLASSPAH 之后找不到对应名称的 class 文件时,抛出该异常。


java.lang.ArithmeticException:算术条件异常。譬如:整数除零等。


java.lang.ArrayIndexOutOfBoundsException:数组索引越界异常。当对数组的索引值为负数或大于等于数组大小时抛出。


java.lang.IndexOutOfBoundsException:索引越界异常。当访问某个序列的索引值小于 0 或大于等于序列大小时,抛出该异常。


java.lang.InstantiationException:实例化异常。当试图通过 newInstance()方法创建某个类的实例,而该类是一个抽象类或接口时,抛出该异常。


java.lang.NoSuchFieldException:属性不存在异常。当访问某个类的不存在的属性时抛出该异常。


java.lang.NoSuchMethodException:方法不存在异常。当访问某个类的不存在的方法时抛出该异常。


java.lang.NullPointerException:空指针异常。当应用试图在要求使用对象的地方使用了 null 时,抛出该异常。譬如:调用 null 对象的实例方法、访问 null 对象的属性、计算 null 对象的长度、使用 throw 语句抛出 null 等等。


java.lang.NumberFormatException:数字格式异常。当试图将一个 String 转换为指定的数字类型,而该字符串确不满足数字类型要求的格式时,抛出该异常。


java.lang.StringIndexOutOfBoundsException:字符串索引越界异常。当使用索引值访问某个字符串中的字符,而该索引值小于 0 或大于等于序列大小时,抛出该异常。


[](()Java 异常处理最佳实践




在 Java 中处理异常并不是一个简单的事情。不仅仅初学者很难理解,即使一些有经验的开发者也需要花费很多时间来思考如何处理异常,包括需要处理哪些异常,怎样处理等等。这也是绝大多数开发团队都会制定一些规则来规范进行异常处理的原因。而团队之间的这些规范往往是截然不同的。


本文给出几个被很多团队使用的异常处理最佳实践。

[](()1. 在 finally 块中清理资源或者使用 try-with-resource 语句

当使用类似 InputStream 这种需要使用后关闭的资源时,一个常见的错误就是在 try 块的最后关闭资源。


public void doNotCloseResourceInTry() {


FileInputStream inputStream = null;


try {


File file = new File("./tmp.txt");


inputStream = new FileInputStream(file);


// use the inputStream to read a file


// do NOT do this


inputStream.close();


} catch (FileNotFoundException e) {


log.error(e);


} catch (IOException e) {


log.error(e);


}


}


问题就是,只有没有异常抛出的时候,这段代码才可以正常工作。try 代码块内代码会正常执行,并且资源可以正常关闭。但是,使用 try 代码块是有原因的,一般调用一个或多个可能抛出异常的方法,而且,你自己也可能会抛出一个异常,这意味着代码可能不会执行到 try 代码块的最后部分。结果就是,你并没有关闭资源。


所以,你应该把清理工作的代码放到 finally 里去,或者使用 try-with-resource 特性。

[](()1.1 使用 finally 代码块

与前面几行 try 代码块不同,finally 代码块总是会被执行。不管 try 代码块成功执行之后还是你在 catch 代码块中处理完异常后都会执行。因此,你可以确保你清理了所有打开的资源。


public void closeResourceInFinally() {


FileInputStream inputStream = null;


try {


File file = new File("./tmp.txt");


inputStream = new FileInputStream(file);


// use the inputStream to read a file


} catch (FileNotFoundException e) {


log.error(e);


} finally {


if (inputStream != null) {


try {


inputStream.close();


} catch (IOException e) {


log.error(e);


}


}


}


}

[](()1.2 Java 7 的 try-with-resource 语法

如果你的资源实现了 AutoCloseable 接口,你可以使用这个语法。大多数的 Java 标准资源都继承了这个接口。当你在 try 子句中打开资源,资源会在 try 代码块执行后或异常处理后自动关闭。


public void automaticallyCloseResource() {


File file = new File("./tmp.txt");


try (FileInputStream inputStream = new FileInputStream(file);) {


// use the inputStream to read a file


} catch (FileNotFoundException e) {


log.error(e);


} catch (IOException e) {


log.error(e);


}


}

[](()2. 优先明确的异常

你抛出的异常越明确越好,永远记住,你的同事或者几个月之后的你,将会调用你的方法并且处理异常。


因此需要保证提供给他们尽可能多的信息。这样你的 API 更容易被理解。你的方法的调用者能够更好的处理异常并且避免额外的检查。


因此,总是尝试寻找最适合你的异常事件的类,例如,抛出一个 NumberFormatException 来替换一个 IllegalArgumentException 。避免抛出一个不明确的异常。


public void doNotDoThis() throws Exception {


...


}


public void doThis() throws NumberFormatException {


...


}

[](()3. 对异常进行文档说明

当在方法上声明抛出异常时,也需要进行文档说明。目的是为了给调用者提供尽可能多的信息,从而可以更好地避免或处理异常。


在 Javadoc 添加 @throws 声明,并且描述抛出异常的场景。


public void doSomething(String input) throws MyBusinessException {


...


}

[](()4. 使用描述性消息抛出异常

在抛出异常时,需要尽可能精确地描述问题和相关信息,这样无论是打印到日志中还是在监控工具中,都能够更容易被人阅读,从而可以更好地定位具体错误信息、错误的严重程度等。


但这里并不是说要对错误信息长篇大论,因为本来 Exception 的类名就能够反映错误的原因,因此只需要用一到两句话描述即可。


如果抛出一个特定的异常,它的类名很可能已经描述了这种错误。所以,你不需要提供很多额外的信息。一个很好的例子是 NumberFormatException 。当你以错误的格式提供 String 时,它将被 java.lang.Long 类的构造函数抛出。


try {


new Long("xyz");


} catch (NumberFormatException e) {


log.error(e);


}

[](()5. 优先捕获最具体的异常

大多数 IDE 都可以帮助你实现这个最佳实践。当你尝试首先捕获较不具体的异常时,它们会报告无法访问的代码块。


但问题在于,只有匹配异常的第一个 catch 块会被执行。 因此,如果首先捕获 IllegalArgumentException ,则永远不会到达应该处理更具体的 NumberFormatException 的 catch 块,因为它是 IllegalArgumentException 的子类。


总是优先捕获最具体的异常类,并将不太具体的 catch 块添加到列表的末尾。


你可以在下面的代码片断中看到这样一个 try-catch 语句的例子。 第一个 catch 块处理所有 NumberFormatException 异常,第二个处理所有非 NumberFormatException 异常的 IllegalArgumentException 异常。


public void catchMostSpecificExceptionFirst() {


try {


doSomething("A message");


} catch (NumberFormatException e) {


log.error(e);


} catch (IllegalArgumentException e) {


log.error(e)


}


}

[](()6. 不要捕获 Throwable 类

Throwable 是所有异常和错误的超类。你可以在 catch 子句中使用它,但是你永远不应该这样做!


如果在 catch 子句中使用 Throwable ,它不仅会捕获所有异常,也将捕获所有的错误。JVM 抛出错误,指出不应该由应用程序处理的严重问题。 典型的例子是 OutOfMemoryError 或者 StackOverflowError 。两者都是由应用程序控制之外的情况引起的,无法处理。


所以,最好不要捕获 Throwable ,除非你确定自己处于一种特殊的情况下能够处理错误。


public void doNotCatchThrowable() {


try {


// do something


} catch (Throwable t) {


// don't do this!


}


}

[](()7. 不要忽略异常

很多时候,开发者很有自信不会抛出异常,因此写了一个 catch 块,但是没有做任何处理或者记录日志。

用户头像

还未添加个人签名 2022.04.13 加入

还未添加个人简介

评论

发布
暂无评论
Java异常面试题_Java_爱好编程进阶_InfoQ写作社区