在了解了面向对象的相关技术后,我们就开始正式合规的写Java代码了,但是实际上,在写代码的过程种会经常有一些错误抛出来,我们称这些运行时抛出的错误为异常,我们要做的就是捕获并处理。
异常定义
异常是程序中的一些错误,但并不是所有的错误都是异常,并且错误有时候是可以避免的。这些异常有的是因为用户错误引起,有的是程序错误引起的,还有其它一些是因为物理错误引起的。要理解Java异常处理是如何工作的,你需要掌握以下三种类型的异常:
- 检查性异常:最具代表的检查性异常是用户错误或问题引起的异常,这是程序员无法预见的。例如要打开一个不存在文件时,一个异常就发生了,这些异常在编译时不能被简单地忽略。非运行时异常
- 运行时异常: 运行时异常是可能被程序员避免的异常。例如用户输入了非法数据RunTimeException,运行时异常
- 错误: 错误不是异常,而是脱离程序员控制的问题。错误在代码中通常被忽略。例如,当栈溢出时,一个错误就发生了,它们在编译也检查不到的。错误
异常指不期而至的各种状况,如:文件找不到、网络连接失败、除0操作、非法参数等。异常是一个事件,它发生在程序运行期间,干扰了正常的指令流程。
综合日常的实战,我可以这么定义异常处理:异常处理通常和日志紧密配合,在可能出现问题的地方捕获系统抛出的异常然后打出对应的日志,方便系统稳定运行和程序员排查问题,搞明白了基础定位和作用之后,再来详细看看异常体系怎么运作。
异常分类
所有的异常类是从 java.lang.Exception 类继承的子类。Exception 类是 Throwable 类的子类。除了Exception类外Throwable还有一个子类Error 。
Throwable分成了两个不同的分支,一个分支是Error,它表示不希望被程序捕获或者是程序无法处理的错误。另一个分支是Exception,它表示用户程序可能捕捉的异常情况或者说是程序可以处理的异常。其中异常类Exception又分为运行时异常(RuntimeException)和非运行时异常
- Error,错误类对象由 Java 虚拟机生成并抛出,大多数错误与代码编写者所执行的操作无关。例如,Java虚拟机运行错误(Virtual MachineError),当JVM不再有继续执行操作所需的内存资源时,将出现 OutOfMemoryError。这些异常发生时,Java虚拟机(JVM)一般会选择线程终止,即使确实发生了错误,本质上也不应该试图去处理它所引起的异常状况。处理不了
- Exception:Exception异常分为两类,一类时运行时异常,一类是非运行时异常。
- RuntimeException(运行时异常),该类型的异常自动为你所编写的程序定义ArrayIndexOutOfBoundsException(数组下标越界)、NullPointerException(空指针异常)、ArithmeticException(算术异常)、MissingResourceException(丢失资源)、ClassNotFoundException(找不到类)等异常,这些异常是不检查异常,程序中可以选择捕获处理,也可以不处理。这些异常一般是由程序逻辑错误引起的,程序应该从逻辑角度尽可能避免这类异常的发生,不处理,运行会报错
- 非运行时异常,类型上属于Exception类及其子类,从程序语法角度讲是必须进行处理的异常,如果不处理,程序就不能编译通过。如IOException、SQLException等以及用户自定义的Exception异常,一般情况下不自定义检查异常。不处理,编译不能通过
还有依据是否是受检查异常而做的一个分类:什么是检查异常呢?在正确的程序运行过程中,很容易出现的、情理可容的异常状况,在一定程度上这种异常的发生是可以预测的,并且一旦发生该种异常,就必须采取某种方式进行处理。
- 检查异常:除了RuntimeException及其子类以外,其他的Exception类及其子类都属于检查异常,当程序中可能出现这类异常,要么使用try-catch语句进行捕获,要么用throws子句抛出,否则编译无法通过。
- 不受检查异常:包括RuntimeException及其子类和Error。
总结而言,不受检查异常为编译器不要求强制处理的异常,检查异常则是编译器要求必须处置的异常
异常处理
上文我们定位到异常有哪些,而且确定不处理错误,不推荐处理运行时异常(因为运行时异常一般为逻辑错误,程序应该从逻辑角度尽可能避免这类异常的发生),必须处理非运行时异常,如何处理异常呢?Java的异常处理本质上是抛出异常和捕获异常,有五个关键字来搭配使用进行异常处理:
- try,用于监听异常。将要被监听的代码(可能抛出异常的代码)放在try语句块之内,当try语句块内发生异常时,异常就被抛出。
- catch,用于捕获异常。catch用来捕获try语句块中发生的异常。每个try后边可能有一个或多个catch,当异常发生时,程序会中止当前流程,依据获取异常类型去执行相应catch代码块。
- finally,finally语句块总是会被执行。它主要用于回收在try块里打开的物理资源(如数据库连接、网络连接和磁盘文件)。只有finally块执行完成之后,才会回来执行try或者catch块中的return或者throw语句,如果finally中使用了return或者throw等终止方法的语句,则就不会跳回执行,直接停止。
- throw, 用于抛出异常。
- throws ,用在方法签名中,用于声明该方法可能抛出的异常。
接下来详细的了解下异常处理的语法
try-catch-finally
try-catch-finally为一个完整的异常捕获到处理的流程,一个完整的异常处理和捕获的范例代码如下:
try{
// 程序代码
}catch(异常类型1 异常的变量名1){
// 程序代码
}catch(异常类型2 异常的变量名2){
// 程序代码
}finally{
// 程序代码
}
以下为有无异常的执行流程:
在发现语句1出错,后边的语句不会执行,也就是说语句2不会被执行了,而是执行finally之后的 try-catch之外的其它语句
语法示例
try-catch-finally分别就检查异常和运行时异常举个例子:
public class Fileexception {
public static void main(String[] args) {
FileInputStream in = null; //一定要定义在try外边,记住作用域是在大括号之间
try {
in = new FileInputStream("myfile.txt");
int b;
b = in.read();
while (b != -1) {
System.out.print((char) b);
b = in.read();
}
//一定要先写FileNotFoundException,因为FileNotFoundException是IOException的子类,因为写catch是先小后大,
//否则,捕获底层异常类的catch子句将可能会被屏蔽
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
System.out.println(e.getMessage());
} finally {
try {
in.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
以下为一个运行时算数异常,不推荐使用,要在逻辑执行时避免。
public class TestException {
public static void main(String[] args) {
int a = 1;
int b = 0;
try { // try监控区域
if (b == 0) throw new ArithmeticException(); // 通过throw语句抛出异常
System.out.println("a/b的值是:" + a / b);
System.out.println("this will not be printed!");
}
catch (ArithmeticException e) { // catch捕捉异常
System.out.println("程序出现异常,变量b不能为0!");
}
System.out.println("程序正常结束。");
}
}
嵌套try
try语句可以被嵌套。也就是说,一个try语句可以在另一个try块的内部。
class NestTry{
public static void main(String[] args){
try{
int a = args.length;
int b = 42 / a;
System.out.println("a = "+ a);
try{
if(a == 1){
a = a/(a-a);
}
if(a == 2){
int c[] = {1};
c[42] =99;
}
}catch(ArrayIndexOutOfBoundsException e){
System.out.println("ArrayIndexOutOfBounds :"+e); //数组越界异常
}
}catch(ArithmeticException e){
System.out.println("Divide by 0"+ e); //除0异常
}
}
}
输出结果为
a=0:Divide by 0java.lang.ArithmeticException: / by zero
a=1:Divide by 0java.lang.ArithmeticException: / by zero
a=2:ArrayIndexOutOfBounds :java.lang.ArrayIndexOutOfBoundsException
每次进入try语句,异常的前后关系都会被推入堆栈。如果一个内部的try语句不含特殊异常的catch处理程序,堆栈将弹出,下一个try语句的catch处理程序将检查是否与之匹配。
- a=0时,外面的try块将产生一个被0除的异常。
- a=1时,嵌套的try块产生一个被0除的异常,由于内部的catch块不匹配这个异常,它将把异常传给外部的try块,在外部异常被处理。
- a=2时,内部try块产生一个数组边界异常
整个过程将继续直到一个catch语句被匹配成功,或者是直到所有的嵌套try语句被检查完毕。如果没有catch语句匹配,Java运行时系统将处理这个异常。
try-catch-finally组合情况
我们都说这个组合来处理异常,那么这几个关键字都是必须的么?关键字组合有如下几种:
- try+catch后有没有finally无所谓,try+catch+finally可以使用;try+catch可以使用
- try必不可少,try+finally可以,try+catch可以
- 三个关键字不能单独使用任何一个,即使是try如果不加catch也必须有finally(声明了异常就一定要处理,不管是在catch还是finally中)
总结就是必须有try+(catch,finally,catch+finally)
三个组合中的一种
return的执行时机
我们知道不管有没有出现异常,finally块中代码都会执行,但是如果说在以上三种关键字所管辖的块内包含return,代码该按照何种顺序执行呢?
finally没有return
finally里没有return的场景,但try或catch里有,finally仍然会执行,因此在return返回时不是直接返回变量的值,而是复制一份,然后返回,因此,对于基本类型的,finally的改变没有影响,对于引用类型的就有影响了
package test;
/
* @author 田茂林
* @data 2017年9月6日 下午9:46:42
*/
public class TestFinally{
//基本类型
public static int testFinally1(){
int result =1;
try {
result = 2;
return result;
} catch (Exception e) {
return 0;
}finally{
result =3;
System.out.println("execult Finally1");
}
}
//引用类型
public static StringBuffer testFinally2(){
StringBuffer s = new StringBuffer("Hello");
try {
return s;
} catch (Exception e) {
return null;
}finally{
s.append("world");
System.out.println("execult Finally2");
}
}
public static void main(String[] args) {
int result1 = testFinally1();
System.out.println(result1);
StringBuffer result2 = testFinally2();
System.out.println(result2);
}
}
输出结果
execult Finally1
2
execult Finally2
Helloworld
对于finally块中没有return语句的情况,方法在返回之前会先将返回值保存在局部变量表中的某个slot中,然后执行finally块中的语句,之后再将保存在局部变量表中某个slot中的数据放入操作数栈的栈顶并进行返回,因此对于基本数据类型而言,若在finally块中改变其值,并不会影响最后return的值
finally里有return
finally中最好不要包含return,否则程序会提前退出,返回值不是try或catch中保存的返回值。返回值是finally里的return返回的值
package test;
/**
* @author 田茂林
* @data 2017年9月6日 下午9:46:42
*/
public class TestFinally {
@SuppressWarnings("finally")
public static int testFinally() { // 基本类型
int i = 1;
try {
++i;
return i;
} catch (Exception e) {
} finally {
return i++; // 2
// return ++i; //3
}
}
public static void main(String[] args) {
int result1 = testFinally();
System.out.println(result1);
}
}
如果是return i++
返回i的值,return ++i
返回的是++i的值,也就是永远返回当前值,对于finally块中包含了return语句的情况,则在try块中的return执行之前,会先goto到finally块中,而在goto之前并不会对try块中要返回的值进行保护,而是直接去执行finally块中的语句,并最终执行finally块中的return语句,而忽略try块中的return语句,因此最终返回的值是在finally块中改变之后的值。
throws和throw
如果一个方法没有捕获到一个检查性异常,那么该方法必须使用 throws 关键字来声明。throws 关键字放在方法签名的尾部。也可以使用 throw 关键字抛出一个异常,一个throws子句列举了一个方法可能引发的所有异常类型。
import java.io.*;
public class className
{
public void deposit(double amount) throws RemoteException
{
// Method implementation
throw new RemoteException();
}
//Remainder of class definition
}
throws异常抛出示例
下面是用throws关键字抛出异常的举例,异常从f2()抛出并层层上抛,直到main方法处理异常
public class ManangeException {
//直到抛到main方法里,不要再main方法里写throws,main方法会打印堆栈信息,但最好在main方法里处理异常
//main方法调用了f2()方法,并监控和捕获可能抛出的异常
public static void main(String[] args) {
ManangeException m=new ManangeException();
try {
m.f2();
} catch (IOException e) {
System.out.println("没有找到该文件");
}
}
//f()方法调用了f2()方法,并抛出可能的异常
public void f2() throws IOException{ //一级一级往外抛
f();
}
//f2()方法抛出可能的异常
public void f() throws FileNotFoundException,IOException{ //不处理只抛出
FileInputStream in = null;
in = new FileInputStream("myfile.txt");
int b;
b = in.read();
while (b != -1) {
System.out.print((char) b);
b = in.read();
}
}
如果在main方法中也不处理并继续向上抛的话,可以看到如下的异常堆栈信息:
Exception in thread "main" java.io.FileNotFoundException: myfile.txt (系统找不到指定的文件。)
at java.io.FileInputStream.open0(Native Method)
at java.io.FileInputStream.open(FileInputStream.java:195)
at java.io.FileInputStream.<init>(FileInputStream.java:138)
at java.io.FileInputStream.<init>(FileInputStream.java:93)
at packageB.Person.f(Person.java:24)
at packageB.Person.f2(Person.java:19)
at packageB.Person.main(Person.java:14)
throw异常抛出示例
除了对可能的异常抛出在方法上声明,还可以通过指定异常抛出直接手动抛出异常
class TestThrow{
static void proc(){
try{
throw new NullPointerException("demo");
}catch(NullPointerException e){
System.out.println("Caught inside proc");
throw e;
}
}
public static void main(String [] args){
try{
proc();
}catch(NullPointerException e){
System.out.println("Recaught outside ");
}
}
}
返回结果如下:
Caught inside proc
Recaught outside
自定义异常
使用Java内置的异常类可以描述在编程时出现的大部分异常情况。除此之外,用户还可以自定义异常。用户自定义异常类,只需继承Exception类即可。在程序中使用自定义异常类,大体可分为以下几个步骤:
- 创建自定义异常类,必须继承Exception类
- 在方法中通过throw关键字抛出异常对象。如果在当前抛出异常的方法中处理异常,可以使用try-catch语句捕获并处理;
- 如果当前方法不处理,那么在方法的声明处通过throws关键字指明要抛出给方法调用者的异常,继续进行下一步操作。
自定义异常举例如下:
class MyException extends Exception {
private int id;
public MyException(String message, int id) {
super(message);
this.id = id;
}
public int getId() {
return id;
}
}
public class TestMyException {
public static void main(String[] args) {
TestMyException t=new TestMyException();
t.m(1);
}
public void m(int i){
try {
regist(i); //方法体里边不能再写方法,但是可以直接调用就好
} catch (MyException e) {
System.out.println("出现故障");
}
}
public void regist(int num) throws MyException{
if(num<0)
throw new MyException("不合法的人数", 1); //这要是异常抛出,则不会打印下边的那句话,因为没有捕获处理
System.out.println(num);
}
}