【问题标题】:Java try/catch/finally best practices while acquiring/closing resourcesJava try/catch/finally 获取/关闭资源时的最佳实践
【发布时间】:2011-05-04 19:44:29
【问题描述】:

在做一个学校项目时,我编写了以下代码:

FileOutputStream fos;
ObjectOutputStream oos;
try {
    fos = new FileOutputStream(file);
    oos = new ObjectOutputStream(fos);

    oos.writeObject(shapes);
} catch (FileNotFoundException ex) {
    // complain to user
} catch (IOException ex) {
    // notify user
} finally {
    if (oos != null) oos.close();
    if (fos != null) fos.close();
}

问题是 Netbeans 告诉我 resource.close() 行会抛出 IOException ,因此必须被捕获或声明。它还抱怨oosfos 可能尚未初始化(尽管进行了空检查)。

这似乎有点奇怪,因为关键是如何将IOException 停在那里。

我的下意识的解决办法是这样做:

} finally {
    try {
        if (oos != null) oos.close();
        if (fos != null) fos.close();
    } catch (IOException ex) { }
}

但在内心深处,这让我很困扰,感觉很脏。

我来自 C# 背景,我会简单地利用 using 块,所以我不确定处理这个问题的“正确”方法是什么。

什么是处理这个问题的正确方法?

【问题讨论】:

  • 请注意,当oos.close() 抛出IOException 时,您的下意识修复不会关闭fos。每个人都需要进行自己的尝试捕获。 if (oos != null) try { oos.close() } catch (IOException logOrIgnore) {} 等等。
  • @BalusC - 这是一个很好的观点。感谢...抓住...那个:D
  • 其实oos.close()也关闭了底层的fos。
  • 在 C# 中,您将使用 using,但是从 IDisposable 上的 Dispose 方法抛出的任何异常都会冒泡,因此等效的 Java 将在关闭时抛出 RuntimeException陈述。但我不知道.NET 系统库是否默认吞下此类异常。
  • 应该有一个规范的问题 - 有很多非常相似的问题,搜索者/读者很难确定要遵循的问题。一些版主类型可以弄清楚如何合并其中一些问题吗?

标签: java resources try-catch


【解决方案1】:

请注意,以下内容仅适用于 Java 6 及更早版本。对于 Java 7 及更高版本,您应该切换到使用 try-with-resources ...,如其他答案中所述。

如果您试图从源头(Java 6 或更早版本)捕获和报告所有异常,更好的解决方案是:

ObjectOutputStream oos = null;
try {
   oos = new ObjectOutputStream(new FileOutputStream(file));
   oos.writeObject(shapes);
   oos.flush();
} catch (FileNotFoundException ex) {
    // complain to user
} catch (IOException ex) {
    // notify user
} finally {
    if (oos != null) {
        try {
            oos.close();
        } catch (IOException ex) {
            // ignore ... any significant errors should already have been
            // reported via an IOException from the final flush.
        }
    }
}

注意事项:

  • 标准的 Java 包装器流、读取器和写入器都将 closeflush 传播到其包装的流等。因此您只需关闭或刷新最外层的包装器。
  • 在 try 块末尾显式刷新的目的是让 IOException 的(实际)处理程序能够看到任何写入失败1
  • 当您对输出流执行关闭或刷新时,由于磁盘错误或文件系统已满而引发异常的可能性“千载难逢”。 你不应该压制这个例外!

如果您经常需要“关闭一个可能为空的流而忽略 IOExceptions”,那么您可以为自己编写一个这样的辅助方法:

public void closeQuietly(Closeable closeable) {
    if (closeable != null) {
        try {
            closeable.close();
        } catch (IOException ex) {
            // ignore
        }
    }
}

那么你可以将前面的 finally 块替换为:

} finally {
    closeQuietly(oos);
}

另一个答案指出 closeQuietly 方法已经在 Apache Commons 库中可用...如果您不介意为项目添加 10 行方法的依赖项。

但请注意,您只能在 IO 异常确实无关紧要的流上使用 closeQuietly

更新closeQuietly 在 Apache Commons API 2.6 版中已弃用。 Java 7+ try-with-resources 让它变得多余。


关于人们在 cmets 中询问的 flush()close() 的问题:

  • 标准的“过滤器”和“缓冲”输出流和写入器有一个 API 协定,规定 close() 会导致所有缓冲输出被刷新。您应该发现执行输出缓冲的所有其他(标准)输出类的行为方式相同。因此,对于标准类,在 close() 之前立即调用 flush() 是多余的。

  • 对于自定义和第 3 方类,您需要进行调查(例如阅读 javadoc,查看代码),但任何不刷新缓冲数据的 close() 方法都可能是损坏 .

  • 最后,还有flush() 实际作用的问题。 javadoc 说的是这个(对于OutputStream ...)

    如果此流的预期目的地是底层操作系统提供的抽象,例如文件,则刷新流仅保证先前写入流的字节被传递给操作系统进行写入;它不能保证它们实际上被写入了物理设备,例如磁盘驱动器。

    所以...如果您希望/想象调用flush() 可以保证您的数据将持续存在,您错了!(如果您需要这样做,请查看@ 987654340@方法...)

【讨论】:

  • +1 用于指出刷新问题,这是最常被忽略的问题。
  • 在 finally 块中刷新对象不是一个好主意吗?
  • @Littlebird - 这是没有意义的,这是close() 方法的责任,如果这是一件有意义的事情,请刷新。
  • IOUtils.closeQuietly 完全按照您的建议进行。这是我通常在我的 Maven 项目中使用的依赖项(来自 Appache 的 commons-io 可在此处获得:commons.apache.org/proper/commons-io
【解决方案2】:

Java 7 将添加 Automatic Resource Management 块。它们与 C# 的 using 非常相似。

Josh Bloch 写了the technical proposal,我强烈推荐阅读。不仅因为它可以让您在即将推出的 Java 7 语言功能上占得先机,还因为规范激发了对这种结构的需求,并且这样做说明了即使在没有 ARM 的情况下如何编写正确的代码。

这是一个 Asker 的代码示例,翻译成 ARM 形式:

try (FileOutputStream fos = new FileOutputStream(file);
        ObjectOutputStream oos = new ObjectOutputStream(fos)) 
{
    oos.writeObject(shapes);
}
catch (FileNotFoundException ex) 
{
    // handle the file not being found
}
catch (IOException ex) 
{
    // handle some I/O problem
}

【讨论】:

  • ARM 是否也会在close(); 之前调用flush();
  • @Muddz 不,ARM 不会调用刷新。值得注意的是,以常用方式使用的大多数常见 Java 类不需要您在调用 close() 之前调用 flush(),因为 close() 的较低级别实现通常会做足够的工作来显式结束-stream 冲洗不必要。不幸的是,这种刷新/关闭合同没有记录在案,它只是人们普遍信任的一种已知的隐含行为。在您知道需要在 close() 之前调用 flush() 的特定情况下,您可以装饰您的流,例如new MyFlushOnCloseOutputStream(originalOutputStream).
  • 好吧,这让我不必担心。谢谢你的解释!
【解决方案3】:

当前涉及可关闭对象(例如文件)的 try/catch/finally 的最佳实践是使用 Java 7 的 try-with-resource 语句,例如:

try (FileReader reader = new FileReader("ex.txt")) {
    System.out.println((char)reader.read());
} catch (IOException ioe) {
    ioe.printStackTrace();
}

在这种情况下,FileReader 会在 try 语句结束时自动关闭,而无需在显式 finally 块中关闭它。这里有几个例子:

http://ppkwok.blogspot.com/2012/11/java-cafe-2-try-with-resources.html

官方Java描述在:

http://docs.oracle.com/javase/7/docs/technotes/guides/language/try-with-resources.html

【讨论】:

    【解决方案4】:

    你做得对。它也困扰着我。您应该将这些流显式初始化为 null - 这是常见的约定。你所能做的就是加入俱乐部并想要using

    【讨论】:

    • 你不能放弃空检查; fos 可能会在打开时抛出,这将导致尝试关闭 oos 如果未检查 null 则抛出 NullPointerException。
    • 感谢“赶上”。好双关,嗯? :)
    • 永远不要捕获 NullPointerException 或异常!
    • 第一次有人这么告诉我。为什么?
    • -1 用于建议捕获 NullPointerException(或 Exception)。最糟糕的建议!
    【解决方案5】:

    这家伙怎么样?没有空检查,不足为奇。退出时一切都被清理干净。

    try {
        final FileOutputStream fos = new FileOutputStream(file);
        try {
            final ObjectOutputStream oos = new ObjectOutputStream(fos);
            try {
                oos.writeObject(shapes);
                oos.flush();
            }
            catch(IOException ioe) {
                // notify user of important exception
            }
            finally {
                oos.close();
            }
        }
        finally {
            fos.close();
        }
    }
    catch (FileNotFoundException ex) {
        // complain to user
    }
    catch (IOException ex) {
        // notify user
    }
    

    【讨论】:

    • 否 - close 引发的异常将替换实际文件操作引发的信息量更大的异常。
    • 您的意思是来自 fos.close() 的异常覆盖来自 oos.writeObject() 的异常?让我们抓住它然后处理它。 (原帖已编辑)
    • RAY 的代码最接近 Java 7 ARM 为您编写的代码。除此之外,Java 7 ARM 将记住在 try{} 中遇到的任何异常,并最终将其优先于在 finally{} 中可能发生的任何可能的异常。
    • +1,除非我不确定你是否需要在最里面的 try 中使用 catch,假设你以相同的方式处理两个 IOException 块。
    • @ILMTitan。谢谢!根据 Software Monkey 的评论添加了最里面的尝试。说错了还请指正,我对他的评论的理解是,如果oos.writeObject()和fos.close()都抛出异常,那么oos.writeObject()就会丢失,不会被处理。
    【解决方案6】:

    我通常有小类 IOUtil,方法如下:

    public static void close(Closeable c) {
        if (c != null) {
            try {
                c.close();
            }
            catch (IOException e) {
                // ignore or log
            }
        }
    }
    

    【讨论】:

    • 这绝对是我的首选方法。我总是确保在关闭时记录一个异常。这可能是一些令人讨厌的事情发生的迹象。虽然它不应该停止正常运行,但我真的觉得这里应该在某个地方记录一个错误。
    【解决方案7】:

    不是对您的观点的直接回答,但不幸的是,因为finallycatch 都与try 相关联,人们认为它们属于同一类。 try 块的最佳设计是拥有 catchfinally,但不能同时拥有。

    在这种情况下,您的 cmets 暗示有问题。为什么,在处理文件 IO 的方法中,我们向用户抱怨任何事情。我们可能在某个地方的服务器上运行很深,看不到任何用户。

    因此,您在上面提供的代码应该有一个finally,以便在出现问题时优雅地失败。但是,它缺乏智能处理错误的能力,因此您的 catch 属于调用链中更高的位置。

    【讨论】:

    • 不同意“非此即彼”部分;我有很多例子,我需要执行一些异常处理,但绝对必须有一个 finally 来清理资源。
    • 通常情况下,我会 100% 同意你的看法,而且我只关心保持适当的责任等等,但这是一个快速的小学校项目,有一个简单的 UI(一个类),旨在让我们了解 Java 中的文件 IO。您应该庆幸我没有将所有代码放在匿名 ActionListener 的事件处理程序中! :D
    • @Software Monkey - 公平调用,我并不是说你不应该有 finally 块,事实上恰恰相反。只是通过使用 finally 块或 catch 块,您可以获得与 C# 的 using 块相同的语义,但如果检查了异常,您必须明确地将方法标记为抛出它们。
    • @Austin Hyde - 快速而肮脏的方法是将您的 try...finally 嵌套在您的 try...catch
    • 这不值得一票否决,很多时候重复尝试是正确的习惯用法,即使大多数开发人员不使用它。见这里:illegalargumentexception.blogspot.com/2008/10/…
    【解决方案8】:

    很遗憾,没有语言级别的支持。但是有很多库可以让这变得简单。检查 commons-io 库。或现代 google-guava @http://guava-libraries.googlecode.com/svn/trunk/javadoc/index.html

    【讨论】:

    猜你喜欢
    • 2021-11-26
    • 2013-12-28
    • 1970-01-01
    • 1970-01-01
    • 2018-05-29
    • 2011-03-26
    • 2015-12-10
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多