JavaRush /Java 博客 /Random-ZH /序列化就是这样。第1部分
articles
第 15 级

序列化就是这样。第1部分

已在 Random-ZH 群组中发布
乍一看,序列化似乎是一个微不足道的过程。真的,还有什么可以更简单呢?声明类来实现接口java.io.Serializable- 就是这样。您可以毫无问题地序列化该类。 序列化就是这样。 第 1 - 1 部分从理论上讲,这是正确的。在实践中,有很多微妙之处。它们与性能、反序列化和类安全相关。还有更多方面。我们将讨论这些微妙之处。本文可以分为以下几个部分:
  • 机制的微妙之处
  • 为什么需要它?Externalizable
  • 表现
  • 但另一方面
  • 数据安全
  • 对象序列化Singleton
让我们继续第一部分——

机制的微妙之处

首先,一个简单的问题。有多少种方法可以使对象可序列化?实践表明,90%以上的开发者以大致相同的方式回答这个问题(就措辞而言)——只有一种方法。与此同时,他们中有两个人。并不是每个人都记得第二个,更不用说谈论它的功能了。那么这些方法有哪些呢?每个人都记得第一个。这是已经提到的实现java.io.Serializable,不需要任何努力。第二种方法也是接口的实现,但是不同:java.io.Externalizable。不同的是java.io.Serializable,它包含两个需要实现的方法 -writeExternal(ObjectOutput)readExternal(ObjectInput)。这些方法包含序列化/反序列化逻辑。 评论。Serializable在下文中,我有时会提到序列化,其中实现是标准的,实现Externalizable是扩展的。其他评论。我现在故意不触及诸如定义readObject和之类的标准序列化控制选项writeObject,因为 我认为这些方法有些不正确。这些方法没有在接口中定义Serializable,实际上是解决限制并使标准序列化变得灵活的道具。Externalizable提供灵活性的方法从一开始就内置在它们中。我们再问一个问题。标准序列化实际上是如何工作的java.io.Serializable?它通过 Reflection API 工作。那些。该类被解析为一组字段,每个字段都写入输出流。我认为很明显,这种操作在性能方面并不是最佳的。稍后我们会知道具体是多少。提到的两种序列化方法之间还有另一个主要区别。即在反序列化机制中。使用时,Serializable反序列化是这样发生的:为对象分配内存,然后用流中的值填充其字段。不调用该对象的构造函数。这里我们需要单独考虑这种情况。好的,我们的类是可序列化的。他的父母呢?完全可选!此外,如果您从Object父级继承一个类,那么父级绝对是不可序列化的。尽管Object我们对字段一无所知,但它们很可能存在于我们自己的父类中。他们会发生什么?它们不会进入序列化流。反序列化后它们会取什么值?让我们看一下这个例子:
package ru.skipy.tests.io;

import java.io.*;

/**
 * ParentDeserializationTest
 *
 * @author Eugene Matyushkin aka Skipy
 * @since 05.08.2010
 */
public class ParentDeserializationTest {

    public static void main(String[] args){
        try {
            System.out.println("Creating...");
            Child c = new Child(1);
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            ObjectOutputStream oos = new ObjectOutputStream(baos);
            c.field = 10;
            System.out.println("Serializing...");
            oos.writeObject(c);
            oos.flush();
            baos.flush();
            oos.close();
            baos.close();
            ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
            ObjectInputStream ois = new ObjectInputStream(bais);
            System.out.println("Deserializing...");
            Child c1 = (Child)ois.readObject();
            System.out.println("c1.i="+c1.getI());
            System.out.println("c1.field="+c1.getField());
        } catch (IOException ex){
            ex.printStackTrace();
        } catch (ClassNotFoundException ex){
            ex.printStackTrace();
        }
    }

    public static class Parent {
        protected int field;
        protected Parent(){
            field = 5;
            System.out.println("Parent::Constructor");
        }
        public int getField() {
            return field;
        }
    }

    public static class Child extends Parent implements Serializable{
        protected int i;
        public Child(int i){
            this.i = i;
            System.out.println("Child::Constructor");
        }
        public int getI() {
            return i;
        }
    }
}
它是透明的——我们有一个不可序列化的父类和一个可序列化的子类。这就是发生的事情:
Creating...
Parent::Constructor
Child::Constructor
Serializing...
Deserializing...
Parent::Constructor
c1.i=1
c1.field=5
反序列化时,调用父非序列化类的无参数构造函数。而如果没有这样的构造函数,反序列化时就会出错。正如上面所说,我们正在反序列化的子对象的构造函数不会被调用。这就是标准机制在使用时的行为方式Serializable。使用时,Externalizable情况有所不同。首先,调用不带参数的构造函数,然后在创建的对象上调用 readExternal 方法,该方法实际上读取其所有数据。因此,任何实现Externalized接口的类都必须有一个不带参数的公共构造函数!而且,由于此类的所有后代也将被视为实现该接口Externalizable,因此它们还必须有一个无参数构造函数!让我们更进一步。有这样一个字段修饰符,如transient。这意味着该字段不应被序列化。然而,正如您自己所理解的,该指令仅影响标准序列化机制。使用时,Externalizable没有人费心序列化该字段以及减去它。如果一个字段被声明为瞬态的,那么当该对象被反序列化时,它将采用默认值。另一个相当微妙的点。使用标准序列化时,具有修饰符的字段static不会被序列化。因此,反序列化后该字段的值不会改变。当然,在实现过程中,Externalizable没有人费心去序列化和反序列化这个字段,但我强烈建议不要这样做,因为 这可能会导致细微的错误。带有修饰符的字段final像常规字段一样被序列化。但有一个例外 - 使用外部化时无法反序列化它们。因为final-поля它们必须在构造函数中初始化,之后就无法在 readExternal 中更改该字段的值。因此,如果您需要序列化具有 -field 的对象final,则只需使用标准序列化。还有一点很多人不知道。标准序列化考虑了类中声明字段的顺序。无论如何,早期版本就是这种情况;在 Oracle 实现的 JVM 版本 1.6 中,顺序不再重要,字段的类型和名称很重要。尽管这些字段通常可能保持不变,但方法的组成很可能会影响标准机制。为了避免这种情况,有以下机制。对于实现该接口的每个类Serializable,在编译阶段都会添加一个字段 -private static final long serialVersionUID。该字段包含序列化类的唯一版本标识符。它是根据类的内容计算的 - 字段、它们的声明顺序、方法、它们的声明顺序。因此,随着类的任何变化,该字段的值也会改变。当类被序列化时,该字段被写入流。顺便说一句,这可能是我所知道的唯一static一个 -field 被序列化的情况。反序列化时,将该字段的值与虚拟机中类的值进行比较。如果值不匹配,则会抛出如下异常:
java.io.InvalidClassException: test.ser2.ChildExt;
    local class incompatible: stream classdesc serialVersionUID = 8218484765288926197,
                                   local class serialVersionUID = 1465687698753363969
然而,有一种方法(如果不能绕过的话)可以欺骗此检查。如果已经定义了类字段集及其顺序,但类方法可能会更改,则这可能很有用。在这种情况下,序列化没有风险,但标准机制不允许使用修改后的类的字节码来反序列化数据。但是,正如我所说,他可能会被欺骗。即手动定义类中的字段private static final long serialVersionUID。原则上,该字段的值绝对可以是任何值。有些人喜欢将其设置为等于代码修改的日期。有的甚至用1L。要获取标准值(内部计算的值),您可以使用 SDK 中包含的 Serialver 实用程序。一旦以这种方式定义,该字段的值将是固定的,因此始终允许反序列化。此外,在 5.0 版本中,文档中大约出现了以下内容:强烈建议所有可序列化类显式声明此字段,因为默认计算对类结构的细节非常敏感,这些细节可能会根据编译器实现而有所不同,从而导致意想不到的InvalidClassException后果。反序列化。最好将此字段声明为private,因为 它仅指声明它的类。尽管规范中没有指定修饰符。现在让我们考虑这个方面。假设我们有这样的类结构:
public class A{
    public int iPublic;
    protected int iProtected;
    int iPackage;
    private int iPrivate;
}

public class B extends A implements Serializable{}
换句话说,我们有一个从不可序列化父级继承的类。是否可以序列化此类,并且需要什么?父类的变量会发生什么变化呢?答案是这样的。是的,B您可以序列化类的实例。为此需要什么?但该类需要A有一个不带参数的构造函数,public或者protected. 然后,在反序列化期间,所有类变量A将使用此构造函数进行初始化。类变量B将使用序列化数据流中的值进行初始化。理论上,可以在类中定义B我在开头 -readObject和, - 开头谈到的方法,通过writeObject执行类变量的(反)序列化,然后对可用变量进行(反)序列化来自类(在我们的例子中,它们是、和,如果它与 位于同一个包中)。然而,在我看来,最好使用扩展序列化。我想谈的下一点是多个对象的序列化。假设我们有以下类结构: Bin.defaultReadObject/out.defaultWriteObjectAiPubliciProtectediPackageBA
public class A implements Serializable{
    private C c;
    private B b;
    public void setC(C c) {this.c = c;}
    public void setB(B b) {this.b = b;}
    public C getC() {return c;}
    public B getB() {return b;}
}
public class B implements Serializable{
    private C c;
    public void setC(C c) {this.c = c;}
    public C getC() {return c;}
}
public class C implements Serializable{
    private A a;
    private B b;
    public void setA(A a) {this.a = a;}
    public void setB(B b) {this.b = b;}
    public B getB() {return b;}
    public A getA() {return a;}
}
序列化就是这样。 第 1 - 2 部分如果序列化该类的实例会发生什么A?它将沿着该类的一个实例拖动B,而该类又将沿着一个C引用该实例的实例拖动A,该实例与一切开始时的实例相同。恶性循环和无限递归?幸运的是,没有。我们看下面的测试代码:
// initiaizing
A a = new A();
B b = new B();
C c = new C();
// setting references
a.setB(b);
a.setC(c);
b.setC(c);
c.setA(a);
c.setB(b);
// serializing
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(a);
oos.writeObject(b);
oos.writeObject(c);
oos.flush();
oos.close();
// deserializing
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(baos.toByteArray()));
A a1 = (A)ois.readObject();
B b1 = (B)ois.readObject();
C c1 = (C)ois.readObject();
// testing
System.out.println("a==a1: "+(a==a1));
System.out.println("b==b1: "+(b==b1));
System.out.println("c==c1: "+(c==c1));
System.out.println("a1.getB()==b1: "+(a1.getB()==b1));
System.out.println("a1.getC()==c1: "+(a1.getC()==c1));
System.out.println("b1.getC()==c1: "+(b1.getC()==c1));
System.out.println("c1.getA()==a1: "+(c1.getA()==a1));
System.out.println("c1.getB()==b1: "+(c1.getB()==b1));
我们在做什么?A我们创建类、B和的实例C,为它们提供相互链接,然后序列化它们中的每一个。然后我们将它们反序列化并运行一系列检查。结果会发生什么:
a==a1: false
b==b1: false
c==c1: false
a1.getB()==b1: true
a1.getC()==c1: true
b1.getC()==c1: true
c1.getA()==a1: true
c1.getB()==b1: true
那么你能从这个测试中学到什么?第一的。反序列化后的对象引用与之前的引用不同。换句话说,在序列化/反序列化期间,对象被复制。该方法有时用于克隆对象。第二个结论更有意义。当序列化/反序列化具有交叉引用的多个对象时,这些引用在反序列化后仍然有效。换句话说,如果在序列化之前它们指向一个对象,那么在反序列化之后它们也将指向一个对象。另一个小测试来证实这一点:
B b = new B();
C c = new C();
b.setC(c);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(b);
oos.writeObject(c);
oos.writeObject(c);
oos.writeObject(c);
oos.flush();
oos.close();
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(baos.toByteArray()));
B b1 = (B)ois.readObject();
C c1 = (C)ois.readObject();
C c2 = (C)ois.readObject();
C c3 = (C)ois.readObject();
System.out.println("b1.getC()==c1: "+(b1.getC()==c1));
System.out.println("c1==c2: "+(c1==c2));
System.out.println("c1==c3: "+(c1==c3));
类对象B具有对类对象的引用C。序列化时,b它与 类 的实例一起序列化С,之后同一个 c 实例被序列化 3 次。反序列化后会发生什么?
b1.getC()==c1: true
c1==c2: true
c1==c3: true
正如您所看到的,所有四个反序列化对象实际上代表一个对象 - 对它的引用是相等的。与连载之前一模一样。Externalizable另一个有趣的点 - 如果我们同时实现和会发生什么Serializable?就像那个问题——大象与鲸鱼——谁会打败谁?会克服的Externalizable。序列化机制首先检查它是否存在,然后才检查它是否存在。Serializable因此,如果实现 的类 BSerializable继承自实现 的类 A,则Externalizable类 B 的字段将不会被序列化。最后一点是继承。当从实现 的类继承时Serializable,无需执行任何其他操作。序列化也将扩展到子类。当从实现 的类继承时Externalizable,必须重写父类的 readExternal 和 writeExternal 方法。否则,子类的字段将不会被序列化。在这种情况下,您应该记住调用父方法,否则父字段将不会被序列化。* * * 我们可能已经完成了细节。然而,有一个我们尚未触及的全球性问题。即——

为什么需要外部化?

为什么我们需要高级序列化?答案很简单。首先,它提供了更大的灵活性。其次,它通常可以在序列化数据量方面提供显着的收益。第三,还有一个方面,就是性能,我们下面会讲到。一切似乎都清晰又灵活。确实,我们可以根据需要控制序列化和反序列化过程,这使我们独立于类中的任何更改(正如我上面所说,类中的更改可以极大地影响反序列化)。因此,我想就成交量的增益说几句话。假设我们有以下课程:
public class DateAndTime{

  private short year;
  private byte month;
  private byte day;
  private byte hours;
  private byte minutes;
  private byte seconds;

}
其余的都不重要。这些字段可以由 int 类型组成,但这只会增强示例的效果。尽管实际上这些字段可能int出于性能原因而被键入。无论如何,这一点是明确的。该类代表日期和时间。主要从序列化的角度来看,这对我们来说很有趣。也许最简单的事情就是存储一个简单的时间戳。它的类型是 long,即 序列化时需要 8 个字节。此外,这种方法需要将组件转换为一个值并返回的方法,即 – 生产力损失。这种方法的优点是可以容纳 64 位的完全疯狂的日期。这是一个巨大的安全边际,但现实中通常不需要。上面给出的类将占用 2 + 5*1 = 7 个字节。加上类和 6 个字段的开销。有什么办法可以压缩这些数据吗?一定。秒和分钟的范围是 0-59,即 为了表示它们,6 位就足够了,而不是 8 位。小时 – 0-23(5 位),天 – 0-30(5 位),月份 – 0-11(4 位)。总计,不考虑年份的所有内容 - 26 位。int 的大小还剩 6 位。理论上,在某些情况下,这可能足够一年。如果不是,添加另一个字节会将数据字段的大小增加到 14 位,这给出了 0-16383 的范围。这在实际应用中已经足够了。总的来说,我们已将存储必要信息所需的数据大小减少到 5 个字节。如果不超过4。缺点和前一种情况一样——如果你存储的是打包的日期,那么就需要转换方法。但我想这样做:将其存储在单独的字段中并以打包形式序列化。这是使用有意义的地方Externalizable
// data is packed into 5 bytes:
//  3         2         1
// 10987654321098765432109876543210
// hhhhhmmmmmmssssssdddddMMMMyyyyyy yyyyyyyy
public void writeExternal(ObjectOutput out){
    int packed = 0;
    packed += ((int)hours) << 27;
    packed += ((int)minutes) << 21;
    packed += ((int)seconds) << 15;
    packed += ((int)day) << 10;
    packed += ((int)month) << 6;
    packed += (((int)year) >> 8) & 0x3F;
    out.writeInt(packed);
    out.writeByte((byte)year);
}

public void readExternal(ObjectInput in){
    int packed = in.readInt();
    year = in.readByte() & 0xFF;
    year += (packed & 0x3F) << 8;
    month = (packed >> 6) & 0x0F;
    day = (packed >> 10) & 0x1F;
    seconds = (packed >> 15) & 0x3F;
    minutes = (packed >> 21) & 0x3F;
    hours = (packed >> 27);
}
事实上,仅此而已。序列化后,我们得到每个类的开销、两个字段(而不是 6 个)和 5 个字节的数据。这已经明显更好了。进一步的包装可以留给专门的图书馆。给出的例子非常简单。其主要目的是展示如何使用高级序列化。尽管在我看来,序列化数据量的可能增加远不是主要优势。主要优点,除了灵活性之外......(顺利地进入下一节......)链接到源:序列化原样
评论
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION