处理错误
异常分类
在Java中,所有的异常对象都是派生于Throwable类的实例,不过后面还可以自定义异常类
Throwable 下层分为两个分支:Error和Exception
- Error是Java运行时系统内部的错误,所以不用处理这种错误
- Exception 才是需要处理的错误,它又分为两个分支:RuntimeException和 非RuntimeException
RuntimeException包括以下问题:
- 数值越界
- 错误的强制类型转换
- 使用null指针
非RuntimeException包括:
- 超越文件末尾读取数据
- 打开一个不存在的文件
- 根据字符串查找一个类时,这个类不存在
RuntimeException 一定是程序本身的问题,这种问题要通过修改代码来处理,也就是说写的代码出bug了
非RuntimeException 才是需要处理的问题,这种异常叫做检查型(checked)。比如说提供一个根据用户输入的名称打开对应的文件,如果用户输入的文件不存在,那么程序就会报错而停止运行,我们要做的就是捕获并且处理这些异常,使得即使用户输入了非法的数据程序也能够继续运行下去,当然还包括其他的异常也是一样。
声明检查型异常
定义一个方法时,假设这个方法是:说根据用户输入的名称打开对应的文件。那么这个方法很有可能出现异常,所以要在定义是声明 这个方法可能出现异常,来告诉调用者在调用的时候需要异常处理
public void openFile(String name) throws FileNotFoundException{}
//声明这个方法可能抛出 FileNotFoundException 异常(即文件找不到异常)
但是如果文件找到了,但是在文件操作的时候有可能出现其他错误,而我们却只考虑了文件找不到的异常,一旦出现其他异常,那么就会立即停止执行这个方法
所以可以声明多个检查型异常,用逗号分隔
public void openFile(String name) throw FileNotFoundException,IOException{}
//声明这个方法可能抛出上面两种异常
现在不用管FileNotFoundException这些类是什么,只要知道这是告诉调用者“这个方法可能抛出异常”就行,那些下面会解释
抛出异常
Java内置了许多异常类,但是如果出现符合语法却不符合我们自己的需求,那么就需要自己抛出一个异常
比如说一个文件已经承诺长度为1024,在读取的时候却发现只有678个字符,这种情况就要手动抛出一个异常,Java API中有 EOFException异常:输入过程遇到了意外的EOF,完美契合我们的需求。
public void openFile(String name,int len) throw EOFException{
FileInputStream in;
try{ //暂时忽略这里,知道这里是打开一个文件就行
in=new FileInputStream(name); //打开一个文件流
} catch(IOException e){
System.out.println(e.getMessage());
}
//正文:
int x,flag=0;
while((x=in.read())!=-1){
flag++;
System.out.print(x);
}
if(flag!=len){ //如果文件长度不够,则抛出异常
throw new EOFException();
}
}
代码中先声明需要抛出什么异常,然后在代码中根据情况抛出异常
创建异常类
如果遇到任何标准异常类都不能描述的异常,那么就需要自定义一个异常类。自定义异常类通常继承 Exception类或者Exception的子类,如 IOException类。
自定义的类通常有两个构造器:默认构造器、带详细参数的构造器;因为超类Throwable中有一个构造器,功能是根据提供的String 参数设置message 字段,就是getMessage() 方法输出的内容,所以在子类中应该调用超类的这个构造器并且定义错误信息。
class MyException extends Exception{
//默认构造器
public MyException(){}
//带参数的构造器
public MyException(String message){
super(message+"My words...");
}
}
现在就能够抛出自定义的异常类型了,定义的时候就要声明 throws MyException,提醒调用者这个方法可能抛出MyException 的异常。
class Employee{
...
//提醒调用者我这个方法可能抛出MyException异常
public void printInfo(String name) throws MyException{
//长度小于100 则抛出异常,并且设置异常信息为:Length isn't enough
if(name.length()<100){
throw new MyException("Length isn't enough!");
}
System.out.println(name);
}
}
捕获异常
捕获异常
如果某个地方抛出了异常但是没有处理,那么程序就会终止,并在控制台打印异常的类型和堆栈轨迹
为了能够继续运行下去,需要使用 try/catch 语句块,最简单的示例:
try{
...
}catch (ExceptionType e){
what are you want to do when you catch a Exception?
}
假设写一段文件数据读取代码:
public openFile(String name){
try{
var in=new FileInputStream(name);
int x;
while((x=in.read())!=-1){
...
}
} catch(IOException e){
e.printStackTrace(); //打印异常信息、堆栈轨迹
}
}
上面的代码中打开了一个文件流,因为读取的时候可能抛出IOException (输入输出流异常),所以用try/catch语句处理。但是这个方法中处理异常的操作仅仅是打印堆栈轨迹,在方法内这些异常没有什么很好的解决方法。这个时候就要选择暂时不处理,将这个异常抛给调用者,让调用者来处理这个异常
一般来说:
- 捕获知道如何处理的异常
- 传播不知道如何处理的异常
要注意的是,传播一个异常时必须在方法首部声明要传播的异常类型,throws ExceptionType ,提醒调用者这个方法可能抛出异常
特例:如果子类覆盖了超类的方法,而这个超类没有声明检查类型异常,那么子类的覆盖方法就不能声明检查类型异常,只能在方法内捕获处理。
class Employee{
public void printStack(String name){}
}
class Employee2 extends Employee{
public void printStack(String name) throws Exception {} //ERROR!
}
如果try 语句中捕获了一个异常:
- 跳过try剩余的代码
- 执行catch 中的代码
- 继续执行方法中的其他语句
//往上数第二个代码块,先用它举个栗子try{
new Employee.printInfo("ABC"); // 1 System.out.println("I am alive!"); // 2
} catch(MyException e){
System.out.println(e.getMessage()); // 3}System.out.println("Yes"); //4
因为 1处会抛出错误,所以跳过try中剩余的语句,执行顺序为:1->3->4
如果try 语句没有捕获异常:
- 执行完try 所有语句
- 跳过 catch 中的语句
- 继续执行方法中的其他语句
try{
new Employee.printInfo("ABC".repeat(40)); // 1
System.out.println("I am alive!"); // 2
} catch(MyException e){
System.out.println(e.getMessage()); // 3
}
System.out.println("Yes"); // 4
跳过catch语句,所以执行顺序为:1->2->4
捕获多个异常
一个语句块中可能出现多种异常,就需要捕获多个异常。
还是比如说根据传递的字符串打开一个文件,并且读取数据。在API文档中,FileInputStream() 方法可能抛出FileNotFoundException异常,read()方法可能抛出 IOException异常,所以要捕获两种异常
public void openFile(String name){
try{
var in=new FileInputStream(name); //FileNotFoundException
int x;
while((x=in.read())!=-1){ //IOException
System.out.println(x);
}
} catch(FileNotFoundException e){
e.printStackTrace();
} catch(IOException e){
System.out.println(e.getMessage);
}
}
如果处理两个异常的动作一样,那么可以合并catch语句:
try{
...
}catch(异常类型1 e){
e.printStackTrace();
}catch(异常类型2 e){
e.printStackTrace();
}
两条catch 执行相同的操作,那么可以合并,语法格式为:
try{
...
}catch(异常类型1 | 异常类型2 e){
e.printStackTrace();
}
在最前面就讲了,所有检查型的异常都派生于Exception 类,所以这些异常都能够被Exception捕获到,像上面的IOException、EOFExcpeiton 等等都能被Exception 捕获到。但是为了更好的处理,就不能够笼统的全都视为Exception 类处理。
再次抛出异常
可以在catch 中抛出异常,比如说我只想记录下异常,然后再抛给调用者继续处理
try{
what are you doing?
} catch(Exception e){
logger.log(level,message,e);
throw e; //记录后又抛出
}
finally 子句
无论有没有捕获到异常,finally子句中的代码都会执行,不管什么情况finally都会执行
try{
...
}catch(Exception e){
...
}finally{
... //这里的语句风雨无阻
}
finally 一般用在关闭资源上,比如说打开了一个文件流,在try中读到一半却抛出了异常,那么就需要finally来关闭流
open a resourse
try{
...
}catch(Exception e){
...
}finally{
close the resourse
}
不过还有一个更好的办法解决关闭资源问题,下面就会讲到
注意:finally中不能够写更改程序执行流程的语句:return、throw、break、continue:
try{
return 10;
}finally{
return 0;
}
即使try中没有抛出异常,执行了return 10,也会在真正的return 10之前执行finally语句,最后返回值是0
try-with-Resourse
对于往上面数第一个代码块的语句,可以改为:
try(open a reourse){
...
}catch(Exception e){
...
}
不管这个try中发生了什么,最后这个资源一定会被关闭