2010-06-03 37 views
53

Có thể có các trường final transient được đặt thành bất kỳ giá trị không mặc định nào sau khi tuần tự hóa trong Java không? Usecase của tôi là một biến bộ nhớ cache - đó là lý do tại sao nó là transient. Tôi cũng có thói quen tạo các trường Map sẽ không bị thay đổi (tức là nội dung của bản đồ bị thay đổi, nhưng bản thân đối tượng vẫn giữ nguyên) final. Tuy nhiên, các thuộc tính này dường như mâu thuẫn - trong khi trình biên dịch cho phép kết hợp như vậy, tôi không thể có trường được đặt thành bất kỳ thứ gì trừ null sau khi unserialization.trường thoáng qua cuối cùng và tuần tự hóa

Tôi đã thử các sau đây, nhưng không thành công:

  • khởi lĩnh vực đơn giản (thể hiện trong ví dụ): đây là những gì tôi thường làm, nhưng khởi dường như không xảy ra sau khi unserialization;
  • khởi tạo trong hàm tạo (tôi tin rằng đây là ngữ nghĩa giống như ở trên);
  • gán trường trong readObject() - không thể thực hiện được vì trường là final.

Trong ví dụ cachepublic chỉ để thử nghiệm.

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

Output:

[email protected] {} 
[email protected] null 

Trả lời

30

Câu trả lời ngắn gọn là "không" không may - Tôi đã thường muốn điều này. nhưng transients không thể là cuối cùng.

Trường cuối cùng phải được khởi tạo bằng cách gán trực tiếp giá trị ban đầu hoặc trong hàm tạo. Trong quá trình deserialization, không phải trong số này được gọi, vì vậy giá trị ban đầu cho transients phải được thiết lập trong phương thức riêng 'readObject()' được gọi trong quá trình deserialization. Và để làm việc đó, các transients phải không phải là cuối cùng.

(Nói đúng ra, trận chung kết chỉ là thức lần đầu tiên họ được đọc, vì vậy có hack mà có thể gán một giá trị trước khi nó được đọc, nhưng đối với tôi đây đang diễn ra một bước quá xa.)

+0

Cảm ơn. Tôi nghi ngờ đó là cách đó, nhưng không chắc tôi đã không bỏ lỡ một cái gì đó. – doublep

+4

Câu trả lời của bạn "transients không thể là final" là không chính xác: hãy giải thích mã nguồn Hibernate với 'final transient' trên tất cả: https://github.com/hibernate/hibernate-orm/blob/4.3.7.Final/hibernate- core/src/main/java/org/hibernate/internal/SessionFactoryImpl.java –

+12

Thực ra câu trả lời là sai. Các trường 'transient' có thể là' final'. Nhưng để làm việc đó với giá trị mặc định khác ('false' /' 0'/'0.0' /' null'), bạn muốn thực hiện không chỉ 'readObject()' mà còn 'readResolve()', hoặc sử dụng * Reflection *. –

14

Bạn có thể thay đổi nội dung của một trường bằng cách sử dụng Reflection. Hoạt động trên Java 1.5+. Nó sẽ hoạt động, bởi vì tuần tự được thực hiện trong một chuỗi đơn. Sau khi một thread khác truy cập cùng một đối tượng, nó không nên thay đổi trường cuối cùng (vì tính lạ trong mô hình bộ nhớ & phản xạ).

Vì vậy, trong readObject(), bạn có thể làm điều gì đó tương tự như ví dụ sau:

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

} 

Hãy nhớ rằng: Final is not final anymore!

+3

Vâng, có vẻ quá lộn xộn, tôi đoán sẽ dễ dàng từ bỏ 'cuối cùng' ở đây;) – doublep

+1

Bạn cũng có thể thực hiện một 'TransientMap', mà bạn đánh dấu' final' nhưng không phải là 'transient'. Mỗi thuộc tính, tuy nhiên, trong bản đồ phải là 'transient', và do đó bản đồ không được tuần tự hóa, nhưng vẫn tồn tại trên unserialization (và trống). – Pindatjuh

+0

@doublep: thực sự, deserialization là lý do tại sao khả năng này tồn tại.Đó cũng là lý do tại sao nó không hoạt động đối với các trường 'static final', các trường' static' không bao giờ được loại bỏ, do đó, không cần một tính năng như vậy. – Holger

5

Các giải pháp chung cho những vấn đề như thế này là sử dụng một "proxy sê-ri" (xem hiệu quả Java 2nd Ed). Nếu bạn cần phải trang bị thêm cho một lớp serialisable hiện có mà không phá vỡ khả năng tương thích nối tiếp, sau đó bạn sẽ cần phải làm một số hack.

+0

Đừng cho rằng bạn có thể mở rộng câu trả lời này, phải không? Tôi sợ tôi không có cuốn sách được đề cập ... – Jules

+0

@ user1803551 Điều đó không thực sự hữu ích. Câu trả lời ở đây là nghĩa vụ phải cung cấp một mô tả thực tế của làm thế nào để giải quyết vấn đề, không chỉ là một con trỏ đến một tìm kiếm google. – Jules

11

Có, điều này có thể dễ dàng bằng cách thực hiện phương thức (dường như ít được biết!) readResolve(). Nó cho phép bạn thay thế đối tượng sau khi nó được deserialized. Bạn có thể sử dụng nó để gọi một hàm khởi tạo để khởi tạo một đối tượng thay thế, tuy nhiên bạn muốn.Một ví dụ:

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

Output - chuỗi được bảo tồn nhưng bản đồ thoáng được đặt lại một trống bản đồ:

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

Sẽ không gọi điều này dễ dàng. Các nhà xây dựng bản sao không phải là tự động, vì vậy nếu tôi có 20 lĩnh vực, 2 người trong số họ thoáng qua, tôi cần phải chọn lọc sao chép 18 lĩnh vực trong các nhà xây dựng bản sao. Tuy nhiên, điều này thực sự đạt được những gì tôi muốn. – doublep

3

Năm năm sau, tôi thấy ban đầu của tôi (nhưng không null!) trả lời không thỏa đáng sau khi tôi tình cờ gặp bài đăng này qua Google. Một giải pháp khác sẽ được sử dụng không có sự phản ánh nào cả, và sử dụng kỹ thuật được Boann đề xuất.

Nó cũng sử dụng lớp GetField được trả về theo phương pháp ObjectInputStream#readFields(), theo đặc điểm kỹ thuật tuần tự hóa phải được gọi theo phương thức riêng readObject(...).

Giải pháp làm cho trường deserialization rõ ràng bằng cách lưu trữ các trường được truy xuất trong trường tạm thời tạm thời (được gọi là FinalExample#fields) của một trường hợp tạm thời được tạo bởi quá trình deserialization. Tất cả các trường đối tượng sau đó được deserialized và readResolve(...) được gọi là: một cá thể mới được tạo ra nhưng lần này sử dụng một hàm tạo, loại bỏ cá thể tạm thời bằng trường tạm thời. Ví dụ này phục hồi một cách rõ ràng từng trường bằng cách sử dụng cá thể GetField; đây là nơi để kiểm tra bất kỳ tham số nào như bất kỳ hàm tạo nào khác. Nếu một ngoại lệ được ném bởi constructor, nó được dịch sang một số InvalidObjectException và deserialization của đối tượng này thất bại.

Phạm vi vi chuẩn bao gồm đảm bảo rằng giải pháp này không chậm hơn serialization/deserialization mặc định. Trên thực tế, nó là trên máy tính của tôi:

Problem: 8.598s Solution: 7.818s 

Sau đó, ở đây là các mã:

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

    } 

} 

Một lưu ý thận trọng: bất cứ khi nào lớp đề cập đến một trường hợp đối tượng, nó có thể là có thể bị rò rỉ tạm thời "instance" được tạo bởi quá trình tuần tự hóa: độ phân giải đối tượng chỉ xảy ra sau khi tất cả các đối tượng phụ được đọc, do đó có thể cho các đối tượng con để giữ tham chiếu đến đối tượng tạm thời. Các lớp học có thể kiểm tra việc sử dụng các trường hợp được xây dựng bất hợp pháp như vậy bằng cách kiểm tra rằng trường tạm thời GetField là không. Chỉ khi nó là null, nó được tạo ra bằng cách sử dụng một hàm tạo thông thường và không thông qua quá trình deserialization.

Lưu ý tự: Có lẽ giải pháp tốt hơn tồn tại sau năm năm. Gặp lại sau!

+1

Lưu ý rằng điều này dường như chỉ hoạt động với các giá trị nguyên thủy. Sau khi thử nghiệm với các giá trị Object, một InternalError được ném ra khi đối tượng GetField không được dự kiến ​​sẽ thoát khỏi phương thức readObject. Do đó câu trả lời này làm giảm câu trả lời của Boann và không thêm gì mới. – Pindatjuh

Các vấn đề liên quan