一、概述 

IO(输入、输出)是比较乏味的事情,因为没有肉眼可见的运行效果,但是输入、输出又是所有程序都必需的部分--使用输入机制以读取程序外的数据,使用输出机制允许程序将数据输入到外部存储设备中(磁盘、光盘、网络设备等)。

Java的IO通过Java.io包下的类和接口来支持,Java的IO包括文件IO和网络IO,本文主要讨论网络IO,Java的网络IO主要包括输入和输出两种IO流,每种输入、输出流又可以分为字节流和字符流两种。除此之外Java的IO流使用了装饰器模式,它将IO流分为分为底层的节点流和上层的处理流,其中节点流用于和底层的物理存储节点直接关联--不同的物理节点使用不同的方式获取数据,但程序可以把不同的节点流包装成统一的处理流,这样就允许程序可以以统一的方式使用这些资源。

 二、分类

Java把不同的输入输出源(键盘、文件、网络)抽象为"流",通过流的方式Java可以以相同的方式来处理这些资源,按照不同的分类方式,可以将流分为不同的形式,本文从以下几个角度对流进行分类,他们在概念上可以存在重叠:
    1、输入流和输出流
    按照流的流向来划分,可以分为输入流和输出流。
    输入流:可以读取数据,不能写入数据;
    输出流:可以写入数据,但是不能读取数据。
    这里的输入输出设计到了方向问题,对于下图的数据流向,数据从内存到硬盘,通常称为输出流,数据从硬盘到内存,称为输入流,也就是输入和输出是相对于内存或者说是当前程序来说的。

JavaIO原理 java的io_输入流

当然对于网络模型来说,客户端从服务器读取数据,客户端需要使用输入流,服务器需要输出数据到客户端,服务器使用输出流。

JavaIO原理 java的io_IO_02


    2、字节流和字符流

    按照不同的数据处理方式,可以分为字节流和字符流。

    字节流:以字节为单位的数据流

    字符流:以字符为单位的数据流

    计算机最基础的存储和操作单位是位(bit),Java体系中,每个字节占用8位(8bit),每个字符占用2个字节(16bit),除了数据单位不同之外,两种数据流的处理方式基本一致,字节流主要由InputStream和OutputStream作为基类,字符流由Reader和Writer作为基类。

    3、节点流和处理流

    按照流的角色来划分,可以分为节点流和处理流。

    节点流:关联特定设备(磁盘、网络程序、光盘)的数据流

    处理流:对节点流包装之后成为处理流,处理流用于实现数据的读写功能

JavaIO原理 java的io_输入流_03


    如上图所示,当时用处理流进行输入输出时,程序并不会直接关联到数据源,而是通过将节点流包装之后在处理,这样的好处就是用户程序可以使用相同的方式来处理不同的数据流,增加了程序的灵活性,并且处理流使用起来比节点流更加方便。

三、概念模型

Java把所有设备中获取的数据抽象成流模型,简化了输入输出的逻辑处理,Java体系提供了超过40个类来处理IO流,这些类看似繁多咱乱,实际上这些类之间有着紧密的联系。这些类大多由四个类派生,InputStream\Reader派生出所有的输入流,前者是字节流的基类,后者是字符流的基类;OutputStream\Writer派生出所有的输出流,前者是字节流基类,后者是字符流基类。

对于InputStream\Reader而言,它们把输入设备抽象成一个"水管",这个"水管"中的每个"水滴"一次排列,输入流使用隐式的指针来表示当前正准备读取那个"水滴",每次读取一个"水滴"之后,当前的记录指针则向后移动一步,字节流和字符流都提供了对应的方法来控制指针的移动。字节流和字符流处理模式一致,这两者之间只是处理的数据单元(水滴大小)不同。

JavaIO原理 java的io_IO_04

对于OutputStream\Writer而言,它们同样把输出设备抽象成了一个"水管",不同点是这个"水滴"是个空水管,每次需要输出数据时,程序依次将需要输出的数据(水滴)放入到输出流的"水管"中,输出流同样适用隐式的指针来标识当前正在输出哪个数据"水滴",每当输出一个数据,指针自动向后移动一步,字节流和字符流操作方式一致,两者都提供了方法来操作指针。

JavaIO原理 java的io_IO_05

处理流模型相对于节点流而言更加灵活,处理流将节点流进行包装之后再提供给用户,从用户角度看,处理流不再需要想节点流一样处理每个数据,处理流的有点体现在以下方面:
    1、通过提供缓冲的方式提高了数据读取的性能;
    2、提供了用户友好的API,使用更加方便;
    3、相对于节点流而言,可以提供统一的编程接口,简化逻辑复杂度;

 四、基类API

1、InputStream

 InputStream是所有字节输入流的基类,本身不能创建实例类,它给所有字节输入流提供了操作模板,InputStream提供的方法如下:

名称

 

功能

read()

int

从输入流读取下一个字节并返回,会阻塞直到读取到数据或者异常

read(byte b[])

int

从输入流中读取固定字节数,并且放入参数中,并返回,会阻塞直到读取到数据或者异常

read(byte b[], int off, int len)

int

从输入流中从off开始读取固定len个字节数,并返回,会阻塞直到读取到数据或者异常

skip(long n)

long

跳过且丢弃次输入流中的n个字节

available()

int

查询当前的输入流总有多少个可读取字节

close()

void

关闭当前输入流,释放资源

mark(int readlimit)

void

线程挂起时标记当前读取位置,线程恢复时,继续从当前位置读取数据

reset()

void

线程恢复时,继续从线程挂起时的输入流总读取数据

markSupported()

boolean

查询当前输入流是否支持挂起及恢复,以标记输入位置并开始读取

下面程序示范了使用FileInputStream来读取自身的效果:

public class FileInputStreamTest {

	public static void main(String[] args) {
		try {
			FileInputStream inputStream = new FileInputStream("FileInputStreamTest.java");
			byte[] buff = new byte[32];
			int hasRead = 0;
			while ((hasRead = inputStream.read(buff)) > 0) {
				String str = new String(buff, 0, hasRead);
				System.out.println(str);
			}
			inputStream.close();
		} catch (FileNotFoundException e) {
			e.printStackTrace();
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
	
}

    2、Reader

Reader是所有字符输入流的基类,跟InputStream一样,不能创建实例,给所有字符输入流提供了操作模板,Reader提供的方法如下:

名称

 

功能

read()

int

读取单个字符,会阻塞直到读取到数据

read(java.nio.CharBuffer target)

int

读取单个字符到缓冲区

read(char cbuf[])

int

读取字符到字符数组中,会阻塞直到读取完毕或者异常

read(char cbuf[], int off, int len)

int

从off开始读取len个字符到数组中,会阻塞直到读取完毕或者异常

skip(long n)

long

跳过此输入流中的n个字节,会阻塞直到读取完毕或者异常

ready()

boolean

查询当前输入流是否已经准备好可以读取数据

markSupported()

boolean

查询当前输入流是否支持挂起及恢复,以标记输入位置并开始读取

mark(int readAheadLimit)

void

线程挂起时标记当前读取位置,线程恢复时,继续从当前位置读取数据

reset()

void

线程恢复时,继续从线程挂起时的输入流总读取数据

close()

void

关闭当前输入流,释放资源,其他方法未执行完毕会抛出异常

下面程序示范了FileReader读取自身的示例:

public class FileReaderTest {

	public static void main(String[] args) {
		try {
			FileReader fileReader = new FileReader("FileReaderTest.java");
			char[] buff = new char[32];
			int hasRead = 0;
			while ((hasRead = fileReader.read(buff)) > 0) {
				System.out.println(new String(buff, 0, hasRead));
			}
			fileReader.close();
		} catch (FileNotFoundException e) {
			e.printStackTrace();
		} catch (Exception e) {
			e.printStackTrace();
		}
	}

}

    3、OutputStream

OutputStream是所有字节输出流的基类,本身不能创建实例类,它给所有字节输出流提供了操作模板,OutputStream提供的方法如下:

名称

返回值类型

功能

write(int b)

void

将指定的字节数据输出到输出流中

write(byte b[])

void

将字节数组输出到输出流中

write(byte b[], int off, int len)

void

将字节数组从off开始输出len个字节到输出流中

flush()

void

把缓冲区中的数据强制刷新到输出流中

close()

void

刷新缓冲区数据到输出流中,关闭输出流,释放资源

下面程序使用FileInputStream执行输入,并使用FileOutputStream进行输出,来实现复制FileOutputStreamTest的效果。

public class FileOutputStreamTest {

	public static void main(String[] args) {
		try {
			InputStream inputStream = new FileInputStream("FileInputStreamTest.java");
			OutputStream outputStream = new FileOutputStream("newFile.txt");
			byte[] buff = new byte[32];
			int hasRead = 0;
			while ((hasRead = inputStream.read(buff)) > 0) {
				outputStream.write(buff, 0, hasRead);
			}
			inputStream.close();
			outputStream.close();
		} catch (FileNotFoundException e) {
			e.printStackTrace();
		} catch (Exception e) {
			e.printStackTrace();
		}
	}

}

    4、Writer

Writer是所有字符输出流的基类,跟OutputStream一样,不能创建实例,给所有字符输出流提供了操作模板,Writer提供的方法如下:

名称

返回值类型

功能

write(int c)

void

输出单个字符,要输出的字符需要包含在给定整数值的16个低位中,16个高位被忽略掉

write(char cbuf[])

void

输出字符到字符数组中

write(char cbuf[], int off, int len)

void

从off开始输出len个字符到字符数组中

write(String str)

void

将字符串输出到输出流中

write(String str, int off, int len)

void

将字符串从off开始输出len个字符到输出流中

append(CharSequence csq)

Writer

将指定的字符序列追加到此输出流中,并返回该输出流

append(CharSequence csq, int start, int end)

Writer

将从start开始到end结束的字符序列追加到输出流中,并返回该输出流

append(char c)

Writer

追加字符到此输出流中并且返回该输出流

flush()

void

刷新缓冲数据到输出流中

close()

void

刷新缓冲数据到输出流中,关闭输出流,释放资源

下面程序示范了使用FileWriter直接输出文件的效果。

public class FileWriterTest {

	public static void main(String[] args) {
		try {
			Writer writer = new FileWriter("poem.txt");
			writer.write("静夜思--李白");
			writer.write("床前明月光,");
			writer.write("疑是地上霜.");
			writer.write("举头望明月,");
			writer.write("低头思故乡.");
			writer.close();
		} catch (IOException e) {
			e.printStackTrace();
		}
	}

}

    下面程序示范了使用处理流PrintWriter输出文件的效果。可以看到,处理流包装了一个节点输出流OutputStream,它隐藏了关联低层存储设备的细节,让程序更加专注于功能逻辑处理,从而简化程序开发复杂度。
 

public class PrintWriterTest {

	public static void main(String[] args) {
		try {
			OutputStream outputStream = new FileOutputStream("newTestFile.txt");
			PrintWriter printWriter = new PrintWriter(outputStream);
			printWriter.println("Hello World!");
			printWriter.println(new FileOutputStreamTest());
			printWriter.close();
		} catch (FileNotFoundException e) {
			e.printStackTrace();
		}
	}

}

五、输入输出流体系

Java的输入输出流体系超过40个类,之所以设计这么复杂,是因为Java为了实现更好的设计,所以从功能角度分为许多类,每个类型都提供了字节流和字符流两种类型,字节流和字符流又可以分为输入流和输出流两种,所以这个输入输出体系比较复杂。
    字节输入流体系如下:

JavaIO原理 java的io_JavaIO原理_06

名称

功能

FileInputStream

访问文件的字节流

ByteArrayInputStream

访问数组的字节流

PipedInputStream

访问管道的字节流

BufferedInputStream

带缓冲区的字节流

ObjectInputStream

对象输入字节流

FilterInputStream

带过滤功能的字节输入流

PushbackInputStream

回退输入流

DataInputStream

特殊流

   字节输出流体系如下:

JavaIO原理 java的io_输出流_07

名称

功能

FileOutputStream

访问文件的字节流

ByteArrayOutputStream

访问数组的字节流

PipedOutputStream

访问管道的字节流

BufferedOutputStream

带缓冲区的字节流

ObjectOutputStream

对象输出字节流

FilterOutputStream

带过滤功能的字节输出流

PrintStream

打印输入流

DataOutputStream

特殊流

    字符输入流体系如下:

JavaIO原理 java的io_输入流_08

名称

功能

FileReader

访问文件的字符流

CharArrayReader

访问数组的字符流

PipedReader

访问管道的字符流

StringReader

访问字符串的字符流

BufferedReader

带缓冲区的字符流

InputStreamReader

将字节输入流转换为字符输入流

FilterReader

带过滤功能的字符输入流

PushbackReader

回退输入流

    字符输出流体系如下:

JavaIO原理 java的io_输入流_09

名称

功能

FileWriter

访问文件的字符流

CharArrayWriter

访问数组的字符流

PipedWriter

访问管道的字符流

StringWriter

访问字符串的字符流

BufferedWriter

带缓冲区的字符流

OutputStreamWriter

将字节输出流转换为字符输出流

FilterWriter

带过滤功能的字符输出流

PrintWriter

打印输出流

    通常来说,字符流比字节流功能更加强大,计算机中的数据是按照二进制字节存储的,字节流可以直接处理这些数据;而文本文件是按照字符进行存储的,使用字符流进行处理会更加简单,所以如果输入输出数据是文本内容,则考虑使用字符流,输入输出数据是二进制文件,则考虑使用字节流处理。