参考链接: Java ObjectInputStream类
什么是IO流? byte序列的读写,Java中的IO流是实现输入/输出的基础.
Java将数据从源(文件、内存、键盘、网络)读入到内存 中,形成了流,然后将这些流还可以写到另外的目的地(文件、内存、控制台、网络),之所以称为流,是因为这个数据序列在不同时刻所操作的是源的不同部分。按照不同的分类标准,IO流分为不同类型。主要有以下几种方式:按照数据流方向、数据处理的单位和功能。
不管流的分类是多么的丰富和复杂,其根源来自于四个基本的类。这个四个类的关系如下:
字节流 字符流 输入流 InputStream Reader 输出流 OutputStream Writer
输入流与输出流:
数据从内存到硬盘,通常认为是输出流,即写操作;相反,从硬盘到内存,通常认为是输入流,即读操作;这里的输入、输出是从内存的角度划分的。
字节(byte 1字节)流和字符(char 2字节)流:
字节流和字符流区别非常简单,它们的用法几乎一样。区别在于字节流和字符流所处理的最小数据单元不同。
文件中以字节的形式存储的。
处理最小数据单元 基类 字节流 byte 8 In/OutStream 字符流 char 16 Reader/writer
节点流和处理流:
节点流是可以从或向一个特定的地方(节点)读写数据,也叫 低级流。如FileReader。
处理流是在对节点流封装的基础上的一种流,通过封装后来实现数据的读写功能,也叫高级流。
节点流 未经封装,low level stream 处理流 封装过, high level stream
常用节点流
父 类 InputStream OutputStream Reader Writer 文 件 FileInputStream FileOutputStrean FileReader FileWriter 文件进行处理的节点流 数 组 ByteArrayInputStream ByteArrayOutputStream CharArrayReader CharArrayWriter 对数组进行处理的节点流(对应的不再是文件,而是内存中的一个数组) 字符串 StringReader StringWriter 对字符串进行处理的节点流 管 道 PipedInputStream PipedOutputStream PipedReader PipedWriter 对管道进行处理的节点流
常用处理流(关闭处理流使用关闭里面的节点流)
父 类 InputStream OutputStream Reader Writer
缓冲流 BufferedImputStrean BufferedOutputStream BufferedReader BufferedWriter
----需要父类作为参数构造,增加缓冲功能,避免频繁读写硬盘,可以初始化缓冲数据的大小,由于带了缓冲功能,所以就写数据的时候需要使用flush 方法咯
转换流 *InputStreamReader OutputStreamWriter- 要inputStream 或OutputStream 作为
参数,实现从字节流到字符流的转换数据流 *DataInputStream DataOutputStream -提供将基础数据类型写入到文件中,或者
读取出来,为什么要有这个流呢?看这样的分析,如果没有这种流的话,有一个long,本身只占8 个字节,如果我要写入到文件,需要转成字符串,然后在转成字符数组,那空间会占用很多,但是有了这种流之后就很方便了,直接将这8 个字节写到文件就完了。。是不是既节约了内存空间有让程序写起来更加方便简单了呐。写倒是很简单,但是读取的时候就注意了,根据读取的数据类型,指针会往下移,所以你写的顺序必须要和读的顺序一致才能完成正确的需求.
Java输入输出流总结:
分类 字节输入流 字节输出流 字符输入流 字符输出流 抽象基类 InputStream OutputStream Reader Writer 访问文件 FileInputStream FileOutputStream FileReader FileWriter 访问数组 ByteArrayInputStream ByteArrayOutputStream CharArrayReader CharArrayWriter 访问管道 PipedInputStream PipedOutputStream PipedReader PipedWriter 访问字符串 StringReader StringWriter 缓冲流 BufferedInputStream BufferedOutputStream BufferedReader BufferedWriter 转换流 InputStreamReader OutputStreamWriter 对象流 ObjectInputStream ObjectOutputStream 抽象基类 FilterInputStream FilterOutputStream FilterReader FilterWriter 打印流 PrintStream PrintWriter 推回输入流 PushbackInputStream PushbackReader 特殊流 DataInputStream DataOutputStream
Java是一种完全面向对象的高级语言,所以在编写程序的时候数据大都存放在对象当中。我们有时会需要将内存中的整个对象都写入到文件中去,然后在适当的时候再从文件中将对象还原至内存。我们可以使用java.io.ObjectInputStream和java.io.ObjectOutputStream类来完成这个任务。
1、什么是对象的序列化(Serialize)?为什么要实现对象的序列化?
序列化是指将对象的状态信息转换为可以存储或传输的形式(2进制数据)的过程。在序列化期间,对象将其当前状态写入到临时或持久性存储区。以后,可以通过从存储区中读取或反序列化对象的状态,重新创建该对象。
序列化的目的:
1)永久的保存对象,保存对象的字节序列到本地文件中;
2)通过序列化对象在网络中传递对象;
3)通过序列化对象在进程间传递对象。
2、ObjectInputStream类 和ObjectOutputStream类
如果我们想要序列化一个对象,如我们自定义的User类的对象,那么这个对象必须实现Serializable接口。Serializable接口没有任何的抽象方法,实现这个接口仅仅是为了通知编译器已这个对象将要被序列化,所以此接口仅仅是一个表示接口。类似的用法还有Cloneable接口,实现这个接口也只是起到通知编译器的作用。
3.对象的序列化和反序列化
想要完成对象的输入输出,还必须依靠ObjectInputStream和ObjectOutputStream;
使用对象输出流输出序列化对象的步骤,也称为序列化,而使用对象输入流读入对象的过程,也称为反序列化。
为了演示如何进行对象的序列化,我们先设计一个User类:
<span style="color:#333333;">package cls;
import java.io.*;
public class User implements Serializable // 实现Serializable接口,</span><span style="color:#ff0000;">仅仅起到标识这个类可被序列化的作用</span><span style="color:#333333;">
{
// 可序列化对象的版本
private static final long serialVersionUID = 1L;
private String name;
private int num;
public User(String name,int num)
{
this.name = name;
this.num = num;
}
public String getName()
{
return name;
}
public int getNum()
{
return num;
}
} </span>
4.serialVersionUID
在对象进行序列化或者反序列化操作的时候,要考虑JDK版本的问题,如果序列化的JDK版本和反序列化的JDK版本不统一则就可能造成异常,所以在序列化操作中引入了一个serialVersionUID的常量,可以通过此常量来验证版本的一致性,在进行反序列化时,JVM会将传过来的字节流中的serialVersionUID与本地相应实体的serialVersionUID进行比较,如果相同就认为是一致的,可以进行反序列化,否则就抛出不一致的异常。
5.到底序列化了哪些东西呢?
所有的对象拥有各自的属性值,但是所有的方法都是公共的,所以序列化对象的时候实际上序列化的就是属性。
下面我们使用ObjectInputStream类 和ObjectOutputStream类 向文件中写入3个User对象,追加1个User对象,最后再从文件中读回对象。
package cls;
import java.io.*;
import java.util.*;
import cls.User;
public class ObjectStreamDemo
{
public static void main(String[] args)
{
User[] user = new User[]{new User("dogg",1),new User("catt",2),new User("pigg",3)};
// 向文件中写入对象
try
{
ObjectStreamDemo.writeObj(user,args[0]);//args[0]传入文件名
}
catch(Exception e)
{
System.out.println(e.toString());
}
// 向文件中追加对象
try
{
// 要追加的对象
User[] u = new User[]{new User("append1",4),new User("append2",5)};
ObjectStreamDemo.appendObj(u,args[0]);
}
catch(Exception e)
{
System.out.println(e.toString());
}
// 读取对象
try
{
List<User> list = ObjectStreamDemo.readObj(args[0]);
// 输出对象信息
Iterator<User> it = list.iterator();
while(it.hasNext())
{
User temp = it.next();
System.out.println(temp.getName() "," temp.getNum());
}
}
catch(Exception e)
{
System.out.println(e.toString());
}
}
static private void appendObj(Object[] objs,String fileName) throws Exception
{
File file = new File(fileName);
// 以追加模式创建文件流对象
FileOutputStream fis = new FileOutputStream(file,true);
ObjectOutputStream oos = new ObjectOutputStream(fis)
{
// 重写 writeStreamHeader()方法,空实现
protected void writeStreamHeader(){};
};
// 写入数据
for(Object o : objs)
{
oos.writeObject(o);
}
// 关闭流
oos.close();
}
static private List<User> readObj(String fileName) throws Exception
{
File file = new File(fileName);
// 使用List保存读取出来的对象
ArrayList<User> list = new ArrayList<User>();
// 创建流对象
FileInputStream fis = new FileInputStream(file);
ObjectInputStream ois = new ObjectInputStream(fis);
// 读取对象并放入List容器中
while(fis.available() > 0)
{
list.add((User)ois.readObject());
}
ois.close();
return list; // 返回List
}
static private void writeObj(Object[] objs,String fileName) throws Exception
{
// 使用命令行参数中指定的文件名
File file = new File(fileName);
// 创建流对象
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(file));
// 写入对象
for(Object o : objs)
{
oos.writeObject(o);
}
// 关闭流
oos.close();
}
}
注意,
当我们想要向一个已经存在的文件中追加对象时,应该重写ObjectOutputStream的writeStreamHeader()方法,并空实现。
因为,ObjectOutputStream在写入数据的时候会加上一个特别的
流头(Stream Header)
,在读取数据的时候会先检查这个流头。所以我们在向文件中追加对象的时候ObjectOutputStream就会再次向文件中写入流头,这样在读取对象的时候会发生StreamCorrupedException异常。
6.Externalzable接口
如果一个类实现了Serializable 接口,则肯定此类可以被序列化下来,那么也就意味着此类多了一项功能,可以被序列化,那么让所有的 类都实现此接口是不是更好啊?
因为JDK是会不断升级的,现在Serializable 接口中没有任何定义,那么以后呢?
使用Serilizable 接口可以方便的序列化一个对象,但是在序列化操作中也提供了另外一种序列化机制——Externalizable 接口。
被Serialization接口声明的类其对象可以被序列化,如果现在用户希望可以自己制定序列化的内容,则可以让一个类实现Externalizable接口,此接口定义如下:
public interface Externalization extends Serializable{
public void writeExternal(ObjectOutput out) throws IOException;
public void writeExternal(ObjectIntput in) throws IOException,ClassNotFoundException;
}
方法:
写入:void writeExternal(ObjectOutput out) throws IOException
读取:void readExternal(ObjectInput in)throws IOException,ClassNotFoundException
程序:
[java]
view plain
copy
import java.io.Externalizable ; public class Person implements Externalizable{ private static final long serialVersionUID = 1l; private String name ; // 声明name属性 private int age ; // 声明age属性 public Person(String name,int age){ // 通过构造设置内容 this.name = name ; this.age = age ; } public String toString(){ // 覆写toString()方法 return "姓名:" this.name ";年龄:" this.age ; } public void writeExternal(ObjectOutput out) throws IOException{ out.writeObject(this.name); //保存姓名属性 out.writeInt(this.age); //保age属性 } public void readExternal(ObjectInput in) throws IOException,ClassNotFoundException{ this.name = in.readObject(); //读取姓名 this.age = in.readInt(); //读取年龄 } };
为了方便测试,现在将,现在序列化及反序列化操作形成方法调用的形式。
[java]
view plain
copy
import java.io.File ; import java.io.IOException ; import java.io.FileOutputStream ; import java.io.OutputStream ; import java.io.ObjectOutputStream ; import java.io.FileInputStream ; import java.io.InputStream ; import java.io.ObjectInputStream ; public class SerDemo03{ public static void main(String args[]) throws Exception{ //ser() ; dser() ; } public static void ser() throws Exception { File f = new File("D:" File.separator "test.txt") ; // 定义保存路径,参考附录1。 ObjectOutputStream oos = null ; // 1.声明对象输出流 OutputStream out = new FileOutputStream(f) ; // 2.文件输出流 oos = new ObjectOutputStream(out) ; //3.用文件输出流实例化对象输出流 oos.writeObject(new Person("张三",30)) ; //4.保存对象 oos.close() ; //5. 关闭 } public static void dser() throws Exception { File f = new File("D:" File.separator "test.txt") ; // 定义保存路径 ObjectInputStream ois = null ; // 声明对象输入流 InputStream input = new FileInputStream(f) ; // 文件输入流 ois = new ObjectInputStream(input) ; // 实例化对象输入流 Object obj = ois.readObject() ; // 读取对象 ois.close() ; // 关闭 System.out.println(obj) ; } };
以上程序执行的时候出现了一个错误:
在使用Externalizable接口的时候需要在被序列化的类中定义一个无参构造,因为此接口在进行反序列化的时候,会先使用
类中的无参构造方法为其进行实例化,之后再将内容分别设置到属性之中,因此,修改Person类:
[java]
view plain
copy
import java.io.Externalizable; import java.io.*; public class Person implements Externalizable{ private static final long serialVersionUID = 1L; private String name; //声明name属性 private int age; //声明age属性 public Person(){} //无参构造 public Person(String name, int age){ this.name = name; this.age = age; } public String toString(){ //覆写toString()方法 return "姓名:" this.name ":年龄:" this.age; } public void writeExternal(ObjectOutput out) throws IOException{ out.writeObject(this.name); //保存姓名属性 out.writeInt(this.age); //保存age属性 } public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException{ this.name = (String)in.readObject(); //读取姓名 this.age = in.readInt(); //读取年龄 } }
可以看出,序列化的一般步骤可以分为5步:
1.声明对象输出流
2.声明文件输出流,并实例化
3.用文件输出流对象实例化对象输出流
4.调用对象输出流的writeObject函数保存对象
5.关闭对象输出流
反序列化步骤:
1.声明对象输入流
2.声明文件输入流
3.用文件输入流对象实例化对象输入流
4.调用对象输入流的readObject函数读取对象,打印读取到的对象内容
5.关闭对象输入流
Externalizable和Serializable接口实现序列化的区别:
transient关键字:
在序列化操作的时候,如果某个属性不希望被序列化下来,则可以直接使用transient 关键字声明。
[java]
view plain
copy
import java.io.Serializable ; public class Person implements Serializable{ private transient String name ; // 声明name属性,但是此属性不被序列化 private int age ; // 声明age属性 public Person(String name,int age){ // 通过构造设置内容 this.name = name ; this.age = age ; } public String toString(){ // 覆写toString()方法 return "姓名:" this.name ";年龄:" this.age ; } };
操作代码:
[java]
view plain
copy
import java.io.File ; import java.io.IOException ; import java.io.FileOutputStream ; import java.io.OutputStream ; import java.io.ObjectOutputStream ; import java.io.FileInputStream ; import java.io.InputStream ; import java.io.ObjectInputStream ; public class SerDemo04{ public static void main(String args[]) throws Exception{ ser() ; dser() ; } public static void ser() throws Exception { File f = new File("D:" File.separator "test.txt") ; // 定义保存路径 ObjectOutputStream oos = null ; // 声明对象输出流 OutputStream out = new FileOutputStream(f) ; // 文件输出流 oos = new ObjectOutputStream(out) ; oos.writeObject(new Person("张三",30)) ; // 保存对象 oos.close() ; // 关闭 } public static void dser() throws Exception { File f = new File("D:" File.separator "test.txt") ; // 定义保存路径 ObjectInputStream ois = null ; // 声明对象输入流 InputStream input = new FileInputStream(f) ; // 文件输入流 ois = new ObjectInputStream(input) ; // 实例化对象输入流 Object obj = ois.readObject() ; // 读取对象 ois.close() ; // 关闭 System.out.println(obj) ; } };
transient Serilizable 接口完全可以取代Externalizable 接口的功能。
序列化一组对象:
如果要保存多个对象,则最好使用对象数组的形式完成。
[java]
view plain
copy
import java.io.File ; import java.io.IOException ; import java.io.FileOutputStream ; import java.io.OutputStream ; import java.io.ObjectOutputStream ; import java.io.FileInputStream ; import java.io.InputStream ; import java.io.ObjectInputStream ; public class SerDemo05{ public static void main(String args[]) throws Exception{ Person per[] = {new Person("张三",30),new Person("李四",31), new Person("王五",32)} ; ser(per) ; //Object obj[]可以接收对象数组 Object o[] = (Object[])dser() ; for(int i=0;i<o.length;i ){ Person p = (Person)o[i] ; System.out.println(p) ; } } public static void ser(Object obj[]) throws Exception { File f = new File("D:" File.separator "test.txt") ; // 定义保存路径 ObjectOutputStream oos = null ; // 声明对象输出流 OutputStream out = new FileOutputStream(f) ; // 文件输出流 oos = new ObjectOutputStream(out) ; oos.writeObject(obj) ; // 保存对象 oos.close() ; // 关闭 } public static Object[] dser() throws Exception { File f = new File("D:" File.separator "test.txt") ; // 定义保存路径 ObjectInputStream ois = null ; // 声明对象输入流 InputStream input = new FileInputStream(f) ; // 文件输入流 ois = new ObjectInputStream(input) ; // 实例化对象输入流 Object obj[] = (Object[])ois.readObject() ; // 读取对象 ois.close() ; // 关闭 return obj ; } };
总结:
1、对象序列化的作用,对象序列化并不一定都向文件中保存,也有可能面向于其他的输入或输出
2、被序列化的对象的类必须实现Serializable 接口,如果某个属性不希望被保存下来,则可以使用transient 关键字声明。
3、ObjectOutputStream 序列化对象,ObjectInputStream 反序列化对象
4、Externalizable 接口作用: 开发人员手式实现序列化的操作
5、使用序列化保存一组对象的时候要使用对象数组的形式操作
拓展:
保存的数据有限,所以为了解决这样的问题,Java 中引入了类集 框架解决数组的存储限制问题。不过已经超出本文的讨论范围,以后有机会将会为大家总结。
附录1:
在Windows下的路径分隔符和Linux下的路径分隔符是不一样的,当直接使用绝对路径时,跨平台会暴出“No such file or diretory”的异常。
比如说要在temp目录下建立一个test.txt文件,在Windows下应该这么写: File file1 = new File ("C:tmptest.txt"); 在Linux下则是这样的: File file2 = new File ("/tmp/test.txt");
如果要考虑跨平台,则最好是这么写: File myFile = new File("C:" File.separator "tmp" File.separator, "test.txt");
File类有几个类似separator的静态字段,都是与系统相关的,在编程时应尽量使用。
separatorChar
public static final char separatorChar
与系统有关的默认名称分隔符。此字段被初始化为包含系统属性 file.separator 值的第一个字符。在 UNIX 系统上,此字段的值为 '/';在 Microsoft Windows 系统上,它为 ''。
separator
public static final String separator
与系统有关的默认名称分隔符,为了方便,它被表示为一个字符串。此字符串只包含一个字符,即 separatorChar。
pathSeparatorChar
public static final char pathSeparatorChar
与系统有关的路径分隔符。此字段被初始为包含系统属性 path.separator 值的第一个字符。此字符用于分隔以路径列表 形式给定的文件序列中的文件名。在 UNIX 系统上,此字段为 ':';在 Microsoft Windows 系统上,它为 ';'。
pathSeparator
public static final String pathSeparator
与系统有关的路径分隔符,为了方便,它被表示为一个字符串。此字符串只包含一个字符,即 pathSeparatorChar。
附录2:
Android中实现序列化有两种选择:一是实现Serializable接口,二是实现Parcelable接口(android特有的功能,效率比实现Serializable接口高效,可用于Intent数据传递,也可以用于IPC)。
前面介绍了Serializable接口,实现Serializable接口来实现对象的序列化很简单,但是性能没有Parcelable接口高。所以建议使用Parcelable 。
1.什么是Parcelable接口:
Parcelable接口定义了将数据写入Parcel和从Parcel读出的接口。一个对象(实例),如果需要封装到消息中去,就必须实现这一接口,实现了这一接口,该实体就称为可打包的了。
2.应用场景:
需要在多个部件(Activity或Service)之间通过Intent传递一些数据时,简单类型的可以直接放入Intent,复杂类型的必须实现Parcelable接口。
3.Parcelable接口的定义:
public interface Parcelable
{
//内容描述接口,基本不用管
public int describeContents();
//写入接口函数,打包
public void writeToParcel(Parcel dest, int flags);
//读取接口,目的是要从Parcel中构造一个实现了Parcelable的类的实例处理。因为实现类在这里还是不可知的,所以需要用到模板的方式,继承类名通过模板参数传入
//为了能够实现模板参数的传入,这里定义Creator嵌入接口,内含两个接口函数分别返回单个和多个继承类实例
public interface Creator<T>
{
public T createFromParcel(Parcel source);
public T[] newArray(int size);
}
}
通过实现Parcelable接口序列化对象的步骤:
1、实现Parcelable接口。
2、并且实现Parcelable接口的public void writeToParcel(Parcel dest, int flags)方法 。将你的对象序列化为一个Parcel对象,即:将类的数据写入外部提供的Parcel中,打包需要传递的数据到Parcel容器保存,以便从 Parcel容器获取数据
3、重写describeContents方法,内容接口描述,默认返回0就可以
4、自定义类型中必须含有一个名称为CREATOR的静态成员,该成员对象要求实现Parcelable.Creator接口及其方法。
注:其中public static final一个都不能少,内部对象CREATOR的名称也不能改变,必须全部大写。需重写本接口中的两个方法:createFromParcel(Parcel in) 实现从Parcel容器中读取传递数据值,封装成Parcelable对象返回逻辑层,newArray(int size) 创建一个类型为T,长度为size的数组,仅一句话即可(return new T[size]),供外部类反序列化本类数组使用。
简而言之:通过writeToParcel将你的对象映射成Parcel对象,再通过createFromParcel将Parcel对象映射成你的对象。也可以将Parcel看成是一个流,通过writeToParcel把对象写到流里面,在通过createFromParcel从流里读取对象,只不过这个过程需要你来实现,因此写的顺序和读的顺序必须一致。
4、Serializable实现与Parcelabel实现的区别:
1)Serializable的实现,只需要implements Serializable 即可。这只是给对象打了一个标记,系统会自动将其序列化。
2)Parcelabel的实现,不仅需要implements Parcelabel,还需要在类中添加一个静态成员变量CREATOR,这个变量需要实现 Parcelable.Creator 接口。
5、两者代码比较:
1)创建Person类,实现Serializable
public class Person implements Serializable
{
private static final long serialVersionUID = -7060210544600464481L;
private String name;
private int age;
public String getName()
{
return name;
}
public void setName(String name)
{
this.name = name;
}
public int getAge()
{
return age;
}
public void setAge(int age)
{
this.age = age;
}
}
2)创建Book类,实现Parcelable
public class Book implements Parcelable
{
private String bookName;
private String author;
private int publishDate;
public Book()
{
}
public String getBookName()
{
return bookName;
}
public void setBookName(String bookName)
{
this.bookName = bookName;
}
public String getAuthor()
{
return author;
}
public void setAuthor(String author)
{
this.author = author;
}
public int getPublishDate()
{
return publishDate;
}
public void setPublishDate(int publishDate)
{
this.publishDate = publishDate;
}
@Override
public int describeContents()
{
return 0;
}
@Override
public void writeToParcel(Parcel out, int flags)
{
//必须按成员变量声明的顺序封装数据,不然会出现获取数据出错 out.writeString(bookName);
out.writeString(author);
out.writeInt(publishDate);
}
//必须实现Parcelable.Creator接口,否则会在获取Book数据的时候报错。
public static final Parcelable.Creator<Book> CREATOR = new Creator<Book>() //实现Parcelable.Creator接口对象名必须为CREATOR,不然同样会报错上面所提到的错; //这个接口实现了从Percel容器读取Book数据,并返回Book对象给逻辑层使用 {
@Override
public Book[] newArray(int size)
{
return new Book[size];
}
@Override
public Book createFromParcel(Parcel in)//从Percel容器读取Book数据,并返回Book对象给逻辑层使用
{
return new Book(in);
}
};
//这是一个Book的构造函数,通过Parcel对象构造Book对象。
public Book(Parcel in) {
//在读取Parcel容器里的数据时,必须按成员变量声明的顺序读取数据,不然会出现获取数据出错 bookName = in.readString();
author = in.readString();
publishDate = in.readInt(); }
}