# 1. 异常

对于异常情况,Java 使用了一种称为异常处理(exception handing)的错误捕获机制

# 1.处理错误

在程序运行的时候可能会出现各种各样的错误,用户期望在出现错误时,程序能够采取合理的行为。如果由于出现错误而使得某些操作没有完成,程序应该:

  • 返回到一种安全状态,并能够让用户执行其他的命令;
  • 允许用户保存所有工作的结果,并以妥善的方式终止程序

异常处理的任务就是将控制权从产生错误的地方转移到能够处理这种情况的错误处理器。

对于方法中的错误,传统的做法是返回一个特殊的错误码,但是我们不可能任何情况下都能返回一个错误码来表示程序存在错误。比如一个返回整型的方法就不能简单地通过返回-1 表示错误,因为-1很可能是一个完全合法的结果,导致无法明确地将有效数据与无效数据加以区分。

那么为了解决这个错误结果和合法数据之间的区分,java采用的是:方法并不返回任何值,而是**抛出 (throw)**一个封装了错误信息的对象。

当出现异常的时候方法将会立刻退出,并不返回正常值(或任何值),也不会从调用这个方法的代码继续执行,取而代之的是,异常处理机制开始搜索能够处理这种异常状况的异常处理器(exception handler),如果存在可以处理这种异常的处理器那么会执行处理器中的步骤,如果不存在那么会抛出这个异常。

# 1.1 异常分类

异常对象都是派生于 Throwable 类的一个类实例,java 中内置的异常类不能满足需求,用户还可以创建自己的异常类,,所有的异常都是由 Throwable 继承而来,但在下一层立即分解为两个分 支:ErrorException

Error 类层次结构

描述了 Java 运行时系统的内部错误和资源耗尽错误。你的应用程序不应该抛出这种类型的对象。如果出现了这样的内部错误,除了通知用户,并尽力妥善地终止程序之外,你几乎无能为力。这种情况很少出现。

Exception 层次结构

这个层次结构又分解为两个分支:一个分支派生于 RuntimeException(运行时异常);另一个分支包含其他异常。

一般规则是:

由编程错误导致的异常属于 RuntimeException;如果程序本身没有问题,但由于像I/O 错误这类问题导致的异常属于其他异常。

Java 语言规范将派生于 Error 类或 RuntimeException 类的所有异常称为非检查型(unchecked)异常,所有其他的异常称为检查型(checked) 异常。编译器将检查你是否为所有的检查型异常提供了异常处理器。

# 1.2 声明检查型异常

如果遇到了无法处理的情况,Java 方法可以抛出一个异常。这个道理很简单:方法不仅需要告诉编译器将要返回什么值,还要告诉编译器有可能发生什么错误。

例如,一段读取文件的代码知道有可能读取的文件不存在,或者文件内容为空,因此,试图处理文件信息的代码就需要通知编译器可能会抛出 IOException 类的异常。 要在方法的首部指出这个方法可能抛出一个异常,所以要修改方法首部,以反映这个方法可能抛出的检查型异常。

例如,下面是标准类库中 FileInputStream 类的一个构造器的声明。

    public FileInputStream(String name) throws FileNotFoundException {
        this(name != null ? new File(name) : null);
    }
1
2
3

这个声明表示这个构造器将根据给定的 String 参数产生一个 FileInputStream 对象,但也 有可能出错而抛出一个 FileNotFoundException 异常。如果发生了这种糟糕情况,构造器将不会初始化一个新的 FileInputStream 对象,而是抛出一个FileNot FoundException 类对象。如果 这个方法真的拋出了这样一个异常对象,运行时系统就会开始搜索知道如何处理 FileNotFoundException 对象的异常处理器。

那什么情况下我们需要在方法上声明抛出异常呢?

  • 调用了一个抛出检查型异常的方法,例如,FileInputStream 构造器。
  • 检测到一个错误,并且利用 throw 语句抛出一个检查型异常(下一节将详细介绍 throw 语句)。
  • 程序出现错误,例如,a[-1]=0 会抛出一个非检查型异常(这里会拋出 ArrayIndexOutOBoundsException)
  • Java 虚拟机或运行时库出现内部错误。

如果出现前两种情况,则必须告诉调用这个方法的程序员有可能抛出异常?因为任何一个抛出异常的方法都有可能是一个死亡陷阱。如果没有处理器捕获这个异常,当前执行的线程就会终止。

一个方法也可以抛出多个异常,所以我们需要再方法声明上声明多个会抛出的异常。

不只是声明异常,我们还可以捕获异常。这样就不会使方法调用被中断执行抛出异常,而是进入异常处理的逻辑,在异常处理中可以选择抛出或者返回相应的异常结果。

如果在子类中覆盖了超类的一个方法,子类方法中声明的检查型异常不能比超类方法中声明的异常更通用 (子类方法可以拋出更特定的异常,或者根本不拋出任何异常)。特别需要说明的是,如果超类方法没有抛出任何检查型异常,子类也不能抛出 任何检查型异常

# 1.3 如何抛出异常

  1. 找到一个合适的异常类。
  2. 创建这个类的一个对象。
  3. 将对象抛出。一旦方法抛出了异常,这个方法就不会返回到调用者。也就是说,不必操心建立一个默认的返回值或错误码。

代码示例:

public class Test {
    public static void main(String[] args) throws FileNotFoundException {
        read("a.txt");
    }
 
    // 如果定义功能时有问题发生需要报告给调用者。可以通过在方法上使用throws关键字进行声明
    public static void read(String path) throws FileNotFoundException {
        if (!path.equals("a.txt")) {//如果不是 a.txt这个文件 
            throw new FileNotFoundException("文件不存在");
        }
    }
}

1
2
3
4
5
6
7
8
9
10
11
12
13

throws用于进行异常类的声明,若该方法可能有多种异常情况产生,那么在throws后面可以写多个异常类,用逗号隔开。

public class Test {
    public static void main(String[] args) throws IOException {
        read("a.txt");
    }
 
    public static void read(String path)throws FileNotFoundException, IOException {
        if (!path.equals("a.txt")) {//如果不是 a.txt这个文件 
            throw new FileNotFoundException("文件不存在");
        }
        if (!path.equals("b.txt")) {
            throw new IOException();
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# 1.4 创建异常类

当我们遇到任何标准异常类都无法描述清楚的问题,我们就可以自定义实现异常类。我们需要做的只是定义一个派生于 Exception 的类,或者派生于Exception 的某个子类,如IOException

首先定义一个登陆异常类RegisterException:

// 业务逻辑异常
public class RegisterException extends Exception {
    /**
     * 空参构造
     */
    public RegisterException() {
    }
 
    /**
     *   message表示异常提示
     */
    public RegisterException(String message) {
        super(message);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# 2.捕获异常

Java中对异常有针对性的语句进行捕获,可以对出现的异常进行指定方式的处理

# 1.捕获异常

如果发生了某个异常,但没有在任何地方捕获这个异常,程序就会终止,并在控制台上 打印一个消息,其中包括这个异常的类型和一个堆栈轨迹。

try-catch的方式就是捕获异常

public class Test {
    public static void main(String[] args) {
        try {// 当产生异常时,必须有处理方式。要么捕获,要么声明。
            read("b.txt");
        } catch (FileNotFoundException e) {// 括号中需要定义什么呢?
          	//try中抛出的是什么异常,在括号中就定义什么异常类型
            System.out.println(e);
        }
        System.out.println("over");
    }
  
    public static void read(String path) throws FileNotFoundException {
        if (!path.equals("a.txt")) {//如果不是 a.txt这个文件 
            throw new FileNotFoundException("文件不存在");
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

如果 try 语句块中的任何代码抛出了 catch 子句中指定的一个异常类,那么

  1. 程序将跳过 try 语句块的其余代码。
  2. 程序将执行 catch 子句中的处理器代码。

当然也可以不处理异常,将异常声明在方法上,将异常传递给调用方法处理。

如果编写一个方法覆盖超类的方法,而这个超类方法没有抛出异常,你就必须捕获你的方法代码中出现的每一个检查型异常。不允许在子类的 throws 说明符中出现超类方法未列出的异常类。

# 2.捕获多个异常

在一个try 语句块中可以捕获多个异常类型,并对不同类型的异常做出不同的处理。要为每个异常类型使用一个单独的 catch 子句,如下例所示:

try 
{
code that might throw exceptions

}catch (FileNotFoundException e){

emergency actionfor missingfiles 

}catch (UnknownHostException e){

emergency action for unknown hosts

}catch (IOException e){

emergency actionfor all other I/O problems
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

# 3.再次抛出异常与异常链

可以在 catch 子句中抛出一个异常。通常,希望改变异常的类型时会这样做。如果开发了一个供其他程序员使用的子系统,可以使用一个指示子系统故障的异常类型,这很有道理。ServletException 就是这样一个异常类型的例子。执行一个servlet 的代码可能不想知道发生错误的细节原因,但希望明确地知道 servlet 是否有问题。

# 4.finally 子句

finally:有一些特定的代码无论异常是否发生,都需要执行。另外,因为异常会引发程序跳转,导致有些语句执行不到。而finally就是解决这个问题的,在finally代码块中存放的代码都是一定会被执行的。

当我们在try语句块中打开了一些物理资源(磁盘文件/网络连接/数据库连接等),我们都得在使用完之后,最终关闭打开的资源。

try...catch....finally:自身需要处理异常,最终还得关闭资源。

注意:finally不能单独使用。

public class Test {
    public static void main(String[] args) {
        try {
            read("a.txt");
        } catch (FileNotFoundException e) {
            //抓取到的是编译期异常  抛出去的是运行期 
            throw new RuntimeException(e);
        } finally {
            System.out.println("不管程序怎样,这里都将会被执行。");
        }
        System.out.println("over");
    }
 
    public static void read(String path) throws FileNotFoundException {
        if (!path.equals("a.txt")) {//如果不是 a.txt这个文件 
            throw new FileNotFoundException("文件不存在");
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

tips:当只有在try或者catch中调用退出JVM的相关方法,此时finally才不会执行,否则finally永远会执行。

# 5.带资源的try语句

它是一个特殊的try语句版本,旨在确保每个资源都在语句结束时关闭。资源是指在程序执行期间必须明确管理的对象,比如文件、网络连接或数据库连接等,在使用完这些资源后应当正确地释放它们以避免资源泄露

try-with-resources语句可以自动管理实现了java.lang.AutoCloseable接口的对象,该接口包含了一个close()方法,用于定义如何释放资源。当try块被执行完毕(不论是正常完成还是因为抛出异常而提前退出),try-with-resources语句会自动调用资源的close()方法来关闭资源。

以下是try-with-resources语句的基本语法:

try (ResourceType resource = new ResourceType()) {
    // 使用资源的代码
} catch(SomeException e) {
    // 异常处理代码
}
1
2
3
4
5

在这个例子中,ResourceType是实现AutoCloseable接口的类,而resource是在try语句中创建的一个实例。当try语句体执行完毕后,resource.close()会被自动调用。

从Java 7开始引入了try-with-resources语句,并且从Java 9开始,如果资源已经在try语句外部声明,那么可以在try-with-resources语句中引用这些变量而不重新声明它们。例如:

ResourceType resource = new ResourceType();
try (resource) { // Java 9 及以上版本支持这种方式
    // 使用资源的代码
} catch(SomeException e) {
    // 异常处理代码
}
1
2
3
4
5
6

这使得代码更加简洁,并且确保了资源总是被适当地关闭,即使发生了异常。

# 6.分析堆栈轨迹元素

堆栈轨迹(stack trace)是程序执行时的函数调用序列,它展示了从程序启动到发生某个事件(例如异常或错误)时所经历的所有方法或函数调用。当一个异常被抛出且未被捕获时,Java虚拟机(JVM)会生成一个堆栈轨迹,并将其打印出来,这有助于开发者定位和理解问题发生的上下文。

分析堆栈轨迹元素通常涉及以下几个方面:

  1. 异常类型:堆栈轨迹的第一行通常包含抛出的异常或错误的类型。了解异常类型对于确定问题的本质非常重要,因为不同的异常可能指示不同类型的编程错误或运行时条件。
  2. 消息描述:紧接在异常类型之后的是一个可选的消息,它提供了关于该异常的更多信息。这个消息是由异常构造函数的参数设置的,可以包括文件名、行号、变量值等。
  3. 类名和方法名:每一行堆栈轨迹都显示了异常传播路径上的一个方法调用,其中包含了类名和方法名。这些信息帮助你追踪代码中哪个方法首先抛出了异常,以及异常是如何从一个方法传递到另一个方法的。
  4. 文件名和行号:如果代码是用Java编译的,并且编译时包含了调试信息,则堆栈轨迹中的每一行还会显示源文件的名称和异常发生的确切行号。这对于直接定位到源代码中的问题位置非常有用。
  5. 库和框架:有时候,堆栈轨迹会延伸到第三方库或框架的方法调用。虽然你可能无法直接修改这些库的源代码,但知道问题是发生在哪个库中可以帮助你找到合适的解决方案,比如更新库版本或者查阅相关文档。
  6. 嵌套异常:某些情况下,一个异常可能是由另一个异常引发的。在这种情况下,原始异常的信息也会包含在堆栈轨迹中,通常通过Caused by:来标识。检查这些嵌套异常可以提供更深入的问题根源信息。
  7. 线程信息:如果应用程序是多线程的,堆栈轨迹也可能包含有关哪个线程引发了异常的信息。这对于诊断并发问题特别有帮助。

当你收到一个堆栈轨迹时,应该从最顶部开始阅读,即异常最初被抛出的地方。然后向下追溯,直到你找到你自己的代码,这样可以帮助你快速定位问题。如果你是在查看一个日志文件中的堆栈轨迹,确保你有足够的上下文来理解它,比如时间戳、线程ID、日志级别等,这些都有助于更好地理解和解决问题。

# 3.使用异常机制的技巧

# 1.异常处理不能代替简单的测试

作为一个示例, 在这里编写了一段代码, 试着上百万次地对一个空栈进行退栈操作。在实施退栈操作之前, 首先要查看栈是否为空

if (!s.empty()) s.pop();

try{
		s.popO;
	}
catch (EmptyStackException e){
    
}
1
2
3
4
5
6
7
8

调用isEmpty的版本运行时间为646毫秒。捕获EmptyStackException的版本运行时间为21739毫秒.与执行简单的测试相比, 捕获异常所花费的时间大大超过了前者, 因此使用异常的基本规则是:只在异常情况下使用异常机制

# 2.不要过分地细化异常

PrintStream out;
Stack s;
for (i = 0;i < 100; i++)
{
    try{
        n = s.popO;
    }catch (EmptyStackException e){
        // stack was empty
    }
    try{
       out.writelnt(n);
    }catch (IOException e){
        // problem writing to file
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

这种编程方式将导致代码量的急剧膨胀。

首先看一下这段代码所完成的任务。在这里,希望从栈中弹出100个数值,然后将它们存人一个文件中。如果栈是空的,则不会变成非空状态;如果文件出现错误, 则也很难给予排除。出现上述问题后,这种编程方式无能为力。因此,有必要将整个任务包装在一个 try语句块中,这样, 当任何一个操作出现问题时, 整个任务都可以取消。

优化后的代码

try{
    for (i = 0; i < 100; i++){
    n = s.popO ;
    out.writelnt(n);
	}
}catch (IOException e){

	// problem writing to file

}catch (EmptyStackException e){

	// stack was empty

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

满足了异常处理机制的其中一个目标,将正常处理与错误处理分开。

# 3.利用异常层次结构

不要只抛出 RuntimeException 异常。应该寻找更加适当的子类或创建自己的异常类,不要只捕获 Thowable 异常,否则,会使程序代码更难读、更难维护

考虑受查异常与非受查异常的区别。 已检查异常本来就很庞大,不要为逻辑错误抛出这些异常 。 将一种异常转换成另一种更加适合的异常时不要犹豫。例如,在解析某个文件中的一个整数时,捕获NumberFormatException异常,然后将它转换成 IOException 或MySubsystemException 的子类。

# 4.不要压制异常

“在编程中处理异常时,不应该简单地捕获异常而不做任何处理或记录。当一个异常被抛出时,它通常指示程序遇到了某种错误或非预期的情况,而如果开发者选择忽视这些异常(即不采取任何行动),可能会导致程序的行为变得不确定、难以调试,并且隐藏了潜在的问题。

public Image loadImage(String s)
{
try
{
// code that threatens to throw checked exceptions
}
catch (Exception e)
{} // so there
}
1
2
3
4
5
6
7
8
9

这段内容描述了一种不良的异常处理实践,即所谓的“压制异常”或“静默捕获异常”。代码示例中展示的方法loadImage试图通过捕获所有可能抛出的受检异常(checked exceptions),然后不做任何处理(即空的catch块)来规避编译器对异常声明的要求。这样做确实可以让代码编译通过,但这是非常不推荐的做法。

# 5.在检测错误时,“ 苛刻” 要比放任更好

当检测到错误的时候, 有些程序员担心抛出异常。在用无效的参数调用一个方法时,返回一个虚拟的数值, 还是抛出一个异常, 哪种处理方式更好? 例如, 当栈空时,Stack.p0p 是返回一个 null, 还是抛出一个异常? 我们认为:在出错的地方抛出一个 EmptyStackException异常要比在后面抛出一个 NullPointerException 异常更好。

# 6.不要羞于传递异常

很多程序员都感觉应该捕获抛出的全部异常。

如果调用了一个抛出异常的方法,例如,FilelnputStream 构造器或 readLine 方法,这些方法就会本能地捕获这些可能产生的异常。其实,传递异常要比捕获这些异常更好:让高层次的方法通知用户发生了错误, 或者放弃不成功的命令更加适宜。

# 7.正确处理异常的建议

  1. 明确处理:对于每个捕获到的异常,应该有明确的处理逻辑。这可能包括尝试恢复操作、通知用户、重试失败的操作等。
  2. 记录日志:即使你不能立即解决异常,也应该记录下异常的信息(如堆栈轨迹)到日志文件中。这对于后续的故障排查和问题分析非常重要。
  3. 传递信息:有时你可能需要将异常包装成一个新的异常类型并再次抛出,以提供更具体的应用上下文给调用者。这种做法被称为异常链,可以更好地传达问题的本质。
  4. 避免空catch块:永远不要使用没有任何代码的catch块,因为这样做会完全忽略异常的发生,使得问题更加难以发现。即使是为了暂时忽略某个异常,也应当添加注释说明原因,并尽快返回修复。
  5. 资源清理:确保所有资源(如文件、网络连接等)都在异常发生后得到正确的释放。Java中的try-with-resources语句是一个很好的工具,可以帮助自动管理资源的生命周期。
  6. 合理使用finallyfinally块中的代码无论是否发生异常都会被执行,适合用于确保某些关键性的清理工作总是被执行。
  7. 考虑业务逻辑:根据应用程序的具体需求,决定是让异常传播给调用者还是在当前层面上进行处理。有些情况下,让异常冒泡到更高的层级可能是更合适的做法。
  8. 不要滥用异常:异常应该用来处理真正的错误条件,而不是作为控制流的一部分。例如,不应通过抛出异常来实现循环退出或普通的流程控制。

# 4.使用断言

断言(assertion)是编程中用于调试目的的一种工具,它允许开发者在代码中插入检查点以验证程序的状态或逻辑是否符合预期。断言通常用于开发和测试阶段,在生产环境中可能会被禁用以避免性能影响。

# 1.断言的基本概念

  • 断言条件:每个断言包含一个布尔表达式,该表达式应该反映程序的一个假设或不变量。
  • 动作:如果断言的条件为false,则会触发一个异常或者错误报告,表明程序状态与预期不符。这可以提示开发者存在潜在的问题需要修复。
  • 调试用途:断言主要用于捕捉不应该发生的逻辑错误,帮助开发者快速定位问题。它们不是用来处理正常的业务逻辑或用户输入验证。

Java中的断言

在Java中,断言是通过assert关键字实现的。语法如下:

assert condition : optionalDetailMessage;
1
  • condition 是一个布尔表达式。
  • optionalDetailMessage 是一个可选参数,当断言失败时,这个消息会被输出作为额外的信息。

例如:

public void divide(int numerator, int denominator) {
    assert denominator ! = 0 : "Denominator must not be zero";
    // method body...
}
1
2
3
4

在这个例子中,如果denominator等于0,那么断言将失败,并且抛出一个AssertionError,同时带有信息"Denominator must not be zero"。

使用场景

  • 前置条件:确保方法调用前满足某些条件,如参数的有效性。
  • 后置条件:保证方法执行完毕后满足某些状态,如返回值的有效性。
  • 不变量:确认对象或系统的某些属性在整个程序运行期间保持不变。

注意事项

  • 性能考虑:由于断言可能会影响性能,因此它们通常只在开发和测试阶段启用。可以通过命令行选项-ea(enable assertions)来启用断言,使用-da(disable assertions)来禁用它们。
  • 不是替代品:断言不应替代正常的错误处理机制,如try-catch块。它们也不应用于处理用户输入或外部资源的状态,因为这些情况应该总是被正常处理而不是依赖于断言。
  • 文档化:虽然断言有助于捕获错误,但不应该仅靠断言来确保程序的正确性。良好的单元测试和代码审查仍然是必要的。

总之,断言是一种强大的调试工具,可以帮助程序员迅速找到并解决代码中的逻辑错误,但在实际部署时应谨慎使用,确保不会影响应用程序的稳定性和性能。

# 2.启用和禁用断言

在默认情况下, 断言被禁用。可以在运行程序时用 -enableassertions 或 -ea 选项启用

java -enableassertions MyApp
1

需要注意的是,在启用或禁用断言时不必重新编译程序。启用或禁用断言是类加载器(class loader) 的功能。当断言被禁用时,类加载器将跳过断言代码, 因此,不会降低程序运行的速度。

也可以在某个类或整个包中使用断言, 例如

java -ea:MyClass -ea:com.mycompanypany.mylib.. , MyApp
1

这条命令将开启 MyClass 类以及在 com.mycompany.mylib 包和它的子包中的所有类的断言。选项 -ea 将开启默认包中的所有类的断言

也可以用选项 -disableassertions 或 -da 禁用某个特定类和包的断言:

java -ea:... -da:MyClass MyApp  
1

有些类不是由类加载器加载, 而是直接由虚拟机加载。可以使用这些开关有选择地启用或禁用那些类中的断言。

然而, 启用和禁用所有断言的 -ea 和 -da 开关不能应用到那些没有类加载器的“ 系统类”上。对于这些系统类来说,

需要使用-enablesystemassertions/-esa 开关启用断言

# 3.使用断言完成参数检查

在 Java 语言中, 给出了 3 种处理系统错误的机制:

  • 抛出一个异常
  • 日志
  • 使用断言

什么时候应该选择使用断言呢? 请记住下面几点:

  • 断言失败是致命的、不可恢复的错误
  • 断言检查只用于开发和测阶段

不应该使用断言向程序的其他部分通告发生了可恢复性的错误,或者,不应该作为程序向用户通告问题的手段。断言只应该用于在测试阶段确定程序内部的错误位置

类加载器设置断言

java.Iang.ClassLoader 1.0

void setDefaultAssertionStatus ( boolean b ) 1.4

对于通过类加载器加载的所有类来说, 如果没有显式地说明类或包的断言状态, 就启用或禁用断言。
    

• void setCIassAssertionStatus ( String className , boolean b ) 1.4

对于给定的类和它的内部类,启用或禁用断言。
    

• void setPackageAssertionStatus ( String packageName , bool ean b ) 1.4

对于给定包和其子包中的所有类,启用或禁用断言。

    
• void clearAssertionStatus( ) 1.4

移去所有类和包的显式断言状态设置, 并禁用所有通过这个类加载器加载的类的断言。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19