本节主要介绍BIO的文件IO相关操作。
传统的IO主要是通过流来完成的具体操作。流(Stream)主要有两种:
- 字节流:InputStream和OutputStream
- 字符流:Reader和Writer
如下图,上面是字节流,下面是字符流:
此外,根据功能可以将流分成以下种类:
- 节点流:基本的字节、字符读写功能流,如InputStream、Reader
- 过滤流:
- 缓冲流(套接流):对读写数据提供了缓冲功能,提高效率;
- 转换流:用于字节数据到字符数据之间的转换;
- 数据流:提供了读写Java中的基本数据类型的功能;
- 对象流:用于直接将对象写入写出;
1、字节流
所有文件的储存是都是字节(byte)的储存,在磁盘上保留的并不是文件的字符而是先把字符编码成字节,再储存这些字节到磁盘。字节流是最基本的,可用于任何类型的对象,包括二进制对象;它提供了处理任何类型的IO操作的功能。(字符流只能处理字符或者字符串)
- 字节流处理单元为1个字节,操作字节和字节数组;
- 字节流在操作时本身不会用到缓冲区(内存),是文件本身直接操作的,而字符流在操作时使用了缓冲区,通过缓冲区再操作文件。
- 常见的子类有:FileInputStream、ByteArrayInputStream、BufferdInputStream
1.1)InputStream常用方法:
InputStream是字节输入流的基类(是一个抽象类),所有的字符输入流都会继承该类。其内部定义read()抽象方法,以及一些公用的方法。
1)read():
public abstract int read() throws IOException;
在InputStream 类中,read()方法是一个抽象的,需要子类去实现。功能:返回当前流中下一个字节(到达流结尾返回-1),如果流中没有数据read就会阻塞直至数据到来或者异常出现或者流关闭。
该方法既然返回的是字节,那为什么返回值为什么会是int类型,而不是byte类型呢?
首先,read 方法返回的值是一个八位的二进制,值区间为:「0000 0000,1111 1111」,也就是范围 [-128,127]。
其次,read 方法同时又规定当读取到文件的末尾,将返回值 -1 。所以如果使用 byte 作为返回值类型,那么当方法返回一个 -1 ,我们该判定这是文件中数据内容,还是流的末尾呢?
而 int 类型占四个字节,高位的三个字节全部为 0,我们只使用它的最低位字节,当遇到流结尾标志时,返回四个字节表示的 -1(32 个 1),这就和表示数据的值 -1(24 个 0 + 8 个 1)区别开来了。
2)read(byte b[]):
public int read(byte b[]) throws IOException {
return read(b, 0, b.length);
}
public int read(byte b[], int off, int len) throws IOException{
//为了不使篇幅过长,方法体大家可自行查看 jdk 源码
}
在InputStream 类中提供默认实现。同样该方法是一个阻塞的。
- 功能:从流中读取指定长度的字节,填充到参数的数组中。
- 返回值:总共读取len个字节,但是往往会出现流中字节数小于len,所以返回的是实际读取到的字节数;如果到达流尾部返回-1;
3)其他方法:
- public long skip(long n):跳过 n 个字节,返回实际跳过的字节数
- public void close():关闭流并释放对应的资源
- public synchronized void mark(int readlimit)
- public synchronized void reset()
- public boolean markSupported()
这些方法也基本都没具体实现,留待子类实现。是一些高阶用法。
skip方法表示跳过指定的字节数,来读取。调用这种方法需要知道,一旦跳过就不能返回到原来的位置。当然,我们可以看到还有剩下的三种方法,他们一起合作实现了可重复读的操作。mark方法在指定的位置打上标记,reset方法可以重新回到之前的标记索引处。
1.2)FileInputStream常用方法:
基类 InputStream 中有一个抽象方法 read 要求所有子类进行实现,而 FileInputStream 使用本地方法进行了实现:
public int read() throws IOException {
return read0();
}
private native int read0() throws IOException;
除此之外,FileInputStream 中还有一些其他的读取相关方法,但大多采用了本地方法进行了实现。
1.3)inputStream.read(byte[] buf)方法详解:
read(byte[] buf)方法返回值有三种情况:
- 如果流中数据大于byte[] buf的长度,则返回buf数组长度(全部填满了),需要多次调用read(byte[] buf)函数才能取完数据;
- 如果流中数据小于byte[] buf的长度,则返回实际从流中读取字节数,buf数组剩余位置补0;
- 流中数据到达结尾了,再调用则返回-1;
1)查看byte[] buf中数据内容:
//D:\\test.txt 内容:abc
public static void fileStreamRead() {
InputStream ins = null;
try {
ins = new FileInputStream("D:\\test.txt");
byte[] buf = new byte[5];
int read = ins.read(buf);
while(read != -1) {
System.out.println("读取字节数:"+read);
System.out.println("buf中数据内容:"+Arrays.toString(buf));
read = ins.read(buf);
}
} catch (Exception e) {
} finally {
try {
if (ins != null) ins.close();
} catch (IOException e) {
}
}
}
输出:
读取字节数:3
buf中数据内容:[97, 98, 99, 0, 0]
根据输出我们可以看到,只用了一次就读取完流中所有数据,buf[]数组中剩余两个位置补了0.
2)read(byte[] buf)方法采用覆盖方式写入buf[]:
同样的代码,将buf[]容量改成2,即:bye[] buf = new byte[2];
运行,输出:
读取字节数:2
buf中数据内容:[97, 98]
读取字节数:1
buf中数据内容:[99, 98]
根据输出可以看到:
- buf[]长度小于流中数据长度不会报错,需要循环调用两次read(byte[])方法取完流中的数据;
- read(byte[] buf)不会清除buf[]中的内容,而是被read方法从前往后覆盖使用;
所以,我们为了能正确(不重复,也不漏掉)从流中读取数据,应该使用循环的方式,每次根据read(byte[] buf)方法返回大小n,从buf[]中取0-n个字节。如下:
public static void fileStreamRead1() {
InputStream ins = null;
try {
ins = new FileInputStream("D:\\test.txt");
byte[] buf = new byte[2];
int read = ins.read(buf);
while(read != -1) {
System.out.println(new String(buf,0,read));
read = ins.read(buf);
}
} catch (Exception e) {
} finally {
try {
if (ins != null) ins.close();
} catch (IOException e) {
}
}
}
3)byte[] buf长度问题?
根据上面讨论我们明显可以看到一个问题,byte[] buf的长度不好确定:
- 如果buf[]定义的小了,需要使用循环多次调用read(byte[])方法覆盖读取到buf中;
- 如果buf[]定义的大了,虽然一次可以读出来所有数据,但太浪费空间(剩余位置补0);
那么如何解决这个问题呢?
方法一:从流中获取其数据的大小,然后定义对应大小的byte[]数组。可以调用InputStream的available()方法获取流中数据大小,不过该方法对于文件IO能否正常返回,对于网络IO由于其“阻塞”的特性,调用该方法往往返回0。
方法二:而最好的解决方案就是使用动态字节数组流,它可以动态调整内部字节数组的大小,保证适当的容量,类似ArrayList会自动扩容一下。详情见线面。
参考:
http://www.51gjie.com/java/700.html
1.4)inputStream.available()方法详解:
要一次读取多个字节时,经常用到InputStream.available()方法,这个方法可以在读写操作前先得知数据流里有多少个字节可以读取。需要注意的是,如果这个方法用在从本地文件读取数据时,一般不会遇到问题,但如果是用于网络操作,就经常会遇到一些麻烦。比如,Socket通讯时,对方明明发来了1000个字节,但是自己的程序调用available()方法却只得到900,或者100,甚至是0,感觉有点莫名其妙,怎么也找不到原因。其实,这是因为网络通讯往往是间断性的,一串字节往往分几批进行发送。本地程序调用available()方法有时得到0,这可能是对方还没有响应,也可能是对方已经响应了,但是数据还没有送达本地。对方发送了1000个字节给你,也许分成3批到达,这你就要调用3次available()方法才能将数据总数全部得到。
1)对于网络IO:
对于网络IO,可以使用如下方式获取流中的大小:
InputStream inputStream = ;//网络输入流
int count = 0;
while(count ==0) {
count = inputStream.available();
}
byte[] b = new byte[count];
inputStream.read(b);
但这种方法也会有风险,如果客户端发动的请求确实是一个空的,那么就会陷入死循环。那么,还有没有其他的方法呢?
在网络流中如果不使用任何标记,是不知道流是否结束的。在读到网络流时,我们是可以知道这次可以读多少字节的内容,方法就是使用inputStream. available (),但一定要在调用read()至少一次之后,也就是说available方法一定要在read后调用,这样可用的字节数就等于 available + 1(read()方法已经获取了一个字节)。否则,直接调用available ()就只能得到零值。
InputStream inputStream = ;//网络输入流
int firstChar = inputStream.read();
int length = inputStream.available();
byte[] b = new byte[length +1];
b[0] = (byte)firstChar;
inputStream.read(b,1,length);
2)对于文件IO:
文件流的可用字节数 available = file.length(),文件的内容长度在创建File对象时就已知了,所以可以直接调用.available ()获取文件流中的数据大小。
FileInputStream fi = new FileInputStream("C:/Users/Administrator/Desktop/yy.txt");
//1. read() 逐字节读取
/* int i = 0;
byte[] bytes = new byte[fi.available()];
while(fi.available() > 0){
bytes[i] = (byte) fi.read();
i++;
}*/
//2. read(byte b[]) 一次读取
byte[] bytes = new byte[fi.available()];
fi.read(bytes);
fi.close();
System.out.println(Arrays.toString(bytes));
}
参考:
https://www.iteye.com/blog/jiangzhengjun-509900
1.5)输出流:
上面介绍的都是输入流,由于输出流原理和输入流基本一致,接下来我们简单介绍一下输出流。
OutputStream是字节输出流的基类(是一个抽象类),所有的字符输出流都会继承该类。其内部定义write()抽象方法,以及一些公用的方法。
1)FileOutputStream构造方法:
FileOutputStream有以下两个构造器:
- public FileOutputStream(String name, boolean append)
- public FileOutputStream(File file, boolean append)
参数 append 指明了,此流的写入操作是覆盖还是追加,true 表示追加,false 表示覆盖(不指定时默认是覆盖)。
2)write()方法:
- write(int b):将包含了待写入字节的int变量作为参数写入到当前文件输出流中
- write(byte[] b):将b.length长度的字节数组写入到文件输出流中
- write(byte[] b, int off, int len):将字节数组b从off位置到len位置的字节写入到文件输出流中
3)flush()方法:
当往FileOutputStream里写数据的时候,这些数据有可能会缓存在内存中。在之后的某个时间,比如,每次都只有X份数据可写,或者FileOutputStream关闭的时候,才会真正地写入磁盘。当FileOutputStream没被关闭,而你又想确保写入到FileOutputStream中的数据写入到磁盘中,可以调用flush()方法,该方法可以保证所有写入到FileOutputStream的数据全部写入到磁盘中。
2、动态字节/字符数组流
上面抛出了一个问题,我们在从流中读取数据到byte[] buf中,buf的长度无法准确指定,例如下面的代码:
FileInputStream fis = new FileInputStream(outputfile);
byte[] buf = new byte[1024];
int len = fis.read(buf);
流中字节数不固定的,因此这里需要的是一个可变长的字节数组。如果自己编写动态可扩展的byte数组又比较浪费时间,因此这里最合适的选择便是ByteArrayOutputStream、ByteArrayInputStream。
2.1)字节数组流:
ByteArrayInputStream和ByteArrayOutputStream是用来表示内存中的字节数组流。其中ByteArrayOutputStream可以用来写入变长的字节数组,内部使用了类似于ArrayList的动态数组扩容的思想。这对于不知道输入内容的具体长度时非常有用,例如:要将一个文件的内容或者网络上的内容读入一个字节数组时
public static void byteArrayOutputStreamExam() {
try {
FileInputStream fis = new FileInputStream("d:\\d.txt");
ByteArrayOutputStream baos = new ByteArrayOutputStream();
byte[] buf = new byte[1024];
int len = 0;
while((len = fis.read(buf))!=-1){
baos.write(buf,0,len);
}
baos.flush();
byte[] result = baos.toByteArray();
String s = new String(result);
System.out.println(s);
baos.close();
fis.close();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
2.2)字符数组流:
ByteArrayInputStream和ByteArrayOutputStream是字节数组流,那么与之对应的字符数组流则是StringReader和StringWriter(早期java使用StringBufferInputStream和StringBufferOutputStream,这两个类在jdk1.1后被废弃),也给出一个例子:
public static void stringReaderExam() {
String str = "This is a good day.今天是个好天气。";
StringReader stringReader = new StringReader(str);
StringWriter stringWriter = new StringWriter();
char[] buf = new char[128];
int len = 0;
try {
while ((len = stringReader.read(buf)) != -1) {
stringWriter.write(buf, 0, len);
}
stringWriter.flush();
String result = stringWriter.toString();
System.out.println(result);
stringWriter.close();
stringReader.close();
} catch (IOException e) {
e.printStackTrace();
}
}
与字节数组流相比,字符数组流反而用得更少,因为StringBuilder和StringBuffer也能方便的用来存储动态长度的字符,而且大家更熟悉这些类。
3、字符流
实际中很多的数据是文本,所以又提出了字符流的概念,字符流处理的单元为2个字节的Unicode字符,分别操作字符、字符数组或字符串。所以字符流是由Java虚拟机将字节转化为2个字节的Unicode字符为单位的字符而成的。
- 对多国语言支持性比较好;
- 字符流在操作时使用了缓冲区,通过缓冲区再操作文件;
- 常见子类有:FileReader、FileWriter
3.1)字节流和字符流的转化:
1)缓冲区:
上面提到了字节流没有使用缓冲区,而字符流使用了。那么,什么是缓冲区?缓冲区可以简单地理解为一段内存区域。某些情况下,如果一个程序频繁地操作一个资源(如文件或数据库),则性能会很低,此时为了提升性能,就可以将一部分数据暂时读入到内存的一块区域之中,以后直接从此区域中读取数据即可,因为读取内存速度会比较快,这样可以提升程序的性能。
在字符流的操作中,所有的字符都是在内存中形成的,在输出前会将所有的内容暂时保存在内存之中,所以使用了缓冲区暂存数据。而字节流是直接操作文件的,每操作一个字节数据都会进行一次IO操作。
2)InputStreamReader、OutputStreamWriter类:
这两个类继承自Read、Writer。可以通过这两个类把字节流转换成字符流。虽然,字节流可以处理任何类型数据,但是由于没有使用缓冲区,所以其效率比较低;故如果要操作字符类数据,可以将字节流转换成字符流来完成。
在从字节流转化为字符流时,实际上就是byte[]转化为String,常用的方法有:
- String(byte[] b,String charsetName) :需要指定字符集,该方法实际底层调用了String(byte bytes[], int offset, int length, Charset charset)
- String(byte[] b,int offset,int length):使用默认字符集
而在字符流转化为字节流时,实际上是String转化为byte[],对应的方法有:
- byte[] getBytes(String charsetName) :指定字符集,将String转成byte[]
3)示例:字节流转成字符流
//字节流
File file = new File("d:\\data.txt");
InputStream is = new FileInputStream(file);
//或者InputStream iis = new FileInputStream("d:\\data.txt");
InputStreamReader ir = new InputStreamReader(is);//字节流转换成字符流
BufferedReader br = new BufferedReader(ir);//字符流的缓冲
//从字符缓冲流中读取数据
String str = null;
while((str=br.readLine()) != null) {
System.out.println(str);
}
br.close();
ir.close();
is.close();
3.2)文件字符流:
根据最上面的图可以得知:FileReader、FileWriter这两个文件字符流继承自两个转换流( InputStreamReader和OutputStreamWriter),它们的内部方法非常简单,只是几个构造方法而已。这两个文件流完全依赖父类,自己基本没有扩展父类,使用的方法都是父类的。
3.3)实验:字节流没有使用缓冲区、字符流使用了缓冲区
1)字节流:
File f = new File("d:" + File.separator + "test.txt"); // 声明File 对象
//通过子类实例化父类对象,通过对象多态性进行实例化
OutputStream out = new FileOutputStream(f);
//准备一个字符串,并转byte数组
byte b[] = "Hello World!!!".getBytes();
out.write(b);// 将内容输出
//关闭输出流
//out.close();
说明:此时没有关闭字节流操作,但是运行完程序后,文件中也依然存在了输出的内容,证明字节流是直接操作文件本身的。
2)字符流:
File f = new File("d:" + File.separator + "test.txt");// 声明File 对象
//通过子类实例化父类对象,通过对象多态性进行实例化
Writer out = new FileWriter(f);
// 准备一个字符串,进行写操作
out.write("Hello World!!!");
//关闭输出流
// out.close();
说明:程序运行后会发现文件中没有任何内容,这是因为字符流操作时使用了缓冲区,而在关闭字符流时会强制性地将缓冲区中的内容进行输出,但是如果程序没有关闭,则缓冲区中的内容是无法输出的,所以得出结论:字符流使用了缓冲区,而字节流没有使用缓冲区。如果想在不关闭时也可以将字符流的内容全部输出,则可以使用Writer类中的flush()方法完成。
4、缓冲流
无论是字节流、还是字符流,我们可以在外面套一层缓冲流,来进一步提高速度。(缓冲类型下,会在后台自动下载定长的内容,去读的时候是从缓冲区中拿东西。)
1)字节流的缓冲流:
对应的类有:BufferedInputStream、BufferedOutputStream
File file = new File("d:\\data.txt");
InputStream is = new FileInputStream(file);//或者 InputStream is = new FileInputStream("d:\\data.txt");
//字节流的缓冲
BufferedInputStream bs = new BufferedInputStream(is);
//设置缓冲区
byte[] bytes = new byte[1024];
//从文件中按字节读取内容,到文件尾部时read方法将返回-1
int n = 0;
while((n=bs.read(bytes)) != -1) {
String str = new String(bytes,0,n);
System.out.println(str);
}
bs.close();
is.close();
2)字符流的缓冲流:
对应的类有:BufferedReader、BufferedWriter
File file = new File("d:\\data.txt");
Reader fr = new FileReader(file);//或者 Reader fr = new FileReader("d:\\data.txt");
//字符流的缓冲
BufferedReader br = new BufferedReader(fr);
//从文件中按字节读取内容,到文件尾部时read方法将返回-1
String str = null;
while((str=br.readLine()) != null) {
System.out.println(str);
}
br.close();
fr.close();
5、装饰者字节流
上述的流都是直接通过操作字节数组来实现输入输出的,那如果我们想要输入一个字符串类型或者int型或者double类型,那还需要调用各自的转字节数组的方法,然后将字节数组输入到流中。我们可以使用装饰流,帮我们完成转换的操作。我们先看DataOutputStream。
public DataOutputStream(OutputStream out)
public synchronized void write(byte b[], int off, int len)
public final void writeBoolean(boolean v)
public final void writeByte(int v)
public final void writeShort(int v)
public final void writeInt(int v)
public final void writeDouble(double v)
可以看到,DataOutputStream只有一个构造方法,必须传入一个OutputStream类型参数。(其实它的内部还是围绕着OutputStream,只是在它的基础上做了些封装)。我们看到,有writeBoolean、writeByte、writeShort、writeDouble等方法,他们内部都是将传入的 boolean,Byte,short,double类型变量转换为了字节数组,然后调用从构造方法中接入的OutputStream参数的write方法。
参考:
https://www.jianshu.com/p/1238fd4f14b5