HiveBrain v1.2.0
Get Started
← Back to all entries
patternjavaMinor

Forcing type-safe IDs for use with Collections and Maps

Submitted by: @import:stackexchange-codereview··
0
Viewed 0 times
withforcingidsmapstypeforsafecollectionsanduse

Problem

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 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 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> 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, List>> 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 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;

Solution

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 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 {
  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 foo = new Deferred() {
  private long id;

  // anon classes can't have constructors :(
  Deferred 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.

Code Snippets

class LazyThing {
    private boolean initialized = false;
    ... // state that is loaded lazily

    private void initialize() {
        ... // initialization code
        initialized = true;
    }

    public void frobnicate() {
        if (! initialized) initialize();
        ...
    }
}
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;
  }
}
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();

Context

StackExchange Code Review Q#38988, answer score: 4

Revisions (0)

No revisions yet.