Java Serializable 序列化和反序列化 (转载)
一:Serializable
1、持久化的简单介绍:
“持久化”意味着对象的“生存时间”并不取决于程序是否正在执行——它存在或“生存”于程序的每一次调用之间。通过序列化一个对象,将其写入磁盘,以后在程序再次调用时重新恢复那个对象,就能圆满实现一种“持久”效果。
2、什么情况下需要序列化
a)当你想把的内存中的对象状态保存到一个文件中或者数据库中时候;
b)当你想用套接字在网络上传送对象的时候;
c)当你想通过RMI传输对象的时候;
3、当对一个对象实现序列化时,究竟发生了什么?
在没有序列化前,每个保存在堆(Heap)中的对象都有相应的状态(state),即实例变量(instance ariable)比如:
Foo myFoo = new Foo(); myFoo .setWidth(37); myFoo.setHeight(70);
当通过下面的代码序列化之后,MyFoo对象中的width和Height实例变量的值(37,70)都被保存到foo.ser文件中,这样以后又可以把它 从文件中读出来,重新在堆中创建原来的对象。当然保存时候不仅仅是保存对象的实例变量的值,JVM还要保存一些小量信息,比如类的类型等以便恢复原来的对象。
FileOutputStream fs = new FileOutputStream("foo.ser"); ObjectOutputStream os = new ObjectOutputStream(fs); os.writeObject(myFoo); os.close(); ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream("foo.ser")); int width= (Integer) objectInputStream.readObject(); int height = (Integer) objectInputStream.readObject(); objectInputStream.close();
4、语言里增加了对象序列化的概念后,可提供对两种主要特性的支持:
- 远程方法调用(RMI)使本来存在于其他机器的对象可以表现出好象就在本地机器上的行为。将消息发给远程对象时,需要通过对象序列化来传输参数和返回值。
- 使用一个Java Bean 时,它的状态信息通常在设计期间配置好。程序启动以后,这种状态信息必须保存下来,以便程序启动以后恢复;具体工作由对象序列化完成。
5、Serializable的一些说明:
- 对象的序列化处理非常简单,只需对象实现了Serializable 接口即可(该接口仅是一个标记,没有方法)
- 序列化的对象包括基本数据类型,所有集合类以及其他许多东西,还有Class 对象
- 对象序列化不仅保存了对象的“全景图”,而且能追踪对象内包含的所有句柄并保存那些对象;接着又能对每个对象内包含的句柄进行追踪
- 使用transient关键字修饰的的变量,在序列化对象的过程中,该属性不会被序列化。
6、序列化和反序列化的步骤:
序列化:
-
- 首先要创建某些OutputStream对象:OutputStream outputStream = new FileOutputStream("output.txt")
- 将其封装到ObjectOutputStream对象内:ObjectOutputStream objectOutputStream = new ObjectOutputStream(outputStream);
- 此后只需调用writeObject()即可完成对象的序列化,并将其发送给OutputStream:objectOutputStream.writeObject(Object);
- 最后不要忘记关闭资源:objectOutputStream.close(), outputStream .close();
反序列化:
-
- 首先要创建某些OutputStream对象:InputStream inputStream= new FileInputStream("output.txt")
- 将其封装到ObjectInputStream对象内:ObjectInputStream objectInputStream= new ObjectInputStream(inputStream);
- 此后只需调用readObject()即可完成对象的反序列化:objectInputStream.readObject();
- 最后不要忘记关闭资源:objectInputStream.close(),inputStream.close();
7、serialVersionUID
实现了Serializable接口之后,Eclipse就会提示你增加一个 serialVersionUID,虽然不加的话上述程序依然能够正常运行。
序列化 ID 在 Eclipse 下提供了两种生成策略
一个是固定的 1L
一个是随机生成一个不重复的 long 类型数据(实际上是使用 JDK 工具,根据类名、接口名、成员方法及属性等来生成)
如果是通过网络传输的话,如果被序列化类的serialVersionUID不一致,那么反序列化就不能正常进行。例如在客户端A中Foo类的serialVersionUID=1L,而在客户端B中Foo类的serialVersionUID=2L 那么就不能重构这个Person对象。
二. transient的作用及使用方法
我们都知道一个对象只要实现了Serilizable接口,这个对象就可以被序列化,java的这种序列化模式为开发者提供了很多便利,我们可以不必关系具体序列化的过程,只要这个类实现了Serilizable接口,这个类的所有属性和方法都会自动序列化。
然而在实际开发过程中,我们常常会遇到这样的问题,这个类的有些属性需要序列化,而其他属性不需要被序列化,打个比方,如果一个用户有一些敏感信息(如密码,银行卡号等),为了安全起见,不希望在网络操作(主要涉及到序列化操作,本地序列化缓存也适用)中被传输,这些信息对应的变量就可以加上transient关键字。换句话说,这个字段的生命周期仅存于调用者的内存中而不会写到磁盘里持久化。
总之,java 的transient关键字为我们提供了便利,你只需要实现Serilizable接口,将不需要序列化的属性前添加关键字transient,序列化对象的时候,这个属性就不会序列化到指定的目的地中。
示例code如下:
import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.io.Serializable; /** * @description 使用transient关键字不序列化某个变量 * 注意读取的时候,读取数据的顺序一定要和存放数据的顺序保持一致 * * @author Alexia * @date 2013-10-15 */ public class TransientTest { public static void main(String[] args) { User user = new User(); user.setUsername("Alexia"); user.setPasswd("123456"); 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("C:/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( "C:/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(); } } } class User implements Serializable { private static final long serialVersionUID = 8294180014912103005L; 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; } }
输出为:
read before Serializable: username: Alexia password: 123456 read after Serializable: username: Alexia password: null
密码字段为null,说明反序列化时根本没有从文件中获取到信息。
2. transient使用小结
1)一旦变量被transient修饰,变量将不再是对象持久化的一部分,该变量内容在序列化后无法获得访问。
2)transient关键字只能修饰变量,而不能修饰方法和类。注意,本地变量是不能被transient关键字修饰的。变量如果是用户自定义类变量,则该类需要实现Serializable接口。
3)被transient关键字修饰的变量不再能被序列化,一个静态变量不管是否被transient修饰,均不能被序列化。
第三点可能有些人很迷惑,因为发现在User类中的username字段前加上static关键字后,程序运行结果依然不变,即static类型的username也读出来为“Alexia”了,这不与第三点说的矛盾吗?实际上是这样的:第三点确实没错(一个静态变量不管是否被transient修饰,均不能被序列化),反序列化后类中static型变量username的值为当前JVM中对应static变量的值,这个值是JVM中的不是反序列化得出的,不相信?好吧,下面我来证明:
import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.io.Serializable; /** * @description 使用transient关键字不序列化某个变量 * 注意读取的时候,读取数据的顺序一定要和存放数据的顺序保持一致 * * @author Alexia * @date 2013-10-15 */ public class TransientTest { public static void main(String[] args) { User user = new User(); user.setUsername("Alexia"); user.setPasswd("123456"); 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("C:/user.txt")); os.writeObject(user); // 将User对象写进文件 os.flush(); os.close(); } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } try { // 在反序列化之前改变username的值 User.username = "jmwang"; ObjectInputStream is = new ObjectInputStream(new FileInputStream( "C:/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(); } } } class User implements Serializable { private static final long serialVersionUID = 8294180014912103005L; 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; } }
运行结果为:
read before Serializable: username: Alexia password: 123456 read after Serializable: username: jmwang password: null
这说明反序列化后类中static型变量username的值为当前JVM中对应static变量的值,为修改后jmwang,而不是序列化时的值Alexia。
3. transient使用细节——被transient关键字修饰的变量真的不能被序列化吗?
思考下面的例子:
import java.io.Externalizable; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.ObjectInput; import java.io.ObjectInputStream; import java.io.ObjectOutput; import java.io.ObjectOutputStream; /** * @descripiton Externalizable接口的使用 * * @author Alexia * @date 2013-10-15 * */ 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(); } }
content变量会被序列化吗?好吧,我把答案都输出来了,是的,运行结果就是:
是的,我将会被序列化,不管我是否被
transient
关键字修饰
这是为什么呢,不是说类的变量被transient关键字修饰以后将不能序列化了吗?
我们知道在Java中,对象的序列化可以通过实现两种接口来实现,若实现的是Serializable接口,则所有的序列化将会自动进行,若实现的是Externalizable接口,则没有任何东西可以自动序列化,需要在writeExternal方法中进行手工指定所要序列化的变量,这与是否被transient修饰无关。因此第二个例子输出的是变量content初始化的内容,而不是null。
序列化-二进制文件格式详解
样例代码
public class Employee implements Serializable { private String name; private double salary; private Date hireDay; public Employee(String n, double s, int year, int month, int day) { name = n; salary = s; GregorianCalendar calendar = new GregorianCalendar(year, month - 1, day); hireDay = calendar.getTime(); } // 略 }
public class Tester{ private static final String SAVED_PATH = "src/main/java/objectStream/employee.dat"; public static void main(String[] args) throws IOException, ClassNotFoundException { // 持久化到本地存储中 Employee harry = new Employee("Harry Hacker", 50000, 1989, 10, 1); try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(SAVED_PATH))) { out.writeObject(obj); } } }
分析
以下是在notepad++下以hex模式打开employee.dat的文件截图
接下来按照顺序逐一解读
1:魔法数 AC ED (可在ObjectStreamConstants接口中找到)
2:序列化格式的版本号 00 05 (可在ObjectStreamConstants接口中找到)
3:接下来的 73 72 解读如下 (参见 协议文档)
73 代表接下来读取到的将是一个对象 (final static byte TC_OBJECT = (byte)0x73;)
72 代表该对象是一个对类的描述 (final static byte TC_CLASSDESC = (byte)0x72;)
4:接下来的 00 15, 指代该类描述信息的长度, 经过转换计算, 内容正好是类Employee的完整命名objectStream.Employee;
5:然后是ed 65 0f 78 f9 97 ff b6,这八位是用来验证该类是否被修改过的验证码. 因为我们没有在实现Serializable接口后, 添加serialVersionUID 字段, 所以JVM会自动帮助我们生成一个.
6:接下来就是 02, 该一个字节长度的标志信息代表了 序列化中标识类版本 ; 该数值也是可以在ObjectStreamConstants接口中找到. (final static byte SC_SERIALIZABLE = 0x02;)
7:继续往下就是 00 03 , 这两个字节长度的标志信息指代的是 该类型中字段的个数. 如这里所见, 正好对应了 Employee 中的三个字段.
8:接着往下就是对这三个字段的逐一解读了,
1:如上所示, 以上标注出的是 double 类型的 salary 字段的解读.
1.1:44 即 D ; 正好对应的是 double
1.2:00 06 代表该字段名称所占的长度
1.3:接下来的6字节长度的73 61 6c 61 72 79 正好是 salary 字符串的16进制版本.
2:接下来的 4c 00 07 68 ... 解读如下
2.1:44 即 L, 所代表的是 对象 , 正好和 java.util.Date 匹配
2.2:00 07 依然是长度
2.3:接下来的7位 也就是 字段名 hireDay 字符的内容了.
2.4:接下来对 Date类型的字段解读如下: 即从 74 00 10 4c 开始
2.5:Date类型的hireDay字段 : 类型 L (4c) , 字段名7位长度, 名称为hireDay, 字段类型为 74(字段类型以74开头), 字段类型 类名长度16, java.lang.String
2.6:最后就是name字段了: 以下就是 字段 name 类型 L (4c)(String属于对象, 不属于基本类型) , 字段名4位长度, 名称为 name, 字段类型为 74, 字段类型 类名长度18, java.lang.String;
9:接下来就是 类型描述信息结束的标识了
10:接下来就是对象信息的描述了
1:首先是double类型的salary, 所以 78 70之后的 40 e8 6a 00 00 00 00 00正是它的值.
2:接下来是Date类型的 hireDay, 注意选中的部分, 前面四个字符73 72 00 0e正是 一个字节长度的 对象标识, 一个字节长度的类描述符标识, 两个字节长度的 长度标识
这里还要注意的一点是, 和我上面红线标出来的不同的是
1:红色标识出来的是 其解析出来, 内容是 java/util/Date
2:而选中部分解析出来, 其内容是 java.util.Date
3:最后是 name 实例字段 数据
出处资料:https://blog.csdn.net/lqzkcx3/article/details/79463450
https://mp.weixin.qq.com/s/7sRNnY7ZFklCW38_fEjA9g