Добавление к ObjectOutputStream

Невозможно ли добавить к ObjectOutputStream?

Я пытаюсь добавить к списку объектов. Следующий фрагмент - это функция, которая вызывается всякий раз, когда задание завершено.

FileOutputStream fos = new FileOutputStream
           (preferences.getAppDataLocation() + "history" , true);
ObjectOutputStream out = new ObjectOutputStream(fos);

out.writeObject( new Stuff(stuff) );
out.close();

Но когда я пытаюсь его прочитать, я получаю только первый файл. Тогда я получаю java.io.StreamCorruptedException.

Чтобы читать, я использую

FileInputStream fis = new FileInputStream
        ( preferences.getAppDataLocation() + "history");
ObjectInputStream in = new ObjectInputStream(fis);    

try{
    while(true)
        history.add((Stuff) in.readObject());
}catch( Exception e ) { 
    System.out.println( e.toString() );
}

Я не знаю, сколько объектов будет присутствовать, поэтому я читаю, пока нет исключений. Из того, что Google говорит, что это невозможно. Мне было интересно, знает ли кто-нибудь способ?

Ответ 1

Вот трюк: подкласс ObjectOutputStream и переопределить метод writeStreamHeader:

public class AppendingObjectOutputStream extends ObjectOutputStream {

  public AppendingObjectOutputStream(OutputStream out) throws IOException {
    super(out);
  }

  @Override
  protected void writeStreamHeader() throws IOException {
    // do not write a header, but reset:
    // this line added after another question
    // showed a problem with the original
    reset();
  }

}

Чтобы использовать его, просто проверьте, существует ли файл истории или нет, и создайте экземпляр этого добавочного потока (в случае, если файл существует = мы append = нам не нужен заголовок) или исходный поток (в случае, если файл не существует = нам нужен заголовок).

Edit

Меня не устраивало первое название класса. Это лучше: он описывает "что это", а не "как это сделал"

Edit

Изменено имя еще раз, чтобы уточнить, что этот поток предназначен только для добавления к существующему файлу. Он не может использоваться для создания нового файла с данными объекта.

Edit

Добавлен вызов reset() после этого вопроса показал, что исходная версия, которая просто переопределила writeStreamHeader, чтобы быть не-оператором, при определенных условиях могла создать который не может быть прочитан.

Ответ 2

Как сообщает API, конструктор ObjectOutputStream записывает заголовок потока сериализации в базовый поток. И этот заголовок ожидается только один раз, в начале файла. Поэтому вызов

new ObjectOutputStream(fos);

несколько раз на FileOutputStream, который ссылается на тот же файл, будет записывать заголовок несколько раз и повреждать файл.

Ответ 3

Из-за точного формата сериализованного файла добавление действительно испортит его. Вы должны записать все объекты в файл как часть одного и того же потока, иначе он будет сбой при чтении метаданных потока при ожидании объекта.

Вы можете прочитать Спецификацию сериализации для более подробной информации или (проще) читать эту тему, где Roedy Green говорит в основном то, что я только что сказал.

Ответ 4

Самый простой способ избежать этой проблемы - сохранить OutputStream открытым при записи данных, а не закрывать его после каждого объекта. Вызов reset() может быть целесообразным избежать утечки памяти.

Альтернативой было бы прочитать файл как серию последовательных ObjectInputStreams. Но это требует от вас подсчитывать, сколько байтов вы читаете (это может быть реализовано с помощью FilterInputStream), затем закройте InputStream, откройте его снова, пропустите это много байтов и только затем оберните его в ObjectInputStream().

Ответ 5

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

Ответ 6

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

import java.io.DataOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.io.OutputStream;

public class AppendableObjectOutputStream extends ObjectOutputStream {

    private boolean append;
    private boolean initialized;
    private DataOutputStream dout;

    protected AppendableObjectOutputStream(boolean append) throws IOException, SecurityException {
        super();
        this.append = append;
        this.initialized = true;
    }

    public AppendableObjectOutputStream(OutputStream out, boolean append) throws IOException {
        super(out);
        this.append = append;
        this.initialized = true;
        this.dout = new DataOutputStream(out);
        this.writeStreamHeader();
    }

    @Override
    protected void writeStreamHeader() throws IOException {
        if (!this.initialized || this.append) return;
        if (dout != null) {
            dout.writeShort(STREAM_MAGIC);
            dout.writeShort(STREAM_VERSION);
        }
    }

}

Этот класс может использоваться как прямая расширенная замена для ObjectOutputStream. Мы можем использовать класс следующим образом:

import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;

public class ObjectWriter {

    public static void main(String[] args) {

        File file = new File("file.dat");
        boolean append = file.exists(); // if file exists then append, otherwise create new

        try (
            FileOutputStream fout = new FileOutputStream(file, append);
            AppendableObjectOutputStream oout = new AppendableObjectOutputStream(fout, append);
        ) {
            oout.writeObject(...); // replace "..." with serializable object to be written
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }

    }

}

Ответ 7

Использование ObjectOutputStream oos.reset() перед записью также работает хорошо.