詳解Java 字節流的read()方法返回int型而非byte型的原因

我們都知道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中低位向高位擴展是補符號位擴展,高位向低位強轉是高位截斷,低位保留

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章