Bootstrap

关于BufferedInputStream的read(byte b[], int off, int len)方法读取文件部分出现的问题

  • 今天在开发的过程中,我们在测试二进制文件读取的时候,用了这样一个方法:
public static byte[] readBinaryFile(boolean bIsReadRecode, String strFileName, int iOffset, int iLength) throws IOException {
        byte[] byteArrayData = null;
        FileInputStream cFileInputStream = null;
        BufferedInputStream in = null;

        try {
            File cFile = new File(strFileName);
            if (cFile.exists() == false) {
                return byteArrayData;
            }
            
            cFileInputStream =new FileInputStream(cFile);
            in = new BufferedInputStream(cFileInputStream);

            if (bIsReadRecode == true) {
                byte[] byteArrayTemp = new byte[iLength];
                int iSize = in.read(byteArrayTemp, 0, iLength);
                if (iSize == iLength) {
                    byteArrayData = byteArrayTemp;
                }
            }
            else {
                byteArrayData = in.readAllBytes();
            }
            
        } catch (FileNotFoundException e) {
            logger.error("ReadBinaryFile:" + strFileName + "  " + e);
        } catch (IOException e) {
            logger.error("ReadBinaryFile:" + strFileName + "  " + e);
        } finally {
            if (cFileInputStream != null) {
                cFileInputStream.close();
                cFileInputStream = null;
            }
            if (in != null) {
                in.close();
                in = null;
            }
        }

        return byteArrayData;
    }

相信大家都可以看懂这段代码的含义,这段代码是读取一个文件,给定了4个参数,bIsReadRecode是否读取全部内容(false为读取全部内容,true读取部分),strFileName是文件名字,就是读取哪一个文件,iOffset是下标开始的位置,len是读取内容的长度。
下面是测试函数

@Test
	public void testReadBinaryFile() throws IOException {
		// 定义测试文件名和测试数据
		String fileName = "test.bin";
		byte[] testData = {0x11, 0x22, 0x33, 0x44, 0x55};

		// 写入测试数据到测试文件
		FileOutputStream out = null;
		try {
			out = new FileOutputStream(fileName);
			out.write(testData);

		} catch (IOException e) {
			e.printStackTrace();
			fail("Failed to write test data to file");
		}finally {
			out.close();
		}

		// 测试读取整个文件内容
		byte[] result = readBinaryFile(false, fileName, 0, 0);
		assertNotNull(result);
		assertArrayEquals(testData, result);
		System.out.println("result[0]==========>"+result[0]+"\n"+"result[1]==========>"+result[1]+"\n"+"result[2]==========>"+result[2]+"\n"+"result[3]==========>"+result[3]+"\n"+"result[4]==========>"+result[4]);
		// 测试读取部分文件内容

		result = readBinaryFile(true, fileName, 1, 3);
		for (int i = 0; i < result.length; i++) {
			System.out.print(result[i]+"\t");
		}
		assertNotNull(result);
		assertEquals(3, result.length);
		assertEquals(testData[1], result[0]);
		assertEquals(testData[2], result[1]);
		assertEquals(testData[3], result[2]);
		// 删除测试文件
		File file = new File(fileName);
		file.delete();
	}

**运行这个测试函数后,我们发现了测试读取全部内容的话是没问题的,但是读取部分内容从下标1开始,读取长度为3的数组时,给我们返回了[0,17,34,51,0],这就很奇怪了,按理说应该是[34,51,68]。
问题出现接下来就是排查问题,我们首先先进入BufferedInputStream的read()源码查看说明。

关于BufferedInputStream

BufferedInputStream类是一个带有缓冲区的输入流,通过缓冲来提高读取数据的效率。

BufferedInputStream的作用是为其它输入流提供缓冲功能。创建BufferedInputStream时,我们会通过它的构造函数指定某个输入流为参数。BufferedInputStream会将该输入流数据分批读取,每次读取一部分到缓冲中;操作完缓冲中的这部分数据之后,再从输入流中读取下一部分的数据。
为什么需要缓冲呢?原因很简单,效率问题!缓冲中的数据实际上是保存在内存中,而原始数据可能是保存在硬盘或NandFlash等存储介质中;而我们知道,从内存中读取数据的速度比从硬盘读取数据的速度至少快10倍以上。


下面我们来看一下源码:
read方法用于从输入流中读取数据到缓冲区中,返回读取的字节数,如果已经到达流末尾,则返回-1。

    public synchronized int read(byte b[], int off, int len)
        throws IOException
    {
        getBufIfOpen(); // Check for closed stream
        if ((off | len | (off + len) | (b.length - (off + len))) < 0) {
            throw new IndexOutOfBoundsException();
        } else if (len == 0) {
            return 0;
        }

        int n = 0;
        for (;;) {
            int nread = read1(b, off + n, len - n);
            if (nread <= 0)
                return (n == 0) ? nread : n;
            n += nread;
            if (n >= len)
                return n;
            // if not closed but no bytes available, return
            InputStream input = in;
            if (input != null && input.available() <= 0)
                return n;
        }
    }

从源码中我们可以看到,该方法接收一个目标字节数组b及其偏移量off和要读取的字节数len,如果已经到达流末尾则返回-1,否则将缓冲区中的数据复制到目标字节数组中,并返回实际读取的字节数。如果要读取的字节数较小,将只从缓冲区中读取数据,避免频繁地访问输入流。该方法是线程安全的,因为它采用了synchronized关键字保证了同步性。

看过源码之后,我们发现,read()方法在执行的过程中,如果是读取部分代码,还是先会new一个原数组长度的数组,这就导致了会在下标为0和下标为4的地方填充0
知道了这里的问题,Java中为我们提供了一个skip方法,用于解决这个问题

public synchronized long skip(long n) throws IOException {
        getBufIfOpen(); // Check for closed stream
        if (n <= 0) {
            return 0;
        }
        long avail = count - pos;

        if (avail <= 0) {
            // If no mark position set then don't keep in buffer
            if (markpos <0)
                return getInIfOpen().skip(n);

            // Fill in buffer to save bytes for reset
            fill();
            avail = count - pos;
            if (avail <= 0)
                return 0;
        }

        long skipped = (avail < n) ? avail : n;
        pos += skipped;
        return skipped;
    }

通过调用这个方法,我们可以直接跳过指定的下标之前的值,直接从指定的下标开始new 新的数组。如下为修改之后的的代码。

public static byte[] readBinaryFile(boolean bIsReadRecode, String strFileName, int iOffset, int iLength) throws IOException {
        byte[] byteArrayData = null;
        FileInputStream cFileInputStream = null;
        BufferedInputStream in = null;

        try {
            File cFile = new File(strFileName);
            if (cFile.exists() == false) {
                return byteArrayData;
            }
            
            cFileInputStream =new FileInputStream(cFile);
            in = new BufferedInputStream(cFileInputStream);

            if (bIsReadRecode == true) {
                byte[] byteArrayTemp = new byte[iLength];
                in.skip(iOffset);
                int iSize = in.read(byteArrayTemp, 0, iLength);
                if (iSize == iLength) {
                    byteArrayData = byteArrayTemp;
                }
            }
            else {
                byteArrayData = in.readAllBytes();
            }
            
        } catch (FileNotFoundException e) {
            logger.error("ReadBinaryFile:" + strFileName + "  " + e);
        } catch (IOException e) {
            logger.error("ReadBinaryFile:" + strFileName + "  " + e);
        } finally {
            if (cFileInputStream != null) {
                cFileInputStream.close();
                cFileInputStream = null;
            }
            if (in != null) {
                in.close();
                in = null;
            }
        }

        return byteArrayData;
    }

到此问题就解决了,如有不足,请不吝赐教!

;