IT TIP

최종 과도 필드 및 직렬화

itqueen 2020. 12. 12. 12:54
반응형

최종 과도 필드 및 직렬화


final transientJava에서 직렬화 후 기본값이 아닌 값으로 설정된 필드 를 가질 수 있습니까? 내 사용 사례는 캐시 변수입니다 transient. 이것이 바로 . 또한 Map변경되지 않는 필드를 만드는 습관이 있습니다 (예 : 맵의 내용은 변경되지만 객체 자체는 동일하게 유지됨) final. 그러나 이러한 속성은 모순되는 것처럼 보입니다. 컴파일러는 이러한 조합을 허용하지만 null직렬화 해제 후에 는 필드를 아무것도 설정할 수 없습니다 .

성공하지 못한 채 다음을 시도했습니다.

  • 간단한 필드 초기화 (예제에 표시됨) : 이것은 일반적으로 수행하는 작업이지만 직렬화 해제 후에 초기화가 발생하지 않는 것 같습니다.
  • 생성자 초기화 (나는 이것이 위와 의미 상 동일하다고 생각합니다);
  • 필드 할당 readObject()— 필드가이므로 수행 할 수 없습니다 final.

이 예제 cachepublic테스트 용입니다.

import java.io.*;
import java.util.*;

public class test
{
    public static void main (String[] args) throws Exception
    {
        X  x = new X ();
        System.out.println (x + " " + x.cache);

        ByteArrayOutputStream  buffer = new ByteArrayOutputStream ();
        new ObjectOutputStream (buffer).writeObject (x);
        x = (X) new ObjectInputStream (new ByteArrayInputStream (buffer.toByteArray ())).readObject ();
        System.out.println (x + " " + x.cache);
    }

    public static class X implements Serializable
    {
        public final transient Map <Object, Object>  cache = new HashMap <Object, Object> ();
    }
}

산출:

test$X@1a46e30 {}
test$X@190d11 null

안타깝게도 짧은 대답은 "아니오"입니다. 저는 종종 이것을 원했습니다. 그러나 과도 현상은 최종적 일 수 없습니다.

최종 필드는 초기 값을 직접 할당하거나 생성자에서 초기화해야합니다. deserialization 중에는 이들 중 어느 것도 호출되지 않으므로 deserialization 중에 호출되는 'readObject ()'개인 메서드에서 과도 상태에 대한 초기 값을 설정해야합니다. 그리고 그것이 작동하려면 과도 상태가 최종적이지 않아야합니다.

(엄밀히 말해서, 결승전은 처음 읽었을 때만 최종적이므로 읽기 전에 값을 할당 할 수있는 해킹이 있지만 나에게는 너무 멀리 가고 있습니다.)


Reflection을 사용하여 필드의 내용을 변경할 수 있습니다. Java 1.5 이상에서 작동합니다. 직렬화가 단일 스레드에서 수행되기 때문에 작동합니다. 다른 스레드가 동일한 객체에 액세스 한 후에는 최종 필드를 변경해서는 안됩니다 (메모리 모델의 이상 함 및 반복 때문에).

따라서에서 readObject()다음 예제와 유사한 작업을 수행 할 수 있습니다.

import java.lang.reflect.Field;

public class FinalTransient {

    private final transient Object a = null;

    public static void main(String... args) throws Exception {
        FinalTransient b = new FinalTransient();

        System.out.println("First: " + b.a); // e.g. after serialization

        Field f = b.getClass().getDeclaredField("a");
        f.setAccessible(true);
        f.set(b, 6); // e.g. putting back your cache

        System.out.println("Second: " + b.a); // wow: it has a value!
    }

}

기억하십시오 : 결승은 더 이상 결승이 아닙니다!


예, 이것은 (분명히 거의 알려지지 않은!) readResolve()방법 을 구현함으로써 쉽게 가능합니다 . 역 직렬화 된 후 개체를 교체 할 수 있습니다. 이를 사용하여 원하는대로 대체 개체를 초기화하는 생성자를 호출 할 수 있습니다. 예 :

import java.io.*;
import java.util.*;

public class test {
    public static void main(String[] args) throws Exception {
        X x = new X();
        x.name = "This data will be serialized";
        x.cache.put("This data", "is transient");
        System.out.println("Before: " + x + " '" + x.name + "' " + x.cache);

        ByteArrayOutputStream buffer = new ByteArrayOutputStream();
        new ObjectOutputStream(buffer).writeObject(x);
        x = (X)new ObjectInputStream(new ByteArrayInputStream(buffer.toByteArray())).readObject();
        System.out.println("After: " + x + " '" + x.name + "' " + x.cache);
    }

    public static class X implements Serializable {
        public final transient Map<Object,Object> cache = new HashMap<>();
        public String name;

        public X() {} // normal constructor

        private X(X x) { // constructor for deserialization
            // copy the non-transient fields
            this.name = x.name;
        }

        private Object readResolve() {
            // create a new object from the deserialized one
            return new X(this);
        }
    }
}

출력-문자열은 보존되지만 임시 맵은 비어 있지만 null이 아닌 맵으로 재설정됩니다.

Before: test$X@172e0cc 'This data will be serialized' {This data=is transient}
After: test$X@490662 'This data will be serialized' {}

이와 같은 문제에 대한 일반적인 해결책은 "직렬 프록시"를 사용하는 것입니다 (Effective Java 2nd Ed 참조). 직렬 호환성을 깨지 않고 기존 직렬화 가능한 클래스로이를 개조해야하는 경우 해킹을해야합니다.


5 년 후 Google을 통해이 게시물을 우연히 발견 한 후 원래 답변이 불만족 스러웠습니다. 또 다른 해결책은 반사를 전혀 사용하지 않고 Boann이 제안한 기술을 사용하는 것입니다.

또한 Serialization 사양에 따라 private 메서드 에서 호출되어야하는 메서드에서 반환 된 GetField 클래스를 사용 ObjectInputStream#readFields()합니다 readObject(...).

이 솔루션은 FinalExample#fields역 직렬화 프로세스에 의해 생성 된 임시 "인스턴스"의 임시 임시 필드 (라고 함 )에 검색된 필드를 저장하여 필드 역 직렬화를 명시 적으로 만듭니다 . 그런 다음 모든 개체 필드가 ​​역 직렬화되고 readResolve(...)호출됩니다. 새 인스턴스가 생성되지만 이번에는 생성자를 사용하여 임시 필드가있는 임시 인스턴스를 삭제합니다. 인스턴스는 GetField인스턴스를 사용하여 각 필드를 명시 적으로 복원 합니다. 다른 생성자처럼 매개 변수를 확인하는 곳입니다. 생성자에 의해 예외가 발생하면로 변환 InvalidObjectException되고이 개체의 역 직렬화가 실패합니다.

포함 된 마이크로 벤치 마크는이 솔루션이 기본 직렬화 / 역 직렬화보다 느리지 않도록 보장합니다. 실제로 내 PC에 있습니다.

Problem: 8.598s Solution: 7.818s

다음은 코드입니다.

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InvalidObjectException;
import java.io.ObjectInputStream;
import java.io.ObjectInputStream.GetField;
import java.io.ObjectOutputStream;
import java.io.ObjectStreamException;
import java.io.Serializable;

import org.junit.Test;

import static org.junit.Assert.*;

public class FinalSerialization {

    /**
     * Using default serialization, there are problems with transient final
     * fields. This is because internally, ObjectInputStream uses the Unsafe
     * class to create an "instance", without calling a constructor.
     */
    @Test
    public void problem() throws Exception {
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(baos);
        WrongExample x = new WrongExample(1234);
        oos.writeObject(x);
        oos.close();
        ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
        ObjectInputStream ois = new ObjectInputStream(bais);
        WrongExample y = (WrongExample) ois.readObject();
        assertTrue(y.value == 1234);
        // Problem:
        assertFalse(y.ref != null);
        ois.close();
        baos.close();
        bais.close();
    }

    /**
     * Use the readResolve method to construct a new object with the correct
     * finals initialized. Because we now call the constructor explicitly, all
     * finals are properly set up.
     */
    @Test
    public void solution() throws Exception {
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(baos);
        FinalExample x = new FinalExample(1234);
        oos.writeObject(x);
        oos.close();
        ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
        ObjectInputStream ois = new ObjectInputStream(bais);
        FinalExample y = (FinalExample) ois.readObject();
        assertTrue(y.ref != null);
        assertTrue(y.value == 1234);
        ois.close();
        baos.close();
        bais.close();
    }

    /**
     * The solution <em>should not</em> have worse execution time than built-in
     * deserialization.
     */
    @Test
    public void benchmark() throws Exception {
        int TRIALS = 500_000;

        long a = System.currentTimeMillis();
        for (int i = 0; i < TRIALS; i++) {
            problem();
        }
        a = System.currentTimeMillis() - a;

        long b = System.currentTimeMillis();
        for (int i = 0; i < TRIALS; i++) {
            solution();
        }
        b = System.currentTimeMillis() - b;

        System.out.println("Problem: " + a / 1000f + "s Solution: " + b / 1000f + "s");
        assertTrue(b <= a);
    }

    public static class FinalExample implements Serializable {

        private static final long serialVersionUID = 4772085863429354018L;

        public final transient Object ref = new Object();

        public final int value;

        private transient GetField fields;

        public FinalExample(int value) {
            this.value = value;
        }

        private FinalExample(GetField fields) throws IOException {
            // assign fields
            value = fields.get("value", 0);
        }

        private void readObject(ObjectInputStream stream) throws IOException,
                ClassNotFoundException {
            fields = stream.readFields();
        }

        private Object readResolve() throws ObjectStreamException {
            try {
                return new FinalExample(fields);
            } catch (IOException ex) {
                throw new InvalidObjectException(ex.getMessage());
            }
        }

    }

    public static class WrongExample implements Serializable {

        private static final long serialVersionUID = 4772085863429354018L;

        public final transient Object ref = new Object();

        public final int value;

        public WrongExample(int value) {
            this.value = value;
        }

    }

}

A note of caution: whenever the class refers to another object instance, it might be possible to leak the temporary "instance" created by the serialization process: the object resolution occurs only after all sub-objects are read, hence it is possible for subobjects to keep a reference to the temporary object. Classes can check for use of such illegally constructed instances by checking that the GetField temporary field is null. Only when it is null, it was created using a regular constructor and not through the deserialization process.

Note to self: Perhaps a better solution exists in five years. See you then!

참고URL : https://stackoverflow.com/questions/2968876/final-transient-fields-and-serialization

반응형