IO流的操作在工作中由于都是工具类来完成,导致一直对其理解不够深,想写篇文章来学习下,分类如下:

io流的分类有哪些JAVA io流的分类有哪些_输出流

一、IO流的概念

Java的IO流是实现输入/输出的基础,它可以方便地实现数据的输入/输出操作,在Java中把不同的输入/输出源抽象表述为"流"。流是一组有顺序的,有起点和终点的字节集合,是对数据传输的总称或抽象。即数据在两设备间的传输称为流,流的本质是数据传输,根据数据传输特性将流抽象为各种类,方便更直观的进行数据操作。

流有输入和输出,输入时是流从数据源流向程序。输出时是流从程序传向数据源,而数据源可以是内存,文件,网络或程序等。

io流的分类有哪些JAVA io流的分类有哪些_io流的分类有哪些JAVA_02

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。

io流的分类有哪些JAVA io流的分类有哪些_io流的分类有哪些JAVA_03

文件字节输入流: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实现了装饰模式。

io流的分类有哪些JAVA io流的分类有哪些_输入流_04


责任链模式则是定义统一的接口,然后通过多个实现该接口的子类串行协作完成一项复杂的功能。Java通过将多个FilterInputStream/FilterOutputStream(不是fileinputstream)的子类串联起来实现了责任链模式。

io流的分类有哪些JAVA io流的分类有哪些_输入流_05

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

io流的分类有哪些JAVA io流的分类有哪些_数据_06

//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就可以了。

具体的类关系如下:

io流的分类有哪些JAVA io流的分类有哪些_输出流_07


使用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
}