2012-04-05 7 views
5

Hãy xem xét ba loại sau đây:Weird hành vi serialization với HashMap

  • EntityTransformer chứa một bản đồ liên kết một Entity với một String
  • Entity là một đối tượng chứa một ID (được sử dụng bởi bình đẳng/hashcode) và chứa tham chiếu đến một số EntityTransformer (lưu ý phụ thuộc vòng tròn)
  • SomeWrapper chứa một EntityTransformer, và duy trì một bản đồ liên kết định danh Entity 's và tương ứng Entity đối tượng.

Các mã sau sẽ tạo ra một EntityTransformer và một Wrapper, thêm hai thực thể đến Wrapper, serialize nó, deserialize nó và kiểm tra sự hiện diện của hai entitites:

public static void main(String[] args) 
    throws Exception { 

    EntityTransformer et = new EntityTransformer(); 
    Wrapper wr = new Wrapper(et); 

    Entity a1 = wr.addEntity("a1"); // a1 and a2 are created internally by the Wrapper 
    Entity a2 = wr.addEntity("a2"); 

    byte[] bs = object2Bytes(wr); 
    wr = (SomeWrapper) bytes2Object(bs); 

    System.out.println(wr.et.map); 
    System.out.println(wr.et.map.containsKey(a1)); 
    System.out.println(wr.et.map.containsKey(a2)); 
} 

Đầu ra là:

{a1 = bất cứ điều gì-a1, a2 = bất cứ điều gì-a2}

sai

true

Vì vậy, về cơ bản, việc tuần tự hóa không thành công, vì bản đồ phải chứa cả hai thực thể dưới dạng Khóa. Tôi nghi ngờ sự phụ thuộc cyclic giữa Entity và EntityTransformer, và thực sự nếu tôi làm tĩnh biến EntityManager instance của Entity, nó hoạt động.

Câu hỏi 1: với điều kiện tôi bị kẹt với sự phụ thuộc theo chu kỳ này, làm thế nào tôi có thể khắc phục vấn đề này?

Một điều rất lạ: nếu tôi xóa Bản đồ duy trì liên kết giữa số nhận dạng và Thực thể trong Trình bao bọc, mọi thứ hoạt động tốt ... ??

Câu hỏi 2: ai đó hiểu điều gì đang xảy ra ở đây?

Bellow là một mã chức năng đầy đủ nếu bạn muốn thử nghiệm nó:

Cảm ơn trước sự giúp đỡ của bạn :)

public class SerializeTest { 

public static class Entity 
     implements Serializable 
{ 
    private EntityTransformer em; 
    private String id; 

    Entity(String id, EntityTransformer em) { 
     this.id = id; 
     this.em = em; 
    } 

    @Override 
    public boolean equals(Object obj) { 
     if (obj == null) { 
      return false; 
     } 
     if (getClass() != obj.getClass()) { 
      return false; 
     } 
     final Entity other = (Entity) obj; 
     if ((this.id == null) ? (other.id != null) : !this.id.equals(
      other.id)) { 
      return false; 
     } 
     return true; 
    } 

    @Override 
    public int hashCode() { 
     int hash = 3; 
     hash = 97 * hash + (this.id != null ? this.id.hashCode() : 0); 
     return hash; 
    } 

    public String toString() { 
     return id; 
    } 
} 

public static class EntityTransformer 
    implements Serializable 
{ 
    Map<Entity, String> map = new HashMap<Entity, String>(); 
} 

public static class Wrapper 
    implements Serializable 
{ 
    EntityTransformer et; 
    Map<String, Entity> eMap; 

    public Wrapper(EntityTransformer b) { 
     this.et = b; 
     this.eMap = new HashMap<String, Entity>(); 
    } 

    public Entity addEntity(String id) { 
     Entity e = new Entity(id, et); 
     et.map.put(e, "whatever-" + id); 
     eMap.put(id, e); 

     return e; 
    } 
} 

public static void main(String[] args) 
    throws Exception { 
    EntityTransformer et = new EntityTransformer(); 
    Wrapper wr = new Wrapper(et); 

    Entity a1 = wr.addEntity("a1"); // a1 and a2 are created internally by the Wrapper 
    Entity a2 = wr.addEntity("a2"); 

    byte[] bs = object2Bytes(wr); 
    wr = (Wrapper) bytes2Object(bs); 

    System.out.println(wr.et.map); 
    System.out.println(wr.et.map.containsKey(a1)); 
    System.out.println(wr.et.map.containsKey(a2)); 
} 



public static Object bytes2Object(byte[] bytes) 
    throws IOException, ClassNotFoundException { 
    ObjectInputStream oi = null; 
    Object o = null; 
    try { 
     oi = new ObjectInputStream(new ByteArrayInputStream(bytes)); 
     o = oi.readObject(); 
    } 
    catch (IOException io) { 
     throw io; 
    } 
    catch (ClassNotFoundException cne) { 
     throw cne; 
    } 
    finally { 
     if (oi != null) { 
      oi.close(); 
     } 
    } 

    return o; 
} 

public static byte[] object2Bytes(Object o) 
    throws IOException { 
    ByteArrayOutputStream baos = null; 
    ObjectOutputStream oo = null; 
    byte[] bytes = null; 
    try { 
     baos = new ByteArrayOutputStream(); 
     oo = new ObjectOutputStream(baos); 

     oo.writeObject(o); 
     bytes = baos.toByteArray(); 
    } 
    catch (IOException ex) { 
     throw ex; 
    } 
    finally { 
     if (oo != null) { 
      oo.close(); 
     } 
    } 

    return bytes; 
} 
} 

EDIT

Có một bản tóm tắt tốt về những gì có khả năng phát trong vấn đề này: http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4957674

Vấn đề là việc thực hiện readObject() của HashMap, theo thứ tự để băm lại bản đồ, gọi phương thức hashCode() của một số khóa của nó, bất kể các khóa đó đã được deserialized hoàn toàn chưa.

Nếu một chìa khóa chứa (trực tiếp hoặc gián tiếp) một tham chiếu vòng tròn để bản đồ , thứ tự sau đây của thi thể trong deserialization --- nếu phím được ghi vào dòng đối tượng trước khi các hashmap:

  1. Khởi tạo khóa
  2. Deserialize các thuộc tính của khóa 2a. Deserialize HashMap (được gán trực tiếp hoặc gián tiếp bởi khóa) 2a-1. Khởi tạo HashMap 2a-2. Đọc khóa và giá trị 2a-3. Gọi hàm hashCode() trên các phím để băm lại bản đồ 2b. Deserialize thuộc tính còn lại của chính

Từ 2a-3 được thực hiện trước khi 2b, hashCode() có thể trả lại sai câu trả lời, bởi vì các thuộc tính của chính chưa được đầy đủ deserialized.

Bây giờ điều đó không giải thích đầy đủ lý do tại sao sự cố có thể được khắc phục nếu HashMap từ Wrapper bị xóa hoặc chuyển sang lớp EntityTransformer.

+0

Tại sao cậu lại làm 'Logic equals' của bạn để phức tạp? Tại sao không chỉ 'return (this.id == null)? (other.id == null): this.id.equals (other.id); '? – trutheality

+0

Bằng/mã băm được tự động tạo bởi NetBeans cho ví dụ này, và vì đây không phải là gốc của vấn đề của tôi, tôi không dành thời gian để xem xét nó – ecniv

+0

@truheality vì vi phạm hợp đồng bằng! – dty

Trả lời

4

Đây là vấn đề với khởi tạo vòng tròn. Trong khi Java serialization có thể xử lý các chu kỳ tùy ý, thì việc khởi tạo phải diễn ra theo một trật tự nào đó.

Có vấn đề tương tự trong AWT, nơi Component (Entity) chứa tham chiếu đến phụ huynh Container (EntityTransformer). AWT làm gì để đặt tham chiếu gốc trong Componenttransient.

transient Container parent; 

Vì vậy, bây giờ mỗi Component có thể hoàn thành khởi động của nó trước khi Container.readObject thêm nó lại:

for(Component comp : component) { 
     comp.parent = this; 
+0

Hum có vẻ là một giải pháp thú vị ... Bây giờ nếu tôi không có khả năng thay đổi EntityTransformer (ví dụ: nó là một lớp bên ngoài từ một thư viện thương mại)? – ecniv

+0

Tôi thấy rằng đặt bản đồ chứa trong Wrapper, trực tiếp vào EntityTransformer dường như để khắc phục vấn đề. Rất lạ ... – ecniv

+0

Nối tiếp tham chiếu tới EntityTransformer đã thực hiện công việc và dường như là một giải pháp sạch hơn, cảm ơn :) – ecniv

0

Bạn có thể ghi đè chuỗi tuần tự hóa để biến tham chiếu thành giá trị khóa trước khi tuần tự hóa và sau đó chuyển đổi nó trở lại trên quá trình deserialization không? Có vẻ như nó sẽ là khá tầm thường để tìm khóa băm của EntityTransformer khi serializing và sử dụng giá trị đó thay vào đó, (có thể cung cấp một giá trị trong cấu trúc được gọi là parentKey) và null ra tham chiếu. Sau đó, khi reserializing, bạn tìm EntityTransformer được kết hợp với giá trị khóa đó và gán tham chiếu của nó.

+0

Hum Tôi không chắc tôi hiểu rõ đề xuất của bạn. Làm thế nào tôi nên tìm EntityTransformer khi reserializing nếu nó đã không thực sự được serialized? (lưu ý rằng trong trường hợp sử dụng của tôi, việc tuần tự hóa có ích khi truyền các đối tượng đến các chương trình khác mà không nhất thiết phải chia sẻ cùng một máy ảo). – ecniv

3

Ngay cả người lạ, nếu bạn làm

Map<Entity, String> map = new HashMap<>(wr.et.map); 
System.out.println(map.containsKey(a1)); 
System.out.println(map.containsKey(a2)); 

Sau serializing và de-serializing, bạn sẽ nhận được kết quả chính xác.

Ngoài ra:

for(Entity a : wr.et.map.keySet()){ 
    System.out.println(a.toString()); 
    System.out.println(wr.et.map.containsKey(a)); 
} 

Cung cấp:

a1 
false 
a2 
true 

Tôi nghĩ rằng bạn tìm thấy một lỗi.Nhiều khả năng, serialization đã phá vỡ băm bằng cách nào đó. Trong thực tế, tôi nghĩ rằng bạn có thể đã tìm thấy this bug.

+0

Tôi càng chơi càng nhiều thì tôi càng đồng ý. – dty