代码改变世界

对象序列化

2017-02-10 16:07  taixuyingcai  阅读(556)  评论(1编辑  收藏  举报

java中的序列化与反序列化

序列化是指将Java中的对象转化为二进制的字节数据,用于在网络上传输或持久化到磁盘上。

Serializable接口

java中的序列化需要实现Serializable接口,这个接口没有任何方法。只是作为一个标识。
如果不实现这个接口是不能进行序列化的。

下面看一个例子:
创建一个可以序列化的类Person

public class Person implements Serializable {
    private String name;
    private int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = 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;
    }
    @Override
    public String toString() {
        return name + "," + age;
    }
}

对Pseron实例进行序列化与反序列化:

public class IOTest {
    public static void main(String[] args) {
        String path = "D:\\iotest.txt";
        try {
            Person p = new Person("aaa", 12);
            writeObj(p, path);
            p = (Person)readObj(path);
            System.out.println(p);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    static void writeObj(Object obj, String path) throws Exception {
        FileOutputStream fo = new FileOutputStream(path);
        ObjectOutputStream oo = new ObjectOutputStream(fo);
        oo.writeObject(obj);
    }

    static Object readObj(String path) throws Exception {
        FileInputStream fi = new FileInputStream(path);
        ObjectInputStream oi = new ObjectInputStream(fi);
        return oi.readObject();
    }
}

通过代码我们看到,序列化与反序列化是通过ObjectOutputStream和ObjectInputStream的方法来实现的。

输出结果:
aaa,12

可以看到对象是被反序列化了。还有一点可以注意到,在反序列化的时候是没有调用
对象的构造方法的。
这里有个问题反序列化后的对象跟原来的对象是否是同一个对象呢? 答案是不是同一个对象
对过"=="比较就可以知道

在ObjectOutputStream类中有一个WriteObject0(Object obj, boolean unshared)方法
其中有段代码是这样的

private void writeObject0(Object obj, boolean unshared)
        throws IOException
{
.........
// remaining cases
            if (obj instanceof String) {
                writeString((String) obj, unshared);
            } else if (cl.isArray()) {
                writeArray(obj, desc, unshared);
            } else if (obj instanceof Enum) {
                writeEnum((Enum<?>) obj, desc, unshared);
            } else if (obj instanceof Serializable) {
                writeOrdinaryObject(obj, desc, unshared);
            } else {
                if (extendedDebugInfo) {
                    throw new NotSerializableException(
                        cl.getName() + "\n" + debugInfoStack.toString());
                } else {
                    throw new NotSerializableException(cl.getName());
                }
            }
.......
}

从这个方法中我们可以看到,序列化的对象必须是String,Array(数组),Enum,或Serializable的实例
这下明白了为什么要想序列化对象必须要实现Serializable接口了吧。

默认序列化

如果只是把对象实现了Serializable接口,其它什么都不做的话,这个对象在序列化的时候将按
默认方式进行序列化。
什么是默认方式呢?
默认方式就是不公会序列化对象本身,而且会把对象中引用的其它对象也序列化,引用对象
引用的对象也会序列化。。。。
所以。。。默认序列化有时会生成很多不必要的数据,而且开销会很大。


那么我们应该如何自己控制序列化过程呢?

1)static关键字
序列化的时候,是不会对static的变量进行序列化的。但是我们不能依靠这个来控制不
需要序列化的字段。
2)transient关键字
被transient关键字修饰的字段是不会被序列化的。在jdk的很多容器类的实现中就有很多
用transient关键字来修饰的。目的就是不想让这些字段序列化
3) writeObject(ObjectOutputStream oo) 和readObject(ObjectInputStream oi)方法
如果在对象中加入这两个方法,那么对象在序列与反序列的时候会通过调用这两个方法
进行的

 private void writeObject(ObjectOutputStream oos) throws IOException {
      //  oos.defaultWriteObject();
        oos.writeInt(age);
    }

    private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
      //  in.defaultReadObject();
        age = in.readInt();
    }

再运行main方法后输出结果:
Persion constructor
null,12

name是null,说明确实是通过这两个方法进行的序列与反序列。
这两个方法是怎么被调用的呢?
其实是通过反射来调用的,在ObjectOutputStream和ObjectInputStream进行对象序列
化的时候反射调用这两个方法的。

4)Externalizabe接口
Externalizable接口是继承自Serializable接口的
如果序列化对象实现这个接口的话要覆盖其中的两个方法
void writeExternal(ObjectOutput out) throws IOException;
void readExternal(ObjectInput in) throws IOException, ClassNotFoundException;
在这两个方法中进行序列化

Externalizabel接口把序列化的行为交给了程序员自己,这样可以更灵活的控制序列化过程。

但是有一点要注意,实现这个接口的对象序列化的时候会调用对象的无参构造方法,
所以,对象必须提供一个无参的构造方法,而且要是public的

单例对象的序列化

我们知道,单例对象在使用的时候要保证始终都是同一个对象。但是上面的例子我们
已经知道了,对象在被反序列化的时候返回的对象跟原来的对象并不是同一个对象。
那么怎么解决这个问题呢?

方法就是在对象中增加readResolve()方法,并且在这个方法中返回单例对象

public class Person implements Serializable {

    private static class InstanceHolder {
        private static final Person instatnce = new Person("John", 31, Gender.MALE);
    }

    public static Person getInstance() {
        return InstanceHolder.instatnce;
    }
    private String name = null;

    private Integer age = null;

    private Gender gender = null;

    private Person() {
        System.out.println("none-arg constructor");
    }

    private Person(String name, Integer age, Gender gender) {
        System.out.println("arg constructor");
        this.name = name;
        this.age = age;
        this.gender = gender;
    }

    protected Object readResolve() throws ObjectStreamException {
        return InstanceHolder.instatnce;
    }
    
}

其实无论是Serializabe接口还是Externalizabel接口,都会调用readResolve()方法
并且会用这个方法里返回的对象替换到反序列化的对象