在理想情况下,我们认为用户输入的数据永远都是按照正确的格式的,选择打开的文件也一定存在,代码永远不会出现bug。但是在现实世界中充满了不良的数据和有问题的代码。那么就必须要对出现问题进行处理,当人们在遇到错误时总会感觉到不爽,导致后面不再使用这个程序。
那么改如何处理这些bug呢? 我们需要先知道程序中可能会出现那些问题,这样才能寻找对应的方法来解决。
那么现实中会遇到那些问题呢?
通过返回特定的错误码
当程序发生这些问题时就会报错且停止执行,所以我们需要提前进行判断,发生时我们返回(return)一个特定的错误码给调用者,例如-1或者null,调用者根据我们对错误码的描述来判断发生了什么错误。但是有可能我们返回的错误码可能是一个一个正确的结果,局限性很强。
Java通过抛出异常来解决
在产生问题的地方,Java程序会立刻退出,但并不会返回任何值,而是通过抛出(throw)一个封装了错误信息的对象。此外,调用这个方法的代码也不会继续执行下去,而是异常处理机制开始搜索能够处理这种异常状况的异常处理器
Error:出现了这样的错误,除了通知用户并尽力妥善的终止程序之外,用户几乎无能为力。
RuntimeException:程序代码写错出现的异常,如果出现RuntimeException异常,那么就一定是程序员的问题了。
IOException:I/O错误产生的问题,程序本身没有问题。这个程序员无法进行处理的,例如我们会想到处理文件时先判断问题是否存在,存在则处理,但是我们可能会在判断文件是否存在时文件时存在的,执行文件操作时文件已经删除了,这个“是否存在”取决于环境,而不只是取决于代码。
检查型异常就是编译器要检查这类异常,因为此类异常的发生难以避免,例如打开一个文件,开发者也不确定这个文件能否打开成功,所以就需要在开发阶段让开发者去解决掉这类异常,如果不解决,编译器一般不会通过。
非检查型异常就是编译器不会检查这类异常,例如被除数为0时,开发者完全可以通过代码避免此类情况的发生,所以编译器不会要求开发者在开发阶段就处理此类异常。
Java语言规范将派生于Error类或RuntimeException类的所有异常称为非检查型异常,所有其他的异常成为检查型异常。
例如public class FileNotFoundException extends IOException
,FileNotFoundException异常是继承于IOException的,所以是检查型异常
声明检查型异常
在方法首部指出这个方法可能抛出的一个异常:
public FileInputStream(String name) throws FileNotFoundException
这个方法如果正常执行会生成一个FileInputStream对象,如果发生异常则会抛出一个FileNotFoundException异常,抛出FileNotFoundException对象后运行时系统就会开始搜索可以处理这个对象的异常处理器。
使用throws的注意事项:
1、自己编写方法时不必声明所有可能的异常,在Java核心卷中声明了4中情况:
对于前两种情况必须告诉调用这个方法的程序员有可能抛出异常,因为任何一个抛出异常的方法都有可能是一个死亡陷阱,没有处理器捕获这个异常的话则当前线程就会终止。
对于第三种情况我们更应该注意的时如何避免这种情况,而不是去抛出这些异常。
对于第四中情况,任何程序都有可能抛出继承Error的异常,我们没有办法控制。
2、throws的使用
a、可以声明抛出一个异常
public FileInputStream(String name) throws FileNotFoundException
b、也可以声明会抛出多个异常
public Image loadImage(String s) throws FileNotFoundException, EOFException
c、子类重写父类方法时throws的使用
如果子类覆盖了父类中的一个方法,子类方法声明的检查型异常不能比父类声明的更通用(就是子类抛出的异常只能和父类的异常一样或者是父类异常的子类或者更低,不抛出异常也是可以的)
在代码抛出异常的地方使用throw抛出一个异常对象即可。例如此方法:
一旦方法抛出了异常,这个方法就不会返回到调用者了。
1、继承一个Exception的类或者派生于Exception的某个子类。
2、自定义这个类应该包含两个构造器,一个默认构造器,另一个是包含详细描述信息的构造器。(父类Throwable的toString方法会返回一个字符串,其中包含这个详细信息)
class FileFormatException extends IOException {
public FileFormatException (){}
public FileFormatException (String gripe){
super(gripe);
}
}
如果发生了某个异常,但没有在任何地方捕获这个异常,程序就会终止,并在控制台打印这个异常的信息。
最简单的捕获异常语句块:
try {
// 正常代码块
} catch (ExceptionType e) {
// 发生异常执行的代码块
}
在上面的程序中,如果try语句块中的代码没有抛出任何异常,那么程序就会跳过catch语句块,如果抛出了异常则查看catch有没有捕获这个异常,如果捕获了这个异常则会执行catch语句块的代码,如果没有捕获则会跳过catch语句块寻找可以处理这个异常的地方。
捕获的意思就是抛出的异常是否是catch(ExceptionType e)括号中e类的本身或者子类。
注意:
有时候我们并不需要处理所有的异常,我们可以将异常传递给调用者去处理,甚至一直传递下去。
将异常交给胜任的处理器进行处理比压制这个异常更好。但是如果父类方法没有抛出异常,子类重写了这个父类方法后就必须也处理所有异常,不能抛出。
try {
} catch (FileNotFoundException e) {
} catch (UnKnownHostException e) {
} catch (IOException e) {
}
try语句块抛出异常后会依次从上往下查看catch是否能处理这个语句块,处理后则不再往后执行,所以一般子类会写在前面。
在Java 7中还可以在同一个catch中捕获多个异常类型。例如:
try {
} catch (FileNotFoundException e | UnKnownHostException e) {
}catch (IOException e) {
}
只有当捕获的异常类型彼此之间不存在子类关系时才需要这个特性。
注意:捕获多个异常时,异常变量隐含为final变量,不能修改。
在catch语句块中也可以再次抛出一个异常。这么做通常是希望改变异常的类型。例如:
try {
// 执行数据库操作
} catch (SQLException e) {
throw new ServletException("database error: " + e.getMessage());
}
还可以将原始异常设置为新异常的原因,这样在子系统中抛出高层异常,也不会丢失原始异常细节。强烈推荐使用这种包装技术。
try {
// 执行数据库操作
} catch (SQLException original) {
var e = throw new ServletException("database error");
e.initCause(original)
throw e;
}
捕获到这个异常时,使用下面这条语句获取原始异常:
Throwable original = caughtException.getCause();
finally子句就是在catch语句块只有又增加的语句块,不论是否抛出异常都会执行这个语句块中的内容,常用于关闭输入流的操作中,因为就算发生异常了也会正确关闭输入流,防止占用系统资源。
代码示例:
try {
} catch() {
} catch() {
} finally {
}
注意:finally中的return将会遮蔽try中的返回值
try {
return 1;
} finally {
return 0;
}
最终返回前会执行finally语句块中的代码,所以最终返回0是一个错误结果。
对于之前的这种代码可以使用try-with-Resources进行优化
//打开资源
try {
} finally {
// 关闭资源
}
优化:
try (Resource res = ...) {
// 程序代码
}
资源类必须是一个实现了AutoCloseable接口的类,接口中有一个close方法,在try代码块退出时会自动调用close方法。
可以在try()括号中写多个资源,不论最后如何推出都会关闭所有资源。
在try-with-resources语句中也可以有catch子句,甚至还可以有一个finally子句,这些子句在关闭资源后执行。
1、异常处理不能代替简单的测试。捕获异常是一个废时的操作,超过了一些简单的逻辑判断。
2、不要过分地细化异常。没必要把所有异常都写到catch中,会导致代码量几句膨胀。
3、充分利用异常层次结构。不要只抛出最顶层的异常,应该寻找一个合适的子类。
4、不要压制异常
5、在检测错误时,“苛刻”要比放任更好。在一些错误的地方尽量返回异常,如果返回值容易混淆或者产生NullPoinerException异常。
6、不要羞于传递异常