问题 如何在Java中组合Closeable对象?


我正在尝试创建一个管理多个的Java类 Closeable 资源。 C ++解决方案可以直接使用大量资源轻松扩展:

class composed_resource
{
    resource_a a;
    resource_b b;
    resource_c c;

    composed_resource(int x)
        : a(x), b(x), c(x)
    { }

    ~composed_resource()
    { }
};

我天真的Java解决方案:

public class ComposedResource implements Closeable
{
    private final ResourceA a;
    private final ResourceB b;
    private final ResourceC c;

    public ComposedResource(int x) /* throws ... */ {
        a = new ResourceA(x);
        try {
            b = new ResourceB(x);
            try {
                c = new ResourceC(x);
            } catch (Throwable t) {
                b.close();
                throw t;
            }
        } catch (Throwable t) {
            a.close();
            throw t;
        }
    }

    @Override
    public void close() throws IOException {
        try {
            a.close();
        } finally {
            try {
                b.close();
            } finally {
                c.close();
            }
        }
    }
}

略有改进的版本:

public class ComposedResource2 implements Closeable
{
    private final ResourceA a;
    private final ResourceB b;
    private final ResourceC c;

    public ComposedResource2(int x) /* throws ... */ {
        try {
            a = new ResourceA(x);
            b = new ResourceB(x);
            c = new ResourceC(x);
        } catch (Throwable t) {
            close();
            throw t;
        }
    }

    @Override
    public void close() throws IOException {
        try {
            if (a != null) a.close();
        } finally {
            try {
                if (b != null) b.close();
            } finally {
                if (c != null) c.close();
            }
        }
    }
}

是否有更优雅的解决方案,避免嵌套的try-catch-blocks,同时仍保持异常安全?它可以通过三种资源进行管理,但更多的东西变得笨拙。 (如果它是本地范围,我可以使用“try-with-resources”语句,但这不适用于此。)


我在工作时想到了这一点 java.rmi。在构造函数中,我正在创建/查找注册表,查找对象和导出对象。 close()需要取消注册和取消导出对象。我想创建包装器对象来处理导出/取消导出(就像我在C ++中用来利用RAII),但后来我注意到这对我没什么帮助(我不是那么多Java专家,但我必须用它来上大学)。

目前我正在使用类似的东西 ComposedResource2 以上,它工作正常。但现在我有兴趣知道是否有更优雅的解决方案。


5081
2017-12-15 07:52


起源

你能介绍一个用例吗?也许使用一个具体的例子会产生一个比抽象问题更好的答案...... - Adam Michalik
为什么不尝试资源不适用? - a_horse_with_no_name
1)避免在构造函数中创建资源 - 理想情况下,它们应该只为字段分配参数,我认为RAI​​I在这里不适用。 2)如果可能的话,使用try-with-resources(当你的构造函数只能在“赋值模式”下工作时,你可能会更有可能)。 3)你可以写你的 Closeable 加入多个的装饰器 Closeable 将实例转换为单个实例。 4)或者你可以使用番石榴 com.google.common.io.Closer。 - Lyubomyr Shaydariv
@AdamMichalik:我已经添加了一些细节,但如果可能的话,我想抽象地讨论它。在我看来,如果它们是文件,套接字或其他东西并不重要 - 如果它可以被创建并且必须被关闭,你必须以某种方式处理以避免泄漏资源(无论是否是文件处理或打开服务器套接字等) - Angra Mainyu
另外,如果你对静态方法很好,你认为装饰器是一种矫枉过正(我个人认为这有几个原因,尽管我上面提到过): gist.github.com/lyubomyr-shaydariv/e6a76630ff6284171aa5 - Lyubomyr Shaydariv


答案:


使用像这样的try-with-resouces。

@Override
public void close() throws IOException {
    try (Closeable cc = c;
         Closeable bb = b;
         Closeable aa = a;) {
        // do nothing
    }
}

11
2017-12-15 08:35



如果资源因初始化错误而为空,该怎么办? - AdamSkywalker
@AdamSkywalker如果b为null,则a和c将被关闭。 - saka1029
@AdamSkywalker try-with-resources 在Java中是空的安全:即使全部 cc, bb,和 aa 是null - 没有失败。此外,不得使用未初始化或初始化不良的对象并抛出初始化异常。最好的情况是始终良好初始化的对象。 - Lyubomyr Shaydariv
@AngraMainyu:在子类中的任何初始值设定项之前调用父类构造函数。因此,假设您的子类具有声明和初始化为的字段 private Foo foo = new Foo(); 。 。 。突然间,如果它覆盖了一个可以从父类构造函数调用的方法,它必须支持这种可能性 foo 仍然是空的!对于孩子级的老板来说,记忆和跟踪太难了,太过于混乱和违反直觉。 - ruakh
@ saka1029:这是真的,但通常它只是用文档处理(如果重写这个方法,你必须调用超类实现),并且不被认为是一个大问题。 (子类所有者理解他们在重写方法时并不总是有100%的自由。必须调用超类实现并不是一个繁重的要求。) - ruakh


怎么用这种方式改变close()?

@Override
public void close() {
   close(a);
   close(b);
   close(c);
}

public void close(Closeable closeable) throws IOException{
       if (closeable != null){
            closeable.close();
       }
}

我认为它更干净......

您还可以将3个资源作为“Closeables”进行管理,并将它们放在一个数组中,以便ComposedResource拥有您想要的尽可能多的资源。做这样的事情:

public class ComposedResource{
    List<Closeable> resources = new ArrayList<Closeable>();

    public void add(Closeable c){
        resources.add(c);
    }

    @Override
    public void close(){
        for (Closeable r : resources){
          close(r);
        }
    }

    public void close(Closeable c){
        try{
           c.close();
        catch (IOException e){
           log(e);
        }
    }
}

所以你将添加资源:

ComposedResource c = new ComposedResource();
c.add(new Resource1());
c.add(new Resource2());
c.add(new Resource3());
...
// do nice thinks
// and, to close;
c.close();

编辑:

@Mohit Kanwar建议抛出异常,就这样:

@Override
public void close() throws IOException {
     for (Closeable r : resources){
         r.close(r);
     }
 }

并编辑了我的代码,@ Lii说,做这个例外将阻止关闭所有资源,我同意Lii所以我拒绝了Mohit的编辑......


1
2017-12-15 08:21



如果没有关闭资源,我们不应该在记录后抛出异常e吗? - Mohit Kanwar
在您的解决方案中,如果第一个资源在关闭时抛出,则后续资源将不会关闭。这不是它应该如何工作! - Lii
@inigoD我现在明白了。我很抱歉错误的编辑。我已经提出了同样的旗帜,因为它花了你一个downvote。再次抱歉 :( - Mohit Kanwar
NP @Mohit Kanwar;) - inigoD
保存所有抛出的异常并在之后关闭所有资源时抛出它们可能是个好主意。否则可能会隐藏错误。这是什么的 Throwable#addSuppressed 方法是为了。我相信这是try-with-resource-statement的工作原理。 - Lii