IO streams, object serialization

Object Serialization

 Converting serialization mechanism allows to achieve serialized Java object into a byte sequence, the byte sequence can be stored on disk, or for network transmission, reconverted to the original object after ready. The serialization mechanism may be disengaged so that the object program is run independent existence.

 Serialization (the Serialize) object refers to a Java object written IO stream corresponding thereto is deserialized (the Deserialize) object refers to the recovery from the IO stream Java object. In order for an object to support serialization mechanism, you have to make it a class is serializable (Serializable). In order for a class is serialized, the class needs to inherit one of the following two interfaces:

  1. Serializable Interface

  2. Externliazble Interface

 

We start with the Serializable interface to begin with. When a class inherits the interface, the class is Serializable serializable, that is created out of objects that can be written to disk or a network transmission (basically classes for network transmission is serialized, otherwise the program will be exception. if contact with the students to develop Java web, HttpSession or ServletContext is serializable web application classes).

How do we operation input / output serializable objects, the answer is to use ObjectInputStream ObjectOutputStream byte stream operation, not only the two streams byte stream or process stream, the stream is required as a basis for an arbitrary node.

Below achieve serializable objects are written to the file, and the object is read from the file, and prints the private attribute values ​​of the object

// serializable class 

class the Person the implements the Serializable { 
    String name; 
    int Age; 
    
    public the Person (String name, int Age) {
         the this .name = name;
         the this .age = Age; 
        System.out.println ( "construction method: I called "+ name +", I am "+ age +" years old "! ); 
    } 

    public String getName () {
         return name; 
    } 

    public  void setName (String name) {
         the 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对象转换成字节序列,这些字节序列可以保存在磁盘上,或者用于网络传输,以备以后重新恢复成原来的对象。序列化机制使得对象可以脱离程序的运行而独立存在。

Guess you like

Origin www.cnblogs.com/hjlin/p/11432648.html