3
\$\begingroup\$

Introduction

I have a hierarchically nested tree class (like SampleFolder, s.b.) that I want to serialize.

The problem arises with the collection of sub items like this one:

List<SampleFolder> subFolders;

I do not want to load the entire tree but only one folder at a time, when I deserialize! So instead of creating a list of the actual objects I need to use IDs. Naturally I am using a primitive based data type for reduced overhead:

List<Double> subFolders;

All good with serialization here. But in a more complex project handling all those Double-IDs, esp. within maps starts to become very confusing during development. As the project increases in complexity it happens that I would hand the wrong ID to a collection.

Map<Double, List<Double>> myAssociations; // what? which id goes where?

So to battle this issue of lost-type safety I created an interface Item and inner class Id (s.b.). This is the resulting usage (more in-depth example see SampleFolder, below):

Map<Id<Car>, List<Id<Wheel>>> myAssociations; // a lot more readable, isn't it?

Question

I couldn't find a best-practice for this issue, so if there is a better way to solve this "type-safety" issue please let me know. Even though this seems to work, I am wondering if this is good code in respect to the use of generics and if the implementation of hashCode() and equals() is correct. I am open for optimization suggestions and other critics.

code to review: Item with Id

interface Item {

    double getId();    

    class Id<T extends Item> implements Serializable {

        private double id;

        public Id(T item) {
            this.id = item.getId();
        }

        public Id(double id) {
            this.id = id;
        }

        public double asDouble() {
            return id;
        }

        @Override
        public boolean equals(Object obj) {
            if (this == obj) {
                return true;
            }

            if (!(obj instanceof Id)) {
                return false;
            }

            Id<?> cast = (Id<?>) obj;

            return id == cast.id;
        }

        @Override
        public int hashCode() {
            return Double.valueOf(id).hashCode();
        }

        public static <E extends Item> Id<E> from(double id) {
            return new Id<E>(id);
        }

        public static <E extends Item> Id<E> from(E enumItem) {
            return new Id<E>(enumItem);
        }

        @Override
        public String toString() {
            return String.valueOf(id);
        }
    }
}

Sample usage (class not part of the actual project; for illustration purposes only):

public class SampleFolder implements Item, Serializable {

    private final double id;
    private List<Id<SampleFolder>> subFolders;
    private List<Id<MoreItem>> moreItems;
    private List<Id<EvenMoreItem>> evenMoreItems;

    public SampleFolder() {
        id = new Date().getTime();
        subFolders = new ArrayList<>();
    }

    public SampleFolder(double id, List<Id<SampleFolder>> subFolders) {
        this.id = id;
        this.subFolders = subFolders;
    }

    @Override
    public double getId() {
        return id;
    }

    public List<Id<SampleFolder>> getSubFolders() {
        return subFolders;
    }

    public void setSubFolders(List<Id<SampleFolder>> subFolders) {
        this.subFolders = subFolders;
    }
}
\$\endgroup\$
3
  • 1
    \$\begingroup\$ Is this working code? Primitive types like double cannot be used as type parameters. Also double is not suitable for equals check and thus for being used neither as IDs nor as map keys. double id = new Date().getTime(): ???. \$\endgroup\$ Commented Jan 10, 2014 at 12:24
  • 1
    \$\begingroup\$ Here is what's wrong with using doubles as IDs even if you do not tamper with them: long l = Long.MAX_VALUE; double d1 = l; double d2 = l - 1; boolean wot = d1 == d2; System.out.println(wot); \$\endgroup\$ Commented Jan 10, 2014 at 12:36
  • \$\begingroup\$ @abuzittingillifirca Yes, the relevant code section compiles and seems to be working. The code fragments in the introduction are probably not. I mixed up Double and double. \$\endgroup\$
    – zsawyer
    Commented Jan 10, 2014 at 16:45

3 Answers 3

4
\$\begingroup\$

Do not use floating point numbers as IDs! Floating point is a horrible minefield of gotchas out there to sabotage your program in subtle ways: Losing precision, imprecise equality, NaNs, …. Using a double only defers, but not solves these issues.

For IDs, always use an integral type like long.

Now you've found a sufficiently elegant solution to represent typesafe IDs by using generics. There are a few other possibilities you might want to consider:

  • Lazy self-loading classes. Each instance has a boolean field initialized which defaults to false. In each public method you put in a if (!initialized) this.initialize() test to load the other fields – but only when they are needed.

    Usage example:

    class LazyThing {
        private boolean initialized = false;
        ... // state that is loaded lazily
    
        private void initialize() {
            ... // initialization code
            initialized = true;
        }
    
        public void frobnicate() {
            if (! initialized) initialize();
            ...
        }
    }
    

    The advantage here is that this pattern is completely invisible to the user.

  • A Deferred<T> wrapper which is basically a “promise” or “future” (or any other single-element monad-like container of your choice). A deferral wrapper represents an object that may or may not be fully loaded. You can get() the element to force loading. This is a bit more awkward to use but properly separates the lazy loading from the wrapped type. The full interface might look like:

    abstract class Deferred<T> {
      boolean initialized = false;
      T instance;
    
      abstract T initialize();  // sadly, Java does not have traits
    
      public T get() {
        if (! initialized) {
          instance = initialize();
          initialized = true;
        }
        return instance;
      }
    }
    

    Usage example:

    Deferred<Thing> foo = new Deferred<Thing>() {
      private long id;
    
      // anon classes can't have constructors :(
      Deferred<Thing> init(long id) {
        this.id = id;
        return this;
      }
    
      @Override
      Thing initialize() {
        ... // code to load the instance
      }
    }.init(1234);
    
    foo.get().frobnicate();
    

    This scheme is extremely flexible because it does not assume the existence or type of an ID.

\$\endgroup\$
3
  • \$\begingroup\$ The switch to long is a reasonable change since I am using new Date().getTime() to generate the id. But I do not really see those double related problems, as they would imho only occur if the double was tampered with. In this context I see it as an immutable. \$\endgroup\$
    – zsawyer
    Commented Jan 10, 2014 at 11:37
  • \$\begingroup\$ The lazing loading is exactly what I need next. But I will probably go with the Future since I don't want the UI thread to possibly lockup because there is hidden lazy loading going on. Great examples - I never really understood Future. Thanks \$\endgroup\$
    – zsawyer
    Commented Jan 10, 2014 at 11:41
  • 1
    \$\begingroup\$ (1) new Date().getTime() is a concurrency bug when you want to use that as an ID. You might want to use a synchronized factory instead. (2) My Deferred<T> is not a future, but it's quite similar. A Future adds ways to compose operations on the contained value, and has ways to manage failures. \$\endgroup\$
    – amon
    Commented Jan 10, 2014 at 11:46
2
\$\begingroup\$

Why don't you just use a GUID for an ID? It will serialize, and is (functionally) unique. You don't have the concurrency issue as with this date thing you're doing. The int or long will work but I think sooner or later you'll have a collision there too.

\$\endgroup\$
2
2
\$\begingroup\$

A better way to solve the type safety issue is to use the proxy pattern. You basically make a proxy, implementing the interface Car or Wheel, which is actually just a wrapper around the id. When a method is called on the proxy that needs the actual object, it can then load the object itself, and from then on simply delegate all method calls to the actual object.

As others have already pointed out, double is a horrible choice of type for an id.

\$\endgroup\$
1
  • \$\begingroup\$ I really like this approach and will probably start using this instead as it simplifies usage of the API by making the implementation transparent to the user. \$\endgroup\$
    – zsawyer
    Commented Aug 22, 2014 at 18:20

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.