面试官:说下对 Java 中异常的理解

打印 上一主题 下一主题

主题 912|帖子 912|积分 2736

引言

Java 中的异常与异常处理机制也是面试中常见的考察点。面试官不仅关注求职者对 Java 异常体系结构的理解,如区分 Exception 以及 Error 的能力,更希望通过了解你对异常处理的方式,来评估你的编程功底和实际项目中的处理履历。
所以,我们今天来看一下,Java 中的异常体系结构以及面试经常被问到的相关知识。
Java 中异常体系的条理结构

Java 的异常体系基于 Throwable 类构建,它有两个主要的子类:Error 和 Exception。这两个类分别代表了不同范例的异常情况。请看图:

Throwable 类中的常用 API


  • String getMessage():返回异常发生时的简要描述
  • Throwable getCause():返回此异常原因,即导致此异常发生的一个 Throwable 对象。
  • String toString():返回异常发生时的详细信息,通常包罗异常类的名称和详细消息。
  • void printStackTrace():利用标准错误流打印 Throwable 对象封装的异常信息,包罗异常的范例、详细消息和调用堆栈。
Exception 与 Error

Exception 和 Error 是 Java 中异常和错误的两个顶层父类。
Exception 和 Error 的主要区别

它们两个的主要区别,我们从以下几个方面进行分析:

  • 严重水平

    • Exception:表现程序可以捕获并可以或许处理的异常情况。这类异常通常是可预见的,可以通过公道的异常处理机制来应对。比方,文件找不到 (FileNotFoundException) 或者网络毗连失败 (SocketException)。
    • Error:表现应用程序无法处理的故障,出现此类异常 JVM 一般都会停止执行,如 OutOfMemoryError 或 StackOverflowError。这些题目通常不在应用程序的控制范围内,因此不发起被应用程序捕获或处理。

  • 处理方式

    • Exception:我们应当对 Exception 进行得当的处理,以确保应用程序可以或许继续正常运行。编码时可以通过 try-catch 块来捕获异常,或者通过 throws 关键字声明方法抛出异常,让调用方自行处理。
    • Error:一般情况下,在程序中不发起实验捕获 Error,因为 Error 代表题目标严重级别很高,通常意味着应用程序已经处于不健康的状态。当系统出现 Error 时,可能需要配合我们的监控系统,采取快速相应措施,减少对业务的影响。

  • 适用场景

    • Exception:适用于开发过程中可能出现的异常情况,特殊是那些可以通过公道计划和编码避免的情况。好比请求输入错误、资源不可用、文件找不到等。
    • Error:适用于超出应用控制范围的严重题目,如硬件故障、JVM 内部错误等。这些题目是编码时难以预见且难以修复的。

什么是 Checked Exception 与 Unchecked Exception

Java 中的 Exception 分为两大类:Checked Exception(检查异常) 和 Unchecked Exception(非检查异常)。

  • Checked Exception:Checked Exception 是指那些必须在编译时被显式处理的异常,如果不处理这类异常,IDE 中的编译器一般会给堕落误提示。如果一个方法可能会抛出 Checked Exception,那么该方法要么通过 throws 声明抛出异常,要么在其内部利用 try-catch 捕获异常。


  • Unchecked Exception:Unchecked Exception 是指那些不需要在编译时显式处理的异常。RuntimeException 及其子类都是 Unchecked Exception。这类通常由编码错误引起,如空指针异常 (NullPointerException) 或数组越界访问异常 (ArrayIndexOutOfBoundsException)。编译器固然不要求显示处理这些异常,但优秀的编码应该尽量避免抛出此类异常。
阿里开发手册中这样要求:
强制】Java 类库中定义的一类 RuntimeException 可以通过预先检查进行规避,而不应该通过 catch 来处理,好比:IndexOutOfBoundsException,NullPointerException 等等。
说明:无法通过预检查的异常除外,如在解析一个外部传来的字符串形式数字时,通过 catch NumberFormatException 来实现。
正例:if (obj != null) {...}
反例:try { obj.method() } catch (NullPointerException e) {...}
常见的 Checked Exception 与 Unchecked Exception

这里整理下常见的这两类异常,面试时有可能会被问到开发中常见的有哪些。
常见的 Checked Exception

  • IOException 相关的:与输入输出操作相关的异常。比方,读写文件或网络毗连失败时抛出。包罗子类如 FileNotFoundException 和 EOFException。
  • SQLException:在执行数据库操作时可能发生的异常。
  • ClassNotFoundException:当应用程序实验加载某个类而在类路径中找不到对应的类文件时抛出。
  • ……
常见的 Unchecked Exception

  • NullPointerException:空指针异常,当访问对象方法或者字段而对象为 null 时抛出。
  • ArrayIndexOutOfBoundsException:数组越界访问异常,访问数组中不存在的索引时抛出。
  • ClassCastException:范例转换异常,当将一个对象强制转换为不是实在际范例的类时抛出。
  • IllegalArgumentException:非法参数异常,当接收到非法或不得当的参数时抛出,可在编码中手动抛出该异常提示参数题目。
  • NumberFormatException:数值格式化异常,好比利用 Long.parseLong 解析一个字符串数值"3.14"时会抛出该异常。
  • ArithmeticException:算术异常,好比在执行 1/0 时抛出 by zero 除零算术异常。
  • ConcurrentModificationException:在迭代集合的同时修改它(没有通过迭代器自身的移除方法),或者检测到并发修改时抛出。
  • ……
NoClassDefFoundError 和 ClassNotFoundException 有什么区别

首先,这两者从名字上就可以看出本质的不同:ClassNotFoundException 是 Exception,而 NoClassDefFoundError 是 Error。
看下 JDK 源码中的表明(摘抄自 JDK 1.8):
  1. /**
  2. * Thrown when an application tries to load in a class through its
  3. * string name using:
  4. * <ul>
  5. * <li>The forName method in class Class.
  6. * <li>The findSystemClass method in class
  7. *     ClassLoader .
  8. * <li>The loadClass method in class ClassLoader.
  9. * </ul>
  10. * <p>
  11. * but no definition for the class with the specified name could be found.
  12. */
  13. public class ClassNotFoundException {}
复制代码
当应用程序实验利用以下字符串类名加载类,但未找到指定名称的类的定义时抛出该异常:

  • Class.forName() 方法。
  • ClassLoader.findSystemClass() 方法。
  • ClassLoader.loadClass() 方法。
  1. /**
  2. * Thrown if the Java Virtual Machine or a ClassLoader instance
  3. * tries to load in the definition of a class (as part of a normal method call
  4. * or as part of creating a new instance using the new expression)
  5. * and no definition of the class could be found.
  6. * <p>
  7. * The searched-for class definition existed when the currently
  8. * executing class was compiled, but the definition can no longer be
  9. * found.
  10. */
  11. public class NoClassDefFoundError {}
复制代码
Java 虚拟机或类加载器在实验加载某个类的定义时(比方作为方法调用的一部分或利用 new 关键字创建新实例时),无法找到该类的定义,则会抛出 NoClassDefFoundError。
在当前执行的代码在编译时能找到并引用了所需的类定义,但在运行时,JVM 却找不到这个类的定义,则会抛出 NoClassDefFoundError。
下面,我们结合场景示例加深一下这两种异常的理解:

  • ClassNotFoundException 通常是在类加载阶段,利用 Class.forName() 等方式加载类时,找不到类的字节码文件(.class 文件)导致的。
场景示例:加载一个不存在的类时
  1. public class ClassNotFoundDemo {
  2.     public static void main(String[] args) {
  3.         try {
  4.             // 不存在的类 com.johnny.NonExistentClass
  5.             Class.forName("com.johnny.NonExistentClass");
  6.         } catch (ClassNotFoundException e) {
  7.             e.printStackTrace();
  8.         }
  9.     }
  10. }
复制代码
运行结果
  1. java.lang.ClassNotFoundException: com.johnny.NonExistentClass
  2.         at java.net.URLClassLoader.findClass(URLClassLoader.java:387)
  3.         at java.lang.ClassLoader.loadClass(ClassLoader.java:418)
  4.         at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:359)
  5.         at java.lang.ClassLoader.loadClass(ClassLoader.java:351)
  6.         at java.lang.Class.forName0(Native Method)
  7.         at java.lang.Class.forName(Class.java:264)
  8.         at com.tender.ClassNotFoundDemo.main(ClassNotFoundDemo.java:7)
复制代码

  • NoClassDefFoundError 是在编译时类是存在的,但在运行时 JVM 无法找到该类的定义。
场景示例:编译时存在,运行时不存在
  1. class HelperClass {
  2. }
  3. public class MainClass {
  4.     public static void main(String[] args) {
  5.         HelperClass h = new HelperClass();
  6.     }
  7. }
复制代码
运行结果
  1. Exception in thread "main" java.lang.NoClassDefFoundError: com/johnny/HelperClass
  2.         at com.johnny.MainClass.main(MainClass.java:7)
  3. Caused by: java.lang.ClassNotFoundException: com.johnny.HelperClass
  4.         at java.net.URLClassLoader.findClass(URLClassLoader.java:387)
  5.         at java.lang.ClassLoader.loadClass(ClassLoader.java:418)
  6.         at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:359)
  7.         at java.lang.ClassLoader.loadClass(ClassLoader.java:351)
  8.         ... 1 more
复制代码
MainClass 与 HelperClass 编译后会生成两个 class 文件 MainClass.class 与 HelperClass.class,现手动将编译后的 HelperClass.class 文件删除,然后执行 MainClass 的 main 方法,执行结果会抛出 NoClassDefFoundError,因为 JVM 无法找到 HelperClass 的定义,因为它对应的 .class 文件已经被删除。
场景示例:类的静态初始化失败,后续继续引用该类时
  1. class FaultyClass {
  2.     static {
  3.         // 模拟类静态初始化失败的情况
  4.         System.out.println(1/0);
  5.     }
  6.     public static void printMessage() {
  7.         System.out.println("这里输出一个消息。");
  8.     }
  9. }
  10. public class NoClassDefFoundErrorExample {
  11.     static {
  12.         try {
  13.             // 加载 FaultyClass
  14.             Class.forName("com.johnny.FaultyClass");
  15.         } catch (ClassNotFoundException e) {
  16.             // 这里不会触发,因为类确实存在
  17.             e.printStackTrace();
  18.         } catch (ExceptionInInitializerError e) {
  19.             // 类静态初始化失败,会抛出该异常
  20.             System.out.println("静态初始化失败: " + e.getCause().getMessage());
  21.         }
  22.     }
  23.     public static void main(String[] args) {
  24.         try {
  25.             // 这里再次使用 FaultyClass 去调用它的静态方法,此时会抛出 NoClassDefFoundError
  26.             System.out.println("类静态初始化失败后,再次使用 FaultyClass 的静态方法...");
  27.             FaultyClass.printMessage();
  28.         } catch (NoClassDefFoundError e) {
  29.             // 捕获 NoClassDefFoundError
  30.             System.out.println("捕获到 NoClassDefFoundError: " + e.getMessage());
  31.             e.printStackTrace();
  32.         }
  33.     }
  34. }
复制代码
运行结果
  1. 静态初始化失败: / by zero
  2. 类静态初始化失败后,再次使用 FaultyClass 的静态方法...
  3. 捕获到 NoClassDefFoundError: Could not initialize class com.johnny.FaultyClass
  4. java.lang.NoClassDefFoundError: Could not initialize class com.johnny.FaultyClass
  5.         at com.johnny.NoClassDefFoundErrorExample.main(NoClassDefFoundErrorExample.java:32)
  6. Caused by: java.lang.ExceptionInInitializerError: Exception java.lang.ArithmeticException: / by zero [in thread "main"]
  7.         at com.johnny.FaultyClass.<clinit>(NoClassDefFoundErrorExample.java:6)
  8.         at java.lang.Class.forName0(Native Method)
  9.         at java.lang.Class.forName(Class.java:264)
  10.         at com.johnny.NoClassDefFoundErrorExample.<clinit>(NoClassDefFoundErrorExample.java:18)
复制代码
从这个示例中,我们可以看到,当我们实验利用 Class.forName() 去加载 FaultyClass 时,由于在该类的静态代码块中出现了除零异常,所以会导致类静态初始化失败,从而抛出 ExceptionInInitializerError 错误,我们 catch 了这个异常却没有做任何的处理,因此在后续的逻辑中再次引用该类时,便会抛出 NoClassDefFoundError。
总结:

  • ClassNotFoundException 是在加载阶段发生的,当 JVM 无法从外存储器找到并加载指定的类时抛出。
  • NoClassDefFoundError 是在链接阶段发生的,当 JVM 在内存中无法找到已经加载过的类的定义时抛出。
增补一下:
根据《Java 虚拟机规范》,类加载分为三个主要阶段:加载(Loading)、链接(Linking)、初始化(Initialization)。其中链接又细分为验证(Verification)、预备(Preparation)、解析(Resolution)。
Java 中的异常处理机制

在 Java 中,异常处理机制为:抛出异常,捕获异常。
try-catch-finally 语句

try-catch-finally 语句用于捕获异常,是 Java 中处理异常的核心机制,由 try、catch、finally 3 个语句块组合而成。
每个语句块的作用


  • try 块:是 try-catch-finally 语句结构中的必选部分,用于包裹可能出现异常的代码,后边可接零或多个 catch 块,如果没有 catch 块,则必须跟一个 finally 块。
  • catch 块:捕获并处来由 try 块中抛出的特定范例的异常,可以有多个 catch 块来处理不同范例的异常,多个 catch 块时,按异常范例逐一匹配,找到与之对应的 catch 块进行处理,如果没有匹配的 catch 块,则异常将传播到调用栈上的更高层。
  • finally 块:是可选的,无论是否发生异常都会执行的代码块,通常用于开释资源(如关闭文件流或数据库毗连)。
语法示例
  1. try {
  2.         // 包裹可能会发生异常的程序代码
  3. } catch (ExceptionType1 e1){
  4.         // 捕获并处理 try 块中抛出的异常 e1
  5. } catch (ExceptionType2 e2){
  6.          // 捕获并处理 try 块中抛出的异常 e2
  7. } finally {
  8.         // 无论是否发生异常都会执行的语句块
  9. }
复制代码
finally 块中的代码肯定会执行吗?

理论上,finally 块中的代码是无论是否发生异常都会执行的代码块,但是在一些特殊情况下,finally 块中的代码不会被执行。这些情况包罗:

  • JVM 强制终止:好比 JVM 内存不敷或其他严重的系统题目导致 JVM 非正常退出。
  • 线程殒命:如果应用程序地点的线程殒命或被中断,finally 块的代码也不会执行。
  • System.exit() 调用:代码中如果在 finally 块前显示调用了 System.exit(),finally 块也不会执行。
不要在 finally 块中利用 return

阿里开发手册中要求:
【强制】不能在 finally 块中利用 return,finally 块中的 return 返回后方法结束执行,不会再执行 try 块中的 return 语句。
因为在 try 或 catch 块中有 return 语句时,这个返回值会被暂时生存在本地变量中,当执行到 finally 块中的 return 时,它会覆盖之前本地变量中生存的返回值,并在方法执行结束时将这个返回值返回。
代码示例:
  1. public class ReturnInFinallyExample {
  2.     public static int methodWithReturnInFinally() {
  3.         try {
  4.             return 1; // 这个返回值会被记住,但不会立即返回
  5.         } catch (Exception e) {
  6.             return 2; // 如果异常发生,这个返回值也会被记住
  7.         } finally {
  8.             System.out.println("Finally block executed");
  9.             return 3; // 最终这个返回值会覆盖之前的返回值
  10.         }
  11.     }
  12.     public static void main(String[] args) {
  13.         System.out.println("Returned value: " + methodWithReturnInFinally()); // 输出:Returned value: 3
  14.     }
  15. }
复制代码
运行结果:
  1. Finally block executed
  2. Returned value: 3
复制代码
throw 与 throws 关键字

throw 与 throws 关键字在 Java 中用来抛出异常。

  • throw:用来显式地抛出一个异常对象。当程序检测到错误条件时,可以通过 throw 来创建并抛出一个异常实例,从而中断当前方法或语句块的执行,并将控制权传递给调用栈上的更高层来处理该异常。
语法:
  1. throw new ExceptionType("Exception Message");
复制代码
ExceptionType 是需要抛出的异常范例(如 IllegalArgumentException,RuntimeException 等)。"Exception Message" 是可选的消息字符串,描述异常的详细原因。
代码示例:
  1. public class ThrowExample {
  2.     public static void validateAge(int age) {
  3.         if (age < 0) {
  4.             throw new IllegalArgumentException("Age cannot be negative.");
  5.         }
  6.         System.out.println("Valid age: " + age);
  7.     }
  8.     public static void main(String[] args) {
  9.         try {
  10.             validateAge(-5); // 将抛出异常
  11.         } catch (IllegalArgumentException e) {
  12.             System.err.println("Caught exception: " + e.getMessage());
  13.         }
  14.     }
  15. }
复制代码
在这个例子中,如果传入的年龄为负数,则会抛出 IllegalArgumentException 异常。

  • throws:用于声明一个方法可能抛出的异常列表。它用来告诉编译器和调用者,这个方法内部可能会抛出某些范例的异常,调用者需要预备好处理这些异常。throws 只是声明,而不是实际抛出异常,实际抛出由 throw 实现。
Checked Exception(检查异常)必须在方法声明利用 throws 进行声明,或者在其方法体内捕获;而 Unchecked Exception(非检查异常)不需要。
语法:
  1. public returnType methodName(parameters) throws ExceptionType1, ExceptionType2 {
  2.     // 方法体
  3. }
复制代码
ExceptionType1,ExceptionType2 是声明的方法可能抛出的异常范例。
代码示例:
  1. public class ThrowsExample {
  2.     // 声明该方法可能会抛出 IOException
  3.     public void readFile(String filePath) throws IOException {
  4.         // 模拟文件读取操作,抛出 IOException
  5.         if (!filePath.endsWith(".txt")) {
  6.             throw new IOException("File must be a text file.");
  7.         }
  8.         System.out.println("Reading file: " + filePath);
  9.     }
  10.     public static void main(String[] args) {
  11.         ThrowsExample example = new ThrowsExample();
  12.         try {
  13.             example.readFile("data.csv"); // 可能抛出异常
  14.         } catch (IOException e) {
  15.             System.err.println("Caught exception: " + e.getMessage());
  16.         }
  17.     }
  18. }
复制代码
在这个例子中,readFile 方法声明了它可能会抛出 IOException。在 main 方法中调用 readFile 时,必须处理这个潜伏的异常,否则代码无法通过编译。
throw vs. throws
关键字throwthrows目标显式抛出一个异常实例声明一个方法可能抛出的异常范例利用位置在方法体内在方法声明上影响直接导致方法或语句块的执行被中断不影响方法体内的逻辑,仅作为接口契约的一部分try-with-resources 语法

try-with-resources 语法是 Java 7 引入的一项紧张特性,为了简化资源管理并确保资源的自动关闭。适用于那些实现了 java.lang.AutoCloseable 接口的资源(如文件流、数据库毗连等),从而避免了手动关闭这些资源的繁琐操作,并可以避免因忘记关闭资源而导致的内存走漏的题目。它是一种语法糖,对利用 try-with-resources 包裹的代码进行反编译后,可以看到仍然是 try-catch-finally 结构。
try-with-resources 的基本用法
  1. try (ResourceType resource = new ResourceType()) {
  2.     // 使用资源
  3. } catch (ExceptionType e) {
  4.     // 处理异常
  5. }
复制代码
ResourceType 需实现 AutoCloseable 接口,资源声明和初始化直接放在 try 括号内,当 try 块执行完毕后,无论是否发生异常,所有声明的资源都将被自动关闭。
为什么需要 try-with-resources

在 Java 7 之前,我们利用像 InputStream、OutputStream 这样的资源时,通常需要遵循以下步骤:

  • 打开资源。
  • 利用资源。
  • 在 finally 块中关闭资源。
好比:
  1. public static void main(String[] args) {
  2.     BufferedReader br = null;
  3.     try {
  4.         br = new BufferedReader(new FileReader("data.txt"));
  5.         String line;
  6.         while ((line = br.readLine()) != null) {
  7.             System.out.println(line);
  8.         }
  9.     } catch (IOException e) {
  10.         // 处理异常
  11.     } finally {
  12.         if (br != null) {
  13.             try {
  14.                 br.close();
  15.             } catch (IOException ex) {
  16.                 // 处理 close() 方法抛出的异常
  17.             }
  18.         }
  19.     }
  20. }
复制代码
这种编码不仅冗长,而且容易堕落。如果我们在开启资源后立即抛出异常的话,则可能会跳过 finally 块中的资源关闭逻辑;另外,我们还需要处理 close() 方法本身可能抛出的异常。
try-with-resources 通过引入更简洁的语法来办理这些题目,确保每个资源都会被正确关闭,即使发生异常也不会遗漏。利用 try-with-resources 后,代码简化如下:
  1. public static void main(String[] args) {
  2.         try (BufferedReader br = new BufferedReader(new FileReader("data.txt"))) {
  3.             String line;
  4.             while ((line = br.readLine()) != null) {
  5.                 System.out.println(line);
  6.             }
  7.         } catch (IOException e) {
  8.             System.err.println("Caught exception: " + e.getMessage());
  9.         }
  10.     }
复制代码
同时,try-with-resources 语句可以同时管理多个资源,只需将它们放在 try 括号内并用分号隔开即可,每个资源都会按照声明的逆序依次关闭
什么是逆序关闭,好比程序中先开启了 ResourceA ,又开启了 ResourceB,那么执行关闭的时候,先关闭 ResourceB,再关闭 ResourceA,这就像我们回家开门进屋一样,从大门依次开门进屋,出去的时候要先从里边开始向外锁门。
为什么逆序关闭?
因为开启的资源间可能会有依赖关系,有依赖关系时,通常是先关闭依赖于其他资源的资源。
好比操作数据库时:打开数据库毗连 (Connection)-->创建语句对象 (Statement)-->执行查询并获取结果集 (ResultSet)。
在这种情况下,ResultSet 依赖于 Statement,而 Statement 又依赖于 Connection。如果按照打开资源的顺序关闭这些资源时,好比先关闭了 Connection,那么当我们再去关闭 Statement 时,可能已经失去了对 Statement 的访问权限,这很可能导致资源泄露。
try-with-resources 的优势


  • 自动资源管理:不再需要显式地调用 close() 方法,简化了代码结构。
  • 减少错误风险:即使发生异常,资源也会被正确关闭,避免了资源泄露的风险。
  • 进步可读性:代码更加紧凑且易于理解,减少了不必要的样板代码。
结语

好了,写到这里,关于 Java 中异常体系相关的知识基本就介绍完了,希望通过这篇文章能资助大家更好地理解和运用 Java 中的 Exception 和 Error。如果你有任何进一步的题目或需要更详细的表明,接待评论区留言!
您的鼓励对我持续创作非常关键,如果本文对您有资助,请记得点赞、分享、在看哦~~~谢谢!
更多精彩内容,请微信搜刮并关注【Java驿站】公众号。

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

尚未崩坏

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表