3

I have a Report {String name, Date date, int score } class. I want to be able to sort a list of reports for any member variable using the new java 8 syntax

So java 8 provides this new

list.sort(Comparator.comparing(report -> report.name)) 

to sort the list on name.

Lets say instead of name I want to provide a variable field name to this method eg. something like

list.sort(Comparator.comparing(report -> report.anyField))

where anyField can be name or date or score. How do I achieve this behavior.

Jekin Kalariya
  • 3,475
  • 2
  • 20
  • 32
dogfish
  • 2,646
  • 4
  • 21
  • 37

7 Answers7

5

Just create a comparator for each property.

static Map<String,Comparator<Report>> ORDER;
static {
    HashMap<String,Comparator<Report>> m=new HashMap<>();
    m.put("name", Comparator.comparing(r -> r.name));
    m.put("date", Comparator.comparing(r -> r.date));
    m.put("score", Comparator.comparingInt(r -> r.score));
    ORDER=Collections.unmodifiableMap(m);
}
public static void sort(List<Report> list, String order) {
    Comparator<Report> c=ORDER.get(order);
    if(c==null) throw new IllegalArgumentException(order);
    list.sort(c);
}

You may consider using an enum as alternative to String, which eliminates the possibility of providing a non-existent property name:

enum ReportOrder {
    NAME(Comparator.comparing(r -> r.name)),
    DATE(Comparator.comparing(r -> r.date)),
    SCORE(Comparator.comparingInt(r -> r.score));

    private Comparator<Report> cmp;
    private ReportOrder(Comparator<Report> c) { cmp=c; }

    public void sort(List<Report> list) {
        list.sort(cmp);
    }
}

Now you can just say, e.g. ReportOrder.NAME.sort(list);. Of course, the other delegation style works as well:

public static void sort(List<Report> list, ReportOrder o) {
    list.sort(o.cmp);
}

 

sort(list, ReportOrder.DATE);
Holger
  • 285,553
  • 42
  • 434
  • 765
2

One very generic solution is to use Java's Reflection and some casting:

    String sortBy = "name";
    list.sort(Comparator.comparing(report -> {
        try {
            return (Comparable) report.getClass().getDeclaredField(sortBy).get(report);
        } catch (Exception e) {
            throw new RuntimeException("Ooops", e);
        }
    }));    

If you use an additional library like https://github.com/jOOQ/jOOR the code becomes even simpler:

    String sortBy = "score";
    list.sort(Comparator.comparing(report -> Reflect.on(report).field(sortBy).get()));

Please be aware that this solution only works with fields that implement Comparable and that it has some runtime overhead.

Peti
  • 1,670
  • 1
  • 20
  • 25
  • 2
    Raw type use. That's the problem here - at runtime you don't know what you're comparing. – RealSkeptic Sep 13 '16 at 07:40
  • 5
    I would only recommend Reflection here if the fields of the Report are unknown at compile time. If the set of fields is fixed (name, date, score) then I think there are cleaner solutions. – Puce Sep 13 '16 at 07:58
1
public class Report {

    //properties and getters

    public static void sort(List<Report> list, Function<Report, Comparable> sortKey) {
        list.sort(Comparator.comparing(sortKey));
    }
}

Report.sort(reports, Report::getName);
Report.sort(reports, Report::getDate);
Report.sort(reports, Report::getScore);

Could make this into a generic util class where you can pass in a List of any class:

public class MyUtil<T> {
    void sort(List<T> t, Function<T, Comparable> sortKey) {
        t.sort(Comparator.comparing(sortKey));
    }
}

MyUtil<Report> util = new MyUtil();
util.sort(ppl, Report::getName);
Roy
  • 11
  • 2
0

If your Report has getter method of various field you can do like this

list.sort(Comparator.comparing(Report::getFieldName));

Or with lambda expression

list.sort((ob1, ob2) -> ob1.getFieldName().compareTo(ob2.getFieldName()));
Jekin Kalariya
  • 3,475
  • 2
  • 20
  • 32
  • 1
    Does this actually answer the question? I can't see how it helps OP to pass in a varying field name. – Dawood ibn Kareem Sep 13 '16 at 06:49
  • David why not , OP can call different method like getFieldName, getAnyField – Jekin Kalariya Sep 13 '16 at 07:16
  • 5
    OP wants to be able to use the field name without deciding in advance, at compile time, which field will be used. Otherwise OP could just type in the field name. What's the difference between typing the name of a getter or typing the name of a field? – RealSkeptic Sep 13 '16 at 07:17
  • 1
    You could probably do something like this, in combination with reflection. But that makes this a partial answer at best. The code you've exhibited here brings no advantage over the code that OP exhibited in the question. – Dawood ibn Kareem Sep 13 '16 at 07:19
  • @DavidWallace I tried using reflection with the following code. It compiles but doesn't sort 'Field field = Report.class.getDeclaredField(sortField); list.sort(Comparator.comparing(report -> field.getName()));' – dogfish Sep 13 '16 at 19:22
0

At some place, you'll have to use a bit of reflection to pull this off. Here's an example using the bean introspection mechanism of java:

    public static class MyBean {
        private String  name;
        private Date    birthday;
        private Integer score;
        ...
        ... (constructor, getters, setters - the usual bean stuff)
    }

    PropertyDescriptor[] pdesc = Introspector.getBeanInfo(MyBean.class).getPropertyDescriptors();
    for(PropertyDescriptor pd : pdesc) {
        if(Comparable.class.isAssignableFrom(pd.getPropertyType())) {
            System.out.println("Property " + pd.getName() + " could be used for comparison");

            Function<MyBean, Comparable> extractor = b -> {
                try {
                    return (Comparable) pd.getReadMethod().invoke(b);
                }
                catch(Exception e) {
                    throw new RuntimeException(e);
                }
            };

            Comparator<MyBean> comp = Comparator.comparing(extractor);

            // do something useful with the comparator :-)
        }
    }

Additionally, you could go a similar way for primitive types (e.g. int, which does not implement Comparable as opposed to Integer.)

mtj
  • 3,381
  • 19
  • 30
0

If the set of properties is fixed (name, date, score) then I think a clean way would be to pass a Comparator:

private void someMethod(Comparator<Report> comparator){
   ...
   list.sort(comparator);
   ...
}

...

someMethod(Comparator.comparing(report::getName));
someMethod(Comparator.comparing(report::getDate));
someMethod(Comparator.comparingInt(report::getScore));
someMethod(Comparator.comparing(report::getName).thenComparingInt(report::getScore));
Puce
  • 37,247
  • 13
  • 80
  • 152
  • 2
    I think you're also missing the point of the OP, which is that the selection of the field has to be dynamic. That is, at runtime he has the field name, not at compile time. – RealSkeptic Sep 13 '16 at 08:07
  • @RealSkeptic as I understand the question the set of properties is fix. Just the property which used for sorting is not fix. – Puce Sep 13 '16 at 08:20
  • 1
    Right. So you only know at run time whether you'll want to use `report::getName`, `report::getDate` etc. - so your answer doesn't address the question of choosing dynamically. I suppose you could say "Use a switch statement" - but you haven't, so you are not answering the OP's question. – RealSkeptic Sep 13 '16 at 09:22
  • @RealSkeptic Well, the rest depends on the use case and we haven't more information. E.g. it could be implemented by event handlers registered in a JavaFX TableView. But then OP maybe doesn't use JavaFX and has some other user interaction. Somewhere something/ someone picks one of the properties. In that call hierarchy somewhere I recommend to instantiate a property specific comparator. The "someMethod"-method stays generic, however, which was my point. – Puce Sep 13 '16 at 09:32
0

There is a Comparator class called NamedMethodComparator that will work as a Comparator for any zero arg method that returns a Comparable posted here: How to use Comparator in Java to sort

Community
  • 1
  • 1
John
  • 3,458
  • 4
  • 33
  • 54