Strider
Strider

Reputation: 3739

Sort a list of objects based on a parameterized attribute of the object

Assuming that we have an object with the following attributes:

public class MyObject {

    private String attr1;
    private Integer attr2;

    //...

    public String getAttr1() {
        return this.attr1;
    }

    public Integer getAttr2() {
        return this.attr2;
    }
}

One way of sorting a list mylist of this object, based on its attribute attr1 is:

mylist.sort(Comparator.comparing(MyObject::getAttr1));

Is it possible to use this code inside a method in a dynamic way and replace the getAttr1 part with a method that returns the getter of an attribute of the object based on its name? Something like:

public void sortListByAttr(List<MyObject> list, String attr) {
    list.sort(Comparator.comparing(MyObject::getGetterByAttr(attr)));
}

The MyObject::getGetterByAttr(attr) part does not compile, I wrote it just as an example to explain my idea

I tried to implement a method with the following code new PropertyDescriptor(attr, MyObject.class).getReadMethod().invoke(new MyObject()) but It's still not possible to call a method with a parameter from the comparing method

Upvotes: 2

Views: 332

Answers (2)

Eugene
Eugene

Reputation: 120848

You could also have a single place where this comparators would be defined:

 static enum MyObjectComparator {

    ATTR1("attr1", Comparator.comparing(MyObject::getAttr1));

    MyObjectComparator(String attrName, Comparator<MyObject> comparator) {
        this.comparator = comparator;
        this.attrName = attrName;
    }

    private final Comparator<MyObject> comparator;

    private final String attrName;

    private static MyObjectComparator[] allValues = MyObjectComparator.values();

    public static Comparator<MyObject> findByValue(String value) {
        return Arrays.stream(allValues)
                .filter(x -> x.attrName.equalsIgnoreCase(value))
                .map(x -> x.comparator)
                .findAny()
                .orElseThrow(RuntimeException::new);
    }

}

And your usage would be:

public void sortListByAttr(List<MyObject> list, String attr) {
    list.sort(MyObjectComparator.findByValue(attr));
}

Upvotes: 3

Holger
Holger

Reputation: 298103

You could add a method like

public static Function<MyObject,Object> getGetterByAttr(String s) {
    switch(s) {
        case "attr1": return MyObject::getAttr1;
        case "attr2": return MyObject::getAttr2;
    }
    throw new IllegalArgumentException(s);
}

to your class, but the returned function is not suitable for Comparator.comparing, as it expects a type fulfilling U extends Comparable<? super U> and while each of String and Integer is capable of fulfilling this constraint in an individual invocation, there is no way to declare a generic return type for getGetterByAttr to allow both type and be still compatible with the declaration of comparing.

An alternative would be a factory for complete Comparators.

public static Comparator<MyObject> getComparator(String s) {
    switch(s) {
        case "attr1": return Comparator.comparing(MyObject::getAttr1);
        case "attr2": return Comparator.comparing(MyObject::getAttr2);
    }
    throw new IllegalArgumentException(s);
}

to be used like

public void sortListByAttr(List<MyObject> list, String attr) {
    list.sort(getComparator(attr));
}

This has the advantage that it also may support properties whose type is not Comparable and requires a custom Comparator. Also, more efficient comparators for primitive types (e.g. using comparingInt) would be possible.

You may also consider using a Map instead of switch:

private static Map<String,Comparator<MyObject>> COMPARATORS;
static {
    Map<String,Comparator<MyObject>> comparators=new HashMap<>();
    comparators.put("attr1", Comparator.comparing(MyObject::getAttr1));
    comparators.put("attr2", Comparator.comparing(MyObject::getAttr2));
    COMPARATORS = Collections.unmodifiableMap(comparators);
}
public static Comparator<MyObject> getComparator(String s) {
    Comparator<MyObject> comparator = COMPARATORS.get(s);
    if(comparator != null) return comparator;
    throw new IllegalArgumentException(s);
}

More dynamic is only possible via Reflection, but this would complicate the code, add a lot of potential error source, with only little benefit, considering that you need only to add one line of source code for adding support for another property in either of the examples above. After all, the set of defined properties gets fixed at compile time.

Upvotes: 6

Related Questions