Loading [MathJax]/jax/output/CommonHTML/config.js
前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >通过异常处理错误(5):异常的限制、构造器

通过异常处理错误(5):异常的限制、构造器

作者头像
用户7886150
修改于 2021-01-08 02:18:45
修改于 2021-01-08 02:18:45
5180
举报
文章被收录于专栏:bit哲学院bit哲学院

参考链接: 捕获基类和派生类为异常

一、异常的限制 

    当覆盖方法的时候,只能抛出在基类方法的异常说明里列出的那些异常。这个限制很有用,因为这意味着,当基类使用的代码应用到其派生类对象的时候,一样能够工作(当然,这是面向对象的基本概念),异常也不例外。 

    下面例子演示了这种(在编译时)施加在异常上面的限制: 

class BaseballException extends Exception {

}

class Foul extends BaseballException {

}

class Strike extends BaseballException {

}

abstract class Inning {

    public Inning() throws BaseballException {

    }

    public void event() throws BaseballException {

    }

    public abstract void atBat() throws Strike, Foul;

    public void walk() {

    }

}

class StormException extends Exception {

}

class RainedOut extends StormException {

}

class PopFoul extends Foul {

}

interface Storm {

    public void event() throws RainedOut;

    public void rainHard() throws RainedOut;

}

public class StormyInning extends Inning implements Storm {

    public StormyInning() throws RainedOut, BaseballException {

    }

    public StormyInning(String s) throws Foul, BaseballException {

    }

//    void walk() throws PopFoul {}

//    public void event() throws RainedOut{}

    @Override

    public void rainHard() throws RainedOut {

    }

    public void event() {

    }

    @Override

    public void atBat() throws PopFoul {

    }

    public static void main(String[] args) {

        try {

            StormyInning si = new StormyInning();

            si.atBat();

        } catch (PopFoul e) {

            System.out.println("Pop foul");

        } catch (RainedOut e) {

            System.out.println("Rained Out");

        } catch (BaseballException e) {

            System.out.println("Generic Baseball Exception");

        }

        try {

            Inning i = new StormyInning();

            i.atBat();

        } catch (Strike e) {

            System.out.println("Strike");

        } catch (Foul e) {

            System.out.println("Foul");

        } catch (RainedOut e) {

            System.out.println("Rained Out");

        } catch (BaseballException e) {

            System.out.println("Generic Baseball Exception");

        }

    }

    在Inning类中,可以看到构造器和event()方法都声明将抛出异常,但实际上没有抛出。这种方式使你能强制用户去捕获可能在覆盖后的event()版本中增加的异常,所以它们很合理。这对于抽象方法同样成立,比如atBat()。 

    接口Storm值得注意,因为它包含了一个在Inning中定义的方法event()和一个不在Inning中定义的方法rainHard()。这两个方法都抛出新的异常RainedOut。如果StormyInning类在扩展Inning类的同时又实现了Storm接口,那么Storm里的event()方法就不能改变在Inning中event()方法的异常接口。否则的话,在使用基类的时候就不能判断是否捕获了正确的异常,所以这也很合理。当然,如果接口里定义的方法不是来自于基类,比如rainHard(),那么此方法抛出什么样的异常都没有问题。 

    异常限制对构造器不起作用。你会发现StormyInning的构造器可以抛出任何异常,而不必理会基类构造器所抛出的异常。然而,因为基类构造器必须以这样或那样的方式被调用(这里默认构造器将自动被调用),派生类构造器的异常说明必须包含基类构造器的异常说明。 

    派生类构造器不能捕获基类构造器抛出的异常。 

    StormInning.walk()不能通过编译的原因是因为:它抛出了异常,而Inning.walk()并没有声明此异常。如果编译器允许这么做的话,就可以在调用Inning.walk()的时候不用做异常处理了,而且当把它替换成Inning的派生类的对象时,这个方法就有可能会抛出异常,于是程序就失灵了。通过强制派生类遵守基类方法的异常说明,对象的可替换性得到了保证。 

    覆盖后的event()方法声明,派生类方法可以不抛出任何异常,即使它是基类所定义的异常。同样这是因为,假使基类的方法会抛出异常,这样做也不会破坏已有的程序,所以也没有问题。类似的情况出现在atBat()身上,它抛出的是PopFoul,这个异常是继承自“会被基类的atBat()抛出”的Foul。这样,如果你写的代码是同Inning打交道,并且调用了它的atBat()的话,那么肯定能捕获Foul。而PopFoul是由Foul派生出来的,因此异常处理程序也能捕获PopFoul。 

    最后一个值得注意的地方是main()。这里可以看到,如果处理的刚好是StormyInning对象的话,编译器只会强制要求你捕获这个类所抛出的异常。但是如果将它向上转型成基类,那么编译器就会(正确的)要求你捕获基类的异常。所有这些限制都是为了能产生更为强壮的异常处理代码。 

    尽管在继承过程中,编译器会对异常说明做强制要求,但异常说明本身并不属于方法类型的一部分,方法类型是由方法的名字与参数的类型组成的。因此,不能基于异常说明来重载方法。此外,一个出现在基类方法的异常说明中的异常,不一定会出现在派生类方法的异常说明里。这点同继承的规则明显不同,在继承中,基类的方法必须出现在派生类里,换句话说,在继承和覆盖的过程中,某个特定方法的“异常说明接口”不是变大了而是变小了--这恰好和类接口的在继承时的情形相反。 

二、构造器 

    有一点很重要,即你要时刻询问自己“如果异常发生了,所有的东西能被正确的清理吗?”尽管大多数情况下是安全的,但涉及构造器时,问题就出现了。构造器会把对象设置成安全的初始状态,但还会有别的动作,比如打开一个文件,这样的动作只有在对象使用完毕并且用户调用了特殊的清理方法之后才能得以清理。如果在构造器内抛出异常,这些清理行为也许就不能正常工作了。这意味着在编写构造器时要格外的细心。 

    也许你会认为使用finally就可以解决问题。但问题并非如此简单,因为finally会每次都执行清理代码。如果构造器在执行过程中半途而废,也许该对象的某些部分还没有被成功创建,而这些部分在finally子句中却是要被清理的。 

    在下面的例子中,建立了一个InputFile类,它能打开一个文件并且每次读取其中的一行。这里使用了java标准输入/输出库中的FileReader和BufferedReader类,这些类的基本用法很简单,应该很容易明白: 

import java.io.BufferedReader;

import java.io.FileNotFoundException;

import java.io.FileReader;

import java.io.IOException;

public class InputFile {

    private BufferedReader in;

    public InputFile(String fname) throws Exception {

        try {

            in = new BufferedReader(new FileReader(fname));

        } catch (FileNotFoundException e) {

            System.out.println("Could not open " + fname);

            throw e;

        } catch (Exception e) {

            try {

                in.close();

            } catch (IOException e1) {

                System.out.println("in.close() unsuccessful");

            }

            throw e;

        } finally {

        }

    }

    public String getLine() {

        String s;

        try {

            s = in.readLine();

        } catch (IOException e) {

            throw new RuntimeException("readLine() failed");

        }

        return s;

    }

    public void dispose() {

        try {

            in.close();

            System.out.println("dispose() successful");

        } catch (IOException e) {

            throw new RuntimeException("in.close() failed");

        }

    }

    InputFile的构造器接受字符串作为参数,该字符串表示所要打开的文件名。在try块中,会使用此文件名建立了FileReader对象。FileReader对象本身用处并不大,但可以用它来建立BufferedReader对象。注意,使用InputFile的好处就是能把两步操作合二为一。 

    如果FileReader的构造器失败了,将抛出FileNotFoundException异常。对于这个异常,并不需要关闭文件,因为这个文件还没有被打开。而任何其他捕获异常的catch子句必须关闭文件,因为在它们捕获到异常之时,文件已经打开了(当然,如果还有其他方法能抛出FileNotFoundException,这个方法就显得有些投机取巧了。这时,通常必须把这些方法分别放到各自的try块里)。close()方法也可能会抛出异常,所以尽管它已经在另一个catch子句块里了,还是要再用一层try-catch--对java编译器而言,这只不过是多了一对花括号。在本地做完处理之后,异常被重新抛出,对于构造器而言这么做是很合适的,因为你总不希望去误导调用方,让他认为“这个对象已经创建完毕,可以使用了”。 

    在本例中,由于finally会在每次完成构造器之后都执行一遍,因此它实在不该调用close()关闭文件的地方。我们希望文件在InputFile对象的整个生命周期内都处于打开状态。 

    getLine()方法会返回表示文件下一行内容的字符串。它调用了能抛出异常的readLine(),但是这个异常已经在方法内得到处理,因此getLine()不会抛出任何异常。在设计异常时有一个问题:应该把异常全部放在这一层处理;还是先处理一部分,然后再向上层抛出相同的(或新的)异常;又或者是不做任何处理直接向上层抛出。如果用法恰当的话,直接向上层抛出的确能简化编程。在这里,getLine()方法将异常转换为RuntimeException,表示一个编程错误。 

    用户在不需要InputFile对象时,就必须调用dispose()方法,这将释放BufferedReader和/或FileReader对象所占用的系统资源(比如文件句柄),在使用完InputFile对象之前是不会调用它的。可能你会考虑把上述功能放到finalize()里面,你不知道finalize()会不会被调用(即使能确定它将被调用,也不知道在什么时候调用)。这也是java缺陷:除了内存清理之外,所有的清理都不会自动发生。所以必须告诉客户端程序员,这是他们的责任。 

    对于在构造阶段可能会抛出异常,并且要求清理的类,最安全的使用方式是使用嵌套的try子句: 

public class Cleanup {

    public static void main(String[] args) {

        try {

            InputFile in = new InputFile("Cleanup.java");

            try {

                String s;

                int i = 1;

                while ((s = in.getLine()) != null)

                    ;

            } catch (Exception e) {

                System.out.println("Caught Exception in main");

                e.printStackTrace(System.out);

            } finally {

                in.dispose();

            }

        } catch (Exception e) {

            System.out.println("InputFile construction faild");

        }

    }

    清仔细观察这里的逻辑:对InputFile对象的构造在其自己的try语句块中有效,如果构造失败,将进入外部的catch子句,而dispose()方法不会被调用。但是,如果构造成功,我们肯定想确保对象能够被清理,因此在构造之后立即创建了一个新的try语句块。执行清理的finally与内部的try语句块相关联。在这中方式中,finally子句在构造失败时是不会执行的,而在构造成功时将总是执行。 

    这种通用的清理惯用法在构造器不抛出任何异常时也应该运用,其基本规则是:在创建要清理的对象之后,立即进入一个try-finally语句块: 

class NeedsCleanup {

    private static long counter = 1;

    private final long id = counter++;

    public void dispose() {

        System.out.println("NeedsCleanup " + id + "disposed");

    }

}

class ConstructionException extends Exception {

}

class NeedsCleanup2 extends NeedsCleanup {

    public NeedsCleanup2() throws ConstructionException {

    }

}

public class CleanupIdiom {

    public static void main(String[] args) {

        // Section 1:

        NeedsCleanup nc1 = new NeedsCleanup();

        try {

            // ...

        } finally {

            nc1.dispose();

        }

        // Section 2:

        NeedsCleanup nc2 = new NeedsCleanup();

        NeedsCleanup nc3 = new NeedsCleanup();

        try {

            // ...

        } finally {

            nc3.dispose();

            nc2.dispose();

        }

        // Section 3:

        try {

            NeedsCleanup2 nc4 = new NeedsCleanup2();

            try {

                NeedsCleanup2 nc5 = new NeedsCleanup2();

                try {

                    // ...

                } finally {

                    nc5.dispose();

                }

            } catch (ConstructionException e) {

                System.out.println(e);

            } finally {

                nc4.dispose();

            }

        } catch (ConstructionException e) {

            System.out.println(e);

        }

    }

    在main()中,Section1相当简单:遵循了在可去除对象之后紧跟try-finally的原则。如果对象构造不能失败,就不需要任何catch。在Section2中,为了构造和清理,可以看到具有不能失败的构造器的对象可以群组在一起。 

    Section3展示了如何处理那些具有可以失败的构造器,且需要清理的对象。为了正确处理这种情况,事情变得很棘手,因为对于每一个构造,都必须包含在其自己的try-finally语句块中,并且每一个对象构造必须都跟随一个try-finally语句块以确保清理。 

    本例中的异常处理的棘手程度,对于应该创建不能失败的构造器是一个有力的论据,尽管这么做并非总是可行。 

    注意,如果dispose()可以抛出异常,那么你可能需要额外的try语句块。基本上,你应该仔细考虑所有的可能性,并确保正确处理每一种情况。 

   如果本文对您有很大的帮助,还请点赞关注一下。

本文系转载,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文系转载,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
Java8编程思想之Java异常机制最佳实践
改进的错误恢复机制是提高代码健壮性的最强有力的方式。错误恢复在我们所编写的每一个程序中都是基本的要素,但是在 Java 中它显得格外重要,因为 Java 的主要目标之一就是创建供他人使用的程序构件。
JavaEdge
2021/02/22
1.2K0
Java8编程思想之Java异常机制最佳实践
编程思想 之「异常及错误处理」
在 Java 的异常及错误处理机制中,用Throwable这个类来表示可以作为异常被抛出的类。Throwable对象可以细分为两种类型(指从Throwable继承而得到的类型),分别为:
CG国斌
2018/03/22
1.5K3
编程思想 之「异常及错误处理」
Java编程思想之通过异常处理错误
1.     异常分为被检查的异常和运行时异常,被检查的异常在编译时被强制要求检查。异常被用来错误报告和错误恢复,但很大一部分都是用作错误报告的。
用户3148059
2018/09/03
6630
Java编程思想之通过异常处理错误
Java异常处理
然而,只做对的的事情是远远不够的,但是,我们也无法穷举所有的异常情况,所以,我们需要异常处理机制。
二十三年蝉
2022/05/11
7320
Java异常处理
Java程序员必备:异常的十个关键知识点
异常是指阻止当前方法或作用域继续执行的问题。比如你读取的文件不存在,数组越界,进行除法时,除数为0等都会导致异常。
捡田螺的小男孩
2020/04/14
9410
异常处理 | 优雅,永不过时
异常处理就好比穿底裤,穿了不能轻易的给别人看,更不能不穿。否则浪潮褪去,沙滩上裸奔的人就是你。
不惑
2023/12/07
5824
异常处理 | 优雅,永不过时
Java 最全异常讲解
实际工作中,遇到的情况不可能是非常完美的。比如:你写的某个模块,用户输入不一定符合你的要求、你的程序要打开某个文件,这个文件可能不存在或者文件格式不对,你要读取数据库的数据,数据可能是空的等。我们的程序再跑着,内存或硬盘可能满了。等等。
帅飞
2019/01/22
5700
Java--违例控制(异常处理)
违例发生时Java处理过程: 首先,创建违例对象:在内存“堆” 里,用new来创建。 随后,停止当前执行路径(记住不可沿这条路径继续下去),然后从当前的环境中释放出违例对象的句柄。 此时,违例控制机制会接管一切,并开始查找一个恰当的地方,用于继续程序的执行。这个恰当的地方便是“违例控制器”(Java的catch块),它的职责是从问题中恢复,使程序要么尝试另一条执行路径,要么简单地继续。 违例属于对象,用new在内存堆里创建,并需调用一个构建器。在所有标准违例中,存在着两个构建器:第一个是默认构建器,第二个则
SuperHeroes
2018/05/22
4600
【Java】异常处理:从基础到进阶
在编程中,异常(Exception)是指程序在运行过程中程序的错误或者意外情况,它会导致程序的控制流发生改变。通常,异常发生时程序会停止正常执行,直到找到能够处理该异常的代码或者终止程序的执行。
Yui_
2025/01/27
2040
【Java 基础篇】Java 异常处理详解
在软件开发中,错误和异常是常见的情况。Java 引入了异常处理机制,使得开发人员可以更加优雅地处理错误和异常情况。本文将详细介绍 Java 异常的概念、类型、处理方式和最佳实践,并提供一些示例代码。
繁依Fanyi
2023/10/12
5170
技术转载——Java 异常处理的十个建议
写在前面:2020年面试必备的Java后端进阶面试题总结了一份复习指南在Github上,内容详细,图文并茂,有需要学习的朋友可以Star一下! GitHub地址:https://github.com/abel-max/Java-Study-Note/tree/master
用户5546570
2020/06/18
5800
技术转载——Java 异常处理的十个建议
Java异常处理和设计
在程序设计中,进行异常处理是非常关键和重要的一部分。一个程序的异常处理框架的好坏直接影响到整个项目的代码质量以及后期维护成本和难度。试想一下,如果一个项目从头到尾没有考虑过异常处理,当程序出错从哪里寻找出错的根源?但是如果一个项目异常处理设计地过多,又会严重影响到代码质量以及程序的性能。因此,如何高效简洁地设计异常处理是一门艺术,本文下面先讲述Java异常机制最基础的知识,然后给出在进行Java异常处理设计时的几个建议。
Spark学习技巧
2019/05/09
1K0
Java异常处理和设计
Java异常处理
异常:在Java语言中,将程序执行中发生的不正常情况称为“异常” 开发过程中的语法错误和逻辑错误不是异常)
Java_慈祥
2024/08/06
1450
Java异常处理
Java异常处理流程
在Java应用中,异常的处理机制分为抛出异常和捕获异常。文章目录1.抛出异常2.捕获异常3.异Java
Java架构师必看
2021/07/15
9310
Java异常处理流程
异常处理
Exception又分为运行异常(RuntimeException和其下子类)和其他类属于编译时异常
木瓜煲鸡脚
2019/07/22
9850
异常处理
【Java】异常处理指南
异常种类繁多,为了对不同异常或者错误进行很好的分类管理,Java内部维护了一个异常的体系结构:
IsLand1314
2024/10/15
2240
【Java】异常处理指南
异常和异常处理
Java将程序执行过程中发生的不正常情况成为异常。Java使用统一的异常机制来提供一致的错误报告模型,从而使程序更加健壮。
别团等shy哥发育
2023/02/25
2.1K0
异常和异常处理
Java编程最佳实践之多态
多态提供了另一个维度的接口与实现分离,以解耦做什么和怎么做。多态不仅能改善代码的组织,提高代码的可读性,而且能创建有扩展性的程序——无论在最初创建项目时还是在添加新特性时都可以“生长”的程序。
JavaEdge
2022/01/11
8890
JAVA学习第四十七课 — IO流(一):文件的读写
如:InputStream的派生类FileInputStream,Reader的派生类FileReader
全栈程序员站长
2022/07/10
3050
【大牛经验】探讨Java的异常与错误处理
探讨Java的异常与错误处理 ENTER TITLE Java中的异常处理机制已经比较成熟,我们的Java程序到处充满了异常的可能,如果对这些异常不做预先的处理,那么将来程序崩溃就无从调试,很难找到异
Java帮帮
2018/03/19
8660
【大牛经验】探讨Java的异常与错误处理
相关推荐
Java8编程思想之Java异常机制最佳实践
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档