java序列化示例与分析

java序列化示例与分析

  • 一、概念
    • 定义
  • 二、实现
    • 1.java中的序列化
      • 1)示例
      • 2)原理
        • 1.为什么实现了Serializable就可以网络传输?
        • 2.WriteObject /ReadObject 原理
      • 3)总结

一、概念

java平台允许我们在内存中创建可复用的内存对象,这些对象在JVM运行时才存在,JVM停止时就消失。现实应用中可能需要将这些内存对象持久化,以便后续JVM启动后再次读取。

定义

序列化:将内存对象转换成另外一种可存储(持久化)或传输(网络通讯)的形式(json、xml)的过程。

反序列化:序列化的逆向,将存储后的文件或网络过来的字节数组转化成对象的过程。

二、实现

1.java中的序列化

1)示例

简单通过socket写一个网络通讯的例子,如下:

网络传输对象User

@Getter
@ToString
public class User implements Serializable {
    private static final long serialVersionUID = 1607879477192902264L;

    private String name;
    private int age;
    private String sex;

    public User setName(String name) {
        this.name = name;
        return this;
    }
    public User setAge(int age) {
        this.age = age;
        return this;
    }
    
    public User setSex(String sex) {
        this.sex = sex;
        return this;
    }
}

socket服务端

public class ServerSocketDemo {
    public static void main(String[] args) {
        ServerSocket serverSocket = null;
        Socket socket = null;
        ObjectInputStream oi = null;
        try {
            serverSocket = new ServerSocket(8080);
            socket = serverSocket.accept();
            oi = new ObjectInputStream(socket.getInputStream());
            User user = (User)oi.readObject();
            System.out.println(user);
        } catch (IOException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } finally{
            if (oi!=null){
                try {
                    oi.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }

            try {
                serverSocket.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

socket客户端

public class SocketClientDemo {
    public static void main(String[] args) {
        Socket socket = null;
        ObjectOutputStream os = null;
        try {
            socket = new Socket("127.0.0.1",8080);
            os = new ObjectOutputStream(socket.getOutputStream());
            User user = new User().setName("zhangsan").setAge(18).setSex("man");
            os.writeObject(user);
            os.flush();
        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            if (os!=null){
                try {
                    os.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            try {
                socket.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

分别启动ServerSocketDemo与ServerClientDemo的main方法,执行结果如下:

java.io.NotSerializableException: com.learn.serialize.User
	at java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1184)
	at java.io.ObjectOutputStream.writeObject(ObjectOutputStream.java:348)
	at com.learn.serialize.SocketClientDemo.main(SocketClientDemo.java:20)

这说明User对象未实现序列化,不能在网络中传输。

大家都知道要实现网络传输,必须要实现Serializable接口,更改代码,让User implements Serializable ,看到如下结果确实是完成了网络传输,socket服务端输出了网络传输的对象反序列化后的结果。
java序列化示例与分析_第1张图片

那么究竟是如何实现序列化和反序列化的?为什么进行网络传输的对象实现了Serializable接口就可以进行网络传输的呢?接下来我们分析一下原理。

2)原理

1.为什么实现了Serializable就可以网络传输?

相信大家看到过很多实现了Serializable类会定义一个serialVersionUID变量(private static final long serialVersionUID = 1607879477192902264L;),serialVersionUID字面意义是序列化版本号,而变量值是ide根据类名、接口名、成员方法及属性等来生成一个 64 位的哈希字段,也就意味着如果序列化的类的未做任何改变,这个变量值不会变化,但如果序列化后将这个类内容改变,则序列化前后两对象的版本号不一致,之前序列化好的内容就不能成功的反序列化。

是否是这个serialVersionUID变量决定了序列化呢?那我们通过下面例子,将对象序列化后存储在文件中,然后更改类内容,查看之前序列化的内容是否可以成功反序列化。如果可以,说明serialVersionUID与序列化无关;但如果不能成功序列化,说明就是serialVersionUID与序列化有关。示例如下:

User.java

@Getter
@ToString
public class User implements Serializable {
    private static final long serialVersionUID = 1607879477192902264L;

    private String name;
    private int age;
    private String sex;

    public User setName(String name) {
        this.name = name;
        return this;
    }
    public User setAge(int age) {
        this.age = age;
        return this;
    }
    
    public User setSex(String sex) {
        this.sex = sex;
        return this;
    }
}

JavaSerialize.java

public class JavaSerialize {

    public  byte[] serializeToFile(T obj) {
        ObjectOutputStream oos = null;
        try {
            oos = new ObjectOutputStream(new FileOutputStream("user"));;
            oos.writeObject(obj);
            oos.flush();
        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            if (oos!=null){
                try {
                    oos.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        return new byte[0];
    }

    public  T deserializeFromFile() {
        ObjectInputStream ois = null;
        try {
            ois = new ObjectInputStream(new FileInputStream(new File("user")));
            return (T)ois.readObject();
        } catch (IOException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }finally {
            if (ois!=null){
                try {
                    ois.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        return null;
    }
}

测试类

public class Demo {
    public static void main(String[] args) {
        JavaSerialize javaSerialize = new JavaSerialize();
        User user = new User().setName("zhangsan").setAge(18).setSex("man");
        javaSerialize.serializeToFile(user);
        System.out.println((User)javaSerialize.deserializeFromFile());
    }
}

结果
java序列化示例与分析_第2张图片

此时修改User.java类,类内容发生变化,UID需要重新生成。

@Getter
@ToString
public class User implements Serializable {
    private static final long serialVersionUID = 8695436283467511144L;

    private String name;
    private int age;
    private String sex;
    private Integer id;

    public void setId(Integer id) {
        this.id = id;
    }

    public User setName(String name) {
        this.name = name;
        return this;
    }
    public User setAge(int age) {
        this.age = age;
        return this;
    }

    public User setSex(String sex) {
        this.sex = sex;
        return this;
    }
}

将之前序列化到文件中user对象读到内存中:

public class Demo {
    public static void main(String[] args) {
        JavaSerialize javaSerialize = new JavaSerialize();
//        User user = new User().setName("zhangsan").setAge(18).setSex("man");
//        javaSerialize.serializeToFile(user);
        System.out.println((User)javaSerialize.deserializeFromFile());

    }
}

结果如下

null
java.io.InvalidClassException: com.learn.serialize.User; local class incompatible: stream classdesc serialVersionUID = 1607879477192902264, local class serialVersionUID = 8695436283467511144
	at java.io.ObjectStreamClass.initNonProxy(ObjectStreamClass.java:687)
	at java.io.ObjectInputStream.readNonProxyDesc(ObjectInputStream.java:1883)
	at java.io.ObjectInputStream.readClassDesc(ObjectInputStream.java:1749)
	at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:2040)
	at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1571)
	at java.io.ObjectInputStream.readObject(ObjectInputStream.java:431)
	at com.learn.serialize.JavaSerialize.deserializeFromFile(JavaSerialize.java:45)
	at com.learn.serialize.Demo.main(Demo.java:12)
Disconnected from the target VM, address: '127.0.0.1:6748', transport: 'socket'

明显可以看出因为serialVersionUID不一致,导致无法正确反序列化。正如上面所猜想的serialVersionUID变量与着序列化有关。

serialVersionUID两种生成方式:

显示指定:

​ 1)指定为1L,类内容发生变化不会报错,只是部分反序列化的值缺失。(不建议使用)

​ 2)ide自动生成,idea里面需要勾选Settings->Editior->Inspections->搜索UID->Serializable class withOut ‘serialVersionUID’。当实现了序列化的接口未定义serialVersionUID时,idea会提醒,在提醒的地方自动导入即可。

隐式指定:

​ 如果没有为指定的class设置serialVersionUID,那么java编译器会自动给这个class进行一个摘要算法,当这个文件有任何改动,UID就会改变。

transient关键字修饰序列化类中的变量时,可以在序列化时,阻止该值序列化到文件中。

2.WriteObject /ReadObject 原理

User.java类给age加上transient关键字修饰,将User对象(name=zhangsan, age=18, sex=man)序列化持久化到文件中,然后在反序列化得到的结果如下:

Connected to the target VM, address: '127.0.0.1:9919', transport: 'socket'
Disconnected from the target VM, address: '127.0.0.1:9919', transport: 'socket'
User(name=zhangsan, age=0, sex=man)

Process finished with exit code 0

但User.java重写了writeObject方法、readObject方法,被transient修饰的字段经反序列化后得到的结果age仍然为18,结果如下所示:

User.java

@Getter
@ToString
public class User implements Serializable {
    private static final long serialVersionUID = 1607879477192902264L;

    private String name;
    private transient int age;
    private String sex;

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

    private void readObject(ObjectInputStream oi) throws IOException, ClassNotFoundException {
        oi.defaultReadObject();
        age=oi.readInt();
    }
    public User setName(String name) {
        this.name = name;
        return this;
    }
    public User setAge(int age) {
        this.age = age;
        return this;
    }

    public User setSex(String sex) {
        this.sex = sex;
        return this;
    }
}

测试类

public class Demo {
    public static void main(String[] args) {
        JavaSerialize javaSerialize = new JavaSerialize();
        User user = new User().setName("zhangsan").setAge(18).setSex("man");
        javaSerialize.serializeToFile(user);
        System.out.println((User)javaSerialize.deserializeFromFile());
    }
}

结果

Disconnected from the target VM, address: '127.0.0.1:10034', transport: 'socket'
User(name=zhangsan, age=18, sex=man)

Process finished with exit code 0

为什么加上writeObject方法和readObject方法后就能改变transient的作用呢?跟踪源码发现序列化调用的是ObjectOutputStream的writeObject方法(将内存中的对象转换成另外一种格式存储或传输),反序列化调用的是ObjectInputStream的readObject方法(从持久化或网络上获取数据,反序列化成对象)。

以序列化为例,跟踪ObjectOutputStream的writeObject方法,发现底层有一层判断,当原序列化类没有重写writeObject方法时,调用defaultWriteFields(obj, slotDesc);重写了 writeObject方法时,会调用slotDesc.invokeWriteObject(obj, this)方法;

private void writeSerialData(Object obj, ObjectStreamClass desc)
    throws IOException
{
    ObjectStreamClass.ClassDataSlot[] slots = desc.getClassDataLayout();
    for (int i = 0; i < slots.length; i++) {
        ObjectStreamClass slotDesc = slots[i].desc;
        if (slotDesc.hasWriteObjectMethod()) {
          ...
                slotDesc.invokeWriteObject(obj, this);
          ...
        } else {
            defaultWriteFields(obj, slotDesc);
        }
    }
}

继续跟踪invokeWriteObject代码发现最终是利用反射调用的原对象中的writeObjectMethod定义的方法,writeObjectMethod变量定义的方法为下面第二张图所示私有的writeObject方法,且参数类型为ObjectOutPutStream,返回值类型为void的方法(所以在重写的时候要严格按照这里的定义重写writeObject方法,否则就表示未重写writeObject方法,这里就调用不到了)。因此可以利用重写该方法,实现序列化特殊处理。
java序列化示例与分析_第3张图片
java序列化示例与分析_第4张图片
ArrayList中也重写了这个方法,如下所示:
java序列化示例与分析_第5张图片

3)总结

  • java序列化只针对对象的状态,不关注对象的方法

  • 当父类实现了序列化,子类自动实现序列化

  • 当字段被transient修饰时,序列化会自动的忽略这个字段

  • 重写writeObject和readObject方法可以实现序列化特殊处理

你可能感兴趣的:(java序列化,java)