9

アプリのデバッグに数時間を費やしたばかりで、(別の o_O) Java バグに出くわしたと思います... スニフ... そうでないといいのですが、これは悲しいことです :(

私は次のことをしています:

  1. maskいくつかのフラグを使用して EnumSet を作成する
  2. それをシリアライズする(でObjectOutputStream.writeObject(mask)
  3. で他のいくつかのフラグをクリアおよび設定するmask
  4. もう一度シリアライズする

期待される結果: 2 番目のシリアル化されたオブジェクトは最初のオブジェクトとは異なります (インスタンスの変更を反映します)。

得られた結果: 2 番目のシリアル化されたオブジェクトは、最初のオブジェクトの正確なコピーです

コード:

enum MyEnum {
    ONE, TWO
}

@Test
public void testEnumSetSerialize() throws Exception {           
    ByteArrayOutputStream bos = new ByteArrayOutputStream();
    ObjectOutputStream stream = new ObjectOutputStream(bos);

    EnumSet<MyEnum> mask = EnumSet.noneOf(MyEnum.class);
    mask.add(MyEnum.ONE);
    mask.add(MyEnum.TWO);
    System.out.println("First serialization: " + mask);
    stream.writeObject(mask);

    mask.clear();
    System.out.println("Second serialization: " + mask);
    stream.writeObject(mask);
    stream.close();

    ObjectInputStream istream = new ObjectInputStream(new ByteArrayInputStream(bos.toByteArray()));

    System.out.println("First deserialized " + istream.readObject());
    System.out.println("Second deserialized " + istream.readObject());
}

それは印刷します:

First serialization: [ONE, TWO]
Second serialization: []
First deserialized [ONE, TWO]
Second deserialized [ONE, TWO]  <<<<<< Expecting [] here!!!!

私はEnumSet間違って使用していますか?クリアするのではなく、毎回新しいインスタンスを作成する必要がありますか?

ご意見ありがとうございます。

**** アップデート ****

私の最初のアイデアはEnumSet、次のメッセージでどのフィールドが存在するか、または存在しないかを示すマスクとして使用することでした。これは、一種の帯域幅と CPU 使用率の最適化です。大間違いでした!!! シリアル化にEnumSetは時間がかかり、各インスタンスには 30 (!!!) バイトかかります! 宇宙経済についてはこれで終わりです:)

一言で言えば、ObjectOutputStreamプリミティブ型の場合は非常に高速ですが (ここでの小さなテストで既にわかっているように: https://stackoverflow.com/a/33753694 )、(特に小さな) オブジェクトでは非常に遅く、非効率的です.. .

そのため、int に裏打ちされた独自の EnumSet を作成し、(オブジェクトではなく) int を直接シリアル化/逆シリアル化することで、この問題を回避しました。

static class MyEnumSet<T extends Enum<T>> {
    private int mask = 0;

    @Override
    public boolean equals(Object o) {
        if (o == null || getClass() != o.getClass()) return false;
        return mask == ((MyEnumSet<?>) o).mask;
    }

    @Override
    public int hashCode() {
        return mask;
    }

    private MyEnumSet(int mask) {
        this.mask = mask;
    }

    public static <T extends Enum<T>> MyEnumSet<T> noneOf(Class<T> clz) {
        return new MyEnumSet<T>(0);
    }

    public static <T extends Enum<T>> MyEnumSet<T> fromMask(Class<T> clz, int mask) {
        return new MyEnumSet<T>(mask);
    }

    public int mask() {
        return mask;
    }

    public MyEnumSet<T> add(T flag) {
        mask = mask | (1 << flag.ordinal());
        return this;
    }

    public void clear() {
        mask = 0;
    }
}

private final int N = 1000000;

@Test
public void testSerializeMyEnumSet() throws Exception {

    ByteArrayOutputStream bos = new ByteArrayOutputStream(N * 100);
    ObjectOutputStream out = new ObjectOutputStream(bos);

    List<MyEnumSet<TestEnum>> masks = Lists.newArrayList();

    Random r = new Random(132477584521L);
    for (int i = 0; i < N; i++) {
        MyEnumSet<TestEnum> mask = MyEnumSet.noneOf(TestEnum.class);
        for (TestEnum f : TestEnum.values()) {
            if (r.nextBoolean()) {
                mask.add(f);
            }
        }
        masks.add(mask);
    }

    logger.info("Serializing " + N + " myEnumSets");
    long tic = TicToc.tic();
    for (MyEnumSet<TestEnum> mask : masks) {
        out.writeInt(mask.mask());
    }
    TicToc.toc(tic);
    out.close();
    logger.info("Size: " + bos.size() + " (" + (bos.size() / N) + "b per object)");

    logger.info("Deserializing " + N + " myEnumSets");
    MyEnumSet<TestEnum>[] deserialized = new MyEnumSet[masks.size()];

    ObjectInputStream in = new ObjectInputStream(new ByteArrayInputStream(bos.toByteArray()));
    tic = TicToc.tic();
    for (int i = 0; i < deserialized.length; i++) {
        deserialized[i] = MyEnumSet.fromMask(TestEnum.class, in.readInt());
    }
    TicToc.toc(tic);

    Assert.assertArrayEquals(masks.toArray(), deserialized);

}

シリアル化中は約 130 倍、逆シリアル化中は 25 倍高速です...

MyEnumSet:

17/12/15 11:59:31 INFO - Serializing 1000000 myEnumSets
17/12/15 11:59:31 INFO - Elapsed time is 0.019 s
17/12/15 11:59:31 INFO - Size: 4019539 (4b per object)
17/12/15 11:59:31 INFO - Deserializing 1000000 myEnumSets
17/12/15 11:59:31 INFO - Elapsed time is 0.021 s

通常の EnumSet:

17/12/15 11:59:48 INFO - Serializing 1000000 enumSets
17/12/15 11:59:51 INFO - Elapsed time is 2.506 s
17/12/15 11:59:51 INFO - Size: 30691553 (30b per object)
17/12/15 11:59:51 INFO - Deserializing 1000000 enumSets
17/12/15 11:59:51 INFO - Elapsed time is 0.489 s

ただし、それほど安全ではありません。たとえば、32 を超えるエントリを持つ列挙型では機能しません。

MyEnumSet の作成時に列挙型の値が 32 未満であることを確認するにはどうすればよいですか?

4

1 に答える 1

12

ObjectOutputStream は、オブジェクトへの参照をシリアル化し、オブジェクトが初めて送信されるときに実際のオブジェクトをシリアル化します。オブジェクトを変更して再度送信する場合、ObjectOutputStream が行うことは、そのオブジェクトへの参照を再度送信することだけです。

これにはいくつかの影響があります

  • オブジェクトを変更しても、それらの変更は表示されません
  • 両端で、これまでに送信されたすべてのオブジェクトへの参照を保持する必要があります。これは、微妙なメモリ リークである可能性があります。
  • これが行われる理由は、ツリーではなくオブジェクトのグラフをシリアル化できるようにするためです。たとえば、A は A を指す B を指します。A を 1 回だけ送信する必要があります。

これを解決してメモリを取り戻す方法は、オブジェクトが完成するたびにreset()を呼び出すことです。たとえば、電話をかける前にflush()

リセットは、ストリームに既に書き込まれているオブジェクトの状態を無視します。状態は、新しい ObjectOutputStream と同じになるようにリセットされます。ストリームの現在のポイントはリセットとしてマークされているため、対応する ObjectInputStream は同じポイントでリセットされます。以前にストリームに書き込まれたオブジェクトは、既にストリームにあるとは見なされません。それらは再びストリームに書き込まれます。

別のアプローチはwriteUnsharedを使用することですが、これは最上位オブジェクトに浅い非共有性を適用します。その場合はEnumSet異なりますが、Enum[]ラップはまだ共有されていますo_O

「非共有」オブジェクトを ObjectOutputStream に書き込みます。このメソッドは writeObject と同じですが、指定されたオブジェクトをストリーム内の新しい一意のオブジェクトとして常に書き込みます (以前にシリアル化されたインスタンスを指す後方参照とは対照的です)。

要するに、これはバグではなく、予想される動作です。

于 2015-12-16T16:27:07.817 に答える