Java异常处理和设计
Java异常处理和设计
  在程序设计中,进⾏异常处理是⾮常关键和重要的⼀部分。⼀个程序的异常处理框架的好坏直接影响到整个项⽬的代码质量以及后期维护成本和难度。试想⼀下,如果⼀个项⽬从头到尾没有考虑过异常处理,当程序出错从哪⾥寻出错的根源?但是如果⼀个项⽬异常处理设计地过多,⼜会严重影响到代码质量以及程序的性能。因此,如何⾼效简洁地设计异常处理是⼀门艺术,本⽂下⾯先讲述Java异常机制最基础的知识,然后给出在进⾏Java异常处理设计时的⼏个建议。
  若有不正之处,请多多谅解和指正,不胜感激。
  请尊重作者劳动成果,转载请标明转载地址:
  以下是本⽂的⽬录⼤纲:
  ⼀.什么是异常
  ⼆.Java中如何处理异常
  三.深刻理解try,catch,finally,throws,throw五个关键字
  四.在类继承的时候,⽅法覆盖时如何进⾏异常抛出声明
  五.异常处理和设计的⼏个建议
⼀.什么是异常
  异常的英⽂单词是exception,字⾯翻译就是“意外、例外”的意思,也就是⾮正常情况。事实上,异常本质上是程序上的错误,包括程序逻辑错误和系统错误。⽐如使⽤空的引⽤、数组下标越界、内存溢出错误等,这些都是意外的情况,背离我们程序本⾝的意图。错误在我们编写程序的过程中会经常发⽣,包括编译期间和运⾏期间的错误,在编译期间出现的错误有编译器帮助我们⼀起修正,然⽽运⾏期间的错误便不是编译器⼒所能及了,并且运⾏期间的错误往往是难以预料的。假若程序在运⾏期间出现了错误,如果置之不理,程序便会终⽌或直接导致系统崩溃,显然这不是我们希望看到的结果。因此,如何对运⾏期间出现的错误进⾏处理和补救呢?Java提供了异常机制来进⾏处理,通过异常机制来处理程序运⾏期间出现的错误。通过异常机制,我们可以更好地提升程序的健壮性。
  在Java中异常被当做对象来处理,根类是java.lang.Throwable类,在Java中定义了很多异常类(如OutOfMemoryError、NullPointerException、IndexOutOfBoundsException等),这些异常类分为两⼤类:Error和Exception。
  Error是⽆法处理的异常,⽐如OutOfMemoryError,⼀般发⽣这种异常,JVM会选择终⽌程序。因此我们编写程序时不需要关⼼这类异常。
  Exception,也就是我们经常见到的⼀些异常情况,⽐如NullPointerException、IndexOutOfBoundsException,这些异常是我们可以处理的异常。
  Exception类的异常包括checked exception和unchecked exception(unchecked exception也称运⾏时异常RuntimeException,当然这⾥的运⾏时异常并不是前⾯我所说的运⾏期间的异常,只是Java中⽤运⾏时异常这个术语来表⽰,Exception类的异常都是在运⾏期间发⽣的)。
  unchecked exception(⾮检查异常),也称运⾏时异常(RuntimeException),⽐如常见的NullPointerException、IndexOutOfBoundsException。对于运⾏时异常,java编译器不要求必须进⾏异常捕获处理或者抛出声明,由程序员⾃⾏决定。
  checked exception(检查异常),也称⾮运⾏时异常(运⾏时异常以外的异常就是⾮运⾏时异常),java编译器强制程序员必须进⾏捕获处理,⽐如常见的IOExeption和SQLException。对于⾮运⾏时异常如果不进⾏捕获或者抛出声明处理,编译都不会通过。
  在Java中,异常类的结构层次图如下图所⽰:
  在Java中,所有异常类的⽗类是Throwable类,Error类是error类型异常的⽗类,Exception类是exception类型异常的⽗
类,RuntimeException类是所有运⾏时异常的⽗类,RuntimeException以外的并且继承Exception的类是⾮运⾏时异常。
  典型的RuntimeException包括NullPointerException、IndexOutOfBoundsException、IllegalArgumentException等。
  典型的⾮RuntimeException包括IOException、SQLException等。
⼆.Java中如何处理异常
  在Java中如果需要处理异常,必须先对异常进⾏捕获,然后再对异常情况进⾏处理。如何对可能发⽣异常的代码进⾏异常捕获和处理呢?使⽤try和catch关键字即可,如下⾯⼀段代码所⽰:
try {
File file = new File("d:/a.txt");
if(!ists())
} catch (IOException e) {
// TODO: handle exception
}
  被try块包围的代码说明这段代码可能会发⽣异常,⼀旦发⽣异常,异常便会被catch捕获到,然后需要在catch块中进⾏异常处理。
  这是⼀种处理异常的⽅式。在Java中还提供了另⼀种异常处理⽅式即抛出异常,顾名思义,也就是说⼀旦发⽣异常,我把这个异常抛出去,让调⽤者去进⾏处理,⾃⼰不进⾏具体的处理,此时需要⽤到throw和throws关键字。 
  下⾯看⼀个⽰例:
public class Main {
public static void main(String[] args) {
try {
createFile();
} catch (Exception e) {
// TODO: handle exception
}
}
public static void createFile() throws IOException{
File file = new File("d:/a.txt");
if(!ists())
}
}
  这段代码和上⾯⼀段代码的区别是,在实际的createFile⽅法中并没有捕获异常,⽽是⽤throws关键字声明抛出异常,即告知这个⽅法的调⽤者此⽅法可能会抛出IOException。那么在main⽅法中调⽤createFile⽅法的时候,采⽤atch块进⾏了异常捕获处理。
  当然还可以采⽤throw关键字⼿动来抛出异常对象。下⾯看⼀个例⼦:
public class Main {
public static void main(String[] args) {
try {
int[] data = new int[]{1,2,3};
System.out.println(getDataByIndex(-1,data));
} catch (Exception e) {
System.out.Message());
}
}
public static int getDataByIndex(int index,int[] data) {
if(index<0||index>=data.length)
throw new ArrayIndexOutOfBoundsException("数组下标越界");
return data[index];
}
}
  然后在catch块中进⾏捕获。
  也就说在Java中进⾏异常处理的话,对于可能会发⽣异常的代码,可以选择三种⽅法来进⾏异常处理:
  1)对代码块⽤atch进⾏异常捕获处理;
  2)在该代码的⽅法体外⽤throws进⾏抛出声明,告知此⽅法的调⽤者这段代码可能会出现这些异常,你需要谨慎处理。此时有两种情况:
    如果声明抛出的异常是⾮运⾏时异常,此⽅法的调⽤者必须显⽰地⽤atch块进⾏捕获或者继续向上层抛出异常。
    如果声明抛出的异常是运⾏时异常,此⽅法的调⽤者可以选择地进⾏异常捕获处理。
  3)在代码块⽤throw⼿动抛出⼀个异常对象,此时也有两种情况,跟2)中的类似:
    如果抛出的异常对象是⾮运⾏时异常,此⽅法的调⽤者必须显⽰地⽤atch块进⾏捕获或者继续向上层抛出异常。
    如果抛出的异常对象是运⾏时异常,此⽅法的调⽤者可以选择地进⾏异常捕获处理。
  (如果最终将异常抛给main⽅法,则相当于交给jvm⾃动处理,此时jvm会简单地打印异常信息)
三.深刻理解try,catch,finally,throws,throw五个关键字 
  下⾯我们来看⼀下异常机制中五个关键字的⽤法以及需要注意的地⽅。
<,catch,finally
  try关键字⽤来包围可能会出现异常的逻辑代码,它单独⽆法使⽤,必须配合catch或者finally使⽤。Java编译器允许的组合使⽤形式只有以下三种形式:
  ;      ;   
  当然catch块可以有多个,注意try块只能有⼀个,finally块是可选的(但是最多只能有⼀个finally块)。
  三个块执⾏的顺序为try—>catch—>finally。
  当然如果没有发⽣异常,则catch块不会执⾏。但是finally块⽆论在什么情况下都是会执⾏的(这点要⾮常注意,因此部分情况下,都会将释放资源的操作放在finally块中进⾏)。
  在有多个catch块的时候,是按照catch块的先后顺序进⾏匹配的,⼀旦异常类型被⼀个catch块匹配,则不会与后⾯的catch块进⾏匹配。
  在使⽤atch..finally块的时候,注意千万不要在finally块中使⽤return,因为finally中的return会覆盖已有的返回值。下⾯看⼀个例⼦:
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
public class Main {
public static void main(String[] args) {
String str = new Main().openFile();
System.out.println(str);
}
public String openFile() {
try {
FileInputStream inputStream = new FileInputStream("d:/a.txt");
int ch = ad();
System.out.println("aaa");
return "step1";
} catch (FileNotFoundException e) {
System.out.println("file not found");
return "step2";
}catch (IOException e) {
return "step3";
}finally{
System.out.println("finally block");
//return "finally";
}
}
}
  这段程序的输出结果为:
  可以看出,在try块中发⽣FileNotFoundException之后,就跳到第⼀个catch块,打印"file not found"信息,并将"step2"赋值给返回值,然后执⾏finally块,最后将返回值返回。
  从这个例⼦说明,⽆论try块或者catch块中是否包含return语句,都会执⾏finally块。
  如果将这个程序稍微修改⼀下,将finally块中的return语句注释去掉,运⾏结果是:
  最后打印出的是"finally",返回值被重新覆盖了。
  因此如果⽅法有返回值,切忌不要再finally中使⽤return,这样会使得程序结构变得混乱。
2.throws和thow关键字
  1)throws出现在⽅法的声明中,表⽰该⽅法可能会抛出的异常,然后交给上层调⽤它的⽅法程序处理,允许throws后⾯跟着多个异常类型;
  2)⼀般会⽤于程序出现某种逻辑时程序员主动抛出某种特定类型的异常。throw只会出现在⽅法体中,当⽅法在执⾏过程中遇到异常情况时,将异常信息封装为异常对象,然后throw出去。throw关键字的⼀个⾮常重要的作⽤就是异常类型的转换(会在后⾯阐述道)。
  throws表⽰出现异常的⼀种可能性,并不⼀定会发⽣这些异常;throw则是抛出了异常,执⾏throw则⼀定抛出了某种异常对象。两者都是消极处理异常的⽅式(这⾥的消极并不是说这种⽅式不好),只是抛出或者可能抛出异常,但是不会由⽅法去处理异常,真正的处理异常由此⽅法的上层调⽤处理。
四.在类继承的时候,⽅法覆盖时如何进⾏异常抛出声明
  本⼩节讨论⼦类重写⽗类⽅法的时候,如何确定异常抛出声明的类型。下⾯是三点原则:
  1)⽗类的⽅法没有声明异常,⼦类在重写该⽅法的时候不能声明异常;
  2)如果⽗类的⽅法声明⼀个异常exception1,则⼦类在重写该⽅法的时候声明的异常不能是exception1的⽗类;
  3)如果⽗类的⽅法声明的异常类型只有⾮运⾏时异常(运⾏时异常),则⼦类在重写该⽅法的时候声明的异常也只能有⾮运⾏时异常(运⾏时异常),不能含有运⾏时异常(⾮运⾏时异常)。
五.异常处理和设计的⼏个建议
  以下是根据前⼈总结的⼀些异常处理的建议:
1.只在必要使⽤异常的地⽅才使⽤异常,不要⽤异常去控制程序的流程
  谨慎地使⽤异常,异常捕获的代价⾮常⾼昂,异常使⽤过多会严重影响程序的性能。如果在程序中能够⽤if语句和Boolean变量来进⾏逻辑判断,那么尽量减少异常的使⽤,从⽽避免不必要的异常捕获和处理。⽐如下⾯这段经典的程序:
public void useExceptionsForFlowControl() {
try {
increaseCount();
}
} catch (MaximumCountReachedException ex) {
}
//Continue execution
}
public void increaseCount() throws MaximumCountReachedException {
if (count >= 5000)
throw new MaximumCountReachedException();
}
  上边的useExceptionsForFlowControl()⽤⼀个⽆限循环来增加count直到抛出异常,这种做法并没有说让代码不易读,⽽是使得程序执⾏效率降低。
2.切忌使⽤空catch块
  在捕获了异常之后什么都不做,相当于忽略了这个异常。千万不要使⽤空的catch块,空的catch块意味着你在程序中隐藏了错误和异常,并且很可能导致程序出现不可控的执⾏结果。如果你⾮常肯定捕获到的异常不会以任何⽅式对程序造成影响,最好⽤Log⽇志将该异常进⾏记录,以便⽇后⽅便更新和维护。
3.检查异常和⾮检查异常的选择
  ⼀旦你决定抛出异常,你就要决定抛出什么异常。这⾥⾯的主要问题就是抛出检查异常还是⾮检查异常。
  检查异常导致了太多的try…catch代码,可能有很多检查异常对开发⼈员来说是⽆法合理地进⾏处理的,⽐如SQLException,⽽开发⼈员却不得不去进⾏try…catch,这样就会导致经常出现这样⼀种情况:逻辑代码只有很少的⼏⾏,⽽进⾏异常捕获和处理的代码却有很多⾏。这样不仅导致逻辑代码阅读起来晦涩难懂,⽽且降低了程序的性能。
  我个⼈建议尽量避免检查异常的使⽤,如果确实该异常情况的出现很普遍,需要提醒调⽤者注意处理的话,就使⽤检查异常;否则使⽤⾮检查异常。
  因此,在⼀般情况下,我觉得尽量将检查异常转变为⾮检查异常交给上层处理。
4.注意catch块的顺序
  不要把上层类的异常放在最前⾯的catch块。⽐如下⾯这段代码:
try {
FileInputStream inputStream = new FileInputStream("d:/a.txt");
int ch = ad();
System.out.println("aaa");
return "step1";
} catch (IOException e) {
        System.out.println("io exception"); 
return "step2";
}catch (FileNotFoundException e) {
System.out.println("file not found");   
nullpointerexception为什么异常return "step3";
}finally{
System.out.println("finally block");
/
/return "finally";
}
  第⼆个catch的FileNotFoundException将永远不会被捕获到,因为FileNotFoundException是IOException的⼦类。
5.不要将提供给⽤户看的信息放在异常信息⾥
  ⽐如下⾯这段代码:
public class Main {
public static void main(String[] args) {
try {
String user = null;

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。