一、概述
IO(输入、输出)是比较乏味的事情,因为没有肉眼可见的运行效果,但是输入、输出又是所有程序都必需的部分--使用输入机制以读取程序外的数据,使用输出机制允许程序将数据输入到外部存储设备中(磁盘、光盘、网络设备等)。
Java的IO通过Java.io包下的类和接口来支持,Java的IO包括文件IO和网络IO,本文主要讨论网络IO,Java的网络IO主要包括输入和输出两种IO流,每种输入、输出流又可以分为字节流和字符流两种。除此之外Java的IO流使用了装饰器模式,它将IO流分为分为底层的节点流和上层的处理流,其中节点流用于和底层的物理存储节点直接关联--不同的物理节点使用不同的方式获取数据,但程序可以把不同的节点流包装成统一的处理流,这样就允许程序可以以统一的方式使用这些资源。
二、分类
Java把不同的输入输出源(键盘、文件、网络)抽象为"流",通过流的方式Java可以以相同的方式来处理这些资源,按照不同的分类方式,可以将流分为不同的形式,本文从以下几个角度对流进行分类,他们在概念上可以存在重叠:
1、输入流和输出流
按照流的流向来划分,可以分为输入流和输出流。
输入流:可以读取数据,不能写入数据;
输出流:可以写入数据,但是不能读取数据。
这里的输入输出设计到了方向问题,对于下图的数据流向,数据从内存到硬盘,通常称为输出流,数据从硬盘到内存,称为输入流,也就是输入和输出是相对于内存或者说是当前程序来说的。
当然对于网络模型来说,客户端从服务器读取数据,客户端需要使用输入流,服务器需要输出数据到客户端,服务器使用输出流。
2、字节流和字符流
按照不同的数据处理方式,可以分为字节流和字符流。
字节流:以字节为单位的数据流
字符流:以字符为单位的数据流
计算机最基础的存储和操作单位是位(bit),Java体系中,每个字节占用8位(8bit),每个字符占用2个字节(16bit),除了数据单位不同之外,两种数据流的处理方式基本一致,字节流主要由InputStream和OutputStream作为基类,字符流由Reader和Writer作为基类。
3、节点流和处理流
按照流的角色来划分,可以分为节点流和处理流。
节点流:关联特定设备(磁盘、网络程序、光盘)的数据流
处理流:对节点流包装之后成为处理流,处理流用于实现数据的读写功能
如上图所示,当时用处理流进行输入输出时,程序并不会直接关联到数据源,而是通过将节点流包装之后在处理,这样的好处就是用户程序可以使用相同的方式来处理不同的数据流,增加了程序的灵活性,并且处理流使用起来比节点流更加方便。
三、概念模型
Java把所有设备中获取的数据抽象成流模型,简化了输入输出的逻辑处理,Java体系提供了超过40个类来处理IO流,这些类看似繁多咱乱,实际上这些类之间有着紧密的联系。这些类大多由四个类派生,InputStream\Reader派生出所有的输入流,前者是字节流的基类,后者是字符流的基类;OutputStream\Writer派生出所有的输出流,前者是字节流基类,后者是字符流基类。
对于InputStream\Reader而言,它们把输入设备抽象成一个"水管",这个"水管"中的每个"水滴"一次排列,输入流使用隐式的指针来表示当前正准备读取那个"水滴",每次读取一个"水滴"之后,当前的记录指针则向后移动一步,字节流和字符流都提供了对应的方法来控制指针的移动。字节流和字符流处理模式一致,这两者之间只是处理的数据单元(水滴大小)不同。
对于OutputStream\Writer而言,它们同样把输出设备抽象成了一个"水管",不同点是这个"水滴"是个空水管,每次需要输出数据时,程序依次将需要输出的数据(水滴)放入到输出流的"水管"中,输出流同样适用隐式的指针来标识当前正在输出哪个数据"水滴",每当输出一个数据,指针自动向后移动一步,字节流和字符流操作方式一致,两者都提供了方法来操作指针。
处理流模型相对于节点流而言更加灵活,处理流将节点流进行包装之后再提供给用户,从用户角度看,处理流不再需要想节点流一样处理每个数据,处理流的有点体现在以下方面:
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为了实现更好的设计,所以从功能角度分为许多类,每个类型都提供了字节流和字符流两种类型,字节流和字符流又可以分为输入流和输出流两种,所以这个输入输出体系比较复杂。
字节输入流体系如下:
名称 | 功能 |
FileInputStream | 访问文件的字节流 |
ByteArrayInputStream | 访问数组的字节流 |
PipedInputStream | 访问管道的字节流 |
BufferedInputStream | 带缓冲区的字节流 |
ObjectInputStream | 对象输入字节流 |
FilterInputStream | 带过滤功能的字节输入流 |
PushbackInputStream | 回退输入流 |
DataInputStream | 特殊流 |
字节输出流体系如下:
名称 | 功能 |
FileOutputStream | 访问文件的字节流 |
ByteArrayOutputStream | 访问数组的字节流 |
PipedOutputStream | 访问管道的字节流 |
BufferedOutputStream | 带缓冲区的字节流 |
ObjectOutputStream | 对象输出字节流 |
FilterOutputStream | 带过滤功能的字节输出流 |
PrintStream | 打印输入流 |
DataOutputStream | 特殊流 |
字符输入流体系如下:
名称 | 功能 |
FileReader | 访问文件的字符流 |
CharArrayReader | 访问数组的字符流 |
PipedReader | 访问管道的字符流 |
StringReader | 访问字符串的字符流 |
BufferedReader | 带缓冲区的字符流 |
InputStreamReader | 将字节输入流转换为字符输入流 |
FilterReader | 带过滤功能的字符输入流 |
PushbackReader | 回退输入流 |
字符输出流体系如下:
名称 | 功能 |
FileWriter | 访问文件的字符流 |
CharArrayWriter | 访问数组的字符流 |
PipedWriter | 访问管道的字符流 |
StringWriter | 访问字符串的字符流 |
BufferedWriter | 带缓冲区的字符流 |
OutputStreamWriter | 将字节输出流转换为字符输出流 |
FilterWriter | 带过滤功能的字符输出流 |
PrintWriter | 打印输出流 |
通常来说,字符流比字节流功能更加强大,计算机中的数据是按照二进制字节存储的,字节流可以直接处理这些数据;而文本文件是按照字符进行存储的,使用字符流进行处理会更加简单,所以如果输入输出数据是文本内容,则考虑使用字符流,输入输出数据是二进制文件,则考虑使用字节流处理。