Java IO之InputStream

  • Post author:
  • Post category:java


Java io部分的知识是比较重要的一部分内容,io是理解nio的基础,nio又是理解netty的基础。

相信看到java io体系的结构图的时候都会感叹他的庞大:

在网上查阅相关资料的时候,也没有一个很详细的理解,大部分都是陈列一下api的用法,所以在这里将自己对io的理解记录下来。

InputStream也就是io中的输入流,用来处理字节对象,也叫字节流,他将数据以字节的形式读取到内存中。

InputStream是一个抽象类,最主要的一个方法就是read()方法,无参就是一个字节一个字节的进行读取数据,使用byte[]作为参数时,则是将读取的字节保存到节数组中。

我们平时接触到的都是他的子类,这里主要通过以下几个类来学习InputStream。

  • FileInputStream

FileInputStream是用于接收文件数据的输入流,他可以读取文件内容到内存中。

看一下它的两个主要的构造方法:

  • 入参为文件完整的路径
public FileInputStream(String name) throws FileNotFoundException {
    this(name != null ? new File(name) : null);
}

  • 入参为File类型
public FileInputStream(File file) throws FileNotFoundException {
    String name = (file != null ? file.getPath() : null);
    SecurityManager security = System.getSecurityManager();
    if (security != null) {
        security.checkRead(name);
    }
    if (name == null) {
        throw new NullPointerException();
    }
    if (file.isInvalid()) {
        throw new FileNotFoundException("Invalid file path");
    }
    fd = new FileDescriptor();
    fd.incrementAndGetUseCount();
    this.path = name;
    open(name);
}

再看下

read()

public int read() throws IOException {
    Object traceContext = IoTrace.fileReadBegin(path);
    int b = 0;
    try {
        b = read0();
    } finally {
        IoTrace.fileReadEnd(traceContext, b == -1 ? 0 : 1);
    }
    return b;
}
private native int read0() throws IOException;

这是无参的read(),一次读取一个字节,方法返回一个int类型的数据,代表字节对应的ASCLL码。


read(byte[]):

public int read(byte b[]) throws IOException {
    Object traceContext = IoTrace.fileReadBegin(path);
    int bytesRead = 0;
    try {
        bytesRead = readBytes(b, 0, b.length);
    } finally {
        IoTrace.fileReadEnd(traceContext, bytesRead == -1 ? 0 : bytesRead);
    }
    return bytesRead;
}
private native int readBytes(byte b[], int off, int len) throws IOException;

和read()不同的是这个方法支持传入一个字节数组用来保存读取的字节,也就是说如果你声明了一个new byte[1024],那么每次读取的字节数就将是1024个,他们全都保存在这个字节数组中,同时该方法也会返回一个int,表示实际上读取到的字节数。

了解了上面这些,我们写一个实例来看下效果,随便写一个txt文件,用FileInputStream看看是否可以读取全部内容:

Txt内容:

就放五个字母 abcde来测试下

测试类:

package io;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;

/**
 * 测试inputStream中的read的字节
 */

/**
 * @author 18092106
 * @create 2018-08-31 15:18
 **/
public class TestInputStreamReadMethod {

    public void testReadOneByteOnce() {
        try(FileInputStream fis = new FileInputStream(new File("E:\\code\\leetcode\\read.txt"))){
            int hasRead = 0;
            int index = 0;
            while ((hasRead = fis.read()) != -1) {
                index++;
                System.out.println("read()方法第" + index + "次读取的字节是" + hasRead);
            }
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public void testReadBytesOnce(){
        try (FileInputStream fis = new FileInputStream(new File("E:\\code\\leetcode\\read.txt"))) {
            byte[] bytes = new byte[2];
            int hasRead = 0;
            int index = 0;
            while((hasRead = fis.read(bytes)) != -1){
                index++;
                System.out.print("read(byte [])第" + index + "次读取读取了" + hasRead + "个字节,byte[]中存放的字节数量是" + bytes.length + ",分别是:");
                for (byte b:bytes) {
                    System.out.print(b + " ");
                }
                System.out.println();
            }



        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }


    public static void main(String[] args) {
        new TestInputStreamReadMethod().testReadOneByteOnce();
        new TestInputStreamReadMethod().testReadBytesOnce();
    }
}

运行结果:

我们分别测试了两种方式,可以看到read()一共执行了5次才读取完文件,read(byte[])中我们用了一个byte[2]来存放数据,一共读了三次,最后一次实际上只读了一个字节,但是byte里面还是有两个字节,这又是为什么呢?

我们用一个图来简单解释下这个过程:

第一次读取a和b,a放入byte[0],b放入byte[1];

第二次读取c和d,c放入byte[0],d放入byte[1];

第三次读取e,e放入byte[0], byte[1]没有修改。

  • ByteArrayInputStream

ByteArrayInputStream从命名就可以看出它是用来接收字节数组类型的数据,他的用法和FileInputStream基本类似。

  • FilterInputStream(装饰器模式)

在java io流的设计中使用到了两种设计模式,这里的FilterInputStream就是用到其中的装饰器模式,我们看下他的源码:

protected FilterInputStream(InputStream in) {
    this.in = in;
}

这是它的构造函数,可以接收一个输入流作为参数

再看下read()方法:

public int read() throws IOException {
    return in.read();
}
public int read(byte b[]) throws IOException {
    return read(b, 0, b.length);
}
public int read(byte b[], int off, int len) throws IOException {
    return in.read(b, off, len);
}

可以看到,它并没有实现任何自己的方法,所有的方法都是调用的构造函数中传入的输入流的方法,真正的实现都在它的子类中,子类继承FilterInputStream,然后重写对应的方法,实现相应的功能,这就是装饰器起到的作用,我们也把这种需要其他节点流作为参数的流叫做处理流。

*装饰器模式:


装饰模式是在不必改变原类文件和使用继承的情况下,动态的扩展一个对象的功能。它是通过创建一个包装对象,也就是装饰来包裹真实的对象。

  • BufferedInputStream

BufferedInputStream就是继承了FilterInputStream的一个处理流,它需要一个节点流作为构造函数的参数才能发挥作用,目的是为了优化原有的节点流,他也被叫做字节缓冲流,我们一起看下BufferedInputStream的源码来学习一下:

构造方法:

public BufferedInputStream(InputStream in) {
    this(in, defaultBufferSize);
}
public BufferedInputStream(InputStream in, int size) {
    super(in);
    if (size <= 0) {
        throw new IllegalArgumentException("Buffer size <= 0");
    }
    buf = new byte[size];
}

可以看到它需要另一个输入流作为参数传入,也就是被装饰的对象,还有一个构造方法提供了一个int类型的参数,我们先放着,后面会介绍。


read(byte[]):

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;
    }
}

可以看到主要读取的方法是read1(),我们看下read1():

private int read1(byte[] b, int off, int len) throws IOException {
    int avail = count - pos;
    if (avail <= 0) {
        /* If the requested length is at least as large as the buffer, and
           if there is no mark/reset activity, do not bother to copy the
           bytes into the local buffer.  In this way buffered streams will
           cascade harmlessly. */
        if (len >= getBufIfOpen().length && markpos < 0) {
            return getInIfOpen().read(b, off, len);
        }
        fill();
        avail = count - pos;
        if (avail <= 0) return -1;
    }
    int cnt = (avail < len) ? avail : len;
    System.arraycopy(getBufIfOpen(), pos, b, off, cnt);
    pos += cnt;
    return cnt;
}

可以看到 这边有一个判断:

if (len >= getBufIfOpen().length && markpos < 0) {

我们看下getBuffIfOpen():

private byte[] getBufIfOpen() throws IOException {
    byte[] buffer = buf;
    if (buffer == null)
        throw new IOException("Stream closed");
    return buffer;
}

这里面的buf对象就是上面我们第二个构造函数的size构造出来的一个字节数组,如果没有传,那么使用默认的一个值:

private static int defaultBufferSize = 8192;

也就是一个大小为8192的字节数据 byte[8192],4K大小,我们继续看刚刚的判断:

	if (len >= getBufIfOpen().length && markpos < 0) {

这个len就是我们使用BufferedInputStream时自己构造的字节数据的大小,如果我们构造的大小比默认的大了,就使用传入的输入流自身的read(byte[])的方法,如果没有默认的大,那么会调用一个fill()方法:

private void fill() throws IOException {
    byte[] buffer = getBufIfOpen();
    if (markpos < 0)
        pos = 0;            /* no mark: throw away the buffer */
    else if (pos >= buffer.length)  /* no room left in buffer */
        if (markpos > 0) {  /* can throw away early part of the buffer */
            int sz = pos - markpos;
            System.arraycopy(buffer, markpos, buffer, 0, sz);
            pos = sz;
            markpos = 0;
        } else if (buffer.length >= marklimit) {
            markpos = -1;   /* buffer got too big, invalidate mark */
            pos = 0;        /* drop buffer contents */
        } else {            /* grow buffer */
            int nsz = pos * 2;
            if (nsz > marklimit)
                nsz = marklimit;
            byte nbuf[] = new byte[nsz];
            System.arraycopy(buffer, 0, nbuf, 0, pos);
            if (!bufUpdater.compareAndSet(this, buffer, nbuf)) {
                // Can't replace buf if there was an async close.
                // Note: This would need to be changed if fill()
                // is ever made accessible to multiple threads.
                // But for now, the only way CAS can fail is via close.
                // assert buf == null;
                throw new IOException("Stream closed");
            }
            buffer = nbuf;
        }
    count = pos;
    int n = getInIfOpen().read(buffer, pos, buffer.length - pos);
    if (n > 0)
        count = n + pos;
}

最核心的就是这个:

int n = getInIfOpen().read(buffer, pos, buffer.length - pos);

还是调用输入流自身的read(byte[])方法,但是这个字节数据会使用BufferedInputStream中默认的那个,也就是byte[8192],通过这种字节数组作为缓冲区,每次都从这里取数据,不够了就再去取8192个字节数的数据回来。

说了这么多理论,我们还是直接用代码来测试一下,验证一下我们的说法:

package io;

import java.io.*;

/**
 * @author 18092106
 * @create 2018-08-31 19:11
 **/
public class TestBufferedInputStream {

    public void useFileInputSream(){
        try (FileInputStream fis = new FileInputStream(new File("D:\\安装包\\软件安装包.rar"))) {
            byte[] bytes = new byte[1024];
            int hasRead;
            long startTime = System.currentTimeMillis();
            while((hasRead = fis.read(bytes)) != -1){}
            long endTime = System.currentTimeMillis();
            System.out.println("文件大小1.8G,FileInputStream用时:" + (endTime - startTime));
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public void useBufferedInputStream(){
        try (FileInputStream fis = new FileInputStream(new File("D:\\安装包\\软件安装包.rar"));
             BufferedInputStream bis = new BufferedInputStream(fis)) {
            byte[] bytes = new byte[1024];
            int hasRead;
             long startTime = System.currentTimeMillis();
            while((hasRead = bis.read(bytes)) != -1){}
            long endTime = System.currentTimeMillis();
            System.out.println("文件大小1.8G,bufferedInputSream用时:" + (endTime - startTime));
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }

    }



    public static void main(String[] args) {
        new TestBufferedInputStream().useBufferedInputStream();
        new TestBufferedInputStream().useFileInputSream();
    }
}

运行结果:

我们选择的节点流是FileInputStream,用FileInputStream和BufferedInputStream分别读取一个同一个文件,比较他们的用时。在声明的字节数组大小为1024时,可以明显的看到BufferedInputStream的效率高很多,我们修改下声明的字节数组的大小为8192,也就是BufferedInputStream中默认大小,再来看看结果:

可以看到在这种情况下,反而FileInputStream的效率更高了,这个也和我们看源码得到的结论一致,总结下来就是:

BufferedInputStream内部构造了一个8192大小的字节数组用作缓冲区,如果每次读取的字节数组小于这个默认的,那么就使用这个默认的字节数组来进行i/O访问,通过减少访问次数提高效率,当每次读取的字节数组大于这个默认的时候,还是调用传入的输入流本身的方法,这种情况由于内部的判断等原因,反而输入流的效率会更高。



版权声明:本文为qq_23585245原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。