How to model cycles between immutable class instances?

柔情痞子 提交于 2019-12-09 11:47:37

问题


Immutable classes are great but there is one big problem i cant think of a sensible way to solve - cycles.

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

How does one model Me having You as a friend who in turn has me as a Friend back ?

IMMUTABILITY This class from the outside world should definitely be immutable. The value held internally should be constant for the purposes of equality checks.


回答1:


[[[ Edit: Added code to demonstrate fully immutable concept ]]]

That's why builders are so nice for immutables - they allow mutability during construction to get everything set before you "freeze" it. In this case, I guess you need a Friend builder that supports creating cycles.

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");

Edit: Added immutable example below to demonstrate approach:

  • There was some discussion in the comments about whether an immutable version was possible.

  • Fields are final and immutable. A modifiable set is used in the constructor, but it only the unmodifiable reference is kept after construction.

  • I have another version that uses Guava ImmutableSet for a truly immutable set rather than JDK's unmodifiable wrapper. It works the same, but uses Guava's nice set builder.

Code:

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

Output:

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



回答2:


The correct way to model a cycle is with a Graph. And a single source code line comment can be enough to enforce inmutability: "can't touch this".

What kind of inmutable enforcement are you looking for? Do you want a a velociraptor to appear whenever you modify the inmutable Set? The difference between mutable and inmutable is just a convention. However, the bits on the RAM can be easily modified and with the Reflection API you can break any encapsulation and data hiding conventions.

Ignoring the velociraptor for a moment, Java does not support an inmutable type. As a workaround, you need to model a datatype that behaves like one.

And for the inmutable property to make sense you need to make Friend an interface, having one implementing class: InmutableFriend, and the construction of the object should fully happen inside the constructor.

Then, since the graph contains cycles, before creating the final inmutable instances you need to store the graph nodes in some mutable temporary structure. You also need to return an unmodifiableSet on the InmutableFriend.friends() method.

Finally, to clone the graph you need to implement a Deep-copy algorithm like Breadth-first search on the Mutable graph. One question though is what happens when the graph is not fully connected.

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



回答3:


Immutability doesn't need to be compiler-enforced to be valid architecturaly. You can have a legitimate immutable object that takes post-construction initialization parameters. For instance...

private Object something;

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

   this.something = something
}

The member field "something" isn't final, but it cannot be set more than once either.

A more complex variant based on discussion in comments...

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


来源:https://stackoverflow.com/questions/4834513/how-to-model-cycles-between-immutable-class-instances

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!