最近做IO时,出现了一个我百思不得其解的错误。虽然经过一番,解决的bug,但是对于这一方面的底层知识还是有待去深入了解。借这个机会,好好学习一下。
一般,可以使用ObjectInputStream把对象写出到文件,再使用ObjectOutputStream把对象读取出来。我以为这样做完美无缺,就像使用FileInputStream和FileOutputStream一样。
直到我遇到这个问题:我以追加的形式写出,每次只写出一个对象,然后就关闭写出流。接着再重复写出一个对象。在读取时这个文件时,每次读一个对象,直到读到EOF。结果让我很意外,我只读取了一个对象就报错了。
大体意思的代码如下:
/* 以追加形式写入一个对象到文件中*/
ObjectOutputStream oos1 = new ObjectOutputStream(new FileOutputStream("test.txt", true));
String str1 = "a";
oos1.writeObject(str1);
oos1.flush();
oos1.close();
/* 再次以相同方式写入一个对象 */
ObjectOutputStream oos2 = new ObjectOutputStream(new FileOutputStream("test.txt", true));
String str2 = "b";
oos2.writeObject(str2);
oos2.flush();
oos2.close();
/* 读取刚才写入的两个对象 */
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("test.txt"));
String str3 = (String) ois.readObject();
String str4 = (String) ois.readObject();
System.out.println(str3 + str4);
ois.close();
控制台提示:
Exception in thread "main" java.io.StreamCorruptedException: invalid type code: AC
at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1601)
at java.io.ObjectInputStream.readObject(ObjectInputStream.java:431)
at Test.main(Test.java:28)
问题处在了第二次调用readObject()时:
String str4 = (String) ois.readObject();
要解决这个错误,首先要了解一个事实:
用对象流写到文件中时,首先会将流的头部信息写出,之后才开始写具体对象数据。
我们可以从ObjectOutputStream的源码(jdk1.8)中看到:
/* ObjectOutputStream的构造器 */
public ObjectOutputStream(OutputStream out) throws IOException {
verifySubclass();
bout = new BlockDataOutputStream(out);
handles = new HandleTable(10, (float) 3.00);
subs = new ReplaceTable(10, (float) 3.00);
enableOverride = false;
writeStreamHeader(); //注意这一行,对象流构造时,趁我们不注意偷偷写入头部信息
bout.setBlockDataMode(true);
if (extendedDebugInfo) {
debugInfoStack = new DebugTraceInfoStack();
} else {
debugInfoStack = null;
}
}
writeStreamHeader这个方法写入了两个short类型的数据:
protected void writeStreamHeader() throws IOException {
bout.writeShort(STREAM_MAGIC);
bout.writeShort(STREAM_VERSION);
}
两个short类型的意义:
/**
* Magic number that is written to the stream header.(标志流头部信息的魔数)
*/
final static short STREAM_MAGIC = (short)0xaced;
/**
* Version number that is written to the stream header.(流信息的版本号)
*/
final static short STREAM_VERSION = 5;
在使用十六进制打开之前对象写入的文件test.txt时,发现里面确实包含有这两个数:
官方文档也说明了输出流的信息是由头部信息和数据组成的:
Primitive data, excluding serializable fields and externalizable data, is written to the ObjectOutputStream in block-data records. A block data record is composed of a header and data. The block data header consists of a marker and the number of bytes to follow the header.
总结一下错误的原因:
ObjectOutputStream在构造时,首先会将流的头部信息(“AC ED 00 05”)写入到文件中。然后在调用write时,写入其他数据,直到关闭。当再次使用ObjectOutputStream追加写入对象时,头部信息又会再次写入。所以当你用ObjectInputStream来读取对象时,流虽然能够将第一个头部信息(“AC ED 00 05”)跳过,但是其他头部信息会当做数据来处理,造成无法解析,所以读取会出现StreamCorruptedException: invalid type code: AC错误。
了解了上述原理后,也就大概有了两种解决思路:
第一种方法比较笨,容易出错,但是实现较为简单。
/* 读取刚才写入的两个对象 */
FileInputStream fs = new FileInputStream("test.txt");
ObjectInputStream ois = new ObjectInputStream(fs);
String str3 = (String) ois.readObject();
fs.skip(4); //跳过两次写入过程中间的头部信息
// ois.skip(4); //如果使用ois的方法依然会有错误,希望有大神解惑
String str4 = (String) ois.readObject();
System.out.println(str3 + str4);
ois.close();
第二种方法需要继承ObjectOutputStream和ObjectInputStream来自定义实现类,但是要特别注意JDK的版本信息。
class MyObjectOutputStream extends ObjectOutputStream {
public MyObjectOutputStream(OutputStream out) throws IOException {
super(out);
}
@Override
protected void writeStreamHeader() throws IOException {
//重写读取头部信息方法:不写入头部信息
super.reset();
}
}
class MyObjectInputStream extends ObjectInputStream {
public MyObjectInputStream(InputStream in) throws IOException {
super(in);
}
@Override
protected void readStreamHeader() throws IOException {
//重写读取头部信息方法:什么也不做
}
}
测试:
/* 以追加形式写入一个对象到文件中*/
MyObjectOutputStream oos1 = new MyObjectOutputStream(new FileOutputStream("test.txt", true));
String str1 = "a";
oos1.writeObject(str1);
oos1.flush();
oos1.close();
/* 再次以相同方式写入一个对象 */
MyObjectOutputStream oos2 = new MyObjectOutputStream(new FileOutputStream("test.txt", true));
String str2 = "b";
oos2.writeObject(str2);
oos2.flush();
oos2.close();
/* 读取刚才写入的两个对象 */
MyObjectInputStream ois = new MyObjectInputStream(new FileInputStream("test.txt"));
String str3 = (String) ois.readObject();
String str4 = (String) ois.readObject();
System.out.println(str3 + str4);
ois.close();