2010-06-03 220 views
53

是否有可能讓final transient字段在Java中序列化後設置爲任何非默認值?我的用例是一個緩存變量 - 這就是爲什麼它是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>(); 
    } 
} 

輸出:

[email protected] {} 
[email protected] null 

回答

30

簡短的回答是 「不」 遺憾的是 - 我經常想這個。但瞬態不能是最終的。

最終字段必須通過直接賦值初始值或在構造函數中進行初始化。在反序列化過程中,這些都不會被調用,因此必須在反序列化期間調用的'readObject()'私有方法中設置初始瞬變值。爲了達到這個目的,瞬變必須是非最終的。

(嚴格地說,總決賽只是最終他們第一次被讀取,所以有黑客是有可能被讀取之前將其分配一個值,但對我來說,這是前進了一大步太遠。)

+0

謝謝。我懷疑這也是,但不知道我沒有錯過任何東西。 – doublep 2010-06-03 19:23:44

+4

你的回答「瞬變不能是最終的」是不正確的:請詳細解釋Hibernate源代碼和最終瞬態:https://github.com/hibernate/hibernate-orm/blob/4.3.7.Final/hibernate- core/src/main/java/org/hibernate/internal/SessionFactoryImpl.java – 2014-12-04 11:08:16

+12

其實答案是錯誤的。 'transient'字段可以是'final'。但是爲了使其他的工作不是默認值('false' /'0' /'0.0' /'null'),你不僅需要實現'readObject()'而且還要實現'readResolve()',或使用* Reflection *。 – 2015-02-26 10:51:54

14

您可以使用反射更改字段的內容。適用於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! 
    } 

} 

記住:Final is not final anymore!

+3

好吧,它看起來太亂了,我猜這是放棄在這裏'最後'更容易;) – doublep 2010-06-03 19:24:18

+1

你也可以實現一個'TransientMap',它標記'final'而不是'transient'。然而,每個屬性在映射中都必須是'transient',因此映射不是序列化的,但仍然存在於非序列化(和空)上。 – Pindatjuh 2011-06-21 14:02:34

+0

@doublep:實際上,反序列化是存在這種可能性的原因。這也是它爲什麼不能用於'static final'字段的原因,'static'字段永遠不會被(序列化),因此不需要這樣的特性。 – Holger 2016-06-03 13:06:51

5

到這樣的問題,一般的解決方法是使用「串行代理」(見效果Java 2nd Ed)。如果您需要在不破壞串行兼容性的情況下將其改造爲現有的可串行化類,那麼您將需要進行一些黑客行爲。

+0

不要以爲你可以擴大這個答案,可以嗎?恐怕我沒有這本書... – Jules 2016-07-22 23:10:55

+0

@ user1803551這並不完全有幫助。這裏的答案應該提供如何解決問題的實際描述,而不僅僅是指向谷歌搜索的指針。 – Jules 2016-07-26 12:25:17

11

是的,這很容易通過實施(顯然很少知道!)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); 
     } 
    } 
} 

輸出 - 字符串被保留,但瞬態地圖被重置爲空白地圖:

Before: [email protected] 'This data will be serialized' {This data=is transient} 
After: [email protected] 'This data will be serialized' {} 
+0

不會這麼簡單。複製構造函數不是自動的,所以如果我有20個字段,其中2個是瞬態的,我需要在複製構造函數中有選擇地複製18個字段。但是,這確實達到了我想要的。 – doublep 2014-11-20 09:29:57

3

五年後,我發現我原來的(但非空!)在我通過谷歌偶然發現這篇文章後,回答不令人滿意。另一種解決方案是根本不使用反射,並使用Boann提出的技術。

它還利用GetField類返回的ObjectInputStream#readFields()方法,該方法根據序列化規範必須在私有方法readObject(...)中調用。

該解決方案通過將取回的字段存儲在由反序列化過程創建的臨時「實例」的臨時瞬態字段(稱爲FinalExample#fields)中,使得字段反序列化變得明確。所有對象字段然後反序列化並調用readResolve(...):創建一個新實例,但這次使用構造函數,放棄臨時字段的臨時實例。該實例使用GetField實例明確地恢復每個字段;與其他構造函數一樣,這是檢查任何參數的地方。如果構造函數拋出異常,則將其轉換爲InvalidObjectException,並且此對象的反序列化失敗。

包含的微基準測試確保此解決方案不會比默認序列化/反序列化慢。事實上,這是我的電腦上:

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; 
     } 

    } 

} 

一個值得注意的問題:每當類指的是另一個對象實例,有可能泄漏臨時由序列化過程創建的「實例」:只有在讀取所有子對象後纔會發生對象解析,因此子對象可能會保留對臨時對象的引用。類可以通過檢查GetField臨時字段爲空來檢查這種非法構造的實例的使用。只有當它爲空時,它纔是使用常規構造函數創建的,而不是通過反序列化過程。

給自己的提示:或許五年後會有更好的解決方案。回頭見!

+1

請注意,這似乎只適用於原始值。在使用Object值進行測試之後,由於不希望GetField對象轉義readObject方法,因此會引發InternalError。因此,這個答案簡化爲Boann的答案,並沒有增加新的東西。 – Pindatjuh 2015-07-12 19:06:20