带你快速看完9.8分神作《Effective Java》—— 序列化篇(所有RPC框架的基石)

article/2025/4/25 3:59:24

🔥 Java学习:Java从入门到精通总结

🔥 Spring系列推荐:Spring源码解析

📆 最近更新:2022年1月20日

🍊 个人简介:通信工程本硕💪、阿里新晋猿同学🌕。我的故事充满机遇、挑战与翻盘,欢迎关注作者来共饮一杯鸡汤

🍊 点赞 👍 收藏 ⭐留言 📝 都是我最大的动力!

文章目录

    • 85 其他方法优先于Java序列化
    • 86 谨慎实现 Serializable 接口
    • 87 考虑使用自定义的序列化形式
    • 88 保护性地编写 readObject 方法
    • 89 对于单例控制,枚举类型优先于readResolve
    • 90 考虑用序列化代理代替序列化实例

85 其他方法优先于Java序列化

🔥 先说结论:

  1. 序列化是危险的,应该避免

  2. 如果从头开始设计一个系统,可以使用跨平台的结构化数据,如JSONprotobuf

  3. 如果必须编写可序列化的类,要加倍小心地进行试验


序列化的一个根本问题是它的可攻击范围太大,且难以防御:通过调用ObjectInputStream上的readObject方法反序列化对象。可以用来实例化类路径上任何类型的对象,只要该类型实现Serializable接口,有了实例化之后的对象,就可以执行这些类的代码,因此所有这些类都在攻击范围内。


static byte[] bomb() {Set<Object> root = new HashSet<>();Set<Object> s1 = root;Set<Object> s2 = new HashSet<>();for (int i = 0; i < 100; i++) {Set<Object> t1 = new HashSet<>();Set<Object> t2 = new HashSet<>();t1.add("foo"); // Make t1 unequal to t2s1.add(t1); s1.add(t2);s2.add(t1); s2.add(t2);s1 = t1;s2 = t2;}return serialize(root); // Method omitted for brevity
}

对象图由201个HashSet实例组成,整个流的⻓度为5744字节,但是在对其进行反序列化之前,资源就已经耗尽了。问题在于,反序列化HashSet实例需要计算其内部元素的哈希码,深度为100,反序列化Set会导致hashCode方法被调用超过2^100次。
在这里插入图片描述

避免序列化利用的最好方法是永远不要反序列化任何东西。还有其他一些机制可以在对象和字节序列之间进行转换,从而避免了Java序列化的许多危险。

这些方法共同点是它们比Java序列化简单得多。它们不支持任意对象图的自动序列化和反序列化。而是支持简单的结构化数据对象,由一组「属性-值」对组成。只支持少数基本数据类型和数组数据类型。


最前沿的跨平台结构化数据表示是JSONprotobuf

JSONprotobuf之间最显著的区别是JSON是基于文本的,而protobuf是二进制的,但效率更高;

如果你不能完全避免Java序列化,这时的最佳选择是永远不要反序列化不可信的数据


86 谨慎实现 Serializable 接口

🔥 先说结论:

  1. 除非一个类只在受保护的环境下使用(版本之间无交互、服务器不会暴露给不可信任的人),否则必须认真考虑是否要实现 Serializable 接口

  2. 如果一个类允许继承,更要加倍小心

  3. 除了静态成员类之外,内部类不要实现这个接口


实现 Serializable 接口的一个主要代价是,一旦类的实现被发布,它就会降低修改灵活性

即使是设计良好的序列化形式,也会限制类的演化;而设计不良的序列化形式,则可能会造成严重后果。

可序列化会使类的演变受到限制,因为每个可序列化的类都有一个与之关联的唯一标识符UID(serial version UID)。

UID是自动产生的,这个值受到类的名称、实现的接口及其大多数成员的影响。例如,通过添加一个临时的方法,生成的序列版本UID就会更改。


实现 Serializable 接口的第二个代价是,增加了出现bug和安全漏洞的可能性

序列化是一种语言之外的对象创建机制。依赖默认的反序列化机制,会让对象容易收到不变性破坏和非法访问。


实现 Serializable 接口的第三个代价是,如果要发布类的新版本,相关的测试负担就会增加

当一个可序列化的类被修改时,重要的是检查是否可以在新版本中序列化一个实例,并在旧版本中反序列化它。

如果在第一次编写类时精心设计了自定义序列化形式,那么测试的工作量就会减少,后面会讨论到


如果一个类要参与一个框架,该框架依赖于Java序列化来进行对象传输或持久化,这对于类来说实现Serializable 接口就是非常重要的。

根据经验,像BigIntegerInstant 这样的值类实现了 Serializable 接口,集合类也实现了 Serializable 接口。表示活动实体(如线程池)的类很少情况适合实现 Serializable 接口。


为继承而设计的类应该尽量不实现 Serializable 接口,接口也应该尽量不继承 Serializable

在为了继承而设计的类中,Throwable 类和 Component 类都实现了 Serializable 接口。正是因为
Throwable 实现了 Serializable 接口,RMI可以将异常从服务器发送到客户端,这其实是不好的。


内部类不应该实现Serializable,静态成员类可以实现这个接口。


87 考虑使用自定义的序列化形式

即如何实现一个自定义的序列化形式,阿里内部最经典的RPC框架HSF其中有一大块就是序列化和反序列化的设计,所以这项技术有很高的实战价值,这里也给出了一些建议。

🔥 先说结论:

  1. 只有当默认的序列化形式能合理描述对象的逻辑状态时,才使用默认的序列化形式

  2. 其他情况,应该设计一个自定义的序列化形式,通过它来合理地描述对象的状态


如果对象的物理表示与其逻辑内容相同,则默认的序列化形式是合适的

例如,默认序列化形式对于Name类来说是合理的,它只表示一个人的名字:

public class Name implements Serializable {/*** Last name. Must be non-null.** @serial*/private final String lastName;/*** First name. Must be non-null.** @serial*/private final String firstName;/*** Middle name, or null if there is none.** @serial*/private final String middleName;public Name(String lastName, String firstName, String middleName) {this.lastName = lastName;this.firstName = firstName;this.middleName = middleName;}// Remainder omitted
}

@serial 告诉Javadoc将此文档放在一个特殊的⻚面上,该⻚面记录序列化的形式。

从逻辑上讲,名字由三个字符串组成:姓、名和中间名。Name的实例字段精确地反映了这个逻辑内容。

此外,还必须提供readObject方法来确保约束关系和安全性。对于 Name 类而言,readObject 方法必须确保字段lastNamefirstName是非null的。


下面的类表示了一个字符串列表:

public final class StringList implements Serializable {private int size = 0;private Entry head = null;private static class Entry implements Serializable {String data;Entry next;Entry previous;}... // Remainder omitted
}

从逻辑上讲,这个类表示字符串序列。在物理上,它将序列表示为双向链表。当对象的物理表示与其逻辑数据内容有很大差异时,使用默认的序列化形式有四个缺点

  1. 它将导出的API永久地束缚在该类的内部实现上

在上面的例子中,私有StringList.Entry类成为公共API的一部分。如果在将来的版本中更改了实现,StringList 类仍然需要接受链表形式的输出,并产生链表形式的输出。这个类永远也摆脱不掉处理链表项所需要的所有代码,即使不再使用链表作为内部数据结构。

  1. 会占用过多的空间

这些链表项以及链接只不过是实现细节,不值得记录在序列化形式中。

  1. 会消耗过多的时间

序列化逻辑不知道对象图的拓扑结构,因此必须经过一个高开销的遍历过程。在上面的例子中,只要沿着next遍历就足够了。

  1. 可能导致堆栈溢出

StringList的合理序列化形式只需要包含列表中的字符串数量和字符串本身即可。这构成了由StringList表示的逻辑数据,去掉了其物理表示的细节。下面是修改后的StringList版本:

public final class StringList implements Serializable {private transient int size = 0;private transient Entry head = null;// No longer Serializable!private static class Entry {String data;Entry next;Entry previous;}// Appends the specified string to the listpublic final void add(String s) {}/*** Serialize this {@code StringList} instance.** @serialData The size of the list (the number of strings* it contains) is emitted ({@code int}), followed by all of* its elements (each a {@code String}), in the proper* sequence.*/private void writeObject(ObjectOutputStream s) throws IOException {s.defaultWriteObject();s.writeInt(size);// Write out all elements in the proper order.for (Entry e = head; e != null; e = e.next)s.writeObject(e.data);}private void readObject(ObjectInputStream s) throws IOException,ClassNotFoundException {s.defaultReadObject();int numElements = s.readInt();// Read in all elements and insert them in listfor (int i = 0; i < numElements; i++)add((String) s.readObject());}// Remainder omitted
}

transient 修饰符表示要从类的默认序列化表单中省略该实例字段

writeObject 做的第一件事是调用defaultWriteObjectreadObject 做的第一件事是调用
defaultReadObject,即使StringList 的所有字段都是transient 的。

序列化规范要求你无论如何都要调用它们。这些调用的存在使得在以后的版本中添加非transient实例字段成为可能,同时保留了向后和向前兼容性。

方法的@serialData标记告诉Javadoc实用工具将此文档放在序列化形式的文档页面上。


如果使用默认的序列化形式,并且标记了一个或多个字段为 transient,请记住,当反序列化实例
时,这些字段将初始化为默认值。如果字段不能被设置为初始值,就必须提供一个readObject方法,该方法调用defaultReadObject方法,然后将transient字段恢复为可接受的值。

另一种方法是延迟加载。


如果在反序列化对象的方法上加了同步,则也必须在对象序列化的方法上加上同步。如果你有一个线程安全的对象,它通过同步每个方法来实现线程安全,并且你选择使用默认的序列化形式,那么使用以下writeObject方法:

private synchronized void writeObject(ObjectOutputStream s) throws IOException {s.defaultWriteObject();
}

无论选择哪种序列化形式,都要在编写的每个可序列化类中声明UID。只需添加一行:

private static final long serialVersionUID = randomLongValue;

一旦写好了randomLongValue之后就不要更改序列版本UID。


88 保护性地编写 readObject 方法

🔥 先说结论:

下面的几点有助于编写更健壮的 readObject 方法:

  1. 对于类中的private对象引用字段,要保护性的拷⻉这些字段中的每个对象。不可变类中的可变组件就属于这一类别

  2. 对于任何约束条件,如果检查失败就抛出一个InvalidObjectException异常。这些检查动作应该
    跟在所有的保护性拷⻉之后

  3. 如果整个对象图在被反序列化之后必须进行验证,就应该使用ObjectInputValidation接口

  4. 无论是直接方法还是间接方法,都不要调用类中任何可被覆盖的方法


之前编写过一个日期类:

public class Period {private final Date start;private final Date end;public Period(Date start, Date end) {this.start = new Date(start.getTime());this.end = new Date(end.getTime());if (this.start.compareTo(this.end) > 0)throw new IllegalArgumentException(this.start + " after " + this.end);}public Date start() {return new Date(start.getTime());}public Date end() {return new Date(end.getTime());}public String toString() {return start + " - " + end;}
}

假设要把这个类成为可序列化的。因为Period对象的物理表示法正好反映了它的逻辑数据内容,所以,使用默认的序列化形式是合理的。

实际上,如果仅在类的声明中增加implements Serializable,这个类就不再保证它的关键约束了。

问题在于readObject方法实际上相当于另外一个公有的构造器,它要求同其他构造器一样:必须检查其参数的有效性,并且在必要的时候对参数进行保护性拷⻉。


假设我们仅仅在Period类的声明加上了implements Serializable,这个代码可能会产生一个Period实例,他的结束时间比起始时间还早。

public class BogusPeriod {private static final byte[] serializedForm = {(byte) 0xac, (byte) 0xed, 0x00, 0x05, 0x73, 0x72, 0x00, 0x06,0x50, 0x65, 0x72, 0x69, 0x6f, 0x64, 0x40, 0x7e, (byte) 0xf8,0x2b, 0x4f, 0x46, (byte) 0xc0, (byte) 0xf4, 0x02, 0x00, 0x02,0x4c, 0x00, 0x03, 0x65, 0x6e, 0x64, 0x74, 0x00, 0x10, 0x4c,0x6a, 0x61, 0x76, 0x61, 0x2f, 0x75, 0x74, 0x69, 0x6c, 0x2f,0x44, 0x61, 0x74, 0x65, 0x3b, 0x4c, 0x00, 0x05, 0x73, 0x74,0x61, 0x72, 0x74, 0x71, 0x00, 0x7e, 0x00, 0x01, 0x78, 0x70,0x73, 0x72, 0x00, 0x0e, 0x6a, 0x61, 0x76, 0x61, 0x2e, 0x75,0x74, 0x69, 0x6c, 0x2e, 0x44, 0x61, 0x74, 0x65, 0x68, 0x6a,(byte) 0x81, 0x01, 0x4b, 0x59, 0x74, 0x19, 0x03, 0x00, 0x00,0x78, 0x70, 0x77, 0x08, 0x00, 0x00, 0x00, 0x66, (byte) 0xdf,0x6e, 0x1e, 0x00, 0x78, 0x73, 0x71, 0x00, 0x7e, 0x00, 0x03,0x77, 0x08, 0x00, 0x00, 0x00, (byte) 0xd5, 0x17, 0x69, 0x22,0x00, 0x78};public static void main(String[] args) {Period p = (Period) deserialize(serializedForm);System.out.println(p);}// Returns the object with the specified serialized formstatic Object deserialize(byte[] sf) {try {return new ObjectInputStream(new ByteArrayInputStream(sf)).readObject();} catch (IOException | ClassNotFoundException e) {throw new IllegalArgumentException(e);}}
}

如果运行这个程序,它会打印出「Fri Jan 01 12:00:00 PST 1999 - Sun Jan 01 12:00:00 PST 1984」

为了修整这个问题,可以为Period提供一个readObject方法,该方法首先调用defaultReadObject,然后检查被反序列化之后的对象有效性。

private void readObject(ObjectInputStream s) throws IOException, ClassNotFoundException{s.defaultReadObject();// Check that our invariants are satisfiedif (start.compareTo(end) > 0)throw new InvalidObjectException(start +" after "+ end);
}

这里依旧隐藏着一个更为微妙的问题。通过伪造字节流,要想创建可变的Period实例仍是有可能的,做法是:

  • 字节流以一个有效的Period实例开头,然后加上两个额外的引用,指向Period实例中的startend。攻击者从ObjectInputStream中读取Period实例,然后读取附加在其后面的引用。这些对象引用使得攻击者能够访问到Period对象内部的startend,攻击者可以改变Period实例。
public class MutablePeriod {public final Period period;// period's start field, to which we shouldn't have accesspublic final Date start;// period's end field, to which we shouldn't have accesspublic final Date end;public MutablePeriod() {try {ByteArrayOutputStream bos = new ByteArrayOutputStream();ObjectOutputStream out = new ObjectOutputStream(bos);// Serialize a valid Period instanceout.writeObject(new Period(new Date(), new Date()));/** Append rogue "previous object refs" for internal* Date fields in Period. For details, see "Java* Object Serialization Specification," Section 6.4.*/byte[] ref = {0x71, 0, 0x7e, 0, 5};// Ref #5bos.write(ref);// The start fieldref[4] = 4;// Ref # 4bos.write(ref);// The end field// Deserialize Period and "stolen" Date referencesObjectInputStream in = new ObjectInputStream(new ByteArrayInputStream(bos.toByteArray()));period = (Period) in.readObject();start = (Date) in.readObject();end = (Date) in.readObject();} catch (IOException | ClassNotFoundException e) {throw new AssertionError(e);}}
}

运行下面的程序可以查看正在进行的攻击:

public static void main(String[] args) {MutablePeriod mp = new MutablePeriod();Period p = mp.period;Date pEnd = mp.end;// Let's turn back the clockpEnd.setYear(78);System.out.println(p);// Bring back the 60s!pEnd.setYear(69);System.out.println(p);
}

这个程序产生的输出结果如下:

Wed Nov 22 00:21:29 PST 2017 - Wed Nov 22 00:21:29 PST 1978
Wed Nov 22 00:21:29 PST 2017 - Sat Nov 22 00:21:29 PST 1969

这个结果明显是错误的,问题的根源在于,Period 的readObject 方法并没有完成足够的保护性拷⻉。

当一个对象被反序列化的时候,对于客户端不应该拥有的对象引用,就必须做保护性拷贝。下面的这些 readObject 方法可以确保 Period 类的约束条件不会遭到破坏:

// readObject method with defensive copying and validity checking
private void readObject(ObjectInputStream s) throws IOException, ClassNotFoundException{s.defaultReadObject();// Defensively copy our mutable componentsstart = new Date(start.getTime());end = new Date(end.getTime());// Check that our invariants are satisfiedif (start.compareTo(end) > 0)throw new InvalidObjectException(start +" after "+ end);
}

为了使用 readObject 方法,我们必须要将startend字段声明成为非final的。

Wed Nov 22 00:23:41 PST 2017 - Wed Nov 22 00:23:41 PST 2017
Wed Nov 22 00:23:41 PST 2017 - Wed Nov 22 00:23:41 PST 2017

89 对于单例控制,枚举类型优先于readResolve

🔥 先说结论:

  1. 应该使用枚举类型来实施单例控制

  2. 如果做不到,就必须提供一个 readResolve 方法,并确保该类的所有实例化字段都是基本类型,或者是 transient


回忆前面的条目3,其中给出了一个单例的示例:

public class Elvis {public static final Elvis INSTANCE = new Elvis();private Elvis() { ... }public void leaveTheBuilding() { ... }
}

如果在这个类上面增加 implements Serializable,它就不再是一个单例了,原因如下:

  • 任何一个 readObject 方法,都会返回一个新建的实例,这个新建的实例不同于类初始化时创建的实例

readResolve 特性允许你用 readObject 创建的实例代替另外一个实例。对于一个正在被反序列化的对象,如果它的类定义了一个 readResolve 方法,那么在反序列化之后,新建对象上的 readResolve 方法就会被调用,返回的对象将取代新建的对象。

如果 Elvis 类要实现 Serializable 接口,下面的 readResolve 方法就足以保证它的单例属性:

private Object readResolve() {// Return the one true Elvis and let the garbage collector// take care of the Elvis impersonator.return INSTANCE;
}

该方法忽略了被反序列化的对象,只返回类初始化创建的那个特殊的 Elvis 实例。

如果依赖 readResolve 进行实例控制,带有对象引用类型的字段都必须声明为transient。否则,攻击者就有可能采用第88条中 MutablePeriod 类似的代码进行攻击。


以下面这个单例为例:

public class Elvis implements Serializable {public static final Elvis INSTANCE = new Elvis();private Elvis() {}private String[] favoriteSongs = {"Hound Dog", "Heartbreak Hotel"};public void printFavorites() {System.out.println(Arrays.toString(favoriteSongs));}private Object readResolve() {return INSTANCE;}
}

写一个「盗用者」类,它既有 readResolve 方法,又有实例字段,实例字段指向被序列化的单例的引用:

public class ElvisStealer implements Serializable {static Elvis impersonator;private static final long serialVersionUID = 0;private Elvis payload;private Object readResolve() {// Save a reference to the "unresolved" Elvis instanceimpersonator = payload;// Return object of correct type for favoriteSongs fieldreturn new String[]{"A Fool Such as I"};}
}

在序列化流中,用「盗用者」实例代替单例的引用,现在就有了一个循环:Elvis包含ElvisStealerElvisStealer包含Elvis。此外,「盗用者」readResolve 方法执行impersonator = payload;,以便该引用可以在 readResolve 方法运行之后被访问到。


下面的代码反序列化一个手工制作的流,为Elvis产生两个截然不同的实例。

package com.wjw.effectivejava1;import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;public class ElvisImpersonator {private static final byte[] serializedForm = {(byte) 0xac, (byte) 0xed, 0x00, 0x05, 0x73, 0x72, 0x00, 0x05, 0x45,0x6c, 0x76, 0x69, 0x73, (byte) 0x84, (byte) 0xe6, (byte) 0x93, 0x33,(byte) 0xc3, (byte) 0xf4, (byte) 0x8b, 0x32, 0x02, 0x00, 0x01, 0x4c,0x00, 0x0d, 0x66, 0x61, 0x76, 0x6f, 0x72, 0x69, 0x74, 0x65, 0x53,0x6f, 0x6e, 0x67, 0x73, 0x74, 0x00, 0x12, 0x4c, 0x6a, 0x61, 0x76,0x61, 0x2f, 0x6c, 0x61, 0x6e, 0x67, 0x2f, 0x4f, 0x62, 0x6a, 0x65,0x63, 0x74, 0x3b, 0x78, 0x70, 0x73, 0x72, 0x00, 0x0c, 0x45, 0x6c,0x76, 0x69, 0x73, 0x53, 0x74, 0x65, 0x61, 0x6c, 0x65, 0x72, 0x00,0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x01, 0x4c,0x00, 0x07, 0x70, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x74, 0x00,0x07, 0x4c, 0x45, 0x6c, 0x76, 0x69, 0x73, 0x3b, 0x78, 0x70, 0x71,0x00, 0x7e, 0x00, 0x02};public static void main(String[] args) {// Initializes ElvisStealer.impersonator and returns// the real Elvis (which is Elvis.INSTANCE)Elvis elvis = (Elvis) deserialize(serializedForm);Elvis impersonator = ElvisStealer.impersonator;elvis.printFavorites();impersonator.printFavorites();}static Object deserialize(byte[] sf) {try {return new ObjectInputStream(new ByteArrayInputStream(sf)).readObject();} catch (IOException | ClassNotFoundException e) {throw new IllegalArgumentException(e);}}
}

这个程序会产生如下的输出,最终证明可以创建两个截然不同的Elvis实例:

[Hound Dog, Heartbreak Hotel]
[A Fool Such as I]

通过将favoriteSongs字段声明为transient,可以修复这个问题,但是最好把Elvis做成一个单元素的枚举类型。如果将一个可序列化的实例受控的类编写为枚举,Java就可以绝对保证除了所声明的常量之外,不会有其他实例。

package com.wjw.effectivejava2;import java.util.Arrays;public enum Elvis {INSTANCE;private String[] favoriteSongs = {"Hound Dog", "Heartbreak Hotel"};public void printFavorites() {System.out.println(Arrays.toString(favoriteSongs));}
}

readResolve的可访问性十分重要:

  • 如果把readResolve 方法放在一个final 类上,它应该是私有的
  • 如果把readResolve 方法放在一个非final 类上,就必须认真考虑它的的访问性
  • 如果它是private的,就不适用于任何一个子类
  • 如果它是default的,就适用于同一个包内的子类
  • 如果它是protected的或者是public的,并且子类没有覆盖它,对序列化的子类进行反序列化,就会产生一个超类实例,这样可能会导致 ClassCastException 异常

90 考虑用序列化代理代替序列化实例

🔥 先说结论:

  • 如果必须在一个不能被继承的类上编写readObjectwriteObject方法时,应该考虑使用序列化代理模式

本篇文章一直在讨论的一件事:决定实现Serializable接口,会增加出错和出现安全问题的可能性,因为它允许利用语言之外的机制来创建实例。序列化代理模式的方法可以减少这些⻛险。


首先,为可序列化的类设计一个私有的静态嵌套类,精确地表示外围类的逻辑状态。这个嵌套类被称为序列化代理(seralization proxy),它应该有一个单独的构造器,其参数类型就是外围类。

外围类及其序列代理都必须声明实现Serializable接口。


以第50条中编写不可变的Period 类为例:

private static class SerializationProxy implements Serializable {private final Date start;private final Date end;SerializationProxy(Period p) {this.start = p.start;this.end = p.end;}private static final long serialVersionUID = 10234098243823485285L; // Any number will do (Item 87)
}

接下来,将下面的 writeReplace 方法添加到外围类中。

private Object writeReplace() {return new SerializationProxy(this);
}

writeReplace 方法在序列化之前,将外围类的实例转变成了它的序列化代理。

有了 writeReplace 方法之后,序列化系统永远不会产生外围类的序列化实例,但是攻击者有可能伪造企图违反该类约束条件的示例。为了防止此类攻击,只需要在外围类中添加如下 readObject 方法:

private void readObject(ObjectInputStream stream) throws InvalidObjectException {throw new InvalidObjectException("Proxy required");
}

最后在 SerializationProxy 类中提供一个 readResolve 方法,他返回一个逻辑上等价的外围类的实例。这个方法的出现,导致序列化系统在反序列化的时候将序列化代理转为外围类的实例。

如果该类的静态工厂或者构造器建立了这些约束条件,并且它的实例方法保持着这些约束条件,你就可以确信序列化也确保着这些约束条件。

private Object readResolve() {return new Period(start, end); // Uses public constructor
}

与前两种方法不同,这种方法允许 Period 类的字段为 final


序列化代理模式允许反序列化实例有着与原始序列化实例不同的类。以 EnumSet 的情况为例,它们返回的是两种子类之一,具体取决于底层枚举类型的大小。如果底层的枚举类型有64个或者少于64个的元素,静态工厂就返回一个RegularEnumSet,否则返回一个JunmboEnumSet

EnumSet 就是使用序列化代理模式:

private static class SerializationProxy<E extends Enum<E>> implements Serializable {private static final long serialVersionUID = 362491234563181265L;// The element type of this enum set.private final Class<E> elementType;// The elements contained in this enum set.private final Enum<?>[] elements;SerializationProxy(EnumSet<E> set) {elementType = set.elementType;elements = set.toArray(new Enum<?>[0]);}private Object readResolve() {EnumSet<E> result = EnumSet.noneOf(elementType);for (Enum<?> e : elements)result.add((E) e);return result;}
}

序列化代理模式有两个局限性:

  1. 不能被继承
  2. 如果企图从一个对象的序列化代理的readResovle方法内部调用这个对象的方法,就会得到一个ClassCastException异常,因为你还没有这个对象,只有它的序列化代理

http://chatgpt.dhexx.cn/article/CfpuUxmn.shtml

相关文章

青岛黄海学院《燃情世界杯》

《燃情世界杯》发行海报 “真的被抢了。虽然来之前就做过很多思想准备&#xff0c;但没想到那么快。刚才在亚历山大贫民区&#xff0c;一伙人突然拔出两把枪,青岛黄海学院&#xff0c;顶着我们录音师的肚子和大腿&#xff0c;一瞬间就抢了他手机&#xff0c;然后飞速逃跑。”北…

“香港女车神”李慧诗场地自行车世界杯香港站连夺两金纪录

1月27日&#xff0c;场地单车世界杯香港站赛事举行。中国香港队主将李慧诗(中)在女子凯琳赛中夺金。图为颁奖礼现场。中新社记者 麦尚旻 摄 中新社香港1月27日电 (香企容)场地自行车世界杯香港站27日在香港举行。“港队王牌女将”李慧诗在凯琳赛上再夺一金&#xff0c;累计26日…

纯CSS写个绿荫足球场,为世界杯喝彩

​ 那要写出这样一个界面,首先了解一下球场结构,如图,具体比例就不坐详细说明了自行百度. ​ 颜色上白线绿地没啥问题,发球弧和角球线通过父级的覆盖可以做到部分弧线效果. 先看一下整体效果: 具体地: 1 )场地外围我们使用混合色: background: radial-gradient(sandybrown,…

历届世界杯冠军

1930年7月13日&#xff0c;第一届世界足球锦标赛&#xff08;后称世界杯赛&#xff09;&#xff0c;在乌拉圭首都蒙得维的亚的“百周年纪念”体育场&#xff08;1930年正值乌拉圭独立一百周年&#xff0c;由此得名&#xff09;隆重开幕。玻利维亚、巴西、智利、墨西哥、巴拉圭、…

世界杯看不明白?没事咱们还会画足球吗。

2022卡塔尔世界杯如期而至&#xff0c;看到朋友圈&#xff0c;群聊里大家对“战况”分析的头头是道&#xff0c;我只能在一旁干瞪眼&#xff0c;插不上嘴。我的世界杯记忆源自于2014巴西世界杯&#xff0c;球员记忆也只有梅西&#xff0c;可以说是个“球盲”了。眼看自己没啥参…

CSS 绘制世界杯足球场

写在之前的话 花有重开日&#xff0c;人无再少年。 疯狂无大小&#xff0c;热爱即疯狂&#xff01; 足球⚽️&#xff0c;是我大学时期的一门选修课&#xff0c;足球需要场地&#xff0c;场地绘制现在就开始&#xff01;&#xff01;&#xff01; 四年一次的世界杯再卡塔尔进行…

c语言结构体世界杯,世界杯冷知识 | 12座球场的结构巡礼

原标题&#xff1a;世界杯冷知识 | 12座球场的结构巡礼 2018世界杯开赛一周&#xff0c;32只球队都已亮相完毕(有些球队在买回家机票了&#xff1f;)&#xff0c;同时12座赛场也都和大家见过面了&#xff0c;也许专(he)心(jiu)看(lu)球(chuan er)的你并没有注意到它们&#xff…

linux Vi搜索和替换字符串

一. 搜索字符串 1. 打开文件 vi sources.list 2. 进入命令模式 用ESC键进入命令模式。 3. 输入搜索的字符串"/http",点击enter键开始搜索&#xff0c;光标指向第一个被搜到的字符串 /http 4. 点击键盘n搜索下一个 二. 搜索并退出字符串 1. 打开文件 sudo vi …

linux vi修改相同的字符,UNIX/Linux下的vi/vim编辑器快速替换字符串

在UNIX/Linux中的vi/vim编辑中可以使用 :s 命令来替换字符串。以前只会使用一种格式来全文替换&#xff0c;今天发现该命令有很多种写法&#xff0c;而且作用十分强大&#xff0c;还有很多需要学习&#xff0c;记录几种常用的在此&#xff0c;方便以后查询。 :s/procat/law/ 替…

linux用vi查找字符串替换,Linux中vi进行字符替换

Linux环境下vi/vim 可以使用 :s 命令来替换字符串。以前只会使用编辑软件进行替换&#xff0c;今天发现该命令有很多种写法(vi 真是强大啊&#xff0c;还有很多需要学习)&#xff0c;记录几种在此&#xff0c;方便以后查询。 :s/well/good/ 替换当前行第一个 well 为 good :s/w…

查看GitHub项目的星星排行榜(GitHub排行榜)

如图&#xff1a;打开GitHub&#xff0c;在输入框输入 stars:>10000 这里的10000是搜索的星星数的项目

如何查看github star排行榜?

输入location:China 可以搜索指定地区用户USER 输入 指定 star数 比如说 stars:>50000,查找star数大于5万的项目 找到优秀的开源项目, 学习它的源码, 是提高编程能力最快的方法, 即使你不是程序员, 也能从github找到一些好的开源工具, 提升你的工作效率, 当你对github有一…

githubRank: Github 项目和用户排行榜

自己平时比较喜欢逛 Github&#xff0c;也热衷于发现各种各样神奇的仓库&#xff0c;所以干脆用官方的接口自己整合一下&#xff0c;便诞生了—— githubRank&#xff0c;这样一个展示仓库与用户排行的网站&#xff0c;虽然已经存在了很多基于官方接口的 github 项目排行的网站…

GitHub 中文排行榜,高分优秀中文项目一网打尽!

点击上方“AI有道”&#xff0c;选择“星标”公众号 重磅干货&#xff0c;第一时间送达 GitHub 是每个程序员经常逛的地方。在 GitHub 里&#xff0c;我们经常能发现一些优秀的开源且高分项目&#xff01;有时候为了找到最优秀的开源项目&#xff0c;却往往要在 GitHub 上寻找很…

超级盘点 | Github年终各大排行榜

2018 年还有半月就要结束了&#xff0c;这一年&#xff0c;大家都学了些什么&#xff1f;在 Github 上度过了多少时间&#xff1f;收藏了多少开源项目&#xff1f;加入了哪些开源社区&#xff1f;是否为哪个项目或社区贡献了自己的一份力量呢&#xff1f;今天&#xff0c;为大家…

很强!GitHub 中文项目排行榜新鲜出炉!

关注 “GitHubDaily” 设为 “星标”&#xff0c;每天带你逛 GitHub&#xff01; 本文转自机器之心 没事逛一逛中文项目排行榜&#xff0c;什么下载插件、投资理财、求职面试、买房指南&#xff0c;你会打开 GitHub 的新世界大门。 当然&#xff0c;还是有编程指南、机器学习之…

如何查看github热门趋势和star排行榜

文章目录 1. 查看github热门趋势2. 查看github star排行榜3. 通过关键词查找需要的项目4. 几个查看github热点的工具4.1 掘金4.2 百度开发者搜索 5 如何查看开源项目star增长曲线 1. 查看github热门趋势 查看全类&#xff1a;https://github.com/trending 只看java类&#xff…

GITHUB排行榜C位出道-手把手教你玩转V语言版的俄罗斯方块

最近 V 语言-一个GO语言最吸晴的项目&#xff0c;在千呼万唤之后&#xff0c;终于迎来开源&#xff0c;并正式发布了首个可用版本&#xff0c;其一经推出&#xff0c;便强势登顶 GitHub的榜首&#xff0c;引来各方热议。目前V已经可以实现自我编译迭代&#xff0c;笔者大致了解…

毫无争议的 GitHub 顶级有用的开源项目排行榜

TOP 14. 假装自己中病毒软件 ❝ 链接&#x1f517;&#xff1a;https://github.com/bitdust/WamaCry 这个用途可太广泛了&#xff1a; 明天就是 deadline 了可是论文还没写完怎么办这稿子还想再拖一周可是借口都用完了怎么办不想加班&#xff0c;又找不到理由开溜怎么办别人都中…

8月,Github 最热开源项目排行榜来啦

【第八期】 哈喽&#xff0c;大家好&#xff0c;我是开源君&#xff0c;一个资深的互联网玩家&#xff0c;致力于为大家分享各领域优质开源项目。 今天梳理一下本周的 Github 热榜项目。及时挖掘和发现有用的项目&#xff0c;发现趋势热点&#xff0c;让我们比其他人走的更快一…