Java学习——通过异常处理错误

简介

Java的基本理念是“结构不佳的代码不能运行”
发现错误的理想时机是在编译阶段,也就是在我们试图运行程序前。但是编译期并不能找出所有的错误,有些问题需要在运行期得到解决。这就需要错误源能够通过某种方式把适当的信息传递给某个知道如何处理这个问题的接收者。



基本异常

异常情形是指阻止当前方法或作用域继续执行的问题,当我们在运行程序中,遇到了一个意料之外的错误时,我们就需要抛出异常,而不是顺着原来的路执行下去。
当抛出异常后,有几件事会随之发生。首先,与Java中其他对象的创建一样,将使用new在堆上创建异常对象。然后,当前的执行路径被终止,并从当前环境中弹出对异常对象的引用。此时异常处理机制接管程序,并开始寻找一个恰当的地方来继续执行程序,这个恰当的地方就是异常处理程序。它的任务是将程序从错误状态中恢复,以使得程序能够继续执行下去。

举个例子。对于某个引用,传递过来时可能是一个空值,尚未被初始化。所以需要在对这个引用操作前对其进行空值检查,如果为空,可以创建一个代表错误信息的对象,并将它从当前环境中抛出,这样的行为我们称之为抛出异常。

if(t == null){
    
    
	throw new NullPointerException();
}

异常参数

与java普通类一样,我们总是使用new在堆上创建异常对象,因此总是伴随存储空间的分配和构造器的调用。所以标准异常类都有两个构造器:一个是默认构造器;另一个是接受字符串作为参数,一变将相关信息放入异常对象的构造器

当我们创建完异常对象后,就需要通过throw关键字来抛出异常了。我们通过return返回对象,因此我们可以把异常抛出看作一种不同的返回机制。



捕获异常

要明白异常是如何被捕获的,必须首先理解监控区域的概念。它是一段可能产生异常的代码,并且后面跟着处理这些异常的代码

try块

如果在方法内抛出了异常,这个方法将在抛出异常的过程中结束。要是不希望方法就此结束,可以在方法内设置一个特殊的块来捕获异常。因为在这个块里尝试各种可能产生异常的方法调用,所以称为try块。

try{
    
    
	//code
}catch(Type1 t1){
    
    
	//handle exceptions of type1
}catch(Type2 t2){
    
    
	//handle exceptions of type2
}

每一个catch子句(异常处理程序)看起来就像是接收一个且紧接收一个特殊类型的参数的方法。

异常处理程序必须紧跟在try块之后,当异常被抛出后,异常处理机制将负责搜寻参数与异常相匹配的第一个处理程序,然后进入catch子句中执行。这时我们认为异常得到了处理。

终止和恢复

异常处理理论上有两种基本模型。Java支持终止模型,在这种模型中,将假设错误非常关键,以至于程序无法返回到异常发生的地方继续执行。一旦异常抛出,就表明错误已经无法挽回,也不能回来继续执行。
另一种称为恢复模型。意思是异常处理的工作是修复错误,然后重新尝试调用出问题的方法,并认为第二次可以处理成功。如果希望使用这种模型,那需要将try块放入while循环中,这样可以不断循环知道得到满意的结果。



自定义异常

Java中存在许多的异常类型,这些异常会在不同的时机被抛出,但是在开发过程中我们不一定会拘泥于这些已有的异常类型。所以需要自定义异常来表示程序中可能会遇到的问题。

要定义自定义异常类,必须从已有的异常类中继承,最好是选择意思相近的异常类。建立新的异常类型最简单的方法就是让编译器为我们产生默认构造器

class SimpleException extends Exception{
    
    

}

通过我们编写自定义异常类的时候会去继承Exception类,看似我们的自定义类中好像什么也没写,但事实上,对于异常来说,最重要的就是类名。

异常说明

Java鼓励人们把方法可能会抛出的异常告知使用此方法的客户程序员。这是种优雅的做法。它使得调用者确切知道些什么样的代码可以捕获所有潜在的异常。如果我们熟知程序源码,我们完全可以通过throw关键字来获知异常的相关信息,但是程序库通常不会与源码一同发布。为了预防这样的问题,Java提供了相应的语法,使你能够礼貌地告诉调用者会出现的异常类型,然后程序员就可以进行相应的处理。这就是异常说明,它属于方法声明的一部分,紧跟在形参列表之后,并使用throws关键字。

演示代码如下

class SimpleException1 extends Exception{
    
    }
class SimpleException2 extends Exception{
    
    }

void f() throws SimpleException1,SimpleException2{
    
    }

捕获所有异常

有时候我们的try块中会出现多种类型的异常,但我们会认为为所有类型的异常都添加一个catch块是一个极为不优雅的操作,甚至,如果出现一个我们的当时没有考虑到的异常,那么我们的catch将形同虚设,所以我们需要一种方式,可以捕获所有的异常。那就是通过catch基类异常Exception,当异常抛出时,异常处理机制会根据catch的形参去匹配异常,所以我们需要保证最后一个catch块一定可以匹配所有的异常,那就是Exception(异常类型的基类,事实上还有其他的基类,但是Exception基本可以满足我们常规开发的所有需求)

由于Exception是与编程有关的所有异常类的基类,所以它不能具有过多的个性,不关可以调用他从基类Throwable继承的方法

  • String getMessage():获取详细信息
  • String getLocalizedMessage():用本地语言表示的详细信息
  • void printStackTrace():打印异常的调用栈
  • void printStackTrace(PrintStream):打印异常的调用栈到输出流
    • void printStackTrace(java.io.PrintWriter):打印异常的调用栈到输出流

重新抛出异常

我们有时希望把刚捕获的异常重新抛出,如下所示

try{
    
    
}catch(Exception ex){
    
    
	throw ex;
}

但是注意一点,我们前面提到了printStackTrace()可以打印异常的调用栈,也就是说我们可以通过这个栈得知异常发生的地方,但是有时候我们希望更新这个信息,也就是将异常的抛出点更新成重新抛出点,有一个方法是重新定义一个异常类进行抛出。但是这样毕竟麻烦,毕竟异常中可以还有其他的记录信息,所以我们还可以调用Throwable的fillInStackTrace方法,这将返回一个Throwable对象,它是通过把当前调用栈信息填入原来那个异常对象而建立的,就像这样

try{
    
    
}catch(Exception ex){
    
    
	throw (Exception)ex.fillInStackTrace();
}

在上一级捕获更新后的异常后,有关原来异常发生点的信息会丢失,剩下的是与新的抛出点有关的信息。

异常链

常常会想要在捕获一个异常后抛出另一个异常,并希望把原始异常的信息保存下来,这杯称为异常链。在JDK1.4前,程序员需要自己编写代码来保存原始异常的信息。现在所有Throwable的子类都可以在构造器中接收予个cause对象作为参数,这个cause对象就代表原始异常,这样通过把原始异常传递给新的异常,使得即使在当前位置创建并抛出了新的异常,也能通过异常链追踪到异常最初发生的位置。
在异Thowable的子类中,只有三种基本的异常类提供了带cause参数的构造器,它们是Error(用于java虚拟机报告系统错误)、Exception以及RuntimeException

public static void main(String[] args) throws Exception {
    
    
        try{
    
    
            String name = null;
            name.toString();
        }catch(NullPointerException ex){
    
    
            ex.printStackTrace(System.out);
            System.out.println("---------------------------");
            throw new Exception(ex);
        }
    }

输出

java.lang.NullPointerException
	at org.example.InnerClass.InnerClass1.main(InnerClass1.java:10)
---------------------------
Exception in thread "main" java.lang.Exception: java.lang.NullPointerException
	at org.example.InnerClass.InnerClass1.main(InnerClass1.java:14)
Caused by: java.lang.NullPointerException
	at org.example.InnerClass.InnerClass1.main(InnerClass1.java:10)

通过上面代码运行结果的对比,我们就能发现执行链能够显示出异常的来源



Java标准异常

Throwable这个Java类被用来表示任何可以作为异常被抛出的类。
Throwable对象分为两种类型:

  • Error:用来表示编译时和系统错误
  • Exception:可以被抛出的基本类型

前者我们通常不需要关心,我们主要关心后者。
通常来说我们的异常需要望文知意

RuntimeException

我们一直在讲解如何抛出异常,但是在学习java的过程中我们也经常发现我们明明没有抛出异常,但为什么还是显示了异常。其实除了显式的通过throw去抛出异常以外,还有一种运行时异常是会被虚拟机自动抛出的。因此不必再异常说明中将其声明。它们也被称为“不受检查异常”,这种异常属于错误,将被自动补获。如果运行时异常没有被捕获而直达main(),那么在程序退出前将调用异常的printStackTrace()方法。

也就是说只要是运行时异常,不论是否显式抛出,我们都不需要在异常说明中声明。但是如果是其他的异常比如Exception,那么编译器会强制要求在方法的异常说明中声明。

public static void main(String[] args)  {
    
    
        throw new NullPointerException();
    }
public static void main(String[] args) throws Exception {
    
    
        throw new Exception();
    }

使用finally进行清理

对于一些代码,可能希望无论try块中异常是否抛出,它们都能得到执行。这通常适用于内存回收之外的情况(因为回收由垃圾回收器完成)。为了达到这样的目的,可在异常处理程序后面加上finally子句。

完整的异常处理程序就像下面的样子

try{
    
    
}catch(...){
    
    
}catch(...){
    
    
}finally{
    
    
}

无论异常是否发生,finally中的语句总能被执行。由于异常发生后程序通常没有办法回到原点,可以通过finally结合计数器的结构,使得程序在放弃尝试前尝试一定的次数,这样能够提升程序的健壮性。

finally能做什么

对于没有析构函数的java来说,通过finally可以实现一些收尾操作,例如关闭打开的网络连接或者关闭打开的一些文件流

在return中使用finally

因为finally子句总是会执行,所以在一个方法中,可以从多个点返回,并保证重要的清理工作仍会执行

public static void main(String[] args) {
    
    
        System.out.println(f());
    }

    public static String f(){
    
    
        try {
    
    
            return "try~";
        }catch (Exception ex){
    
    
            System.out.println(ex.getMessage());
        }finally {
    
    
            System.out.println("finally~~");
        }
        return "~~";
    }

输出

finally~~
try~

我们很明显可以看出,即时我们在try中直接返回了结果,但是fianlly中的语句依旧被执行了。

但这时候,我们有个疑问,如果我在fianll一种也执行return,那么最后到底会返回谁的值呢。

    public static void main(String[] args) {
    
    
        System.out.println(f());
    }

    public static String f(){
    
    
        try {
    
    
            return "try~";
        }catch (Exception ex){
    
    
            System.out.println(ex.getMessage());
        }finally {
    
    
            return "finally~";
        }
    }

输出

finally~

通过检严我们发现我们在最后返回的是finall中的值。

那么为什么会发生这样的情况?因为无论try中写了多少return,在return的一刹那,会被finally捕获,之后执行finally中的代码,finally中return了就没有try中return什么事了。

有在try块中执行不到finally的情况吗?

肯定是有,在try块中有System.exit(0);这样的语句,System.exit(0);是终止Java虚拟机JVM的,连JVM都停止了,所有都结束了,当然finally语句也不会被执行到。

缺憾:异常丢失

前面提到,不论代码是跳到try还是catch,最后都会执行finally。那么除了前面提到的在finally中进行return以外,还有一个问题,如果发生了异常,但是我在在异常被捕获前我通过finally再次抛出一个异常,那么,最后会是两个异常还是某个异常。

public class ExceptionLearning1 {
    
    
    public static void main(String[] args) {
    
    
        f();
    }

    public static void f(){
    
    
        try {
    
    
            try {
    
    
                throw new Exception("a exception1");
            }finally {
    
    
                throw new Exception("a exception2");
            }
        }catch (Exception ex){
    
    
            System.out.println(ex.getMessage());
        }

    }
}

输出

a exception2

从结果中我们惊奇的发现一开始抛出的异常被finally中抛出的异常覆盖了。同时如果在try中抛出异常但在finally中直接return也会造成异常的丢失。这是java的缺憾,也许会被修复,但是我们使用时也需要注意这个事。



异常的限制

当覆盖方法的时候,只能抛出在基类方法的异常说明中列出的那些异常。

class SimpleException1 extends Exception{
    
    
}

class SimpleException2 extends Exception{
    
    
}

interface Father{
    
    
    void f() throws SimpleException1;
}

class FatherImpl implements Father{
    
    

    @Override
    public void f() throws SimpleException1{
    
    
        //throw new SimpleException2();编译错误
    }
}

注意:异常限制对构造器不起作用,子类构造器依旧可以抛出父类构造器所没有声明的异常。然而,因为基类构造器必须被调用,所以导出类构造器的异常说明必须包含基类构造器的异常说明。(导出类构造器不能够补获基类构造器抛出的异常)

class SimpleException1 extends Exception{
    
    
}

class SimpleException2 extends Exception{
    
    
}

class FatherClass{
    
    
    public FatherClass() throws SimpleException1 {
    
    
        throw new SimpleException1();
    }
}

class FatherImpl extends FatherClass{
    
    


    public FatherImpl() throws SimpleException1, SimpleException2 {
    
    
        throw new SimpleException2();
    }
}

尽管在继承结构中,编译器会对异常说明做强制要求,但异常说明本身并不属于方法类型的一部分,因此不能够基于异常说明来重载方法。此外,一个出现在基类方法的异常说明中的异常,不一定会出现在导出类方法的异常说明中,这等同于窄化了异常说明,这与常规的继承刚好相反

public class ExceptionLearning2 {
    
    
}

class SimpleException1 extends Exception{
    
    
}

class SimpleException2 extends Exception{
    
    
}

class FatherClass{
    
    
    public void f() throws SimpleException1 {
    
    
        throw new SimpleException1();
    }
}

class SonClass extends FatherClass{
    
    


    @Override
    public void f(){
    
    
    }
}

在上面例子中,FatherClass的f方法声明了SimpleException1异常,但是在SonClass重写f方法时,并没有声明父类f方法中声明的异常。如果是继承的规则,那么子类只能方法不能收窄。所以这里可以证明异常说明与一般的参数协变正好相反。

构造器

拥有finally后我们自然地认为所有东西都能够被正确的清理。绝大数的情况确实如此,但涉及构造器时,问题就出现了。

现在假设我们存在一个文件读取类,它会通过构造函数传入一个文件名,然后通过这个文件名打开文件的输入流,最后在结束使用时调用某个方法对其进行清理。这样的逻辑看似毫无问题,但是很明显,fianlly在此处无用武之地啊,因为我构造器只是打开文件流,操作还有留给其他方法,最后才是清理。因此我们假想,构造器在打开文件流时失败了抛出异常怎么处理,有人说,那我们可以在构造器中套一个try-catch就可以了,是的,这样可以解决流的问题,但是对调用者来说这很奇怪,因为在他看来,对象创建成功,应该可以使用了,所以我们在catch中除了处理异常以外还有抛出一个异常是的对象实例化失败。就像下面这样

public class InputFile{
    
    
	private BufferedReader in;
	public InputFile(String name){
    
    
		try{
    
    
			in = new BufferedReader(new FileReader(name));
		}catch(FileNotFoundException ex){
    
    
			throw ex
		}catch(Exception ex){
    
    
			in.close();
			throw ex
		}
	}
	...
}

异常匹配

抛出异常的时候,异常处理系统会按照代码的书写顺序找出最近的处理程序,找到匹配的处理程序之后,他就认为异常将得到处理,然后不再继续查找
查找的时候并不要求抛出的异常同处理程序所申明的类型完全一致,允许向上转型

class SimpleException extends Exception{
    
    }

try{
    
    
	throw new SimpleException();
}catch(SimpleException ex){
    
    
	System.out.println(ex.getMessage());
}catch(Exception ex){
    
    
	System.out.println(ex.getMessage());
}

上面代码会在第一个处理程序中匹配



异常使用指南

应该在下列情况下使用异常:

  1. 在恰当的级别处理问题(在知道如何处理的情况下才捕获异常)
  2. 解决问题并重新调用产生异常的方法
  3. 进行少许修补,然后绕过异常发送的地方继续执行
  4. 用别的数据进行计算,以替代方法预计会返回的值
  5. 把当前运行环境下能做的事情尽量做完,然后把相同的异常重抛到更高层
  6. 把当前运行环境下能做的事情尽量做完,然后把不同的异常抛到更高层
  7. 终止程序
  8. 进行简化
  9. 让类库和程序更加安全。


总结

异常处理的优点之一就是使得我们能够在某处集中精力处理你要解决的问题,而在另一处处理你编写的这段代码中产生的错误

猜你喜欢

转载自blog.csdn.net/qq_33905217/article/details/109569658