一、 概念
“异常”看起来是程序中的错误的意思,有时候会导致歧义,如果程序中出现错误,就是你写的程序有问题,为什么不去修复它,把它变成一个没有异常的程序,这是值得去推敲的。首先程序不是万能的,它有一定的逻辑和规定,而且程序为了灵活性,往往是动态接收数据。比如一个算数算法,它往往只能接收整型数据,但是非要传入一些其它类型的数据,比如字符串等,为了健壮性考虑,程序往往会返回一些信息,告知调用者是不接收这类型数据的。而早期往往是返回一些标志位,因为函数的返回类型只能有一个,所以很可能通过”-1”等标识表示“类型错误”等含义。
但是这里面可能存在的一些问题就是:一方面对于返回标识的判断和业务逻辑判断放置在一起很容易混淆,不便于程序阅读性,而且返回标识位的方式,也不利于调用者的记忆和使用;另外,如果调用者在自身的程序不对这些“错误”做处理,往往出现问题,调用者会认为他的程序本身没有问题,是提供的接口存在错误,所以可以出现强制规定,避免这种随意性,最后,如果程序中出现多处调用都可能存在错误,那么你需要分别在这些地方加上判断,这样代码看起来非常的臃肿,如果能捕获一段代码中所有错误,并且可以转到特定区域去处理,那么业务和错误就进行了分离,这样即简单代码结果又使结构清晰了,而这些目的就是“异常”的特性。
所以,“异常”可以看作是一些语言提出来对于编程中“错误”处理的一种体系结构,使它更加清晰和便利被开发者去使用。
二、 Java异常基本常识
首先了解异常的一个特点:就是异常发生时,表示程序出现问题,当前程序执行步骤会终止,一般需要返回到上一级环境,就比如以前通过【return】返回。
Java中关于异常的知识点:
1. 异常也是对象,需要在内存堆中创建实例。
2. Java所有异常的基类是【Exception】,JDK提供很多预定义的异常类,它们都是Exception的子类。
3. 异常发送时,当前程序执行终止,Java通过throw关键字抛出异常对象,比如:
if (obj = null){
throw new NullPointerException();
//obj.method();
}
代码的含义:对象引用变量obj如果还未初始化,那么它默认值为null,就不能调用方法,这时候需要抛出这个异常,异常抛出后到上一个环境,这是由jvm去处理,至于具体哪里后面具体介绍规则。
4. 异常是对象,它有两种形式的构造函数,一个是无参数的默认构造函数;还有一种就是接收单个字符串的。异常的信息基本都是通过异常的名称去传递的,比如NullPointerException类名的含义就是所谓的“空指针异常”。
三、 捕获和处理异常
异常以throws的方式抛出时,当前执行步骤将不会继续向下进行,而是返回到上一环境,如果你想程序继续运行,java提供一种捕获异常的方式,语法形式:
首先它是一种尝试捕获,因为你也不确定是否一定会出现异常,另外它是用{}圈起来的,表示它是监督一段区域,而不是一行代码,所以用try这个词。try{
// Code that might generate exception
}
捕获的异常必须在某处得到处理。这个“地点”就是异常处理程序,可以针对每一个捕获异常,准备相应的处理。异常处理程序是语法结构:
try{
// Code that might generate exceptions
}catch(Type1 id1){
// Handle exceptions of Type1
}catch(Type2 id2){
//Handle exceptions of Type2
} catch(Type3 id3){
//Handle exceptions of Type3
}
在try的监控区域中发现异常,那么就依次操作catch语句,如果异常类型和catch的中Type1相同,或者是Type1的类型包含出现的异常(是Type1子类),则去执行内部的语句。
注意:如果catch语句中没有终止程序的语句,将跳出try-catch语句,进行向下执行。
public class ExceptionMethods { public static void main(String[] agrs) { try{ throw new RuntimeException("异常提示信息"); }catch(RuntimeException e) { e.printStackTrace(); }catch(Exception e) { System.out.println("不会执行的异常捕获"); } System.out.println("异常被吞咽"); } }
虽然在异常发生时,我们可以抛出或者捕获异常,但是往往捕获异常后,我们并不能做出有意义工作。因为如果你自身编写的程序抛出异常后,你再试试去捕获它,做一些修复是不太可能的;如果是别人程序的抛出异常,如果你要捕获修复它,那么就要详细了解对方的代码,增加了耦合(也就是你高度依赖别人的程序),假设别人的代码做出修改,你可能也要跟进相关变动。所以Java支持终止模式,异常抛出,就表示错误无法挽回,不需要继续执行。当然这不代表捕获异常就是没有必要事情,因为你捕获异常后,也可以进行抛出异常,在抛出之前你可以打印一些信息等。当然try-catch使用比较多的,主要在一些需要关闭资源的程序,比如IO编程,后面举例说明。
四、 创建定义异常
除了上面的预定义异常,也就是Jdk已经提供的,可以根据自己需要创建自定义异常,创建它,必须要从已有的异常类继承。
因为异常对象的构造函数有两个,所以分两种形式:
<span style="font-size:12px;">class MyException extends Exception {
publicMyException(){}
publicMyException(String msg){super(msg);}
}</span>
五、 异常说明
Java鼓励你们把方法可能抛出的异常告知使用此方法的客户端程序员,这样可以让调用者确切的指定写什么代码捕获所有潜在的异常。当然,如果你提供了源代码,客户端程序远可以在源代码中查找throw语句来获知相关信息,但是并不是往往别人是不会提供源代码给你的。为了预防这种问题,Java提供了相应的语法,是你可以以其它方式告知客户端程序员某个方法可能会抛出的异常类型,这就是异常说明,它属于方法声明的一部分,紧跟在形式参数列表之后。
异常说明使用了关键字throws,注意需要跟上面throw区别,它多一个s,区别的方式就是异常声明throws可以跟多个异常类型,而throw是抛出单个异常对象。
没有异常说明:void f()
增加异常说明:void f() throws TooBig, TooSmall,DivZero{}
throws和throws的区别:
voidf()throws TooBig, TooSmall,DivZero{
throw new TooBig();
throw new TooSmall ();
throw new DivZero ();
}
异常说明的强制性:
第一点:如果方法内部产生异常,那么你必须对异常进行处理,比如捕获异常处理,如果不捕获异常或者捕获异常处理时继续抛出异常,那么你必须要有异常声明,否则编译报错。
通过的说方法中出现throws异常时,要么你用try-catch语句处理掉这个异常,要么通过异常声明throws出去告知调用者。
第二点:如果外部调用这个方法,这个方法包含异常声明,那么你的调用程序必须可以出现的异常做出处理,处理方式参照一条;如果不处理,则编译报错。
六、 捕获所有的异常
可以只写一个异常处理程序来捕获所有类型的异常,通过捕获异常类型的基类Exception就可以做到这有点:
catch(Exception e) {
System.out.println(“Caught an exception”);
}
七、 异常方法
异常的主要信息是透过异常的名称来体现的,所以异常类的方法比较少,但是还是有几个要了解的,Exception方法继承之Throwable类:
一、printStackTrace()把异常信息打印到标准错误流,可以在控制台查看。
public classExceptionMethods {
public static void main(String[] agrs){
try{
throw newException("异常提示信息");
}catch(Exception e) {
e.printStackTrace();
}
}
}
我们经常遇到异常信息,但是对于具体信息的组成和由来可能没有更多了解。第一行可以通过toString()方法返回,他其实有两部分组成:类名+初始化异常对象的字符串信息,内部实现中关于类名可以通过getClass()或者.class属性获取,而“初始化异常对象的字符串信息”可以通过getMessage()或者getLocalizedMessage()返回,这两个方法默认的返回值相同。复写toString()或者getMessage()可以改变控制台第一行的打印信息。
public classExceptionMethods {
public static void main(String[] agrs){
try{
throw newException("异常提示信息");
}catch(Exception e) {
System.out.println("printStackTrace():");
e.printStackTrace();
System.out.println("toString():"+e);
System.out.println("getMessage():"+e.getMessage());
System.out.println("getLocalizedMessage():"+e.getLocalizedMessage());
}
}
}
class ExceptionByMySelfextends Exception{
private String str;
ExceptionByMySelf(){
}
ExceptionByMySelf(String msg){
super(msg);
this.str = msg;
}
public String toString(){
if("".equals(getMessage()))
return "mystyle: " +getClass().getName();
return "my style: "+getClass().getName()+ ": " +getMessage();
}
public String getMessage(){
if (str==null)
return "";
return str;
}
}
classExceptionDemo{
public static void main(String[] args){
try{
throw newExceptionByMySelf();
}catch(ExceptionByMySelfe){
e.printStackTrace();
}
try{
throw newExceptionByMySelf("自定义的异常");
}catch(ExceptionByMySelf e){
e.printStackTrace();
}
}
}
接着看看从第二行开始就是异常在每一个调用方法中轨迹,主要是方法名和行数;最上面是最后调用函数,最下面是开始的调用的方法。它其实是可以通过getStackTrace()返回,它是一个数值,类型为StackTraceElement,代码示例:
public classWhoCalled {
static void f(){
try{
throw newException();
}catch(Exception e){
for(StackTraceElementste : e.getStackTrace()){
System.out.println(ste.getMethodName());
}
}
}
static void g(){f();}
static void h(){g();}
public static void main(String[] args){
f();
System.out.println("----------------");
g();
System.out.println("----------------");
h();
}
}
以h()方法举例,首先虚拟机找到main方法,main方法入栈 ,然后调用h(),h方法压栈,接着调用g(),g方法压栈,接着f方法压栈。
最上面是元素的0角标。
八、 特例:RuntimeException
关于RuntimeException的我还不能很好的理解,但是关于它的由来大致可以这样理解:
比如t.f(),为了保证调用成功我们必须做健壮性判断
if( t == null) {
Throw new NullPointerException
}
再比如Person p = (Person)obj,同样为了健壮性必须判断
If( obj instanceof Person ){
Person p = (Person)Obj;
}
但是这里面存在一个问题就是这样的操作(方法调用和类型转换)在我们开发过程会经常出现,为了程序的健壮性考虑,我们可能会把代码变的非常臃肿。所以java为了解决这类问题,就是提倡可以不做这些重复判断,系统本身会去做这些检查,如果发现异常系统本身会把这些异常抛出来,而它们都是RuntimeException的子类;
RuntimeException有一个特性:就是它属于程序运行时,系统自身去检查的异常,然后抛出,那么是不需要编程者对它进行处理的,所以你可以不需要去捕获或者进行异常声明,当然它本身是允许你去捕获,但是不建议。
public class NeverCaught{
staticvoid f(){
thrownew RuntimeException("From f()");
}
staticvoid g(){
f();
}
publicstatic void main(String[] args){
g();
}
}
结果:即使你不对抛出的异常进行捕获或者声明,编译依然通过。
总结:个人觉得RuntimeException在概念的边界上是很难区分的,而且往往会陷入是创建一个Exception和RuntimeException子类对象的纠结中,我的理解就是如果你为了明确告知别人方法可能抛出的具体类型,用Exception子类对象,这样好处通过异常声明明确了异常的类型,但是这样也是有弊端的,就是别人必须要异常进行处理,一般用作接口程序。而RuntimeException的好处就在,假如这个程序完全由我编写,出现异常的完全了解,直接抛出RuntimeException,最终抛回给main函数,通过虚拟机在控制台中打印出信息就可以,就避免了后续方法调用时强制对异常进行捕获和声明代码。其实两者完全都是可以代替的,但是RuntimeException更加简单和偷懒一点。
九、 使用finally进行清理
对于一些代码,可能希望无论try块中的异常是否抛出,他们都能得到执行,通常使用与内存回收之外的情形,为了达到这个效果,需要在异常处理程序后面加上finally。
结构:
try{
}catch(A a){
}catch(B b){
}catch(C c){
}finally{
}
示例1:
class ThreeException extends Exception {}
public class FinallyWorks {
staticint count = 0;
publicstatic void main(String[] args) {
while(true){
try{
if(count++==0){
thrownew ThreeException();
}
System.out.println("Noexception");
}catch(ThreeExceptione){
System.out.println("ThreeException");
}finally{
System.out.println("Infinally cluse");
if(count== 2) break;
}
}
}
}
示例2:
没有Finally子句:
class Switch {
privateboolean state = false;
publicboolean read(){return state;}
publicvoid on(){state=true;System.out.println(this);}
publicvoid off(){state=false;System.out.println(this);}
publicString toString(){ return state? "on" : "false";}
}
class OnOffException1 extends Exception{}
class OnOffException2 extends Exception{}
public class OnOffSwitch{
privatestatic Switch sw = new Switch();
publicstatic void f() throws OnOffException1,OnOffException2 {}
publicstatic void main(String[] args) {
try{
sw.on();
f();
sw.off();
}catch(OnOffException1e){
System.out.println("OnOffException1");
sw.off();
}catch(OnOffException2e){
System.out.println("OnOffException2");
sw.off();
}
}
}
有Finally子句:
public class WithFinally {
staticSwitch sw = new Switch();
publicstatic void main(String[] args){
try{
sw.on();
OnOffSwitch.f();
}catch(OnOffException1e){
System.out.println("OnOffException1");
}catch(OnOffException2e){
System.out.println("OnOffException2");
}finally{
sw.off();
}
}
}
当涉及到break、continue、return语句,finally都会得到执行。
实例3:
public class MultipleReturns{
publicstatic void f(int i){
System.out.println("Initializationthat requires cleanup");
try{
System.out.println("Point1");
if(i== 1) return;
System.out.println("Point2");
if(i== 2) return;
System.out.println("Point3");
if(i== 3) return;
System.out.println("End");
return;
}finally{
System.out.println("Performingcleanup");
}
}
publicstatic void main(String[] args){
for(inti = 1; i <= 4; i++){
f(i);
}
}
}
示例4:异常被吞咽和丢失
吞咽:
public class ExceptionMethods{
public static void main(String[] agrs){
try{
throw new Exception("异常提示信息");
}catch(Exception e) {
e.printStackTrace();
}
System.out.println("异常被吞咽");
}
}
因为捕获异常后,我们只是打印了异常信息,这样导致的问题就是:明明我们try中语句出现问题了,但是程序即没有终止,也没修复,而是进行向下执行。但是向下执行已经没有意义。
丢失:
class VeryImportantException extendsException {
publicString toStrin(){
return"A very important exception";
}
}
class HoHumException extends Exception{
publicString toString(){
return"A trivial exception";
}
}
public class LostMessage {
voidf() throws VeryImportantException {
thrownew VeryImportantException();
}
voiddispose() throws HoHumException{
thrownew HoHumException();
}
publicstatic void main(String[] args){
try{
LostMessagelm = new LostMessage();
try{
lm.f();
}finally{
lm.dispose();
}
}catch(Exceptione){
System.out.println(e);
}
}
}
lm.f()异常没有被处理,这些都是不正确的异常处理方式。
十、 异常的限制
当我们继承类,会出现方法覆盖,超类(子类)只能抛出基类(父类)列出的异常,这种限制的目的是为了解决超类在向上转型为基类时,本来为基类设计的代码,超类也能使用。
比如我们常用多态的示例:
void f(Person p){
try{
p.getAge();
}catch(TooSmall e) {//它只抛出一个年龄太小的错误
}
}
class Teacher extends Perosn {
publicint getAge() throw TooSmall,TooBig {//方法复写抛出比基类多的异常
}
}
f(new Teacher());//把子类对象传给f方法,子类向上转型,这是典型的多态,但是出现的问题,子类方法的异常声明有两个,而原来的代码只捕获了一个,这会造成错误。
1. 子类方法的异常声明不能多于父类,可以少或者无,另外子类的异常类型可以与父类相同,或者是其子类,因为子类异常也可以向上转型与父类相同,但不能向下转型。
2. 子类的构造函数必须包含父类构造函数的异常,当然子类构造函数可以有自己的异常,因为子类构造函数必须调用父类的构造函数。
3. 子类对象向上转型为父类的引用,通过父类引用调用发放,抛出的异常以父类的为准,而不是子类。
示例:
class BaseballException extends Exception{}
class Foul extends BaseballException{}
class Strike extends BaseballException{}
abstract class Inning {
publicInning() throws BaseballException{}
publicvoid event() throws BaseballException{}
publicabstract void atBat() throws Strike,Foul;
publicvoid walk(){}
}
class StormException extends Exception {}
class RainedOut extends StormException {}
class PopFoul extends Foul {}
interface Storm {
publicvoid event() throws RainedOut;
publicvoid rainHard () throws RainedOut;
}
public class StormyInning extends Inningimplements Storm {
publicStormyInning() throws RainedOut,BaseballException{}
publicStormyInning(String s) throws Foul,BaseballException{}
//publicvoid walk() throws PopFoul{}
//publicvoid event() throws RainedOut{}
publicvoid rainHard() throws RainedOut{}
publicvoid event(){}
publicvoid atBat() throws PopFoul{}
publicstatic void main(String[] agrs) {
try{
StormyInningsi = new StormyInning();
si.atBat();
}catch(PopFoule){
System.out.println("Popfoul");
}catch(RainedOute){
System.out.println("RainedOut");
}catch(BaseballExceptione){
System.out.println("Genericbaseball exception");
}
try{
Inningi = new StormyInning();
i.atBat();
}catch(Strikee){
System.out.println("Strike");
}catch(Foule){
System.out.println("Foul");
}catch(RainedOute){
System.out.println("Rainedout");
}catch(BaseballExceptione){
System.out.println("Genericbaseball exception");
}
}
}
十一、 构造器
在使用finally进行资源关闭时候,需要注意的就是,被清理的对方不一定在构造时就是成功的,比如我们熟悉的IO流操作。这时候我们往往需要使用try-catch嵌套,这里就不展看说明,因为需要好好的总结一下,由于时间问题,暂时放置两段代码:
import java.io.*;
public class InputFile {
privateBufferedReader in;
publicInputFile(String fname) throws Exception {
try{
in= new BufferedReader(new FileReader(fname));
}catch(FileNotFoundExceptione){
System.out.println("Couldnot open " + fname);
throwe
}catch(Exceptione){
try{
in.close();
}catch(IOExceptione2){
System.out.println("in.close() unsuccessful");
}
throwe;
}finally{
}
}
publicString getLine(){
Strings;
try{
s= in.readLine();
}catch(IOException){
thrownew RuntimeException("readLine() failed");
}
returns;
}
publicvoid dispose(){
try{
in.close();
System.ou.println("dispose()successful");
}catch(IOExceptione2){
thrownew RuntimeException("in.close() failed");
}
}
}
import java.io.*;
public class InputFile {
privateBufferedReader in;
publicInputFile(String fname) throws Exception {
try{
in= new BufferedReader(new FileReader(fname));
}catch(FileNotFoundExceptione){
System.out.println("Couldnot open " + fname);
throwe
}catch(Exceptione){
try{
in.close();
}catch(IOExceptione2){
System.out.println("in.close() unsuccessful");
}
throwe;
}finally{
}
}
publicString getLine(){
Strings;
try{
s= in.readLine();
}catch(IOException){
thrownew RuntimeException("readLine() failed");
}
returns;
}
publicvoid dispose(){
try{
in.close();
System.ou.println("dispose()successful");
}catch(IOExceptione2){
thrownew RuntimeException("in.close() failed");
}
}
}
十二、 异常处理的可选方式
主要有两种方式:
1、 把异常传递给控制台,就是通过层层抛出的方式。
2、 把“被检查的异常”转换为“不检查的异常”,就是把非RuntimeException变成RuntimeException。方式:new RuntimeException(e),e非RuntimeException异常对象。
而且可以通过getCause()返回它原来的异常类型。