引言

在持久化数据对象的时候我们很少使用Java序列化,而是使用数据库等方式来实现。但是在我看来,Java 序列化是一个很重要的内容,序列化不仅可以保存对象到磁盘进行持久化,还可以通过网络传输。在平时的面试当中,序列化也是经常被谈及的一块内容。

谈到序列化时,大家可能知道将类实现Serializable接口就可以达到序列化的目的,但当看到关于序列化的面试题时我们却常常一脸懵逼。

1)可序列化接口和可外部接口的区别是什么?

2)序列化时,你希望某些成员不要序列化?该如何实现?

3)什么是 serialVersionUID ?如果不定义serialVersionUID,会发生什么?

是不是突然发现我们对这些问题其实都还存在很多疑惑?本文将总结一些Java序列化的常见问题,并且通过demo来进行测试和解答。

问题一:什么是 Java 序列化?

序列化是把对象改成可以存到磁盘或通过网络发送到其它运行中的 Java 虚拟机的二进制格式的过程,并可以通过反序列化恢复对象状态。Java 序列化API给开发人员提供了一个标准机制:通过实现 java.io.Serializable 或者 java.io.Externalizable 接口,ObjectInputStream 及ObjectOutputStream 处理对象序列化。实现java.io.Externalizable 接口的话,Java 程序员可自由选择基于类结构的标准序列化或是它们自定义的二进制格式,通常认为后者才是最佳实践,因为序列化的二进制文件格式成为类输出 API的一部分,可能破坏 Java 中私有和包可见的属性的封装。

序列化到底有什么用?

实现 java.io.Serializable。

定义用户类:

class User implements Serializable {
private String username;
private String passwd; public String getUsername() {
return username;
} public void setUsername(String username) {
this.username = username;
} public String getPasswd() {
return passwd;
} public void setPasswd(String passwd) {
this.passwd = passwd;
}
}

我们把对象序列化,通过ObjectOutputStream存储到txt文件中,再通过ObjectInputStream读取txt文件,反序列化成User对象。

public class TestSerialize {

    public static void main(String[] args) {

        User user = new User();
user.setUsername("hengheng");
user.setPasswd(""); System.out.println("read before Serializable: ");
System.out.println("username: " + user.getUsername());
System.err.println("password: " + user.getPasswd()); try {
ObjectOutputStream os = new ObjectOutputStream(
new FileOutputStream("/Users/admin/Desktop/test/user.txt"));
os.writeObject(user); // 将User对象写进文件
os.flush();
os.close();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
try {
ObjectInputStream is = new ObjectInputStream(new FileInputStream(
"/Users/admin/Desktop/test/user.txt"));
user = (User) is.readObject(); // 从流中读取User的数据
is.close(); System.out.println("\nread after Serializable: ");
System.out.println("username: " + user.getUsername());
System.err.println("password: " + user.getPasswd()); } catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}

运行结果如下:

序列化前数据:
username: hengheng
password: 序列化后数据:
username: hengheng
password:

到这里,我们大概知道了什么是序列化。

问题二:序列化时,你希望某些成员不要序列化,该如何实现?

答案:声明该成员为静态或瞬态,在 Java 序列化过程中则不会被序列化。

  • 静态变量:加static关键字。
  • 瞬态变量: 加transient关键字。

我们先尝试把变量声明为瞬态。

class User implements Serializable {
private String username;
private transient String passwd; public String getUsername() {
return username;
} public void setUsername(String username) {
this.username = username;
} public String getPasswd() {
return passwd;
} public void setPasswd(String passwd) {
this.passwd = passwd;
}

在密码字段前加上了transient关键字再运行。运行结果:

序列化前数据:
username: hengheng
password: 序列化后数据:
username: hengheng
password: null

通过运行结果发现密码没有被序列化,达到了我们的目的。

再尝试在用户名前加static关键字。

class User implements Serializable {
private static String username;
private transient String passwd; public String getUsername() {
return username;
} public void setUsername(String username) {
this.username = username;
} public String getPasswd() {
return passwd;
} public void setPasswd(String passwd) {
this.passwd = passwd;
}

运行结果:

序列化前数据:
username: hengheng
password: 序列化后数据:
username: hengheng
password: null

我们发现运行后的结果和预期的不一样,按理说username也应该变为null才对。是什么原因呢?

原因是:反序列化后类中static型变量username的值为当前JVM中对应的静态变量的值,而不是反序列化得出的。

我们来证明一下:

public class TestSerialize {

    public static void main(String[] args) {

        User user = new User();
user.setUsername("hengheng");
user.setPasswd(""); System.out.println("序列化前数据: ");
System.out.println("username: " + user.getUsername());
System.err.println("password: " + user.getPasswd()); try {
ObjectOutputStream os = new ObjectOutputStream(
new FileOutputStream("/Users/admin/Desktop/test/user.txt"));
os.writeObject(user); // 将User对象写进文件
os.flush();
os.close();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
User.username = "小明";
try {
ObjectInputStream is = new ObjectInputStream(new FileInputStream(
"/Users/admin/Desktop/test/user.txt"));
user = (User) is.readObject(); // 从流中读取User的数据
is.close(); System.out.println("\n序列化后数据: ");
System.out.println("username: " + user.getUsername());
System.err.println("password: " + user.getPasswd()); } catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
} class User implements Serializable {
public static String username;
private transient String passwd; public String getUsername() {
return username;
} public void setUsername(String username) {
this.username = username;
} public String getPasswd() {
return passwd;
} public void setPasswd(String passwd) {
this.passwd = passwd;
}
}

在反序列化前把静态变量username的值改为『小明』。

User.username = "小明";

再运行一次:

序列化前数据:
username: hengheng
password: 序列化后数据:
username: 小明
password: null

果然,这里的username是JVM中静态变量的值,并不是反序列化得到的值。

问题三:serialVersionUID有什么用?

我们经常会在类中自定义一个serialVersionUID:

private static final long serialVersionUID = 8294180014912103005L

这个serialVersionUID有什么用呢?如果不设置的话会有什么后果?

serialVersionUID 是一个 private static final long 型 ID,当它被印在对象上时,它通常是对象的哈希码。serialVersionUID可以自己定义,也可以自己去生成。

不指定 serialVersionUID的后果是:当你添加或修改类中的任何字段时,已序列化类将无法恢复,因为新类和旧序列化对象生成的 serialVersionUID 将有所不同。Java 序列化的过程是依赖于正确的序列化对象恢复状态的,并在序列化对象序列版本不匹配的情况下引发 java.io.InvalidClassException 无效类异常。

举个例子大家就明白了:

我们保持之前保存的序列化文件不变,然后修改User类。

class User implements Serializable {
public static String username;
private transient String passwd;
private String age; public String getUsername() {
return username;
} public void setUsername(String username) {
this.username = username;
} public String getPasswd() {
return passwd;
} public void setPasswd(String passwd) {
this.passwd = passwd;
} public String getAge() {
return age;
} public void setAge(String age) {
this.age = age;
}
}

加了一个属性age,然后单另写一个反序列化的方法:

public static void main(String[] args) {
try {
ObjectInputStream is = new ObjectInputStream(new FileInputStream(
"/Users/admin/Desktop/test/user.txt"));
User user = (User) is.readObject(); // 从流中读取User的数据
is.close(); System.out.println("\n修改User类之后的数据: ");
System.out.println("username: " + user.getUsername());
System.err.println("password: " + user.getPasswd()); } catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}

报错了,我们发现之前的User类生成的serialVersionUID和修改后的serialVersionUID不一样(因为是通过对象的哈希码生成的),导致了InvalidClassException异常。

自定义serialVersionUID:

class User implements Serializable {
private static final long serialVersionUID = 4348344328769804325L; public static String username;
private transient String passwd;
private String age; public String getUsername() {
return username;
} public void setUsername(String username) {
this.username = username;
} public String getPasswd() {
return passwd;
} public void setPasswd(String passwd) {
this.passwd = passwd;
} public String getAge() {
return age;
} public void setAge(String age) {
this.age = age;
}
}

再试一下:

序列化前数据:
username: hengheng
password: 序列化后数据:
username: 小明
password: null

运行结果无报错,所以一般都要自定义serialVersionUID。

问题四:是否可以自定义序列化过程?

答案当然是可以的。

之前我们介绍了序列化的第二种方式:

实现Externalizable接口,然后重写writeExternal() 和readExternal()方法,这样就可以自定义序列化。

比如我们尝试把变量设为瞬态。

public class ExternalizableTest implements Externalizable {

    private transient String content = "我是被transient修饰的变量哦";

    @Override
public void writeExternal(ObjectOutput out) throws IOException {
out.writeObject(content);
} @Override
public void readExternal(ObjectInput in) throws IOException,
ClassNotFoundException {
content = (String) in.readObject();
} public static void main(String[] args) throws Exception { ExternalizableTest et = new ExternalizableTest();
ObjectOutput out = new ObjectOutputStream(new FileOutputStream(
new File("test")));
out.writeObject(et); ObjectInput in = new ObjectInputStream(new FileInputStream(new File(
"test")));
et = (ExternalizableTest) in.readObject();
System.out.println(et.content); out.close();
in.close();
}
}

运行结果:

我是被transient修饰的变量哦

这里实现的是Externalizable接口,则没有任何东西可以自动序列化,需要在writeExternal方法中进行手工指定所要序列化的变量,这与是否被transient修饰无关。

通过上述介绍,是不是对Java序列化有了更多的了解?

作者:杨亨

来源:宜信技术学院

关于Java序列化的问题你真的会吗?的更多相关文章

  1. 【总结】你所不知道的Java序列化

    我们都知道,Java序列化可以让我们记录下运行时的对象状态(对象实例域的值),也就是我们经常说的对象持久化 .这个过程其实是非常复杂的,这里我们就好好理解一下Java的对象序列化. 1. 首先我们要搞 ...

  2. Java 序列化 序列化与单例模式 [ 转载 ]

    Java 序列化 序列化与单例模式 [ 转载 ] @author Hollis 本文将通过实例+阅读Java源码的方式介绍序列化是如何破坏单例模式的,以及如何避免序列化对单例的破坏. 单例模式,是设计 ...

  3. Java 序列化 transient关键字

    Java 序列化 transient关键字 @author 敏敏Alexia 转自:http://www.cnblogs.com/lanxuezaipiao/p/3369962.html 1. tra ...

  4. Java序列化和反序列化,你该知道得更多

    序列化 (Serialization)是将对象的状态信息转换为可以存储或传输的形式的过程.在序列化期间,对象将其当前状态写入到临时或持久性存储区.以后,可以通过从存储区中读取或反序列化对象的状态,重新 ...

  5. Java序列化系列教程(下)

    一引言 将 Java 对象序列化为二进制文件的 Java 序列化技术是 Java 系列技术中一个较为重要的技术点,在大部分情况下,开发人员只需要了解被序列化的类需要实现 Serializable 接口 ...

  6. 再来认识一下 Java 序列化

    前言 在面试中,Java 序列化被问到的几率还是挺高的.所以搜集了 Java 序列化常见的问题,由浅入深的帮助大家进一步学习和理解. 序列化基础知识 什么是序列化? Java 序列化是 JDK 1.1 ...

  7. Java 序列化与反序列化

    1.什么是序列化?为什么要序列化? Java 序列化就是指将对象转换为字节序列的过程,而反序列化则是只将字节序列转换成目标对象的过程. 我们都知道,在进行浏览器访问的时候,我们看到的文本.图片.音频. ...

  8. Java序列化与反序列化

    Java序列化与反序列化是什么?为什么需要序列化与反序列化?如何实现Java序列化与反序列化?本文围绕这些问题进行了探讨. 1.Java序列化与反序列化 Java序列化是指把Java对象转换为字节序列 ...

  9. java序列化

    什么是java序列化,如何实现java序列化? 我们有时候将一个java对象变成字节流的形式传出去或者从一个字节流中恢复成一个java对象,例如,要将java对象存储到硬盘或者传送给网络上的其他计算机 ...

随机推荐

  1. 分辨率与px的关系

    此篇文章的目的用于打印套打的位置计算,顺便科普下知识: 1寸=2.54厘米 14寸=355.6毫米 15.6寸=39.624厘米=396.24毫米: 21寸=533.4毫米 21.7寸=551.18毫 ...

  2. 吴裕雄--天生自然 HADOOP大数据分布式处理:使用WinSCP连接本机与虚拟机

  3. Android 7.0终极开发者预览版全攻略!

    近日,Google的工程部副总裁Dave Burke在官方博客上正式发布开发者预览版5,此预览版是android 7.0 “牛轧糖”正式发布前最后一个预览版,同时也是在性能.功能上等多方面的表现上最接 ...

  4. 3dmax2012卸载/安装失败/如何彻底卸载清除干净3dmax2012注册表和文件的方法

    3dmax2012提示安装未完成,某些产品无法安装该怎样解决呢?一些朋友在win7或者win10系统下安装3dmax2012失败提示3dmax2012安装未完成,某些产品无法安装,也有时候想重新安装3 ...

  5. iOS UITableView Tips(2)

    #TableView Tips(2) (本来想一章就结束TableView Tips,但是发现自己还是太天真了~too young,too simple) ##架构上的优化 在Tips(1)中指出了一 ...

  6. understanding android build layer · Dylan

    build / android 先看看Android官方的解释 Understand Build Layers The build hierarchy includes the abstraction ...

  7. java枚举与常量类的区别

    1)枚举型可以直接与数据库打交道,我通常使用varchar类型存储,对应的是枚举的常量名.(数据库中好像也有枚举类型,不过也没用过) 2) switch语句支持枚举型,当switch使用int.Str ...

  8. Python---8函数(函数的参数&递归函数)

    一.函数的参数 Python的函数定义非常简单,但灵活度却非常大.除了正常定义的必选参数外,还可以使用默认参数.可变参数和关键字参数,使得函数定义出来的接口,不但能处理复杂的参数,还可以简化调用者的代 ...

  9. JS数字千分

    JS数字千分: 1.例子:1000--->1,000 2.实现如下: salesToFormat: function (num) { var num = (num || 0).toString( ...

  10. 吴裕雄--天生自然python学习笔记:Beautiful Soup 4.2.0模块

    Beautiful Soup 是一个可以从HTML或XML文件中提取数据的Python库.它能够通过你喜欢的转换器实现惯用的文档导航,查找,修改文档的方式.Beautiful Soup会帮你节省数小时 ...