欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

谈谈Java中使用DataInputStream与DataOutputStream的雷区

程序员文章站 2022-05-11 10:09:53
(本人新手,如果有误,请不吝赐教) 首先,讲下DataInputStream、DataOutputStream这两个类,它们是对输入输出流的再一层封装,封装了一些按照一定格式读取或写入指定类型的数据,比如今天要讲的readInt(),它将从输入流中读取4个字节并返回一个int类型的数据。 今天之所以 ......

(本人新手,如果有误,请不吝赐教)

首先,讲下DataInputStream、DataOutputStream这两个类,它们是对输入输出流的再一层封装,封装了一些按照一定格式读取或写入指定类型的数据,比如今天要讲的readInt(),它将从输入流中读取4个字节并返回一个int类型的数据。

今天之所以写这篇文章,是因为今天我在读取一张bmp图片时候遇到的问题。下面贴代码(C代码)

    FILE *f; //文件指针
    fopen_s(&f,"C:\\Users\\IVAN\\Desktop\\1.bmp", "r"); //由于安全检查只能使用这个API,原型是fopen
    if (f == NULL) //判断是否打开
        return 0;
    fseek(f, 0x0012, SEEK_SET);  //偏移18位,接下来就是这张图片的长度和宽度,想了解的可以去研究下bmp图片的编码
    int height,width;
    fread(&width, sizeof(int), 1, f); //读取宽度
    fread(&height, sizeof(int), 1, f);//读取长度
    printf("width %d,height %d\n", width, height);//打印
    fclose(f); //释放

这段代码作用是读取一张名为1.bmp的图片长度及宽度。
下面java的代码:

    public static void main(String[] args) throws IOException {
        File file = new File("C:\\Users\\IVAN\\Desktop\\1.bmp");
        FileInputStream fin = new FileInputStream(file);
        byte[] bytes = new byte[(int) file.length()];
        fin.skip(0x0012);
        //以上操作就是打开输入流,并偏移18位
        DataInputStream din = new DataInputStream(fin);//今天讨论的对象
        int weight = din.readInt(); //使用readInt()读入一个int类型的数据,也是本次讨论的重点
        System.out.println("weight " + weight);

        fin.close();
    }

以上两段代码结果是不同的(并不一定是不同的,原因后面会讲),以下是结果截图(C程序输出的结果是正确的):
谈谈Java中使用DataInputStream与DataOutputStream的雷区
谈谈Java中使用DataInputStream与DataOutputStream的雷区


为什么会不同呢?我们在学习c的时候,老师或者考试一定都有问过int的长度是16或者32位的(具体大小跟你使用的机器和编译器有关),当我们在c里面调用sizeof(int)会发现这个API返回的值是4,其实这个4代表的是一个int型的变量占用4个字节(byte)。为什么要扯这个问题呢?其实今天的重点就是机器是如何把这4个字节转化成int类型的数据。先看看下面的两段源码:

    //DataOutputStream.java
    ...
    
    /**
     * Writes an <code>int</code> to the underlying output stream as four
     * bytes, high byte first. If no exception is thrown, the counter
     * <code>written</code> is incremented by <code>4</code>.
     *
     * @param      v   an <code>int</code> to be written.
     * @exception  IOException  if an I/O error occurs.
     * @see        java.io.FilterOutputStream#out
     */
    public final void writeInt(int v) throws IOException {
        out.write((v >>> 24) & 0xFF);
        out.write((v >>> 16) & 0xFF);
        out.write((v >>>  8) & 0xFF);
        out.write((v >>>  0) & 0xFF);
        incCount(4);
    }
    ...
    
    //DataInputStream.java
    ...
    
    /**
     * See the general contract of the <code>readInt</code>
     * method of <code>DataInput</code>.
     * <p>
     * Bytes
     * for this operation are read from the contained
     * input stream.
     *
     * @return     the next four bytes of this input stream, interpreted as an
     *             <code>int</code>.
     * @exception  EOFException  if this input stream reaches the end before
     *               reading four bytes.
     * @exception  IOException   the stream has been closed and the contained
     *             input stream does not support reading after close, or
     *             another I/O error occurs.
     * @see        java.io.FilterInputStream#in
     */
    public final int readInt() throws IOException {
        int ch1 = in.read();
        int ch2 = in.read();
        int ch3 = in.read();
        int ch4 = in.read();
        if ((ch1 | ch2 | ch3 | ch4) < 0)
            throw new EOFException();
        return ((ch1 << 24) + (ch2 << 16) + (ch3 << 8) + (ch4 << 0));
    }
    ...

当读取int类型的数据时,我们先找到这个数据的地址,读取长度为4个字节的数据,将其****做处理****后就得到了我们想要的数据。为什么使用DataInputStream的readInt()方法得到了不正确的结果呢?

重点就在"做处理"这个步骤,DataInputStream/DataOutputStream固定按照[b1][b2][b3][b4]的顺序输入输出,这样做并没有问题,并且保证使用DataOutputStream/DataInputStream的输出输入得到的数据是一致的,但是从最开始的那两个例子来看,结果并不正确。

原因在于运行c代码时,是直接将数据写入内存,虽然int数据类型存放空间长度是一致的,但是存放的顺序却是相反的。这个过程是由CPU决定的。

大多数计算机按正向顺序存储一个数,Intel CPU按逆向顺序存储一个数,因此,如果试图将基于Intel CPU的计算机连到其它类型的计算机上,就可能会引起混乱。
一个32位的数占4个字节的存储空间,如果我们按有效位从高到低的顺序,分别用Mm,Ml,Lm和Ll表示这4个字节,那么可以有4!(4的阶乘,即24)种方式来存储这些字节。在过去的这些年中,人们在设计计算机时,几乎用遍了这24种方式。然而,时至今天,只有两种方式是最流行的,一种是(Mm,MI,Lm,LD,也就是高位优先顺序,另一种是(Ll,Lm,Ml,Mm),也就是低位优先顺序。和存储16位的数一样,大多数计算机按高位优先顺序存储32位的数,但基于Intel CPU的计算机按低位优先顺序存储32位的数。
"http://blog.sina.com.cn/s/blog_9e2e84050101dipx.html"

到现在可以解释为什么之前例子的结果可能是不同的。下面是一段用于验证的C代码:

    int show;
    char buf[4] = {80,0,0,0};
    memcpy_s(&show, sizeof(int),buf,4);
    printf("低位为80结果 %d\n", show);

    char buf2[4] = { 0,0,0,80 };
    printf("高位为80结果 %d\n", *(int*)buf2);
    
    system("pause");
    

谈谈Java中使用DataInputStream与DataOutputStream的雷区

我们可以对之前的java代码做出改造:

import java.io.*;

public class Main {
    public static void main(String[] args) throws IOException {
        File file = new File("C:\\Users\\IVAN\\Desktop\\1.bmp");
        FileInputStream fin = new FileInputStream(file);
        byte[] bytes = new byte[(int) file.length()];
        fin.skip(0x0012);
        DataInputStream din = new DataInputStream(fin);
        int weight = readInt(din);
        System.out.println("weight " + weight);

        fin.close();

    }

    public static int readInt(InputStream in) throws IOException {
        int ch4 = in.read();
        int ch3 = in.read();
        int ch2 = in.read();
        int ch1 = in.read();
        if ((ch1 | ch2 | ch3 | ch4) < 0)
            throw new EOFException();
        return ((ch1 << 24) + (ch2 << 16) + (ch3 << 8) + (ch4 << 0));
    }

}

结果正如我们所期望的:

谈谈Java中使用DataInputStream与DataOutputStream的雷区