何时在Java中使用哪个Writer子类;常见做法

20

我一直对Java中不同的IO实现方式感到有些困惑,现在在我的项目开发中完全陷入困境时,我正在花时间阅读一些有用的东西。

我意识到除了Writer类的API中简短的解释之外,没有面向新手的比较介绍Writer类的不同子类。所以我想问一个问题,这些不同的子类有什么好处?

例如,我通常使用一个带有BufferedWriter包装的FileWriter来输出文件,但是我一直被一个烦人的事情所困扰:没有像println()那样的方法,必须每隔一行使用newLine()(以使输出易于阅读)。PrintWriter具有println()方法,但没有支持追加的构造函数...

如果您可以从自己的经验中给我一些建议,或者介绍您遇到过的一些不错的指南/教程,我会非常感激。

编辑:谢谢大家的回复,我非常感激在这里传递的信息。很遗憾,整个append()的事情成为了重点,只是它作为一个例子。我的问题主要是关于所有不同实现的需要和用途,我想这在一些答案中有所提及。

很难选择一个被接受的答案,因为有三个非常好的答案,每个都对我理解问题做出了贡献。这次我得选择Anon,因为他的声望点数最少(我认为他是SO上的新手)。他有15个答案,其中一些表述得非常好,没有提出任何问题。我认为这是非常好的贡献,值得推广。

话虽如此,ColinD和Jay也提供了非常好的答案,并指出了有趣的想法。特别是Jay关于Java自动包装BufferedWriter的评论值得注意。再次感谢大家,真的很感激!

4个回答

9

java.io类通常遵循装饰器模式。因此,虽然PrintWriter没有您想要的特定构造函数,但它有一个接受另一个Writer的构造函数,所以您可以执行以下操作:

FileOutputStream fos = null;
try
{
    fos = new FileOutputStream("foo.txt");
    PrintWriter out = new PrintWriter(
                          new BufferedWriter(
                              new OutputStreamWriter(fos, "UTF-8")));
    // do what you want to do
    out.flush();
    out.close();
}
finally
{
    // quietly close the FileOutputStream (see Jakarta Commons IOUtils)
}

作为一般使用注意事项,您总是希望将低级Writer(例如FileWriter或OutputStreamWriter)包装在BufferedWriter中,以最小化实际IO操作。但是,这意味着您需要显式刷新和关闭最外层的Writer,以确保所有内容都被写入。
然后,您需要在finally块中关闭低级Writer,以确保不会泄漏资源。
编辑:
查看MForster的答案让我重新审视了FileWriter的API。我发现它不需要显式字符集,这是非常糟糕的事情。因此,我编辑了我的代码片段,使用了一个采用显式字符集的OutputStreamWriter包装的FileOutputStream。

感谢您提供详尽的答案;不过我还有几个问题:i)您代码中的“dos”应该是“fos”,对吧?ii)每次需要一个写入器时都要以那种方式初始化,这不是很麻烦吗?我知道开发人员想要给出许多不同的选项,但为什么不创建一个执行“默认”工作的包装类呢?iii)字符集为什么如此重要? - posdef
@posdef - (i) 你说得对,我会进行编辑。(ii) 在Java中,这种资源管理模式经常发生,也是人们认为Java“过于冗长”的原因之一。闭包是一种解决方案,你可以使用匿名内部类来实现它们,尽管这仍然很冗长。模板方法模式是另一种解决方案,尽管它至少同样冗长。(iii) 如果你要将可能的非ASCII数据写入到另一台机器上读取的文件中,则字符集非常重要。这涵盖了大多数非玩具应用程序(以及所有Web应用程序)。 - Anon
请注意,PrintWriter 会产生另一个问题。它会默默地忽略错误。要检查是否存在问题,您需要使用 PrintWriter.checkError() 方法,该方法返回 boolean 类型。这样,就无法获取潜在的 IOException 错误了。 - Venkata Raju

7

FileWriter 通常不是一个可接受的类来使用。它不允许您指定用于写入的 Charset,这意味着您被困在运行平台默认字符集上。不用说,这使得无法一致地使用相同的字符集读取和写入文本文件,并可能导致数据损坏。

与其使用 FileWriter,您应该将 FileOutputStream 包装在 OutputStreamWriter 中。 OutputStreamWriter 允许您指定字符集:

File file = ...
OutputStream fileOut = new FileOutputStream(file);
Writer writer = new BufferedWriter(new OutputStreamWriter(fileOut, "UTF-8"));

为了使用PrintWriter,只需将BufferedWriter包装在PrintWriter中即可:
PrintWriter printWriter = new PrintWriter(writer);

你也可以直接使用带有File和字符集名称参数的PrintWriter构造函数:

PrintWriter printWriter = new PrintWriter(file, "UTF-8");

这种方法可以很好地适应您的特定情况,并且实际上与上面的代码执行的是相同的操作,但了解如何通过包装各种部分来构建它是很有用的。

其他Writer类型大多用于专门的用途:

  • StringWriter只是一个Writer,可用于创建StringCharArrayWriter对于char[]也是一样。
  • PipedWriter用于管道连接到PipedReader

编辑:

我注意到您在另一个回答中评论了以此方式创建writer的冗长性。请注意,有像Guava这样的库可以帮助减少常见操作的冗长性。例如,将字符串写入特定字符集的文件。使用Guava,您只需编写以下内容:

Files.write(text, file, Charsets.UTF_8);

您可以像这样创建一个BufferedWriter
BufferedWriter writer = Files.newWriter(file, Charsets.UTF_8);

感谢您的详细回复。只是为了更好地理解这些细节,如果FileWriter没有用,为什么还有它的子类?StringWriterStringBuilder之间有什么区别呢? - posdef
@posdef:并不是每个Java类都被设计得很好且易于使用。FileWriterFileReader 就是其中的例子。我猜想在它们被编写时,使用平台默认编码被认为是可以接受的......毕竟,Java选择了默认编码而不是像UTF-8这样的特定字符集,而今天几乎肯定会选择UTF-8(例如C#使用UTF-8作为其默认字符集)。 - ColinD
@posdef:就StringWriterStringBuilder而言,它们是不同的API。StringWriter实现了Writer,这意味着您可以将其与写入文本到Writer的API一起使用,生成一个String而不是一个File或其他内容。例如,您可以使用包装在PrintWriter中的StringWriter以及Throwable.printStackTrace(PrintWriter)来获取异常的堆栈跟踪作为一个String。但是,您无法使用StringBuilder完成此操作。 - ColinD

4
PrintWriter没有带有“append”参数的构造函数,但是FileWriter有。在我看来,这似乎是它应该属于的地方。PrintWriter不知道你是在写入文件、套接字、控制台、字符串等,对于向套接字写入时的“追加”操作是什么意思呢?
因此,实现您想要的正确方法很简单:
PrintWriter out=new PrintWriter(new BufferedWriter(new FileWriter(myfile, append)));

有趣的一点是:如果你将一个OutputStream包装在PrintWriter中,Java会自动在中间插入一个BufferedWriter。但是如果你将一个Writer包装在PrintWriter中,它不会这样做。因此,说出下面的话并没有什么意义:

PrintWriter out=new PrintWriter(new BufferedWriter(new OutputStreamWriter(new FileOutputStream(myfile))));

只需省略BufferedWriter和OutputStreamWriter,因为它们已经免费提供了。我不知道是否存在某些不一致的好理由。
ColinD指出,您确实无法在FileWriter中指定字符编码。我不知道这是否使其“不可接受”。我几乎总是很愿意接受默认编码。也许如果您使用的是英语以外的语言,这就成为问题了。
当我开始使用Java时,需要将Writer或OutputStream包装在层中令我感到困惑。但是一旦你掌握了它,就没有什么大不了的。您只需弯曲思维并进入写作框架即可。每个作者都有一个功能。将其视为,“我想打印到文件中,因此我需要在PrintWriter中包装FileWriter。”或者,“我想将输出流转换为作者,因此我需要一个OutputStreamWriter。”等等。
或者,您只需习惯自己经常使用的那些。找到方法并记住如何做。

很多人(不幸地)完全满足于使用默认编码。是的,根据您所做的事情,您可以在几年内潜在地得以脱身,但是如果没有持续指定字符集(即UTF-8,这通常是一个不错的选择),则不能保证您的程序在各个平台上正常工作。您可能会在一个字符集中编写文件,然后在其他地方以另一种字符集读取该文件,从而得到不正确/损坏的数据。请参阅此讨论:http://groups.google.com/group/guava-discuss/browse_thread/thread/88976574f28394b0/e46fad488792f084 - ColinD
@ColinD:是的,这是可能的。但在实践中,我使用的系统:(a) 几乎总是只使用 ASCII 字符,(b) 在同一平台上读取和写入。实际上,我不知道有任何情况会引起问题。我想你可以说指定它的工作量很小。另一方面,我面对的实际问题太多了,我不会花十分钟担心可能会遇到的理论问题。当我所使用的系统开始处理除英语以外的其他语言文字时,我才会开始担心它。 - Jay
@ColinD:哦,让我补充一下:如果你正在处理多种语言的数据系统,那么这是一个实际问题。 - Jay
感谢您的友好回复,也感谢你们两位进行了一场有趣的讨论。现在感觉稍微清晰一些了。 - posdef

2
您可以像这样创建一个附加的PrintWriter
OutputStream os = new FileOutputStream("/tmp/out", true);
PrintWriter writer = new PrintWriter(os);

编辑:Anon的帖子关于使用BufferedWriter和指定编码都是正确的。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接