0
点赞
收藏
分享

微信扫一扫

BIO之Stream

河南妞 2022-06-20 阅读 72

本节主要介绍BIO的文件IO相关操作。

传统的IO主要是通过流来完成的具体操作。流(Stream)主要有两种:

  • 字节流:InputStream和OutputStream
  • 字符流:Reader和Writer

如下图,上面是字节流,下面是字符流:

BIO之Stream_数据

BIO之Stream_字节流_02 

此外,根据功能可以将流分成以下种类:

  • 节点流:基本的字节、字符读写功能流,如InputStream、Reader
  • 过滤流
  • 缓冲流(套接流):对读写数据提供了缓冲功能,提高效率;
  • 转换流:用于字节数据到字符数据之间的转换;
  • 数据流:提供了读写Java中的基本数据类型的功能;
  • 对象流:用于直接将对象写入写出;

1、字节流

所有文件的储存是都是字节(byte)的储存,在磁盘上保留的并不是文件的字符而是先把字符编码成字节,再储存这些字节到磁盘。字节流是最基本的,可用于任何类型的对象,包括二进制对象;它提供了处理任何类型的IO操作的功能。(字符流只能处理字符或者字符串)

  1. 字节流处理单元为1个字节,操作字节和字节数组;
  2. 字节流在操作时本身不会用到缓冲区(内存),是文件本身直接操作的,而字符流在操作时使用了缓冲区,通过缓冲区再操作文件。
  3. 常见的子类有: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字符为单位的字符而成的。

  1. 对多国语言支持性比较好;
  2. 字符流在操作时使用了缓冲区,通过缓冲区再操作文件;
  3. 常见子类有: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​​

 


举报

相关推荐

Netty - I/O模型之BIO

redis之Stream

flutter之stream

BIO编程

Doris之Stream load

Java 8 之 Stream

BIO基础

0 条评论