views:

245

answers:

5

I would like to find a way to take the object specific routine below and abstract it into a method that you can pass a class, list, and fieldname to get back a Map. If I could get a general pointer on the pattern used or , etc that could get me started in the right direction.

  Map<String,Role> mapped_roles = new HashMap<String,Role>();
    List<Role> p_roles = (List<Role>) c.list();
    for (Role el :  p_roles) {
        mapped_roles.put(el.getName(), el);
    }

to this? (Pseudo code)

  Map<String,?> MapMe(Class clz, Collection list, String methodName)
  Map<String,?> map = new HashMap<String,?>();
    for (clz el :  list) {
        map.put(el.methodName(), el);
    }

is it possible?

+1  A: 

Using reflection and generics:

public static <T> Map<String, T> MapMe(Class<T> clz, Collection<T> list, String methodName)
throws Exception{
  Map<String, T> map = new HashMap<String, T>();
  Method method = clz.getMethod(methodName);
  for (T el : list){
    map.put((String)method.invoke(el), el);
  }
  return map;
}

In your documentation, make sure you mention that the return type of the method must be a String. Otherwise, it will throw a ClassCastException when it tries to cast the return value.

Michael Angstadt
+1 At times like these I wish there was a "favorite answer" option.
Amir Rachum
perfect, this makes sense. Thanks
ebt
-1 - this might work on a good day, but it is fragile ... and probably a lot more expensive than the original code.
Stephen C
There's no sane reason to do it this way, just use a Function instead of reflection
Jorn
@Stephen C: What do you mean it's "fragile"? It works perfectly, as long as the programmer passes in the right method name.
Michael Angstadt
@mangst - it breaks (with no compilation errors) if the programmer passes the wrong name or the method's signature changes, or the list signature changes ...
Stephen C
+1  A: 

Here's what I would do. I am not entirely sure if I am handling generics right, but oh well:

public <T> Map<String, T> mapMe(Collection<T> list) {
   Map<String, T> map = new HashMap<String, T>();
   for (T el : list) {
       map.put(el.toString(), el);
   }   
   return map;
}

Just pass a Collection to it, and have your classes implement toString() to return the name. Polymorphism will take care of it.

quantumSoup
I like the idea of using this vs reflection, Ill have to implement both methods. Thanks
ebt
Since this only works with `toString()` as the function for getting the index key, it's not a very good general solution and obviously doesn't work for any arbitrary property of an object. The reflection solution is even worse and breaks without compiler errors on refactoring.
ColinD
+2  A: 

Avoid reflection like the plague.

Unfortunately, Java's syntax for this is verbose. (A recent JDK7 proposal would make it much more consise.)

interface ToString<T> {
    String toString(T obj);
}

public static <T> Map<String,T> stringIndexOf(
    Iterable<T> things,
    ToString<T> toString
) {
    Map<String,T> map = new HashMap<String,T>();
    for (T thing : things) {
        map.put(toString.toString(thing), thing);
    }
    return map;
}

Currently call as:

Map<String,Thing> map = stringIndexOf(
    things,
    new ToString<Thing>() { public String toString(Thing thing) {
        return thing.getSomething();
    }
);

In JDK7, it may be something like:

Map<String,Thing> map = stringIndexOf(
    things,
    { thing -> thing.getSomething(); }
);

(Might need a yield in there.)

Tom Hawtin - tackline
verbose and about as clear as mud :), would requiring a toString method be more pragmatic?
ebt
@ebt I don't think there's anything wrong with the clarity. Well names could be better chosen. / I don't understand your comment about a `toString` method. Do you mean `Object.toString` on `T` - that wouldn't be very useful.
Tom Hawtin - tackline
ok, took me several iterations to piece it together. Implementing toString requires that your input objects implement toString instead of assuming that the toString method exists and throws an error at runtime (although you could add throws on the method right?)
ebt
never mind, idiotic question. toString is inherited from Object... palm -> face.
ebt
+2  A: 

using Google Collections:

Map<String,Role> mappedRoles = Maps.uniqueIndex(yourList, Functions.toStringFunction());

Or, if you want to supply your own method that makes a String out of the object:

Map<String,Role> mappedRoles = Maps.uniqueIndex(yourList, new Function<Role,String>() {
  public String apply(Role from) {
    return from.getName(); // or something else
  }});
Jorn
I'd recommend linking to Guava rather than Google Collections, as Guava has officially superseded it.
ColinD
I don't *think* the questioner wants `Object.toString`. Or at least the question seems to imply not wanting it.
Tom Hawtin - tackline
@Tom Then he only has to supply his own function instead of the prefab toString one
Jorn
@Jorn Nice to see you've added a solution to your answer that answers the question! With a big dependency.
Tom Hawtin - tackline
@Tom What dependency do you mean?
Jorn
@Jorn Dependency on Google Collections.
Tom Hawtin - tackline
@Tom I feel like Google Collections/Guava is a library that most Java projects could benefit from. Especially if the alternative is reimplementing its functionality from scratch and in a more limiting fashion (like your answer) or doing something... unsafe... like the reflection answer.
ColinD
@Tom That dependency was already there in the first part of the answer. And Colin explains perfectly why it's an asset to have the dependency instead of a cost like you're implying
Jorn
A: 

If you're sure that each object in the List will have a unique index, use Guava with Jorn's suggestion of Maps.uniqueIndex.

If, on the other hand, more than one object may have the same value for the index field (which, while not true for your specific example perhaps, is true in many use cases for this sort of thing), the more general way do this indexing is to use Multimaps.index(Iterable<V> values, Function<? super V,K> keyFunction) to create an ImmutableListMultimap<K,V> that maps each key to one or more matching values.

Here's an example that uses a custom Function that creates an index on a specific property of an object:

List<Foo> foos = ...
ImmutableListMultimap<String, Foo> index = Multimaps.index(foos,
    new Function<Foo, String>() {
      public String apply(Foo input) {
        return input.getBar();
      }
    });

// iterate over all Foos that have "baz" as their Bar property
for (Foo foo : index.get("baz")) { ... }
ColinD