Home > front end >  Sort list of objects by names of fields in Java
Sort list of objects by names of fields in Java

Time:04-04

I have a templated repository holding a list of entities.
The type of the list is Collection<T>, where T is the type of each entity.

How can I implement the method public Iterable<T> sort(Sort sort) on the repository to return the list of entities sorted by using the information from the argument sort?

Bascially, an object of type Sort is holding a list of type:
[("field1", ASC), ("field2", DESC), ("field3", ASC) ...], where the Strings "field1", "field2", "field3" are attributes names of objects T.

I have to implement the solution by using Java Streams.
So, when calling repository.sort(Sort.by("name").ascending().and(Sort.by("age").descending())) the output should be a list of objects sorted ascending by name, and descending by age.
I am not allowed to use the existing similar Java Spring mechanism.

class Sort:

package Repository.Sorting;

import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;

public class Sort {

    public static final Direction DEFAULT_DIRECTION = Direction.ASC;
    private List<Order> orders;

    public Sort(String property) {
        this.orders = List.of(new Order(DEFAULT_DIRECTION, property));
    }

    public Sort(List<Order> orders) {
        this.orders = orders;
    }

    public Sort(Direction direction, List<String> properties) {
        if (!properties.isEmpty()) {
            this.orders = properties
                    .stream()
                    .map(property -> new Order(direction, property))
                    .collect(Collectors.toList());
        }
    }

    public static Sort by(String... properties) {
        return new Sort(DEFAULT_DIRECTION, Arrays.asList(properties));
    }

    public Sort by(Direction direction, String... properties) {
        return new Sort(direction, Arrays.asList(properties));
    }

    public Sort by(List<Order> orders) {
        return new Sort(orders);
    }

    public Sort ascending() {
        return this.by(
                this.orders
                        .stream()
                        .map(order -> new Order(Direction.ASC, order.getProperty()))
                        .collect(Collectors.toList())
        );
    }

    public Sort descending() {
        return this.by(
                this.orders
                        .stream()
                        .map(order -> new Order(Direction.DESC, order.getProperty()))
                        .collect(Collectors.toList())
        );
    }

    public Sort and(Sort sort) {
        List<Order> ownOrders = this.orders;
        List<Order> otherOrders = sort.orders;
        List<Order> allOrders = Stream
                .concat(ownOrders.stream(), otherOrders.stream())
                .collect(Collectors.toList());

        return new Sort(allOrders);
    }

    public boolean unsorted() {
        return this.orders.isEmpty();
    }

    public List<Order> getOrders() {
        return this.orders;
    }

}

class Direction:

package Repository.Sorting;

public enum Direction {
    ASC,
    DESC
}

class Order:

package Repository.Sorting;

public class Order {

    private final Direction direction;
    private final String property;

    public Order(Direction direction, String property) {
        this.direction = direction;
        this.property = property;
    }

    public Direction getDirection() {
        return this.direction;
    }

    public String getProperty() {
        return this.property;
    }
}

I tried creating a Comparator object by using all the attributes names encapsulated by Sort object, but I did not manage to solve the problem.

CodePudding user response:

I hope that I understood well your issue :)

You need to used Java Reflection API to do that.

I give you below an example of implementation.

First, the creation of the entity class :

abstract class MyEntityAbstract {

}

class MyEntity extends MyEntityAbstract {
    private Integer field1;
    private String field2;

    public MyEntity(int field1, String field2) {
        this.field1 = field1;
        this.field2 = field2;
    }

    @Override
    public String toString() {
        return "MyEntity{"  
                "field1='"   field1   '\''  
                ", field2='"   field2   '\''  
                '}';
    }
}

Next, definition of an enum class which supply a Comparator associated to the type of your entity field (String (field2) / Integer (field1) in this example)

enum ComparatorType {
    STRING(String.class) {
        @Override
        public Comparator getComparator() {
            return (o1, o2) -> {
                String val1 = (String) o1;
                String val2 = (String) o2;
                return val1.compareTo(val2);
            };
        }
    },
    INTEGER(Integer.class) {
        @Override
        public Comparator getComparator() {
            return (o1, o2) -> {
                Integer val1 = (Integer) o1;
                Integer val2 = (Integer) o2;
                return val1.compareTo(val2);
            };
        }
    };

    private final Class clazz;

    ComparatorType(Class clazz) {
        this.clazz = clazz;
    }

    public abstract Comparator getComparator();

    public static ComparatorType from(Class<?> type) {
        return Arrays.stream(values())
                .filter(p -> p.clazz.equals(type))
                .findFirst()
                .orElseThrow(() -> new IllegalArgumentException("Comparator type for '"   type   "' is not allow"));
    }

}

Finally, the implemetation of the sort method in the Repository class :

class Repository<T extends MyEntityAbstract> {
    private final List<T> myEntities = new ArrayList<>();

    public void addEntity(T entity) {
        this.myEntities.add(entity);
    }

    public Collection<T> sort(Sort sort) {
        List<T> entitiesCopy = new ArrayList<>(myEntities);
        entitiesCopy.sort((o1, o2) -> {
            try {
                int result = 0;
                for (Order order : sort.getOrders()) {

                    // Recover the declared field with his name
                    Field field = o1.getClass().getDeclaredField(order.getProperty());
                    Field field2 = o2.getClass().getDeclaredField((order.getProperty()));

                    // Need to set accessible the field to recover his value
                    boolean isAccessible = field.isAccessible();
                    if (!isAccessible) {
                        field.setAccessible(true);
                        field2.setAccessible(true);
                    }

                    // Recover fields value 
                    Object value1 = field.get(o1);
                    Object value2 = field2.get(o2);

                    // Reinit fields accessibility 
                    field.setAccessible(isAccessible);
                    field2.setAccessible(isAccessible);

                    // Recover the comparator with the type of the field
                    Comparator comparatorFromClass = ComparatorType.from(field.getType()).getComparator();

                    // Compare values
                    if (order.getDirection() == Direction.ASC) {
                        result  = comparatorFromClass.compare(value1, value2);
                    } else {
                        result  = comparatorFromClass.compare(value2, value1);
                    }

                    // You need to break for loop if the compare for this order return 1 or -1 (order is already done, no need to order by the next fields in the list)
                    // If result == 0, the next order in your iteration will be apply 
                    if (result != 0) {
                        break;
                    }
                }
                return result;
            } catch (NoSuchFieldException | IllegalAccessException e) {
                // Handle Exception
                e.printStackTrace();
                return 0;
            }
        });
        return entitiesCopy;
    }
}

For testing the code, I used this main method

public static void main(String[] args) {
        Repository<MyEntity> repo = new Repository<>();
        repo.addEntity(new MyEntity(110, "James"));
        repo.addEntity(new MyEntity(163, "Robert"));
        repo.addEntity(new MyEntity(31, "John"));
        repo.addEntity(new MyEntity(256, "Michael"));
        repo.addEntity(new MyEntity(59, "William"));
        repo.addEntity(new MyEntity(59, "Robert"));
        repo.addEntity(new MyEntity(59, "Bob"));
        repo.addEntity(new MyEntity(59, "Thomas"));
        repo.addEntity(new MyEntity(59, "Uber"));
        repo.addEntity(new MyEntity(46, "David"));
        repo.addEntity(new MyEntity(877, "Richard"));
        repo.addEntity(new MyEntity(8, "Joseph"));
        repo.addEntity(new MyEntity(39, "Thomas"));

        System.out.println("Sort by 'field1' ASC");
        repo.sort(Sort.by("field1")).forEach(System.out::println);

        System.out.println("");
        System.out.println("Sort by 'field1' DESC");
        repo.sort(Sort.by(Direction.DESC, "field1")).forEach(System.out::println);

        System.out.println("");
        System.out.println("Sort by 'field2'");
        repo.sort(Sort.by("field2")).forEach(System.out::println);

        System.out.println("");
        System.out.println("Sort by 'field2' DESC");
        repo.sort(Sort.by(Direction.DESC, "field2")).forEach(System.out::println);


        System.out.println("");
        System.out.println("Sort by 'field1' ASC and 'field2' ASC");
        repo.sort(Sort.by("field1").and(Sort.by("field2"))).forEach(System.out::println);

        System.out.println("");
        System.out.println("Sort by 'field2' ASC and 'field1' ASC");
        repo.sort(Sort.by("field2").and(Sort.by("field1"))).forEach(System.out::println);


        System.out.println("");
        System.out.println("Sort by 'field1' DESC and 'field2' ASC");
        repo.sort(Sort.by(Direction.DESC, "field1").and(Sort.by("field2"))).forEach(System.out::println);

        System.out.println("");
        System.out.println("Sort by 'field1' DESC and 'field2' DESC");
        repo.sort(Sort.by(Direction.DESC, "field1").and(Sort.by(Direction.DESC, "field2"))).forEach(System.out::println);


    }

The output :

Sort by 'field1' ASC
MyEntity{field1='8', field2='Joseph'}
MyEntity{field1='31', field2='John'}
MyEntity{field1='39', field2='Thomas'}
MyEntity{field1='46', field2='David'}
MyEntity{field1='59', field2='William'}
MyEntity{field1='59', field2='Robert'}
MyEntity{field1='59', field2='Bob'}
MyEntity{field1='59', field2='Thomas'}
MyEntity{field1='59', field2='Uber'}
MyEntity{field1='110', field2='James'}
MyEntity{field1='163', field2='Robert'}
MyEntity{field1='256', field2='Michael'}
MyEntity{field1='877', field2='Richard'}

Sort by 'field1' DESC
MyEntity{field1='877', field2='Richard'}
MyEntity{field1='256', field2='Michael'}
MyEntity{field1='163', field2='Robert'}
MyEntity{field1='110', field2='James'}
MyEntity{field1='59', field2='William'}
MyEntity{field1='59', field2='Robert'}
MyEntity{field1='59', field2='Bob'}
MyEntity{field1='59', field2='Thomas'}
MyEntity{field1='59', field2='Uber'}
MyEntity{field1='46', field2='David'}
MyEntity{field1='39', field2='Thomas'}
MyEntity{field1='31', field2='John'}
MyEntity{field1='8', field2='Joseph'}

Sort by 'field2'
MyEntity{field1='59', field2='Bob'}
MyEntity{field1='46', field2='David'}
MyEntity{field1='110', field2='James'}
MyEntity{field1='31', field2='John'}
MyEntity{field1='8', field2='Joseph'}
MyEntity{field1='256', field2='Michael'}
MyEntity{field1='877', field2='Richard'}
MyEntity{field1='163', field2='Robert'}
MyEntity{field1='59', field2='Robert'}
MyEntity{field1='59', field2='Thomas'}
MyEntity{field1='39', field2='Thomas'}
MyEntity{field1='59', field2='Uber'}
MyEntity{field1='59', field2='William'}

Sort by 'field2' DESC
MyEntity{field1='59', field2='William'}
MyEntity{field1='59', field2='Uber'}
MyEntity{field1='59', field2='Thomas'}
MyEntity{field1='39', field2='Thomas'}
MyEntity{field1='163', field2='Robert'}
MyEntity{field1='59', field2='Robert'}
MyEntity{field1='877', field2='Richard'}
MyEntity{field1='256', field2='Michael'}
MyEntity{field1='8', field2='Joseph'}
MyEntity{field1='31', field2='John'}
MyEntity{field1='110', field2='James'}
MyEntity{field1='46', field2='David'}
MyEntity{field1='59', field2='Bob'}

Sort by 'field1' ASC and 'field2' ASC
MyEntity{field1='8', field2='Joseph'}
MyEntity{field1='31', field2='John'}
MyEntity{field1='39', field2='Thomas'}
MyEntity{field1='46', field2='David'}
MyEntity{field1='59', field2='Bob'}
MyEntity{field1='59', field2='Robert'}
MyEntity{field1='59', field2='Thomas'}
MyEntity{field1='59', field2='Uber'}
MyEntity{field1='59', field2='William'}
MyEntity{field1='110', field2='James'}
MyEntity{field1='163', field2='Robert'}
MyEntity{field1='256', field2='Michael'}
MyEntity{field1='877', field2='Richard'}

Sort by 'field2' ASC and 'field1' ASC
MyEntity{field1='59', field2='Bob'}
MyEntity{field1='46', field2='David'}
MyEntity{field1='110', field2='James'}
MyEntity{field1='31', field2='John'}
MyEntity{field1='8', field2='Joseph'}
MyEntity{field1='256', field2='Michael'}
MyEntity{field1='877', field2='Richard'}
MyEntity{field1='59', field2='Robert'}
MyEntity{field1='163', field2='Robert'}
MyEntity{field1='39', field2='Thomas'}
MyEntity{field1='59', field2='Thomas'}
MyEntity{field1='59', field2='Uber'}
MyEntity{field1='59', field2='William'}

Sort by 'field1' DESC and 'field2' ASC
MyEntity{field1='877', field2='Richard'}
MyEntity{field1='256', field2='Michael'}
MyEntity{field1='163', field2='Robert'}
MyEntity{field1='110', field2='James'}
MyEntity{field1='59', field2='Bob'}
MyEntity{field1='59', field2='Robert'}
MyEntity{field1='59', field2='Thomas'}
MyEntity{field1='59', field2='Uber'}
MyEntity{field1='59', field2='William'}
MyEntity{field1='46', field2='David'}
MyEntity{field1='39', field2='Thomas'}
MyEntity{field1='31', field2='John'}
MyEntity{field1='8', field2='Joseph'}

Sort by 'field1' DESC and 'field2' DESC
MyEntity{field1='877', field2='Richard'}
MyEntity{field1='256', field2='Michael'}
MyEntity{field1='163', field2='Robert'}
MyEntity{field1='110', field2='James'}
MyEntity{field1='59', field2='William'}
MyEntity{field1='59', field2='Uber'}
MyEntity{field1='59', field2='Thomas'}
MyEntity{field1='59', field2='Robert'}
MyEntity{field1='59', field2='Bob'}
MyEntity{field1='46', field2='David'}
MyEntity{field1='39', field2='Thomas'}
MyEntity{field1='31', field2='John'}
MyEntity{field1='8', field2='Joseph'}

Hope that will help you !

And sorry for my bad english :) Trying my best !

CodePudding user response:

repository.sort(Sort.by("name").ascending().and(Sort.by("age").descending())) 

Could use Collection<T>.sort(Comparator<T>).

repository.sort(Comparator.comparing(MyEntity::getName)
                .thenComparing(
                    Comparator.comparingInt(MyEntity::getAge).reversed()));

(Assuming int age.)

The complex syntax is unfortunately caused by doing descendant (reversed) only for the age field.

When there is no need for a meta-level, like generating SQL, you probably would still be better of by the standard Comparator<MyEntity>.

Otherwise there are also frameworks like JPA (eclipseLink, hibernate) with some expressive power.

  • Related