装饰流使用
除了按照流的方向可以把流划分为输入流和输出流两类,按照流读写数据的基本单位把流划分为字节流和字符流两类以外,还可以按照流是否直接连接实际数据源,例如文件、网络、字节数组等,将流又可以划分为实体流和装饰流两大类。
其中实体流指直接连接数据源的流类,如前面介绍的FileInputStream/FileOutputStream和FileReader和FileWriter,该类流直接实现将数据源转换为流对象,在实体流类中实现了流和数据源之间的转换,实体流类均可单独进行使用。
而装饰流指不直接连接数据源,而是以其它流对象(实体流对象或装饰流对象)为基础建立的流类,该类流实现了将实体流中的数据进行转换,增强流对象的读写能力,比较常用的有DataInputStream/DataOutputStream和BufferedReader/BufferedWriter等,装饰流类不可以单独使用,必须配合实体流或装饰流进行使用。
由于装饰流都是在已有的流对象基础上进行创建的,所以这种创建流的方式被称作“流的嵌套”,通过流的嵌套,可以修饰流的功能,例如使读写的速度增加或者提供更多的读写方式,方便数据格式的处理。
装饰流不改变原来实体流对象中的数据内容,只是从实体流对象基础上创建出的装饰流对象相对于实体流对象进行了一些功能的增强。
流的嵌套是学习IO编程时必须掌握的知识,使用它才可以让你真正体会到IO类设计时的设计思路,也可以方便的使用IO类。
下面分别以DataInputStream/DataOutputStream和BufferedReader/BufferedWriter为例子,详细介绍装饰类的使用。
11.3.3.1 DataInputStream/DataOutputStream
在前面的示例中,在向流中写入的数据必须首先转换为byte数组或char数 组,当写入的数据比较少、比较简单时,则向流中写入数据时还是不是很麻烦的,但是如果向流中写入数据比较多时,手动转换数据格式则会比较麻烦。当然,很多 文件都是根据文件存储的需要设计了专门的存储格式,但是这些格式一般都比较复杂,需要阅读专门的格式文档才可以读写这些特定格式的文件。
为了简化程序员对于流的操作,使得程序员可以从繁杂的数据格式中解脱出来,在IO类中专门设计了两个类——DataInputStream/DataOutputStream类简化流数据的读写,使用这两个类,可以实现以增强型的读写方法读写数据,使得读写流的数据变得比较简单。
在实际使用这两个类时,必须匹配起来进行使用。也就是说,只有使用DataOutputStream流格式写入的数据,在实际读取时才可以使用DataInputStream进行读取。因为在使用DataOutputStream向流中写入数据时,除了写入实际的数据内容以外,还写入了特定的数据格式,该格式对于程序员来说是透明的,这种特定的格式不需要程序员熟悉,而只需要使用DataInputStream读取即可,读取时的顺序和写入时的顺序和类型保持一致即可。
在DataInputStream类中,增加了一系列readXXX的方法,例如readInt、readUTF、readBoolean等等,而在DataOutputStream类中,也增加了一系列writeXXX的方法,例如writeInt、writeUTF、writeBoolean等等,使得对于数据的读写更加方便很容易。
下面以读写文件为例子,演示DataInputStream/DataOutputStream类的基本使用。
/**
* 模拟需要存储到文件中的数据
* 该类中保存4种类型的数据
*/
public class MyData {
boolean b;
int n;
String s;
short sh[];
public MyData(){}
public MyData(boolean b,int n,String s,short sh[]){
this.b = b;
this.n = n;
this.s = s;
this.sh = sh;
}
}
在该示例中,需要将MyData类型的对象内部保存的数据按照一定的格式存储到文件中,这里列举了2种基本数据类型boolean和int,以及两种引用数据类型String和数组,在下面的示例代码中将会以一定的格式写入到文件中。
import java.io.*;
/**
* 使用DataOutputStream书写具有一定格式的文件
*/
public class WriteFileUseDataStream {
public static void main(String[] args) {
short sh[] = {1,3,134,12};
MyData data =new MyData(true,100,"Java语言",sh);
//写入文件
writeFile(data);
}
/**
* 将MyData对象按照一定格式写入文件中
* @param data 数据对象
*/
public static void writeFile(MyData data){
FileOutputStream fos = null;
DataOutputStream dos = null;
try{
//建立文件流
fos = new FileOutputStream("test.my");
//建立数据输出流,流的嵌套
dos = new DataOutputStream(fos);
//依次写入数据
dos.writeBoolean(data.b);
dos.writeInt(data.n);
dos.writeUTF(data.s);
//写入数组
int len = data.sh.length;
dos.writeInt(len); //数组长度
//依次写入每个数组元素
for(int i = 0;i < len;i++){
dos.writeShort(data.sh[i]);
}
}catch(Exception e){
e.printStackTrace();
}finally{
try {
dos.close();
fos.close();
} catch (Exception e2){
e2.printStackTrace();
}
}
}
}
在该示例代码中,首先建立一个实体流fos,该实体流连接到数据源——文件,然后以该实体流对象为基础,使用流的嵌套,建立装饰流对象dos,由于需要写入流中的对象data中包含的数据比较多,所以需要以一定的格式写入流,这里使用DataOutputStream避免自定义数据格式,而写入流中的顺序就是该流的格式,也就是文件test.my的格式,这种格式对于程序员来说是透明的。
使用对象dos中对应的writeXXX方法依次将需要存储的数据写入流中,在写入字符串时,为了使字符编码保持一致,一般使用writeUTF写入字符串,也就是先将字符串转换为utf-8格式的byte数组,然后再将该数组以一定的格式写入到流中。而在写入数组时,则首先写入数组的长度,然后再将数组的内容依次写入到流中,使用这种方式就可以很方便的将数组写入到流中。
这样文件test.my文件就具有了自己特定的文件格式,程序员需要记忆的就是该文件在写入时的写入顺序,可以很方便的使用DataInputStream读取出来。
下面的代码是使用DataInputStream读取test.my文件的代码,注意文件格式的处理。
import java.io.*;
/**
* 使用DataInputStream读取自定义格式的文件
*/
public class ReadFileUseDataStream {
public static void main(String[] args) {
MyData data = readFile();
System.out.println(data.b);
System.out.println(data.n);
System.out.println(data.s);
int len = data.sh.length;
for(int i = 0;i < len;i++){
System.out.println(data.sh[i]);
}
}
/**
* 从文件test.my中读取数据,并使用读取到的数据初始化data对象
* @return 读取到的对象内容
*/
public static MyData readFile(){
MyData data = new MyData();
FileInputStream fis = null;
DataInputStream dis = null;
try {
//建立文件流
fis = new FileInputStream("test.my");
//建立数据输入流,流的嵌套
dis = new DataInputStream(fis);
//依次读取数据,并赋值给data对象
data.b = dis.readBoolean();
data.n = dis.readInt();
data.s = dis.readUTF();
int len = dis.readInt();
data.sh = new short[len];
for(int i = 0;i < len;i++){
data.sh[i] = dis.readShort();
}
} catch (Exception e) {
e.printStackTrace();
}finally{
try {
dis.close();
fis.close();
} catch (Exception e) {
e.printStackTrace();
}
}
return data;
}
}
在该示例代码中,首先建立实体流fis,然后以该流对象为基础建立dos装饰流,然后按照写入文件的顺序,依次将流中的数据读取出来,并将读取到的数值赋值给data对象中对应的属性,从而实现将数据从文件中恢复到实际的对象。
最后再次强调,DataInputStream和DataOutputStream必须匹配起来进行使用,也就是使用DataInputStream读取的流数据必须是使用DataOutputStream流写入的数据,这样才能保持格式上的统一。
当然,使用DataInputStream和DataOutputStream和其它的实体流也可以匹配起来进行使用,例如和ByteArrayInputStream和ByteArrayOutputStream匹配使用将可以实现方便的把数据转换为特定格式的byte数组以及将byte数组恢复回来,使用的格式和上面的示例类似,这里就不再重复了。
BufferedReader/BufferedWriter
在进行IO操作时,除了功能以外,程序的执行效率也是必须要考虑的问题。基本的IO类只是注重功能的实现,例如将特定的数据源转换为流对象,而没有过多的关注读写的效率问题,而实际在进行项目开发时,读写效率也是必须要考虑的问题。
为了提高IO类的读写效率,在装饰流中专门制作了一类缓冲流,该类流的作用就是提高流的读写效率,这组缓冲流包含:BufferedInputStream/BufferedOutputStream、BufferedReader/BufferedWriter。
该部分以BufferedReader/BufferedWriter为基础进行介绍。
由于前面介绍DataInputStream/DataOutputStream时,是以文件流作为实体流进行介绍,这里就不再重复了,这里以前面介绍的接收控制台输入为基础介绍缓冲输入流的使用。
由于装饰流在进行嵌套时,只能嵌套相同类型的流,例如InputStream类型的流之间可以嵌套,但是InputStream和Reader两个体系之间的流就无法直接嵌套,为了使用新的IO类带来的特性,在IO类中提供了两个专门的类,实现体系之间的转换,这两个流类被形象的称为“桥接流”。
桥接流主要包含2个,依次是:
1、InputStreamReader
该类实现将InputStream及其子类的对象转换为Reader体系类的对象,实现将字节输入流转换为字符输入流。
2、OutputStreamWriter
该类实现将OutputStream及其子类的对象转换为Writer体系类的对象,实现将字节输入流转换为字符输入流。
这两个桥接流使得字节流可以被转换为字符流,但是需要注意的是,字符流无法转换为字节流。
在读取控制台输入时,直接使用System.in进行读取,虽然在功能上可以实现,但是这种方式读写流的效率比较差,所以在实际使用时一般需要提高读写的效率,这就需要使用装饰流中的缓冲流,这是一个典型的流的嵌套的示例。该代码实现的功能是如果回显用户输入,当用户输入quit时程序退出。该示例的代码如下:
import java.io.*;
/**
* 使用BufferedReader读取控制台输入
*/
public class ReadConsoleWithBuffer {
public static void main(String[] args) {
BufferedReader br = null;
String s = null;
try{
//使用流的嵌套构造缓冲流
br = new BufferedReader(
new InputStreamReader(System.in));
do{
//输出提示信息
System.out.println("请输入:");
//按行读取输入
s = br.readLine();
//输出用户输入
System.out.println(s);
}while(!s.equals("quit"));
}catch(Exception e){
e.printStackTrace();
}finally{
try{
br.close();
}catch(Exception e){
e.printStackTrace();
}
}
}
}
在该示例代码中,首先使用流的嵌套构建了BufferedReader类型的对象br,然后使用BufferedReader中的readLine方法,每次读取用户输入的一行信息,使用readLine方法读取内容时,系统以”\r\n”作为每次的结束符号,而且读取的内容不包含”\r\n”,当读取到流的末尾时readLine方法的返回值是null。然后使用do-while循环判断用户输入的是否是quit,如果输入的是quit,则程序结束,否则继续下一次循环。
关于BufferedWriter的使用,没有什么特别的地方,这里就不单独举例说明了。
11.3.3.3 装饰流小结
前面介绍了两类比较常见的装饰流,在实际的开发中,根据逻辑的需要还可能会用到其它的装饰流,这些装饰流的使用和前面介绍的类类似,在实际使用时通过查阅JDK API文档找到根据功能找到合适的装饰流,然后进行使用即可。