Skip to main content

A type-safe map-like class

Posted by fabriziogiudici on January 11, 2010 at 5:58 AM PST

In the forceTen GeoCoding API there's a simple map-like class named FactSheet, that contains a few attributes about a geographic entity (such as the population count, or the official elevation of the place). It's the typical scenario where you'd use a Map or a map-like class, because data items can be there or not for some entities and/or for different service providers.
This class is used with something like:

FactSheet factSheet = ...



if (factSheet.contains("population"))

  {

    long population = (long)factSheet.get("population");

  }

where a logical improvement is to define a String constant POPULATION to use as the key. The little annoyance is the required (long) cast, which also jeopardize the IDE auto-completion code (the “assign to variable” shortcut would generate an Object). It's easy to make an error here: is POPULATION an int or a long? ELEVATION will be a float or a double, or an int?

This can be easily solved by replacing the String POPULATION with a simple generified Key:

@Immutable

public final static class Key<T> implements Comparable<Key<T>>

  {

    @Nonnull

    private final String name;



    protected Key (final @Nonnull String name)

      {

        this.name = name;

      }



    @Override

    public int compareTo (final @Nonnull Key<T> other) { ... }



    @Override

    public boolean equals (final @CheckForNull Object object) { ... }



    @Override

    public int hashCode() { ... }

  }

Note that there's no apparent use of the generic type T - I mean that there are no variables, parameters or return types declared as T, which is the common use of a generic. Instead, in this case it is used to mark the type of the data item that it associated with the key. In this way, FactSheet can implement the get() method in a type-safe fashion, that is you can write:

long population = factSheet.get(POPULATION);

Sector bounds = factSheet.get(BOUNDS);

TimeZone timeZone = factSheet.get(TIMEZONE);

This is the relevant part of FactSheet:

@Immutable

public final static class FactSheet

  {

    public final static Key<Long> POPULATION = new Key<Long>("population");

    public final static Key<Integer> ELEVATION = new Key<Integer>("elevation");

    public final static Key<TimeZone> TIMEZONE = new Key<TimeZone>("timezone");

    public final static Key<Sector> BOUNDS = new Key<Sector>("bounds");



    private final SortedMap<Key<?>, Object> itemMap = new TreeMap<Key<?>, Object>();



    public FactSheet (final @Nonnull Map<Key<?>, Object> itemMap)

      {

        this.itemMap.putAll(itemMap);

      }



    @Nonnull

    public <T> T get (final @Nonnull Key<T> key)

      throws NotFoundException

      {

        return NotFoundException.throwWhenNull((T)itemMap.get(key), "Not found: " + key);

      }



    public boolean contains (final @Nonnull Key<?> item)

      {

        return itemMap.containsKey(item);

      }

  }

Related Topics >>

Comments

Type safety

Hi Fabrizio,

thanks for your informative post. I like the idea in general, but I think there is also a drawback.

Opposed to "real" objects it is not clear, which attributes are allowed within a given FactSheet instance. If for instance there are two methods, which both have a FactSheet as parameter, the caller doesn't know which attributes she is expected to put into the FactSheet parameter for each method, and which ones she mustn't put into. The same is true for return values, the user doesn't know which attributes can be retrieved from a given FactSheet.

Thinking about this problem I came to a solution which I described in my web log a while ago:

http://musingsofaprogrammingaddict.blogspot.com/2009/11/generic-and-still-type-safe-dto-part-2.html

The approach presented there doesn't come with the disadvantage described above, OTOH it is a bit wordier than your's.

Happily looking foward to your feedback,

Gunnar