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

serialVersionUID的作用

时间:2019-09-12 18:16:25      阅读:83      评论:0      收藏:0      [点我收藏+]

标签:web   属性   tran   volatil   操作   tac   单例   resolve   java   

Serializable是一个所谓的标记接口,也就是说,实现这个接口是给这个类贴个标签,说它是Serializable的就可以了,具体实现是由JVM内部实现的,这个标签实际上是告诉JVM,你可以将我序列化。但这个标签不是随便贴的,如果你给一个类贴了这个标签,却在内部用到没贴这个标签的类,那运行时就可能有异常抛出。标记接口的用法现在一般被Annotation代替了,但Serializable是在Annotation还没出现前就存在了的。

---------------------

   客户端访问了某个能开启会话功能的资源, web服务器就会创建一个与该客户端对应的HttpSession对象,每个HttpSession对象都要站用一定的内存空间。如果在某一时间段内访问站点的用户很多,web服务器内存中就会积累大量的HttpSession对象,消耗大量的服务器内存,即使用户已经离开或者关闭了浏览器,web服务器仍要保留与之对应的HttpSession对象,在他们超时之前,一直占用web服务器内存资源。

 

        web服务器通常将那些暂时不活动但未超时的HttpSession对象转移到文件系统或数据库中保存,服务器要使用他们时再将他们从文件系统或数据库中装载入内存,这种技术称为Session的持久化。

 

         HttpSession对象保存到文件系统或数据库中,需要采用序列化的方式将HttpSession对象中的每个属性对象保存到文件系统或数据库中;将HttpSession对象从文件系统或数据库中装载如内存时,需要采用反序列化的方式,恢复HttpSession对象中的每个属性对象。所以存储在HttpSession对象中的每个属性对象必须实现Serializable接口。

 

serialVersionUID 的作用

serialVersionUID 用来表明类的不同版本间的兼容性

 

       Java的序列化机制是通过在运行时判断类的serialVersionUID来验证版本一致性的。在进行反序列化时,JVM会把传来的字节流中的serialVersionUID与本地相应实体(类)的serialVersionUID进行比较,如果相同就认为是一致的,可以进行反序列化,否则就会出现序列化版本不一致的异常。

 

      当实现java.io.Serializable接口的实体(类)没有显式地定义一个名为serialVersionUID,类型为long的变量时,Java序列化机制会根据编译的class自动生成一个serialVersionUID作序列化版本比较用,这种情况下,只有同一次编译生成的class才会生成相同的serialVersionUID

 

       如果我们不希望通过编译来强制划分软件版本,即实现序列化接口的实体能够兼容先前版本,未作更改的类,就需要显式地定义一个名为serialVersionUID,类型为long的变量,不修改这个变量值的序列化实体都可以相互进行串行化和反串行化。

 

      引起这个疑问,还是从Hibernate使用查询缓存说起;对象实例除了存在于内存,二级缓存还会将对象写进硬盘在需要的时候再读取出来使用,此时就必须提到一个概念:序列化。

 

      程序在运行时实例化出对象,这些对象存在于内存中,随着程序运行停止而消失,但如果我们想把某些对象(一般都是各不相同的属性)保存下来或者传输给其他进程,在程序终止运行后这些对象仍然存在,可以在程序再次运行时读取这些对象的信息,或者在其他程序中利用这些保存下来的对象信息恢复成实例对象。这种情况下就要用到对象的序列化和反序列化。

 

 其实很早就知道的,在Java中常见的几个类,如:Interger/String等,都实现了java.io.Serializable接口。这个序列化接口没有任何方法和域,仅用于标识序列化语意;实现 Serializable 接口的类是可序列化的,没有实现此接口的类将不能被序列化和反序列化。序列化类的所有子类本身都是可序列化的,不再需要显式实现 Serializable 接口。只有经过序列化,才能兼容对象在磁盘文本以及在网络中的传输,以及恢复对象的时候反序列化等操作。

 

问题一:为何要实现序列化?

答:序列化就是对实例对象的状态(State 对象属性而不包括对象方法)进行通用编码(如格式化的字节码)并保存,以保证对象的完整性和可传递性。

 

简而言之:序列化,就是为了在不同时间或不同平台的JVM之间共享实例对象

 

// 经常使用如下:

public static void main(String[] args) throws Exception {

    File file = new File("user.ser");

 

    ObjectOutputStream oout = new ObjectOutputStream(new FileOutputStream(file));

    User user = new User("zhang", 18, Gender.MALE);

    oout.writeObject(user);

    oout.close();

 

    ObjectInputStream oin = new ObjectInputStream(new FileInputStream(file));

    Object newUser = oin.readObject();

    oin.close();

    System.out.println(newUser);

}

  如没有 实现Serializable接口,在序列化时,使用ObjectOutputStreamwriteobject)方法将对象保存时将会出现异常。其实 java.io.Serializable 只是一个没有属性和方法的空接口,但是问题来了。。

 

问题二:为何一定要实现 Serializable 才能进行序列化呢?

使用 ObjectOutputStream 来持久化对象, 对于此处抛出的异常,查看该类中实现如下:

 

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、数组、EnumSerializable,就可以进行序列化,否则将抛出NotSerializableException 

 

最后提点注意:

1、在序列化对象时,不仅会序列化当前对象本身,还会对该对象引用的其它对象也进行序列化,如此引用传递序列化。如果一个对象包含的成员变量是容器类等并深层引用,那么序列化过程开销也较大。

 

2、当字段被声明为 transient 后,默认序列化机制就会忽略该字段。(还有方法就是自定义writeObject方法,见下代码示例)

 

3、在单例类中添加一个readResolve()方法(直接返回单例对象),以保证在序列化过程仍保持单例特性。

 

此外补充一下,

 在路径下jdk中还有另外一种形式的对象持久化,即:外部化(Externalization)。

 

public interface Externalizable extends java.io.Serializable {

  void writeExternal(ObjectOutput out) throws IOException;

  void readExternal(ObjectInput in) throws IOException, ClassNotFoundException;

}

  外部化和序列化是实现同一目标的两种不同方法。

 

  通过 Serializable 接口对对象序列化的支持是jdk内支持的 API ,但是java.io.Externalizable的所有实现者必须提供读入和写出的具体实现,怎么实现完全由你自定义。序列化(Serializable )会自动存储所有必要的信息(如属性以及属性类型等),用以反序列化成原来一样的实例,而外部化(Externalizable)则只保存被存储实例中你需要的信息。

 

示例代码如下:

 

public class User implements Externalizable {

    private String name;

    transient private Integer age;  // 屏蔽字段

    private Gender gender;

 

    public User() {

        System.out.println("none constructor");

    }

 

    public User(String name, Integer age, Gender gender) {

        System.out.println("arg constructor");

        this.name = name;

        this.age = age;

        this.gender = gender;

    }

 

    // 实现读写

    private void writeObject(ObjectOutputStream out) throws IOException {

        out.defaultWriteObject();

        out.writeInt(age);

        // 屏蔽gender

    }

    private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {

        in.defaultReadObject();

        age = in.readInt();

    }

 

    // 具体重写

    @Override

    public void writeExternal(ObjectOutput out) throws IOException {

        out.writeObject(name);

        out.writeInt(age);

        // 屏蔽gender

    }

    @Override

    public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {

        name = (String) in.readObject();

        age = in.readInt();

    }

}

 注意,用Externalizable进行序列化,当读取对象时,会调用被序列化类的无参构造器创建一个新的对象,然后再将被保存对象的字段的值分别填充到新对象中。实现Externalizable接口的类必须要提供一个无参的构造器,且访问权限为 public

---------------------

serialVersionUID适用于Java的序列化机制。简单来说,Java的序列化机制是通过判断类的serialVersionUID来验证版本一致性的。在进行反序列化时,JVM会把传来的字节流中的serialVersionUID与本地相应实体类的serialVersionUID进行比较,如果相同就认为是一致的,可以进行反序列化,否则就会出现序列化版本不一致的异常,即是InvalidCastException

具体的序列化过程是这样的:序列化操作的时候系统会把当前类的serialVersionUID写入到序列化文件中,当反序列化时系统会去检测文件中的serialVersionUID,判断它是否与当前类的serialVersionUID一致,如果一致就说明序列化类的版本与当前类版本是一样的,可以反序列化成功,否则失败。

serialVersionUID有两种显示的生成方式:        
一是默认的1L,比如:private static final long serialVersionUID = 1L;        
二是根据类名、接口名、成员方法及属性等来生成一个64位的哈希字段,比如:        
private static final  long   serialVersionUID = xxxxL;

当一个类实现了Serializable接口,如果没有显示的定义serialVersionUIDEclipse会提供相应的提醒。面对这种情况,我们只需要在Eclipse中点击类中warning图标一下,Eclipse就会      自动给定两种生成的方式。如果不想定义,在Eclipse的设置中也可以把它关掉的,设置如下:
Window ==> Preferences ==> Java ==> Compiler ==> Error/Warnings ==> Potential programming problems
Serializable class without serialVersionUIDwarning改成ignore即可。

当实现java.io.Serializable接口的类没有显式地定义一个serialVersionUID变量时候,Java序列化机制会根据编译的Class自动生成一个serialVersionUID作序列化版本比较用,这种情况下,如果Class文件(类名,方法明等)没有发生变化(增加空格,换行,增加注释等等),就算再编译多次,serialVersionUID也不会变化的。

如果我们不希望通过编译来强制划分软件版本,即实现序列化接口的实体能够兼容先前版本,就需要显式地定义一个名为serialVersionUID,类型为long的变量,不修改这个变量值的序列化实体都可以相互进行串行化和反串行化。

下面用代码说明一下serialVersionUID在应用中常见的几种情况。

1)序列化实体类

package com.sf.code.serial;

import java.io.Serializable;

public class Person implements Serializable {

    private static final long serialVersionUID = 123456789L;

    public int id;

    public String name;

 

    public Person(int id, String name) {

        this.id = id;

        this.name = name;

    }

 

    public String toString() {

        return "Person: " + id + " " + name;

    }

}

2)序列化功能:

 

package com.sf.code.serial;

import java.io.FileOutputStream;import java.io.IOException;import java.io.ObjectOutputStream;

public class SerialTest {

 

    public static void main(String[] args) throws IOException {

        Person person = new Person(1234, "wang");

        System.out.println("Person Serial" + person);

        FileOutputStream fos = new FileOutputStream("Person.txt");

        ObjectOutputStream oos = new ObjectOutputStream(fos);

        oos.writeObject(person);

        oos.flush();

        oos.close();

    }

}

3)反序列化功能:

package com.sf.code.serial;

import java.io.FileInputStream;import java.io.IOException;import java.io.ObjectInputStream;

public class DeserialTest {

    public static void main(String[] args) throws IOException, ClassNotFoundException {

        Person person;

 

        FileInputStream fis = new FileInputStream("Person.txt");

        ObjectInputStream ois = new ObjectInputStream(fis);

        person = (Person) ois.readObject();

        ois.close();

        System.out.println("Person Deserial" + person);

    }

 

}

情况一:假设Person类序列化之后,从A端传输到B端,然后在B端进行反序列化。在序列化Person和反序列化Person的时候,A端和B端都需要存在一个相同的类。如果两处的serialVersionUID不一致,会产生什么错误呢?
【答案】可以利用上面的代码做个试验来验证:
先执行测试类SerialTest,生成序列化文件,代表A端序列化后的文件,然后修改serialVersion值,再执行测试类DeserialTest,代表B端使用不同serialVersion的类去反序列化,结果报错:

Exception in thread "main" java.io.InvalidClassException: com.sf.code.serial.Person; local class incompatible: stream classdesc serialVersionUID = 1234567890, local class serialVersionUID = 123456789

    at java.io.ObjectStreamClass.initNonProxy(ObjectStreamClass.java:621)

    at java.io.ObjectInputStream.readNonProxyDesc(ObjectInputStream.java:1623)

    at java.io.ObjectInputStream.readClassDesc(ObjectInputStream.java:1518)

    at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:1774)

    at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1351)

    at java.io.ObjectInputStream.readObject(ObjectInputStream.java:371)

    at com.sf.code.serial.DeserialTest.main(DeserialTest.java:13)

 

情况二:假设两处serialVersionUID一致,如果A端增加一个字段,然后序列化,而B端不变,然后反序列化,会是什么情况呢?

 

package com.sf.code.serial;

import java.io.Serializable;

public class Person implements Serializable {

    private static final long serialVersionUID = 1234567890L;

    public int id;

    public String name;

    public int age;

 

    public Person(int id, String name) {

        this.id = id;

        this.name = name;

    }

    

    public Person(int id, String name, int age) {

        this.id = id;

        this.name = name;

        this.age = age;

    }

 

    public String toString() {

        return "Person: " + id

                + ",name:" + name

                + ",age:" + age;

    }

}

 

 

public class SerialTest {

 

    public static void main(String[] args) throws IOException {

        Person person = new Person(1234, "wang", 100);

        System.out.println("Person Serial" + person);

        FileOutputStream fos = new FileOutputStream("Person.txt");

        ObjectOutputStream oos = new ObjectOutputStream(fos);

        oos.writeObject(person);

        oos.flush();

        oos.close();

    }

}

Person DeserialPerson: 1234,name:wang

【答案】新增 public int age; 执行SerialTest,生成序列化文件,代表A端。删除 public int age,反序列化,代表B端,最后的结果为:执行序列化,反序列化正常,但是A端增加的字段丢失(B端忽略)

 

情况三:假设两处serialVersionUID一致,如果B端减少一个字段,A端不变,会是什么情况呢?

 

package com.sf.code.serial;

import java.io.Serializable;

public class Person implements Serializable {

    private static final long serialVersionUID = 1234567890L;

    public int id;

    //public String name;

    

    public int age;

 

    public Person(int id, String name) {

        this.id = id;

        //this.name = name;    }

 

    public String toString() {

        return "Person: " + id

                //+ ",name:" + name

                + ",age:" + age;

    }

}

 

Person DeserialPerson: 1234,age:0

【答案】序列化,反序列化正常,B端字段少于A端,A端多的字段值丢失(B端忽略)

 

情况四:假设两处serialVersionUID一致,如果B端增加一个字段,A端不变,会是什么情况呢?
验证过程如下:
先执行SerialTest,然后在实体类Person增加一个字段age,如下所示,再执行测试类DeserialTest.

 

package com.sf.code.serial;

import java.io.Serializable;

public class Person implements Serializable {

    private static final long serialVersionUID = 1234567890L;

    public int id;

    public String name;

    public int age;

 

    public Person(int id, String name) {

        this.id = id;

        this.name = name;

    }

    

    /*public Person(int id, String name, int age) {

        this.id = id;

        this.name = name;

        this.age = age;

    }*/

 

    public String toString() {

        return "Person: " + id

                + ",name:" + name

                + ",age:" + age;

    }

}

 

结果:Person DeserialPerson: 1234,name:wang,age:0

说明序列化,反序列化正常,B端新增加的int字段被赋予了默认值0


最后通过下面的图片,总结一下上面的几种情况。

 技术图片

静态变量序列化

情境:查看清单 2 的代码。

清单 2. 静态变量序列化问题代码

 public class Test implements Serializable {

 

private static final long serialVersionUID = 1L;

 

public static int staticVar = 5;

 

public static void main(String[] args) {

try {

//初始时staticVar为5

ObjectOutputStream out = new ObjectOutputStream(

new FileOutputStream("result.obj"));

out.writeObject(new Test());

out.close();

 

//序列化后修改为10

Test.staticVar = 10;

 

ObjectInputStream oin = new ObjectInputStream(new FileInputStream(

"result.obj"));

Test t = (Test) oin.readObject();

oin.close();

 

//再读取,通过t.staticVar打印新的值

System.out.println(t.staticVar);

 

} catch (FileNotFoundException e) {

e.printStackTrace();

} catch (IOException e) {

e.printStackTrace();

} catch (ClassNotFoundException e) {

e.printStackTrace();

}

}

}

清单 2 中的 main 方法,将对象序列化后,修改静态变量的数值,再将序列化对象读取出来,然后通过读取出来的对象获得静态变量的数值并打印出来。依照清单 2,这个 System.out.println(t.staticVar) 语句输出的是 10 还是 5 呢?

最后的输出是 10,对于无法理解的读者认为,打印的 staticVar 是从读取的对象里获得的,应该是保存时的状态才对。之所以打印 10 的原因在于序列化时,并不保存静态变量,这其实比较容易理解,序列化保存的是对象的状态,静态变量属于类的状态,因此 序列化并不保存静态变量。

 

 

父类的序列化与 Transient 关键字

情境:一个子类实现了 Serializable 接口,它的父类都没有实现 Serializable 接口,序列化该子类对象,然后反序列化后输出父类定义的某变量的数值,该变量数值与序列化时的数值不同。

解决:要想将父类对象也序列化,就需要让父类也实现Serializable 接口。如果父类不实现的话的,就 需要有默认的无参的构造函数。在父类没有实现 Serializable 接口时,虚拟机是不会序列化父对象的,而一个 Java 对象的构造必须先有父对象,才有子对象,反序列化也不例外。所以反序列化时,为了构造父对象,只能调用父类的无参构造函数作为默认的父对象。因此当我们取父对象的变量值时,它的值是调用父类无参构造函数后的值。如果你考虑到这种序列化的情况,在父类无参构造函数中对变量进行初始化,否则的话,父类变量值都是默认声明的值,如 int 型的默认是 0string 型的默认是 null

Transient 关键字的作用是控制变量的序列化,在变量声明前加上该关键字,可以阻止该变量被序列化到文件中,在被反序列化后,transient 变量的值被设为初始值,如 int 型的是 0,对象型的是 null

特性使用案例

我们熟悉使用 Transient 关键字可以使得字段不被序列化,那么还有别的方法吗?根据父类对象序列化的规则,我们可以将不需要被序列化的字段抽取出来放到父类中,子类实现 Serializable 接口,父类不实现,根据父类序列化规则,父类的字段数据将不被序列化,形成类图如图 2 所示。

2. 案例程序类图

 技术图片

上图中可以看出,attr1attr2attr3attr5 都不会被序列化,放在父类中的好处在于当有另外一个 Child 类时,attr1attr2attr3 依然不会被序列化,不用重复抒写 transient,代码简洁。

 

static final 修饰的serialVersionUID如何被写入到序列化文件中的,看下面的源码:

序列化写入时的ObjectStreamClass.java中,

void writeNonProxy(ObjectOutputStream out) throws IOException {

        out.writeUTF(name);

        out.writeLong(getSerialVersionUID());

 

        byte flags = 0;

        ...

 

    public long getSerialVersionUID() {

        // REMIND: synchronize instead of relying on volatile?

        if (suid == null) {

            suid = AccessController.doPrivileged(

                new PrivilegedAction<Long>() {

                    public Long run() {

                        return computeDefaultSUID(cl);

                    }

                }

            );

        }

        return suid.longValue();

}

 技术图片

技术图片

 

 

serialVersionUID的作用

标签:web   属性   tran   volatil   操作   tac   单例   resolve   java   

原文地址:https://www.cnblogs.com/yhxb/p/11513161.html

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