Java之异常
Java异常
基本语法
在 Java 中,异常处理的基本语法主要包括
try
、catch
、finally
、throw
和 throws
关键字。
1. try
和 catch
try
代码块用于包围可能会抛出异常的代码。catch
代码块用于捕获并处理try
块中抛出的异常。
语法:
1 | try { |
示例:
1 | try { |
2. finally
finally
代码块用于在try
和catch
执行完毕后,执行一定的清理工作,无论是否发生异常。finally
块总是会执行,除非程序提前终止(如调用System.exit()
)。
语法:
1 | try { |
示例:
1 | try { |
3. throw
throw
关键字用于显式地抛出一个异常对象。它可以在方法内的任何地方使用。
语法:
1 | throw new ExceptionType("异常信息"); |
示例:
1 | public void checkAge(int age) { |
4. throws
throws
关键字用于在方法声明中指明该方法可能抛出的异常类型。调用这个方法时,必须处理这些异常,通常通过try-catch
或者继续声明throws
。
语法:
1 | public void someMethod() throws ExceptionType1, ExceptionType2 { |
示例:
1 | public void readFile(String filePath) throws IOException { |
5. 自定义异常
- 可以通过继承
Exception
类来定义自己的异常类型。
语法:
1 | public class MyCustomException extends Exception { |
示例:
1 | public class MyCustomException extends Exception { |
异常处理的深入探讨
1. 捕获并处理异常
- 精细化捕获:在异常处理过程中,使用精细化的异常捕获策略。避免捕获过于宽泛的异常类型,如
Exception
,应尽量捕获具体的异常类型。这有助于明确错误的来源并提供更有针对性的处理方式。例如,如果你知道代码中可能出现IOException
和NullPointerException
,应该分别捕获它们:
1
2
3
4
5
6
7
8
9try {
// 可能抛出异常的代码
} catch (IOException e) {
// 处理IO异常
e.printStackTrace();
} catch (NullPointerException e) {
// 处理空指针异常
e.printStackTrace();
}
- 嵌套异常捕获:在一些情况下,可能需要在同一段代码中捕获多个异常,并在不同层次上进行处理。例如,你可以在较低层次捕获具体异常并重新抛出一个更通用的异常:
1
2
3
4
5
6
7public void readFile(String fileName) throws CustomException {
try {
// 可能抛出IOException的代码
} catch (IOException e) {
throw new CustomException("Error reading file: " + fileName, e);
}
}
通过这种方式,调用者可以捕获
CustomException
,而不需要了解具体的
IOException
细节。
2. 区分业务异常和系统异常
业务异常处理:业务异常是应用程序运行过程中可以预见并处理的错误。例如,用户输入的格式错误、数据缺失等。这类异常通常继承自
Exception
,可以在应用层中捕获并处理。系统异常处理:系统异常,如
OutOfMemoryError
或StackOverflowError
,表示 JVM 层面的严重错误。这类异常通常无法恢复,应避免捕获并在适当的位置进行日志记录,以便于排查问题。
1
2
3
4
5
6try {
// 可能导致系统异常的代码
} catch (Error e) {
// 记录系统异常,通常不建议进行恢复处理
logError(e);
}
3. 使用自定义异常
- 自定义异常类:自定义异常不仅可以帮助明确问题的类型,还能携带特定的错误信息和上下文。例如,一个订单处理系统可能有特定的业务异常,如
OrderNotFoundException
或InvalidOrderStateException
。
1
2
3
4
5public class OrderNotFoundException extends Exception {
public OrderNotFoundException(String orderId) {
super("Order not found: " + orderId);
}
}
在业务逻辑中,捕获和处理这些异常可以提供更有针对性的错误反馈和解决方案。
4. 避免捕获
Throwable
- 捕获
Throwable
会捕获所有的错误和异常,包括Error
,这些通常代表系统级别的严重错误,如OutOfMemoryError
。这类错误不应在正常的应用逻辑中捕获,除非有明确的理由和相应的处理策略。例如,在一些高度关键的系统中,可能需要捕获所有异常并安全地关闭系统。
5. finally
的使用
- 资源清理:
finally
块通常用于释放资源,如关闭文件流、数据库连接等。即使在try
块中出现异常,finally
块中的代码也会被执行,从而保证资源的正确释放。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15FileInputStream fis = null;
try {
fis = new FileInputStream("file.txt");
// 处理文件
} catch (IOException e) {
// 处理异常
} finally {
if (fis != null) {
try {
fis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
在 Java 7 及更高版本中,可以使用 try-with-resources
语法来自动关闭资源:
1
2
3
4
5try (FileInputStream fis = new FileInputStream("file.txt")) {
// 处理文件
} catch (IOException e) {
// 处理异常
}
6. 使用 throws
关键字
- 向上传递异常:在某些情况下,方法内部无法处理异常,可以使用
throws
将异常抛出,由调用者来处理。这种方式常用于库或框架开发中,允许用户决定如何处理特定的异常。
1
2
3
4
5
6public void processData(String data) throws InvalidDataException {
if (data == null || data.isEmpty()) {
throw new InvalidDataException("Data cannot be null or empty");
}
// 处理数据
}
7. 避免空异常处理
- 合理的异常处理:空异常处理意味着捕获异常后没有采取任何操作,这样可能会掩盖潜在问题,导致更严重的错误。在最低限度下,应该记录异常信息,确保问题能够被识别和排查。
1
2
3
4
5
6try {
// 可能抛出异常的代码
} catch (Exception e) {
// 记录异常而不是忽略
logException(e);
}
实际应用场景
处理数据库连接异常
在处理数据库操作时,异常处理非常重要,尤其是在连接、查询或更新数据时。以下是一个示例,展示了如何处理数据库连接异常并确保资源释放:
1 | import java.sql.Connection; |
捕获异常详解
Java 异常处理简化
1. 基本用法
- 在
try
块中放置可能抛出异常的代码,并使用catch
捕获异常:
1
2
3
4
5try {
process1();
} catch (IOException e) {
System.out.println(e);
}
1.
try
块
- 作用:
try
块用于包裹可能会抛出异常的代码。它表示程序尝试执行其中的代码,并准备应对潜在的错误。- 示例说明:在这段代码中,
process1()
方法是一个可能会抛出异常的操作。例如,process1()
可能是读取文件、网络连接或其他 I/O 操作。如果在执行process1()
时发生异常,这个异常将被抛出,并立即跳转到catch
块。2.
catch
块
作用:
catch
块用于捕获在try
块中抛出的异常。它接收一个特定类型的异常对象,并定义了处理这种异常的代码。
IOException
:IOException
是 Java 中的一个异常类,表示在进行输入输出操作时发生的错误。IOException
是Exception
类的一个子类,专门处理I/O操作的异常情况,如文件无法读取、网络中断等。
1 catch (IOException e)
- 含义:这个
catch
块表示它专门捕获IOException
类型的异常。如果process1()
抛出一个IOException
,程序将执行catch
块中的代码。异常对象
e
:e
是一个IOException
类型的对象,它包含了有关异常的信息,如错误消息、堆栈跟踪等。你可以使用这个对象来获取更多关于异常的细节。
1 System.out.println(e);
- 输出异常信息:
System.out.println(e);
会调用异常对象的toString()
方法,输出该异常的详细信息(通常包括异常的类名和错误消息)。这是调试时常用的方法,可以帮助程序员了解出错的原因和位置。这段代码的功能是:
- 尝试执行
process1()
方法,如果没有异常发生,程序会正常继续执行。- 如果
process1()
抛出了IOException
,程序会跳转到catch
块,捕获异常并打印异常信息。- 通过这种方式,程序可以在遇到 I/O 错误时优雅地处理错误,而不是直接崩溃或抛出未处理的异常。
2. 多 catch
语句
- 可以使用多个
catch
捕获不同类型的异常,顺序很重要,子类异常要放在前面:
1
2
3
4
5
6
7try {
process1();
} catch (UnsupportedEncodingException e) {
System.out.println("Bad encoding");
} catch (IOException e) {
System.out.println("IO error");
}
1.
try
块
- 作用:
try
块用于包含可能会抛出异常的代码。程序将尝试执行其中的代码,并准备在发生异常时跳转到相应的catch
块处理异常。- 示例说明:在这段代码中,
process1()
是一个可能会抛出异常的方法。假设process1()
涉及处理文件或字符编码,那么它可能会抛出UnsupportedEncodingException
或IOException
。2.
catch
块
- 作用:
catch
块用于捕获在try
块中抛出的特定类型的异常。每个catch
块处理一种特定类型的异常。第一个
catch
块:UnsupportedEncodingException
1
2
3 catch (UnsupportedEncodingException e) {
System.out.println("Bad encoding");
}
UnsupportedEncodingException
:这是一个异常类,用于表示尝试使用不受支持的字符编码时发生的错误。例如,如果试图将字节流转换为字符串,而指定的编码不受支持,就会抛出这个异常。- 异常处理:如果
process1()
抛出了UnsupportedEncodingException
,程序会跳转到这个catch
块,执行其中的代码。- 输出消息:
System.out.println("Bad encoding");
会输出 "Bad encoding"。这通常用于告知用户或开发者,操作失败是由于不受支持的字符编码导致的。第二个
catch
块:IOException
1
2
3 catch (IOException e) {
System.out.println("IO error");
}
IOException
:这是一个更通用的异常类,表示在进行输入/输出操作时发生的错误。IOException
是UnsupportedEncodingException
的父类,通常用于处理文件读取、网络通信等操作中的错误。- 异常处理:如果
process1()
抛出了其他类型的IOException
(但不是UnsupportedEncodingException
),程序会跳转到这个catch
块。- 输出消息:
System.out.println("IO error");
会输出 "IO error"。这用于告知用户或开发者,操作失败是由于输入/输出错误导致的。3.
catch
块的顺序
顺序的重要性:在多个
catch
块中,子类异常(如UnsupportedEncodingException
)必须放在父类异常(如IOException
)之前。否则,子类异常会被父类异常捕获,导致子类异常的catch
块永远不会被执行。示例说明:在这个代码中,
UnsupportedEncodingException
是IOException
的子类。因此,必须先捕获UnsupportedEncodingException
,然后再捕获更广泛的IOException
。这样可以确保具体的异常被适当处理。这段代码的功能是:
- 尝试执行
process1()
方法。- 如果
process1()
抛出UnsupportedEncodingException
,程序会输出 "Bad encoding"。- 如果
process1()
抛出其他类型的IOException
,程序会输出 "IO error"。- 通过这种方式,程序可以根据不同的异常类型提供不同的错误处理方式,使得错误信息更加明确和有针对性。
3. finally
语句
finally
块无论是否发生异常都会执行,通常用于清理工作:
1
2
3
4
5
6
7try {
process1();
} catch (IOException e) {
System.out.println("IO error");
} finally {
System.out.println("END");
}
1.
try
块
- 作用:
try
块包含了可能会抛出异常的代码。程序首先会尝试执行try
块中的代码。如果在执行过程中抛出异常,程序会跳转到相应的catch
块进行处理。- 示例说明:在这段代码中,
process1()
是一个可能会抛出异常的方法。假设process1()
涉及文件操作或网络通信,那么它可能会抛出IOException
。2.
catch
块
- 作用:
catch
块用于捕获和处理在try
块中抛出的特定类型的异常。在这个例子中,catch
块捕获并处理IOException
异常。
IOException
IOException
:这是一个广泛使用的异常类,表示在进行输入/输出操作时可能发生的错误。它涵盖了许多不同的I/O相关错误,例如文件未找到、网络连接失败等。- 异常处理:如果
process1()
抛出了IOException
,程序会跳转到catch
块,执行其中的代码。- 输出消息:
System.out.println("IO error");
会输出 "IO error"。这表示发生了某种输入/输出错误,程序将通知用户或开发者具体发生了什么问题。3.
finally
块
- 作用:
finally
块中的代码无论是否抛出异常都会执行。它通常用于清理资源或执行必须执行的收尾操作。- 示例说明:在这段代码中,
System.out.println("END");
位于finally
块中。因此,无论process1()
是否抛出异常,程序都会执行这个println
语句。
finally
块的执行流程
- 无异常时:如果
process1()
没有抛出任何异常,程序将按顺序执行try
块中的代码,跳过catch
块,直接执行finally
块。- 有异常时:如果
process1()
抛出了IOException
,程序将停止执行try
块中的剩余代码,跳转到catch
块,执行相应的异常处理代码,然后继续执行finally
块中的代码。4. 执行流程总结
正常流程:当
process1()
正常执行时,程序输出 "END"。异常流程:当
process1()
抛出IOException
时,程序先输出 "IO error",然后输出 "END"。5.
finally
块的意义
资源管理:
finally
块通常用于释放资源(如关闭文件、断开网络连接等),以确保资源在异常发生后仍然得到正确处理。确保执行:即使代码在
try
块或catch
块中返回了值、抛出了异常或跳出方法(例如使用return
),finally
块中的代码仍然会被执行。这段代码的功能是:
- 尝试执行
process1()
方法。- 如果
process1()
抛出IOException
,程序会输出 "IO error" 以通知发生了I/O错误。- 无论是否抛出异常,程序都会执行
finally
块,输出 "END"。- 这种结构确保了程序在异常处理后能够进行一些必要的操作,比如资源清理。
4. 合并捕获多种异常
- 如果多个异常的处理逻辑相同,可以使用
|
合并捕获:
1
2
3
4
5
6
7try {
process1();
} catch (IOException | NumberFormatException e) {
System.out.println("Bad input");
} catch (Exception e) {
System.out.println("Unknown error");
}
1.
try
块
- 作用:
try
块包含了可能会抛出异常的代码。程序会首先尝试执行try
块中的代码。如果在执行过程中抛出异常,程序会跳转到相应的catch
块进行处理。- 示例说明:在这段代码中,
process1()
是一个可能抛出异常的方法,具体的异常类型可以是IOException
、NumberFormatException
或其他类型的异常。2. 第一个
catch
块多异常捕获 (
IOException | NumberFormatException
)
- 作用:该
catch
块用于捕获IOException
和NumberFormatException
这两种类型的异常,并且使用|
符号将它们合并在一起。- 异常解释:
IOException
:表示在输入/输出操作时可能发生的错误,比如读取文件时发生错误。NumberFormatException
:表示当尝试将字符串转换为数值类型时,字符串格式不正确,从而引发的异常。- 处理方式:如果
process1()
抛出了这两种异常中的任何一种,程序会跳转到这个catch
块执行,输出 "Bad input"。- 示例:
- 如果
process1()
尝试读取文件但失败了,抛出IOException
,会输出 "Bad input"。- 如果
process1()
尝试将一个非法的字符串转换为数字,抛出NumberFormatException
,也会输出 "Bad input"。3. 第二个
catch
块捕获
Exception
- 作用:这个
catch
块用于捕获所有其他未在之前catch
块中明确处理的异常。Exception
是 Java 中所有异常的父类,所以这个catch
块可以捕获除了IOException
和NumberFormatException
之外的任何其他异常。- 处理方式:如果
process1()
抛出了除IOException
和NumberFormatException
之外的其他异常,程序会跳转到这个catch
块,输出 "Unknown error"。- 作用:这是一个兜底的
catch
块,用于处理那些可能未预见到的异常情况,确保即使出现未知错误,程序也能处理并继续执行。4. 执行流程总结
正常流程:当
process1()
正常执行且未抛出任何异常时,catch
块不会被触发,程序继续向下执行。异常流程:
- 如果
process1()
抛出IOException
或NumberFormatException
,会触发第一个catch
块,输出 "Bad input"。- 如果抛出其他类型的异常(未被前面
catch
捕获的异常),会触发第二个catch
块,输出 "Unknown error"。多异常捕获:通过使用
|
符号,多个异常类型可以在同一个catch
块中进行捕获和处理,简化了代码,减少了冗余。通用异常捕获:
Exception
作为最后的catch
,确保了程序可以捕获和处理所有未明确指定的异常,增加了程序的健壮性。
小细节总结:
- 子类异常放在前面,多个异常处理相同可以合并到一起。
抛出异常详解
异常的传播与抛出
当 Java
程序执行过程中发生错误时,通常会抛出一个异常对象(Exception
),该对象封装了错误的详细信息。异常一旦被抛出,如果在当前方法中没有被捕获,便会向上层调用方法传播,直到遇到一个适当的
try-catch
块进行处理为止。如果一直没有被捕获,最终会被 JVM
捕获并终止程序运行。
1. 异常的传播
以下代码演示了异常的传播过程:
1 | public class Main { |
在这个例子中:
process2()
方法试图将null
转换为整数,导致NumberFormatException
异常被抛出。- 由于
process2()
没有捕获这个异常,异常会向上传播到调用它的process1()
。 - 同样地,
process1()
也没有捕获该异常,异常继续向上传播到main()
方法。 - 最终,
main()
方法捕获了这个异常,并使用printStackTrace()
打印出异常栈轨迹。
异常栈轨迹:
1 | java.lang.NumberFormatException: null |
栈轨迹中详细列出了异常发生的调用链,从最深层的调用开始,逐层向上直到
main()
方法。这对于调试错误非常有用,可以快速定位问题发生的具体位置。
当一个方法抛出异常且没有在该方法内部捕获异常时,异常会沿着调用栈向上传播,直到遇到合适的
try...catch
语句进行捕获和处理。代码解析
main()
方法
- 这是程序的入口点。在
main()
方法中,用try...catch
语句包裹了对process1()
方法的调用。- 如果
process1()
方法或它调用的任何方法抛出异常,异常会被捕获,并由catch (Exception e)
处理。
process1()
方法
process1()
方法调用了process2()
方法。- 这个方法本身没有任何异常处理机制,所以如果
process2()
抛出异常,process1()
也不会处理,而是会将异常继续向上传播到调用它的main()
方法。
process2()
方法
process2()
方法使用Integer.parseInt(null)
试图将null
转换为一个整数。- 因为
null
不能转换为整数,这会导致NumberFormatException
异常被抛出。异常传播机制
- 异常的抛出:
- 当
Integer.parseInt(null)
执行时,Java 会抛出NumberFormatException
异常。此异常说明输入的字符串不能转换为有效的整数。- 异常的传播:
NumberFormatException
首先在process2()
方法中抛出,但process2()
没有任何捕获异常的机制,因此异常会被自动传播到调用process2()
的方法process1()
。process1()
同样没有处理这个异常,所以异常继续传播到main()
方法。- 异常的捕获与处理:
- 异常最终被
main()
方法的try...catch
块捕获。catch (Exception e)
捕获到异常后,调用e.printStackTrace()
方法打印出异常的堆栈跟踪信息。堆栈跟踪信息
假设运行上述代码后,输出的堆栈跟踪信息如下:
1
2
3
4
5
6 java.lang.NumberFormatException: null
at java.base/java.lang.Integer.parseInt(Integer.java:614)
at java.base/java.lang.Integer.parseInt(Integer.java:770)
at Main.process2(Main.java:16)
at Main.process1(Main.java:12)
at Main.main(Main.java:5)
- java.lang.NumberFormatException: null: 说明异常的类型是
NumberFormatException
,且由于null
不能被转换为整数导致异常。- at Main.process2(Main.java:16): 这表示异常在
Main
类的process2()
方法中第 16 行发生(即Integer.parseInt(null)
这一行)。- at Main.process1(Main.java:12): 异常继续传播到
process1()
方法,并在第 12 行被记录。- at Main.main(Main.java:5): 最后,异常传播到
main()
方法,并在第 5 行被捕获和处理。通过
printStackTrace()
打印的堆栈跟踪信息,可以追踪到异常是如何从Integer.parseInt(null)
一直传播到main()
方法的。这对调试非常有帮助,因为它能够精确定位问题发生的位置,以及异常是如何在调用栈中传播的。
2. 抛出异常
当发生错误时,我们可以主动抛出异常来终止执行或者通知调用者某些错误情况。抛出异常通常涉及两个步骤:
- 创建异常对象:用特定的异常类实例化一个对象。
- 用
throw
语句抛出异常:将该异常对象抛出,终止当前的执行路径。
示例:
1 | void process2(String s) { |
在这个例子中,如果 s
为 null
,会抛出一个
NullPointerException
异常。这样,调用
process2()
的代码可以捕获到这个异常并做出相应处理。
这段代码展示了如何在 Java 中手动抛出异常,尤其是在遇到非法或不合适的输入时抛出一个特定类型的异常。具体来说,这里在输入字符串为
null
时抛出了NullPointerException
异常。代码解析
1
2
3
4
5 void process2(String s) {
if (s == null) {
throw new NullPointerException();
}
}1. 方法签名
1 void process2(String s)
void
表示这个方法没有返回值。- 方法
process2
接受一个String
类型的参数s
。2. 输入参数检查
1 if (s == null) {
- 这行代码检查传入的参数
s
是否为null
。null
在 Java 中表示一个对象引用没有指向任何实际的对象。对于字符串来说,null
意味着没有提供任何有效的字符串。3. 抛出异常
1 throw new NullPointerException();
- 如果条件
s == null
为真(即传入的字符串为null
),代码会执行throw new NullPointerException();
。throw
关键字用于在方法中抛出一个异常。new NullPointerException()
创建了一个新的NullPointerException
实例。NullPointerException
是 Java 中的一个内建异常类,通常在尝试访问null
对象的成员时会自动抛出。不过,这里通过显式地使用throw
语句手动抛出了该异常。目的和应用场景
- 防止非法输入:
- 这段代码的主要目的在于防止方法处理
null
值,从而避免在后续代码中可能由于null
导致的异常问题(例如NullPointerException
)。通过提前抛出异常,方法明确表示不接受null
作为有效输入。- 提高代码的健壮性:
- 通过对输入参数的严格检查,程序可以更早地发现问题,并且可以更明确地告诉调用者出现了什么问题(在这个例子中,即调用者传递了一个
null
而不是预期的字符串)。- 异常的手动抛出:
- 通常情况下,
NullPointerException
是在尝试调用null
对象的方法或访问其字段时由 Java 运行时自动抛出的。但是在这种情况下,开发者可以主动选择在何时、为何条件抛出该异常,从而更好地控制程序的执行流。
3. 转换异常类型
有时候,捕获到某个异常后,我们可能希望转换为另一种更适合当前语境的异常类型。这时可以在
catch
块中抛出新的异常:
1 | void process1() { |
在这个例子中:
process2()
抛出NullPointerException
后被process1()
捕获。- 然后
process1()
将其转换为IllegalArgumentException
并再次抛出。
这段代码演示了如何在 Java 中捕获异常并将其转换成另一种异常。这是处理异常时的一种常见做法,尤其是在需要将一个异常类型映射到另一个异常类型时。
代码解析
1
2
3
4
5
6
7 void process1() {
try {
process2();
} catch (NullPointerException e) {
throw new IllegalArgumentException();
}
}1. try 块
1
2
3 try {
process2();
}
try
块中的代码调用了process2()
方法。这个方法可能会抛出异常。2. catch 块
1
2
3 catch (NullPointerException e) {
throw new IllegalArgumentException();
}
catch
块用于捕获try
块中抛出的异常。在这里,它捕获NullPointerException
类型的异常。- 捕获到
NullPointerException
后,代码会抛出一个新的IllegalArgumentException
异常。这个新的异常与捕获的异常类型不同,通常用于转换异常类型以适应调用者的错误处理逻辑。目的
- 异常转换:
process1
捕获了process2
方法抛出的NullPointerException
,然后抛出了一个新的IllegalArgumentException
。这种转换可以将低层次的、可能不易理解的异常(如NullPointerException
)映射到更高层次的、更具描述性的异常(如IllegalArgumentException
),以更好地表明出错的上下文。- 隐藏实现细节:
- 通过抛出
IllegalArgumentException
,process1
方法可以隐藏process2
方法的具体实现细节。调用者只会看到IllegalArgumentException
,而不知道实际是由NullPointerException
导致的。示例用法
如果
process2()
方法在处理时遇到了null
值,它会抛出NullPointerException
。process1()
方法捕获这个异常并将其转换为IllegalArgumentException
。这种做法通常用于将底层的实现异常映射到更具业务逻辑的异常,以便调用者能够更好地理解错误的上下文并采取适当的措施。
4. 保持原始异常信息
如果在捕获异常后抛出新的异常,为了保留原始异常的信息,可以将原始异常作为参数传递给新抛出的异常对象。这样,异常链的信息不会丢失:
1 | void process1() { |
在上述代码中,IllegalArgumentException
包含了
NullPointerException
的信息,运行结果如下:
1 | java.lang.IllegalArgumentException: java.lang.NullPointerException |
解释如下:
在 Java 中,当我们捕获一个异常并抛出一个新的异常时,通常希望保留原始异常的信息,以便能够追踪到最初的问题源头。将原始异常作为参数传递给新抛出的异常对象,可以实现这一点。
异常链
在你的代码中,
IllegalArgumentException
被用来包装NullPointerException
。这样做的目的是保留原始异常的信息,使得后续的异常处理能够了解最初的错误原因。代码解析
1
2
3
4
5
6
7 void process1() {
try {
process2();
} catch (NullPointerException e) {
throw new IllegalArgumentException(e);
}
}1. 捕获原始异常
1 catch (NullPointerException e) {
- 这里捕获了
process2
方法抛出的NullPointerException
。2. 抛出新的异常
1 throw new IllegalArgumentException(e);
- 抛出了一个新的
IllegalArgumentException
,并将原始的NullPointerException
作为参数传递给IllegalArgumentException
的构造函数。- 这样,
IllegalArgumentException
就会“链入”NullPointerException
的信息,即在IllegalArgumentException
的getCause()
方法中能够获取到原始的异常信息。异常输出
当异常链存在时,异常输出将包含完整的异常信息链。运行结果如下:
1
2
3
4
5
6 java.lang.IllegalArgumentException: java.lang.NullPointerException
at Main.process1(Main.java:15)
at Main.main(Main.java:5)
Caused by: java.lang.NullPointerException
at Main.process2(Main.java:20)
at Main.process1(Main.java:13)输出解释:
java.lang.IllegalArgumentException: java.lang.NullPointerException
:
- 表示发生了
IllegalArgumentException
异常,消息中包含了NullPointerException
的信息。Caused by: java.lang.NullPointerException
:
- 说明
IllegalArgumentException
是由NullPointerException
引发的。这里的Caused by
提供了原始异常的详细信息和堆栈跟踪,使我们能够追踪到根本问题的来源。堆栈跟踪详细信息:
Main.process1(Main.java:15)
:
- 表示
IllegalArgumentException
异常发生在process1
方法的第15行。Main.process2(Main.java:20)
:
- 表示原始的
NullPointerException
异常发生在process2
方法的第20行。小结:
- 保留异常链信息: 通过将原始异常作为参数传递给新抛出的异常对象,我们可以保留和传递异常链信息,这样在调试和异常处理时,可以获取更完整的错误上下文和信息。
- 异常链: Java 的异常处理机制允许我们创建异常链,这有助于将低级别的异常转换为更高层次的异常,同时保留详细的异常信息。这种做法有助于将异常信息从底层传递到上层调用者,使得最终处理异常的代码能够获得更多的上下文信息。
5. 异常屏蔽与 Suppressed Exception
在异常处理过程中,如果在 finally
块中抛出异常,那么原来在 catch
块中准备抛出的异常就会被“屏蔽”,即不会再被抛出。为了保留所有异常信息,可以使用
Throwable.addSuppressed()
方法来保存被屏蔽的异常。
示例:
1 | public class Main { |
在这个例子中,即使 finally
中抛出新的异常,也会通过
addSuppressed()
保留 catch
中的异常信息。
输出结果:
1 | Exception in thread "main" java.lang.IllegalArgumentException |
这段代码展示了如何在 Java 中处理多个异常,并确保在
finally
块中抛出的异常保留原始异常信息的过程。代码使用了异常链和异常抑制技术来管理和记录异常信息。代码解析
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 public class Main {
public static void main(String[] args) throws Exception {
Exception origin = null; // 初始化一个变量,用于保存原始异常
try {
Integer.parseInt("abc"); // 故意抛出一个 NumberFormatException 异常
} catch (Exception e) {
origin = e; // 捕获异常并将其保存到 origin 变量中
throw e; // 重新抛出捕获的异常
} finally {
Exception e = new IllegalArgumentException(); // 创建一个新的异常对象
if (origin != null) {
e.addSuppressed(origin); // 将原始异常添加到新的异常对象的 suppressed 异常列表中
}
throw e; // 重新抛出新的异常
}
}
}详细步骤
初始化和异常捕获
1
2
3
4
5
6
7 Exception origin = null;
try {
Integer.parseInt("abc"); // 这行代码抛出 NumberFormatException
} catch (Exception e) {
origin = e; // 捕获异常并保存到 origin 变量
throw e; // 重新抛出捕获的异常
}
Integer.parseInt("abc")
这行代码试图将一个非法的字符串"abc"
解析为整数,因此会抛出NumberFormatException
。catch
块捕获到这个异常并将其保存到origin
变量中,然后重新抛出相同的异常。
finally
块
1
2
3
4
5
6
7 finally {
Exception e = new IllegalArgumentException(); // 创建一个新的 IllegalArgumentException 异常对象
if (origin != null) {
e.addSuppressed(origin); // 将原始异常添加到新的异常的 suppressed 异常列表中
}
throw e; // 重新抛出新的异常
}
finally
块总是会执行,尽管try
块中异常被重新抛出。- 在
finally
块中,创建了一个新的IllegalArgumentException
对象。- 使用
e.addSuppressed(origin)
方法将之前捕获的NumberFormatException
添加到新的异常对象中。这样,新异常对象会持有原始异常作为其抑制的异常(suppressed exception)。- 然后,新的异常 (
IllegalArgumentException
) 被抛出。异常链
origin
: 捕获并保存了最初的异常 (NumberFormatException
)。e.addSuppressed(origin)
: 将origin
添加到新创建的IllegalArgumentException
的抑制异常列表中。这允许最终的异常 (IllegalArgumentException
) 记录原始异常的信息,使得异常链的信息不会丢失。throw e
: 重新抛出新创建的IllegalArgumentException
异常。异常输出
运行此代码会产生以下异常信息:
1
2
3
4
5
6 java.lang.IllegalArgumentException
at Main.main(Main.java:12) // 抛出 IllegalArgumentException 的位置
Caused by: java.lang.NumberFormatException: For input string: "abc"
at java.base/java.lang.Integer.parseInt(Integer.java:652) // 原始异常发生的位置
at java.base/java.lang.Integer.parseInt(Integer.java:770)
at Main.main(Main.java:7)输出解释:
java.lang.IllegalArgumentException
:
- 这是
finally
块中抛出的新异常。Caused by: java.lang.NumberFormatException: For input string: "abc"
:
Caused by
部分显示了原始异常 (NumberFormatException
) 的详细信息。通过Caused by
部分,我们可以看到IllegalArgumentException
是由NumberFormatException
引发的。- 行号信息:
- 表明
IllegalArgumentException
是在Main.main
方法的第12行抛出的,而原始的NumberFormatException
是在Main.main
方法的第7行发生的。小结
- 异常链: 通过在
finally
块中使用addSuppressed
方法,将原始异常作为抑制异常添加到新的异常对象中,可以保留异常链信息。- 异常信息保留: 这种方法允许最终处理的异常保留有关原始异常的详细信息,帮助更好地理解和调试错误的根本原因。
- finally 块: 确保在
finally
块中处理资源或进行清理时即使发生异常也能够执行。
6. 最佳实践
- 捕获并再次抛出异常时:应保留原始异常信息,以便更容易定位问题根源。
- 避免在
finally
块中抛出异常:通常不建议在finally
中抛出异常,因为这会屏蔽try
或catch
中的异常,使调试变得复杂。
自定义异常详解
在 Java 中,异常机制是处理运行时错误的重要工具。了解如何合理地使用和自定义异常类型对于编写健壮的代码至关重要。
Java 标准库中的常用异常
Java 标准库定义了许多常用的异常类型。了解这些异常的分类有助于更好地处理和抛出异常:
Exception
- 基础异常类,所有异常类的基类。
RuntimeException
(非检查型异常,运行时异常)NullPointerException
: 访问空对象引用时抛出。IndexOutOfBoundsException
: 访问数组或集合中不存在的索引时抛出。SecurityException
: 违反安全策略时抛出。IllegalArgumentException
: 方法参数不合法时抛出。NumberFormatException
: 尝试将一个字符串解析为数字时格式不正确。
IOException
(检查型异常,输入输出异常)UnsupportedCharsetException
: 不支持的字符集异常。FileNotFoundException
: 文件未找到异常。SocketException
: 网络套接字异常。
ParseException
: 解析错误异常,一般用于解析操作(如日期、数字等)。GeneralSecurityException
: 一般安全异常,涵盖多种安全相关问题。SQLException
: 数据库操作异常,处理 SQL 语句错误时抛出。TimeoutException
: 操作超时异常,表示操作未能在预期时间内完成。
抛出标准异常
在代码中处理参数检查或其他常见问题时,使用 Java 标准库中定义的异常是一个良好的做法。例如:
1 | static void process1(int age) { |
这里,我们使用 IllegalArgumentException
来表示参数无效的情况。这种做法能使代码更具可读性和一致性。
自定义异常
在大型项目中,可能需要创建自定义异常来处理特定业务逻辑中的异常情况。创建自定义异常类时,建议遵循以下原则:
定义基础异常类
创建一个基础异常类
BaseException
,通常继承自RuntimeException
,以便所有自定义异常都能从BaseException
派生:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17public class BaseException extends RuntimeException {
public BaseException() {
super();
}
public BaseException(String message, Throwable cause) {
super(message, cause);
}
public BaseException(String message) {
super(message);
}
public BaseException(Throwable cause) {
super(cause);
}
}- 构造方法: 提供多个构造方法以支持不同的异常信息传递方式,包括消息、原因以及消息和原因的组合。
创建业务异常
基于
BaseException
创建具体的业务异常类:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17public class UserNotFoundException extends BaseException {
public UserNotFoundException() {
super();
}
public UserNotFoundException(String message) {
super(message);
}
public UserNotFoundException(String message, Throwable cause) {
super(message, cause);
}
public UserNotFoundException(Throwable cause) {
super(cause);
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17public class LoginFailedException extends BaseException {
public LoginFailedException() {
super();
}
public LoginFailedException(String message) {
super(message);
}
public LoginFailedException(String message, Throwable cause) {
super(message, cause);
}
public LoginFailedException(Throwable cause) {
super(cause);
}
}- 业务异常类: 继承
BaseException
使其适应特定业务场景。
- 业务异常类: 继承
- 使用标准异常: 在合适的场景中使用 JDK 标准库定义的异常类型可以确保代码的一致性和可读性。
- 自定义异常: 在需要处理特定业务逻辑中的异常时,可以定义自定义异常类型。继承
RuntimeException
并提供多个构造方法,可以使自定义异常更灵活。- 异常链: 保留原始异常信息可以帮助调试。在自定义异常时,确保能够传递和记录原始异常信息,避免丢失有用的异常信息。
例子:
以下是一个自定义异常的完整示例,包括定义自定义异常类和如何在实际代码中使用它们。我们将创建一个简单的应用程序,用于处理用户注册,其中包含自定义异常来处理特定的错误情况。
自定义异常类
首先,我们定义几个自定义异常类,继承自
RuntimeException
。这些异常类用于表示用户注册过程中的特定错误。
1 | // 自定义异常:用户未找到 |
使用自定义异常的业务逻辑
接下来,我们创建一个简单的用户注册系统,该系统使用上述自定义异常来处理错误情况。
1 | import java.util.HashSet; |
解释
- 自定义异常类:
UserNotFoundException
: 当用户未找到时抛出。UsernameAlreadyExistsException
: 当尝试注册一个已存在的用户名时抛出。
UserService
类:registerUser(User user)
: 注册用户。如果用户为空、用户名为空或用户名已存在,抛出相应的自定义异常。findUser(String username)
: 查找用户。如果用户不存在,抛出UserNotFoundException
。
Main
类:- 演示了如何使用
UserService
类进行用户注册和查找。 - 捕获并处理了自定义异常和其他可能的异常情况。
- 演示了如何使用
NullPointerException介绍
在Java中,NullPointerException
(NPE)是最常见的异常之一,通常在以下情况下抛出:
- 当试图调用
null
对象的实例方法。 - 当试图访问
null
对象的字段。 - 当试图对
null
对象进行操作,如数组下标等。
例如,以下代码会抛出 NullPointerException
:
1 | public class Main { |
处理
NullPointerException
NullPointerException
通常表示代码逻辑错误。良好的编码实践可以帮助减少这种异常的发生:
初始化变量:在定义成员变量时进行初始化。
1
2
3public class Person {
private String name = "";
}返回空集合或空字符串:避免返回
null
。1
2
3
4
5
6public String[] readLinesFromFile(String file) {
if (getFileSize(file) == 0) {
return new String[0]; // 返回空数组而不是 null
}
...
}使用
Optional
:当null
表示缺失值时,考虑使用Optional
。1
2
3
4
5
6public Optional<String> readFromFile(String file) {
if (!fileExist(file)) {
return Optional.empty(); // 使用 Optional 来处理可能的 null
}
...
}
定位
NullPointerException
从 Java 14 开始,JVM 可以提供详细的 NullPointerException
信息,帮助我们快速定位问题。要启用这一功能,需要在 JVM 启动时添加
-XX:+ShowCodeDetailsInExceptionMessages
参数。例如:
1 | java -XX:+ShowCodeDetailsInExceptionMessages Main |
这样,如果在运行时出现 NullPointerException
,JVM
会显示更详细的信息,指出哪个对象为 null
。
断言(Assertion)简介
断言是一种调试工具,用于验证程序中假设的正确性。Java使用
assert
关键字来实现断言,允许你在代码中嵌入检查点,以便在开发和测试阶段发现潜在的问题。
使用断言的基本示例
1 | public class Main { |
在这个示例中,assert x >= 0;
用于检查变量
x
是否大于或等于 0。如果条件为假,程序会抛出
AssertionError
,提示断言失败。
带消息的断言
可以为断言添加一个消息,使调试更为方便:
1 | assert x >= 0 : "x must be >= 0"; // 断言失败时的消息 |
当断言失败时,AssertionError
会带上消息
"x must be >= 0"
。
断言的特点
- 用于调试:断言仅用于开发和测试阶段,检查代码中的假设。
- 不可恢复的错误:断言失败通常表示代码逻辑错误,不适用于可恢复的错误处理。
- 程序退出:断言失败会抛出
AssertionError
,这可能导致程序退出。
使用断言的最佳实践
非恢复性错误:例如,在数据验证时,如果发现参数为空,应该抛出异常,而不是使用断言。
1
2
3
4
5void sort(int[] arr) {
if (arr == null) {
throw new IllegalArgumentException("Array cannot be null");
}
}调试而非生产:在生产环境中应禁用断言,因为断言对运行时性能有一定影响。
启用断言
默认情况下,Java虚拟机(JVM)会忽略断言。要启用断言,需要在启动 JVM
时使用 -ea
(或
-enableassertions
)参数。例如:
1 | java -ea Main |
启用特定类或包的断言
启用特定类的断言:
1 java -ea:com.example.Main Main启用特定包的断言:
1 java -ea:com.example... Main用途:断言用于开发和测试阶段,检查程序中的假设。
注意:不可用于处理可恢复的错误,应该用异常处理机制。
启用断言:需要在 JVM 启动时添加
-ea
参数,默认情况下断言是禁用的。单元测试:在实际开发中,使用单元测试(如 JUnit)更为常见且有效。
断言是调试工具中的一部分,但在实际开发中,编写单元测试通常能更有效地验证程序的正确性。
日志(Logging)
概念:
- 日志(Logging) 是一种记录程序运行状态、错误和调试信息的技术。
- 通过使用日志,可以替代
System.out.println()
来更高效地调试程序。
Java 的日志框架:
- Java 标准库提供了
java.util.logging
包用于日志记录。 - 常用的第三方日志框架有 Log4j 和 SLF4J。
使用 java.util.logging
:
基本使用:
- 使用
Logger
类记录不同级别的日志消息。 - 日志级别(从高到低):
SEVERE
:严重错误WARNING
:警告INFO
:普通信息CONFIG
:配置相关FINE
:详细信息FINER
:更详细FINEST
:最详细
示例代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23import java.util.logging.Level;
import java.util.logging.Logger;
public class LoggingExample {
private static final Logger logger = Logger.getLogger(LoggingExample.class.getName());
public static void main(String[] args) {
logger.info("Application starting...");
try {
performTask();
} catch (Exception e) {
logger.log(Level.SEVERE, "An error occurred", e);
}
logger.fine("This is a fine-level log message.");
logger.warning("This is a warning message.");
}
private static void performTask() throws Exception {
// Simulate a task
logger.info("Performing task...");
throw new RuntimeException("Simulated exception");
}
}详细解释
LoggingExample
类导入必要的包
1
2import java.util.logging.Level;
import java.util.logging.Logger;java.util.logging.Level
:定义了日志级别,如INFO
,WARNING
,SEVERE
等。java.util.logging.Logger
:用于记录日志的核心类。
定义
LoggingExample
类1
2public class LoggingExample {
private static final Logger logger = Logger.getLogger(LoggingExample.class.getName());Logger.getLogger(LoggingExample.class.getName())
:获取一个与LoggingExample
类相关联的Logger
实例。private static final Logger logger
:声明一个Logger
实例,static
关键字表示所有LoggingExample
类的实例共享这个Logger
,final
表示logger
变量的引用不可变。
main
方法1
2
3
4
5
6
7
8
9
10public static void main(String[] args) {
logger.info("Application starting...");
try {
performTask();
} catch (Exception e) {
logger.log(Level.SEVERE, "An error occurred", e);
}
logger.fine("This is a fine-level log message.");
logger.warning("This is a warning message.");
}logger.info("Application starting...")
:记录一条INFO
级别的日志消息,表示应用程序开始运行。try
块中的performTask()
:调用performTask
方法,该方法可能会抛出异常。catch
块捕获所有异常(Exception e
),并使用logger.log(Level.SEVERE, "An error occurred", e)
记录SEVERE
级别的日志消息。e
作为异常对象会附加在日志消息中,方便排查问题。logger.fine("This is a fine-level log message.")
:记录一条FINE
级别的日志消息。由于默认日志级别是INFO
,所以FINE
级别的日志不会被输出,除非调整日志级别。logger.warning("This is a warning message.")
:记录一条WARNING
级别的日志消息。
performTask
方法1
2
3
4
5private static void performTask() throws Exception {
// Simulate a task
logger.info("Performing task...");
throw new RuntimeException("Simulated exception");
}logger.info("Performing task...")
:记录一条INFO
级别的日志消息,表示任务正在执行。throw new RuntimeException("Simulated exception")
:抛出一个RuntimeException
异常,模拟任务中的错误。
- 使用
日志配置:
- 配置文件
logging.properties
可用来设置日志行为,例如日志输出位置、级别等。
logging.properties
示例:1
2
3
4
5
6
7handlers=java.util.logging.FileHandler, java.util.logging.ConsoleHandler
java.util.logging.FileHandler.pattern = myapp%u.log
java.util.logging.FileHandler.limit = 50000
java.util.logging.FileHandler.count = 3
java.util.logging.FileHandler.formatter = java.util.logging.SimpleFormatter
java.util.logging.ConsoleHandler.level = INFO
java.util.logging.FileHandler.level = ALL这段配置是用来设置
java.util.logging
包的日志处理器。配置解释
handlers=java.util.logging.FileHandler, java.util.logging.ConsoleHandler
- 解释:指定日志的处理器(handlers)。这里使用了两个处理器:
java.util.logging.FileHandler
:将日志输出到文件。java.util.logging.ConsoleHandler
:将日志输出到控制台(终端)。
- 解释:指定日志的处理器(handlers)。这里使用了两个处理器:
java.util.logging.FileHandler.pattern = myapp%u.log
- 解释:定义文件日志的文件名模式。
%u
是一个占位符,用于生成唯一的文件名(例如,myapp1.log
,myapp2.log
)。这个配置会创建日志文件并按模式命名。
- 解释:定义文件日志的文件名模式。
java.util.logging.FileHandler.limit = 50000
- 解释:设置每个日志文件的最大字节数,单位是字节。
50000
字节即 50 KB。当文件大小达到这个限制时,会轮转到下一个文件。
- 解释:设置每个日志文件的最大字节数,单位是字节。
java.util.logging.FileHandler.count = 3
- 解释:指定日志文件的最大数量。设置为
3
表示最多保留 3 个日志文件(当前文件加上 2 个历史文件)。当日志文件数量超过此设置时,最旧的文件将被删除。
- 解释:指定日志文件的最大数量。设置为
java.util.logging.FileHandler.formatter = java.util.logging.SimpleFormatter
- 解释:设置文件日志的格式化器(formatter)。
SimpleFormatter
是一种简单的格式化器,它将日志消息格式化为人类可读的文本格式。
- 解释:设置文件日志的格式化器(formatter)。
java.util.logging.ConsoleHandler.level = INFO
- 解释:指定
ConsoleHandler
的日志级别为INFO
。这意味着控制台只会输出INFO
级别及以上(WARNING
,SEVERE
)的日志消息。低于INFO
级别的日志(如FINE
,FINER
,FINEST
)不会被输出到控制台。
- 解释:指定
java.util.logging.FileHandler.level = ALL
- 解释:指定
FileHandler
的日志级别为ALL
。这意味着FileHandler
会记录所有级别的日志消息,包括SEVERE
,WARNING
,INFO
,CONFIG
,FINE
,FINER
,FINEST
等。此配置会确保日志文件记录所有级别的日志。
- 解释:指定
Java 代码加载配置:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20import java.util.logging.Logger;
public class ConfiguredLoggingExample {
private static final Logger logger = Logger.getLogger(ConfiguredLoggingExample.class.getName());
public static void main(String[] args) {
logger.info("Starting application with custom logging configuration...");
processData();
}
private static void processData() {
logger.info("Processing data...");
try {
String data = null;
data.toString(); // NullPointerException
} catch (Exception e) {
logger.log(Level.SEVERE, "Error occurred while processing data", e);
}
}
}运行命令:
1
java -Djava.util.logging.config.file=logging.properties ConfiguredLoggingExample
- 配置文件
日志级别的使用:
- SEVERE:记录严重错误,通常会中断程序。
- WARNING:记录警告信息,表明可能的问题。
- INFO:记录普通运行信息,描述程序的正常操作。
- CONFIG、FINE、FINER、FINEST:记录详细调试信息,帮助诊断问题。
优点:
- 可配置:可以通过配置文件或命令行调整日志行为。
- 输出到文件:日志可以被重定向到文件中,便于后续分析。
- 动态调整:可以设置不同的日志级别,控制输出的信息量。
总结:
- 使用日志记录程序信息,比
System.out.println()
更适合调试和维护。 java.util.logging
提供了灵活的日志记录功能,但配置可能稍复杂。