Any nice way to make two immutable objects refer t

2020-06-08 12:52发布

问题:

Take these two Java classes:

class User {
   final Inventory inventory;
   User (Inventory inv) {
       inventory = inv;
   }
}

class Inventory {
   final User owner;
   Inventory (User own) {
       owner = own;
   }
}

Is there any way without using reflection* to pull this off? I don't actually expect it is, but it can't hurt to ask.

Update: Since in bytecode construction has two steps (1. allocate object, 2. call constructor**) could this be (ab)used to do this, with handwritten bytecode or a custom compiler? I'm talking about performing step 1 for both objects first, then step 2 for both, using references from step 1. Of course something like that would be rather cumbersome, and this part of the question is academic.

(* Because reflection may give trouble with a security manager)

(** Says my limited knowledge)

回答1:

This can only work cleanly if one of the objects is created by the other. For example you can change your User class to something like this (while keeping the Inventory class unchanged):

class User {
   private final Inventory inventory;
   User () {
       inventory = new Inventory(this);
   }
}

You need to be careful about accessing the User object in the Inventory constructor, however: it's not fully initialized yet. For example, its inventory field will still be null!

Ad Update: I've now verified that the bytecode-manipulation approach does not work. I've tried it using Jasmin and it always failed to load with a VerifyError.

Delving deeper into the issue, I found§ 4.10.2.4 Instance Initialization Methods and Newly Created Objects. This section explains how the JVM ensures that only initialized object instances get passed around.



回答2:

You can do it if you don't need to inject one of the objects.

class User {
   private final Inventory inventory;
   User () {
       inventory = new Inventory(this);
   }
}


回答3:

class User {
    private final Inventory inventory;
    User (/*whatever additional args are needed to construct the inventory*/) {
        //populate user fields
        inventory = new Inventory(this);
    }
}

class Inventory {
    private final User owner;
    Inventory (User own) {
        owner = own;
    }
}

That's the best I can think of. Maybe there's a better pattern.



回答4:

Slightly pedantic, but it's not strictly speaking necessary to create one inside the other, if you don't mind a little indirection. They could both be inner classes.

public class BadlyNamedClass {
    private final User owner;
    private final Inventory inventory;

    public BadlyNamedClass() {
        this.owner = new User() {
            ... has access to BadlyNamedClass.this.inventory;
        };
        this.inventory = new Inventory() {
            ... has access to BadlyNamedClass.this.owner;
        };
    }
    ...
}

Or even:

public class BadlyNamedClass {
    private final User owner;
    private final Inventory inventory;

    public BadlyNamedClass() {
        this.owner = new User(this);
        this.inventory = new Inventory(this);
    }
    public User getOwner() { return owner; }
    public Inventory getInventory() { return inventory; }
    ...
}


回答5:

This is one "solution", though the loss of one final is inconvenient.

class User {
   Inventory inventory;
   User () { }
   // make sure this setter is only callable from where it should be,
   // and is called only once at construction time
   setInventory(inv) {
       if (inventory != null) throw new IllegalStateException();
       inventory = inv;
   }
}

class Inventory {
   final User owner;
   Inventory (User own) {
       owner = own;
   }
}


回答6:

If you are only interested in JVM bytecode and don't care about coding in Java specifically, perhaps using Scala or Clojure could help. You'll need some kind of letrec machinery.



回答7:

B: "Inventory created by the User is our last hope".
Y: "No, there is another."

If you abstract the references to a third party, you can control the relationship therein.

For example.

public class User
{
    private final String identifier;  // uniquely identifies this User instance.

    public User(final String myIdentifier)
    {
        identifier = myIdentifier;

        InventoryReferencer.registerBlammoUser(identifier); // Register the user with the Inventory referencer.
    }

    public Inventory getInventory()
    {
        return InventoryReferencer.getInventoryForUser(identifier);
    }
}

public interface Inventory // Bam!
{
    ... nothing special.
}

// Assuming that the Inventory only makes sence in the context of a User (i.e. User must own Inventory).
public class InventoryReferencer
{
    private static final Map<String, Inventory> referenceMap = new HashMap<String, Inventory>();

    private InventoryReferencer()
    {
        throw ... some exception - helps limit instantiation.
    }

    public static void registerBlammoUser(final String identifier)
    {
        InventoryBlammo blammo = new InventoryBlammo();
        referenceMap.add(indentifier, blammo);
    }

    public static void registerKapowUser(final String identifier)
    {
        InventoryBlammo kapow = new InventoryKapow();
        referenceMap.add(indentifier, kapow);
    }

    public static Inentory getInfentoryForUser(final String identifier)
    {
        return referenceMap.get(identifier);
    }
}

// Maybe package access constructors.
public class InventoryBlammo implements Inventory
{
    // a Blammo style inventory.
}

public class InventoryKapow implements Inventory
{
    // a Kapow style inventory.
}