本文索引
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)