我們都知道java中io操作分爲字節流和字符流,對於字節流,顧名思義是按字節的方式讀取數據,所以我們常用字節流來讀取二進制流(如圖片,音樂等文件)。問題是爲什麼字節流中定義的read()方法返回值爲int類型呢?既然它一次讀出一個字節數據爲什麼不返回byte類型呢?
網上的說法不能說它是錯誤的,只是我感覺沒有解釋清楚,接下來我們以FileInputStream /FileOutputStream和BufferedInputStream/BufferedOutputStream爲例,這兩個來解釋一下爲什麼是這樣的,以及中間實現過程是什麼。
一、我們以FileInputStream/FileOutputStream角度來說,這個主要是與調用的native方法有關,想知道必須看native層怎麼實現。
FileInputStream中read()源碼:
public int read() throws IOException {
return read0();
}
private native int read0() throws IOException;
這裏我們是看不出爲什麼read()方法爲什麼要返回int類型的,所以我們深入點,看Native層代碼:
/////////////////////////////////////////////////////////////////////
// FilelnputStream.c
JNIEXPORT jint JNICALL
Java_java_io_FileInputStream_read0(JNIEnv *env, jobject this) {
return readSingle(env, this, fis_fd);
}
////////////////////////////////////////////////////////////////////
// io_util.c文件
jint
readSingle(JNIEnv *env, jobject this, jfieldID fid) {
jint nread;
char ret;
FD fd = GET_FD(this, fid);
if (fd == -1) {
JNU_ThrowIOException(env, "Stream Closed");
return -1;
}
// 調用IO_Read
nread = IO_Read(fd, &ret, 1);
if (nread == 0) { /* EOF */
return -1;
} else if (nread == -1) { /* error */
JNU_ThrowIOExceptionWithLastError(env, "Read error");
}
return ret & 0xFF;
}
這裏Java_java_io_FileInputStream_read0方法就是JDK中read0()調用的native方法
可以看到這裏,我們重點關注的是如果讀取結束,則返回一個int類型的-1,否則的話就是返回ret(這裏是一個byte數據)返回的是byte,而& 0xFF是爲了保證由byte 類型向上拓展成int的時候,不進行符號拓展,而是0拓展。
以上就是實現過程,相信看到這裏大家也知道爲什麼Java IO流的read()返回的是int而不是byte了,因爲底層對byte進行了int擴展
那麼爲什麼這麼做呢,我們知道字節輸入流可以操作任意類型的文件,比如圖片音頻等,這些文件底層都是以二進制形式的存儲的,如果每次讀取都返回byte,會有可能在讀到111111111,而11111111是byte類型的-1,程序在遇到-1就會停止讀取,用int類型接收遇到11111111會在其前面補上24個0湊足4個字節,那麼byte類型的-1就變成int類型的255了,這樣可以保證整個數據讀完。而結束標記的-1是int類型的來作爲判斷的,像下面這樣。
(這裏我們要時刻明白一個道理,使用的-1這個結束標誌是通過一個判斷文件結尾的返回的,而不是輸入流讀到了一個-1,這裏就是爲了避免這種情況!)
利用是&0xFF進行0擴展的原因(計算機中存儲數據都是用的補碼形式,不懂的先去了解下)
1個字節8位,(byte) 4個字節32位,(int)
byte -1 —>int -1(將byte提升爲int)
byte 是1一個字節,即8位,如果取到連續11111111,爲了避免讀到連續8個1(就是-1)和定義的結束標記-1相同(read()返回-1就是讀到末尾)。所以在保留11111111的基礎上,在轉成int類型時,前面24位補0而不補1。
如果是補1(比如強轉) 11111111 11111111 11111111 11111111不還是-1?
所以前面24位補0,-1變成了255,既可以保留原字節數據不變(最低8位,配合write方法強轉),又可以避免-1的出現。
我們繼續看FileOutputStream裏面的write方法,也是調用了一個本地native方法。
public void write(int b) throws IOException {
write(b, fdAccess.getAppend(fd));
}
private native void write(int b, boolean append) throws IOException;
直接看Native層代碼:
////////////////////////////////////////////////////////////////////
// FileOutputStream.c
JNIEXPORT void JNICALL
Java_java_io_FileOutputStream_write(JNIEnv *env, jobject this, jint byte, jboolean append) {
// writeSingle:寫入單個字節
writeSingle(env, this, byte, append, fos_fd);
}
////////////////////////////////////////////////////////////////////
// io_util.c文件
void
writeSingle(JNIEnv *env, jobject this, jint byte, jboolean append, jfieldID fid) {
// Discard the 24 high-order bits of byte. See OutputStream#write(int)
// 這裏通過強轉處理了int的高24位字節,變成了一個字節
char c = (char) byte;
jint n;
// 獲取文件句柄(Windows)
FD fd = GET_FD(this, fid);
if (fd == -1) {
JNU_ThrowIOException(env, "Stream Closed");
return;
}
if (append == JNI_TRUE) {
// 向後追加
n = IO_Append(fd, &c, 1);
} else {
// 覆蓋寫入
n = IO_Write(fd, &c, 1);
}
if (n == -1) {
JNU_ThrowIOExceptionWithLastError(env, "Write error");
}
}
這裏就是將字節一個個的寫入,在寫入前在OutputStream.write(int)方法中通過強轉處理了int的高24位字節,實現原數據的還原
對這JDK有明確表示
所以這樣一來就保證了數據讀和寫的無差錯。這也是爲什麼JDK的OutputStream及其子類爲什麼方法write(int b)的入參類型是int的原因。(Java中有byte類型,c語言中沒有,可能也是用int的原因)
二、擴展:從BufferedInputStream/BufferedOutputStream角度來說
BufferedInputStream中的read()方法的實現
/**
* See
* the general contract of the <code>read</code>
* method of <code>InputStream</code>.
*
* @return the next byte of data, or <code>-1</code> if the end of the
* stream is reached.
* @exception IOException if this input stream has been closed by
* invoking its {@link #close()} method,
* or an I/O error occurs.
* @see java.io.FilterInputStream#in
*/
public synchronized int read() throws IOException {
if (pos >= count) {
fill();
if (pos >= count)
return -1;
}
return getBufIfOpen()[pos++] & 0xff;
}
private byte[] getBufIfOpen() throws IOException {
byte[] buffer = buf;
if (buffer == null)
throw new IOException("Stream closed");
return buffer;
}
從上面的源碼我們可以看到,getBufIfOpen()方法返回的byte[],也通過& 0xff進行了0擴展,即read()方法的最後一行把讀到的字節 0擴展成了int。實現了一個字節的數據的返回,如果達到流的末尾, 則返回-1(這裏是int類型)
而在BufferedOutputStream中的writer()方法果斷的又將int強制轉成了byte(截取後八位)
public synchronized void write(int b) throws IOException {
if (count >= buf.length) {
flushBuffer();
}
buf[count++] = (byte)b;
}
想必看到這裏,應該知道這樣做的原因吧:在用輸入流讀取一個byte數據時,有時會出現連續8個1的情況,這個值在計算機內部表示-1,正好符合了流結束標記。所以爲了避免流操作數據提前結束,將讀到的字節進行int類型的擴展。保留該字節數據的同時,前面都補0,避免出現-1的情況。
真正讀到文件最後結束是通過這句實現的:if (pos >= count) return -1;
我們使用的-1這個結束標誌是通過這句返回的,而不是輸入流讀到了一個-1。
最後總結一下,Java IO流中read()方法返回int而不是byte主要就是爲了避免流操作提前結束,要保證能完全讀取到數據。
P.S.小知識:Java中低位向高位擴展是補符號位擴展,高位向低位強轉是高位截斷,低位保留