Как моделировать циклы между неизменяемыми экземплярами классов? - PullRequest
5 голосов
/ 29 января 2011

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

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

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

неизменности Этот класс из внешнего мира определенно должен быть неизменным. Внутреннее значение должно быть постоянным для проверки на равенство.

Ответы [ 3 ]

8 голосов
/ 29 января 2011

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

Вот почему сборщики так хороши для неизменяемых - они позволяют изменчивости во время построения получить все, что нужно, прежде чем вы "заморозите" его.В этом случае, я думаю, вам нужен построитель друзей, который поддерживает создание циклов.

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)
  }
}
0 голосов
/ 29 января 2011

Правильный способ моделирования цикла - График .И одного комментария к строке исходного кода может быть достаточно для обеспечения неизменности: « не может коснуться этого ».

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

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

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

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

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

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);
    }
}
0 голосов
/ 29 января 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" );
   }
}
...