Java基础丨序列化

前言

序列化有什么用处呢是怎么回事呢?序列化相信大家都很熟悉,但是序列化有什么用处呢是怎么回事呢,下面就让我(小编)带大家一起了解吧。

介绍序列化

什么是序列化

  • 序列化:把 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() 方法,从输入流中读取到字节序列,反序列化成为一个对象,最后将其返回。

使用序列化

使用序列化的步骤:

  1. 创建一个实体类,实现 Serializable 接口;
  2. 使用 ObjectOutputStream 类的 writeObject() 方法,实现序列化;
  3. 使用 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基础 系列的文章了,现在还来得及。

打赏
评论区
头像
文章目录