十五、异常(5)

news2025/1/12 11:59:05

本章概要

  • 异常限制
  • 构造器

异常限制

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

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

// exceptions/StormyInning.java
// Overridden methods can throw only the exceptions
// specified in their base-class versions, or exceptions
// derived from the base-class exceptions
class BaseballException extends Exception {
}

class Foul extends BaseballException {
}

class Strike extends BaseballException {
}

abstract class Inning {
    Inning() throws BaseballException {
    }

    public void event() throws BaseballException {
    // Doesn't actually have to throw anything
    }

    public abstract void atBat() throws Strike, Foul;

    public void walk() {
    } // Throws no checked exceptions
}

class StormException extends Exception {
}

class RainedOut extends StormException {
}

class PopFoul extends Foul {
}

interface Storm {
    void event() throws RainedOut;

    void rainHard() throws RainedOut;
}

public class StormyInning extends Inning implements Storm {
    // OK to add new exceptions for constructors, but you
    // must deal with the base constructor exceptions:
    public StormyInning()
            throws RainedOut, BaseballException {
    }

    public StormyInning(String s)
            throws BaseballException {
    }

    // Regular methods must conform to base class:
    //- void walk() throws PopFoul {} //Compile error
    // Interface CANNOT add exceptions to existing
    // methods from the base class:
    //- public void event() throws RainedOut {}
    // If the method doesn't already exist in the
    // base class, the exception is OK:
    @Override
    public void rainHard() throws RainedOut {
    }

    // You can choose to not throw any exceptions,
    // even if the base version does:
    @Override
    public void event() {
    }

    // Overridden methods can throw inherited exceptions:
    @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");
        }
        // Strike not thrown in derived version.
        try {
            // What happens if you upcast?
            Inning i = new StormyInning();
            i.atBat();
            // You must catch the exceptions from the
            // base-class version of the method:
        } 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 的构造器可以抛出任何异常,而不必理会基类构造器所抛出的异常。然而,因为基类构造器必须以这样或那样的方式被调用(这里默认构造器将自动被调用),派生类构造器的异常说明必须包含基类构造器的异常说明。

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

StormyInning.walk() 不能通过编译是因为它抛出了一个 Inning.walk() 中没有声明的异常。如果编译器允许这么做的话,就可以编写调用Inning.walk()却不处理任何异常的代码。 但是当使用 Inning派生类的对象时,就会抛出异常,从而导致程序出现问题。通过强制派生类遵守基类方法的异常说明,对象的可替换性得到了保证。

覆盖后的 event() 方法表明,派生类版的方法可以不抛出任何异常,即使基类版的方法抛出了异常。因为这样做不会破坏那些假定基类版的方法会抛出异常的代码。类似的情况出现在 atBat()上,它抛出的异常PopFoul是由基类版atBat()抛出的Foul 异常派生而来。如果你写的代码同 Inning 一起工作,并且调用了 atBat()的话,那么肯定能捕获 Foul 。又因为 PopFoul 是由 Foul派生而来,因此异常处理程序也能捕获 PopFoul

最后一个有趣的地方在 main()。如果处理的刚好是 Stormylnning 对象的话,编译器只要求捕获这个类所抛出的异常。但是如果将它向上转型成基类型,那么编译器就会准确地要求捕获基类的异常。所有这些限制都是为了能产生更为健壮的异常处理代码。

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

构造器

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

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

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

import java.io.*;

public class InputFile {
    private BufferedReader in;

    public InputFile(String fname) throws Exception {
        try {
            in = new BufferedReader(new FileReader(fname));
            // Other code that might throw exceptions
        } catch (FileNotFoundException e) {
            System.out.println("Could not open " + fname);
            // Wasn't open, so don't close it
            throw e;
        } catch (Exception e) {
            // All other exceptions must close it
            try {
                in.close();
            } catch (IOException e2) {
                System.out.println("in.close() unsuccessful");
            }
            throw e; // Rethrow
        } finally {
            // Don't close it here!!!
        }
    }

    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 e2) {
            throw new RuntimeException("in.close() failed");
        }
    }
}

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

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

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

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

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

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

// exceptions/Cleanup.java
// Guaranteeing proper cleanup of a resource
public class Cleanup {
    public static void main(String[] args) {
        try {
            InputFile in = new InputFile("D:\\onJava\\myTest\\base\\Cleanup.java");
            try {
                String s;
                int i = 1;
                while ((s = in.getLine()) != null) {
                    // Perform line-by-line processing here...
                }
            } 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 failed");
        }
    }
}

输出为:

在这里插入图片描述

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

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

// exceptions/CleanupIdiom.java
// Disposable objects must be followed by a try-finally
class NeedsCleanup { // Construction can't fail
    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 {
    // Construction can fail:
    NeedsCleanup2() throws ConstructionException {
    }
}

public class CleanupIdiom {
    public static void main(String[] args) {
        // [1]:
        NeedsCleanup nc1 = new NeedsCleanup();
        try {
            // ...
        } finally {
            nc1.dispose();
        }
        // [2]:
        // If construction cannot fail,
        // you can group objects:
        NeedsCleanup nc2 = new NeedsCleanup();
        NeedsCleanup nc3 = new NeedsCleanup();
        try {
            // ...
        } finally {
            nc3.dispose(); // Reverse order of construction
            nc2.dispose();
        }
        // [3]:
        // If construction can fail you must guard each one:
        try {
            NeedsCleanup2 nc4 = new NeedsCleanup2();
            try {
                NeedsCleanup2 nc5 = new NeedsCleanup2();
                try {
                    // ...
                } finally {
                    nc5.dispose();
                }
            } catch (ConstructionException e) { // nc5 const.
                System.out.println(e);
            } finally {
                nc4.dispose();
            }
        } catch (ConstructionException e) { // nc4 const.
            System.out.println(e);
        }
    }
}

输出为:

在这里插入图片描述

  • [1] 相当简单,遵循了在可去除对象之后紧跟 try-finally 的原则。如果对象构造不会失败,就不需要任何 catch。
  • [2] 为了构造和清理,可以看到将具有不能失败的构造器的对象分组在一起。
  • [3] 展示了如何处理那些具有可以失败的构造器,且需要清理的对象。为了正确处理这种情况,事情变得很棘手,因为对于每一个构造,都必须包含在其自己的 try-finally 语句块中,并且每一个对象构造必须都跟随一个 try-finally 语句块以确保清理。

本例中异常处理的混乱情形,有力的论证了应该创建不会抛出异常的构造器,尽管这并不总会实现。

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1071283.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

基于SSM的校园资讯推荐系统设计与实现

末尾获取源码 开发语言:Java Java开发工具:JDK1.8 后端框架:SSM 前端:采用JSP技术开发 数据库:MySQL5.7和Navicat管理工具结合 服务器:Tomcat8.5 开发软件:IDEA / Eclipse 是否Maven项目&#x…

关联规则挖掘:Apriori算法的深度探讨

目录 一、简介什么是关联规则挖掘?什么是频繁项集?什么是支持度与置信度?Apriori算法的重要性应用场景 二、理论基础项和项集支持度(Support)置信度(Confidence)提升度(Lift&#xf…

SSM - Springboot - MyBatis-Plus 全栈体系(十八)

第四章 SpringMVC SpringMVC 实战:构建高效表述层框架 一、SpringMVC 简介和体验 1. 介绍 Spring Web MVC 是基于 Servlet API 构建的原始 Web 框架,从一开始就包含在 Spring Framework 中。正式名称“Spring Web MVC”来自其源模块的名称&#xff08…

OCR让点读笔如虎添翼

点读笔是一种智能学习工具,它可以通过识别文字来提供相应的语音或图像反馈。在实现文字识别功能时,点读笔通常会借助OCR(Optical Character Recognition,光学字符识别)技术。下面将详细介绍点读笔如何利用OCR技术实现文…

浅析人脸活体检测技术的两种方法

随着人脸识别技术日趋成熟,商业化应用愈加广泛,然而人脸极易用照片、视频等方式进行复制,因此对合法用户人脸的假冒是人脸识别与认证系统安全的重要威胁。目前基于动态视频人脸检测、人脸眨眼、热红外与可见光人脸关联等领先业界的人脸活体检测算法,已经取得了一定的…

Linux0.12内核源码解读(2)-Bootsect.S

作者:小牛呼噜噜 | https://xiaoniuhululu.com 计算机内功、源码解析、科技故事、项目实战、面试八股等更多硬核文章,首发于公众号「小牛呼噜噜」 文章目录 回顾计算机启动过程8086、80x86是什么意思?寄存器初始化CS:IPCPU是如何和ROM相连的?加载MBR到…

餐饮蛋糕鲜花便利店水果店外卖自提小程序开发

对于实体门店小微商家来说,做私域和复购永远是最划算的买卖。店里每天有新客到老客到,做好私域就可以零成本形成稳定客流而且还可以通过活动形成社交裂变和口碑效应。不做好私域和留存就是白白看着自己客户被同行一口一口吃掉。因此也就有了自提外卖小程…

制作婚礼邀请函只需三步,轻松制作走心请柬

制作自己的婚礼邀请函是一种流行的方式来传达你的婚礼信息给你的亲朋好友。在这个数字化的时代,你可以使用在线制作平台来创建自定义的婚礼邀请函。下面是一个简单的步骤指南,教你如何使用乔拓云网在线制作平台制作出超有感觉的婚礼请柬。 首先&#xff…

【Overload游戏引擎分析】从视图投影矩阵提取视锥体及overload对视锥体的封装

overoad代码中包含一段有意思的代码,可以从视图投影矩阵逆推出摄像机的视锥体,本文来分析一下原理 一、平面的方程 视锥体是用平面来表示的,所以先看看平面的数学表达。 平面方程可以由其法线N(A, B, C)和一个点Q(x0,…

【发表案例】计算机类SCIE,2区,2个月2天录用

计算机类SCIE 【期刊简介】IF:4.0-5.0,JCR2区,中科院3区 【检索情况】SCIE 在检,正刊 【征稿领域】提高安全性和隐私性的边缘/云的智能方法的研究,如数字孪生等 录用案例:2个月2天录用 2023.09.27 | A…

吃鸡玩家必备!提升战斗力,分享干货,保护账号安全!

你好!吃鸡玩家们的福利来了!在这里,我将为大家分享一些关于提高游戏战斗力、分享顶级游戏作战干货以及保护账号安全的实用技巧。 首先,让我们来谈提高游戏战斗力的技巧。绝地求生是一款战略性的游戏,而好的作图工具可以…

Java笔记八(instanceof,类型转换,static详解,抽象类,接口,内部类以及异常)

instanceof 引用类型,判断一个对象是什么类型 使用方法: System.out.println(X instanceof Y); 代码理解: public class Application {public static void main(String[] args) {//Obiect>String//…

如何成为合格的测试开发工程师?

是入职两年半的测试开发工程师小编,虽然目前很菜,但还是希望自己继续努力,早日成为一名合格的测试开发工程师,本篇文章也是通过对身边同事的了解,整理了几点对自己的要求,以及重新梳理了下今后的学习路径&a…

CI522 13.56MHZ电动车NFC测试资料

Ci522是一颗工作在13.56MHz频率下的非接触式读写芯片,支持读A卡(CI523支持读A/B卡),可做智能门锁、电动车NFC一键启动、玩具NFC开锁等应用。为部分要求低成本,PCB小体积的产品提供了可靠的选择。 Ci522与Si522/MFRC52…

母婴店做微信小程序开发的重要性

随着移动互联网的发展,母婴店传统的线下销售模式已经无法满足现代年轻父母的需求。微信小程序作为一种新型的应用形态,为母婴店提供了新的销售渠道和推广方式。本文将探讨母婴店做微信小程序开发的作用。 一、拓展销售渠道 微信小程序是一种轻量级的应用…

混沌工程初分享

混沌工程初分享 一、什么是混沌工程 1、什么是混沌 混沌是一种现象,在一个动力系统中,因为各种不同的参数变化导致的一系列的连锁反应。比如: 在南美洲亚马逊河流域热带雨林中的蝴蝶,偶尔的几次振翅,可以在两周以后引…

下载遥感数据慢的原因

由于地图资源下载工具2.0工具能够“批量下载”、“错误重试”和“断点续传“,我能够长时间下载并监测网络下载的情况及看到错误,因此我对部分下载进行了分析和总结: 这类网站例如:LANDSAT下载网站!下载速度并不是一直慢而是分时段…

A Survey and Framework of Cooperative Perception 论文阅读

论文链接 A Survey and Framework of Cooperative Perception: From Heterogeneous Singleton to Hierarchical Cooperation 0. Abstract 首次提出统一的 CP(Cooperative Percepetion) 框架回顾了基于不同类型传感器的 CP 系统与分类对节点结构&#x…

CCF CSP认证 历年题目自练Day25

题目 试题编号: 201403-3 试题名称: 命令行选项 时间限制: 1.0s 内存限制: 256.0MB 问题描述: 问题描述   请你写一个命令行分析程序,用以分析给定的命令行里包含哪些选项。每个命令行由若干个字符串组成,它们之间…

解决远程git服务器路径改变导致本地无法push的问题

解决远程git服务器路径改变导致本地无法push的问题 (1)第一步:查看git配置 git config -l(2)第二步:删除远程git地址 git remote remove origin(3)第三步:再次查看git配…