前言
序列化有什么用处呢是怎么回事呢?序列化相信大家都很熟悉,但是序列化有什么用处呢是怎么回事呢,下面就让我(小编)带大家一起了解吧。
介绍序列化
什么是序列化
- 序列化:把 Java 对象转换为字节序列;
- 反序列:把字节序列恢复为 Java 对象。
序列化的意义
Java 对象是运行在 JVM 的堆内存中的,如果 JVM 停止后,它的生命也就走到尽头了。如果想在 JVM 停止后,把这些对象保存到磁盘或者通过网络传输到另一远程机器,怎么办呢?
磁盘这些硬件可不认识 Java 对象,它们只认识二进制这些机器语言,所以就要把这些对象转化为字节序列,这些字节序列可以保存在磁盘上,或通过网络传输,以达到以后恢复成原来的对象。
序列化机制使得对象可以脱离程序的运行而独立存在。
序列化的用途
序列化机制可以让 Java 对象地保存到硬盘上,减轻内存压力的同时,也起了持久化的作用;同时可以让 Java 对象可在网络上传输。
序列化常用的API
Serializable 接口
Serializable 接口是一个标记接口,没有任何方法或字段。一旦实现了此接口,就标志该类的对象就是可序列化的。
package java.io;
public interface Serializable {
}
Externalizable 接口
Externalizable 接口继承了 Serializable 接口,还定义了两个抽象方法:writeExternal()
和 readExternal()
。
如果使用 Externalizable 来实现序列化和反序列化,需要重写 writeExternal() 和 readExternal() 方法,这是强制性的;此外,必须提供 public
的无参构造器,因为在反序列化的时候需要反射创建对象。
package java.io;
import java.io.ObjectOutput;
import java.io.ObjectInput;
public interface Externalizable extends java.io.Serializable {
void writeExternal(ObjectOutput out) throws IOException;
void readExternal(ObjectInput in) throws IOException, ClassNotFoundException;
}
ObjectOutputStream类
表示对象输出流,它的 writeObject(Object obj)
方法可以对指定 obj 对象参数进行序列化,再把得到的字节序列写到一个目标输出流中。
ObjectInputStream类
表示对象输入流,它的 readObject()
方法,从输入流中读取到字节序列,反序列化成为一个对象,最后将其返回。
使用序列化
使用序列化的步骤:
- 创建一个实体类,实现 Serializable 接口;
- 使用 ObjectOutputStream 类的 writeObject() 方法,实现序列化;
- 使用 ObjectInputStream 类的 readObject() 方法,实现反序列化。
实践1
import java.io.*;
/**
* @author yyt
* 对象序列化:将内存中保存的对象以二进制数据流的形式进行处理,可以实现对象的保存或网络传输。
* 不是所有的对象都可以被序列化,如果需要进行序列化,需要实现 java.io.Serializable 父接口;这个接口没有任何方法。
* 想要实现序列化操作需要使用 ObjectOutputStream 类,反序列化需要使用 ObjectInputStream 类;
* 如果想要实现一组对象的序列化,可以使用对象数组完成。
*/
public class JavaDemo41 {
private static final File SAVED_FILE = new File("D:" + File.separator + "Desktop" + File.separator + "Serializable.user");
public static void main(String[] args) throws IOException, ClassNotFoundException {
// saveObject(new User1("张三", "123456", 22));
System.out.println(loadObject());
}
public static void saveObject(Object obj) throws IOException {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(SAVED_FILE));
// 调用 ObjectOutputStream 类的序列化方法
oos.writeObject(obj);
oos.close();
}
public static Object loadObject() throws IOException, ClassNotFoundException {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(SAVED_FILE));
// 调用 ObjectInputStream 类的反序列化方法
Object obj = ois.readObject();
ois.close();
return obj;
}
}
/**
* 定义一个可以序列化的类
* transient 关键字可以指定哪些属性不需要序列化,在序列化处理时这个属性就不会被保存,那么读取时值会变为 null
*/
class User1 implements Serializable {
private static final long serialVersionUID = 1L;
private transient String username;
private String password;
private int age;
public User1(String username, String password, int age) {
this.username = username;
this.password = password;
this.age = age;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public String toString() {
return "【User1】信息:username=" + username +
", password=" + password + ", age=" + age;
}
}
其中,我定义了一个变量 SAVED_FILE
用于定义存储文件的路径(自定义),运行 main
方法执行 saveObject()
这一行(已注释掉了),则会在指定的路径创建指定的文件,将 User1
类的信息序列化存储进去。当然,打开那个文件会显示乱码。
执行 loadObject()
方法则是从指定的路径读取文件,反序列化显示在控制台。
在 User1
类的构造方法中添加一句:System.out.println("生成 User1 类对象");
public User1(String username, String password, int age) {
System.out.println("生成 User1 类对象");
this.username = username;
this.password = password;
this.age = age;
}
在执行loadObject()
方法时,并不会出现这句话,说明反序列化并不会调用构造方法。反序列的对象是由 JVM 自己生成的对象,不通过构造方法生成。
成员是引用的序列化
如果一个可序列化的类的成员不是基本类型,也不是 String 类型(其实,String 也实现了 java.io.Serializable
接口),那这个引用类型也必须是可序列化的;否则,会导致此类不能序列化。
实践2
import java.io.*;
/**
* @author yyt
*/
public class JavaDemo43 {
private static final File SAVED_FILE = new File("D:" + File.separator + "Desktop" + File.separator + "vip.txt");
public static void main(String[] args) throws IOException, ClassNotFoundException {
VipRight vr = new VipRight(007, "2021年11月3日", "2021年12月3日");
MonthVipUser mv = new MonthVipUser("李四", vr);
// 运行到这,就会抛出 java.io.NotSerializableException 异常
saveObject(mv);
// System.out.println(loadObject());
}
public static void saveObject(Object obj) throws IOException {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(SAVED_FILE));
// 调用 ObjectOutputStream 类的序列化方法
oos.writeObject(obj);
oos.close();
}
public static Object loadObject() throws IOException, ClassNotFoundException {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(SAVED_FILE));
// 调用 ObjectInputStream 类的反序列化方法
Object obj = ois.readObject();
ois.close();
return obj;
}
}
/**
* Vip 权益,此类没有实现 java.io.Serializable 接口
* @author yyt
*/
class VipRight {
private int id;
private String startDate;
private String endDate;
// 省略有参构造
// 省略 setter 与 getter
}
/**
* 月付 Vip 用户,实现了序列化接口
* @author yyt
*/
class MonthVipUser implements Serializable {
private String username;
// 此处引用了 VipRight 类
private VipRight vr;
// 也省略有参构造
// 也省略 setter 与 getter
@Override
public String toString() {
return "MonthVipUser{username='" + username
+ '\'' + ", vr=" + vr + '}';
}
}
以上代码中,VipRight
类并没有实现 Serializable
接口。在运行 main
方法执行到 saveObject(mv);
这一句话时,就会抛出异常:
Exception in thread "main" java.io.NotSerializableException: secjavademo.VipRight
at java.base/java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1185)
at java.base/java.io.ObjectOutputStream.defaultWriteFields(ObjectOutputStream.java:1553)
at java.base/java.io.ObjectOutputStream.writeSerialData(ObjectOutputStream.java:1510)
at java.base/java.io.ObjectOutputStream.writeOrdinaryObject(ObjectOutputStream.java:1433)
at java.base/java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1179)
at java.base/java.io.ObjectOutputStream.writeObject(ObjectOutputStream.java:349)
at secjavademo.JavaDemo43.saveObject(JavaDemo43.java:23)
at secjavademo.JavaDemo43.main(JavaDemo43.java:16)
transient 关键字
有些时候,某些属性不需要序列化。那么可以使用 transient
关键字选择不需要序列化的字段。
实践3
import java.io.*;
/**
* @author yyt
*/
public class JavaDemo43 {
private static final File SAVED_FILE = new File("D:" + File.separator + "Desktop" + File.separator + "vip.txt");
public static void main(String[] args) throws IOException, ClassNotFoundException {
VipUser mv = new VipUser("龙傲天");
System.out.println("序列化之前:" + mv.toString());
saveObject(mv);
// 修改静态属性值 gender
VipUser.gender = "男";
System.out.println("序列化后:");
System.out.println(loadObject());
}
public static void saveObject(Object obj) throws IOException {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(SAVED_FILE));
// 调用 ObjectOutputStream 类的序列化方法
oos.writeObject(obj);
oos.close();
}
public static Object loadObject() throws IOException, ClassNotFoundException {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(SAVED_FILE));
// 调用 ObjectInputStream 类的反序列化方法
Object obj = ois.readObject();
ois.close();
return obj;
}
}
/**
* Vip 用户,实现了序列化接口
* @author yyt
*/
class VipUser implements Serializable {
private String username;
/**
* 使用 static 修饰,可以被序列化
*/
public static String gender = "神";
/**
* 使用 transient 修饰,不会被序列化
*/
transient int age = 98;
public VipUser(String username) {
this.username = username;
}
// 省略 setter 与 getter
@Override
public String toString() {
return "VipUser{" +
"username='" + username + '\'' +
", gender='" + gender + '\'' +
", age=" + age +
'}';
}
}
运行结果:
从输出的结果可知,使用序列化之前:VipUser{username='龙傲天', gender='神', age=98}
序列化后:VipUser{username='龙傲天', gender='男', age=0}
transient
修饰的属性,Java 序列化时,会忽略掉此字段,所以反序列化出的对象,被 transient
修饰的属性是默认值。即:引用类型对应 null;基本类型对应 0;布尔类型对应 false。
序列版本号serialVersionUID
serialVersionUID
表面意思就是序列化版本号 ID,其实每一个实现 Serializable
接口的类,都有一个 private static final long serialVersionUID
,即使没有人为显式地给它定义一个,编译器也会为它自动声明一个。
Java 序列化的机制是通过判断类的 serialVersionUID
来验证的版本一致的。如果相同说明是一致的,可以进行反序列化,即使更改了序列化属性,对象也可以正确被反序列化回来,否则会出现反序列化版本一致的异常,即 InvalidCastException
。
实践4
以上面的 VipUser
类为基础,类中没有显式定义 serialVersionUID
,在序列化之后,再从 VipUser
类中增加一个属性 password
,运行反序列化时,会抛出异常。
public class JavaDemo43 {
private static final File SAVED_FILE = new File("D:" + File.separator + "Desktop" + File.separator + "vip.txt");
public static void main(String[] args) throws IOException, ClassNotFoundException {
// VipUser mv = new VipUser("龙傲天");
// System.out.println("序列化之前:" + mv.toString());
// saveObject(mv);
System.out.println("序列化后:");
System.out.println(loadObject());
}
public static void saveObject(Object obj) throws IOException {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(SAVED_FILE));
// 调用 ObjectOutputStream 类的序列化方法
oos.writeObject(obj);
oos.close();
}
public static Object loadObject() throws IOException, ClassNotFoundException {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(SAVED_FILE));
// 调用 ObjectInputStream 类的反序列化方法
Object obj = ois.readObject();
ois.close();
return obj;
}
}
/**
* Vip 用户,实现了序列化接口
* @author yyt
*/
class VipUser implements Serializable {
private String username;
/**
* 假设版本升级了,新增了一个属性 password
*/
private String password;
public VipUser(String username) {
this.username = username;
}
// 省略 setter 与 getter
@Override
public String toString() {
return "VipUser{" +
"username='" + username + '\'' +
'}';
}
}
控制台输出:
如果不显式指定Exception in thread "main" java.io.InvalidClassException: secjavademo.VipUser; local class incompatible: stream classdesc serialVersionUID = 7274932503003786147, local class serialVersionUID = -4334542806013252401
at java.base/java.io.ObjectStreamClass.initNonProxy(ObjectStreamClass.java:689)
at java.base/java.io.ObjectInputStream.readNonProxyDesc(ObjectInputStream.java:2012)
at java.base/java.io.ObjectInputStream.readClassDesc(ObjectInputStream.java:1862)
at java.base/java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:2169)
at java.base/java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1679)
at java.base/java.io.ObjectInputStream.readObject(ObjectInputStream.java:493)
at java.base/java.io.ObjectInputStream.readObject(ObjectInputStream.java:451)
at secjavademo.JavaDemo43.loadObject(JavaDemo43.java:31)
at secjavademo.JavaDemo43.main(JavaDemo43.java:18)Process finished with exit code 1
serialVersionUID
,一旦像上面一样更改了类的结构或者信息,则类的 serialVersionUID 也会跟着变化,自然会抛出异常终止程序运行。如果指定了版本号,如果只是修改方法、瞬态变量(transient 修饰的变量),反序列化不受影响,无需修改版本号。
实践5
在 VipUser
类指定版本号,并新增一个方法,序列化后,再修改方法名,依旧可以正常反序列化。
// 指定序列化版本号
private static final long serialVersionUID = 20211203L;
/**
* 序列化后,再修改方法名为 doSomething()
*/
public void doSomething666() {
System.out.println("I am doing something...");
}
实践6
在 VipUser
类指定版本号,序列化后,再新增一个属性,记得重写 toString()
方法,依旧可以正常反序列化,只不过反序列化回来新增的是默认值。对应的,如果减少了实例变量,反序列化时会忽略掉减少的实例变量。
// 指定序列化版本号
private static final long serialVersionUID = 202112021L;
/**
* 序列化后,再新增属性 password
*/
private String password;
@Override
public String toString() {
return "VipUser{" +
"username='" + username + '\'' +
", password='" + password + '\'' +
'}';
}
所以,凡是实现 Serializable
接口的类,都最好显式地为它指定一个 serialVersionUID
明确值。
RMI
远程接口
import java.rmi.Remote;
import java.rmi.RemoteException;
/**
* @author yyt
* 远程接口
*/
public interface IMyRemote extends Remote {
String sayHello(String msg) throws RemoteException;
}
远程接口的实现类如下:
import java.net.MalformedURLException;
import java.rmi.Naming;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.server.UnicastRemoteObject;
import java.text.DateFormat;
import java.util.Date;
//远程接口实现-远程对象
public class MyRemoteImpl extends UnicastRemoteObject implements IMyRemote {
public MyRemoteImpl() throws RemoteException {
super();
}
@Override
public String sayHello(String msg) throws RemoteException {
// 日期格式化
// SimpleDateFormat now = new SimpleDateFormat("EEEE-MMMM-dd-yyyy");
DateFormat now = DateFormat.getDateTimeInstance(DateFormat.FULL, DateFormat.FULL);
Date date = new Date();
return "接受到【客户端】的信息:" + msg + "\r\n"
+ "来自【服务端】的提示:现在是" + now.format(date).toString() + ", '你好,'我是大帅比,收到请回复";
}
public static void main(String[] args) throws RemoteException, MalformedURLException {
IMyRemote service = new MyRemoteImpl();
// 启动本地rmi registry,默认端口1099
// Port already in use: 1099; nested exception is: java.net.BindException: Address already in use: JVM_Bind
LocateRegistry.createRegistry(1099);
// 注册远程对象 本地 10.21.91.89
Naming.rebind("rmi://localhost:1099/RemoteHello", service);
}
}
运行该远程对象的服务器代码如下:
import java.net.MalformedURLException;
import java.rmi.Naming;
import java.rmi.NotBoundException;
import java.rmi.RemoteException;
/**
* @author yyt
* 客户端
*/
public class MyRemoteClient {
private void go() throws RemoteException, NotBoundException, MalformedURLException {
// 中间为隔壁同学电脑的 IP,端口号随便改
IMyRemote service = (IMyRemote) Naming.lookup("rmi://xxx.xxx.xxx.x:1099/RemoteHello");
System.out.println(service.sayHello("Fuck the life"));
}
public static void main(String[] args) throws RemoteException, NotBoundException, MalformedURLException {
new MyRemoteClient().go();
}
}
参考
稀土掘金 - 9龙
稀土掘金 - CodeSheep
CSDN - lmy86263
好久没写 Java基础
系列的文章了,现在还来得及。