Java异常

基本语法

在 Java 中,异常处理的基本语法主要包括 trycatchfinallythrowthrows 关键字。

1. trycatch

  • try 代码块用于包围可能会抛出异常的代码。
  • catch 代码块用于捕获并处理 try 块中抛出的异常。

语法:

1
2
3
4
5
try {
// 可能抛出异常的代码
} catch (ExceptionType e) {
// 异常处理代码
}

示例:

1
2
3
4
5
6
try {
int[] numbers = {1, 2, 3};
System.out.println(numbers[3]); // 可能抛出 ArrayIndexOutOfBoundsException
} catch (ArrayIndexOutOfBoundsException e) {
System.out.println("数组索引越界:" + e.getMessage());
}

2. finally

  • finally 代码块用于在 trycatch 执行完毕后,执行一定的清理工作,无论是否发生异常。finally 块总是会执行,除非程序提前终止(如调用 System.exit())。

语法:

1
2
3
4
5
6
7
try {
// 可能抛出异常的代码
} catch (ExceptionType e) {
// 异常处理代码
} finally {
// 总是执行的代码
}

示例:

1
2
3
4
5
6
7
try {
int result = 10 / 0; // 可能抛出 ArithmeticException
} catch (ArithmeticException e) {
System.out.println("除数不能为零:" + e.getMessage());
} finally {
System.out.println("这是 finally 代码块,无论如何都会执行。");
}

3. throw

  • throw 关键字用于显式地抛出一个异常对象。它可以在方法内的任何地方使用。

语法:

1
throw new ExceptionType("异常信息");

示例:

1
2
3
4
5
public void checkAge(int age) {
if (age < 18) {
throw new IllegalArgumentException("年龄必须大于或等于18岁");
}
}

4. throws

  • throws 关键字用于在方法声明中指明该方法可能抛出的异常类型。调用这个方法时,必须处理这些异常,通常通过 try-catch 或者继续声明 throws

语法:

1
2
3
public void someMethod() throws ExceptionType1, ExceptionType2 {
// 方法体
}

示例:

1
2
3
4
5
public void readFile(String filePath) throws IOException {
FileReader file = new FileReader(filePath);
BufferedReader fileInput = new BufferedReader(file);
throw new IOException("文件读取错误");
}

5. 自定义异常

  • 可以通过继承 Exception 类来定义自己的异常类型。

语法:

1
2
3
4
5
public class MyCustomException extends Exception {
public MyCustomException(String message) {
super(message);
}
}

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class MyCustomException extends Exception {
public MyCustomException(String message) {
super(message);
}
}

public class TestCustomException {
public static void main(String[] args) {
try {
throw new MyCustomException("这是自定义异常");
} catch (MyCustomException e) {
System.out.println(e.getMessage());
}
}
}

异常处理的深入探讨

1. 捕获并处理异常

  • 精细化捕获:在异常处理过程中,使用精细化的异常捕获策略。避免捕获过于宽泛的异常类型,如 Exception,应尽量捕获具体的异常类型。这有助于明确错误的来源并提供更有针对性的处理方式。例如,如果你知道代码中可能出现 IOExceptionNullPointerException,应该分别捕获它们:

1
2
3
4
5
6
7
8
9
try {
// 可能抛出异常的代码
} catch (IOException e) {
// 处理IO异常
e.printStackTrace();
} catch (NullPointerException e) {
// 处理空指针异常
e.printStackTrace();
}

  • 嵌套异常捕获:在一些情况下,可能需要在同一段代码中捕获多个异常,并在不同层次上进行处理。例如,你可以在较低层次捕获具体异常并重新抛出一个更通用的异常:

1
2
3
4
5
6
7
public void readFile(String fileName) throws CustomException {
try {
// 可能抛出IOException的代码
} catch (IOException e) {
throw new CustomException("Error reading file: " + fileName, e);
}
}

通过这种方式,调用者可以捕获 CustomException,而不需要了解具体的 IOException 细节。

2. 区分业务异常和系统异常

  • 业务异常处理:业务异常是应用程序运行过程中可以预见并处理的错误。例如,用户输入的格式错误、数据缺失等。这类异常通常继承自 Exception,可以在应用层中捕获并处理。

  • 系统异常处理:系统异常,如 OutOfMemoryErrorStackOverflowError,表示 JVM 层面的严重错误。这类异常通常无法恢复,应避免捕获并在适当的位置进行日志记录,以便于排查问题。

1
2
3
4
5
6
try {
// 可能导致系统异常的代码
} catch (Error e) {
// 记录系统异常,通常不建议进行恢复处理
logError(e);
}

3. 使用自定义异常

  • 自定义异常类:自定义异常不仅可以帮助明确问题的类型,还能携带特定的错误信息和上下文。例如,一个订单处理系统可能有特定的业务异常,如 OrderNotFoundExceptionInvalidOrderStateException

1
2
3
4
5
public 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
15
FileInputStream 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
5
try (FileInputStream fis = new FileInputStream("file.txt")) {
// 处理文件
} catch (IOException e) {
// 处理异常
}

6. 使用 throws 关键字

  • 向上传递异常:在某些情况下,方法内部无法处理异常,可以使用 throws 将异常抛出,由调用者来处理。这种方式常用于库或框架开发中,允许用户决定如何处理特定的异常。

1
2
3
4
5
6
public 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
6
try {
// 可能抛出异常的代码
} catch (Exception e) {
// 记录异常而不是忽略
logException(e);
}

实际应用场景

处理数据库连接异常

在处理数据库操作时,异常处理非常重要,尤其是在连接、查询或更新数据时。以下是一个示例,展示了如何处理数据库连接异常并确保资源释放:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;

public class DatabaseExample {
public static void main(String[] args) {
String url = "jdbc:mysql://localhost:3306/mydb";
String user = "user";
String password = "password";

try (Connection conn = DriverManager.getConnection(url, user, password)) {
// 执行数据库操作
} catch (SQLException e) {
// 处理数据库连接或查询异常
e.printStackTrace();
}
}
}

捕获异常详解

Java 异常处理简化

1. 基本用法

  • try 块中放置可能抛出异常的代码,并使用 catch 捕获异常:

1
2
3
4
5
try {
process1();
} catch (IOException e) {
System.out.println(e);
}

1. try

  • 作用try 块用于包裹可能会抛出异常的代码。它表示程序尝试执行其中的代码,并准备应对潜在的错误。
  • 示例说明:在这段代码中,process1() 方法是一个可能会抛出异常的操作。例如,process1() 可能是读取文件、网络连接或其他 I/O 操作。如果在执行 process1() 时发生异常,这个异常将被抛出,并立即跳转到 catch 块。

2. catch

  • 作用catch 块用于捕获在 try 块中抛出的异常。它接收一个特定类型的异常对象,并定义了处理这种异常的代码。

  • IOExceptionIOException 是 Java 中的一个异常类,表示在进行输入输出操作时发生的错误。IOExceptionException 类的一个子类,专门处理I/O操作的异常情况,如文件无法读取、网络中断等。

    1
    catch (IOException e)
    • 含义:这个 catch 块表示它专门捕获 IOException 类型的异常。如果 process1() 抛出一个 IOException,程序将执行 catch 块中的代码。
  • 异常对象 ee 是一个 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
7
try {
process1();
} catch (UnsupportedEncodingException e) {
System.out.println("Bad encoding");
} catch (IOException e) {
System.out.println("IO error");
}

1. try

  • 作用try 块用于包含可能会抛出异常的代码。程序将尝试执行其中的代码,并准备在发生异常时跳转到相应的 catch 块处理异常。
  • 示例说明:在这段代码中,process1() 是一个可能会抛出异常的方法。假设 process1() 涉及处理文件或字符编码,那么它可能会抛出 UnsupportedEncodingExceptionIOException

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:这是一个更通用的异常类,表示在进行输入/输出操作时发生的错误。IOExceptionUnsupportedEncodingException 的父类,通常用于处理文件读取、网络通信等操作中的错误。
  • 异常处理:如果 process1() 抛出了其他类型的 IOException(但不是 UnsupportedEncodingException),程序会跳转到这个 catch 块。
  • 输出消息System.out.println("IO error"); 会输出 "IO error"。这用于告知用户或开发者,操作失败是由于输入/输出错误导致的。

3. catch 块的顺序

  • 顺序的重要性:在多个 catch 块中,子类异常(如 UnsupportedEncodingException)必须放在父类异常(如 IOException)之前。否则,子类异常会被父类异常捕获,导致子类异常的 catch 块永远不会被执行。

  • 示例说明:在这个代码中,UnsupportedEncodingExceptionIOException 的子类。因此,必须先捕获 UnsupportedEncodingException,然后再捕获更广泛的 IOException。这样可以确保具体的异常被适当处理。

  • 这段代码的功能是:

    • 尝试执行 process1() 方法。
    • 如果 process1() 抛出 UnsupportedEncodingException,程序会输出 "Bad encoding"。
    • 如果 process1() 抛出其他类型的 IOException,程序会输出 "IO error"。
    • 通过这种方式,程序可以根据不同的异常类型提供不同的错误处理方式,使得错误信息更加明确和有针对性。

3. finally 语句

  • finally 块无论是否发生异常都会执行,通常用于清理工作:

1
2
3
4
5
6
7
try {
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
7
try {
process1();
} catch (IOException | NumberFormatException e) {
System.out.println("Bad input");
} catch (Exception e) {
System.out.println("Unknown error");
}

1. try

  • 作用try 块包含了可能会抛出异常的代码。程序会首先尝试执行 try 块中的代码。如果在执行过程中抛出异常,程序会跳转到相应的 catch 块进行处理。
  • 示例说明:在这段代码中,process1() 是一个可能抛出异常的方法,具体的异常类型可以是 IOExceptionNumberFormatException 或其他类型的异常。

2. 第一个 catch

多异常捕获 (IOException | NumberFormatException)

  • 作用:该 catch 块用于捕获 IOExceptionNumberFormatException 这两种类型的异常,并且使用 | 符号将它们合并在一起。
  • 异常解释
    • IOException:表示在输入/输出操作时可能发生的错误,比如读取文件时发生错误。
    • NumberFormatException:表示当尝试将字符串转换为数值类型时,字符串格式不正确,从而引发的异常。
  • 处理方式:如果 process1() 抛出了这两种异常中的任何一种,程序会跳转到这个 catch 块执行,输出 "Bad input"。
  • 示例
    • 如果 process1() 尝试读取文件但失败了,抛出 IOException,会输出 "Bad input"。
    • 如果 process1() 尝试将一个非法的字符串转换为数字,抛出 NumberFormatException,也会输出 "Bad input"。

3. 第二个 catch

捕获 Exception

  • 作用:这个 catch 块用于捕获所有其他未在之前 catch 块中明确处理的异常。Exception 是 Java 中所有异常的父类,所以这个 catch 块可以捕获除了 IOExceptionNumberFormatException 之外的任何其他异常。
  • 处理方式:如果 process1() 抛出了除 IOExceptionNumberFormatException 之外的其他异常,程序会跳转到这个 catch 块,输出 "Unknown error"。
  • 作用:这是一个兜底的 catch 块,用于处理那些可能未预见到的异常情况,确保即使出现未知错误,程序也能处理并继续执行。

4. 执行流程总结

  • 正常流程:当 process1() 正常执行且未抛出任何异常时,catch 块不会被触发,程序继续向下执行。

  • 异常流程

    • 如果 process1() 抛出 IOExceptionNumberFormatException,会触发第一个 catch 块,输出 "Bad input"。
    • 如果抛出其他类型的异常(未被前面 catch 捕获的异常),会触发第二个 catch 块,输出 "Unknown error"。
  • 多异常捕获:通过使用 | 符号,多个异常类型可以在同一个 catch 块中进行捕获和处理,简化了代码,减少了冗余。

  • 通用异常捕获Exception 作为最后的 catch,确保了程序可以捕获和处理所有未明确指定的异常,增加了程序的健壮性。

小细节总结:

  • 子类异常放在前面,多个异常处理相同可以合并到一起。

抛出异常详解

异常的传播与抛出

当 Java 程序执行过程中发生错误时,通常会抛出一个异常对象(Exception),该对象封装了错误的详细信息。异常一旦被抛出,如果在当前方法中没有被捕获,便会向上层调用方法传播,直到遇到一个适当的 try-catch 块进行处理为止。如果一直没有被捕获,最终会被 JVM 捕获并终止程序运行。

1. 异常的传播

以下代码演示了异常的传播过程:

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) {
try {
process1();
} catch (Exception e) {
e.printStackTrace();
}
}

static void process1() {
process2();
}

static void process2() {
Integer.parseInt(null); // 会抛出NumberFormatException
}
}

在这个例子中:

  • process2() 方法试图将 null 转换为整数,导致 NumberFormatException 异常被抛出。
  • 由于 process2() 没有捕获这个异常,异常会向上传播到调用它的 process1()
  • 同样地,process1() 也没有捕获该异常,异常继续向上传播到 main() 方法。
  • 最终,main() 方法捕获了这个异常,并使用 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)

栈轨迹中详细列出了异常发生的调用链,从最深层的调用开始,逐层向上直到 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 异常被抛出。

异常传播机制

  1. 异常的抛出:
    • Integer.parseInt(null) 执行时,Java 会抛出 NumberFormatException 异常。此异常说明输入的字符串不能转换为有效的整数。
  2. 异常的传播:
    • NumberFormatException 首先在 process2() 方法中抛出,但 process2() 没有任何捕获异常的机制,因此异常会被自动传播到调用 process2() 的方法 process1()
    • process1() 同样没有处理这个异常,所以异常继续传播到 main() 方法。
  3. 异常的捕获与处理:
    • 异常最终被 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. 抛出异常

当发生错误时,我们可以主动抛出异常来终止执行或者通知调用者某些错误情况。抛出异常通常涉及两个步骤:

  1. 创建异常对象:用特定的异常类实例化一个对象。
  2. throw 语句抛出异常:将该异常对象抛出,终止当前的执行路径。

示例:

1
2
3
4
5
void process2(String s) {
if (s == null) {
throw new NullPointerException();
}
}

在这个例子中,如果 snull,会抛出一个 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 语句手动抛出了该异常。

目的和应用场景

  1. 防止非法输入:
    • 这段代码的主要目的在于防止方法处理 null 值,从而避免在后续代码中可能由于 null 导致的异常问题(例如 NullPointerException)。通过提前抛出异常,方法明确表示不接受 null 作为有效输入。
  2. 提高代码的健壮性:
    • 通过对输入参数的严格检查,程序可以更早地发现问题,并且可以更明确地告诉调用者出现了什么问题(在这个例子中,即调用者传递了一个 null 而不是预期的字符串)。
  3. 异常的手动抛出:
    • 通常情况下,NullPointerException 是在尝试调用 null 对象的方法或访问其字段时由 Java 运行时自动抛出的。但是在这种情况下,开发者可以主动选择在何时、为何条件抛出该异常,从而更好地控制程序的执行流。

3. 转换异常类型

有时候,捕获到某个异常后,我们可能希望转换为另一种更适合当前语境的异常类型。这时可以在 catch 块中抛出新的异常:

1
2
3
4
5
6
7
void process1() {
try {
process2();
} catch (NullPointerException e) {
throw new IllegalArgumentException();
}
}

在这个例子中:

  • 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),以更好地表明出错的上下文。
  • 隐藏实现细节:
    • 通过抛出 IllegalArgumentExceptionprocess1 方法可以隐藏 process2 方法的具体实现细节。调用者只会看到 IllegalArgumentException,而不知道实际是由 NullPointerException 导致的。

示例用法

如果 process2() 方法在处理时遇到了 null 值,它会抛出 NullPointerExceptionprocess1() 方法捕获这个异常并将其转换为 IllegalArgumentException。这种做法通常用于将底层的实现异常映射到更具业务逻辑的异常,以便调用者能够更好地理解错误的上下文并采取适当的措施。

4. 保持原始异常信息

如果在捕获异常后抛出新的异常,为了保留原始异常的信息,可以将原始异常作为参数传递给新抛出的异常对象。这样,异常链的信息不会丢失:

1
2
3
4
5
6
7
void process1() {
try {
process2();
} catch (NullPointerException e) {
throw new IllegalArgumentException(e);
}
}

在上述代码中,IllegalArgumentException 包含了 NullPointerException 的信息,运行结果如下:

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 中,当我们捕获一个异常并抛出一个新的异常时,通常希望保留原始异常的信息,以便能够追踪到最初的问题源头。将原始异常作为参数传递给新抛出的异常对象,可以实现这一点。

异常链

在你的代码中,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 的信息,即在 IllegalArgumentExceptiongetCause() 方法中能够获取到原始的异常信息。

异常输出

当异常链存在时,异常输出将包含完整的异常信息链。运行结果如下:

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
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");
} catch (Exception e) {
origin = e;
throw e;
} finally {
Exception e = new IllegalArgumentException();
if (origin != null) {
e.addSuppressed(origin);
}
throw e;
}
}
}

在这个例子中,即使 finally 中抛出新的异常,也会通过 addSuppressed() 保留 catch 中的异常信息。

输出结果

1
2
3
4
Exception in thread "main" java.lang.IllegalArgumentException
at Main.main(Main.java:11)
Suppressed: java.lang.NumberFormatException: For input string: "abc"
...

这段代码展示了如何在 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. 初始化和异常捕获

    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 变量中,然后重新抛出相同的异常。
  2. 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 中抛出异常,因为这会屏蔽 trycatch 中的异常,使调试变得复杂。

自定义异常详解

在 Java 中,异常机制是处理运行时错误的重要工具。了解如何合理地使用和自定义异常类型对于编写健壮的代码至关重要。

Java 标准库中的常用异常

Java 标准库定义了许多常用的异常类型。了解这些异常的分类有助于更好地处理和抛出异常:

  1. Exception

    • 基础异常类,所有异常类的基类。
  2. RuntimeException(非检查型异常,运行时异常)

    • NullPointerException: 访问空对象引用时抛出。
    • IndexOutOfBoundsException: 访问数组或集合中不存在的索引时抛出。
    • SecurityException: 违反安全策略时抛出。
    • IllegalArgumentException: 方法参数不合法时抛出。
      • NumberFormatException: 尝试将一个字符串解析为数字时格式不正确。
  3. IOException(检查型异常,输入输出异常)

    • UnsupportedCharsetException: 不支持的字符集异常。
    • FileNotFoundException: 文件未找到异常。
    • SocketException: 网络套接字异常。
  4. ParseException: 解析错误异常,一般用于解析操作(如日期、数字等)。

  5. GeneralSecurityException: 一般安全异常,涵盖多种安全相关问题。

  6. SQLException: 数据库操作异常,处理 SQL 语句错误时抛出。

  7. TimeoutException: 操作超时异常,表示操作未能在预期时间内完成。

抛出标准异常

在代码中处理参数检查或其他常见问题时,使用 Java 标准库中定义的异常是一个良好的做法。例如:

1
2
3
4
5
static void process1(int age) {
if (age <= 0) {
throw new IllegalArgumentException("Age must be positive.");
}
}

这里,我们使用 IllegalArgumentException 来表示参数无效的情况。这种做法能使代码更具可读性和一致性。

自定义异常

在大型项目中,可能需要创建自定义异常来处理特定业务逻辑中的异常情况。创建自定义异常类时,建议遵循以下原则:

  1. 定义基础异常类

    创建一个基础异常类 BaseException,通常继承自 RuntimeException,以便所有自定义异常都能从 BaseException 派生:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    public 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);
    }
    }
    • 构造方法: 提供多个构造方法以支持不同的异常信息传递方式,包括消息、原因以及消息和原因的组合。
  2. 创建业务异常

    基于 BaseException 创建具体的业务异常类:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    public 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
    17
    public 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// 自定义异常:用户未找到
public class UserNotFoundException extends RuntimeException {
public UserNotFoundException() {
super();
}

public UserNotFoundException(String message) {
super(message);
}

public UserNotFoundException(String message, Throwable cause) {
super(message, cause);
}

public UserNotFoundException(Throwable cause) {
super(cause);
}
}

// 自定义异常:用户名已存在
public class UsernameAlreadyExistsException extends RuntimeException {
public UsernameAlreadyExistsException() {
super();
}

public UsernameAlreadyExistsException(String message) {
super(message);
}

public UsernameAlreadyExistsException(String message, Throwable cause) {
super(message, cause);
}

public UsernameAlreadyExistsException(Throwable cause) {
super(cause);
}
}

使用自定义异常的业务逻辑

接下来,我们创建一个简单的用户注册系统,该系统使用上述自定义异常来处理错误情况。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
import java.util.HashSet;
import java.util.Set;

// 用户类
class User {
private String username;

public User(String username) {
this.username = username;
}

public String getUsername() {
return username;
}
}

// 用户注册服务类
class UserService {
private Set<String> existingUsernames = new HashSet<>();

// 注册用户
public void registerUser(User user) {
if (user == null) {
throw new IllegalArgumentException("User cannot be null");
}

String username = user.getUsername();
if (username == null || username.isEmpty()) {
throw new IllegalArgumentException("Username cannot be empty");
}

if (existingUsernames.contains(username)) {
throw new UsernameAlreadyExistsException("Username '" + username + "' already exists");
}

existingUsernames.add(username);
System.out.println("User '" + username + "' registered successfully");
}

// 查找用户
public User findUser(String username) {
if (!existingUsernames.contains(username)) {
throw new UserNotFoundException("User with username '" + username + "' not found");
}
return new User(username);
}
}

// 主程序
public class Main {
public static void main(String[] args) {
UserService userService = new UserService();

try {
User user1 = new User("john_doe");
userService.registerUser(user1);

// 尝试注册一个已经存在的用户
User user2 = new User("john_doe");
userService.registerUser(user2); // 这将引发 UsernameAlreadyExistsException

} catch (UsernameAlreadyExistsException e) {
System.out.println("Error: " + e.getMessage());
} catch (IllegalArgumentException e) {
System.out.println("Invalid argument: " + e.getMessage());
}

try {
// 尝试查找一个不存在的用户
User user = userService.findUser("jane_doe"); // 这将引发 UserNotFoundException
} catch (UserNotFoundException e) {
System.out.println("Error: " + e.getMessage());
}
}
}

解释

  1. 自定义异常类:
    • UserNotFoundException: 当用户未找到时抛出。
    • UsernameAlreadyExistsException: 当尝试注册一个已存在的用户名时抛出。
  2. UserService:
    • registerUser(User user): 注册用户。如果用户为空、用户名为空或用户名已存在,抛出相应的自定义异常。
    • findUser(String username): 查找用户。如果用户不存在,抛出 UserNotFoundException
  3. Main:
    • 演示了如何使用 UserService 类进行用户注册和查找。
    • 捕获并处理了自定义异常和其他可能的异常情况。

NullPointerException介绍

在Java中,NullPointerException(NPE)是最常见的异常之一,通常在以下情况下抛出:

  • 当试图调用 null 对象的实例方法。
  • 当试图访问 null 对象的字段。
  • 当试图对 null 对象进行操作,如数组下标等。

例如,以下代码会抛出 NullPointerException

1
2
3
4
5
6
public class Main {
public static void main(String[] args) {
String s = null;
System.out.println(s.toLowerCase()); // 这里将抛出 NullPointerException
}
}

处理 NullPointerException

NullPointerException 通常表示代码逻辑错误。良好的编码实践可以帮助减少这种异常的发生:

  1. 初始化变量:在定义成员变量时进行初始化。

    1
    2
    3
    public class Person {
    private String name = "";
    }
  2. 返回空集合或空字符串:避免返回 null

    1
    2
    3
    4
    5
    6
    public String[] readLinesFromFile(String file) {
    if (getFileSize(file) == 0) {
    return new String[0]; // 返回空数组而不是 null
    }
    ...
    }
  3. 使用 Optional:当 null 表示缺失值时,考虑使用 Optional

    1
    2
    3
    4
    5
    6
    public 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
2
3
4
5
6
7
public class Main {
public static void main(String[] args) {
double x = Math.abs(-123.45);
assert x >= 0; // 断言,检查x是否大于等于0
System.out.println(x);
}
}

在这个示例中,assert x >= 0; 用于检查变量 x 是否大于或等于 0。如果条件为假,程序会抛出 AssertionError,提示断言失败。

带消息的断言

可以为断言添加一个消息,使调试更为方便:

1
assert x >= 0 : "x must be >= 0"; // 断言失败时的消息

当断言失败时,AssertionError 会带上消息 "x must be >= 0"

断言的特点

  1. 用于调试:断言仅用于开发和测试阶段,检查代码中的假设。
  2. 不可恢复的错误:断言失败通常表示代码逻辑错误,不适用于可恢复的错误处理。
  3. 程序退出:断言失败会抛出 AssertionError,这可能导致程序退出。

使用断言的最佳实践

  • 非恢复性错误:例如,在数据验证时,如果发现参数为空,应该抛出异常,而不是使用断言。

    1
    2
    3
    4
    5
    void 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

  1. 基本使用

    • 使用 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
    23
    import 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
    2
    import java.util.logging.Level;
    import java.util.logging.Logger;
    • java.util.logging.Level:定义了日志级别,如 INFO, WARNING, SEVERE 等。
    • java.util.logging.Logger:用于记录日志的核心类。

    定义 LoggingExample

    1
    2
    public 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 类的实例共享这个 Loggerfinal 表示 logger 变量的引用不可变。

    main 方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    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.");
    }
    • 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
    5
    private 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 异常,模拟任务中的错误。
  2. 日志配置

    • 配置文件 logging.properties 可用来设置日志行为,例如日志输出位置、级别等。

    logging.properties 示例

    1
    2
    3
    4
    5
    6
    7
    handlers=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 包的日志处理器。

    配置解释

    1. handlers=java.util.logging.FileHandler, java.util.logging.ConsoleHandler
      • 解释:指定日志的处理器(handlers)。这里使用了两个处理器:
        • java.util.logging.FileHandler:将日志输出到文件。
        • java.util.logging.ConsoleHandler:将日志输出到控制台(终端)。
    2. java.util.logging.FileHandler.pattern = myapp%u.log
      • 解释:定义文件日志的文件名模式。%u 是一个占位符,用于生成唯一的文件名(例如,myapp1.log, myapp2.log)。这个配置会创建日志文件并按模式命名。
    3. java.util.logging.FileHandler.limit = 50000
      • 解释:设置每个日志文件的最大字节数,单位是字节。50000 字节即 50 KB。当文件大小达到这个限制时,会轮转到下一个文件。
    4. java.util.logging.FileHandler.count = 3
      • 解释:指定日志文件的最大数量。设置为 3 表示最多保留 3 个日志文件(当前文件加上 2 个历史文件)。当日志文件数量超过此设置时,最旧的文件将被删除。
    5. java.util.logging.FileHandler.formatter = java.util.logging.SimpleFormatter
      • 解释:设置文件日志的格式化器(formatter)。SimpleFormatter 是一种简单的格式化器,它将日志消息格式化为人类可读的文本格式。
    6. java.util.logging.ConsoleHandler.level = INFO
      • 解释:指定 ConsoleHandler 的日志级别为 INFO。这意味着控制台只会输出 INFO 级别及以上(WARNING, SEVERE)的日志消息。低于 INFO 级别的日志(如 FINE, FINER, FINEST)不会被输出到控制台。
    7. 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
    20
    import 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:记录普通运行信息,描述程序的正常操作。
  • CONFIGFINEFINERFINEST:记录详细调试信息,帮助诊断问题。

优点

  • 可配置:可以通过配置文件或命令行调整日志行为。
  • 输出到文件:日志可以被重定向到文件中,便于后续分析。
  • 动态调整:可以设置不同的日志级别,控制输出的信息量。

总结

  • 使用日志记录程序信息,比 System.out.println() 更适合调试和维护。
  • java.util.logging 提供了灵活的日志记录功能,但配置可能稍复杂。