异常处理 | 优雅,永不过时

2023-12-07 16:56:06 浏览数 (1)

前言

异常处理就好比穿底裤,穿了不能轻易的给别人看,更不能不穿。否则浪潮褪去,沙滩上裸奔的人就是你。

异常是一种错误的事件,它在程序执行过程中发生,影响了程序的正常流程。当一个方法遇到异常情况时,它通常会创建一个包含异常信息的对象,然后将控制权交给程序的某个地方,这个地方可以是异常处理代码或者调用栈的其他部分。Java中的异常处理机制是基于传统的C 异常处理机制的,它强制程序员捕获可能出现的异常并进行处理。这使得程序的可靠性得以提高,因为异常情况不再被忽略,而是被明确地处理。

异常处理的主要目标是使程序更具可读性和可维护性,因为异常处理代码通常集中在一起,而不是分散在程序的各个部分。通过使用异常,程序员可以更容易地理解和维护代码,因为异常处理的代码与正常的业务逻辑相分离。

在 Java 中,异常是指在程序执行期间发生的意外情况,它可能导致程序终止或产生不确定的结果。异常分为两种类型:已检查异常(checked exceptions)未检查异常(unchecked exceptions)

已检查异常通常表示程序无法预测的外部条件,例如文件不存在或网络连接中断。

未检查异常通常表示程序内部错误,例如空指针引用或数组越界。在任何情况下,异常都提供了一种将错误处理代码从正常的业务逻辑中分离出来的方法,以确保程序的健壮性。

异常分为两类

受检异常(checked exceptions)和未受检异常(unchecked exceptions)。

受检异常(Checked Exceptions)

这类异常是在编译时检查的,必须在代码中进行处理,否则程序无法通过编译。常见的例子包括 IOException SQLException。可以使用 try-catch 块来捕获并处理这些异常,或者在方法签名中使用 throws 关键字声明方法可能抛出的异常。

代码语言:javascript复制
try {
    // 一些可能抛出异常的代码
} catch (IOException e) {
    // 处理 IOException
} catch (SQLException e) {
    // 处理 SQLException
}

未受检异常(Unchecked Exceptions)

这类异常通常是由程序错误引起的,是在运行时检测的。常见的例子包括 NullPointerException ArrayIndexOutOfBoundsException。通常,最好通过编码和设计来避免这些异常的发生。但如果确信无法避免,可以使用try-catch 来处理。

代码语言:javascript复制
try {
    // 一些可能抛出异常的代码
} catch (NullPointerException e) {
    // 处理 NullPointerException
} catch (ArrayIndexOutOfBoundsException e) {
    // 处理 ArrayIndexOutOfBoundsException
}
  • finally 块:

无论异常是否被捕获,finally 块中的代码都将被执行。这通常用于确保资源的释放,比如关闭文件或网络连接。

代码语言:javascript复制
try {
    // 一些可能抛出异常的代码
} catch (Exception e) {
    // 处理异常
} finally {
    // 无论是否发生异常,都会执行这里的代码
}

这就是 Java 异常处理的基本概念。理解异常机制并合理处理异常有助于编写更稳定、可靠的 Java 代码。

Java异常类层次结构图

Java 异常类层次结构图主要分为两个分支:Throwable 作为根类,分为 ErrorException 两个主要的子类。

Throwable

Error(错误)

代表了系统级别的错误,通常是由虚拟机报告的。程序通常无法捕获这类错误,因为它们表示了严重的问题,无法通过程序来恢复。

  • OutOfMemoryError:内存耗尽
  • StackOverflowError:栈溢出

Exception(异常)

代表了程序运行期间的异常情况,分为受检异常和未受检异常。

受检异常(Checked Exceptions)

需要在代码中显式处理或者在方法签名中声明可能抛出的异常。

  • IOException:输入输出异常
  • SQLException:数据库访问异常

未受检异常(Unchecked Exceptions)

通常是由程序错误引起的,不需要显式处理。

  • RuntimeException:运行时异常的基类
  • NullPointerException:空指针异常
  • ArrayIndexOutOfBoundsException:数组下标越界异常
  • ArithmeticException:算术异常
  • IllegalArgumentException:非法参数异常
  • ClassCastException:类转换异常
  • 等等...

这只是异常类层次结构的一个概览,实际上,Java 提供了众多的异常类以覆盖各种可能的异常情况。理解这个层次结构对于编写健壮的 Java 代码至关重要。

异常关键字

try

try 关键字用于定义一个包含可能抛出异常的代码块。在这个代码块中,可以放置可能引发异常的语句。

代码语言:java复制
try {
    // 可能引发异常的代码
} catch (Exception e) {
    // 处理异常的代码
} finally {
    // 无论是否发生异常都会执行的代码
}

catch

catch 关键字用于捕获并处理异常。在 catch 块中,可以指定要捕获的异常类型,并编写处理异常的代码。

代码语言:java复制
try {
    // 可能引发异常的代码
} catch (ExceptionType e) {
    // 处理特定类型的异常的代码
}

finally

finally 关键字用于定义一个代码块,其中的代码无论是否发生异常都会被执行。通常用于清理资源或确保某些代码总是执行。

代码语言:java复制
try {
    // 可能引发异常的代码
} catch (Exception e) {
    // 处理异常的代码
} finally {
    // 无论是否发生异常都会执行的代码
}

throw

throw 关键字用于手动抛出异常。可以使用它来抛出特定类型的异常对象。

代码语言:java复制
throw new MyException("This is a user exception");

throws

throws 关键字用于在方法签名中声明可能抛出的异常。它告诉调用方可能需要处理的异常类型。

代码语言:javascript复制
void myMethod() throws MyException {
    // 方法体
}

这些关键字一起构成了Java中的异常处理机制,通过它们,可以更好地管理和处理程序中的异常情况。

throw 和 throws 关键字

throw 关键字:

  • 用于手动抛出异常,即在代码中明确指定某个异常对象的抛出。
  • 语法:throw 异常对象;

当使用 throw 关键字时,在代码中明确指定某个异常对象的抛出。这通常发生在方法内部,表示在特定条件下手动引发异常。下面是一个简单的例子:

代码语言:java复制
package com.example.demo.exception;

public class ExampleThrow {
    public static void main(String[] args) {
        try {
            validateAge(15);
        } catch (CustomException e) {
            System.out.println("Caught exception: "   e.getMessage());
        }
    }

    static void validateAge(int age) throws CustomException {
        if (age < 18) {
            throw new CustomException("Age must be 18 or older");
        } else {
            System.out.println("Valid age: "   age);
        }
    }
}

class CustomException extends Exception {
    public CustomException(String message) {
        super(message);
    }
}

在这个例子中,validateAge 方法接受一个年龄参数,如果年龄小于 18,则使用 throw 关键字手动抛出一个 CustomException 异常。在 main 方法中,我们调用了 validateAge(15),由于年龄小于 18,所以会抛出异常,然后我们在 catch 块中捕获并处理了这个异常。

throws 关键字:

  • 用于在方法签名中声明可能抛出的异常类型,通常用于告诉调用方可能需要处理的异常。
  • 语法:void myMethod() throws MyException { // 方法体 }

现在,让我们看看 throws 关键字的使用。throws 通常用于在方法签名中声明可能抛出的异常类型。下面是一个例子:

代码语言:java复制
package com.example.demo.exception;

public class ExampleThrows {
    public static void main(String[] args) {
        try {
            processInput(25);
            processInput(-5);
        } catch (CustomException e) {
            System.out.println("Caught exception: "   e.getMessage());
        }
    }

    static void processInput(int input) throws CustomException {
        if (input < 0) {
            throw new CustomException("Input cannot be negative");
        } else {
            System.out.println("Processed input: "   input);
        }
    }
}

class CustomException extends Exception {
    public CustomException(String message) {
        super(message);
    }
}

在这个例子中,processInput 方法声明了可能抛出 CustomException 异常。在 main 方法中,我们调用了 processInput(25),因为输入是正数,所以不会抛出异常。如果我们调用 processInput(-5),则由于输入是负数,会抛出 CustomException 异常。

总结一下:

  • throw 用于在代码中手动抛出异常。
  • throws 用于在方法签名中声明可能抛出的异常类型,以便调用方知道需要处理哪些异常。

创建自定义异常类

  • 继承自 Exception 或其子类。
  • 可以添加构造函数,通常通过调用父类的构造函数来设置异常消息。
代码语言:java复制
public class CustomException extends Exception {
    public CustomException() {
        super();
    }

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

异常的捕获

try-catch

示例:

代码语言:java复制
public class TryCatchExample {
    public static void main(String[] args) {
        try {
            int result = divide(10, 0);
            System.out.println("Result: "   result);
        } catch (ArithmeticException e) {
            System.out.println("Error: "   e.getMessage());
        }
    }

    static int divide(int numerator, int denominator) {
        return numerator / denominator;
    }
}

解释:

  • try 块中,我们尝试执行除法操作。
  • 如果分母为零,将引发 ArithmeticException
  • catch 块中,我们捕获并处理 ArithmeticException,打印错误消息。

try-catch-finally

示例:

代码语言:java复制
public class TryCatchFinallyExample {
    public static void main(String[] args) {
        try {
            int result = divide(10, 0);
            System.out.println("Result: "   result);
        } catch (ArithmeticException e) {
            System.out.println("Error: "   e.getMessage());
        } finally {
            System.out.println("This block always executes.");
        }
    }

    static int divide(int numerator, int denominator) {
        return numerator / denominator;
    }
}

解释:

  • 与前面的例子相似,但有一个额外的 finally 块。
  • 无论是否发生异常,finally 块中的代码都会执行。
  • 这在需要确保资源释放或清理的情况下很有用。

try-finally

示例:

代码语言:java复制
public class TryFinallyExample {
    public static void main(String[] args) {
        try {
            // 一些可能抛出异常的代码
        } finally {
            System.out.println("This block always executes.");
        }
    }
}

解释:

  • 这个示例没有 catch 块,仅有 try 块和 finally 块。
  • 无论是否发生异常,finally 块中的代码都会执行。
  • 这在只关心资源清理而不关心异常处理的情况下很有用。

try-with-resources

try-with-resources 是 Java 7 引入的一个语法糖,用于更方便地管理资源,如文件、网络连接等,而无需显式地在代码中添加资源关闭的语句。这个语法确保在 try 块结束时,所有在括号中声明的资源都会被关闭,即使在 try 块中发生异常。

示例:

代码语言:javascript复制
package com.example.demo.exception;

import java.io.FileReader;
import java.io.BufferedReader;
import java.io.IOException;

public class TryWithResourcesExample {
    public static void main(String[] args) {
        // 使用 try-with-resources 读取文件内容
        try (FileReader fileReader = new FileReader("E:\example.txt");
             BufferedReader bufferedReader = new BufferedReader(fileReader)) {

            String line;
            // 逐行读取文件内容
            while ((line = bufferedReader.readLine()) != null) {
                System.out.println("Read from file: "   line);
            }

        } catch (IOException e) {
            System.out.println("Error reading file: "   e.getMessage());
        }
    }
}
  • 在这个例子中,我们使用了 try-with-resources 来读取文件内容。
  • FileReaderBufferedReader 都是实现了 AutoCloseable 接口的资源。
  • try 括号内声明了这两个资源,无需手动关闭,它们会在 try 块结束时自动关闭。

注意事项

  1. 资源必须实现 AutoCloseableCloseable 接口。
  2. try-with-resources 语句结束时,会按照声明的顺序逆序关闭资源,即先声明的资源先关闭。
  3. 资源的关闭顺序与声明的顺序相反,这是因为后声明的资源可能依赖于先声明的资源。

异常的主要观点和总结

异常的基本概念:

  • 异常是程序运行中的问题,可能导致程序无法继续正常执行。
  • 异常分为两类:Error(错误)和Exception(异常)。其中,Error 表示严重的问题,通常是无法恢复的,而Exception 表示可捕获和处理的问题。

异常处理的优势:

  • 异常处理提供了一种机制,使得程序员可以在发生异常时采取特定的行动,而不是简单地让程序崩溃。
  • 异常处理使得程序更加健壮,可以更好地适应和应对各种运行时问题。

捕获和处理异常:

  • 使用 try-catch 块可以捕获并处理异常,防止它们传播到程序的上层。
  • 合理地选择捕获和处理异常,可以使程序更容易调试和维护。

异常的层次结构:

  • 异常类之间形成了层次结构,允许程序员根据具体的异常类型来捕获和处理异常。
  • 异常的层次结构有助于更精细地处理不同类型的异常情况。

finally块的作用:

  • finally 块中的代码无论是否发生异常,都会被执行。
  • finally 块通常用于确保资源的释放或清理操作。

自定义异常:

  • 程序员可以根据需要创建自定义异常类,使得异常信息更具体和有意义。
  • 自定义异常有助于更好地反映程序的逻辑结构和错误情况。

异常与错误处理的哲学观点:

  • 程序员应该在能够合理处理异常的地方捕获和处理异常,而不是简单地忽略或直接传播异常。
  • 异常处理应该成为程序设计的一部分,而不仅仅是错误的响应机制。

常见的异常类

已检查异常(Checked Exceptions)

IOException:

  • 描述输入输出操作可能发生的问题,如文件不存在、无法读写等。

FileNotFoundException:

  • 继承自IOException,表示尝试访问文件而文件不存在。

ParseException:

  • 描述解析操作可能发生的问题,通常与日期和时间解析有关。

ClassNotFoundException:

  • 描述尝试加载类时找不到类的情况。

SQLException:

  • 描述与数据库相关的问题,如连接失败、SQL语句执行错误等。:

InterruptedException:

  • 描述一个线程在等待某个对象的锁时,被其他线程中断。

FileNotFoundException:

  • 继承自IOException,表示尝试访问文件而文件不存在。

ParseException:

  • 描述解析操作可能发生的问题,通常与日期和时间解析有关。

ClassNotFoundException:

  • 描述尝试加载类时找不到类的情况。

未检查异常(Unchecked Exceptions)

NullPointerException:

  • 尝试访问对象的属性或调用方法时,对象为null。

ArrayIndexOutOfBoundsException:

  • 尝试访问数组的超出范围的索引。

ArithmeticException:

  • 在进行数学运算时出现错误,例如除数为零。

IllegalArgumentException:

  • 方法接收到非法的参数。

IllegalStateException:

  • 对象处于不正确的状态。

NumberFormatException:

  • 字符串转换为数字时,字符串的格式不正确。

ClassCastException:

  • 尝试将对象转换为其子类,而实际对象类型不允许这样的转换。

NullPointerException:

  • 尝试在空对象上调用方法或访问属性。

除了未检查异常和已检查异常,Java还包括一些其他类型的异常。这些异常通常是作为Error类的子类,表示更加严重且通常是不可恢复的问题。以下是一些其他常见的异常:

Error 异常

OutOfMemoryError:

  • 当Java虚拟机耗尽内存资源无法继续分配时,抛出此错误。

StackOverflowError:

  • 当递归调用或方法调用层次太深导致栈空间不足时,抛出此错误。

NoClassDefFoundError:

  • 当Java虚拟机尝试加载某个类,但找不到该类的定义时,抛出此错误。

LinkageError:

  • 当类的链接过程失败时,抛出此错误,例如虚拟机找到了类的定义,但找不到该类的父类。

AssertionError:

  • 当断言语句(assert)失败时,抛出此错误。通常在开发和调试阶段使用。

ExceptionInInitializerError:

  • 当类的初始化过程中发生异常时,抛出此错误。通常是在类的静态初始化块中发生异常。


我正在参与2023腾讯技术创作特训营第四期有奖征文,快来和我瓜分大奖!

0 人点赞