专栏首页编舟记Java内部类的异常处理

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

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • Base-x 编码的奥秘

    Base Encoding 是一组二进制转文本的编码模式(Encoding Scheme),常见的有 Base64、Base58、Base32、Base16。可...

    lambeta
  • Clojure component 设计哲学

    Component 是一个微型的 Clojure 框架用于管理那些包含运行时状态的软件组件的生命周期和依赖。

    lambeta
  • 当我们做区块链时,我们在做什么

    关于区块链是什么,网上的解释多如牛毛。这里,我从需求的角度总结一下:当做记录保存(身份存证)时,它是分布式账本;当做交易或支付(跨境支付)时,它是信任机器。虽然...

    lambeta
  • java安全编码指南之:声明和初始化

    根据JLS(Java Language Specification)中的定义,class在初始化过程中,需要同时初始化class中定义的静态初始化程序和在该类中...

    程序那些事
  • 位运算

    用户3004328
  • 为什么要说“再见ERP”?

    昨天《再见ERP,一个时代的结束》一文,引起了业内的广泛关注,有人曾私信我,如果说ERP再见,那么未来企业用什么?我觉得很多人可能没有看完整篇文章,我的结论是:...

    人称T客
  • web应用常见安全攻击手段

    JS注入 解决方法:HTML编码 如:<%=Html.Encode(feedback.Message)%> HTML5引入的新标签包括、、、

    城市中的游牧民族
  • 词法分析程序

    SuperHeroes
  • 四大组件的启动过程

    1.Activity启动过程 http://blog.csdn.net/lxj1137800599/article/details/53644259

    提莫队长
  • java的poi技术写Excel的Sheet

    那么在Excel里面什么叫做Sheet呢?如下图红色框里面的内容就是Excel的Sheet了。

    Hongten

扫码关注云+社区

领取腾讯云代金券