百度360必应搜狗淘宝本站头条
当前位置:网站首页 > IT技术 > 正文

序列化,Java的实现方式

wptr33 2025-03-10 21:13 29 浏览

当程序创建的对象,在程序终止后仍要存在,并在程序下次运行时重建对象,且拥有与程序上次运行时所拥有的信息相同。序列化可以将对象写入字节流,反序列化就是将字节流恢复为对象,如下图所示。

Java 中提供了一种通用序列化机制,实现将对象写出到输出流中,并在之后将其读回。

对象序列化

定义一个 Student 类,如下所示。

public class Student implements Serializable {
    private static final long serialVersionUID = -4496225960550340595L;
    private String name;
    private Integer age;
    private Double score;
    ...getter与setter...
    @Override
    public String toString() {
        return new StringJoiner(", ", Student.class.getSimpleName() + "[", "]")
                .add("name='" + name + "'")
                .add("age=" + age)
                .add("score=" + score)
                .toString();
    }
}

在程序中使用 Student 类可以创建实例来表示某一学生。

Student s = new Student();
s.setName("小赵");
s.setAge(24);
s.setScore(98.5);

但是,在程序结束后,该实例会被销毁,如果想在下次运行时拥有与上次运行时相同的信息,使用 ObjectOutputStream 实例将 Student 实例序列化保存到文件中。

ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("xxx"));
out.writeObject(s);
out.close();

对象序列化才使用 writeObject/readObject 方法,基本类型需要使用 writeInt/readIntwriteDouble/readDouble 这样的方法,对象流类都实现了 DataInput/DataOutput 接口。

保存到文件中后,想要使用的话就可以通过创建 ObjectInputStream 实例并调用 readObject() 方法来获取。

ObjectInputStream in = new ObjectInputStream(new FileInputStream("xxx"));
Student saved = (Student) in.readObject();
in.close();

Serializable

Java 想要使一个类能被序列化,就需要像 Student 一样,实现 Serializable 接口。

public interface Serializable {
}

该接口没有任何方法需要实现,只是作为类能被序列化的标记。但是没有该接口的话,序列化会报
java.io.NotSerializableException
异常。源码中序列化如下所示。

if (obj instanceof String) {
    writeString((String) obj, unshared);
} else if (cl.isArray()) {
    writeArray(obj, desc, unshared);
} else if (obj instanceof Enum) {
    writeEnum((Enum) obj, desc, unshared);
} else if (obj instanceof Serializable) {    // 判断是否实现了 Serializable 接口
    writeOrdinaryObject(obj, desc, unshared);
} else {
    if (extendedDebugInfo) {
        throw new NotSerializableException(
                    cl.getName() + "\n" + debugInfoStack.toString());
    } else {
        throw new NotSerializableException(cl.getName());
    }
}

当然,实现 Serializable 接口的可序列的类,在序列化时,会默认将类中所有信息序列化,如果想要控制序列化对象,可以使用 transient 关键字标识需要关闭序列化的字段,如下所示。

public class Student implements Serializable {
    private static final long serialVersionUID = -4496225960550340595L;
    private String name;
    private transient Integer age;
    private Double score;
    ···
}
// 序列化前:Student[name='小赵', age=24, score=98.5]
// 反序列化后:Student[name='小赵', age=null, score=98.5]

这样使用 transient 进行标记,就可以在对象序列化时跳过。

使用 Serializable 的默认序列化会降低改变类实现的灵活性,增加 Bug 和安全漏洞的可抗性,测试也会增加负担等,因此想使用 Serializable 实现序列化时要考虑清除。

Externalizable

除了 Serializable 接口外,Java 还提供了继承 Serializable 接口的 Externalizable 接口,并且还要实现两个方法。

public class Student implements Externalizable {
    ...
    @Override
    public void writeExternal(ObjectOutput out) throws IOException {
    }
    @Override
    public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
    }
}

Java 提供的 Externalizable 接口可以控制对象序列化时,不想被其序列化的信息,和反序列时,不被反序列化的信息。

@Override
public void writeExternal(ObjectOutput out) throws IOException {
    out.writeObject(name);
    out.writeObject(score);
}

@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {    // 反序列顺序要与序列化时的顺序一致,否则会报 java.lang.ClassCastException 异常
    name = (String) in.readObject();
    score = (Double) in.readObject();
}

这样就不会将 Student 中的 age 序列化了,对序列化过程进行控制。

readObjectwriteObject 方法是私有的,并且只能被序列化机制调用。与此不同的是,readExternalwriteExternal 方法是公共的。特别是,readExternal 还潜在地允许修改现有对象的状态。

serialVersionUID

在实现 Serializable 接口的可序列化的类里,都需要显示地增加下面这行代码。

private static final long serialVersionUID = -4496225960550340595L;

该静态数据域用于显示声明序列版本 UID,并在序列化机制中,通过判断 serialVersionUID 来验证版本一致性。因此,反序列时,只要 serialVersionUID 与本地相应实体类的 serialVersionUID 一致,就可以反序列化,实现兼容,否则会报
java.io.InvalidClassException
异常。因此,只要 serialVersionUID 不变,序列化就可以读入这个类的对象的不同版本。

如果被序列化的对象具有在当前版本中所有没有的数据域,反序列化时会忽略额外的数据;如果当前版本具有在被序列化的对象所没有的数据域,那么新添加的域将被设置成它们的默认值。

显式的声明 serialVersionUID 也会带来小小的性能好处。如果没有提供显式的序列版本 UID,编译器会选择一个摘要算法,并在运行时通过高开销的计算过程来产生一个序列版本 UID,只要这个类有改动,得到的 UID 也就会变化,到时对象输入流将会拒绝反序列具有不同序列版本 UID。因此,在可序列化的类中声明显式的 serialVersionUID

约束安全

当你确定默认的序列化形式就满足了当前环境,还必须提供一个 readObject 方法添加验证或其他行为以保证约束关系和安全性。

private void readObject(ObjectInputStream in);
private void writeObject(ObjectOutputStream out);

之后,数据域就再也不会被自动序列化,取而代之的是调用这些方法。

如上所示,当反序列化时,如果学生分数不在 0~100 之间,就是错误的数值,可以保护性地编写 readObject() 方法,维护其约束条件。

public class Student implements Serializable {
    ...
    private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
        in.defaultReadObject();    // 调用默认反序列化方法
        if (score < 0 score> 100)
            throw new IllegalArgumentException("The score is between 0 and 100.");    // 判断如果学生成绩有问题,抛出异常,终止操作。
    }
    private void writeObject(ObjectOutputStream out) throws IOException {
        out.defaultWriteObject();    // 调用默认序列化方法
    }
}

枚举序列化

当目标对象唯一时,可使用枚举实现序列化,如下所示。

public enum Week {
    MONDAY,       // 星期一
    TUESDAY,      // 星期二
    WEDNESDAY,    // 星期三
    THURSDAY,     // 星期四
    FIRDAY,       // 星期五
    SATURDAY,     // 星期六
    SUNDAY;       // 星期日
}

为了保证枚举类型及其定义的枚举变量在 VM 中是唯一的,Java规定了枚举常量的序列化是通过ObjectOutputStream 将枚举的 name() 方法返回的值做序列化;反序列化时,通过 ObjectInputStream 从流中读取常量名称,然后调用 java.lang.Enum.valueOf() 方法获得反序列化常量,并将常量的枚举类型和收到的常量名称作为参数传递。

public static <T extends Enum> T valueOf(Class enumType,
                                                String name) {
    T result = enumType.enumConstantDirectory().get(name);
    if (result != null)
        return result;
    if (name == null)
        throw new NullPointerException("Name is null");
    throw new IllegalArgumentException(
        "No enum constant " + enumType.getCanonicalName() + "." + name);
}

编译器在序列化和反序列化时,会忽略枚举类型定义的任何类特定的 writeObjectreadObjectReadObjectNodeDatawriteReplacereadResolve方法。类似地,也会忽略任何 serialPersistentFieldsserialVersionUID 字段声明,所有枚举类型都具有规定的 serialVersionUID 0L。记录枚举类型的可序列化字段和数据是不必要的,因为发送的数据类型没有变化。

在序列化和反序列化时,如果目标对象是唯一的,那么你必须加倍当心,这通常会在实现单例和类型安全的枚举时发生。

但在枚举之前,是通过使用 static final 来表示枚举类型,如下所示。

public class Week {
    public static final Week MONDAY = new Week(1);
    public static final Week TUESDAY = new Week(2);
    public static final Week WEDNESDAY = new Week(3);
    public static final Week THURSDAY = new Week(4);
    public static final Week FIRDAY = new Week(5);
    public static final Week SATURDAY = new Week(6);
    public static final Week SUNDAY = new Week(7);
    private int value;
    private Week(int v) {
        this.value = v;
    }
}

但是,为这个类实现 Serializable 接口变成可序列化的类后,默认序列化机制就不再适用,任何 readObject 方法都会返回一个新键的实例。

public class Week implements Serializable {
    public static final Week MONDAY = new Week(1);
    ···
}

Week w = FIRDAY;
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("xxx/ioweek.txt"));
out.writeObject(w);
out.close();
ObjectInputStream in = new ObjectInputStream(new FileInputStream("xxx/ioweek.txt"));
Week s = (Week) in.readObject();
in.close();

反序列化获取的 Week 对象与序列化的 Week 对象比较 w == sfalse,新键的实例与该类初始化时创建的实例不同,说明即使构造器私有,序列化机制也可以创建新实例。

readResolve 特性允许用 readObject 创建的实例代替另一个实例。因此,在 Week 类中定义 readResolve 方法,如下所示。

private Object readResolve() throws ObjectStreamException {
    if (value == 1) return Week.MONDAY;
    if (value == 2) return Week.TUESDAY;
    if (value == 3) return Week.WEDNESDAY;
    if (value == 4) return Week.THURSDAY;
    if (value == 5) return Week.FIRDAY;
    if (value == 6) return Week.SATURDAY;
    if (value == 7) return Week.SUNDAY;
    throw new ObjectStreamException();
}

定义 readResolve 方法后,反序列化时新键的对象会通过调用 readResolve 方法返回的值称为 readObject 的返回值,因此 w == s 返回的就是 true

序列化实现克隆

可序列化的类可以使用序列化机制实现对象克隆。如下所示,为可序列化的 Student 类提供克隆方法。

public class Student implements Cloneable, Serializable {
    ...
    @Override
    public Object clone() throws CloneNotSupportedException {
        ByteArrayOutputStream bout = new ByteArrayOutputStream();    
        try {
            // save the object to a byte array
            try (
                    ObjectOutputStream out = new ObjectOutputStream(bout)
                    ) {

                out.writeObject(this);
            }
            // read a clone of the object from the byte array
            try (
                    ObjectInputStream in = new ObjectInputStream(new ByteArrayInputStream(bout.toByteArray()))
            ) {
                return in.readObject();
            }

        } catch (IOException | ClassNotFoundException e) {
            CloneNotSupportedException ex = new CloneNotSupportedException();
            ex.initCause(e);
            throw ex;
        }
    }
}

如上所述,类想实现 clone 方法,需要实现 Cloneable 接口,之后调用该 clone 方法即可。使用 ByteArrayOutputStream 将数据保存到字节数组中,而不必将对象写出到文件中。

Student s = new Student();
s.setName("小赵");
s.setAge(24);
s.setScore(98.5);

Student sc = (Student) s.clone();

但这种方式也会比复制或克隆数据域的克隆方法要慢得多。当然,Java 序列化也是有风险的,最好是避免在程序中使用。

相关推荐

oracle数据导入导出_oracle数据导入导出工具

关于oracle的数据导入导出,这个功能的使用场景,一般是换服务环境,把原先的oracle数据导入到另外一台oracle数据库,或者导出备份使用。只不过oracle的导入导出命令不好记忆,稍稍有点复杂...

继续学习Python中的while true/break语句

上次讲到if语句的用法,大家在微信公众号问了小编很多问题,那么小编在这几种解决一下,1.else和elif是子模块,不能单独使用2.一个if语句中可以包括很多个elif语句,但结尾只能有一个else解...

python continue和break的区别_python中break语句和continue语句的区别

python中循环语句经常会使用continue和break,那么这2者的区别是?continue是跳出本次循环,进行下一次循环;break是跳出整个循环;例如:...

简单学Python——关键字6——break和continue

Python退出循环,有break语句和continue语句两种实现方式。break语句和continue语句的区别:break语句作用是终止循环。continue语句作用是跳出本轮循环,继续下一次循...

2-1,0基础学Python之 break退出循环、 continue继续循环 多重循

用for循环或者while循环时,如果要在循环体内直接退出循环,可以使用break语句。比如计算1至100的整数和,我们用while来实现:sum=0x=1whileTrue...

Python 中 break 和 continue 傻傻分不清

大家好啊,我是大田。今天分享一下break和continue在代码中的执行效果是什么,进一步区分出二者的区别。一、continue例1:当小明3岁时不打印年龄,其余年龄正常循环打印。可以看...

python中的流程控制语句:continue、break 和 return使用方法

Python中,continue、break和return是控制流程的关键语句,用于在循环或函数中提前退出或跳过某些操作。它们的用途和区别如下:1.continue(跳过当前循环的剩余部分,进...

L017:continue和break - 教程文案

continue和break在Python中,continue和break是用于控制循环(如for和while)执行流程的关键字,它们的作用如下:1.continue:跳过当前迭代,...

作为前端开发者,你都经历过怎样的面试?

已经裸辞1个月了,最近开始投简历找工作,遇到各种各样的面试,今天分享一下。其实在职的时候也做过面试官,面试官时,感觉自己问的问题很难区分候选人的能力,最好的办法就是看看候选人的github上的代码仓库...

面试被问 const 是否不可变?这样回答才显功底

作为前端开发者,我在学习ES6特性时,总被const的"善变"搞得一头雾水——为什么用const声明的数组还能push元素?为什么基本类型赋值就会报错?直到翻遍MDN文档、对着内存图反...

2023金九银十必看前端面试题!2w字精品!

导文2023金九银十必看前端面试题!金九银十黄金期来了想要跳槽的小伙伴快来看啊CSS1.请解释CSS的盒模型是什么,并描述其组成部分。答案:CSS的盒模型是用于布局和定位元素的概念。它由内容区域...

前端面试总结_前端面试题整理

记得当时大二的时候,看到实验室的学长学姐忙于各种春招,有些收获了大厂offer,有些还在苦苦面试,其实那时候的心里还蛮忐忑的,不知道自己大三的时候会是什么样的一个水平,所以从19年的寒假放完,大二下学...

由浅入深,66条JavaScript面试知识点(七)

作者:JakeZhang转发链接:https://juejin.im/post/5ef8377f6fb9a07e693a6061目录由浅入深,66条JavaScript面试知识点(一)由浅入深,66...

2024前端面试真题之—VUE篇_前端面试题vue2020及答案

添加图片注释,不超过140字(可选)1.vue的生命周期有哪些及每个生命周期做了什么?beforeCreate是newVue()之后触发的第一个钩子,在当前阶段data、methods、com...

今年最常见的前端面试题,你会做几道?

在面试或招聘前端开发人员时,期望、现实和需求之间总是存在着巨大差距。面试其实是一个交流想法的地方,挑战人们的思考方式,并客观地分析给定的问题。可以通过面试了解人们如何做出决策,了解一个人对技术和解决问...