Java - удалить строку из текстового файла, перезаписав ее при чтении

Я пытаюсь удалить строку текста из текстового файла без копирования во временный файл. Я пытаюсь сделать это, используя Printwriter и 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» и «строка не найдена». Он указывает на разные линии; кажется, что любой из вызовов nextLine() может сделать это. Можно ли удалить строку таким образом? Если да, то что я делаю неправильно? Если нет, то почему? (Кстати, на всякий случай, если вам это нужно, текстовый файл составляет около 500 строк. Я не знаю, считается ли это большим или даже имеет значение.)


person Shelley    schedule 25.06.2011    source источник
comment
Почему бы не использовать временный файл? Каково ваше отвращение к этому?   -  person Hovercraft Full Of Eels    schedule 25.06.2011
comment
@Hovercraft Full Of Eels Честно говоря, я действительно не знаю, как это сделать, и все это копирование туда и обратно кажется довольно неэффективным. Мне пришлось бы удалить исходный файл и переименовать временный, чтобы заменить его, да? Не проще ли и эффективнее просто одновременно читать и записывать этот файл?   -  person Shelley    schedule 25.06.2011
comment
это не проще, это неправильно. Пожалуйста, смотрите ответ.   -  person Hovercraft Full Of Eels    schedule 25.06.2011


Ответы (3)


Как указывали другие, вам может быть лучше использовать временный файл, если есть малейший риск сбоя вашей программы на полпути:

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();
}
person aioobe    schedule 25.06.2011
comment
@aioobe: но почему, в чем преимущество, когда его / ее основная причина сделать это первоначальным способом была связана с невежеством, а не с необходимостью? А что, если он/она захочет позже вставить текст, который больше удаленного текста? - person Hovercraft Full Of Eels; 25.06.2011
comment
Наиболее очевидным преимуществом является то, что он не требует вдвое больше места на жестком диске. Если файл действительно большой, может быть невозможно иметь две копии даже временно. - person aioobe; 25.06.2011
comment
@aioobe: 500 строк текста считаются действительно большими? Причина, по которой я спрашиваю, заключается в том, что я боюсь, что ваш ответ введет в заблуждение кого-то, кто плохо знаком с Java, думая, что это лучший ответ для ее ситуации. Я считаю, что это не так. - person Hovercraft Full Of Eels; 25.06.2011
comment
Очевидно, зависит от длины линий. Не так ли ;-) Все равно обновил ответ. - person aioobe; 25.06.2011
comment
@aioobe Я ценю это ;) @Hovercraft Full Of Eels Что ж, моим обоснованием была эффективность, но я признаю, что я немного новичок в Java. Я собираюсь сделать это с временным файлом, как это должно быть сделано. Тем не менее ... Не могли бы вы объяснить немного подробнее, почему делать это так, как я пытался, неправильно, если это возможно и занимает меньше места? Объяснение очень помогло бы мне в моем стремлении к знаниям Java :-) - person Shelley; 25.06.2011
comment
@Shelley: я не знаю о каких-либо относительных преимуществах скорости одного метода по сравнению с другим, но как aioobe, RAF имеет некоторые преимущества в размере, в то время как, с другой стороны, есть определенные преимущества безопасности, когда исходный файл остается нетронутым до тех пор, пока процесс не будет завершить, и даже тогда, возможно, переименовать исходный файл, чтобы его можно было сохранить в качестве резервной копии, прежде чем присвоить временному файлу исходное имя. - person Hovercraft Full Of Eels; 25.06.2011
comment
@aioobe: спасибо за редактирование и согласен. Изменено -1 голос на +1. - person Hovercraft Full Of Eels; 25.06.2011

Вы не можете сделать это таким образом. FileWriter может только добавлять к файлу, а не записывать в его середину. Вам нужен RandomAccessFile, если вы хотите писать в середине. Что вы делаете сейчас - вы переопределяете файл при первой записи в него (и он становится пустым - поэтому вы получаете исключение). Вы можете создать FileWriter с флагом добавления, установленным в true, но таким образом вы будете добавлять в файл, а не писать в его середине.

Я бы действительно рекомендовал записать в новый файл, а затем переименовать его в конце.

person Jarek Potiuk    schedule 25.06.2011
comment
Да, 1+ за рекомендацию записать в новый файл и переименовать его. - person Hovercraft Full Of Eels; 25.06.2011

@shelley: ты не можешь делать то, что пытаешься сделать, и более того, ты не должен. Вы должны прочитать файл и записать во временный файл по нескольким причинам, во-первых, это можно сделать таким образом (в отличие от того, что вы пытаетесь сделать), а во-вторых, если процесс будет поврежден, вы можете сбросить без потери исходного файла. Теперь вы можете обновить определенное местоположение файла с помощью RandomAccessFile, но это обычно делается (по моему опыту), когда вы имеете дело с записями фиксированного размера, а не с типичными текстовыми файлами.

person Hovercraft Full Of Eels    schedule 25.06.2011
comment
Это хороший момент. Ваш совет принят. Еще один вопрос: для создания временного файла я бы использовал createTempFile или просто конструктор с другим именем файла? (Извините, если это покажется глупым вопросом, я новичок в вводе-выводе с файлами.) - person Shelley; 25.06.2011
comment
@Shelley: вы ознакомились с учебными пособиями по Java, разделом ввода-вывода? Если нет, советую посмотреть: Базовый ввод-вывод< /а> - person Hovercraft Full Of Eels; 25.06.2011