transient关键字和serialVersionUID
此文章很大部分转载于Java的架构师技术栈微信公众号,博主均测试通过加上自己理解写出
最近阅读java集合的源码,发现transient关键字,就了解了一下他的用法,transient关键字一般在实现Serializable接口的类中出现.如下:
一、初识transient关键字
其实这个关键字的作用很好理解,一句话:将不需要序列化的属性前添加关键字transient,序列化对象的时候,这个属性就不会被序列化
下面使用代码去验证一下:User类实现序列化接口,在age属性前加上该关键字
public class User implements Serializable { private static final long serialVersionUID = 123456L; private transient int age; private String name; //getter和setter方法 //toString方法 }
然后我们在Test中去验证一下:
public static void main(String[] args) throws Exception, IOException { SerializeUser(); DeSerializeUser(); } //序列化 private static void SerializeUser() throws FileNotFoundException, IOException, ClassNotFoundException { User user = new User(); user.setName("Java的架构师技术栈"); user.setAge(24); ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("D://Test/template.txt")); oos.writeObject(user); oos.close(); System.out.println("添加transient关键字序列化age= "+user.getAge()); } //反序列化 private static void DeSerializeUser() throws IOException, ClassNotFoundException { File file = new File("D://Test/template.txt"); ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file)); User newUser = (User)ois.readObject(); System.out.println("添加了transient关键字反序列化:age= "+newUser); }
看一下结果:
从上面的这张图可以看出,age属性变为了0,说明被transient关键字修饰之后没有被序列化。
二、深入分析transient关键字
为了更加深入的去分析transient关键字,我们需要带着几个问题去解读:
(1)transient底层实现的原理是什么?
(2)被transient关键字修饰过得变量真的不能被序列化嘛?
(3)静态变量能被序列化吗?被transient关键字修饰之后呢?
带着这些问题一个一个来解决:
1、transient底层实现原理是什么?
java的serialization提供了一个非常棒的存储对象状态的机制,说白了serialization就是把对象的状态存储到硬盘上 去,等需要的时候就可以再把它读出来使用。有些时候像银行卡号这些字段是不希望在网络上传输的,transient的作用就是把这个字段的生命周期仅存于调用者的内存中而不会写到磁盘里持久化,意思是transient修饰的age字段,他的生命周期仅仅在内存中,不会被写到磁盘中。
2、被transient关键字修饰过得变量真的不能被序列化嘛?
想要解决这个问题,首先还要再重提一下对象的序列化方式:
Java序列化提供两种方式。
– 一种是实现Serializable接口
– 另一种是实现Exteranlizable接口。需要重写writeExternal和readExternal方法,它的效率比Serializable高一些,并且可以决定哪些属性需要序列化(即使是transient修饰的),但是对大量对象,或者重复对象,则效率低。
从上面的这两种序列化方式,我想你已经看到了,使用Exteranlizable接口实现序列化时,我们自己指定那些属性是需要序列化的,即使是transient修饰的。下面就验证一下
首先我们定义User1类:这个类是被Externalizable接口修饰的
public class User1 implements Externalizable{ private transient String name; //getter、setter、toString方法 @Override public void writeExternal(ObjectOutput out) throws IOException { out.writeObject(name); } @Override public void readExternal(ObjectInput in) throws IOException { name = (String) in.readObject(); } }
然后我们就可以测试了
public static void main(String[] args) throws Exception, IOException { SerializeUser(); DeSerializeUser(); } //序列化 private static void SerializeUser() throws FileNotFoundException, IOException { User1 user = new User1(); user.setName("Java的架构师技术栈"); ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("D://template")); oos.writeObject(user); oos.close(); System.out.println("使用Externalizable接口,"+ "添加了transient关键字序列化之前:"+user); } //反序列化 private static void DeSerializeUser() throws IOException, ClassNotFoundException { File file = new File("D://template"); ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file)); User1 newUser = (User1)ois.readObject(); System.out.println("使用Externalizable接口,"+ " 添加了transient关键字序列化之后:"+newUser); }
上面,代码分了两个方法,一个是序列化,一个是反序列化。里面的代码和一开始给出的差不多,只不过,User1里面少了age这个属性。
然后看一下结果:
结果验证了我们的猜想,也就是说,实现了Externalizable接口,哪一个属性被序列化使我们手动去指定的,即使是transient关键字修饰也不起作用。
3、静态变量能被序列化吗?没被transient关键字修饰之后呢?
这个我可以提前先告诉结果,静态变量是不会被序列化的,即使没有transient关键字修饰。下面去验证一下,然后再解释原因。
首先,在User类中对age属性添加transient关键字和static关键字修饰。
public class User2 implements Serializable { private static final long serialVersionUID = 12345L; private static transient int age; private String name; public static long getSerialVersionUID() { return serialVersionUID; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } public String getName() { return name; } public void setName(String name) { this.name = name; } @Override public String toString() { return "User{" + "age=" + age + ", name='" + name + '\'' + '}'; } }
然后,在Test类中去测试
public class Test2 { public static void main(String[] args) throws Exception, IOException { SerializeUser(); DeSerializeUser(); } //序列化 private static void SerializeUser() throws FileNotFoundException, IOException, ClassNotFoundException { User2 user = new User2(); user.setName("Java的架构师技术栈"); //序列化之前静态变量age年龄是24. user.setAge(24); //将数据24写入磁盘 ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("D://template")); oos.writeObject(user); oos.close(); //再读取,通过getAge()打印新的值 System.out.println("static、transient关键字修饰age之前:"+user.getAge()); //现在把年龄改成18,此时18只存在内存 user.setAge(18); } //反序列化 private static void DeSerializeUser() throws IOException, ClassNotFoundException { File file = new File("D://template"); ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file)); User2 newUser = (User2)ois.readObject(); System.out.println("使用Externalizable接口,"+ " 添加了transient关键字序列化之后:"+newUser); } }
最后,看看结果
结果已经很明显了。现在解释一下,为什么会是这样,其实在前面已经提到过了。因为静态变量在全局区,本来流里面就没有写入静态变量,我打印静态变量当然会去全局区查找,而我们的序列化是写到磁盘上的,所以JVM查找这个静态变量的值,是从全局区查找的,而不是磁盘上。user.setAge(18);年龄改成18之后,被写到了全局区,其实就是方法区,只不过被所有的线程共享的一块空间。因此可以总结一句话:
静态变量不管是不是transient关键字修饰,都不会被序列化
三、transient关键字总结
java 的transient关键字为我们提供了便利,你只需要实现Serilizable接口,将不需要序列化的属性前添加关键字transient,序列化对象的时候,这个属性就不会序列化到指定的目的地中。像银行卡、密码等等这些数据。这个需要根据业务情况了。
四.serialVersionUID
serialVersionUID适用于Java的序列化机制。简单来说,Java的序列化机制是通过判断类的serialVersionUID来验证版本一致性的。在进行反序列化时,JVM会把传来的字节流中的serialVersionUID与本地相应实体类的serialVersionUID进行比较,如果相同就认为是一致的,可以进行反序列化,否则就会出现序列化版本不一致的异常,即是InvalidCastException
具体的序列化过程是这样的:序列化操作的时候系统会把当前类的serialVersionUID写入到序列化文件中,当反序列化时系统会去检测文件中的serialVersionUID,判断它是否与当前类的serialVersionUID一致,如果一致就说明序列化类的版本与当前类版本是一样的,可以反序列化成功,否则失败。
下面我们测试一下 serialVersionUID = 123456L;
public class User implements Serializable { private static final long serialVersionUID = 123456L; private transient int age; private String name; public static long getSerialVersionUID() { return serialVersionUID; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } public String getName() { return name; } public void setName(String name) { this.name = name; } @Override public String toString() { return "User{" + "age=" + age + ", name='" + name + '\'' + '}'; } }
序列化到磁盘
public static void main(String[] args) throws Exception, IOException { SerializeUser(); //DeSerializeUser(); } //序列化 private static void SerializeUser() throws FileNotFoundException, IOException, ClassNotFoundException { User user = new User(); user.setName("Java的架构师技术栈"); user.setAge(24); ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("D://Test/template.txt")); oos.writeObject(user); oos.close(); System.out.println("添加transient关键字序列化age= "+user.getAge()); }
看下结果
可以看到,成功了,现在我们修改一下serialVersionUID = 12345L;
public class User implements Serializable { private static final long serialVersionUID = 12345L; private transient int age; private String name; public static long getSerialVersionUID() { return serialVersionUID; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } public String getName() { return name; } public void setName(String name) { this.name = name; } @Override public String toString() { return "User{" + "age=" + age + ", name='" + name + '\'' + '}'; } }
再反序列试一下
public static void main(String[] args) throws Exception, IOException { //SerializeUser(); DeSerializeUser(); } //序列化 private static void SerializeUser() throws FileNotFoundException, IOException, ClassNotFoundException { User user = new User(); user.setName("Java的架构师技术栈"); user.setAge(24); ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("D://Test/template.txt")); oos.writeObject(user); oos.close(); System.out.println("添加transient关键字序列化age= "+user.getAge()); } //反序列化 private static void DeSerializeUser() throws IOException, ClassNotFoundException { File file = new File("D://Test/template.txt"); ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file)); User newUser = (User)ois.readObject(); System.out.println("添加了transient关键字反序列化:age= "+newUser); }
运行,看结果
出现了序列化版本不一致的异常,即是InvalidCastException
当实现java.io.Serializable接口的类没有显式地定义一个serialVersionUID变量时候,Java序列化机制会根据编译的Class自动生成一个serialVersionUID作序列化版本比较用,
serialVersionUID注释掉,去除transient关键字
测试一下:
public class User implements Serializable { //private static final long serialVersionUID = 12345L; private int age; private String name; /*public static long getSerialVersionUID() { return serialVersionUID; }*/ public int getAge() { return age; } public void setAge(int age) { this.age = age; } public String getName() { return name; } public void setName(String name) { this.name = name; } @Override public String toString() { return "User{" + "age=" + age + ", name='" + name + '\'' + '}'; } }
测试代码如下:
public class Test { public static void main(String[] args) throws Exception, IOException { SerializeUser(); DeSerializeUser(); } //序列化 private static void SerializeUser() throws FileNotFoundException, IOException, ClassNotFoundException { User user = new User(); user.setName("Java的架构师技术栈"); user.setAge(24); ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("D://Test/template.txt")); oos.writeObject(user); oos.close(); System.out.println("添加transient关键字序列化age= "+user.getAge()); } //反序列化 private static void DeSerializeUser() throws IOException, ClassNotFoundException { File file = new File("D://Test/template.txt"); ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file)); User newUser = (User)ois.readObject(); System.out.println("添加了transient关键字反序列化:age= "+newUser); } }
运行,看一下结果
可以看到,序列化与反序列化都很正常
这种情况下,如果Class文件(类名,方法明等)没有发生变化(增加空格,换行,增加注释等等),就算再编译多次,serialVersionUID也不会变化的。
如果我们不希望通过编译来强制划分软件版本,即实现序列化接口的实体能够兼容先前版本,就需要显式地定义一个名为serialVersionUID,类型为long的变量,不修改这个变量值的序列化实体都可以相互进行串行化和反串行化。