本文有一些个人观点,如果有异议/更好的建议,可以在下面评论,或者联系我canliture#outlook.com(#改为@)
如果你对象流不是很明白的,可以先看看The Java™ Tutorials——(2)Essential Classes——Basic I/O 之 7. 对象流(Object Streams)的讲述,链接中给出了一些程序例子,很容易理解。
这里描述的java.io.EOFException异常
是在对象流
(也就是ObjectInputStream,ObjectOutputStream
)的使用过程中,抛出的。
对象流中引发的EOF异常
可以尝试着本文寻找解决方案。当然其它环境下的EOF异常
或许也能够从本文中找到解决方法的思路
。
下面给出一个有EOF异常问题的程序,本文就尝试着以探索的方式来解决此问题。
public static void main(String[] args) throws IOException {
File f0 = new File("kkk.out");
FileInputStream fis = null;
FileOutputStream fos = null;
ObjectInputStream dis = null;
ObjectOutputStream dos = null;
try{
if(!f0.exists())f0.createNewFile();
fos = new FileOutputStream(f0);
fis = new FileInputStream(f0);
// 1. 初始化Object流语句
dis = new ObjectInputStream(fis);
dos = new ObjectOutputStream(fos);
// 2. 写"对象"语句
dos.writeInt(1);
dos.writeObject(new Integer(3));
// 3. 读取,输出语句
System.out.println(dis.readInt() + ","+ dis.readInt());
} catch (Exception e){
e.printStackTrace();
if(fos != null) fos.close();
if(fis != null) fis.close();
if(dos != null) dos.close();
if(dis != null) dis.close();
}
}
上面代码想传达的意思很简单:向使用对象流 向文件kkk.out 写入1,3两个数据,然后使用对象流读取出来这些数据并打印。
现在运行这段代码,发现报如下错误:java.io.EOFException
,它提示我们报错的那一行在这:
// 1. 初始化Object流语句
dis = new ObjectInputStream(fis); // 报错的就是这一行,第xx行
dos = new ObjectOutputStream(fos);
java.io.EOFException
at java.io.ObjectInputStream$PeekInputStream.readFully(ObjectInputStream.java:2681)
at java.io.ObjectInputStream$BlockDataInputStream.readShort(ObjectInputStream.java:3156)
at java.io.ObjectInputStream.readStreamHeader(ObjectInputStream.java:862)
at java.io.ObjectInputStream.<init>(ObjectInputStream.java:358)
at Test.main(Test.java:xx行) // 第xx行报错
现在为了看清这个问题,我们先看看一下下面的例子:
首先运行程序前,保证项目目录下,没有kkk.out
这个文件。
public static void main(String[] args) throws IOException {
File file = new File("kkk.out");
FileInputStream is = null;
try {
if(!file.exists()) file.createNewFile();
is = new FileInputStream(file);
int i = is.read();
System.out.println(i);
} catch (IOException e) {
e.printStackTrace();
if(is != null) is.close();
}
}
运行程序,通过平常的学习,很容易知道,输出为-1,因为没有数据在新创建的文件里面,FileInputStream的read()函数返回-1。
那么我们再看看下面的例子。
同理, 在运行前,需要保证项目目录下,没有kkk.out
这个文件。
public static void main(String[] args) throws IOException, ClassNotFoundException {
File file = new File("kkk.out");
FileInputStream is = null;
ObjectInputStream ois = null;
try {
if(!file.exists()) file.createNewFile();
is = new FileInputStream(file);
ois = new ObjectInputStream(is);
int i = (Integer) ois.readObject();
System.out.println(i);
} catch (IOException e) {
e.printStackTrace();
if(is != null) is.close();
}
}
现在,我们运行程序,发现报错:java.io.EOFException
。
由此可见, 对象流不同于普通的字节流,当对象流中没有数据时,程序却尝试读取异常,会报EOF错误;而字节流就不会出现这种情况,字节流会返回-1
。
我们现在回到最初的程序,它的目的无非就是使用对象流 向文件kkk.out 写入1,3两个数据,然后使用对象流读取出来这些数据并打印。
这两个动作在同一个程序中发生,现在,我们将两个行为放到两个程序中看会不会出错?
先运行写入对象程序K1
:
public static void main(String[] args) throws IOException {
File f0 = new File("kkk.out");
FileOutputStream fos = null;
ObjectOutputStream dos = null;
try {
if (!f0.exists()) f0.createNewFile();
fos = new FileOutputStream(f0);
// 1. 初始化Object流语句
dos = new ObjectOutputStream(fos);
// 2. 写"对象"语句
dos.writeInt(1);
dos.writeObject(new Integer(3));
} catch (Exception e) {
e.printStackTrace();
if (dos != null) dos.close();
}
}
再运行读出对象程序K2
:
public static void main(String[] args) throws IOException {
File f0 = new File("kkk.out");
FileInputStream fis = null;
ObjectInputStream dis = null;
try {
if (!f0.exists()) f0.createNewFile();
fis = new FileInputStream(f0);
dis = new ObjectInputStream(fis);
// 2. 读取,输出语句
System.out.println(dis.readInt() + "," + dis.readInt());
} catch (Exception e) {
e.printStackTrace();
if (dis != null) dis.close();
}
}
我们发现,第一个写入程序无任何异常,第二个程序报错java.io.EOFException,错误提示为这一行代码: System.out.println(dis.readInt() + "," + dis.readInt());
。
显然,我们写入的是Integer,而读出来用readInt()肯定会出错;我们修改上面程序为readObject()发现没有任何错误
// 2. 读取,输出语句
System.out.println(dis.readInt() + "," + dis.readObject());
// 正常输出:
1,3
现在我们把最开始的程序也改为dis.readObject(),我们发现仍然是和最初一样的错误。因为我们改的只是后面的错误,最开始的错误仍然没有解决:
调用栈/JDK源码
查找问题根源
现在我们找到最初错误的地方,找到程序异常的调用栈
:
// 1. 初始化Object流语句
dis = new ObjectInputStream(fis); // 报错的就是这一行,第xx行
dos = new ObjectOutputStream(fos);
java.io.EOFException
at java.io.ObjectInputStream$PeekInputStream.readFully(ObjectInputStream.java:2681)
at java.io.ObjectInputStream$BlockDataInputStream.readShort(ObjectInputStream.java:3156)
at java.io.ObjectInputStream.readStreamHeader(ObjectInputStream.java:862)
at java.io.ObjectInputStream.<init>(ObjectInputStream.java:358)
at Test.main(Test.java:xx行) // 第xx行报错
现在我们也不知道怎么解决这个问题,我们看看错误到底怎么出现的吧。我们按照异常调用栈来研究一下
。我们首先看看错误中的at java.io.ObjectInputStream.
,发现抛出错误的地方是一个函数readStreamHeader();
我们等会儿再研究readStreamHeader();
函数,现在我们研究一下ObjectInputStream构造函数:
/**
* Creates an ObjectInputStream that reads from the specified InputStream.
* A serialization stream header is read from the stream and verified.
* This constructor will block until the corresponding ObjectOutputStream
* has written and flushed the header.
* ...// 第二段就不列出来了,对问题的讨论没啥影响
*/
public ObjectInputStream(InputStream in) throws IOException {
verifySubclass();
bin = new BlockDataInputStream(in);
handles = new HandleTable(10);
vlist = new ValidationList();
serialFilter = ObjectInputFilter.Config.getSerialFilter();
enableOverride = false;
readStreamHeader(); // 这一行在异常调用栈中,
bin.setBlockDataMode(true);
}
上面的函数看不懂没啥关系,对问题的讨论没啥影响。 我们只需要弄清楚函数的注释和找到readStreamHeader();函数即可
ObjectInputStream构造函数的注释中有这么一段话,是非常重要的!:
ObjectInputStream构造函数会从传入的InputStream来读取数据。首先会读取序列化流的头部(serialization stream header)并验证头部。此构造器会一直地"阻塞",直到与之对应的ObjectOutputStream写入或者了序列化头部。
文档注释中的所说的"阻塞"并不是完全正确的!!!,这个我们最后会提到。
而fos = new FileOutputStream(f0);
这句代码,我们看看FileOutputStream的构造函数,构造函数调用的是this(file, false);而false的意思是append追加的意思,也就是说,默认是不追加的。
那么:使用FileOutputStream(File file)实例化一个FileOutputStream导致的结果就是此文件首先被清空。
也就是说, 在实例化ObjectInputStream之前,我们就已经把文件清空了(不管文件之前是否存在,是否有数据)
public FileOutputStream(File file) throws FileNotFoundException {
this(file, false);
}
public FileOutputStream(File file, boolean append){
// ... 省略
}
现在我们可以做一个小实验来验证我们的猜想,首先我们写入对象程序K1
,先把数据写进去,然后我们把程序代码顺序稍作修改:
// 1. 初始化Object流语句
dis = new ObjectInputStream(fis);
System.out.println("Sleep Start...");
TimeUnit.SECONDS.sleep(3);
System.out.println("Sleep Exit...");
// 注意这里,我们把FileOutputStream和ObjectOutputStream的
// 初始化放在ObjectInputStream初始化后面
fos = new FileOutputStream(f0);
dos = new ObjectOutputStream(fos);
// 2. 写"对象"语句
dos.writeInt(1);
dos.writeObject(new Integer(3));
//2. 读取,输出语句
System.out.println(dis.readInt() + "," + dis.readObject());
运行结果,发现程序能够正常运行输出:
Sleep Start...
Sleep Exit...
1,3
上面的程序先构造ObjectInputStream
,而我们已经运行过写入对象程序K1
,文件里面已经有数据了,那么序列化头部也一定在里面
,所有,初始化没任何问题。接下来实例化FileOutputStream,ObjectOutputStream清空文件,之后开始写入数据,最后读取出来,非常顺利地运行。
下面我们再分析一下上面提到的readStreamHeader();
,我们研究研究它的代码:
/**
* The readStreamHeader method is provided to allow subclasses to read and
* verify their own stream headers. It reads and verifies the magic number
* and version number.
* // 其它注释信息省略
*/
protected void readStreamHeader() throws IOException, StreamCorruptedException {
short s0 = bin.readShort(); // 分析异常抛出调用栈,这里是程序出错的那一行
short s1 = bin.readShort();
if (s0 != STREAM_MAGIC || s1 != STREAM_VERSION) {
throw new StreamCorruptedException(
String.format("invalid stream header: %04X%04X", s0, s1));
}
}
首先看注释,注释很重要!!
:
readStreamHeader函数用来给子类读取并验证流的头部。头部有两个字段:
magic number
和
version number
通过看源码我们直到这两个字段就是s0和s1:
short s0 = bin.readShort(); // 分析异常抛出调用栈,这里是程序出错的那一行
short s1 = bin.readShort();
好了,我们现在再往更深层次的异常调用栈进一步吧——研究一下readShort()
。
public short readShort() throws IOException {
if (!blkmode) {
pos = 0;
in.readFully(buf, 0, 2);// 分析异常抛出调用栈,这里是程序出错的那一行
} else if (end - pos < 2) {
return din.readShort();
}
short v = Bits.getShort(buf, pos);
pos += 2;
return v;
}
这里没有啥好研究的,就是通过readFully读取两个字节(short),我们再看看更深层次的异常抛出调用栈——readFully():
void readFully(byte[] b, int off, int len) throws IOException {
int n = 0;
while (n < len) {
int count = read(b, off + n, len - n);
if (count < 0) {
throw new EOFException();
}
n += count;
}
}
这里的read(b, off + n, len - n);
它最终是通过底层的InputStream也就是最初传入ObjectInputStream构造函数的InputStream调用read()函数来读取数据的。
好了,通过递归调用栈,我们已经找到了最终错误异常抛出的地方
了。
// 1. 初始化Object流语句
dis = new ObjectInputStream(fis); // 报错的就是这一行,第xx行
dos = new ObjectOutputStream(fos);
java.io.EOFException
at java.io.ObjectInputStream$PeekInputStream.readFully(ObjectInputStream.java:2681)
at java.io.ObjectInputStream$BlockDataInputStream.readShort(ObjectInputStream.java:3156)
at java.io.ObjectInputStream.readStreamHeader(ObjectInputStream.java:862)
at java.io.ObjectInputStream.<init>(ObjectInputStream.java:358)
at Test.main(Test.java:xx行) // 第xx行报错
通过递归调用栈的分析,我们能够找到错误的底层原因
了:
实例化ObjectInputStream(InputStream)时,会首先从传入的InputStream中读取两个short字节的序列化头部字段:magic number和version number这两个字段并验证。如果与之对应的ObjectOutputStream还没将序列化头部字段写入,那么ObjectInputStream构造函数会一直"阻塞"。
文档注释中的阻塞并不是完全正确的!!!
为啥不完全正确?从实验结果来看,根本没有任何阻塞的迹象,只有异常现象。
这里我们需要知道,对于Socket对应的InputStream来说,它的read()函数是一个阻塞函数,必须等"服务端"发送数据过来read()才能返回。然而对于FileInputStream来说,我们上面的例子讲过,它是非阻塞,如果流中存在数据,则返回读取的数据,没有则返回-1,而这正是抛出EOFException的根本原因:
// readFully源码中抛出EOFException
int count = read(b, off + n, len - n);
if (count < 0) {
throw new EOFException();
}
看懂了ObjectOutputStream
,还不算真正地理解。最后,我们来看看ObjectOutputStream
,懂了这个,我们才能真正地知道EOF问题怎么发生的,并且改正程序使程序避免出这样的错误。
我们看看ObjectOutputStream构造函数:
/**
* Creates an ObjectOutputStream that writes to the specified OutputStream.
* This constructor writes the serialization stream header to the
* underlying stream; callers may wish to flush the stream immediately to
* ensure that constructors for receiving ObjectInputStreams will not block
* when reading the header.
* // 省略对问题讨论来说并不重要的注释
*/
public ObjectOutputStream(OutputStream out) throws IOException {
// ... 省略部分代码
writeStreamHeader();
// ... 省略部分代码
}
先看注释!
ObjectOutputStream(OutputStream)构造函数创建一个ObjectOutputStream,此对象流写数据到传入的OutputStream流中。构造函数会首先立即写序列化头部到OutputStream中,确保构造函数的用户(调用者)不会因为读不到序列化头部而“阻塞”
再次说明,这里的“阻塞”一词并不完全正确
显然,通过这个注释,就暗示了我们:
最好在实际使用的过程中,我们先实例化ObjectOutputStream,再实例化 ObjectInputStream,保证在在同一资源的对象流ObjectInputStream能够及时读取到序列化头而不至于阻塞或者引发EOF异常(阻塞对应于Socket IO,EOF异常对应于文件IO)
我们再看看ObjectOutputStream的writeStreamHeader();这个从名字来看,与ObjectInputStream中的readStreamHeader();是配套的。
由此我们不得不赞叹类的设计者,这不就跟跟网络协议类似嘛?协议标准制定者规定双方需要遵循一定的数据交流协议,而此协议的精髓主要就体现在协议头部,在这里就是序列化流头部(serialization stream header)。
废话少说,继续看writeStreamHeader()源码
/**
* The writeStreamHeader method is provided so subclasses can append or
* prepend their own header to the stream. It writes the magic number and
* version to the stream.
*
* @throws IOException if I/O errors occur while writing to the underlying
* stream
*/
protected void writeStreamHeader() throws IOException {
bout.writeShort(STREAM_MAGIC);
bout.writeShort(STREAM_VERSION);
}
既然"协议"是配套的
,那么这里writeStreamHeader也就很容易理解
了,写两个头部字段magic number
和version
到底层的OutputStream流中
。
好了,我们最终的问题就解决了。
dis.readInt() 改为 dis.readObject()
按ObjectOutputStream,ObjectInputStream的先后顺序,实例化对象流
对象流不同于普通的字节流,当对象流中没有数据时,程序却尝试读取数据,会报EOFException;而字节流就不会出现这种情况,字节流会返回-1
ObjectInputStream写入的数据,在ObjectOutputStream上读取时,应该按照相同的数据类型依次读取,否则数据类型不等会抛出EOFException
最好在实际使用的过程中,我们先实例化ObjectOutputStream,再实例化 ObjectInputStream,这是由这两个类的设计思想所决定的。如此能保证在同一资源的对象流ObjectInputStream能够及时读取到序列化头而不至于阻塞或者引发EOF异常(阻塞对应于Socket IO,EOF异常对应于文件IO)