10

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?

James P.
  • 19,313
  • 27
  • 97
  • 155
ebt
  • 1,358
  • 1
  • 13
  • 18
  • @Aircule: One could use the old functor hack just fine here. – Anon. Jul 07 '10 at 23:41
  • I don't get it. You want to have a list of `x` thing and convert it to a map where the key is the thing name and the value is the thing? – OscarRyz Jul 07 '10 at 23:41
  • right, the key is a string value returned from the method passed (object Person has method name(), persons name becomes the key), the value is the object itself. Its much easier to compare strings than having to have an object on hand for comparison. – ebt Jul 07 '10 at 23:50

6 Answers6

33

Using Guava (formerly 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
  }});
Pang
  • 9,564
  • 146
  • 81
  • 122
Jorn
  • 20,612
  • 18
  • 79
  • 126
  • 3
    I'd recommend linking to Guava rather than Google Collections, as Guava has officially superseded it. – ColinD Jul 08 '10 at 01:19
  • I don't *think* the questioner wants `Object.toString`. Or at least the question seems to imply not wanting it. – Tom Hawtin - tackline Jul 08 '10 at 09:45
  • 1
    @Tom Then he only has to supply his own function instead of the prefab toString one – Jorn Jul 08 '10 at 10:21
  • @Jorn Nice to see you've added a solution to your answer that answers the question! With a big dependency. – Tom Hawtin - tackline Jul 08 '10 at 16:57
  • @Tom What dependency do you mean? – Jorn Jul 08 '10 at 22:14
  • @Jorn Dependency on Google Collections. – Tom Hawtin - tackline Jul 08 '10 at 23:17
  • 3
    @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 Jul 09 '10 at 00:27
  • 1
    @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 Jul 09 '10 at 15:04
  • Google Guava project is moved [GitHub](https://github.com/google/guava). – gilchris Apr 07 '15 at 12:54
4

Java 8 streams and method references make this so easy you don't need a helper method for it.

Map<String, Foo> map = listOfFoos.stream()
    .collect(Collectors.toMap(Foo::getName, Function.identity()));

If there may be duplicate keys, you can aggregate the values with the toMap overload that takes a value merge function, or you can use groupingBy to collect into a list:

//taken right from the Collectors javadoc
Map<Department, List<Employee>> byDept = employees.stream()
    .collect(Collectors.groupingBy(Employee::getDepartment));

As shown above, none of this is specific to String -- you can create an index on any type.

If you have a lot of objects to process and/or your indexing function is expensive, you can go parallel by using Collection.parallelStream() or stream().parallel() (they do the same thing). In that case you might use toConcurrentMap or groupingByConcurrent, as they allow the stream implementation to just blast elements into a ConcurrentMap instead of making separate maps for each thread and then merging them.

If you don't want to commit to Foo::getName (or any specific method) at the call site, you can use a Function passed in by a caller, stored in a field, etc.. Whoever actually creates the Function can still take advantage of method reference or lambda syntax.

Jeffrey Bosboom
  • 13,313
  • 16
  • 79
  • 92
4

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
  • 27,197
  • 9
  • 43
  • 57
  • I like the idea of using this vs reflection, Ill have to implement both methods. Thanks – ebt Jul 07 '10 at 23:59
  • 2
    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 Jul 08 '10 at 01:42
  • 1
    Well in that case you can use public static Map. with BaseModel containing for example the field id and getId() ... Obviously this will only works if the Set of object you are trying to convert into map is of type BaseModel – Alexis May 31 '16 at 11:07
3

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
  • 145,806
  • 30
  • 211
  • 305
  • 1
    verbose and about as clear as mud :), would requiring a toString method be more pragmatic? – ebt Jul 08 '10 at 00:32
  • 1
    @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 Jul 08 '10 at 09:43
  • 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 Jul 08 '10 at 19:27
  • never mind, idiotic question. toString is inherited from Object... palm -> face. – ebt Jul 16 '10 at 20:15
  • +1 for avoid reflection – hovanessyan Sep 04 '14 at 13:43
1

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
  • 34,873
  • 17
  • 75
  • 109
  • +1 At times like these I wish there was a "favorite answer" option. – Amir Rachum Jul 07 '10 at 23:55
  • 7
    -1 - this might work on a good day, but it is fragile ... and probably a lot more expensive than the original code. – Stephen C Jul 08 '10 at 00:28
  • 5
    There's no sane reason to do it this way, just use a Function instead of reflection – Jorn Jul 08 '10 at 00:32
  • @Stephen C: What do you mean it's "fragile"? It works perfectly, as long as the programmer passes in the right method name. – Michael Jul 08 '10 at 18:49
  • 3
    @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 Jul 08 '10 at 22:15
1

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
  • 108,630
  • 30
  • 201
  • 202