深入浅出 对象序列化EOF异常(java.io.EOFException)

  • Post author:
  • Post category:java




1. 说明


本文有一些个人观点,如果有异议/更好的建议,可以在下面评论,或者联系我canliture#outlook.com(#改为@)

  • 如果你对象流不是很明白的,可以先看看


    The Java™ Tutorials——(2)Essential Classes——Basic I/O 之 7. 对象流(Object Streams)


    的讲述,链接中给出了一些程序例子,很容易理解。

  • 这里描述的

    java.io.EOFException异常

    是在

    对象流

    (也就是

    ObjectInputStream,ObjectOutputStream

    )的使用过程中,抛出的。


  • 对象流中引发的EOF异常

    可以尝试着本文寻找解决方案。当然

    其它环境下的EOF异常

    或许也能够从本文中找到

    解决方法的思路



2. 一个简单问题的引发的深入思考


下面给出一个有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行报错

现在为了看清这个问题,我们先看看一下下面的例子:



3. FileInputStream和ObjectInputStream对读取空文件的应对策略



首先运行程序前,保证项目目录下,没有

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





3. 初步查找错误

我们现在回到最初的程序,它的目的无非就是

使用对象流 向文件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(),我们发现仍然是和最初一样的错误。因为我们改的只是后面的错误,最开始的错误仍然没有解决:




4. 深入

调用栈/JDK源码

查找

问题根源



4.1 ObjectInputStream构造函数解析

现在我们找到最初错误的地方,找到

程序异常的调用栈

// 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.<init>(ObjectInputStream.java:358)

,发现

抛出错误的地方是一个函数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清空文件,之后开始写入数据,最后读取出来,非常顺利地运行。



4.2. readStreamHeader()源码解析

下面我们再分析一下上面提到的

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()函数来读取数据的。



5. ObjectInputStream问题解读汇总

好了,通过递归调用栈,我们已经找到了

最终错误异常抛出的地方

了。

// 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();



}



6. ObjectOutputStream问题解读

看懂了

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的先后顺序,实例化对象流



7. 对象流 EOFException 必坑指南




  • 对象流不同于普通的字节流,当对象流中没有数据时,程序却尝试读取数据,会报EOFException;而字节流就不会出现这种情况,字节流会返回-1





  • ObjectInputStream写入的数据,在ObjectOutputStream上读取时,应该按照相同的数据类型依次读取,否则数据类型不等会抛出EOFException





  • 最好在实际使用的过程中,我们先实例化ObjectOutputStream,再实例化 ObjectInputStream,这是由这两个类的设计思想所决定的。如此能保证在同一资源的对象流ObjectInputStream能够及时读取到序列化头而不至于阻塞或者引发EOF异常(阻塞对应于Socket IO,EOF异常对应于文件IO)




版权声明:本文为qq_37206105原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。