异常处理
目前,衡量一门语言是否成熟,异常处理是重要的方面。
增加异常处理之后,程序会具有更好的容错性,会更加健壮。
比如一个简单的程序:
让用户输入2个数字,结算它们的除法的结果:
(1)分别获取用户输入的每个子串
(2)每个子串强转为一个数字。
(3)计算他们的结果。
软件是什么? 软件是一个虚拟的世界。
用户会有一些习惯、操作,并不符合我们程序的期望,或程序运行过程中,总会有些情况, 超出程序的预料……
如果没有很好的错误处理(如果没有考虑这些情况),程序就会报错、中止。
为了让程序能真正正确——不管用户如何操作,程序应该能运行良好,我们需要做大量的判断。
- 判断用户是否输入了2个子串,
- 判断用户是否输入非数字
- 判断用户输入的除数是否为0
于是程序需要这样写(传统的错误处理流程):
if(错误1)
{
// 处理错误
// retry
}
if(错误2)
{
// 处理错误
// retry
}
if(错误3)
{
// 处理错误
// retry
}
...
else
{
}
传统错误处理机制,主要如下两个缺点:
- 无法穷举所有异常情况:因为人类知识的限制,异常情况总比可以考虑到的情况多,总有“漏网之鱼”的异常情况,所以程序总是不够健壮。
- 错误处理代码和业务实现代码混杂:这种错误处理和业务实现混杂的代码严重影响程序的可读性,会增加程序维护的难度
考虑将上面写法改为如下形式:
if(任何错误)
{
// 处理错误
// retry
}
else
{
// 业务处理
}
但上面的任何错误这个条件,无法用计算机语言来表达,因此考虑写成如下形式:
try
{
// 业务处理
}
catch(异常类 异常变量名) ____只要有异常,都会被catch块捕捉,从而进入catch块。
{
}
▲ 用try catch来捕捉异常
(1) 业务代码代码执行过程中,如果出现错误,系统会将该错误包装成异常对象,并提交给JRE
这个过程叫抛出异常。
(2) JRE收到异常后,JRE会依次寻找try块之后的多个catch块,如果异常对象是catch后的形参类型
或其子类的实例(instanceof),程序就会进入对应的catch块,这个过程叫捕获异常。
catch块可以有0~N个,N个catch块分别捕捉不同类型的异常。
多个catch块,应该先捕捉小异常(子类异常)。
▲ Java的异常体系:
Throwable
↗ ↖
Error Exception
↗ ↖ ↗ ↖
IOError LinkedError RuntimeException IOException
↗ ↖
NullPointerException ClassCastException
▲ Java 7的多异常捕捉:
传统情况下, 每个catch块后只有一个异常类型,因此每个catch块只能捕捉一个异常。
Java 7的多异常捕捉可以让个catch块后包含多个异常类型
多异常捕捉的优点:代码更加简洁。
缺点:由于一个catch块捕捉了多个异常,因此程序无法分清到底是哪种异常情况。
多异常捕捉的注意点:catch块中异常型形参,相当默认有一个final修饰。
▲ 访问异常信息:
当我们用catch块捕捉到异常之后,程序可能需要了解异常发生详细情况,此时就可通过异常对象进行获取:
- getMessage():返回该异常的详细描述字符串。
- printStackTrace():将该异常的跟踪栈信息输出到标准错误输出。
- printStackTrace(PrintStream s):将该异常的跟踪栈信息输出到指定输出流。
- getStackTrace():返回该异常的跟踪栈信息。
异常的跟踪栈信息,信息非常丰富,可以向我们提供异常依次触发的“轨迹”,
因此当我们调试程序时,一定要找第一个at、我们自己的写的类所导致的异常。
▲ finally
代表一定会执行的块。作用是用于保证关闭、回收物理资源(比如网络连接、数据连接、文件IO资源等)。
放在finally,即可保证关闭资源的代码一定会执行。
如果finally块包含了return语句,那么该return语句会直接结束方法。
- finally不怕return,return无法阻止finally的执行。
- 退出虚拟机(System.eixt(0))可以阻止finally的执行。
完整的语法格式为:
try
{
// 业务代码
}
catch(异常1 ex1) 0~N次
{
}
finally 0~1次
{
}
但try不能单独存在,catch、finally不能同时出现零次。
▲ Java 7的自动关闭资源
传统的资源关闭,程序代码十分臃肿。
如果使用Java7的自动关闭资源的try语句,可以既没有finally,也没有catch。
对于自动关闭资源的try语句, 可以没有catch和finally——try块可以单独地存在。
自动关闭资源的try语句,有两个注意点:
- 只有放在try后面的圆括号里的资源才会被关闭。
自动关闭的资源必须在try后的圆括号内声明、并初始化。
自动关闭的资源只在try块中有效。
- 能被自动关闭的资源必须实现Closeable 或 AutoCloseable接口。
▲ checked异常与runtime异常
runtime异常 - 所有RuntimeException的实例,或RuntimeException的子类的实例,都是runtime异常。
它属于运行异常,程序员想捕捉、就捕捉,不捕捉也可以。
即使一个程序编译时,没有任何问题。但运行时有可能会引发错误。
ArithmeticException, ClassCastException,IllegalArgumentException, IndexOutOfBoundsException,
NullPointerException. NumberFormatExcpetion
checked异常 - 一个异常,不是runtime异常,就是checked异常。
编译器会强制检查checked异常,如果一个行代码可能引发checked异常,
编译器要求程序要么显式用try...catch捕捉该异常,要么使用throws声明抛出异常。
checked异常的争议:
缺点:编译器强制程序员要么用try...catch捕捉该异常,要么用throws声明抛出异常。
你知道如何处理该异常,就用catch捕捉、并处理该异常;如果不知道处理,用throws抛给别人处理。
因此导致编程比较繁琐。
实际上,只有Java有checked异常。
优点:checked异常时体现了一个设计哲学:没有完善的异常处理的代码,不应该有执行的机会。
throws —— 1。不能独立使用,只能放在方法签名上使用,用于声明抛出某个异常类。
2。throws可以同时声明抛出多个Checked异常类名。
备注:只有存在checked异常的前提下,throws关键字才有存在的意义。
▲ throws与方法重写
方法重写:2同(方法名、形参列表)2小(返回值类型、声明抛出的异常)1大(访问权限)
▲ throw抛出异常:
throw 异常实例;
- throw 异常实例;是一个可执行性的语句,只能放在方法、构造器、初始化块
- throw后面只能是一个异常对象。
在有些事情,用户输入的数据、提供的参数,与我们的业务规则不符合,编译器也无法保证我们业务规则,
此时就必须有成员来显式使用throw抛出异常。
如果你自己throw了一个checked异常,同样要么用try...catch捕捉,要么用throws声明抛出。
——但通常都是用throws声明抛出。
▲ 自定义异常:
当程序出现了与业务不符合的情况时,此时程序就应该使用throw来抛出异常。
此时,大部分应该都应该抛出项目自定义异常(因为系统的特定的异常类,本身就代表了一种具体的异常情况)
最好是异常的类名能让人做到“见名思义”,可以通过异常名表现出程序到底出现了什么问题。
如果要定义普通异常,继承Exception。
如果要定义runtime异常,继承RuntimeException。
通常来说,异常类体只要定义2个构造器即可,其中一个带String参数的构造器中,只要一行简单的super(msg)即可。
▲ 异常转译、异常链。
一个异常出现后,
- 不知道在如何处理, throws声明抛出。
- 怎么如何处理,catch到之后,把异常处理完,无需其他方法继续处理
catch到异常之后,可以进行部分处理,不能处理完。
因此异常还需要留给下一个调用者继续处理,此时就需要再次使用throw抛出一个自定义异常
catch块中,再次使用throw来抛出业务相关的自定义异常。
——这就会把原始异常,转译成的新的异常,这个过程被称为异常转译。
目前,衡量一门语言是否成熟,异常处理是重要的方面。
增加异常处理之后,程序会具有更好的容错性,会更加健壮。
比如一个简单的程序:
让用户输入2个数字,结算它们的除法的结果:
(1)分别获取用户输入的每个子串
(2)每个子串强转为一个数字。
(3)计算他们的结果。
软件是什么? 软件是一个虚拟的世界。
用户会有一些习惯、操作,并不符合我们程序的期望,或程序运行过程中,总会有些情况, 超出程序的预料……
如果没有很好的错误处理(如果没有考虑这些情况),程序就会报错、中止。
为了让程序能真正正确——不管用户如何操作,程序应该能运行良好,我们需要做大量的判断。
- 判断用户是否输入了2个子串,
- 判断用户是否输入非数字
- 判断用户输入的除数是否为0
于是程序需要这样写(传统的错误处理流程):
if(错误1)
{
// 处理错误
// retry
}
if(错误2)
{
// 处理错误
// retry
}
if(错误3)
{
// 处理错误
// retry
}
...
else
{
}
传统错误处理机制,主要如下两个缺点:
- 无法穷举所有异常情况:因为人类知识的限制,异常情况总比可以考虑到的情况多,总有“漏网之鱼”的异常情况,所以程序总是不够健壮。
- 错误处理代码和业务实现代码混杂:这种错误处理和业务实现混杂的代码严重影响程序的可读性,会增加程序维护的难度
考虑将上面写法改为如下形式:
if(任何错误)
{
// 处理错误
// retry
}
else
{
// 业务处理
}
但上面的任何错误这个条件,无法用计算机语言来表达,因此考虑写成如下形式:
try
{
// 业务处理
}
catch(异常类 异常变量名) ____只要有异常,都会被catch块捕捉,从而进入catch块。
{
}
▲ 用try catch来捕捉异常
(1) 业务代码代码执行过程中,如果出现错误,系统会将该错误包装成异常对象,并提交给JRE
这个过程叫抛出异常。
(2) JRE收到异常后,JRE会依次寻找try块之后的多个catch块,如果异常对象是catch后的形参类型
或其子类的实例(instanceof),程序就会进入对应的catch块,这个过程叫捕获异常。
catch块可以有0~N个,N个catch块分别捕捉不同类型的异常。
多个catch块,应该先捕捉小异常(子类异常)。
▲ Java的异常体系:
Throwable
↗ ↖
Error Exception
↗ ↖ ↗ ↖
IOError LinkedError RuntimeException IOException
↗ ↖
NullPointerException ClassCastException
▲ Java 7的多异常捕捉:
传统情况下, 每个catch块后只有一个异常类型,因此每个catch块只能捕捉一个异常。
Java 7的多异常捕捉可以让个catch块后包含多个异常类型
多异常捕捉的优点:代码更加简洁。
缺点:由于一个catch块捕捉了多个异常,因此程序无法分清到底是哪种异常情况。
多异常捕捉的注意点:catch块中异常型形参,相当默认有一个final修饰。
▲ 访问异常信息:
当我们用catch块捕捉到异常之后,程序可能需要了解异常发生详细情况,此时就可通过异常对象进行获取:
- getMessage():返回该异常的详细描述字符串。
- printStackTrace():将该异常的跟踪栈信息输出到标准错误输出。
- printStackTrace(PrintStream s):将该异常的跟踪栈信息输出到指定输出流。
- getStackTrace():返回该异常的跟踪栈信息。
异常的跟踪栈信息,信息非常丰富,可以向我们提供异常依次触发的“轨迹”,
因此当我们调试程序时,一定要找第一个at、我们自己的写的类所导致的异常。
▲ finally
代表一定会执行的块。作用是用于保证关闭、回收物理资源(比如网络连接、数据连接、文件IO资源等)。
放在finally,即可保证关闭资源的代码一定会执行。
如果finally块包含了return语句,那么该return语句会直接结束方法。
- finally不怕return,return无法阻止finally的执行。
- 退出虚拟机(System.eixt(0))可以阻止finally的执行。
完整的语法格式为:
try
{
// 业务代码
}
catch(异常1 ex1) 0~N次
{
}
finally 0~1次
{
}
但try不能单独存在,catch、finally不能同时出现零次。
▲ Java 7的自动关闭资源
传统的资源关闭,程序代码十分臃肿。
如果使用Java7的自动关闭资源的try语句,可以既没有finally,也没有catch。
对于自动关闭资源的try语句, 可以没有catch和finally——try块可以单独地存在。
自动关闭资源的try语句,有两个注意点:
- 只有放在try后面的圆括号里的资源才会被关闭。
自动关闭的资源必须在try后的圆括号内声明、并初始化。
自动关闭的资源只在try块中有效。
- 能被自动关闭的资源必须实现Closeable 或 AutoCloseable接口。
▲ checked异常与runtime异常
runtime异常 - 所有RuntimeException的实例,或RuntimeException的子类的实例,都是runtime异常。
它属于运行异常,程序员想捕捉、就捕捉,不捕捉也可以。
即使一个程序编译时,没有任何问题。但运行时有可能会引发错误。
ArithmeticException, ClassCastException,IllegalArgumentException, IndexOutOfBoundsException,
NullPointerException. NumberFormatExcpetion
checked异常 - 一个异常,不是runtime异常,就是checked异常。
编译器会强制检查checked异常,如果一个行代码可能引发checked异常,
编译器要求程序要么显式用try...catch捕捉该异常,要么使用throws声明抛出异常。
checked异常的争议:
缺点:编译器强制程序员要么用try...catch捕捉该异常,要么用throws声明抛出异常。
你知道如何处理该异常,就用catch捕捉、并处理该异常;如果不知道处理,用throws抛给别人处理。
因此导致编程比较繁琐。
实际上,只有Java有checked异常。
优点:checked异常时体现了一个设计哲学:没有完善的异常处理的代码,不应该有执行的机会。
throws —— 1。不能独立使用,只能放在方法签名上使用,用于声明抛出某个异常类。
2。throws可以同时声明抛出多个Checked异常类名。
备注:只有存在checked异常的前提下,throws关键字才有存在的意义。
▲ throws与方法重写
方法重写:2同(方法名、形参列表)2小(返回值类型、声明抛出的异常)1大(访问权限)
▲ throw抛出异常:
throw 异常实例;
- throw 异常实例;是一个可执行性的语句,只能放在方法、构造器、初始化块
- throw后面只能是一个异常对象。
在有些事情,用户输入的数据、提供的参数,与我们的业务规则不符合,编译器也无法保证我们业务规则,
此时就必须有成员来显式使用throw抛出异常。
如果你自己throw了一个checked异常,同样要么用try...catch捕捉,要么用throws声明抛出。
——但通常都是用throws声明抛出。
▲ 自定义异常:
当程序出现了与业务不符合的情况时,此时程序就应该使用throw来抛出异常。
此时,大部分应该都应该抛出项目自定义异常(因为系统的特定的异常类,本身就代表了一种具体的异常情况)
最好是异常的类名能让人做到“见名思义”,可以通过异常名表现出程序到底出现了什么问题。
如果要定义普通异常,继承Exception。
如果要定义runtime异常,继承RuntimeException。
通常来说,异常类体只要定义2个构造器即可,其中一个带String参数的构造器中,只要一行简单的super(msg)即可。
▲ 异常转译、异常链。
一个异常出现后,
- 不知道在如何处理, throws声明抛出。
- 怎么如何处理,catch到之后,把异常处理完,无需其他方法继续处理
catch到异常之后,可以进行部分处理,不能处理完。
因此异常还需要留给下一个调用者继续处理,此时就需要再次使用throw抛出一个自定义异常
catch块中,再次使用throw来抛出业务相关的自定义异常。
——这就会把原始异常,转译成的新的异常,这个过程被称为异常转译。