Java - 在读取文本文件时覆盖删除其中的行

8
我正在尝试从文本文件中删除一行文本,而不需要将其复制到临时文件中。我试图通过使用PrintWriter和Scanner来实现这一点,并使它们同时遍历文件,写入器写入Scanner读取的内容并覆盖每一行相同的东西,直到到达我想要删除的行。然后,我推进Scanner但不是写手,然后像以前一样继续。以下是代码:
但首先,参数:我的文件名称是数字,因此这将读取1.txt或2.txt等等,因此f指定文件名。我在文件的构造函数中将其转换为字符串。int n是我想要删除的行的索引。
public void deleteLine(int f, int n){
 try{
 Scanner reader = new Scanner(new File(f+".txt")); 
 PrintWriter writer = new PrintWriter(new FileWriter(new File(f+".txt")),false); 
 for(int w=0; w<n; w++)
   writer.write(reader.nextLine()); 
 reader.nextLine(); 
 while(reader.hasNextLine())
   writer.write(reader.nextLine());
 } catch(Exception e){
   System.err.println("Enjoy the stack trace!");
   e.printStackTrace();
 }
}

我遇到了一些奇怪的错误。在堆栈跟踪中,它显示“NoSuchElementException”和“no line found”。它指向不同的行; 似乎任何nextLine()调用都可能导致这种情况。是否可能通过这种方式删除一行?如果是,我做错了什么?如果不是,为什么?(顺便说一句,以防你需要知道,文本文件大约有500行。我不知道这是否算大或是否重要。)


为什么不使用临时文件?你对此有何厌恶之处? - Hovercraft Full Of Eels
@Hovercraft Full Of Eels 说实话,我不太清楚如何操作,而且来回复制似乎相当低效。我必须删除原始文件并重命名临时文件,然后才能替换它,是吗?直接同时读写该文件不是更容易和更有效吗? - Shelley
这不是更简单,而是错误的。请查看答案。 - Hovercraft Full Of Eels
3个回答

24

正如其他人指出的那样,如果你的程序有一点崩溃的风险,最好使用临时文件:

public static void removeNthLine(String f, int toRemove) throws IOException {

    File tmp = File.createTempFile("tmp", "");

    BufferedReader br = new BufferedReader(new FileReader(f));
    BufferedWriter bw = new BufferedWriter(new FileWriter(tmp));

    for (int i = 0; i < toRemove; i++)
        bw.write(String.format("%s%n", br.readLine()));

    br.readLine();

    String l;
    while (null != (l = br.readLine()))
        bw.write(String.format("%s%n", l));

    br.close();
    bw.close();

    File oldFile = new File(f);
    if (oldFile.delete())
        tmp.renameTo(oldFile);

}

请注意编码、换行符和异常处理的不规范处理。


然而,我不喜欢用“我不会告诉你如何做,因为无论如何你都不应该这样做”来回答问题。(例如,在某些情况下,你可能正在使用一个大于硬盘一半大小的文件!)所以,以下是解决方案:

你需要使用RandomAccessFile。使用此类,你可以使用同一个对象同时读取和写入文件:

public static void removeNthLine(String f, int toRemove) throws IOException {
    RandomAccessFile raf = new RandomAccessFile(f, "rw");

    // Leave the n first lines unchanged.
    for (int i = 0; i < toRemove; i++)
        raf.readLine();

    // Shift remaining lines upwards.
    long writePos = raf.getFilePointer();
    raf.readLine();
    long readPos = raf.getFilePointer();

    byte[] buf = new byte[1024];
    int n;
    while (-1 != (n = raf.read(buf))) {
        raf.seek(writePos);
        raf.write(buf, 0, n);
        readPos += n;
        writePos += n;
        raf.seek(readPos);
    }

    raf.setLength(writePos);
    raf.close();
}

@aioobe:但是为什么呢?有什么好处吗?他/她最初采用原始方式的主要理由是无知而不是需要,那现在如果他/她想要插入比删除的文本更大的文本怎么办? - Hovercraft Full Of Eels
显然取决于线的长度。是吧;-) 无论如何,已经更新了答案。 - aioobe
@aioobe 我很感激;@Hovercraft Full Of Eels 嗯,我的想法是为了效率,但我承认我对Java有些陌生。我将使用临时文件的方式来完成它,这样做应该是正确的。不过...如果我的方法是可行的并且占用的空间更少,您能否详细解释一下为什么这种方法是错误的?这样的解释会对我的Java知识之旅有大帮助 :-) - Shelley
@Shelley:我不知道哪种技术相对更快,但像aioobe所说,RAF在大小方面有一些优势,另一方面,在进程完成之前保留原始文件肯定具有明显的安全优势,甚至可以将原始文件重命名以便在将临时文件命名为原始名称之前备份。 - Hovercraft Full Of Eels
@aioobe:感谢您的编辑并且同意。我已将-1票更改为+1。 - Hovercraft Full Of Eels
显示剩余2条评论

2
您不能这样做。FileWriter只能向文件追加内容,而不能在其中间写入 - 如果您想要在中间写入,则需要使用RandomAccessFile。现在您所做的是 - 第一次写入时覆盖了该文件(并且它变为空 - 这就是为什么会出现异常)。您可以创建带有附加标志设置为true的FileWriter - 但是这样将追加到文件中,而不是在其中间写入。
我真的建议先写入新文件,然后在最后将其重命名。

1
是的,推荐将内容写入新文件并重命名。 - Hovercraft Full Of Eels

1
@shelley: 你不能做你试图做的事情,而且更重要的是,你不应该这样做。你应该读取文件并将其写入临时文件,有几个原因:首先,这种方式是可行的(与你试图做的方式相反),其次,如果进程出现错误,你可以退出而不会丢失原始文件。现在,你可以使用RandomAccessFile更新文件的特定位置,但在我看来,这通常是用于处理固定大小记录而不是典型的文本文件。

这是一个很好的观点。我会采纳你的建议。还有一个问题:为了创建一个临时文件,我应该使用createTempFile还是只是用不同的文件名构造函数?(如果这个问题看起来很傻,请原谅我;我对文件I/O方面还是新手。) - Shelley
@Shelley:你有看过Java教程中的I/O部分吗?如果没有,我建议你去看一下:基本I/O - Hovercraft Full Of Eels

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