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