额外功能处理流的意思是在基础流(InputStream/OutputStream/Reader/Writer)的基础上提供额外的功能。常见的额外功能可归纳为以下几种。
- 是否使用缓冲功能:BufferedInputStream/BufferedOutputStream/BufferedReader/BufferedWriter(字符流的缓冲对象还提供了操作行的方法)
- 是否串联多个字节输入流:SequenceInputStream
- 是否使用对象序列化功能:ObjectInputStream/ObjectOutputStream(涉及序列化接口Serializable)
- 是否让流保证数据类型不变:DataInputStream/DataOutputStream
- 是否让输出流输出时保证输出字面符号:PrintStream/PrintWriter(打印流)
- 是否要操作内存中的字符串和数组:ByteArrayInputStream/ByteArrayOutputStream/CharArrayReader/CharArrayWriter
Bufferedxxx类和Array相关的功能此处不做介绍。本文将介绍除此之外的其余功能以及对象序列化时涉及到的序列化接口Serializable。
1.输入流的串联:SequenceInputStream
SequenceInputStream按照IO体系命名的特点来理解,大致是"将字节输入流存放到Sequence序列中",实际上,它可用来串联多个输入流。意思是:有输入流1、输入流2、输入流3,原本的行为是按照顺序先后读取输入流1、2、3,现在将这3个输入流按顺序连起来当作一个大输入流,直到输入流3读完后才到流的末尾。
这个序列输入流类在IO体系里有点特立独行,它只有输入流,没有对应的输出流。它的作用是以操作一个输入流的方式来将多个输入流按序追加读取。例如,将多个文件的数据以追加的方式写入到一个目标文件中。
当调用SequenceInputStream的close()方法时,它将会自动关闭所有它所串联的输入流。
如下图:
要使用SequenceInputStream,首先看构造方法SequenceInputStream(Enumeration<? extends InputStream> e)
,可见它只能接收枚举出来的字节输入流。但如何获取到这些枚举元素?可以将各个输入流存放到一个集合中,然后使用Collections工具类中的enumeration(Collection c)方法将这个集合转换为Enumeration对象。在此还需说明的是,通常SequenceInputStream要串联的多个流都是有先后顺序的,例如1.txt,2.txt,3.txt依序串联下去,所以枚举时也要保证能够依序枚举出来,这也要求在Collection转换为Enumeration时,集合中的流对象在集合中也是有序的,这意味着使用List集合来存储这些流对象是最佳的。
例如,下面的示例中将{1..6}.txt共6个txt文件按文件名排序先后串联成一个SequenceInputStream。
//存储多个字节输入流对象到List集合中
List<FileInputStream> list = new ArrayList<FileInputStream>();
for(int i=1;i<=6;i++){
list.add(new FileInputStream(i+".txt"));
}
//将List集合转换为枚举对象Enumeration
Enumeration<FileInputStream> en = Collections.enumeration(list);
//将枚举出来的各个字节输入流串联起来
SequenceInputStream sis = new SequenceInputStream(en);
2. ObjectInputStream/ObjectOutputStream和序列化接口Serializable
输入流和输出流可以按字节、存储读取媒体类、文本类文件,但能否将java中的对象也作为数据持久化到文件中呢?io包中提供了ObjectInputStream和ObjectOutputStream来读、写对象。
例如给定如下Student类,将以此类作为ObjectInputStream/ObjectOutputStream流读、写的对象。
class Student {
String name;
int age;
Student(String name,int age){
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public int getAge() {
return age;
}
public String toString(){
return "{name="+name+",age="+age+"}";
}
}
下面使用ObjectOutputStream将Student对象写入到文件中,这类文件的规范后缀名为".object"。该类的构造方法为ObjectOutputStream(OutputStream out)
。
import java.io.*;
import java.util.*;
public class ObjectStream {
public static void main(String[] args) throws IOException {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("d:/temp/a.txt"));
Student stu = new Student("malongshuai",22);
oos.writeObject(stu);
}
}
编译并执行上述代码,将抛出NotSerializableException
异常,意思是未序列化。那么谁没有序列化?Student对象。要想让某对象序列化的方式很简单,只需让Student类实现Serializable接口即可。如下:
class Student implements Serializable {
String name;
int age;
Student(String name,int age){
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public int getAge() {
return age;
}
public String toString(){
return "{name="+name+",age="+age+"}";
}
}
可以使用ObjectInputStream从文件中读取曾被序列化的数据。这称为"反序列化"。
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("d:/temp/a.txt"));
Object obj = ois.readObject();
System.out.println("read Object from file:"+stu1.toString()); //多态
看上去序列化和反序列化是一件很简单的事情。确实如此,但其有不少知识点和需要注意的关键点。
- 什么是序列化?
从前面的例子中可以看出,序列化的方式非常简单,只需实现Serializable接口即可。它不提供任何方法。序列化的意义仅仅只是为类进行一种特殊的标识,即所谓的"盖戳"。就像夫妻如何证明他们是夫妻,颁发一个结婚证即可,再例如猪肉凭什么是合格的?给它贴一张合格标签即可。 - 序列化的目的是什么?
为了将某些对象持久化保存起来,供以后反序列化的时候读取。 - 序列化后的对象在存储时会存储哪些数据?
存储的内容包括:类名和类签名(类的序列化版本号SerialVersionUID)、对象的字段值和数组值,以及从初始对象中引用的其他所有对象的闭包。大致可以看作是存储了一个类的版本号、类名、某些字段的值以及引用的对象。当然,并非所有字段值都会存储,见下文的第6点。 - 对某个对象序列化后,修改对象的属性(例如将成员变量的修饰符从public改为private),反序列化时将会如何?
因为保存起来的序列化数据带有一个类签名SerialVersionUID,而修改类的定义后,编译时这个类会生成Class文件,而这个文件中的版本号将不再和之前序列化时保存的版本号相同。于是抛出异常。
由此可以明确一点:class文件中仅只存储类的定义语句,在new对象时将在堆内存中开辟一段空间并存储对象数据(如成员变量)。反序列化实际上是将保存起来的类对象数据加载到这个开辟出来的对象空间中。java.io.InvalidClassException: Student; local class incompatible: stream classdesc serialVersionUID = -9151998530267376490, local class serialVersionUID = -3521625297801190192
- 如何保证反序列化的成功?
强烈建议显式在实现了Serializable接口的类中,声明一个固定的序列化。如:
这样一来,无论是序列化保存时,还是后来修改了类定义生成的class文件中,其版本号都是固定且相同的,也就是说不会因为序列化版本号不同而反序列化失败。public/private/... static final long serialVersionUID = 123456L;
- 类中所有属性都应该序列化吗?
显然不是。有两类数据不会被保存:静态变量(static)、瞬态变量(transient)。例如密码字段、时间点等随时改变、有安全隐患的数据不应该被序列化保存。在进行序列化的时候,只是将堆内存中的数据保存起来,所以加了static关键字的静态属性不会被序列化。而加了transient关键字的(如为Student类的age加上瞬态属性public transient int age;
)也不会被序列化。
看上去说了一大堆,其实操作起来非常简单,只需为待序列化的对象实现Serializable接口并声明serialVersionUID就可以了。
以下是ObjectInputStream和ObjectOutputStream序列化、反序列化多个对象的示例。序列化的时候使用了集合的方式,将多个Student对象存储到集合中,然后遍历集合来序列化各个Student对象。反序列化的时候,由于ObjectInputStream的readObject()一次读取一个对象示例的数据,且没有提供合适的判断流结尾的返回值,只是在读取到结尾时会抛出EOFException异常。因此此处采用while无限循环的方式,并通过抛出的EOFException异常来结束循环。
import java.io.*;
import java.util.*;
public class ObjectStream {
public static void main(String[] args) {
//将各学生对象存放到集合中
List<Student> list = new ArrayList<Student>();
list.add(new Student("Malongshuai",22));
list.add(new Student("Gaoxiaofang",22));
//序列化
//writeObj(list,"d:/temp/a.object");
//反序列化
readObj("d:/temp/a.object");
}
//序列化
public static void writeObj(List list,String filename) {
//遍历集合中的对象并将它们序列化
ObjectOutputStream oos = null;
try {
oos = new ObjectOutputStream(new FileOutputStream(filename));
for(Iterator it = list.iterator();it.hasNext();) {
oos.writeObject(it.next());
}
} catch (FileNotFoundException f) {
f.printStackTrace();
} catch (IOException i) {
i.printStackTrace();
} finally {
if(oos!=null) {
try {
oos.close();
} catch(IOException i){
i.printStackTrace();
}
}
}
}
//反序列化:读取序列化数据
public static void readObj(String filename) {
ObjectInputStream ois = null;
try {
ois = new ObjectInputStream(new FileInputStream(filename));
while(true) {
Student student = (Student)ois.readObject();
System.out.println(student.toString());
}
} catch (EOFException e){
} catch (FileNotFoundException f) {
f.printStackTrace();
} catch (IOException i) {
i.printStackTrace();
} catch (ClassNotFoundException c){
c.printStackTrace();
} finally {
if(ois!=null) {
try {
ois.close();
} catch (IOException i) {
i.printStackTrace();
}
}
}
}
}
class Student implements Serializable {
static final long serialVersionUID = 123456l;
String name = "hello";
public int age;
Student(String name,int age){
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public int getAge() {
return age;
}
public String toString(){
return "{name="+name+",age="+age+"}";
}
}
3.PrintStream/PrintWriter
首先看一个容易出现疑惑的现象。
import java.io.*;
public class DataStream {
public static void main(String[] args) throws IOException {
FileOutputStream fos = new FileOutputStream("d:/temp/b.txt");
fos.write(97);
fos.write(353);
fos.close();
FileInputStream fis = new FileInputStream("d:/temp/b.txt");
byte[] buf = new byte[10];
int len = 0;
while((len=fis.read(buf))!=-1) {
String str = new String(buf);
System.out.println(str);
}
fis.close();
}
}
上面的代码中,向文件中写入的是数值97和353,但无论是用记事本解析还是从这里读取到的结果都是"aa"共两个字节的字母。为什么会如此?
在write(Int i)方法写入数据时,它会将最低位字节写入,而忽略前三个字节。例如97的二进制码为"00000000 00000000 00000000 01100001",忽略前三个字节,写入到文件中的二进制数据就只剩下"01100001",而这被读取或被解析时正好解析为字母a。同理353,它的二进制数据为"00000000 00000000 00000001 01100001",虽然第三个字节最后一位为1,但它还是被忽略,导致写入到文件中的二进制数据仍然为"01100001",解析后就是字母a。
要避免这种问题,可以使用字节打印流PrintStream或字符打印流PrintWriter,它会将数据按照字面展现形式输出。例如下面的例子中,将会向文件中分别写入"a97"。
FileOutputStream fos = new FileOutputStream("d:/temp/a.txt");
PrintStream ps = new PrintStream(fos);
//PrintStream ps = new PrintStream("d:/temp/a.txt")
ps.write(97); //它调用的其实还是fos.write(),所以仍然存储字母a
ps.print(97); //存储字面符号97
ps.close();
使用println()可以换行,使用printf()可以以C语言的打印格式输出。
另外,在PrintWriter中(不包括PrintStream),有一个自动更新autoFlush的概念,它表示每输出一次换行符就自动flush一次。但注意,PrintWriter的自动刷新只对println()和printf()方法有效,对print()无效。之所以不包括PrintStream,是因为PrintWriter因为字符集处理的原因在输出的时候涉及了一个额外的缓冲区,自动刷新就是将此缓冲区的数据flush,而PrintStream则没有这个额外的缓冲区,因此它是实时输出的。
4.DataInputStream/DataOutputStream
还是前面的问题,如何将97作为int数据类型保存到文件中(即将4个字节的97存到文件中)。也就是保证数据的数据类型不变。使用DataInputStream/DataOutputStream即可。
DataOutputStream dos = new DataOutputStream(new FileOutputStream("d:/temp/a.txt"));
dos.writeInt(97);
DataInputStream dis = new DataInputStream(new FileInputStream("d:/temp/a.txt"));
System.out.println(dis.readInt());
这样将会把97的4个字节存储到文件中,如果使用记事本去解析,得到的结果会是" a",共4个字节,虽然得到的结果是a,但这只是用记事本解析的而已。使用上面的readInt()读取的结果则是正确的。
注:若您觉得这篇文章还不错请点击右下角推荐,您的支持能激发作者更大的写作热情,非常感谢!