Как моделировать циклы между неизменяемыми экземплярами класса?

Неизменяемые классы — это здорово, но есть одна большая проблема, которую я не могу придумать, — это циклы.

class Friend {
   Set<Friend> friends();
}

Как кто-то моделирует Меня, имеющего Тебя в качестве друга, который, в свою очередь, имеет меня в качестве Друга?

НЕИЗМЕНИМОСТЬ Этот класс из внешнего мира обязательно должен быть неизменяемым. Внутреннее значение должно быть постоянным для целей проверки на равенство.


person mP.    schedule 29.01.2011    source источник
comment
Вы заранее знаете все двунаправленные отношения, которые вам понадобятся, или вы добавляете их по одному? В последнем случае действительно невозможно получить гарантию неизменности, поскольку вы действительно меняете объекты.   -  person templatetypedef    schedule 29.01.2011
comment
В данном случае - нет, я пытаюсь сделать это просто.   -  person mP.    schedule 29.01.2011


Ответы (3)


[[[ Редактировать: добавлен код для демонстрации полностью неизменной концепции ]]]

Вот почему строители так хороши для неизменяемых — они позволяют изменять во время построения, чтобы все было установлено, прежде чем вы «заморозите» это. В этом случае, я думаю, вам нужен Friend Builder, который поддерживает создание циклов.

final FriendBuilder john = new FriendBuilder().setName("john");
final FriendBuilder mary = new FriendBuilder().setName("mary");
final FriendBuilder susan = new FriendBuilder().setName("susan");
john
  .likes(mary)
  .likes(susan);
mary
   .likes(susan)
   .likes(john);
susan
   .likes(john);

// okay lets build the immutable Friends
Map<Friend> friends = FriendsBuilder.createCircleOfFriends(john, mary, susan);
Friend immutableJohn = friends.get("john");

Изменить: ниже добавлен неизменяемый пример для демонстрации подхода:

  • В комментариях была дискуссия о том, возможна ли неизменяемая версия.

  • Поля являются окончательными и неизменяемыми. В конструкторе используется модифицируемый набор, но после построения сохраняется только немодифицируемая ссылка.

  • У меня есть другая версия, которая использует Guava ImmutableSet для действительно неизменного набора, а не неизменяемую оболочку JDK. Он работает так же, но использует хороший конструктор наборов Guava.

Код:

import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.IdentityHashMap;
import java.util.Map;
import java.util.Set;

/**
 * Note: potentially cycle graph - be careful of deep equals/hashCode/toString/etc.
 * Immutable
 */
public class Friend {

    public static class Builder {

        private final String name;
        private final Set<Builder> friends =
            new HashSet<Builder>();

        Builder(final String name) {
            this.name = name;
        }

        public String getName() {
            return name;
        }

        public Set<Builder> getFriends() {
            return friends;
        }

        void likes(final Builder... newFriends) {
            for (final Builder newFriend : newFriends)
            friends.add(newFriend);
        }

        public Map<String, Friend> createCircleOfFriends() {
            final IdentityHashMap<Builder, Friend> existing =
                new IdentityHashMap<Builder, Friend>();

            // Creating one friend creates the graph
            new Friend(this, existing);
            // after the call existingNodes contains all the nodes in the graph

            // Create map of the all nodes
            final Map<String, Friend> map =
                new HashMap<String, Friend>(existing.size(), 1f);
            for (final Friend current : existing.values()) {
                map.put(current.getName(), current);
            }

            return map;
        }
    }

    final String name;
    final Set<Friend> friends;

    private Friend(
            final Builder builder,
            final Map<Builder, Friend> existingNodes) {
        this.name = builder.getName();

        existingNodes.put(builder, this);

        final IdentityHashMap<Friend, Friend> friends =
            new IdentityHashMap<Friend, Friend>();
        for (final Builder current : builder.getFriends()) {
            Friend immutableCurrent = existingNodes.get(current);
            if (immutableCurrent == null) {
                immutableCurrent =
                    new Friend(current, existingNodes);
            }
            friends.put(immutableCurrent, immutableCurrent);
        }

        this.friends = Collections.unmodifiableSet(friends.keySet());
    }

    public String getName() {
        return name;
    }

    public Set<Friend> getFriends() {
        return friends;
    }


    /** Create string - prints links, but does not traverse them */
    @Override
    public String toString() {
        final StringBuffer sb = new StringBuffer();
        sb.append("Friend ").append(System.identityHashCode(this)).append(" {\n");
        sb.append("  name = ").append(getName()).append("\n");
        sb.append("  links = {").append("\n");
        for (final Friend friend : getFriends()) {
            sb
            .append("     ")
            .append(friend.getName())
            .append(" (")
            .append(System.identityHashCode(friend))
            .append(")\n");
        }
        sb.append("  }\n");
        sb.append("}");
        return sb.toString();
    }

    public static void main(final String[] args) {
        final Friend.Builder john = new Friend.Builder("john");
        final Friend.Builder mary = new Friend.Builder("mary");
        final Friend.Builder susan = new Friend.Builder("susan");
        john
          .likes(mary, susan);
        mary
           .likes(susan, john);
        susan
           .likes(john);

        // okay lets build the immutable Friends
        final Map<String, Friend> friends = john.createCircleOfFriends();

        for(final Friend friend : friends.values()) {
            System.out.println(friend);
        }

        final Friend immutableJohn = friends.get("john");
    }
}

Выход:

Node 11423854 {
  value = john
  links = {
     susan (19537476)
     mary (2704014)
  }
}
Node 2704014 {
  value = mary
  links = {
     susan (19537476)
     john (11423854)
  }
}
Node 19537476 {
  value = susan
  links = {
     john (11423854)
  }
}
person Bert F    schedule 29.01.2011
comment
Было бы полезно отметить, что шаблон компоновщика просто скрывает тот факт, что вы выполняете инициализацию после построения. Класс Friend не мог иметь окончательной структуры для хранения друзей. - person Konstantin Komissarchik; 29.01.2011
comment
@Konstantin Komissarchik Вы можете сделать это с несколькими друзьями в стеке (диаметр графа в лучшем случае, все в худшем) и при этом сохранить неизменность. - person Tom Hawtin - tackline; 29.01.2011
comment
@ Том, я не куплюсь на это. Friend должен иметь что-то нефинальное, чтобы в конечном итоге получить два объекта Friend, ссылающихся друг на друга. Окончательная ссылка на прокси (он же билдер), который внутренне не является окончательным, не учитывается. Это просто еще один способ отложенной инициализации. - person Konstantin Komissarchik; 29.01.2011
comment
@Konstantin Komissarchik class A { final B b; A() { this.b = new B(this); }} class B { final A a; B(A a) { this.a = a; }} Думаю, хорошо известные вещи в функциональных языках. - person Tom Hawtin - tackline; 29.01.2011
comment
@Tom - можно ли это обобщить на пример @mP? - person Stephen C; 29.01.2011
comment
@Top - Хорошо, этот ограниченный случай работает, но я очень надеюсь, что на практике никто не пишет такой код. - person Konstantin Komissarchik; 29.01.2011
comment
@Konstantin - Возможно, ты прав в целом насчет could not have a fully final structure. Но я думаю, что вы делаете предположения о том, как хранятся отношения (т.е. как прямые ссылки). Если отношения хранятся в виде индексов, имен или других средств перенаправления, то класс, безусловно, может быть заморожен. Стоит ли использовать косвенность, чтобы получить полностью окончательную структуру? Вероятно, нет, но я не собираюсь говорить, что у вас не может быть окончательных полей, если есть вынуждающий фактор, делающий их достаточно важными. - person Bert F; 29.01.2011
comment
@Bert - Да, вы можете использовать индекс/имя. Вы также должны убедиться, что все, что содержит сопоставление, является неизменным после инициализации. В конце концов, если отображение может измениться, эффективное дерево объектов на самом деле не является неизменным. Вы также должны быть осторожны в течение всего процесса инициализации, чтобы никогда не разрешать ссылку, поскольку вы можете наблюдать изменчивость из-за того, что сопоставление не инициализируется в какой-то момент и инициализируется позже. - person Konstantin Komissarchik; 29.01.2011
comment
@Konstantin - согласен - индексируемая структура также должна быть неизменной. Я представлял что-то вроде одного из неизменяемых классов Guava. Поскольку метод createCircleOfFriends() создает закрытый граф друзей, я легко могу увидеть создание этой структуры. Метод create — это момент времени, когда все замораживается. И я также согласен с тем, что эта структура доступна только для записи во время создания, но я не понимаю, почему вы должны читать ее, поскольку цель состоит в том, чтобы ее создать. - person Bert F; 29.01.2011
comment
@bert Это может быть единственный способ сделать это. Поля не являются окончательными, но снаружи экземпляры Friend выглядят неизменными. Оставлю это открытым, чтобы собрать больше мнений. Я полагаю, что возможность интернирования + поиска в конструкторе - лучший способ иметь одноэлементные экземпляры друзей. - person mP.; 29.01.2011
comment
@Stephen C Конечно, но этот комментарий, вероятно, немного мал. - person Tom Hawtin - tackline; 29.01.2011
comment
@Stephen C - см. редактирование для обобщенного примера того, что говорил Том. - person Bert F; 01.02.2011
comment
@Konstantin - см. редактирование для класса Friend с полностью окончательной структурой - person Bert F; 01.02.2011
comment
@Bert F - я не это имел в виду. Я не спрашиваю о подходе Builder. Я спрашиваю об обобщении подхода в комментарии Тома Хоутина... в котором все делается в конструкторах. - person Stephen C; 01.02.2011
comment
@Stephen C - я видел код Тома как демонстрацию того, что 2 неизменяемых класса могут ссылаться друг на друга без проблемы с курицей и яйцом - путем создания другого экземпляра в середине построения 1-го. Конструктор неизменяемого класса, приведенный выше, обобщает эту идею более чем для двух узлов: он создает больше узлов в середине построения первого. Построитель - это просто удобная оболочка для захвата желаемого графика/отношений, поэтому он не жестко запрограммирован, как в примере Тома. - person Bert F; 01.02.2011

Правильный способ моделирования цикла — с помощью Graph. И одного комментария в исходном коде может быть достаточно, чтобы обеспечить неизменяемость: «это нельзя трогать".

Какого неизменного принуждения вы ищете? Вы хотите, чтобы велоцираптор появлялся каждый раз при изменении неизменного набора? Разница между mutable и inmutable просто условность. Однако биты в ОЗУ можно легко изменить с помощью Reflection API вы можете нарушить любые соглашения об инкапсуляции и сокрытии данных.

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

А чтобы неизменяемое свойство имело смысл, нужно сделать Friend interface, имея один реализующий класс: InmutableFriend, а построение объекта должно полностью происходить внутри конструктора.

Затем, поскольку граф содержит циклы, перед созданием окончательных неизменяемых экземпляров необходимо сохранить узлы графа в некоторой изменяемой временной структуре. Вам также необходимо вернуть ссылку unmodifiedSet в методе InmutableFriend.friends().

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

interface Friend {
    public Set<Friend> friends();
}

class MutableFriend {
    private Set<MutableFriend> relations = new HashSet<MutableFriend>();

    void connect(MutableFriend otherFiend) {
        if (!relations.contains(otherFriend)) {
            relations.add(otherFiend);
            otherFriend.connect(this);
        }
    }

    Friend freeze() {
        Map<MutableFriend, InmutableFriend> table = ...;

        /*
         * FIXME: Implement a Breadth-first search to clone the graph,
         * using this node as the starting point.
         *
         * TODO: If the graph is not connected this won't work.
         *
         */
    }
}

class InmutableFriend() implements Friend {
    private Set<Friend> connections;

    public Set<Friend> friends() {
        return connections;
    }

    public InmutableFriend(Set<Friend> connections) {
        // Can't touch this.
        this.connections = Collections.unmodifiableSet(connections);
    }
}
person vz0    schedule 29.01.2011
comment
Шаблон заморозки действительно уродлив, имхо, я начинаю думать, что Friend будет выглядеть неизменным, но глубоко внутри конструктор добавит к нему что-то, а затем заморозит, после чего его можно будет отдать внешнему миру ... - person mP.; 29.01.2011
comment
@mP: Заморозка — это не шаблон, это алгоритм под названием Deep Copy: en.wikipedia.org /wiki/Object_copy#Deep_copy - person vz0; 29.01.2011
comment
извини, приятель, я неправильно понял MutableField = Field :) игнорируй мой оригинальный комментарий. - person mP.; 30.01.2011
comment
@mP: В любом случае, если программист хочет сломать неизменяемый класс, он может сделать это с помощью Reflection API. - person vz0; 31.01.2011

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

private Object something;

public void init( final Object something )
{
   if( this.something != null )
   {
       throw new IllegalStateException();
   }

   this.something = something
}

Поле участника «что-то» не является окончательным, но его также нельзя задать более одного раза.

Более сложный вариант, основанный на обсуждении в комментариях...

private boolean initialized;
private Object a;
private Object b;

public void init( final Object a, final Object b )
{
   if( this.initialized )
   {
       throw new IllegalStateException();
   }

   this.initialized = true;
   this.a = a;
   this.b = b;
}

public Object getA()
{
   assertInitialized();
   return this.a;
}

public Object getB()
{
   assertInitialized();
   return this.b;
}

private void assertInitialized()
{
   if( this.initialized )
   {
       throw new IllegalStateException( "not initialized" );
   }
}
person Konstantin Komissarchik    schedule 29.01.2011
comment
Я думаю, вы имели в виду: если (что-то == ноль) - person Mnementh; 29.01.2011
comment
На самом деле я имел в виду if( this.something != null ). Оператор if предназначен для обнаружения попытки установки чего-либо более одного раза. - person Konstantin Komissarchik; 29.01.2011
comment
Части, которые меня беспокоят в этом, это (1) этот частный метод инициализации может по ошибке никогда не вызываться, и (2) этот код не будет работать, если null если допустимое значение для something. Если null является допустимым значением something, то инициализацию something необходимо отслеживать в отдельном флаге. - person Bert F; 29.01.2011
comment
@Bert - (1) легко решается, если другие методы в классе терпят неудачу, если класс не инициализирован; (2) вы уже указали решение. В более сложных версиях этого шаблона я обычно вижу инициализированный логический элемент, отслеживающий это состояние, поскольку обычно инициализируется более одного поля. - person Konstantin Komissarchik; 29.01.2011
comment
@Konstantin - (1) Это абсолютно адресно, за счет большей сложности - выполнение проверки "я-я-полностью-инициализировано" в каждом методе. Это одна из причин, по которой я стараюсь избегать этого частично инициализированного шаблона, когда это возможно. (2) Я подумал, что стоит указать на нулевую проблему и решение, чтобы люди, использующие этот шаблон на практике, рассмотрели этот случай. - person Bert F; 29.01.2011
comment
Ваш общедоступный метод init() sux, потому что его можно вызвать дважды в коде на одном и том же экземпляре, даже если во время выполнения произойдет сбой. - person mP.; 29.01.2011
comment
@mP - Любое решение вашего вопроса будет неоптимальным. Нет оптимального решения. Преимущество моего решения в том, что оно является самым простым для реализации и понимания. Плюс после инициализации на объекты ссылаются напрямую. Решение сборки/прокси не имеет проблемы с инициализатором после построения, но вам нужно написать прокси, а затем всегда ссылаться на объекты через него. Более сложный. Сложнее доказать свою правоту. Плюсы и минусы в обоих решениях. На практике я чаще вижу шаблон инициализатора, чем шаблон компоновщика/прокси. Бери оттуда, что хочешь. - person Konstantin Komissarchik; 29.01.2011
comment
@KK Вы совершенно правы, но есть способы, которые вызывают меньше проблем, чем другие, при достижении конечной цели. - person mP.; 31.01.2011