一、异常
1. 异常分类
在 Java 中所有异常类型都是内置类 java.lang.Throwable 类的子类,即 Throwable 位于异常类层次结构的顶层。
Throwable 类是所有异常和错误的超类,下面有 Error 和 Exception 两个子类分别表示错误和异常。其中异常类 Exception 又分为运行时异常(RuntimeException)和非运行时异常(如IOExcption),这两种异常有很大的区别,也称为非受检异常(Unchecked Exception)和受检异常(Checked Exception)。
- Error类:描述了JAVA运行时系统的内部错误和资源耗尽错误,一般指的是 JVM 错误,如堆栈溢出。通常我们不关注这个类,因为它们很少出现,并且通常是灾难性的致命错误,不是程序可以控制的,我们对此无能为力。
- 运行时异常:都是 RuntimeException 类及其子类异常,一般都是由于代码里面出现了很明显的逻辑错误,如:错误的类型转换ClassCastException、数组访问越界ArraylndexOutOfBoundException、访问空指针NullPointerException等。因此如果出现这个异常,那么“就一定是你的问题”,应该从代码逻辑角度避免这种异常,而不是选择抛出。
- 非运行时异常:也即编译期异常,该异常必须在代码中被抛出,否则编译就不会通过。它们是RuntimeException 以外的异常,类型上都属于 Exception 类及其子类。我们最常抛出的是这类异常,就是说这种异常是未知的,比如我们无法预见用户的输入是否满足我们要求、当我们打开一个文件时无法知道这个目录下是否真的存在这个文件等。因此,我们无法在程序中避免这类异常的发生,只能当发生异常时将其抛出。
Error类及其子类、RuntimeException类及其子类都是非受检异常,而其他的需要抛出的是受检异常。
Java 的异常处理通过 5 个关键字来实现:try、catch、throw、throws 和 finally。try catch 语句用于捕获并处理异常,finally 语句用于在任何情况下(除特殊情况外)都必须执行的代码,throw 语句用于拋出异常,throws 语句用于声明可能会出现的异常。
2. try-catch语句
在 Java 中通常采用 try catch 语句来捕获异常并处理。
典型语法如下:
try {
// 可能发生异常的语句
} catch(ExceptionType e) {
// 处理异常语句
}
- 如果 try 语句块中发生异常,那么一个相应的异常对象就会被拋出,然后 catch 语句就会依据所拋出异常对象的类型进行捕获,并处理。处理之后,程序会跳过 try 语句块中剩余的语句,转到 catch 语句块后面的第一条语句开始执行。
- 如果 try 语句块中没有异常发生,那么 try 块正常结束,后面的 catch 语句块被跳过,程序将从 catch 语句块后的第一条语句开始执行。
在上面语法的处理代码块 1 中,可以使用以下 3 个方法输出相应的异常信息。
- printStackTrace() 方法:指出异常的类型、性质、栈层次及出现在程序中的位置(关于 printStackTrace 方法的使用可参考下面的异常跟踪栈一节)。
- getMessage() 方法:输出错误的性质。
- toString() 方法:给出异常的类型与性质。
catch可以捕获多个异常,如下:
在多个 catch 代码块的情况下,当一个 catch 代码块捕获到一个异常时,其它的 catch 代码块就不再进行匹配。
注意:当捕获的多个异常类之间存在父子关系时,捕获异常时一般先捕获子类,再捕获父类。所以子类异常必须在父类异常的前面,否则子类捕获不到。
自定义异常
只需要定义一个派生于Exception的类,或者派生于Exception子类的类即可。习惯上,定义的类应该包含两个构造器,一个是默认构造器,另一个是带有默认信息的构造器(主要用于传入字符串,方便以后打印异常信息)。如下:
调用如下:
抛出异常那一句也可以写为:throw new FileFormatException("this file is not format."),这样就可以使用toString方法打印了。
异常跟踪栈printStackTrace
异常对象的 printStackTrace() 方法用于打印异常的跟踪栈信息,根据 printStackTrace() 方法的输出结果,开发者可以找到异常的源头,并跟踪到异常一路触发的过程。
例如:
class SelfException extends RuntimeException {
SelfException() {
}
SelfException(String msg) {
super(msg);
}
}
public class PrintStackTraceTest {
public static void main(String[] args) {
firstMethod();
}
public static void firstMethod() {
secondMethod();
}
public static void secondMethod() {
thirdMethod();
}
public static void thirdMethod() {
throw new SelfException("自定义异常信息");
}
}
输出:
上面运行结果的第 2 行到第 5 行之间的内容是异常跟踪栈信息,从打印的异常信息我们可以看出,异常从 thirdMethod 方法开始触发,传到 secondMethod 方法,再传到 firstMethod 方法,最后传到 main 方法,在 main 方法终止,这个过程就是 Java 的异常跟踪栈。
只要异常没有被完全捕获(包括异常没有被捕获,或异常被处理后重新抛出了新异常),异常从发生异常的方法逐渐向外传播,首先传给该方法的调用者,该方法调用者再次传给其调用者……,直至最后传到 main 方法,如果 main 方法依然没有处理该异常,则 JVM 会中止该程序,并打印异常的跟踪栈信息。
下面这个例子输出:"自定义异常信息"
class SelfException extends RuntimeException {
SelfException() {
}
SelfException(String msg) {
super(msg);
}
}
public class Main {
public static void main(String[] args) {
firstMethod();
}
public static void firstMethod() {
secondMethod();
}
public static void secondMethod() {
try {
thirdMethod();
} catch (SelfException e) {
System.out.println(e.getMessage());
}
}
public static void thirdMethod() {
throw new SelfException("自定义异常信息");
}
}
下面这个例子输出:
class SelfException extends RuntimeException {
SelfException() {
}
SelfException(String msg) {
super(msg);
}
}
public class Main {
public static void main(String[] args) {
firstMethod();
}
public static void firstMethod() {
secondMethod();
}
public static void secondMethod() {
try {
thirdMethod();
} catch (SelfException e) {
e.printStackTrace();
}
}
public static void thirdMethod() {
throw new SelfException("自定义异常信息");
}
}
3. try-catch-finally语句
在实际开发中,根据 try catch 语句的执行过程,try 语句块和 catch 语句块有可能不被完全执行,而有些处理代码则要求必须执行。例如,程序在 try 块里打开了一些物理资源(如数据库连接、网络连接和磁盘文件等),这些物理资源都必须显式回收。这时就需要使用finally.
try {
// 可能会发生异常的语句
} catch(ExceptionType e) {
// 处理异常语句
} finally {
// 清理代码块
}
此外,finally 语句也可以和 try 语句匹配使用,其语法格式如下:
try {
// 逻辑代码块
} finally {
// 清理代码块
}
注意:
- 异常处理语法结构中只有 try 块是必需的,也就是说,如果没有 try 块,则不能有后面的 catch 块和 finally 块;
- catch 块和 finally 块都是可选的,但 catch 块和 finally 块至少出现其中之一,也可以同时出现;
- 可以有多个 catch 块,捕获父类异常的 catch 块必须位于捕获子类异常的后面;
- 不能只有 try 块,既没有 catch 块,也没有 finally 块;
- 多个 catch 块必须位于 try 块之后,finally 块必须位于所有的 catch 块之后。
- finally 与 try 语句块匹配的语法格式,此种情况会导致异常丢失,所以不常见。
非常重要的一点:无论是否有异常拋出,都会执行 finally 语句块中的语句!!!
- 如果 try 代码块中没有拋出异常,则执行完 try 代码块之后直接执行 finally 代码块,然后执行 try catch finally 语句块之后的语句。
- 如果 try 代码块中拋出异常,并被 catch 子句捕捉,那么在拋出异常的地方终止 try 代码块的执行,转而执行相匹配的 catch 代码块,之后执行 finally 代码块。如果 finally 代码块中没有拋出异常,则继续执行 try catch finally 语句块之后的语句;如果 finally 代码块中拋出异常,则把该异常传递给该方法的调用者。
- 如果 try 代码块中拋出的异常没有被任何 catch 子句捕捉到,那么将直接执行 finally 代码块中的语句,并把该异常传递给该方法的调用者。
finally和return的执行顺序(非常重要)
只需要记住一点:因为不管怎么样,finally语句块必须执行,所以当在try或catch中遇到return时,先不要着急return,而要先去finally语句块里执行完。如果finally里也有return,那么就可以直接return了,如果finally没有return,那么执行完finally后,再回到try或catch里的return部分,执行return语句。(不过需要注意,如果finally里没有return语句,那么即使在finally里更改返try的回值,也不会起作用)
例1:try 中带有 return,finally中没有return
public class tryDemo {
public static int show() {
try {
return 1;
} finally {
System.out.println("执行finally模块");
}
}
public static void main(String args[]) {
System.out.println(show());
}
}
输出:
执行finally模块
1
例2:try 和 catch 中都带有 return,finally中不带有return
public class tryDemo {
public static int show() {
try {
int a = 8 / 0;
return 1;
} catch (Exception e) {
return 2;
} finally {
System.out.println("执行finally模块");
}
}
public static void main(String args[]) {
System.out.println(show());
}
}
输出:
执行finally模块
2
例3: finally 中带有 return,但是没有更改try的返回值
public class tryDemo {
public static int show() {
try {
int a = 8 / 0;
return 1;
} catch (Exception e) {
return 2;
} finally {
System.out.println("执行finally模块");
return 0;
}
}
public static void main(String args[]) {
System.out.println(show());
}
}
输出:
执行finally模块
0
例4:try中有return,finally中没有return,并且finally改变try的返回值
public class tryDemo {
public static int show() {
int result = 0;
try {
return result;
} finally {
System.out.println("执行finally模块");
result = 1;
}
}
public static void main(String args[]) {
System.out.println(show());
}
}
输出:
执行finally模块
0
例5: try和finally中都没有return,但是finally改变了try变量的值
public class tryDemo {
public static void show() {
int result = 0;
try {
result = 1;
} finally {
System.out.println("执行finally模块");
result = 2;
}
System.out.println(result);
}
public static void main(String args[]) {
show();
}
}
输出:
执行finally模块
2
4. 声明和抛出异常
Java 中的异常处理除了捕获异常和处理异常之外,还包括声明异常和拋出异常。声明异常用throws,抛出异常用throw。
throws声明异常
当一个方法产生一个它不处理的异常时,那么就需要在该方法的头部声明这个异常,以便将该异常传递到方法的外部进行处理。
典型格式如下:
returnType method_name(paramList) throws Exception 1,Exception2,…{…}
声明的这些异常类可以是方法中调用了可能拋出异常的方法而产生的异常,也可以是方法体中生成并拋出的异常。
使用 throws 声明抛出异常的思路是,当前方法不知道如何处理这种类型的异常,该异常应该由向上一级的调用者处理;如果 main 方法也不知道如何处理这种类型的异常,也可以使用 throws 声明抛出异常,该异常将交给 JVM 处理。JVM 对异常的处理方法是,打印异常的跟踪栈信息,并中止程序运行,这就是前面程序在遇到异常后自动结束的原因。
例如:创建一个 readFile() 方法,该方法用于读取文件内容,在读取的过程中可能会产生 IOException 异常,但是在该方法中不做任何的处理,而将可能发生的异常交给调用者处理。在 main() 方法中使用 try catch 捕获异常,并输出异常信息。
import java.io.FileInputStream;
import java.io.IOException;
public class Test04 {
public void readFile() throws IOException {
// 定义方法时声明异常
FileInputStream file = new FileInputStream("read.txt"); // 创建 FileInputStream 实例对象
int f;
while ((f = file.read()) != -1) {
System.out.println((char) f);
f = file.read();
}
file.close();
}
public static void main(String[] args) {
Throws t = new Test04();
try {
t.readFile(); // 调用 readFHe()方法
} catch (IOException e) {
// 捕获异常
System.out.println(e);
}
}
}
方法重写时声明抛出异常有一个限制:子类方法声明抛出的异常类型应该是父类方法声明抛出的异常类型的子类或相同,子类方法声明抛出的异常不允许比父类方法声明抛出的异常多。看如下程序。
public class OverrideThrows {
public void test() throws IOException {
FileInputStream fis = new FileInputStream("a.txt");
}
}
class Sub extends OverrideThrows {
// 子类方法声明抛出了比父类方法更大的异常
// 所以下面方法出错
public void test() throws Exception {
}
}
throw 拋出异常
当 throw 语句执行时,它后面的语句将不执行,此时程序转向调用者程序,寻找与之相匹配的 catch 语句,执行相应的异常处理程序。如果没有找到相匹配的 catch 语句,则再转向上一层的调用程序。这样逐层向上,直到最外层的异常处理程序终止程序并打印出调用栈情况。
throws 关键字和 throw 关键字在使用上的几点区别如下:
- throws 用来声明一个方法可能抛出的所有异常信息,表示出现异常的一种可能性,但并不一定会发生这些异常;throw 则是指拋出的一个具体的异常类型,执行 throw 则一定抛出了某种异常对象。
- 通常在一个方法(类)的声明处通过 throws 声明方法(类)可能拋出的异常信息,而在方法(类)内部通过 throw 声明一个具体的异常信息。
- throws 通常不用显示地捕获异常,可由系统自动将所有捕获的异常信息抛给上级方法; throw 则需要用户自己捕获相关的异常,而后再对其进行相关包装,最后将包装后的异常信息抛出。
二、断言
1. 断言格式
Java中断言有两种格式:
assert 条件;
或者
assert 条件 :表达式;
这两种形式都会对条件进行检测,如果结果为fasle,则会抛出一个AssertionError异常。在第二种形式中,表达式将被传入AssertionError的构造器,并转换成一个消息字符串。
1.2 启用与禁用断言
在默认情况下,断言是被禁用的。可以使用-enableassertions或-ea选项启用它。
1. 对整个的大型应用程序MyApp的全部包、类开启断言有效:java -enableassertions MyAPP
2. 只对特定的包、类开启断言有效:java -ea:MyClass -ea:com.mycompany.myLib ... MyApp // 将开启MyClass类以及在com.mycompany.myLib包及其所有子包中所有类的断言;
3. 可以使用-disableassertions或-da禁用某个特定类或包的断言:java -ea:... -da:MyClass MyApp // 禁用MyClass类中的断言
三、记录日志
一定要记住,在规范化的开发代码时,一定不要使用System.out.println这种将信息输出到控制台的方式,而是要采用记录日志的方法。记录日志有如下好处:
具体的日志记录方法可以百度一下,一大堆。