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访问,通过减少访问次数提高效率,当每次读取的字节数组大于这个默认的时候,还是调用传入的输入流本身的方法,这种情况由于内部的判断等原因,反而输入流的效率会更高。