IO流的操作在工作中由于都是工具类来完成,导致一直对其理解不够深,想写篇文章来学习下,分类如下:
一、IO流的概念
Java的IO流是实现输入/输出的基础,它可以方便地实现数据的输入/输出操作,在Java中把不同的输入/输出源抽象表述为"流"。流是一组有顺序的,有起点和终点的字节集合,是对数据传输的总称或抽象。即数据在两设备间的传输称为流,流的本质是数据传输,根据数据传输特性将流抽象为各种类,方便更直观的进行数据操作。
流有输入和输出,输入时是流从数据源流向程序。输出时是流从程序传向数据源,而数据源可以是内存,文件,网络或程序等。
2.字节流和字符流
字节流和字符流和用法几乎完全一样,区别在于字节流和字符流所操作的数据单元不同。
字符流的由来: 因为数据编码的不同,而有了对字符进行高效操作的流对象。本质其实就是基于字节流读取时,去查了指定的码表。字节流和字符流的区别:
(1)读写单位不同:字节流以字节(8bit)为单位,字符流以字符为单位,根据码表映射字符,一次可能读多个字节。
(2)处理对象不同:字节流能处理所有类型的数据(如图片、avi等),而字符流只能处理字符类型的数据。
3.节点流和处理流
按照流的角色来分,可以分为节点流和处理流。
可以从/向一个特定的IO设备(如磁盘、网络)读/写数据的流,称为节点流,节点流也被成为低级流。
处理流是对一个已存在的流进行连接或封装,通过封装后的流来实现数据读/写功能,处理流也被称为高级流。
//节点流,直接传入的参数是IO设备
FileInputStream fis = new FileInputStream("test.txt");
//处理流,直接传入的参数是流对象
BufferedInputStream bis = new BufferedInputStream(fis);
当使用处理流进行输入/输出时,程序并不会直接连接到实际的数据源,没有和实际的输入/输出节点连接。使用处理流的一个明显好处是,只要使用相同的处理流,程序就可以采用完全相同的输入/输出代码来访问不同的数据源,随着处理流所包装节点流的变化,程序实际所访问的数据源也相应地发生变化。
实际上,Java使用处理流来包装节点流是一种典型的装饰器设计模式,通过使用处理流来包装不同的节点流,既可以消除不同节点流的实现差异,也可以提供更方便的方法来完成输入/输出功能。
流介绍
InputStream,Reader,OutputStream以及Writer,这四大抽象基类,本身并不能创建实例来执行输入/输出,但它们将成为所有输入/输出流的模版,所以它们的方法是所有输入/输出流都可以使用的方法。类似于集合中的Collection接口。
1.InputStream
InputStream 是所有的输入字节流的父类,它是一个抽象类,主要包含三个方法:
//读取一个字节并以整数的形式返回(0~255),如果返回-1已到输入流的末尾。
int read() ;
//读取一系列字节并存储到一个数组buffer,返回实际读取的字节数,如果读取前已到输入流的末尾返回-1。
int read(byte[] buffer) ;
//读取length个字节并存储到一个字节数组buffer,从off位置开始存,最多len, 返回实际读取的字节数,如果读取前以到输入流的末尾返回-1。
int read(byte[] buffer, int off, int len) ;
2.Reader
//读取一个字符并以整数的形式返回(0~255),如果返回-1已到输入流的末尾。
int read() ;
//读取一系列字符并存储到一个数组buffer,返回实际读取的字符数,如果读取前已到输入流的末尾返回-1。
int read(char[] cbuf) ;
//读取length个字符,并存储到一个数组buffer,从off位置开始存,最多读取len,返回实际读取的字符数,如果读取前以到输入流的末尾返回-1。
int read(char[] cbuf, int off, int len)
对比InputStream和Reader所提供的方法,就不难发现两个基类的功能基本一样的,只不过读取的数据单元不同。
在执行完流操作后,要调用close()方法来关系输入流,因为程序里打开的IO资源不属于内存资源,垃圾回收机制无法回收该资源,所以应该显式关闭文件IO资源。
除此之外,InputStream和Reader还支持如下方法来移动流中的指针位置:
//在此输入流中标记当前的位置
//readlimit - 在标记位置失效前可以读取字节的最大限制。
void mark(int readlimit)
// 测试此输入流是否支持 mark 方法
boolean markSupported()
// 跳过和丢弃此输入流中数据的 n 个字节/字符
long skip(long n)
//将此流重新定位到最后一次对此输入流调用 mark 方法时的位置
void reset()
3.OutputStream
Writer 是所有的输出字符流的父类,它是一个抽象类,主要包含如下六个方法:
//向输出流中写入一个字符数据,该字节数据为参数b的低16位。
void write(int c);
//将一个字符类型的数组中的数据写入输出流,
void write(char[] cbuf)
//将一个字符类型的数组中的从指定位置(offset)开始的,length个字符写入到输出流。
void write(char[] cbuf, int offset, int length);
//将一个字符串中的字符写入到输出流。
void write(String string);
//将一个字符串从offset开始的length个字符写入到输出流。
void write(String string, int offset, int length);
//将输出流中缓冲的数据全部写出到目的地。
void flush()
4.Writer
Writer 是所有的输出字符流的父类,它是一个抽象类,主要包含如下六个方法:
//向输出流中写入一个字符数据,该字节数据为参数b的低16位。
void write(int c);
//将一个字符类型的数组中的数据写入输出流,
void write(char[] cbuf)
//将一个字符类型的数组中的从指定位置(offset)开始的,length个字符写入到输出流。
void write(char[] cbuf, int offset, int length);
//将一个字符串中的字符写入到输出流。
void write(String string);
//将一个字符串从offset开始的length个字符写入到输出流。
void write(String string, int offset, int length);
//将输出流中缓冲的数据全部写出到目的地。
void flush()
可以看出,Writer比OutputStream多出两个方法,主要是支持写入字符和字符串类型的数据。
使用Java的IO流执行输出时,不要忘记关闭输出流,关闭输出流除了可以保证流的物理资源被回收之外,还能将输出流缓冲区的数据flush到物理节点里(因为在执行close()方法之前,自动执行输出流的flush()方法)
根据设计思路来说继承
有了抽象类,就一定会有子类。针对不同的数据来源,InputStream和OutputStream存在三种子类:一种是基于内存的ByteArrayInputStream/ByteArrayOutputStream,一种是基于磁盘文件的FileInputStream/FileOutputStream,还有一种是基于网络的SocketInputStream/SocketOutputStream。
文件字节输入流:FileInputStream/FileOutputStream类
java.io.FileInputStream 类是文件输入流,从文件中读取字节。
循环读取数据
int read;
FileInputStream fis=new FileInputStream(new File("D:\\123.txt"));//新建一个FileInputStream对象
while((read = fis.read()) != -1){
System.out.print((char)read);
}
或者
byte[] b=new byte[fis.available()];//新建一个字节数组
fis.read(b);//将文件中的内容读取到字节数组中
fis.close();
String str2=new String(b);//将字节数组转换成string
文件字节输出流:FileOutputStream类
java.io.FileOutputStream 类是文件输出流,用于将数据写出到文件。
第一种:当你创建一个流对象时,必须传入一个文件路径。该路径下,如果没有这个文件,会创建该文件。如果有这个文件,会清空这个文件的数据。(会清空)
第二种:创建对象,参数中都需要传入一个boolean类型的值, true 表示追加数据, false 表示清空原有数据。这样创建的输出流对象,就可以指定是否追加续写了。(实现数据追加续写)
注意
1、使用FileOutputStream类输出的是数字97、98,但是文件显示的是字符;这是因为文本编辑器,自己帮我们转换的;
2、我们如果要换行 \r\n 或者其它操作要注意,操作的是byte类型数据,需要转换;
FileOutputStream fos = new FileOutputStream("test.txt");
fos.write("teateatestest".getBytes());
fos.close();
FileInputStream fis = new FileInputStream("test.txt");
byte[] buf = new byte[100];
int len = fis.read(buf);
System.out.println(new String(buf,0,len));
fis.close();
ByteArrayInputStream/ByteArrayOutputStream
public class ByteArrayOutputStream extends OutputStream
此类实现了一个输出流,其中的数据被写入一个 byte 数组。缓冲区会随着数据的不断写入而自动增长。可使用 toByteArray()和 toString()获取数据。
关闭 ByteArrayOutputStream 无效。此类中的方法在关闭此流后仍可被调用,而不会产生任何IOException。
我的个人理解是ByteArrayOutputStream是用来缓存数据的(数据写入的目标(output stream原义)),向它的内部缓冲区写入数据,缓冲区自动增长,当写入完成时可以从中提取数据。由于这个原因,ByteArrayOutputStream常用于存储数据以用于一次写入。
/**
* 多次读入,一次写出
* @param args
* @throws IOException
*/
public static void main(String[] args) throws IOException {
FileInputStream fos = new FileInputStream("D:" + File.separator + "temp.out");
int size = 0;
double i = 1;
ByteArrayOutputStream out = new ByteArrayOutputStream();
for(;(size=fos.read())!=-1;i++){
out.write(size);
}
fos.close();
byte retArr[] = out.toByteArray();
}
/**
* 多次读入,一次写出
* @param args
* @throws IOException
*/
public static void main(String[] args) throws IOException {
FileInputStream fos = new FileInputStream("D:" + File.separator + "temp.out");
int size = 0;
double i = 1;
ByteArrayOutputStream out = new ByteArrayOutputStream();
for(;(size=fos.read())!=-1;i++){
out.write(size);
}
fos.close();
byte retArr[] = out.toByteArray();
}
SocketInputStream/SocketOutputStream
SocketInputStream使用 private native int socketRead0(FileDescriptor fd,byte b[], int off, int len,int timeout)这个native方法读取远程服务器的数据流。所有read方法都是基于这个本地方法实现的。
SocketOutputStream 使用private native void socketWrite0(FileDescriptor fd, byte[] b, int off,int len)这个native方法来进行远程数据流的写入,所有的write方法都是基于这个方法实现的。
点评:InputStream和OutputStream是对流的抽象,不同的具体流通过继承去实现,对于Java本地平台,最基本的就是基于文件系统的流,当涉及到远程系统,就会出现网络流,基于内存的流一般不会用到。
st = new Socket("127.0.0.1",8989);
dis = new DataInputStream(st.getInputStream());
System.out.println(dis.readInt());
dis.close();
扩展点二:对IO流行为的扩展
装饰模式可以对一个类的行为进行扩展,并且不改变它的接口,Java通过FilterInputStream/FilterOutputStream实现了装饰模式。
责任链模式则是定义统一的接口,然后通过多个实现该接口的子类串行协作完成一项复杂的功能。Java通过将多个FilterInputStream/FilterOutputStream(不是fileinputstream)的子类串联起来实现了责任链模式。
FilterInputStream/FilterOutputStream
FilterInputStream本身不实现输入流的功能,而是通过构造函数传入另一个InputStream的子类,把输入流的功能交给它做。通过继承FilterInputStream可以对输入输出流的行为进行扩展,这是装饰模式的典型用法。通过多个装饰类实现责任链模式,它将对一个输入流的不同处理分散到不同的FilterInputStream中去。FilterOutputStream和FilterInputStream的原理一样。
public
class FilterInputStream extends InputStream {
protected volatile InputStream in;
protected FilterInputStream(InputStream in) {
this.in = in;
}
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);
}
}
BufferedInputStream/BufferedOutputStream
继承了FilterInputStream,实现了输入流处理中的缓冲的功能。底层的流会先被读取到一个字节数组中,用户使用BufferedInputStream读取数据的时候,会先读取字节数组中的数据,读完了才会调用底层的流进行进一步的读取。这种方法可以提升读取的性能。
继承了FilterOutputStream,实现了输出流处理中的缓冲功能。当用户写入数据的时候,其实是先写入到BufferedOutputStream的一个字节数组中,当这个字节数组满了,才会真正调用底层的输出流执行输出动作。这种方法可以提升写入的性能。在使用BufferedOutputStream的写入功能时,一定要使用flush,因为缓冲数组不满的时候是不会写入底层流的,在写入最后一点数据的时候,缓冲数据不一定被填满,这时候就需要调用flush进行强制刷新。
File f = new File("d:/a.txt");
FileOutputStream fos = new FileOutputStream(f);
// 构建FileOutputStream对象,文件不存在会自动新建
BufferedOutputStream bos = new BufferedOutputStream(fos);
bos.write("1我是中文".getBytes());
bos.close();
// 关闭输出流,写入数据,如果下面还要写用flush();
// 因为是BufferOutputStream链接到FileOutputStream,只需关闭尾端的流
// 所以不需要关闭FileOutputStream;
FileInputStream fis = new FileInputStream(f);
BufferedInputStream bis = new BufferedInputStream(fis);
BufferedReader reader = new BufferedReader (new InputStreamReader(bis));
//之所以用BufferedReader,而不是直接用BufferedInputStream读取,是因为BufferedInputStream是InputStream的间接子类,
//InputStream的read方法读取的是一个byte,而一个中文占两个byte,所以可能会出现读到半个汉字的情况,就是乱码.
//BufferedReader继承自Reader,该类的read方法读取的是char,所以无论如何不会出现读个半个汉字的.
StringBuffer result = new StringBuffer();
while (reader.ready()) {
result.append((char)reader.read());
}
System.out.println(result.toString());
reader.close();
BufferedOutputStream和ByteArrayOutputStream区别
二者的共同点是:都是为了缓存数据的输出,提高数据输出的效率;
而二者的区别是:ByteArrayOutputStream是将数据全部缓存到自身,然后一次性输出;而BufferedOutputStream是缓存一部分后,一次一次的输出。
这是我理解的二者的一个区别,当然,他们在用法上和使用领域上还有很多区别了,这里我就不多说了。
另外,使用ByteArrayOutputStream、ByteArrayInputStream另一个好处是:当使用完他们缓存的字节流以后或者关闭它们之后,其中的字节流依然存在,可在此利用。
曾经在编程中,我就遇到这么一个需求:
从网络上取回图片的输入流之后,我要同时用之做两个用处,一是:将此流转为Bitmap以工UI层显;二是:将此图片流输出到SD卡中。此时,就碰到一个问题了,如果用普通的输入输出流,当流被使用完以后,其中字节流就 不复存在了,要想再次利用已经不可能。由于本人基础不扎实,苦思冥想也没有更好的办法,最后终于在网上看ByteArrayOutputStream的使用,才终于解决多次利用一个流的数据的问题
1.BufferedOutputStream会首先创建一个默认的容器量, capacity = 8192 = 8KB, 每次在写的时候都会去比对capacity是否还够用, 如果不够用的时候, 就flushBuffer(), 把buf中的数据写入对应的outputStream中, 然后将buf清空, 一直这样等到把内容写完. 在这过程中主要起到了一个数据缓冲的功能.
public synchronized void write(byte b[], int off, int len) throws IOException {
// 在这判断需要写的数据长度是否已经超出容器的长度了,如果超出则直接写到相应的outputStream中,并清空缓冲区
if (len >= buf.length) {
flushBuffer();
out.write(b, off, len);
return;
}
// 判断缓冲区剩余的容量是否还够写入当前len的内容,如果不够则清空缓冲区
if (len > buf.length - count) {
flushBuffer();
}
// 将要写的数据先放入内存中,等待数据达到了缓冲区的长度后,再写到相应的outputStream中
System.arraycopy(b, off, buf, count, len);
count += len;
}
flushBuffer () 这个方法干了些什么呢, 来看看源码
private void flushBuffer() throws IOException {
if (count > 0) {
// 把写入内存中的数据写到构造方法里传入的OutputStream句柄里, 并把容量大小清楚
out.write(buf, 0, count);
count = 0;
}
}
这个类最重要的就是这2个方法, 这样节省了大量的内存空间, 合理的分配内存来完成数据输出, 当你资源不是那么充沛时, 选择这个类来实现你想要的东西是不是很合适呢?
ByteArrayOutputStream也会首先创建一个默认的容器量, capacity = 32 = 32b, 每次在写的时候都会去比对capacity是否还够用, 如果不够用的时候, 就重新创建buf的容量, 一直等到内容写完, 这些数据都会一直处于内存中.
public synchronized void write(byte b[], int off, int len) {
if ((off < 0) || (off > b.length) || (len < 0) ||
((off + len) > b.length) || ((off + len) < 0)) {
throw new IndexOutOfBoundsException();
} else if (len == 0) {
return;
}
// 不断对自己的容量进行相加
int newcount = count + len;
// 如果新的容量大小已经超过了现有的大小时,则重新开辟新的内存区域来保存当前的数据
if (newcount > buf.length) {
buf = Arrays.copyOf(buf, Math.max(buf.length << 1, newcount));
}
System.arraycopy(b, off, buf, count, len);
count = newcount;
}
总结 : 当你资源不足够用时,选择BufferedOutputStream是最佳的选择, 当你选择快速完成一个作业时,可以选择ByteArrayOutputStream之类的输出流
有时候我们需要对同一个InputStream对象使用多次。比如,客户端从服务器获取数据 ,利用HttpURLConnection的getInputStream()方法获得Stream对象,这时既要把数据显示到前台(第一次读取),又想把数据写进文件缓存到本地(第二次读取)。
但第一次读取InputStream对象后,第二次再读取时可能已经到Stream的结尾了(EOFException)或者Stream已经close掉了。
而InputStream对象本身不能复制,因为它没有实现Cloneable接口。此时,可以先把InputStream转化成ByteArrayOutputStream,后面要使用InputStream对象时,再从ByteArrayOutputStream转化回来就好了。代码实现如下:
InputStream input = httpconn.getInputStream();
5
6 ByteArrayOutputStream baos = new ByteArrayOutputStream();
7 byte[] buffer = new byte[1024];
8 int len;
9 while ((len = input.read(buffer)) > -1 ) {
10 baos.write(buffer, 0, len);
11 }
12 baos.flush();
13
14
15 InputStream stream1 = new ByteArrayInputStream(baos.toByteArray());
16
17
18 //TODO:显示到前台
19
20
21 InputStream stream2 = new ByteArrayInputStream(baos.toByteArray());
22
23
24 //TODO:本地缓存
PrintStream
继承FilterOutputStream,这个类的print和println方法可以把java的一些基本类型数据转换成字节写入到底层输出流,但是PrintStream对String的转换是平台相关的,不同的平台会有不同的编码,所以写入到底层的字节也不同,因此PrintStream只适合于测试输出,不适合于一般的I/O操作,特别是网络流。
DataInputStream/DataOutputStream
这两个类继承了FilterInputStream/FilterOutputStream,用来实现将java基本类型转换成二进制来进行读写操作,这两个类的readUTF和writeUTF方法使用了一种特殊的UTF编解码方式,只能用于java程序,因此不建议在网络流或者跨平台的应用中使用者两个类
DataInputStream in = new DataInputStream(new FileInputStream("test.txt"));
DataOutputStream out = new DataOutputStream(new FileOutputStream("test1.txt"));
BufferedReader d = new BufferedReader(new InputStreamReader(in));
String count;
while((count = d.readLine()) != null){
String u = count.toUpperCase();
System.out.println(u);
out.writeBytes(u + " ,");
}
d.close();
out.close();
PushbackInputStream
继承了FilterInputStream,提供了一种回退的机制,可以实现unread,本质是使用缓冲数组实现了,也就是说,回退的范围是有限的。
try (
// 创建一个PushbackReader对象,指定推回缓冲区的长度为64
PushbackReader pr = new PushbackReader(new FileReader(
"src/net/nyist/io/PushBackTest.java"), 64);
) {
char[] buf = new char[32];
// 用以保存上次读取字符串的内容
String lastContent = "";
int hasRead = 0;
// 循环读取文件内容
while ((hasRead = pr.read(buf)) > 0) {
// 将读取的内容转化为字符串
String content = new String(buf, 0, hasRead);
int targetIndex = 0;
// 将上次读取的字符串和本次读取的字符串拼接起来
// 查看是否包含目标字符串,
// 如果包含目标字符串
if ((targetIndex = (lastContent + content)
.indexOf("new PushbackReader")) > 0) {
// 将本次的内容和上次的内容一起推回缓冲区
pr.unread((lastContent + content).toCharArray());
// 重现定义一个长度为targetIndex的char类型的数组
if (targetIndex > 32) {
buf = new char[targetIndex];
}
// 再次读取指定长度的内容,即目标字符串之前的内容
pr.read(buf, 0, targetIndex);
// 答应读取指定长度的内容
System.out.println(new String(buf, 0, targetIndex));
System.exit(0);
} else {
// 打印上次读取的内容
System.out.println(lastContent);
// 将本次读取的内容设置为上次读取的内容
lastContent = content;
}
}
} catch (IOException ioe) {
ioe.printStackTrace();
}
}
当程序调用推回输入流的unread()方法时,系统会把指定数组的内容推回到该缓冲区中,而推回输入流每次调用read()方法时,总是先从推回缓冲区读取内容,只有完全读取了推回缓冲区里的内容后,但是还没有装满read()所需要的数组时才会从原输入流中读取
Reader/Writer出现的原因
InputStream和OutputStream是面向字节的,而人类的习惯是面向字符,因此InputStream和OutputStream对于程序猿的用户体验不是太好,于是就需要提供一些面向字符的流。由于DataInputStream/DataOutputStream在跨平台的情况下存在问题,因此,java设计者干脆仿照InputStream和OutputStream重新设计了一套面向字符的I/O,也就是Reader/Writer
InputStreamReader/OutputStreamWriter
由于计算机只识别字节,所以Reader/Writer的数据来源最终还是字节,而他们无法直接和字节打交道,这时候就需要一个中介者将Reader/Writer和InputStream和OutputStream进行打通,于是就有了InputStreamReader和OutputStreamWriter
//1.创建InputStreamReader对象,构造方法中传递字节输入流和指定的编码表名称
3 //InputStreamReader isr = new InputStreamReader(new FileInputStream("E:\\gbk.txt"),"UTF-8");//???
4 //将匿名对象流中的字节流按照GBK进行解码为字符流
5 InputStreamReader isr = new InputStreamReader(new FileInputStream("E:\\gbk.txt"),"GBK");//你好
6
7 //2.使用InputStreamReader对象中的方法read读取文件
8 int len = 0;
9 while((len = isr.read())!=-1){
10 System.out.println((char)len);
11 }
12 //3.释放资源
13 isr.close();
或者
public static void main(String[] args) throws IOException {
//字节输入流
InputStream is = System.in ;
Reader r = new InputStreamReader(is) ;
FileWriter fw = new FileWriter("d.txt") ;
char[] chs = new char[1024] ;
int len ;
while((len = r.read(chs))!=-1){
fw.write(chs, 0, len);
fw.flush();
}
r.close();
fw.close();
}
文件字符输入流:FileReader/FileWriter类
FileReader类从InputStreamReader类继承而来。。构造时使用系统默认的字符编码和默认字节缓冲区。
Windows系统的中文编码默认是GBK编码表。
字符不是字节,不能之间存储到文件中,文件只能存储字节,中间会有个缓冲区,会先保存到缓冲区中,不会直接进入文件中,使用flush刷新或close关闭的时候,会找到码表转成字节存入到文件中。
File file = new File("Hello1.txt");
// 创建文件
file.createNewFile();
// creates a FileWriter Object
FileWriter writer = new FileWriter(file);
// 向文件写入内容
writer.write("This\n is\n an\n example\n");
writer.flush();
writer.close();
// 创建 FileReader 对象
FileReader fr = new FileReader(file);
char[] a = new char[50];
fr.read(a); // 读取数组中的内容
for (char c : a)
System.out.print(c); // 一个一个打印字符
fr.close();
CharArrayReader/CharArrayWriter
继承了InputStreamReader/OutputStreamWriter,使用char数组作为数据源,用的比较少
对Reader/Writer行为的扩展
类似于字节流,也使用了装饰模式和责任链模式
FilterReader/FilterWriter
对Reader/Writer的代理,底层使用其他Reader/Writer作为真正的操作。
BufferedReader/BufferedWriter
继承了FilterReader/FilterWriter,BufferedReader使用char数组作为数据的缓冲区,读取数据先从缓存区读,读不到在从底层的Reader读,Reader其实用到是更底层的InputStream,尽量不要用BufferedInputStream作为底层InputStream,两层缓冲区没有必要。BufferedWriter先写入缓冲区,待缓冲区写满了再使用底层真正的Writer写,Writer其实用的是更底层的OutputStream。尽量不要用BufferedOutputStream作为底层OutputStream,两层缓冲区没必要。
PushbackReader
继承了FilterReader,实现了可退回的写,本质是使用了一个char数组,所以可退回是有界限。
PrintWriter
用于取代PrintStream,它可以java基本类型转换成字节输出,而且可以正确处理不同字符集的国际化问题。
开源库对Java IO的扩展
通过上面的解读我们知道,java IO本身的扩展点有两个,一个是通过继承对数据来源的扩展,第二个是通过装饰模式对行为进行扩展。下面介绍的commons-io选择了对行为进行扩展,并提供一些IO操作的工具方法,简化IO操作,而okio则不走寻常路,废弃了java IO的体系,设计出了source/sink接口体系。
commons-io
8.1.1、扩展行为
最新的commons-io 2.5提供了对input和output的各种扩展,通过继承FilterInputStream/FilterOutputStream实现
input:
AutoCloseInputStream:当IO流读到EOF时,会进行自动关闭
BOMInputStream:用于处理含有BOM的输入流,比如Windows下用记事本保存的文件
BoundedInputStream:含有读取界限的输入流,超过这个界限读取将会停止
CountingInputStream:含有统计功能的输入流
DemuxInputStream:这个输入流会将真正的流保存在ThreadLocal中
ProxyInputStream:一个抽象类,提供了读取一个字节之前后之后的回调方法
TaggedInputStream:这个类在抛异常的时候会给异常设置标记,从而用于跟踪异常
TeeInputStream:从一个源读取数据,同时会保存到一个指定的源,类似于unix的tee命令
UnixLineEndingInputStream:这个流在读取换行符的时候会按照unix格式读取
WindowsLineEndingInputStream:这个流在读取换行符的时候会按照Windows格式读取
output
ChunkedOutputStream:写入流的时候按照chunk分批写入
CountingOutputStream:具有统计功能的输出流
DemuxOutputStream:这个输出流会将真正的流保存在ThreadLocal中
ProxyOutputStream:一个抽象类,提供了写入一个字节之前后之后的回调方法
TaggedOutputStream:这个类在抛异常的时候会给异常设置标记,从而用于跟踪异常
TeeOutputStream:写数据到一个源,同时会保存到一个指定的源,类似于unix的tee命令
8.1.2、工具方法
IOUtils工具类,主要提供以下工具方法:
closeQuietly - 关闭一个流,忽略异常
toXxx/read - 从某个流读取数据
write - 向某个流写入数据
copy -从一个流复制到另一个流
contentEquals - 比较两个流中的内容
okio
如果使用原生的Java IO进行基本类型的读写,我们需要使用DataInputStream/DataOutputStream以及BufferedReader/BufferedWriter这四个类,除此之外,我们还需要了解InputStreamReader/OutputStreamWriter以及Java IO之间的责任链,对于一般的Java开发者来说,这显然太复杂了。于是okio重新设计了接口Source/Sink,提供了访问基本类型的接口和缓冲功能,同时屏蔽了底层复杂的IO体系,开发者只要传入InputStream和OutputStream就可以了。
具体的类关系如下:
使用Okio的Java代码如下:
BufferedSource bufferedSource = Okio.buffer(Okio.source(new FileInputStream("1.txt")));
int i = bufferedSource.readInt();
long l = bufferedSource.readLong();
String s = bufferedSource.readString(Charset.forName("UTF-8"));
BufferedSink bufferedSink = Okio.buffer(Okio.sink(new FileOutputStream("2.txt")));
bufferedSink.writeInt(1);
bufferedSink.writeLong(2L);
bufferedSink.writeString("123", Charset.forName("UTF-8"));
} catch (Exception e) {
// process exception
}