英文:
How to write to a text file from an ArrayList
问题
我试图从一个类的ArrayList
中获取数据,并将其写入文本文件。它创建了临时文件,但没有对其进行任何操作。它会打印我尝试放入文件的内容,并且不会删除临时文件。我做错了什么?
try
{
File temp = new File("temp.txt");
File file = new File(prop.getProperty("path"));
BufferedWriter writer = new BufferedWriter(new FileWriter(temp));
ArrayList<Contact> contacts = gestor.checkData();
if(temp.createNewFile())
{
for(int i = 0; i < contacts.size(); i++)
{
writer.write(contacts.get(i).getName());
writer.write(contacts.get(i).getLastNames());
DateFormat df = new SimpleDateFormat("yyyy-MM-dd");
String date = df.format(contacts.get(i).getBirthday());
writer.write(date);
writer.write(contacts.get(i).getEmail());
}
writer.close();
file.delete();
temp.renameTo(file);
}
}
catch (IOException e)
{
e.printStackTrace();
}
checkData()
方法的代码只是返回 ArrayList<Contact> contactList
。
英文:
I'm trying to take data from an ArrayList
of a class and write it into a text file. It creates the temporary file, but doesn't do anything with it. It prints what I'm trying to put in the file and doesn't delete the temporary file. What am I doing wrong?
try
{
File temp = new File("temp.txt");
File file = new File(prop.getProperty("path"));
BufferedWriter writer = new BufferedWriter(new FileWriter(temp));
ArrayList<Contact> contacts = gestor.checkData();
if(temp.createNewFile())
{
for(int i = 0; i < contacts.size(); i++)
{
writer.write(contacts.get(i).getName());
writer.write(contacts.get(i).getLastNames());
DateFormat df = new SimpleDateFormat("yyyy-MM-dd");
String date = df.format(contacts.get(i).getBirthday());
writer.write(date);
writer.write(contacts.get(i).getEmail());
}
writer.close();
file.delete();
temp.renameTo(file);
}
}
catch (IOException e)
{
e.printStackTrace();
}
The code of checkData()
just returns the ArrayList<Contact> contactList
.
答案1
得分: 3
你的代码存在许多问题:
资源泄漏
像 new FileWriter
这样的操作是一种 资源。资源 必须 被关闭。因此,你不应该让资源泄漏(当你创建一个资源时通常会意识到:要么调用 new X
,其中 X 明确代表一个资源,要么调用类似 socket.getInputStream
或 Files.newBufferedReader
的方法……除非你这样做得“正确”。有两种正确的方式来处理资源关闭:
try (FileWriter w = new FileWriter(...)) {
// 在此处使用资源
}
或者,如果你需要将资源作为类的字段,那么唯一安全的方式是将你的类作为资源:使其 implements AutoClosable
,创建一个 close
方法,现在使用该 try 语法的负担在于使用你的类的人身上。没有安全的方式可以在不使用 try-with-resources 的情况下安全地执行这些操作*。
字符集混乱
文件是字节,而不是字符。此外,文件系统不知道所谓的“编码”是什么。你如何将 ☃
,或者更确切地说,é
转换为字节?答案取决于字符集编码。所有将字节转换为字符或反之亦然的方法,如果不在 java.nio.file
包中,它们都使用所谓的“平台默认编码”。这是一种非常糟糕的做法,因为它将在你的计算机上“正常工作”,并通过所有测试,然后在生产环境中在最坏的时刻失败。解决方案是永远不要依赖于平台默认编码。如果你真的打算使用它,那就明确指定它。不幸的是,直到 Java 11,FileWriter 都没有指定字符集编码的方法,这使得它成为一个完全无用的类,你实际上无法在不编写有缺陷的代码的情况下使用它。所以不要使用它。我建议你切换到新的文件 API,它默认使用 UTF-8(相当明智的默认值),并且可以在一行代码中完成许多复杂的操作。
换行问题
你只是使用 .write
写入了所有这些数据。write
正是字面意思,会精确地写入字符串,它不会添加任何额外的内容。具体而言,它不会添加任何换行符。你肯定不想将数据像这样混乱地写入文件中,比如 JoeSteel1990-10-01
吧?为了避免这种情况,你应该写入 \n
,或者预先构建整个字符串,包括你想要的换行符,然后再写入。
在意外情况下静默地什么都不做
如果“temp”文件已经存在,你的代码将会在静默状态下什么都不做。这听起来像是设计上的一个奇怪情况(因为临时文件在此后会被“重命名”)。作为一个经验法则,当你遇到你知道不太可能或看似不可能的场景时,“静默地什么都不做” 是完全错误的反应。正确的反应是完全相反的:尽可能地惊人地失败。(当然,最好的选择是思考奇怪的情况意味着什么,并正确处理它,但这并不总是可能的)。所以,不要这么做,试试这样:
if (!temp.creatNewFile()) {
throw new RuntimeException("That is weird - tempfile already exists: " + temp);
}
这是正确的心态:如果发生了奇怪的事情,就尽快地失败,带有足够的细节,以便你知道出了什么问题。(最可能的“正确”处理方式是删除临时文件。这个临时文件的整个目的就是,如果你进入这段代码时它仍然存在,那么上一次的尝试在中途失败了,所以只需删除失败操作的产物,它没有用处)。
异常处理
虽然在示例和甚至是 IDE 模板中很常见,但你的做法是错误的。你绝对不应该通过打印一些内容并继续执行来处理异常。想一想:如果出现了问题,继续执行代码要么会引起严重的问题(当你编写代码时,肯定没有考虑到程序中的某个步骤失败),要么会引发另一个错误。如果所有错误都像这样处理,一旦出现问题,你的日志中就会出现 85 条错误跟踪。这是没有用的。如果出现问题,而你不知道如何处理它,请不要继续运行。唯一明智的“我不知道如何处理这个”异常处理方式是:
catch (IOException e) {
throw new RuntimeException("unhandled", e);
}
有时可以使用比 RuntimeException 更好的异常类型(例如 UncheckedIOException 或 ServletException,这取决于情况)。此外,有时正确的答案是只是将异常传递给上游。记住,public static void main
可以(而且通常应该!)声明为 throws Exception
。
将所有内容整合在一起
try {
Path temp = Paths.get("temp.txt");
Path file = Paths.get(prop.getProperty("path"));
ArrayList<Contact> contacts = gestor.checkData();
try (BufferedWriter writer = Files.newBuffered
<details>
<summary>英文:</summary>
Many problems with your code:
## resource leakage
Something like a `new FileWriter` is a __resource__. Resources __MUST__ be closed. As a consequence, you should never make resources (you usually know it when you make a resource: you either call `new X` where X is clearly representative of a resource, or you call something like `socket.getInputStream` or `Files.newBufferedReader`... unless you do it 'right'. There are only two ways to do it right:
try (FileWriter w = new FileWriter(...)) {
// use it here
}
or, if you need the resource to be a field of your class, then the only safe way is to turn your class into a resource: make it `implements AutoClosable`, make a close method, and now the burden of using that try syntax is on whomever uses your class. __There is no way to safely do this stuff without try-with-resources__*.
## Charset messup
files are bytes. not characters. In addition, the filesystem has no clue what the 'encoding' is. How do you turn `☃`, or for that matter, `é` into bytes? The answer depends on charset encoding. The problem with all methods that turn bytes into characters or vice versa that are NOT in the `java.nio.file` package is that they use 'platform default encoding'. That's a funny way of saying 'the worst idea ever', as that will 'work' on your machine and will pass all tests, and then fail in production, at the worst possible moment. The solution is to __never__ rely on platform default, ever. If you really really intend to use that, make it explicit. Unfortunately, FileWriter, until java11, has __no way__ of specifying charset encoding, which makes it an utterly useless class you cannot actually ever use without writing buggy code. So don't. I suggest you switch to the new file API, which defaults to UTF-8 (quite a sane default), and can do a lot of complicated things in one-liners.
## No newlines
You just `.write` all this data. write does exactly what it says, and writes precisely the string. It does not print anything more. Specifically, it does not print any newlines. Surely you did not intend to just write, say, `JoeSteel1990-10-01` to the file, all in one jumbled mess like that? write `\n` to avoid this, or preconstruct the entire string just as you want it (with newlines), and then write that.
## on unexpected condition silently do nothing
Your code will silently do nothing if the 'temp' file already exists. It sounds like the design is that this is a weird situation (as the temp file is 'renamed' right after). As a general rule of thumb, 'silently do nothing' is entirely the wrong instinct to have when you run into scenarios you know are unlikely or seemingly impossible. The right instinct is the exact opposite: Fail as spectacularly as you can. (The best option, of course, is to think about what the weird scenario means and handle it properly, but that's not always possible). So, instead of that, try:
if (!temp.creatNewFile()) {
throw new RuntimeException("That is weird - tempfile already exists: " + temp);
}
That's the right mindset: If weird things happen, blow up, as fast as you can, with enough detail so you know what's going wrong. (The most likely 'correct' way to handle this is to just delete the temp file. The whole point of that temp file is that if it's still there when you enter this code, that the previous attempt failed halfway through, so just delete the product of the failed operation, it isn't useful).
## exception handling.
Whilst common in examples and even IDE templates, you're not doing it right. You should NEVER handle an exception by printing something and carrying on. Think about it: If something went wrong, continuing with the code execution is either going to cause nasty problems (as you surely did not consider that one of the steps in your program failed when you write your code), or is going to cause another error. And if all errors are handled like this, one thing goes wrong and you get 85 error traces in your logs. That's not useful. __If a problem occurs and you do not know how to handle it, do NOT continue running__. The only sensible 'I do not know how to handle this' exception handling is:
catch (IOException e) {
throw new RuntimeException("unhandled", e);
}
sometimes a better exception than RuntimeException is possible (such as UncheckedIOException or ServletException, it depends on the situation). Also, sometimes the right answer is to just `throws` the exception onwards. Remember, `public static void main` can (and usually should!) be declared as `throws Exception`.
## Putting it all together
try {
Path temp = Paths.get("temp.txt");
Path file = Paths.get(prop.getProperty("path"));
ArrayList<Contact> contacts = gestor.checkData();
try (BufferedWriter writer = Files.newBufferedWriter(temp)) {
for (Contact contact : contacts) {
writer.write(contact.getName());
writer.write("\n");
writer.write(contact.getLastNames());
writer.write("\n");
DateFormat df = new SimpleDateFormat("yyyy-MM-dd");
String date = df.format(contact.getBirthday());
writer.write(date);
writer.write("\n");
writer.write(contact.getEmail());
writer.write("\n");
}
}
Files.delete(file);
Files.move(temp, file);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
The above will probably fail, but it will fail telling you exactly why it failed. For example, it might tell you that the directory you are trying to write to does not exist, or you have no write access. Whereas your code will then silently just do nothing.
*) For you java pros out there, sure, you can handroll your own try/finally loops. Let me know how many programmers new to java ever managed to dodge every mine in the minefield when you do that. Until you are fairly wintered, this rule of thumb becomes effectively a rule of law: You can't do it safely without t-w-r. Let's keep it simple.
</details>
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论