7

I have a List of Value Objects [VO]. These Objects have many properties and corresponding get/set methods. I want to sort this List based on a property which I'll be getting in runtime. Let me explain in detail...

My VO is like this:

public class Employee {
    String name;
    String id;

    private String getName() {
        return name;
    }

    private String getId() {
        return id;
    }
}

I will be getting a string sortType in runtime, which can be either "id" or "name". I want to sort the list based on the value of the String.

I have tried to use Comparator and reflection together, but no luck. May be I didn't use it properly. I don’t want to use conditional if block branching to create whichever new specific Comparator [anonymous inner Class] is needed (at the runtime). Any other thoughts?


The try catch should be inside the new class. Here is the working code. If you want to use a separate class for Comparator, please find it in @Bohemian's comment below.

        String sortType = "name"; // determined at runtime
        Collections.sort(results, new Comparator<Employee>() {
        public int compare(Employee c1, Employee c2) {
            try{
            Method m = c1.getClass().getMethod("get" + StringUtils.capitalize(sortType));
            String s1 = (String)m.invoke(c1);
            String s2 = (String)m.invoke(c2);
            return s1.compareTo(s2);
            }
            catch (Exception e) {
                return 0;
            }
        }
       });
cellepo
  • 4,001
  • 2
  • 38
  • 57
jijo
  • 765
  • 3
  • 18
  • 35
  • 2
    Please use the appropriate language tag. – NPE Nov 26 '12 at 15:07
  • If you post your code which used reflection, we might be able to tell you what went wrong… – akuhn Nov 27 '12 at 05:09
  • I've added the code I tried with. – jijo Nov 27 '12 at 05:40
  • Is the try block not applicable to the inner class which will be created by new Comparator() ? I'm just wondering. I'm getting the errors on the first 3 lines of compare() method. – jijo Nov 27 '12 at 06:07
  • Guys.. Should I be deleting all the piece of codes which didn't work from the post? This is my first post and don't have much idea about the proceedings. – jijo Nov 27 '12 at 06:14
  • In this case, I think it would be fine to delete old code and only show the current version. – jahroy Nov 27 '12 at 06:25
  • sorry for the confusion, the variable sort is the one which will be passed in the runtime, I've followed bohemian's way to avoid confusion and updated the code. – jijo Nov 27 '12 at 06:39
  • @jahroy: Well, there is. Its in the same link you posted. public Method getMethod(String name, Class>... parameterTypes) – jijo Nov 28 '12 at 06:16
  • Why don't you want to use an `if` statement? It exactly fits the requirements here. Do you _ever_ use `if` statement branches at all, or do you just not like them in general? – cellepo May 20 '20 at 07:11

4 Answers4

18

Create a Comparator for the job:

public class EmployeeComparator implements Comparator<Employee> {

    private final String type;

    public EmployeeComparator (String type) {
        this.type = type;
    }

    public int compare(Employee e1, Employee e2) {
        if (type.equals("name")) {
             return e1.getName().compareTo(e2.getName());
        }
        return e1.getId().compareTo(e2.getId());
    }

}

Then to use it

String type = "name"; // determined at runtime
Collections.sort(list, new EmployeeComparator(type));

The reflective version would be similar, except you would look for a method on the object of "get" + type (capitalised) and invoke that and hard cast it to Comparable and use compareTo (I'll try to show the code, but I'm using my iPhone and its a bit of a stretch, but here goes)

public class DynamicComparator implements Comparator<Object> {
    private final String type;
    // pass in type capitalised, eg "Name" 
    // ie the getter method name minus the "get"
    public DynamicComparator (String type) {
        this.type = type;
    }
    public int compare(Object o1, Object o2) {
        // try-catch omitted 
        Method m = o1.getClass().getMethod("get" + type);
        String s1 = (String)m.invoke(o1);
        String s2 = (String)m.invoke(o2);
        return s1.compareTo(s2);
    }
}

OK... Here's how to do it without creating a class, using an anonymous class (with exception handling so code compiles):

List<?> list;
final String attribute = "Name"; // for example. Also, this is case-sensitive
Collections.sort(list, new Comparator<Object>() {
    public int compare(Object o1, Object o2) {
        try {
            Method m = o1.getClass().getMethod("get" + attribute);
            // Assume String type. If different, you must handle each type
            String s1 = (String) m.invoke(o1);
            String s2 = (String) m.invoke(o2);
            return s1.compareTo(s2);
        // simply re-throw checked exceptions wrapped in an unchecked exception
        } catch (SecurityException e) {
            throw new RuntimeException(e); 
        } catch (NoSuchMethodException e) {
            throw new RuntimeException(e);
        } catch (IllegalAccessException e) {
            throw new RuntimeException(e);
        } catch (InvocationTargetException e) {
            throw new RuntimeException(e);
        }
    }
});
Bohemian
  • 412,405
  • 93
  • 575
  • 722
  • 1
    This looks good and simple. Thanks !! Will try and let you know. – jijo Nov 27 '12 at 05:40
  • Awesome man..!! This is exactly what I've been looking for ! Let me try it. – jijo Nov 27 '12 at 05:48
  • This exception handling is killing me again. I tried to do it in a single shot instead of creating a class and I'm getting all sorts of exceptions now. Here's what I tried. – jijo Nov 27 '12 at 05:53
  • You should create a Comparator class. That's what allows you to _construct_ a Comparator at runtime with the specified sort field (by passing it in a constructor). Your most recently posted code does not allow for any way to specify the sort field. @Bohemian's code allows the client code to specify the sort field by passing a String parameter in the Comparator's constructor. – jahroy Nov 27 '12 at 06:27
  • The code which I posted have a 'sort' variable inside the getMethod which can be passed in runtime. I thought its a waste to create a new class, but I haven't tested this yet. Will let you know if it works. – jijo Nov 27 '12 at 06:33
  • What makes you think it's a "_waste_" to create a class? – jahroy Nov 27 '12 at 06:57
  • Well, if we can get the desired result without creating a new class, and without any hit on performance, its better to follow the shortest way to achieve it right. – jijo Nov 27 '12 at 08:49
  • @jijo You could do all this using an anonymous class. See edited answer for the requried kung fu – Bohemian Nov 27 '12 at 10:27
  • Thank You !! Is there any performance impact or any other side effect in using this way instead of creating a new class? – jijo Nov 27 '12 at 11:53
  • @jijo no impact at all. The compiler creates a class for you, so performance is identical. The only impact is that your colleagues will be in awe of your java king fu :) – Bohemian Nov 27 '12 at 14:12
  • @jio if you care about performance, you should not use reflection. It is sloooooooooooooooow. If you care about runtime performance then create an `if/else` block with ten branches, one for each attribute! (Also, that safe against injection attacks…) – akuhn Nov 27 '12 at 16:58
2

Do the following:

  • get the name of the field from the client
  • build the name of the getter -> "get" + field name (after capitalizing the first character)
  • try to find the method with reflection by using Class.getDeclaredMethod()
  • if found, invoke the returned Method object on two instances of your VO class
  • use the results of the invoked getter methods for sorting
jahroy
  • 22,322
  • 9
  • 59
  • 108
  • 1
    You might just as well access the fields (if the getters are really that trivial). – akuhn Nov 27 '12 at 05:12
  • Yeah just a call to `employee.getClass().getField(fieldName)` would be more maintainable/readable: https://docs.oracle.com/javase/8/docs/api/java/lang/Class.html#getField-java.lang.String- – cellepo May 20 '20 at 06:24
1

Keep it simple!

If the choice is only id or name—use an if statement.

Picking between two choices. That’s what if has been invented for.

Or, if it is many properties, then use reflection, or store the data in a Map in the first place. Sometimes Map is better than an class. In particular if your VO has not methods other than getters and setters.

Caution though, using reflection is might be unsafe in this case as your client might inject any term in the CGI parameters in an attack akin to SQL injection.

akuhn
  • 27,477
  • 2
  • 76
  • 91
  • The OP states that his class has _many_ properties. I think he just showed two of them to keep it simple. – jahroy Nov 27 '12 at 05:03
  • Thats the problem. Its not just id and name, my VO is quite large with around 10+ properties in it. – jijo Nov 27 '12 at 05:04
  • 1
    Added rational on why *NOT* using reflection. – akuhn Nov 27 '12 at 05:04
  • The OP will surely have an opportunity to constrain the input... Don't see any risk of injection attack here. – jahroy Nov 27 '12 at 05:05
  • Okay, if it is all properties, then use reflection—or store the data in a `Map` in the first place. Sometimes `Map` is better than an object. In particular if your VO has not methods other than getters and setters. – akuhn Nov 27 '12 at 05:07
  • _Many_ attributes can also describe including _just 2 attributes_. If the Question intended to ask about many more than 2 attributes, it should have mentioned that - particularly when it explicitly is coded with just the 2 attributes, without mentioning that's just an example (and instead many more fields are secretly desired to be accounted for...). – cellepo May 20 '20 at 07:31
  • Better yet - actually _code into the question_ the secret 10+ properties. Why not do that? The Question is literally asking something different otherwise. – cellepo May 20 '20 at 07:32
  • With the Question _as it has been posed_, I agree that just an `if` statement would actually be the best solution: The complexity added of otherwise using Reflection is not worth just trying to avoid `if` statements. – cellepo May 20 '20 at 07:35
0

For whatever reason you don't want to use an if statement... Leverage using java.lang.reflect.Field:

String sortType = "name"; // determined at runtime
Comparator<Employee> comparator = new Comparator<Employee>(){
    public int compare(Employee e0, Employee e1){
        try {
            Field sortField = e0.getClass().getField(sortType);
            return sortField.get(e0).toString()
                       .compareTo(sortField.get(e1).toString()));

        } catch(Exception e) {
            throw new RuntimeException(
                "Couldn't get field with name " + sortType, e);
        }
    };
}

But that only works for String fields - below handles any type of Field (or at least, Comparable fields):

String sortType = "name"; // determined at runtime
Comparator<Employee> comparator = new Comparator<Employee>(){
    public int compare(Employee e0, Employee e1){
        try {
            Field sortField = e0.getClass().getField(sortType);
            Comparable<Object> comparable0
                = (Comparable<Object>) sortField.get(e0);
            Comparable<Object> comparable1
                = (Comparable<Object>) sortField.get(e1);

            return comparable0.compareTo(comparable1);

        } catch(Exception e) {
            throw new RuntimeException(
                          "Couldn't get field with name " + sortType, e);
        }
    };
}

** Note still however that won't ignore case for String/Character/char (although it will account for negative numbers) - for example, that would order capital "Z" before lowercase "a". If you want to account for both of those cases, I don't see a way around (wait for it...) an if statement!

You can see a full implementation of handling all those cases, plus the below cases as well, in my JavaSortAnyTopLevelValueObject Github project (available in dependency JAR too):

  • Handles multiple sort-tie precedence fields (i.e: sort first by precedence0, sort second by precedence1 [in case of ties], sort third by ...)
  • Handles sorting ascending/descending, by each sort parameter
  • Handles any top-level, public (or otherwise visible modifier from call-location) field type of {primitive, Boxed Primitive, or String}
  • Ignores character casing
cellepo
  • 4,001
  • 2
  • 38
  • 57