Есть ли хороший способ сделать так, чтобы два неизменяемых объекта ссылались друг на друга?

Возьмите эти два класса Java:

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

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

Есть ли способ без использования отражения*, чтобы осуществить это ? Я на самом деле не ожидаю, что это так, но не помешает спросить.

Обновление: Поскольку в построении байт-кода есть два шага (1. выделение объекта, 2. вызов конструктора **), можно ли это (ab) использовать для этого с рукописным байт-кодом или специальным компилятором? Я говорю о выполнении шага 1 сначала для обоих объектов, затем шага 2 для обоих, используя ссылки из шага 1. Конечно, что-то подобное было бы довольно громоздким, и эта часть вопроса носит академический характер.

(* Поскольку отражение может вызвать проблемы с менеджером по безопасности)

(** Говорит о моих ограниченных знаниях)


person Bart van Heukelom    schedule 02.11.2011    source источник
comment
циклические зависимости не всегда являются хорошей идеей   -  person Bozho    schedule 02.11.2011
comment
Извините, не могли бы вы немного подробнее рассказать о проблеме. Я не понимаю, в чем проблема, поскольку у вас уже есть ссылка на другой объект?   -  person Anders    schedule 02.11.2011
comment
@ Проблема в том, что оба требуют ссылки на другой во время строительства, и такая ссылка доступна только после построения.   -  person Bart van Heukelom    schedule 02.11.2011


Ответы (7)


Это может работать только в том случае, если один из объектов создан другим. Например, вы можете изменить свой класс User на что-то вроде этого (при сохранении класса Inventory без изменений):

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

Однако вам нужно быть осторожным при доступе к объекту User в конструкторе Inventory: он еще не полностью инициализирован. Например, его поле inventory по-прежнему будет null!

Обновление объявления: я убедился, что метод манипулирования байт-кодом не работает. Я пробовал это с помощью Jasmin, и он всегда не загружался с VerifyError.

Углубившись в проблему, я нашел§ 4.10.2.4 Методы инициализации экземпляра и вновь созданные объекты. В этом разделе объясняется, как JVM обеспечивает передачу только инициализированных экземпляров объектов.

person Joachim Sauer    schedule 02.11.2011
comment
Есть идеи о трюках с байт-кодом (см. обновленный вопрос)? Хотя то, что я там описываю, наверное, невозможно. - person Bart van Heukelom; 02.11.2011
comment
@BartvanHeukelom: теоретически это может сработать. Я не знаю, действительно ли JVM нравится new без правильного invokespecial для вызова конструктора. Позвольте мне попробовать это ... - person Joachim Sauer; 02.11.2011
comment
Были внесены изменения в 1.4 (после 2-го издания JVSpec), которые давали определенный доступ для инициализации конечных полей вне класса. Точных изменений не помню. - person Tom Hawtin - tackline; 02.11.2011
comment
@TomHawtin-tackline изменения, внесенные в 1.4, касались назначения полей перед супер вызов конструктора, но не повлиял на спецификацию; это был верификатор HotSpot, который не соответствовал требованиям. - person Holger; 18.09.2019

Вы можете сделать это, если вам не нужно вводить один из объектов.

class User {
   private final Inventory inventory;
   User () {
       inventory = new Inventory(this);
   }
}
person Peter Lawrey    schedule 02.11.2011

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

Это лучшее, что я могу придумать. Может быть, есть лучший образец.

person G_H    schedule 02.11.2011

Немного педантично, но, строго говоря, нет необходимости создавать одно внутри другого, если вы не возражаете против небольшой косвенности. Они оба могут быть внутренними классами.

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

Или даже:

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; }
    ...
}
person Community    schedule 02.11.2011
comment
Я нахожу это довольно хорошим решением. Отношения между пользователем и инвентарем — это то, что имеет свою собственную цель и поэтому заслуживает отдельного класса. Ссылка на другой объект может также означать, что вам нужно пройти дополнительный путь, чтобы спросить кого-то, на кого ссылается экземпляр. - person SpaceTrucker; 19.11.2013
comment
Хорошая мысль, однако проблема все еще остается, поскольку ваш третий неизменяемый объект экранируется в своем конструкторе. - person Alexandre Dupriez; 21.02.2014
comment
@AlexandreDupriez Какой? семантика полей final и т. д. owner и inventory не имеют значения. Они покрыты теми из BadlyNamedClass. (String, который обычно реализуется с использованием char[], является каноническим примером этого.) - person Tom Hawtin - tackline; 21.02.2014
comment
@TomHawtin-tackline: я имею в виду this в конструкторе BadlyNamedClass. Вы можете возразить, что знаете, что User и Inventory не сделают ничего вредного с неэкземплярами this BadlyNamedClass, но это потому, что у вас есть полный контроль над реализацией. Что было бы, если бы User и Inventoy были клиентскими объектами, применяющими этот шаблон? Что вы думаете? - person Alexandre Dupriez; 25.02.2014
comment
@AlexandreDupriez Они могут раскрыть себя через переданные аргументы или глобальные значения в той или иной форме, но это ортогональная проблема. Это были бы сломанные классы, что бы с ними ни делал любой другой код. - person Tom Hawtin - tackline; 25.02.2014
comment
@TomHawtin-tackline: Эрф... Не уверен, что хорошо понимаю. Но да, на самом деле это другой вопрос. - person Alexandre Dupriez; 25.02.2014

Это одно "решение", хотя потеря одного final неудобна.

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;
   }
}
person Bart van Heukelom    schedule 02.11.2011
comment
Верно. У вас больше нет двух неизменяемых объектов, о которых в названии вашего вопроса говорится как о предпосылке;) - person aioobe; 02.11.2011
comment
@aioobe Действительно, поэтому я отредактировал его и заключил решение в кавычки. Однако он по-прежнему неизменен для внешнего мира, потому что нет никакого сеттера, который они могли бы использовать. - person Bart van Heukelom; 02.11.2011
comment
Правильно, если вы не считаете другие классы в том же пакете внешним миром: P - person aioobe; 02.11.2011
comment
@aioobe Можно использовать довольно сомнительный метод проверки вызывающего абонента с помощью трассировки нового исключения. Однако это связало бы класс с фабрикой, и в этом случае класс мог бы также быть собственной фабрикой. Несколько лучшим приемом было бы объявить установщик устаревшим и подавить предупреждение в том месте, где его можно использовать. - person Bart van Heukelom; 02.11.2011

Если вас интересует только байт-код JVM и вас не интересует кодирование конкретно на Java, возможно, вам поможет использование Scala или Clojure. Вам понадобится какое-то letrec оборудование.

person Basile Starynkevitch    schedule 02.11.2011

Б: "Инвентарь, созданный пользователем, — наша последняя надежда".
И: "Нет, есть еще один".

Если вы абстрагируете ссылки на третью сторону, вы можете контролировать отношения в них.

Например.

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.
}
person DwB    schedule 02.11.2011