Search code examples
javagenericsreflectioncomparator

Proof of concept: how I can create a generic comparator method with reflection?


I hava a lot of classes like this:

Class Person {
  protected String name;
  protected String surname;
  ...getters and setters...
}

I'd like to order a Collection of Person by name or by surname. Actually I'm simply doing this:

Collections.sort(listofpersons, new Comparator<Person>(){
  @Override 
  public int compare(Person p1, Person p2) { return p1.getName().compareTo(p2.getName()); }      
})

or

Collections.sort(listofpersons, new Comparator<Person>(){
  @Override 
  public int compare(Person p1, Person p2) { return p1.getSurname().compareTo(p2.getSurname()); }      
})

I'm trying to implement a generic comparator, like:

MyUtils.sort(listofpersons,"getName");
MyUtils.sort(listofpersons,"getSurame");

and I'm trying to understand of to do it with generics and reflection, but I'm stuck. I'm doing this:

public static <T> void sortCollection(Collection<T> list, final String methodName) {
  Comparator<T> comparator = new Comparator<T>() {
    @Override
    public int compare(T o1, T o2) {
      try {
        String a=o1.getClass().getMethod(methodName).invoke(o1).toString();
        String b=o2.getClass().getMethod(methodName).invoke(o2).toString();
        return a.compareTo(b);
      } catch (Exception ex) {
        ...log somewhere...
        return 0;
      }
    }
  };
  Collections.sort(list, comparator);
}

forget the catch Exception and the String casting for now, the point is that Collections.sort(Collection,Comparator) does not exists, but I don't know of to create a Comparator... I'm very curious to know if my idea have any sense or not (an why) and what's the correct way implement it.

Thank you!


Solution

  • The problem is in the signature of the sortCollection method. You should change the type of the list parameter from Collection<T> to List<T>. In other words, your method's header should be:

    public static <T> void sortCollection(List<T> list, final String methodName)
    

    That's because Collections.sort() expects its first parameter to be a List (See its javadoc)

    EDIT:

    Regarding your implementation, there are some generic/reflection tricks which you can use. Here's a possible alternative impl.

    @SuppressWarnings({ "unchecked", "rawtypes" })
    public static<T> Comparator<T> 
      newMethodComparator(Class<T> cls, String methodName) throws Exception {    
      Method method = cls.getMethod(methodName);
      if (method.getParameterTypes().length != 0) 
        throw new Exception("Method " + method + " takes parameters");
    
      Class<?> returnType = method.getReturnType();
      if (!Comparable.class.isAssignableFrom(returnType))
        throw new Exception("The return type " + returnType + " is not Comparable");
    
      return newMethodComparator(method, (Class<? extends Comparable>) returnType);      
    }
    
    private static<T,R extends Comparable<R>> Comparator<T> newMethodComparator(
        final Method method, final Class<R> returnType) throws Exception {    
      return new Comparator<T>() {
        @Override
        public int compare(T o1, T o2) {
          try {
            R a = invoke(method, o1);
            R b = invoke(method, o2);
            return a.compareTo(b);
          } catch (Exception e) {
            throw new RuntimeException(e);
          }
        }
    
        private R invoke(Method method, T o) throws Exception {
          return returnType.cast(method.invoke(o));
        }
      };
    }
    

    Here's how you would use it:

      List<Person> ps = new ArrayList<>(Arrays.asList(
        new Person("A", "D"), new Person("B",  "C")));
      ...
    
      Comparator<Person> byNameComparator = 
        newMethodComparator(Person.class, "getName");
      Collections.sort(ps,  byNameComparator);
    

    Rationale:

    (a) This impl. will work for any method that returns a Comparable object. Your implementation is using string-comparison which will not work nicely, for one, if the method return an int.

    (b) This impl. checks the method's return type and the fact that it takes zero arguments at the point where the Comparator is created, which is before the sorting has started. Thus, there is lesser chance of Collections.sort() throwing an exception on you.

    (c) Note that the @SuppressWarnings({ "unchecked", "rawtypes" }) does not pose any real risk. If the method's return type is Comparable then the downcasts will succeed. If the return type is not Comparable then Comparable.class.isAssignableFrom(returnType)) will return false, the method will throw an explicit exception and the downcast will never be reached.