详解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中低位向高位扩展是补符号位扩展,高位向低位强转是高位截断,低位保留

  • 4
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值