Java内部类的异常处理

问题

最近遇到一个问题,使用Java写某个DSL标记语言X的parser(解析器)Maven插件的时候,对外暴露一个名为Callback的接口和一个待实现的方法getHTML()——基于调用处传入的文件名srcX构造出HTML文件的输出路径(其实此处的Callback就是一个闭包,文件名是一个自由变量)。大致代码如下:

parser.parse(srcX, new Callback() {
    @Override
    public FileWriter getHTML() {
        return new FileWriter(outputPath(suffix(srcX, "html")));
    }
});

private String suffix(String filename, String suffix) {
    return Joiner.on(".").join(filename, suffix);
}

//这里假设输入和输出根路径地址已知
private File outputPath(String file) {
    return new File(
               file.replace(srcDir.getAbsolutePath(), //srcDir: File
                 outputDir.getAbsolutePath())); //outputDir: File
}

目前为止还没有任何问题。但若是运行时,这段程序很可能抛出异常java.io.FileNotFoundException: your-file-name (No such file or directory)。原因在于file的路径当中可能存在多级父级目录,例如:outputDir/p1/p2/srcX.html,那么当FileWriter尝试创建srcX.html就会失败。此时最简单的方法就是提前创建好所有的父级目录,于是outputPath()方法会变成下面这样:

private File outputPath(String file) {
    File outputFile = new File(
            file.replace(srcDir.getAbsolutePath(),
                    outputDir.getAbsolutePath()));
    outputFile.getParentFile().mkdirs(); //创建可能不存在的父级目录

    return outputFile;
}

似乎这段程序可以正常工作了,但是创建文件夹这样的操作是可能失败的。所以我们需要关注是否创建成功,若失败,则写入Log文件当中。修改程序如下:

private File outputPath(String file) {
    File outputFile = new File(
            file.replace(srcDir.getAbsolutePath(),
                    outputDir.getAbsolutePath()));
    final File parentDirs = outputFile.getParentFile();
    if (!parentDirs.exists()) {
        if (!parentDirs.mkdirs()) {//创建可能不存在的父级目录
            getLog().error("Cannot create parent dirs for {}", outputFile);
        }
    }
    return outputFile;
}

注意
这里我们需要先判断父级目录是否存在,即parentDirs.exists()?可是parentDirs.mkdirs()不是直接返回boolean值来表示是否创建成功吗?是这样么?这儿有mkdirs()方法的说明:

public boolean mkdirs()
Creates the directory named by this abstract pathname, including any necessary but nonexistent parent directories. Note that if this operation fails it may have succeeded in creating some of the necessary parent directories.
Returns:
true if and only if the directory was created, along with all necessary parent directories; false otherwise

也就是说只有当这个目录及其所有的父级目录都被创建时,才返回true,反之返回false。照这个推论,如果所有目录事先已经存在了,这个方法应该也会返回true,毕竟都被创建过了嘛。但是只要稍微看一眼源码,你就会发现事实并非如此:

//mkdirs源码
if (exists()) {
    return false;
}

所以这里需要特别强调was created是一种操作,如果没有进行这个操作,那就不能算这个方法成功。

前面已经提到过,我需要写一个maven的插件,所以最好在这种导致程序崩溃的地方抛出一个maven中通用的异常MojoExecutionException。这样,更改代码如下:

private File outputPath(String file) {
    File outputFile = new File(
            file.replace(srcDir.getAbsolutePath(),
                    outputDir.getAbsolutePath()));
    final File parentDirs = outputFile.getParentFile();
    if (!parentDirs.exists()) {
        if (!parentDirs.mkdirs()) {//创建可能不存在的父级目录
            getLog().error("Cannot create parent dirs for {}", outputFile);
            throw new MojoExecutionException("Cannot create parent dirs");
        }
    }
    return outputFile;
}

此时,问题才显出端倪——异常MojoExecutionException是一个受检的异常(checked Exception),它间接继承自java.lang.Exception。可是我们的getHTML()方法并没有在签名中抛出任何异常,编译无法通过。那唯一的办法就是try...catch了,但是我不应该捕获自己刚刚抛出来的异常,否则抛出受检异常的意义何在?

这时,自然而然会想到,将方法签名改成getHTML() throws MojoExecutionException。确实可行,但是并不合适,因为MojoExecutionException只是Maven插件规定的异常,而getHTML()则是一个对外暴露的API,不应该依赖于某个具体的异常。所以我将异常扩大化:getHTML() throws Exception,这样做的好处很明显,坏处也很显眼。

好处

  1. 牢记《Unix编程艺术》中的“宽收严发”原则。即子类实现父类、接口的方法,入参可以扩大,出参可以缩小。举个例子:父类、接口有个方法
public Object something(HashMap map) throws Exception

那么子类实现这个方法可以这样写

public String something(Map map)
                  throws ExecutionException, NoSuchMethodException

这里,入参是HashMap,出参是Object和Exception。入参扩大,所以子类出现了Map;出参缩小,所以子类出现了String和ExecutionException和NoSuchMethodException。同理,此处getHTML() throws Exception由子类实现的形式可以是getHTML() throws MojoExecutionException

坏处

  1. 不管getHTML()是否需要抛出异常,你都得在实现代码中抛出异常;
  2. 由于对外表现的是抛出较宽泛的Exception,所以丧失了对于具体受检 (checked exception)异常进行检查的好处。

这里有个JDK中比较类似的例子,就是关于RunnableCallable接口的设计问题:

public interface Runnable {
    public void run();
}

public interface Callable<V> {
    V call() throws Exception;
}

它们就是两个极端,Runnable必须将受检的异常转换成非受检(unchecked exception)或者发明一种方式来将异常暴露给调用者;Callable就是无论如何都得抛出异常,而且迫使用户去捕获一个较宽泛的异常。

解决方式

这个时候,泛型就派上用场了。

interface Callback<E extends Exception> {
    FileWriter getHTML() throws E;
}

//interface parser
public <E extends Exception> void parse(String srcX, Callback<E> cb) throws E;

通过这种方式,我们可以捕获具体的异常:

try {
    parser.parse(srcX, new Callback<MojoExecutionException>() {
        @Override
        public FileWriter getHTML() throws MojoExecutionException {
            return new FileWriter(outputPath(suffix(srcX, "html")));
        }
    });
} catch (MojoExecutionException e) {
    getLog().error("Failed to execute. {}", e);
}

使用lambda表达式可以简化成下面的模样:

try {
    parser.parse(srcX, (Callback<MojoExecutionException>) () -> new FileWriter(outputPath(suffix(srcX, "html"))));
} catch (MojoExecutionException e) {
    getLog().error("Failed to execute. {}", e);
}

我们解决了迫使用户去捕获一个较宽泛的异常的问题,但是无论如何都得抛出异常这个问题还是没有得到解决。或许我们需要一个像是throws Nothing一样的语法,表示什么也没有抛出来。我们知道RuntimeException是非受检的异常(unchecked exception),所以throws RuntimeException就表明这个异常跟没有抛出异常一样,不需要捕获。如下:

parser.parse(srcX, new Callback<Nothing>() {
    @Override
    public FileWriter getHTML() throws Nothing {
        return new FileWriter(outputPath(suffix(srcX, "html")));
    }
});

public abstract class Nothing extends RuntimeException {}

走到这一步,我们算是较为完全地解决了匿名内部类的异常处理问题。

异常透明化

With the throws type parameter on the Block interface, we can now accurately generify over the set of exceptions thrown by the Block; with the generic forEach method, we can mirror the exception behavior of the block in forEach(). This is called exception transparency because now the exception behavior of forEach can match the exception behavior of its block argument. Exception transparency simplifies the construction of library classes that implement idioms like internal iteration of data structures, because it is common that methods that accept function-valued arguments will invoke those functions, meaning that the library method will throw a superset of the exceptions thrown by its function-valued arguments.

 interface Block<T, throws E> {
     public void invoke(T element) throws E;
 }

 interface NewCollection<T> {
     public<throws E> forEach(Block<T, throws E> block) throws E;
 }

异常透明化,简单来讲,就是调用者的签名中的异常完全由它的函数值(function-valued)的参数决定,所有这些调用者最终的异常都会是该函数值所注异常的超集。

异常透明化就是用来解决我们常用的通过内部类模拟闭包调用时异常处理的手法了。


闭包的定义
一个包含了自由变量的开发表达式,和该自由变量的约束环境组合之后,产生了一种封闭的状态。


参考链接
[1] Exception Transparency
[2] Throwing Checked Exceptions from Anonymous Inner Classes

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,298评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,701评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 107,078评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,687评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,018评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,410评论 1 211
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,729评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,412评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,124评论 1 239
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,379评论 2 242
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,903评论 1 257
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,268评论 2 251
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,894评论 3 233
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,014评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,770评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,435评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,312评论 2 260

推荐阅读更多精彩内容

  • 通俗编程——白话JAVA异常机制 - 代码之道,编程之法 - 博客频道 - CSDN.NEThttp://blog...
    葡萄喃喃呓语阅读 3,086评论 0 25
  • 1. Java基础部分 基础部分的顺序:基本语法,类相关的语法,内部类的语法,继承相关的语法,异常的语法,线程的语...
    子非鱼_t_阅读 31,292评论 18 399
  • 六种异常处理的陋习 你觉得自己是一个Java专家吗?是否肯定自己已经全面掌握了Java的异常处理机制?在下面这段代...
    Executing阅读 1,264评论 0 6
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,087评论 18 139
  • 那一年我十七岁,在匆匆而过的青春里,留下了一道道伤痕。这时的我们,已经失去了年少时的勇气,却也渐渐的明白,那份曾被...
    我们的丶天空阅读 385评论 0 0