1.概述
序列化与反序列化:
Java序列化是指把Java对象转换为字节序列的过程;
Java反序列化是指把字节序列恢复为Java对象的过程。
序列化分为两大部分:序列化和反序列化。序列化是这个过程的第一部分,将数据分解成字节流,以便存储在文件中或在网络上传输。
反序列化就是打开字节流并重构对象。对象序列化不仅要将基本数据类型转换成字节表示,有时还要恢复数据。恢复数据要求有恢复数据的对象实例。
为什么需要序列化与反序列化?
我们知道,当两个进程进行远程通信时,可以相互发送各种类型的数据,包括文本、图片、音频、视频等, 而这些数据都会以二进制序列的形式在网络上传送。那么当两个Java进程进行通信时,能否实现进程间的对象传送呢?答案是可以的。如何做到呢?这就需要Java序列化与反序列化了。换句话说,一方面,发送方需要把这个Java对象转换为字节序列,然后在网络上传送;另一方面,接收方需要从字节序列中恢复出Java对象。
当我们明晰了为什么需要Java序列化和反序列化后,我们很自然地会想Java序列化的好处。其好处一是实现了数据的持久化,通过序列化可以把数据永久地保存到硬盘上(通常存放在文件里),二是,利用序列化实现远程通信,即在网络上传送对象的字节序列。
① 想把内存中的对象保存到一个文件中或者数据库中时候;
② 想用套接字在网络上传送对象的时候;
③ 想通过RMI传输对象的时候
几种常见的序列化与反序列化协议:
XML & SOAP:
XML 是一种常用的序列化和反序列化协议,具有跨机器,跨语言等优点,SOAP(Simple Object Access protocol) 是一种被广泛应用的,基于 XML 为序列化和反序列化协议的结构化消息传递协议。
JSON(Javascript Object Notation)
Protobuf
2.序列化实现:
只有实现了Serializable或者Externalizable接口的类的对象才能被序列化为字节序列。(不是则会抛出异常) 。
Serializable 接口是Java提供的序列化接口,他是一个空接口:
public interface Serializable {
}
Serializable 用来标识当前类可以被ObjectOutputStream序列化,以及被ObjectInputStream反序列化。
Serializable 接口的特点
Serializable是Java提供的序列化接口,是一个空接口,为对象提供标准的序列化与反序列化操作。使用Serializable实现序列化过程相当简单,只需要在类声明的时候指定一个标识,便可以自动的实现默认的序列化过程。
private static final long serialVersionUID = 1L;
上面已经说明让对象实现序列化,只需要让当前类实现Serializable接口,并且声明一个serialVersionUID就可以了,非常的简单方便。实际上serialVersionUID都不是必须的,没有它同样可以正常的实现序列化操作。
User类就是一个实现了Serialzable的类,它是可以被序列化和反序列化的。
public class User implements Serializable {
private static final long serialVersionUID = 1L;
private String userId;
private String userName;
}
通过Serializable实现对象的序列化过程非常的简单,无需任何操作,系统就为我们自动实现了。如何进行对象的序列化与反序列化操作也是非常的简单,只需要通ObjectOutputStream,ObjectInputStream进行操作就可以了。
//序列化过程
public void toSerial() {
try {
User user = new User("id", "user");
ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("user.txt"));
objectOutputStream.writeObject(user);
objectOutputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
//反序列化过程
public void fromSerial(){
try {
ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream("user.txt"));
User user = (User) objectInputStream.readObject();
objectInputStream.close();
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
这个serialVersionUID是用来辅助对象的序列化与反序列化的,原则上序列化后的数据当中serialVersionUID与当前类当中的serialVersionUID一致,那么该对象才能被反序列化成功。这个serialVersionUID的详细的工作机制是:在序列化的时候系统将serialVersionUID写入到序列化的文件中去,当反序列化的时候系统会先去检测文件中的serialVersionUID是否跟当前的文件的serialVersionUID是否一致,如果一直则反序列化成功,否则就说明当前类跟序列化后的类发生了变化,比如是成员变量的数量或者是类型发生了变化,那么在反序列化时就会发生crash,并且回报出错误:
注意:
- 一个实现 Serializable 接口的子类也是可以被序列化的。
- 静态成员变量是不能被序列化
- transient 标识的对象成员变量不参与序列化
Externalizable 接口:
继承自 Serializable接口,Externalizable接口定义了两个抽象方法:writeExternal()与readExternal(),通过这些方法指定序列化哪些属性不序列化哪些属性。注意:实现Externalizable接口的类可以不设置serialVersionUID常量,但必须要求序列化前后的两个类完全相同,为了编程更显灵活,推荐设置serialVersionUID常量。
import java.io.Externalizable;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectOutput;
public class UserInfo implements Externalizable {
private int age;
private String name;
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 void writeExternal(ObjectOutput out) throws IOException {
out.writeObject(name);
out.writeInt(age);
}
@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
name = (String) in.readObject();
age = in.readInt();
}
@Override
public String toString() {
return "name='" + name + '\'' + ", age=" + age;
}
}
import java.io.*;
public class Test {
/**
* 序列化
*
* @author GaoHuanjie
*/
public static void serialize(){
UserInfo userInfo = new UserInfo();
userInfo.setAge(23);
userInfo.setName("Tom");
System.out.println(userInfo);
ObjectOutput objectOutput = null;
try {
objectOutput = new ObjectOutputStream(new FileOutputStream("D:\\user_info.ser"));
objectOutput.writeObject(userInfo);
objectOutput.flush();
} catch (IOException e) {
e.printStackTrace();
} finally {
if (objectOutput!=null) {
try {
objectOutput.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
/**
* 反序列化
*
* @author GaoHuanjie
*/
public static void deserialize(){
ObjectInput objectInput = null;
try {
objectInput = new ObjectInputStream(new FileInputStream("D:\\user_info.ser"));
UserInfo userInfo = (UserInfo) objectInput.readObject();
System.out.println(userInfo);
} catch (Exception e) {
e.printStackTrace();
} finally {
if (objectInput!=null) {
try {
objectInput.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
public static void main(String[] args) {
serialize();//序列化
deserialize();//反序列化
}
}
Serializable接口与Externalizable接口区别:
- Serializable反序列化时不会调用默认构造方法,而Externalizable反序列化时会调用默认构造器方法;
- Serializable反序列化时构造方法可以是任意访问权限的控制符,而Externalizable反序列化时只能是public;
- Serializable序列化时,static或transient修饰的属性不会被序列化,而Externalizable序列化时只能通过writeExternal()和readExternal()方法指定序列化哪些属性不序列化哪些属性,不能使用transient修饰符
序列化ID:
我们在进行序列化时,加了一个serialVersionUID字段,这便是序列化ID。
private static final long serialVersionUID = 1L;
它决定着是否能够成功反序列化!java的序列化机制是通过判断运行时类的serialVersionUID来验证版本一致性的,在进行反序列化时,JVM会把传进来的字节流中的serialVersionUID与本地实体类中的serialVersionUID进行比较,如果相同则认为是一致的,便可以进行反序列化,否则就会报序列化版本不一致的异常。
序列化ID是为了保证成功进行反序列化。
如何生成serialVersionUID?
- 使用 AS plugin 插件就可以生成
- 在JDK中,可以利用 JDK 的 bin 目录下的 serialver 工具产生这个serialVersionUID,对于 Student.class,执行命令:serialver com.example.seriable.Student
➜ classes git:(master) ✗ /Library/Java/JavaVirtualMachines/jdk1.8.0_111.jdk/Contents/Home/bin/serialver com.example.seriable.Student
com.example.seriable.Student: private static final long serialVersionUID = -6840182814363029482L;//这个就是工具生成的 SerialVersionUID 值了
serialVersionUID 发生改变有三种情况:
- 手动去修改导致当前的 serialVersionUID 与序列化前的不一样。
- 我们根本就没有手动去写这个 serialVersionUID 常量,那么 JVM 内部会根据类结构去计算得到这个 serialVersionUID 值,在类结构发生改变时(属性增加,删除或者类型修改了)这种也是会导致 serialVersionUID 发生变化。
- 假如类结构没有发生改变,并且没有定义 serialVersionUID ,但是反序列和序列化操作的虚拟机不一样也可能导致计算出来的 serialVersionUID 不一样。
默认序列化ID:
当我们一个实体类中没有显式的定义一个名为“serialVersionUID”、类型为long的变量时,Java序列化机制会根据编译时的class自动生成一个serialVersionUID作为序列化版本比较,这种情况下,只有同一次编译生成的class才会生成相同的serialVersionUID。譬如,当我们编写一个类时,随着时间的推移,我们因为需求改动,需要在本地类中添加其他的字段,这个时候再反序列化时便会出现serialVersionUID不一致,导致反序列化失败。
3.Java 反射
1.反射机制有什么用?
通过java语言中的反射机制可以操作字节码文件(可以读和修改字节码文件。)
通过反射机制可以操作代码片段。(class文件。)
2.反射机制所在包:
java.lang.reflect.*;
3.反射机制有关的类:
类 | 含义 |
---|---|
java.lang.Class | 代表整个字节码。代表一个类型,代表整个类。 |
java.lang.reflect.Method | 代表字节码中的方法字节码。代表类中的方法。 |
java.lang.reflect.Constructor | 代表字节码中的构造方法字节码。代表类中的构造方法。 |
java.lang.reflect.Field | 代表字节码中的属性字节码。代表类中的成员变量(静态变量+实例变量)。 |
注:必须先获得Class才能获取Method、Constructor、Field。
java.lang.Class:
public class User{
// Field
int no;
// Constructor
public User(){
}
public User(int no){
this.no = no;
}
// Method
public void setNo(int no){
this.no = no;
}
public int getNo(){
return no;
}
}
4.获取Class的三种方式:
要操作一个类的字节码,需要首先获取到这个类的字节码,怎么获取java.lang.Class实例?
方式 | 备注 |
---|---|
Class.forName(“完整类名带包名”) | 静态方法 |
对象.getClass() | |
任何类型.class |
5.通过反射实例化对象
对象.newInstance()
newInstance()方法内部实际上调用了无参数构造方法,必须保证无参构造存在才可以。否则会抛出java.lang.InstantiationException异常。
class ReflectTest02{
public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException {
// 下面这段代码是以反射机制的方式创建对象。
// 通过反射机制,获取Class,通过Class来实例化对象
Class c = Class.forName("javase.reflectBean.User");
// newInstance() 这个方法会调用User这个类的无参数构造方法,完成对象的创建。
// 重点是:newInstance()调用的是无参构造,必须保证无参构造是存在的!
Object obj = c.newInstance();
System.out.println(obj);
}
}
6.Class.forName导致类加载
如果你只是希望一个类的静态代码块执行,其它代码一律不执行,可以使用:
Class.forName("完整类名");