码迷,mamicode.com
首页 > 其他好文 > 详细

IO流,对象序列化

时间:2019-08-30 13:28:02      阅读:99      评论:0      收藏:0      [点我收藏+]

标签:stream   web应用   默认   存在   context   array   状态   获取对象   over   

对象序列化

 序列化机制允许将实现序列化的Java对象转换成字节序列,这些字节序列可以保存在磁盘上,或者用于网络传输,以备以后重新恢复成原来的对象。序列化机制使得对象可以脱离程序的运行而独立存在。

 对象的序列化(Serialize)指将一个Java对象写入IO流中,与此对应的是,对象的反序列化(Deserialize)则是指从IO流中恢复该Java对象。如要让某个对象支持序列化机制,则必须要让它的类是可序列化的(Serializable)。为了让某个类是可序列化的,该类需要继承如下两个接口之一:

  1. Serializable接口

  2. Externliazble接口

 

我们先从Serializable接口开始说起。当类继承了Serializable接口时,该类是可序列化的,即创建出来的对象,可以写入磁盘或者用于网络传输(基本上用于网络传输的类都是序列化的,否则程序会出现异常。若有接触过Java web开发的同学,在web应用中的HttpSession 或ServletContext就是可序列化的类)。

我们如何操作输入/输出可序列化的对象呢,答案是使用ObjectInputStream与ObjectOutputStream的字节流进行操作,这两个流不仅仅是字节流还是处理流,因此需要一个任意节点流作为基础。

下面实现将可序列化对象写入文件,并从该文件读取该对象,并打印该对象的私有属性值

//可序列化 类

class Person implements Serializable{
    String name;
    int age;
    
    public Person(String name,int age){
        this.name = name;
        this.age = age;
        System.out.println("构造方法:我叫"+name+",我今年"+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;
    }
}
//实现可序列化对象的传输
public
class IO { public void readObject(){ try{ ObjectInputStream ois = new ObjectInputStream(new FileInputStream("out.txt")); Person p = (Person)ois.readObject(); System.out.println("对象序列化,我叫"+p.getName()+",我今年"+p.getAge()+"岁了!"); ois.close(); }catch(Exception e){} } public void writeObject(){ try{ ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("out.txt")); Person p = new Person("HJL",23); oos.writeObject(p); oos.close(); }catch(Exception e){e.printStackTrace();} } public static void main(String[] args){ IO io = new IO(); io.writeObject(); io.readObject(); } }

上述代码中,我们先创建了p对象,并将这个p对象写入到当前程序目录下的out.txt文件里,在从该文件中取出对象,并打印该对象的属性值。运行效果如下:

构造方法:我叫HJL,我今年23岁了!
对象序列化,我叫HJL,我今年23岁了!

out.txt文件内容如下:

?? sr File.Person??,H:2{: I ageL namet Ljava/lang/String;xp   t HJL

这里要提醒一点,不是所有的输入流,对磁盘或者网络能写入对象,便可以实现对象的序列化,不如说通过重定向标准输入流,对System.out原先是输出到控制台(显示器)变成输出到指定文件,这样是可以将对象写入到文件中,不过文件中存储的只是当前程序使用该对象时的引用地址,当程序关闭时,该地址时不存在的,因此实现对象序列化的两个步骤,1时让类继承Serializable或者Extemalizable接口并创建对象,第二,通过ObjectInputStream或者ObjectOutputStream操作可序列化的对象。

 

对象引用的序列化:

 在之前的代码中,我们可以看到的继承Serializable接口的属性都是基本数据类型。若是该类中有引用数据类型时,为了保证该类的实例能正常的序列化与反序列化,该类的引用数据类型对应的类也需要继承Serializable或者Extemaliazble接口。

如下代码:

class Teacher implements Serializable{
    Person student;
    String name;
    
    public Teacher(String name,Person student){
        this.student = student;
        this.name = name;
    }

    public Person getStudent() {
        return student;
    }

    public void setStudent(Person student) {
        this.student = student;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}
public class IO {
    public void readObject(){
        try{
            ObjectInputStream ois = new ObjectInputStream(new FileInputStream("out.txt")); 
            Teacher t1 = (Teacher)ois.readObject();
            Teacher t2 = (Teacher)ois.readObject();
            Person p = (Person)ois.readObject();
            Teacher t3 = (Teacher)ois.readObject();
            System.out.println(t1);
            System.out.println(t2);
            System.out.println(t3);
            System.out.println(p);
            System.out.println(t1.getStudent() == p);
            System.out.println(t2.getStudent() == p);
            
            ois.close();
        }catch(Exception e){}
    }
    public void writeObject(){
        try{
            ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("out.txt"));
            Person p = new Person("HJL",23);
            Teacher t1 = new Teacher("PP",p);
            Teacher t2 = new Teacher("CC",p);
            oos.writeObject(t1);
            oos.writeObject(t2);
            oos.writeObject(p);
            oos.writeObject(t2);
            
            oos.close();
        }catch(Exception e){e.printStackTrace();}
    }
    
    public static void main(String[] args){
        IO io = new IO();
        io.writeObject();
        io.readObject();
    }
}

其运行结果为:

构造方法:我叫HJL,我今年23岁了!
File.Teacher@7ba4f24f
File.Teacher@3b9a45b3
File.Teacher@3b9a45b3
File.Person@7699a589
true
true

是所以会产生这样的运行结果,是因为在java序列机制中采用了一种特殊的序列化算法,其算法内容如下:

 1. 所有保存到磁盘中的对象都有一个序列化编号。

 2. 当程序试图序列化一个对象时,程序会先检查该对象是否已经被序列化过,只有该对象从未(本次虚拟机中)被序列化过,系统才会将该对象转换成字节序列并输出。

 3. 若该对象已经序列化过了,程序将知识直接输出一个序列化编号,而不是再次重新序列化该对象。

因此在上述代码中,当从文件中取出对象时,先是序列化了p对象,再是序列化t1对象,然后序列化t2对象时,发现p对象已经序列化过了,返回序p的列化编号,第三序列化p对象时,也发现被序列化过了,因此返回p的序列化编号,最后,序列化t2对象时,发现t2对象也被序列化过了,因此也返回t2的序列化编号。PS,写入/读取对象都是按顺序来的。

public class IO {
    public void readObject(){
        try{
            ObjectInputStream ois = new ObjectInputStream(new FileInputStream("out.txt"));            
            Teacher t1 = (Teacher)ois.readObject();
            Teacher t2 = (Teacher)ois.readObject();
            Person p = (Person)ois.readObject();
            Teacher t3 = (Teacher)ois.readObject();
            System.out.println(t1);
            System.out.println(t1.getStudent().getName());
            System.out.println(t2);
            System.out.println(t2.getStudent().getName());
            System.out.println(t3);
            System.out.println(p);
            System.out.println(t1.getStudent() == p);
            System.out.println(t2.getStudent() == p);
            
            ois.close();
        }catch(Exception e){}
    }
    public void writeObject(){
        try{
            ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("out.txt"));    
            Person p = new Person("HJL",23);
            Teacher t1 = new Teacher("PP",p);
            System.out.println("read: t1.student.name"+t1.getStudent().getName());
            oos.writeObject(t1);
            p.setName("HJJ");
            Teacher t2 = new Teacher("CC",p);
            System.out.println("read: t2.student.name"+t2.getStudent().getName());
            oos.writeObject(t2);
            oos.writeObject(p);
            oos.writeObject(t2);
            
            oos.close();
        }catch(Exception e){e.printStackTrace();}
    }
    
    public static void main(String[] args){
        IO io = new IO();
        io.writeObject();
        io.readObject();
    }
}

运行效果如下:

构造方法:我叫HJL,我今年23岁了!
read: t1.student.nameHJL
read: t2.student.nameHJJ
File.Teacher@7ba4f24f
HJL
File.Teacher@3b9a45b3
HJL
File.Teacher@3b9a45b3
File.Person@7699a589
true
true

程序中第一段粗体字代码先使用writeObject()方法写入了一个Person对象,接着程序改变了Person对象的name实例变量值,然后程序再次输出Person对象,但这次的输出已经不会将对象转换成字节序列并输出了,而是仅仅输出了一个序列化编号。

程序中两次调用readObject()方法读取了序列化文件中的Java对象,比较两次读取的Java对象将完全相同,程序输出第二次读取的Person对象的name实例变量的值依然是“HJL”,表明改变后的Person对象并没有被写入----这与Java序列化机制相符。

 

自定义序列化:

在一些特殊的场景下,如果一个类里包含的某些实例变量是敏感信息,例如银行账户信息等等,这时不希望系统将该实例变量值进行序列化:或者某个实例变量的类型时不可序列的,因此不希望对该实例变量进行递归序列化,以避免引发java.io.NotSetializableException异常。

当对某个对象进行序列化时,系统会自动把该对象的所有实例变量一次进行序列化,如果某个实例变量引用到另一个对象,则被引用的对象也会被序列化;如果被引用的对象的实例变量也引用了其他对象,则被引用的对象也会被序列化,这种情况被称为递归序列化。

通过在实例变量前面使用transient关键字修饰,可以指定Java序列化时无须理会该实例变量。如下Person类与前面的Person类几乎完全一样,只是它的age使用了transint关键子修饰。

修改的Person类:

class Person implements Serializable{
    String name;
    transient int age;
    
    public Person(String name,int age){
        this.name = name;
        this.age = age;
        System.out.println("构造方法:我叫"+name+",我今年"+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;
    }
}
public class IO {
    public void readObject(){
        try{
            ObjectInputStream ois = new ObjectInputStream(new FileInputStream("out.txt")); 
            Person p = (Person)ois.readObject();
            System.out.println(p.getAge());
            
            ois.close();
        }catch(Exception e){}
    }
    public void writeObject(){
        try{
            ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("out.txt"));            
            Person p = new Person("HJL",23);
            oos.writeObject(p);
            
            oos.close();
        }catch(Exception e){e.printStackTrace();}
    }
    
    public static void main(String[] args){
        IO io = new IO();
        io.writeObject();
        io.readObject();
    }
}

运行结果如下:

构造方法:我叫HJL,我今年23岁了!
0

由于我们使用了transient关键字修饰了类的成员变量,因此,该成员变量理论上是不被序列化以及反序列化,实际上是该变量以空值的形式进行序列化与反序列化,而int类型的空值是为0。因此上述代码中,从文件取出了对象,并在控制台打印对象的age变量,该变量的为0。

 

尽管transient关键字修饰实例变量虽然简单,方便,但被transient修饰的实例变量将被完全隔离在序列化机制之外,这样导致在反序列化恢复Java对象时,无法取得该实例变量值。Java还提供了一种自定义序列化机制,通过这种自定义序列化机制可以让程序控制如何序列化各实例变量,甚至完全不序列化某些实例变量(与使用transient关键字的效果相同),而这种自定义序列化机制,是在要实现序列化以及反序列化的类中,通过重写ObjectInputStream的readObject()方法与ObjectOutputStream的writeObject()方法。

一般在默认的情况下,writeObject()方法会调用out.defaultWriteObject来保存Java对象的各实例变量,从而可以实现序列化Java对象状态的目的,而readObject()方法会调用in.defalutWriteObject来反序列化该对象。

除这两种之外,还有另外一种方法,readObjectNoData(),当对方收到的java版本与我发送的java版本不一致的时候,或者因为传输过程中,该反序列化流变得不完整,系统会使用该方法,实现反序列化对象,不过该对象是初始化的状态。

下面我们通过修改上述的person类来实现:

class Person implements Serializable{
    String name;
    int age;
    
    public Person(String name,int age){
        this.name = name;
        this.age = age;
        System.out.println("构造方法:我叫"+name+",我今年"+age+"岁了!");
    }

    private void writeObject(java.io.ObjectOutputStream out) throws IOException{
        // TODO Auto-generated method stub
        //将name实例变量值反转后写入二进制流中
        out.writeObject(new StringBuffer(name).reverse());
//        out.writeInt(age);
    }
    
    private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException{
        // TODO Auto-generated method stub
        this.name = ((StringBuffer)in.readObject()).reverse().toString();
//        this.age = in.readInt();
    }
    
    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;
    }
}
public class IO {
    public void readObject(){
        try{
            ObjectInputStream ois = new ObjectInputStream(new FileInputStream("out.txt")); 
            Person t = (Person)ois.readObject();
            System.out.println(t.getAge());
            
            ois.close();
        }catch(Exception e){}
    }
    public void writeObject(){
        try{
            ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("out.txt"));
            Person p = new Person("HJL",23);
            oos.writeObject(p);
            
            oos.close();
        }catch(Exception e){e.printStackTrace();}
    }
    
    public static void main(String[] args){
     IO io = new IO(); io.writeObject(); io.readObject(); } }

运行结果如下:

构造方法:我叫HJL,我今年23岁了!
0

 

writeReplace()方法:这是自定义序列化的实现方法之一,比上述的writeObject更彻底,甚至可以在序列化对象时将该对象其换成其他对象。

同样是person类,但这是重写的是writeReplace();

class Person implements Serializable{
    String name;
//    transient int age;
    int age;
    
    public Person(String name,int age){
        this.name = name;
        this.age = age;
        System.out.println("构造方法:我叫"+name+",我今年"+age+"岁了!");
    }
    
    private Object writeReplace(){
        ArrayList<Object> list = new ArrayList<Object>();
        list.add(name);
        list.add(age);
        return list;
    }
    
    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;
    }
}

在上述代码中,我们重写了writeReplace()的方法,在方法中创建了一个list,对list添加数据以及返回list。然后我们继续对这个类的实例写入文件中,并读取文件中该实例数据,并打印该实例数据的类型:

public class IO {
    public void readObject(){
        try{
            ObjectInputStream ois = new ObjectInputStream(new FileInputStream("out.txt")); 
            //打印从文件获取对象的类型
            System.out.println(ois.readObject().getClass().getTypeName());
            
            ois.close();
        }catch(Exception e){}
    }
    public void writeObject(){
        try{
            ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("out.txt"));
            Person p = new Person("HJL",23);
            Teacher t = new Teacher("CC", p);
            oos.writeObject(p);
            
            oos.close();
        }catch(Exception e){e.printStackTrace();}
    }
    
    public static void main(String[] args){
        IO io = new IO();
        io.writeObject();
        io.readObject();
    }
}

运行效果如下:

构造方法:我叫HJL,我今年23岁了!
java.util.ArrayList

我们可以看到,虽然我们是写入了person类的p对象到out.txt文件中,但中out.txt文件中取出来的对象类型确实list数据类型的。这是因为在Person类中有writeReplace()方法,当要将该类的实例进行序列化前,先会调用writeReplace()方法,并取得该方法的返回值,将该方法的返回值进行序列化后通过对象流存入到了out.txt文件中,所以我们从out.txt文件取对象时,该对象是list类的实例而不是person类的实例。

 

与上述方法相对应的就是readResolve方法,该方法是作用是当类的实例进行反序列化后,会调用readResolve方法,并获取该方法的返回值,且该返回值会代替原来反序列化的对象:

class Person implements Serializable{
    String name;
//    transient int age;
    int age;
    
    public Person(String name,int age){
        this.name = name;
        this.age = age;
        System.out.println("构造方法:我叫"+name+",我今年"+age+"岁了!");
    }
private Object readResolve(){ Teacher t = new Teacher("CC",this); return t; } 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; } }

我们继续对person类进行修改,添加了readResolve()的方法。IO类保持不变,运行后效果如下:

构造方法:我叫HJL,我今年23岁了!
File.Teacher

 

值得注意的是,writeReplace()方法与readResolve()方法,是可以使用任意的访问控制符的,如果该类有父类,并且父类实现了writeReplace()方法或者readResolve()方法,并且子类没有重写该方法,将会使得子类序列化或者反序列化的时候执行父类的writeReplace()方法或者readResolve()方法,这明显是程序要的结果,而且开发人员也很难发现错误。但总是让子类重写writeReplace()方法或这readResolve方法无疑是一种负担,因此建议是用final来进行修饰,或者使用peivate进行修饰。

 

Externalizable接口

在介绍如何实现序列化时,提到了两种实现方法,其一是继承Serializable接口,其二是继承Externalizable接口。若类继承了Externalizable接口,该类要实现如下的两个方法:

  1. readExternal(ObjectInput in):

  2. writeExternal(ObjectOutput out):

上述两个方法与类实现自定义序列化时的readObject()与writeObject()方法很类似,都是用于实现自定义序列化,只是一个使用的是Serializable接口,一个是使用Externalizable接口。

下面我们修改person类,接口Externalizable接口,并重写上述两个方法实现,使用Externalizable接口实现自定义序列化:

class Person implements Externalizable{
    String name;
    int age;
    
    public Person(){}
    
    public Person(String name,int age){
        this.name = name;
        this.age = age;
        System.out.println("构造方法:我叫"+name+",我今年"+age+"岁了!");
    }
    
    @Override
    public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
        // TODO Auto-generated method stub
        this.name = ((StringBuffer)in.readObject()).reverse().toString();
//        this.age = in.readInt();
    }

    @Override
    public void writeExternal(ObjectOutput out) throws IOException {
        // TODO Auto-generated method stub
        out.writeObject(new StringBuffer(name).reverse());
//        out.writeInt(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;
    }
}

这里要注意的是,使用Externalizable接口实现对象序列化,但反序列化时,先是根据类的无参构造方法来创建实例,然后才执行readExternal()方法,因此实现Externalizable的序列化类必须提供public的无参构造方法

IO类如下:

public class IO {
    public void readObject(){
        try{
            ObjectInputStream ois = new ObjectInputStream(new FileInputStream("out.txt")); 
            Person p = (Person)ois.readObject();
            System.out.println("对象序列化,我叫"+p.getName()+",我今年"+p.getAge()+"岁了!");
            
            ois.close();
        }catch(Exception e){}
    }
    public void writeObject(){
        try{
            ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("out.txt"));
            
            Person p = new Person("HJL",23);
            Teacher t = new Teacher("CC", p);
            oos.writeObject(p);
            
            oos.close();
        }catch(Exception e){e.printStackTrace();}
    }
    
    public static void main(String[] args){
        IO io = new IO();
        io.writeObject();
        io.readObject();
    }
}

运行结果如下:

构造方法:我叫HJL,我今年23岁了!
对象序列化,我叫HJL,我今年0岁了!

由于writeExternal()方法只对name成员变量进行序列化以及readExternal()方法也只反序列化name成员变量,因此age成员变量从out.txt文件取出来后值为0。

 

关于对象序列化,还有以下几点需要注意的:

1.对象的类名,实例变量(包括基本类型,数组,对其他对象的引用)都会被序列化;方法,类变量(被static修饰的成员变量),transient实例的成员变量(也称瞬态实例变量)都不会被序列化

2. 保证序列化对象的实例变量类型也是可序列化的,否则需要使用transient关键字来修饰该实例

3.反序列化对象时必须有序列化对象的class文件

4.当通过文件,网络来读取序列化后的对象时,必须按实际写入的顺序读取,不可乱读。

 

版本问题:

我们开发java时,都会被开发的java类来制定版本,以便于我们管理。但反序列化Java对象时必须提供该对象的class文件,假如说,我使用person类创建实例,并写入了文件中,过了一会,我修改了person类的成员变量(可以是添加成员变量,可以是删除成员变量以及修改成员变量名),然后从该文件中取出了person类的对象。那么便存在着一个问题,就是从person类中取出来的对象是旧的person类,而现在的person.class文件是新得person类,旧的person类的成员变量与新的person类的成员变量不尽相同,引发了对person类两个不同版本的兼容性问题。

为了解决上述问题。Java序列机制中提供了private static final的serialVersionUID值,用于标识该Java类的序列化版本,若修改person类后,只要serialVersionUID的值与旧版本的值一致,序列机制也会把它们当成同一个序列化版本。

那么修改类的那么内容后,serialVersionUID无需修改值或者需要修改值呢?根据对象序列化主要注意的点中,对象序列化是对对象的类名,实例变量(static与transient修饰的成员变量不算)都会被序列化,因此若一个类修改了成员变量,添加了成员变量,删除了成员变量的情况,是需要进行修改serialVersionUID的值,除此之外是不用对serialVersionUID进行修改的。

 

总结:序列化机制允许将实现序列化的Java对象转换成字节序列,这些字节序列可以保存在磁盘上,或者用于网络传输,以备以后重新恢复成原来的对象。序列化机制使得对象可以脱离程序的运行而独立存在。

IO流,对象序列化

标签:stream   web应用   默认   存在   context   array   状态   获取对象   over   

原文地址:https://www.cnblogs.com/hjlin/p/11432648.html

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!