6

次のコードはをスローしNullPointerExceptionます。

import java.io.*;

public class NullFinalTest {
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        Foo foo = new Foo();
        foo.useLock();
        ByteArrayOutputStream buffer = new ByteArrayOutputStream();
        new ObjectOutputStream(buffer).writeObject(foo);
        foo = (Foo) new ObjectInputStream(new ByteArrayInputStream(buffer.toByteArray())).readObject();
        foo.useLock();
    }

    public static class Foo implements Serializable {
        private final String lockUsed = "lock used";
        private transient final Object lock = new Object();
        public void useLock() {
            System.out.println("About to synchronize");
            synchronized (lock) { // <- NullPointerException here on 2nd call
                System.out.println(lockUsed);
            }
        }
    }
}

出力は次のとおりです。

About to synchronize
lock used
About to synchronize
Exception in thread "main" java.lang.NullPointerException
    at NullFinalTest$Foo.useLock(NullFinalTest.java:18)
    at NullFinalTest.main(NullFinalTest.java:10)

どうしてlocknullになる可能性がありますか?

4

3 に答える 3

14

A transient final field used as a lock is null

過渡変数に関するいくつかの事実は次のとおりです。

-インスタンス変数でTransientキーワードを使用すると、そのインスタンス変数がシリアル化されなくなります。

-逆シリアル化すると、一時変数はデフォルト値になります....。

例えば:

  • オブジェクト参照変数からnull
  • int to0
  • ブール値false,など......。

だから、NullPointerExceptionそれを逆シリアル化するときに、あなたが得る理由です...

于 2012-09-07T20:03:38.630 に答える
4

宣言されたフィールドはtransientシリアル化されません。さらに、このブログ投稿によると、フィールド値はデフォルトのコンストラクターによって設定される値にさえ初期化されていません。これは、transientフィールドがである場合に課題を作成しますfinal

Serializable javadocによると、デシリアライズは次のメソッドを実装することで制御できます。

private void readObject(java.io.ObjectInputStream in)
    throws IOException, ClassNotFoundException;

この優れたStackOverflowの回答に基づいて、次の解決策を思いつきました。

import java.io.*;
import java.lang.reflect.*;

public class NullFinalTestFixed {
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        Foo foo = new Foo();
        foo.useLock();
        ByteArrayOutputStream buffer = new ByteArrayOutputStream();
        new ObjectOutputStream(buffer).writeObject(foo);
        foo = (Foo) new ObjectInputStream(new ByteArrayInputStream(buffer.toByteArray())).readObject();
        foo.useLock();
    }

    public static class Foo implements Serializable {
        private final String lockUsed = "lock used";
        private transient final Object lock = new Object();
        public void useLock() {
            System.out.println("About to synchronize");
            synchronized (lock) {
                System.out.println(lockUsed);
            }
        }

        private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
            in.defaultReadObject();
            initLocks(this, "lock");
        }
    }

    public static void initLocks(Object obj, String... lockFields) {
        for (String lockField: lockFields) {
            try {
                Field lock = obj.getClass().getDeclaredField(lockField);
                setFinalFieldValue(obj, lock, new Object());
            } catch (NoSuchFieldException e) {
                throw new RuntimeException(e);
            }
        }
    }

    public static void setFinalFieldValue(Object obj, Field field, Object value) {
        Exception ex;
        try {
            field.setAccessible(true);
            Field modifiers = Field.class.getDeclaredField("modifiers");
            modifiers.setAccessible(true);
            modifiers.setInt(field, field.getModifiers() & ~Modifier.FINAL);
            field.set(obj, value);
            return;
        } catch (IllegalAccessException e) {
            ex = e;
        } catch (NoSuchFieldException e) {
            ex = e;
        }
        throw new RuntimeException(ex);
    }
}

これを実行すると、次の出力(no NullPointerException)が生成されます。

About to synchronize
lock used
About to synchronize
lock used
于 2012-09-07T19:47:57.387 に答える
2

前に指摘したように、以下の宣言は期待どおりに機能しません。

transient final Object foo = new Object()

キーワードは、メンバーがシリアル化されるのtransientを防ぎます。デフォルト値での初期化は、デシリアライズ中は受け入れられないため、デシリアライズ後fooに行わnullれます。

キーワードは、final一度設定されたメンバーを変更できないようにします。nullこれは、逆シリアル化されたインスタンスで永遠に立ち往生していることを意味します。

いずれの場合も、finalキーワードを削除する必要があります。privateこれは不変性を犠牲にしますが、通常はメンバーにとって問題にはなりません。

次に、2つのオプションがあります。

オプション1:オーバーライドreadObject()

transient Object foo = new Object();

@Override
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
    in.defaultReadObject();
    foo = new Object();
}

新しいインスタンスを作成するfooと、はデフォルト値に初期化されます。デシリアライズするときは、カスタムreadObject()メソッドがそれを処理します。

これはJREでは機能しますが、Androidの実装にはメソッドがないため、Androidでは機能しませSerializablereadObject()

オプション2:遅延初期化

宣言:

transient Object foo;

アクセス時:

if (foo == null)
    foo = new Object();
doStuff(foo);

これは、コード内のどこにアクセスする場合でも実行する必要がありますfoo。これは、最初のオプションよりも作業が多く、エラーが発生しやすい可能性がありますが、JREとAndroidで同様に機能します。

于 2016-06-12T15:54:44.027 に答える