Tiny
Tiny

Reputation: 27909

Sorting a collection in a generic method in Java 8

The following method performs ordering.

public List<Comparator<Entity>> sort(Map<String, String> map) {
    List<Comparator<Entity>> list = new ArrayList<Comparator<Entity>>();

    for (Map.Entry<String, String> entry : map.entrySet()) {
        boolean sortOrder = entry.getValue().equalsIgnoreCase("asc");

        switch (entry.getKey()) {
            case "id":
                list.add(sortOrder ? Comparator.comparing(Entity::getId) : Comparator.comparing(Entity::getId, Comparator.reverseOrder()));
                break;
            case "size":
                list.add(sortOrder ? Comparator.comparing(Entity::getSize) : Comparator.comparing(Entity::getSize, Comparator.reverseOrder()));
                //break;
        }
    }

    return list;
}

The list being returned by the above method is used in the following way.

// map is initialized somewhere based on client's interactions with sorting.
// Based on client's interactions, map may be empty or it may contain one or more ordering fields.

if (MapUtils.isNotEmpty(map)) {  // map = new LinkedHashMap<String, String>();

    List<Comparator<Entity>> comparators = sort(map);
    Comparator<Entity> comparator = comparators.remove(0);

    for (Comparator<Entity> c : comparators) {
        comparator = comparator.thenComparing(c);
    }

    list = list.stream().sorted(comparator).collect(Collectors.toList());
} else {
    // This is the default ordering.
    list = list.stream().sorted(Comparator.comparing(Entity::getId).reversed()).collect(Collectors.toList());
}

Entity contains two fields named id of type Integer and size of type BigDecimal and list is a type of List<Entity>.

Since there are several other classes having the same fields with the same datatypes, I want this method to be generic so that it has to be defined only once like so,

public <T extends Object> List<Comparator<T>> sort(Map<String, String> map, Class<T> clazz) {
    List<Comparator<T>> list = new ArrayList<Comparator<T>>();

    // Sorting logic.

    return list;
}

But doing so, expressions like T::getId will not compile as obvious, since the generic type parameter T evaluates to Object.

Is there a way to code sorting without knowing the actual class type so that this method can be prevented from being repeated everywhere, when it is needed?

Upvotes: 2

Views: 2749

Answers (4)

Holger
Holger

Reputation: 298559

If you want to create a compound Comparator anyway, there is no point in filling a List first. Just do it in one operation:

public static <T> Comparator<T> getOrdering(
    Map<String, String> map, Map<String,Comparator<T>> defined) {

    return map.entrySet().stream().map(e -> {
        Comparator<T> c=defined.get(e.getKey());
        return e.getValue().equalsIgnoreCase("asc")? c: c.reversed();
    })
    .reduce(Comparator::thenComparing)
    .orElseThrow(()->new IllegalArgumentException("empty"));
}

This works for arbitrary types but requires to provide a map of existing comparators for a type. But this map isn’t a restriction, it actually improves the operation as it removes the hardcoded set of existing named property comparators. You can use it with an arbitrary type, Entity being exemplary here, as follows:

Map<String,Comparator<Entity>> map=new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
map.put("id", Comparator.comparing(Entity::getID));
map.put("size", Comparator.comparing(Entity::getSize));

Comparator<Entity> cmp=getOrdering(param, map);

whereas param is the ordered map of your question, mapping from property name to either "asc" or "desc". The map holding the predefined comparators can be created once in initialization code and then be re-used.

The creation code doesn’t look so complicated that it deserves implementing a dynamic solution, however, if you still wish to do it, here is the code to generate such a map for arbitrary classes:

public final class DynamicComparators<T> {
    public static <T> Map<String,Comparator<T>> getComparators(Class<T> cl) {
        return CACHE.get(cl).cast(cl).comps;
    }

    private static final ClassValue<DynamicComparators> CACHE
                         =new ClassValue<DynamicComparators>() {
        @Override protected DynamicComparators computeValue(Class<?> type) {
            return new DynamicComparators<>(type);
        }
    };
    private final Class<T> theClass;
    private final Map<String, Comparator<T>> comps;

    private DynamicComparators(Class<T> cl) {
        theClass=cl;
        Map<String,Comparator<T>> map=new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
        try {
            BeanInfo bi=Introspector.getBeanInfo(cl);
            MethodHandles.Lookup l=MethodHandles.lookup();
            MethodType invoked=MethodType.methodType(Function.class);
            for(PropertyDescriptor pd: bi.getPropertyDescriptors()) {
                Method m=pd.getReadMethod();
                if(m==null) continue;
                Class<?> t=m.getReturnType();
                if(!t.isPrimitive() && !Comparable.class.isAssignableFrom(t))
                    continue;
                MethodHandle mh=l.unreflect(m);
                MethodType mt=mh.type();
                @SuppressWarnings("unchecked")Comparator<T> cmp
                  = Comparator.comparing((Function<T,Comparable>)LambdaMetafactory
                    .metafactory(l, "apply", invoked, mt.generic(), mh, mt)
                    .getTarget().invokeExact());
                map.put(pd.getName(), cmp);
            }
        } catch(Throwable ex) {
            throw new RuntimeException(ex);
        }
        this.comps=Collections.unmodifiableMap(map);
    }
    @SuppressWarnings("unchecked") <U> DynamicComparators<U> cast(Class<U> cl) {
        if(cl!=theClass) throw new ClassCastException();
        return (DynamicComparators<U>)this;
    }
}

Upvotes: 2

ZhongYu
ZhongYu

Reputation: 19702

You'll probably need something more dynamic. Some annotations may help

class Shoe

    @Column("id")
    @Sortable
    public int getId(){ ... }

    @Column("Description")
    public String getDescription(){...}

Given any class, you can reflect on columns to display, columns that can be sorted ("id", ...), and values of columns ("getId()", ...).

Upvotes: 2

Tunaki
Tunaki

Reputation: 137299

A simple way, without having to rely on reflection magic, is to introduce a common interface for all the types having the same fields with the same datatypes as Entity.

Consider the following IdSize interface with the following Entity.

interface IdSize {
    Integer getId();
    BigDecimal getSize();
}

class Entity implements IdSize {

    private Integer id;
    private BigDecimal size;
    @Override
    public Integer getId() {
        return id;
    }
    @Override
    public BigDecimal getSize() {
        return size;
    }

}

Then you can make your method generic like this:

public <T extends IdSize> List<Comparator<T>> sort(Map<String, String> map) {
    List<Comparator<T>> list = new ArrayList<Comparator<T>>();
    for (Map.Entry<String, String> entry : map.entrySet()) {
        boolean sortOrder = entry.getValue().equalsIgnoreCase("asc");
        Comparator<T> comparator = null;
        switch (entry.getKey()) {
            case "id":
                comparator = Comparator.comparing(IdSize::getId);
                break;
            case "size":
                comparator = Comparator.comparing(IdSize::getSize);
                break;
            default: // do something here, throw an exception?
        }
        list.add(sortOrder ? comparator : comparator.reversed());
    }
    return list;
}

(I refactored a little the switch-case statement to remove the duplicated code.). Also, you might want to add a default clause.

Upvotes: 4

Jean Logeart
Jean Logeart

Reputation: 53859

Use interfaces:

public interface Sizable {
    BigDecimal getSize();
}

public interface Id {
    int getId();
}

Have your classes implement those interface and use them in your generic methods:

public <T extends Id & Sizable> List<Comparator<T>> sort(Map<String, String> map) {
    // ...
}

Upvotes: 3

Related Questions