本文总结自《疯狂Java讲义》
IO流分类:
根据数据流向的不同可分为输入流和输出流;
根据处理的数据类型不同可分为字节流和字符流;
根据流的角色来分,可以分为节点流和处理流;
1. 输入流输出流:
一个流的流向是相对于当前这个程序的,从这个程序的内存中传输数据出去,就是输出流;将数据从外部写入程序内存中,就是输入流。
在 Java 中,输入流主要由 InputStream 和 Reader 作为基类,输出流主要由 OutputStream 和 Writer 作为基类。
2. 字节流字符流:
这两个流的用法几乎完全相同,区别在于字节流和字符流所操作的数据单元不同——字节流操作的数据单元是 8 位的字节,而字符流操作的数据单元是 16 位的字符。
在 Java 中,字节流主要由 InputStream 和 OutputStream作为基类,字符流主要由 Reader 和 Writer 作为基类。
3. 节点流处理流:
可以从/向一个特定的 IO 设备(如磁盘、网络)读/写数据的流,称为节点流,节点流也被称为低级流。
处理流则用于对一个已存在的流进行连接或封装,通过封装后的流来实现数据读/写功能。处理流也称为高级流。
节点流和处理流就是一个典型的装饰器设计模式,通过使用处理流来包装节点流,既可以消除不同节点流的实现差异,也可以提供更方便的方法来完成输入/输出功能。
比如上一篇的序列化的介绍中,就是用处理流 ObjectOutputStream 包装节点流 ByteArrayWriter ,再调用 ObjectOutputStream 的方法将序列化的对象写入文件中。
处理流:
- Buffering缓冲流:在读入或写出时,对数据进行缓存,以减少I/O的次数:BufferedReader与BufferedWriter、BufferedInputStream与BufferedOutputStream。
- Filtering 滤流:在数据进行读或写时进行过滤:FilterReader与FilterWriter、FilterInputStream与FilterOutputStream。
- Converting between Bytes and Characters 转换流:按照一定的编码/解码标准将字节流转换为字符流,(Stream到Reader):InputStreamReader、OutputStreamWriter。
- Object Serialization 对象流 :ObjectInputStream、ObjectOutputStream。
- DataConversion数据流: 按基本数据类型读、写(处理的数据是Java的基本类型(如布尔型,字节,整数和浮点数)):DataInputStream、DataOutputStream 。
- Peeking Ahead预读流: 通过缓存机制,进行预读 :PushbackReader、PushbackInputStream。
- Printing打印流: 包含方便的打印方法 :PrintWriter、PrintStream
节点流:
- 文件:File开头的流
- 数组:CharArray或ByteArray开头的流
- 字符串:String开头的流
- 管道:Pipe开头的流
我们通过查看源码可以发现,所有的处理流都会有以 Reader、Writer、InputStream、OutputStream 作为参数的构造器,就是用来传入(装饰)节点流的。
而节点流中,我们可以发现他们也都是分别继承自 Reader、Writer、InputStream、OutputStream ,或者他们的子类 InputStreamReader、OutputStreamWriter,但是没有这些构造器。
输入流InputStream和Reader
InputStream 和 Reader 是所有输入流的抽象基类,本身并不能创建实例来执行输入,但他们将成为所有输入流的模板,所以他们的方法是所有输入流都可以使用的方法。
他们中包含如下三个方法(Reader的方法中的参数不是 byte[] 而是char[] ):
- int read():从输入流中读取单个字节,返回所读取的字节数据(字节数据可直接转换为int类型)
- int read(byte[] b):从输入流中最多读取 b.length 个字节的数据,并将其存储在字节数组 b 中,返回实际读取的字节数
- int read(byte[] b, int off, int len):从输入流中最多读取 len 个字节的数据,并将其存储在数组 b 中,放入数组 b 中时,并不是从数组起点开始,而是从 off 位置开始,返回实际读取的字节数。
此处使用他们的子类,一个节点流 FileInputStream 示范一下读取文件内容的效果:
先在项目目录中创建一个 txt 文件,写入内容 “文件输入流” ,并命名为 test.txt 。
执行下面的程序:
public static void main(String[] args) {
FileInputStream fis = null;
try {
fis = new FileInputStream("test.txt");
byte[] bbuf = new byte[1024];
int hasRead = 0; //用来存储实际读取的数目
while((hasRead = fis.read(bbuf))>0){ //重复读取,但文件内数据较少一次就可以读取完。
System.out.println(new String(bbuf, 0, hasRead)); //将读出来的字节转为字符串输出
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
fis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
输出结果为
文件输入流
除此之外,InputStream 和 Reader 还支持如下几个方法来移动记录指针:
- void mark(int readAheadLimit):在记录指针当前位置记录一个标记(mark)。
- boolean markSupported():判断此输入流是否支持 mark() 操作,即是否支持记录标记。
- void reset():将此流的记录指针重新定位到上一次记录的标记(mark)的位置。
- long skip(int n):记录指针向前移动 n 个字节/字符
输出流OutputStream和Writer
OutputStream 和 Writer 也非常相似,两个流都提供了如下三个方法(Reader的方法中的参数不是 byte[] 而是char[] ):
- int write(int c):将指定的字节/字符输出到输出流中,(其中 c 既可以代表字节,也可以代表字符)
- int write(byte[] b):将字节数组/字符数组中的数据输出到指定输出流中。
- int write(byte[] b, int off, int len):将字节数组/字符数组中从 off 位置开始,长度为 len 的字节/字符输出到输出流中
因为字符流直接以字符作为操作单位。所以 Writer 可以用字符串来代替字符数组,即以 String 对象作为参数。Writer 里还包含如下两个方法:
- void write(String str):将 str 字符串里包含的字符输出到指定输出流中
- void write(String str, int off, int len):将 str 字符串里从 off 位置开始,长度为 len 的字符输出到指定输出流中。
下面来个示范:
public static void main(String[] args) {
FileInputStream fis = null;
FileOutputStream fos = null;
try {
fis = new FileInputStream("test.txt");
fos = new FileOutputStream("newtest.txt");
byte[] bbuf = new byte[1024];
int hasRead = 0;
while((hasRead = fis.read(bbuf))>0){
fos.write(bbuf, 0, hasRead);
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
fis.close();
fos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
执行完后,可以发现项目目录先多了一个 newtest.txt ,里面的内容与 test.txt 一致
注:
- 当使用完IO流后,要记得关闭,如果有输出流有 flush() 方法,应在 close() 执行前执行一次,保证数据的完整输出,尤其是在非 Java 封装的流中
- 如果用处理流包装了节点流,关闭处理流即可,节点流会由处理流自动关闭
转换流
- InputStreamReader 将字节输入流转换成字符输入流。
- OutputStreamWriter 将字节输出流转换成字符输出流。
Java使用System.in代表标准输入,即键盘输入,但这个标准输入流是字节输入流 InputStream 类的实例,使用不太方便,而且我们知道键盘输入内容都是文本内容,所以我们可以使用 InputStreamReader 将其转化成字符输入流,普通 Reader 读取输入内容时依然不太方便,我们可以将普通 Reader 再次包装成 BufferedReader ,利用 BufferedReader 的 readLine() 方法可以一次读取一行内容。如下程序所示:
public static void main(String[] args) {
BufferedReader br = null;
try{
//将System.in对象转换成Reader对象
InputStreamReader reader = new InputStreamReader(System.in);
//将普通Reader包装成BufferedReader
br = new BufferedReader(reader);
String buffer = null;
//采用循环方式来一行一行的读取
while((buffer = br.readLine()) != null){
//如果读取的字符串为"exit", 程序退出
if(buffer.equals("exit")){
System.exit(1);
}
System.out.println("输入内容为:"+buffer);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try{
br.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
为什么没有字符流转为字节流的转换流?
字节流比字符流的使用范围更广,但字符流比字节流操作方便。如果有一个流已经是字符流了,也就是说是一个用起来更方便的流,为什么要转换成字节流呢?反之,如果现在已有一个字节流,但我们知道这个字节流的内容都是文本内容,那么把它转换成字符流来处理就会更方便一些,所以Java只提供了将字节流转换字符流的转换流,没有提供将字符流转换成字节流的转换流。