1 异常讲解
1.1 异常机制概述
异常机制是指当程序出现错误后,程序如何处理。具体来说,异常机制提供了程序退出的安全通道。当出现错误后,程序执行的流程发生改变,程序的控制权转移到异常处理器。
1.2 异常处理的流程
当程序中抛出一个异常后,程序从程序中导致异常的代码处跳出,java
虚拟机检测寻找和try
关键字匹配的处理该异常的catch
块,如果找到,将控制权交到catch
块中的代码,然后继续往下执行程序,try
块中发生异常的代码不会被重新执行。如果没有找到处理该异常的catch
块,在所有的finally
块代码被执行和当前线程的所属的ThreadGroup
的uncaughtException
方法被调用后,遇到异常的当前线程被中止。
1.3 异常的结构
异常的继承结构:Throwable为基类,Error和Exception继承Throwable,RuntimeException和IOException等继承Exception。Error和RuntimeException及其子类成为未检查异常(unchecked),其它异常成为已检查异常(checked)
1.3.1 Error异常
Error
表示程序在运行期间出现了十分严重、不可恢复的错误,在这种情况下应用程序只能中止运行,例如JAVA
虚拟机出现错误。Error
是一种unchecked Exception
,编译器不会检查Error
是否被处理,在程序中不用捕获Error
类型的异常。一般情况下,在程序中也不应该抛出Error
类型的异常。
1.3.2 RuntimeException异常
Exception
异常包括RuntimeException
异常和其他非RuntimeException
的异常。
RuntimeException
是一种Unchecked Exception
,即表示编译器不会检查程序是否对RuntimeException
作了处理,在程序中不必捕获RuntimException
类型的异常,也不必在方法体声明抛出RuntimeException
类。RuntimeException
发生的时候,表示程序中出现了编程错误,所以应该找出错误修改程序,而不是去捕获RuntimeException
。
大致有一下几类:
-
java.lang.ArrayIndexOutOfBoundsException
:数组索引越界异常。当对数组的索引值为负数或大于等于数组大小时抛出。 -
java.lang.ArithmeticException
:算术条件异常。譬如:整数除零等。 -
java.lang.NullPointerException
:空指针异常。当应用试图在要求使用对象的地方使用了null
时,抛出该异常。譬如:调用null
对象的实例方法、访问null
对象的属性、计算null
对象的长度、使用throw
语句抛出null
等等 -
java.lang.NegativeArraySizeException
:数组长度为负异常 -
java.lang.ArrayStoreException
:数组中包含不兼容的值抛出的异常 -
java.lang.SecurityException
:安全性异常 -
java.lang.IllegalArgumentException
:非法参数异常
1.3.3 Checked Exception异常
Checked Exception
异常,这也是在编程中使用最多的Exception
,所有继承自Exception
并且不是RuntimeException
的异常都是checked Exception
,上图中的IOException
和ClassNotFoundException
。JAVA
语言规定必须对checked Exception
作处理,编译器会对此作检查,要么在方法体中声明抛出checked Exception
,要么使用catch语句捕获checked Exception
进行处理,不然不能通过编译
比如:
-
java.lang.ClassNotFoundException
:找不到类异常。当应用试图根据字符串形式的类名构造类,而在遍历CLASSPAH之后找不到对应名称的class文件时,抛出该异常。
1.4 异常处理
1.4.1 在声明方法时候抛出异常
语法:throws(略)
为什么要在声明方法抛出异常?
- 方法是否抛出异常与方法返回值的类型一样重要。假设方法抛出异常却没有声明该方法将抛出异常,那么客户程序员可以调用这个方法而且不用编写处理异常的代码。那么,一旦出现异常,那么这个异常就没有合适的异常控制器来解决。
为什么抛出的异常一定是已检查异常?
-
RuntimeException
与Error
可以在任何代码中产生,它们不需要由程序员显示的抛出,一旦出现错误,那么相应的异常会被自动抛出。遇到Error
,程序员一般是无能为力的;遇到RuntimeException
,那么一定是程序存在逻辑错误,要对程序进行修改;只有已检查异常才是程序员所关心的,程序应该且仅应该抛出或处理已检查异常。而已检查异常是由程序员抛出的,这分为两种情况:客户程序员调用会抛出异常的库函数;客户程序员自己使用throw语句抛出异常。
注意
:覆盖父类某方法的子类方法不能抛出比父类方法更多的异常,所以,有时设计父类的方法时会声明抛出异常,但实际的实现方法的代码却并不抛出异常,这样做的目的就是为了方便子类方法覆盖父类方法时可以抛出异常。
1.4.2 在方法中如何抛出异常
语法:throw(略)
抛出什么异常?
对于一个异常对象,真正有用的信息是异常的对象类型,而异常对象本身毫无意义。比如一个异常对象的类型是ClassCastException
,那么这个类名就是唯一有用的信息。所以,在选择抛出什么异常时,最关键的就是选择异常的类名能够明确说明异常情况的类。
异常对象通常有两种构造函数:一种是无参数的构造函数;另一种是带一个字符串的构造函数,这个字符串将作为这个异常对象除了类型名以外的额外说明。
为什么要创建自己的异常?
当Java
内置的异常都不能明确的说明异常情况的时候,需要创建自己的异常。需要注意的是,唯一有用的就是类型名这个信息,所以不要在异常类的设计上花费精力。
1.4.3 throw和throws的区别
throw和throws区别:
-
throw
是语句抛出一个异常,throws
是方法可能抛出异常的声明。(用在声明方法时,表示该方法可能要抛出异常) -
throw
语句用在方法体内,表示抛出异常,由方法体内的语句处理。 -
throws
语句用在方法声明后面,表示再抛出异常,由该方法的调用者来处理。 -
throws
主要是声明这个方法会抛出这种类型的异常,使它的调用者知道要捕获这个异常。 -
throw
是具体向外抛异常的动作,所以它是抛出一个异常实例。
基本区别:
-
throws
是用来声明一个方法可能
抛出的所有异常信息 -
throw
则是指抛出的一个具体的异常类型。 - 通常在一个
方法(类)
的声明处通过throws
声明方法(类)可能抛出的异常信息,而在方法(类)
内部通过throw
声明一个具体的异常信息。 -
throws
通常不用显示的捕获异常,可由系统自动将所有捕获的异常信息抛给上级方法; -
throw
则需要用户自己捕获相关的异常,而后在对其进行相关包装,最后再将包装后的异常信息抛出。
对异常处理方式不同:
-
throws
对异常不处理,谁调用谁处理,throws
的Exception
的取值范围要大于方法内部异常的最大范围,而cathch
的范围又要大于throws
的Exception
的范围; -
throw
主动抛出自定义异常类对象.throws
抛出的是类,throw
抛出的是对象.在方法定义中表示的是陈述语气,第三人称单数,throw
显然要加s
。(throws
一般用作方法定义的子句)
在函数体中要用throw
,实际上是祈使句+强调,等价于DO throw ....,do +动词原形
throw
用于引发异常,可引发预定义异常和自定义异常。
public class TestThrow
{
public static void main(String[] args)
{
try
{
//调用带throws声明的方法,必须显式捕获该异常
//否则,必须在main方法中再次声明抛出
throwChecked(-3);
}
catch (Exception e)
{
System.out.println(e.getMessage());
}
//调用抛出Runtime异常的方法既可以显式捕获该异常,
//也可不理会该异常
throwRuntime(3);
}
public static void throwChecked(int a)throws Exception
{
if (a > 0)
{
//自行抛出Exception异常
//该代码必须处于try块里,或处于带throws声明的方法中
throw new Exception("a的值大于0,不符合要求");
}
}
public static void throwRuntime(int a)
{
if (a > 0)
{
//自行抛出RuntimeException异常,既可以显式捕获该异常
//也可完全不理会该异常,把该异常交给该方法调用者处理
throw new RuntimeException("a的值大于0,不符合要求");
}
}
}
补充:throwChecked函数的另外一种写法如下所示
public static void throwChecked(int a)
{
if (a > 0)
{
//自行抛出Exception异常
//该代码必须处于try块里,或处于带throws声明的方法中
try
{
throw new Exception("a的值大于0,不符合要求");
}
catch (Exception e)
{
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
注意
:此时在main
函数里面throwChecked
就不用try
异常了。
那么应该在声明方法抛出异常还是在方法中捕获异常?
处理原则:捕捉并处理哪些知道如何处理的异常,而传递哪些不知道如何处理的异常
1.4.4 使用finally块释放资源
finally
关键字保证无论程序使用任何方式离开try
块,finally
中的语句都会被执行。在以下三种情况下会进入finally
块:
- try块中的代码正常执行完毕。
- 在try块中抛出异常。
- 在try块中执行return、break、continue。
因此,当你需要一个地方来执行在任何情况下都必须执行的代码时,就可以将这些代码放入finally
块中。当你的程序中使用了外界资源,如数据库连接,文件等,必须将释放这些资源的代码写入finally
块中。
必须注意的是
:在finally
块中不能抛出异常。JAVA
异常处理机制保证无论在任何情况下必须先执行finally
块然后再离开try
块,因此在try
块中发生异常的时候,JAVA
虚拟机先转到finally
块执行finally
块中的代码,finally
块执行完毕后,再向外抛出异常。如果在finally
块中抛出异常,try
块捕捉的异常就不能抛出,外部捕捉到的异常就是finally
块中的异常信息,而try
块中发生的真正的异常堆栈信息则丢失了。
请看下面的代码:
Connection con = null;
try
{
con = dataSource.getConnection();
……
}
catch(SQLException e)
{
……
throw e;//进行一些处理后再将数据库异常抛出给调用者处理
}
finally
{
try
{
con.close();
}
catch(SQLException e)
{
e.printStackTrace();
……
}
}
运行程序后,调用者得到的信息如下
java.lang.NullPointerException
at myPackage.MyClass.method1(methodl.java:266)
而不是我们期望得到的数据库异常。这是因为这里的con
是null的关系,在finally
语句中抛出了NullPointerException
,在finally
块中增加对con
是否为null
的判断可以避免产生这种情况。
丢失的异常
请看下面的代码:
public void method2()
{
try
{
……
method1(); //method1进行了数据库操作
}
catch(SQLException e)
{
……
throw new MyException("发生了数据库异常:"+e.getMessage);
}
}
public void method3()
{
try
{
method2();
}
catch(MyException e)
{
e.printStackTrace();
……
}
}
上面method2的代码中,try
块捕获method1
抛出的数据库异常SQLException
后,抛出了新的自定义异常MyException
。这段代码是否并没有什么问题,但看一下控制台的输出:
MyException:发生了数据库异常:对象名称'MyTable' 无效。
at MyClass.method2(MyClass.java:232)
at MyClass.method3(MyClass.java:255)
原始异常SQLException
的信息丢失了,这里只能看到method2
里面定义的MyException
的堆栈情况;而method1
中发生的数据库异常的堆栈则看不到,如何排错呢,只有在method1
的代码行中一行行去寻找数据库操作语句了。
JDK
的开发者们也意识到了这个情况,Throwable
类增加了两个构造方法,public Throwable(Throwable cause)
和public Throwable(String message,Throwable cause)
,在构造函数中传入的原始异常堆栈信息将会在printStackTrace
方法中打印出来。
1.4.5 try{ return }finally{}中的return
代码讲解
class Test {
public int aaa() {
int x = 1;
try {
return ++x;
} catch (Exception e) {
} finally {
++x;
}
return x;
}
public static void main(String[] args) {
Test t = new Test();
int y = t.aaa();
System.out.println(y);
}
}
运行结果是2而不是3
- 在
try
语句块里使用return
语句,那么finally
语句块还会执行
根据已有的知识知道:
return
是可以当作终止语句来用的,我们经常用它来跳出当前方法,并返回一个值给调用方法。然后该方法就结束了,不会执行return
下面的语句。
finally
:无论try
语句发生了什么,无论抛出异常还是正常执行。finally
语句都会执行。
那么问题来了。。。。在try语句里使用return后,finally是否还会执行?finally一定会执行的说法是否还成立?如果成立,那么先执行return还是先执行finally?
官网解释:
当
try
语句退出时肯定会执行finally
语句。这确保了即使发了一个意想不到的异常也会执行finally
语句块。但是finally
的用处不仅是用来处理异常——它可以让程序员不会因为return
、continue
、或者break
语句而忽略了清理代码。把清理代码放在finally
语句块里是一个很好的做法,即便可能不会有异常发生也要这样做。
注意,当try
或者catch
的代码在运行的时候,JVM
退出了。那么finally
语句块就不会执行。同样,如果线程在运行try
或者catch
的代码时被中断了或者被杀死了(killed
),那么finally
语句可能也不会执行了,即使整个运用还会继续执行。如果
try
语句里有return
,那么代码的行为如下:
- 如果有返回值,就把返回值保存到局部变量中
- 执行jsr指令跳到finally语句里执行
- 执行完finally语句后,返回之前保存在局部变量表里的值
从上面的官方说明,我们知道无论try
里执行了return
语句、break
语句、还是continue
语句,finally
语句块还会继续执行。同时,在stackoverflow
里也找到了一个答案,我们可以调用System.exit()
来终止它:
finally will be called.
The only time finally won’t be called is: if you call System.exit(), another thread interrupts current one, or if the JVM crashes first.
另外,在java
的语言规范有讲到,如果在try
语句里有return
语句,finally
语句还是会执行。它会在把控制权转移到该方法的调用者或者构造器前执行finally
语句。也就是说,使用return
语句把控制权转移给其他的方法前会执行finally
语句
其实,此处还牵扯到了值传递和引用传递关系问题
-
try
块:用于捕获异常。其后可接零个或多个catch
块,如果没有catch
块,则必须跟一个finally
块。 -
catch
块:用于处理try
捕获到的异常。 -
finally
块:无论是否捕获或处理异常,finally
块里的语句都会被执行。当在try
块或catch
块中遇到return
语句时,finally
语句块将在方法返回之前被执行。在以下4种特殊情况下,finally块不会被执行:
1)在finally
语句块中发生了异常。
2)在前面的代码中用了System.exit()
退出程序。
3)程序所在的线程死亡。
4)关闭CPU。