0

I have an ArrayList<Task> named tasks which I want to print, sorted according to each field of the Task object. Task class has three private fields { String title, Date date, String project }. The class has some public get methods that allow other classes to read the fields { getTitle(), getDate(), getProject(), getTaskDetails() }.

I have a simple method that uses a stream to sort and print the ArryaList tasks:

tasks.stream()
     .sorted(Comparator.comparing(Task::getProject))
     .map(Task::getTaskDetails)
     .forEach(System.out::println);

Instead of creating 3 different methods, to sort according each different getter method, I wanted to use Reflection API. But I am having trouble invoking the methods ( getTitle(), getDate(), getProject() ) inside the Comparator comparing method:

Used import java.lang.reflect.Method;

Declared the method Method taskMethod = Task.class.getDeclaredMethod(methodName); where methodName will be the parameter String received with the method name ("getTitle" or "getDate" or "getProject").

Then tried to do something like this, but didn't workout:

            tasks.stream()
                    .sorted(Comparator.comparing(task -> {
                                try {
                                    taskMethod.invoke(task);
                                } catch (Exception e) {
                                    e.printStackTrace();
                                }
                            }))
                    .map(Task::getTaskDetails)
                    .forEach(System.out::println);

Is this possible at all? Or is there any simpler solution?

Only found this question but didn't solve my problem.

Thank you for any feedback.

Endovelicvs
  • 109
  • 1
  • 11
  • 3
    First, thing that comes in my head is: Why? Why would you want to do this? Is this to improve your skills with reflection? Or is this an actual use case? From there I could possibly help you further. – Remco Buddelmeijer Oct 09 '19 at 12:50
  • How are you going to deal with type casts? It will look a lot messy than current approach.. – Prashant Oct 09 '19 at 13:08
  • 1
    @Remco I wanted to avoid code duplication. Why Reflection? Read about it and was trying to figure how if it was possible to do it using it. Also, as a beginner in Java, didn't come out with a better solution. Could I solve this in an easier way (then creating 3 different methods)? Thanks. – Endovelicvs Oct 09 '19 at 13:09
  • 1
    How about using Strategy design pattern instead. It fits very nicely with lambdas. – Prashant Oct 09 '19 at 13:14
  • 1
    @Endovelicvs Yes, you can. I would suggest making one compareTo function where you compare all the method outcomes with the current comparing Object (so: this.getName().compareTo(o.getName()) ). It is no use case for code duplication to say that you do not want to compare on two or more expressions, as you are anyways comparing your object with the rule that all the three methods compare must evaluate positively. This is just making your code more complicated and unreadable. – Remco Buddelmeijer Oct 09 '19 at 13:17
  • Also, something you could possibly consider is: do you really want to compare your objects in multiple fields? Why would you do this? And what do you want the outcome to be? From there you can use multiple sorts or a multiple-layered expression in your compareTo method. – Remco Buddelmeijer Oct 09 '19 at 13:20

2 Answers2

2

The answer that you linked to basically contains the core idea, even though it refers to fields ("properties") and not to methods: Create a way of obtaining the desired value from the object, and then simply compare these values for two objects.

One could consider it as a duplicate, but I'm not sure.

In any case:

You should carefully think about whether reflection is the right approach here.

It might be much more elegant (i.e. less hacky) to generate the required comparators without reflection. This could be an enum where you attach the proper Comparator to each enum value:

enum TaskProperty {
    TITLE(comparatorForTitle), 
    DATE(comparatorForDate), ...
}

// Using it:
tasks.stream().sorted(TaskProperty.TITLE.getComparator()).forEach(...);

Or maybe (less type safe, but a tad more flexible), using a map where you can look them up via a string, as in

// Put all comparators into a map
map.put("getDate", compareTaskByDate);
...
// Later, use the string to pick up the comparator from the map:
tasks.stream().sorted(map.get("getDate")).forEach(...);

If you have carefully thought this through, and really want to use the reflection based approach:

You can create a utility method that obtains the return value of a certain method for a given object. Then create a comparator that calls this method for the given objects, and compares the return values. For flexibility sake, you can pass the return values to a downstream comparator (which can be naturalOrder by default).

An example of how this could be done is shown here. But the list of exceptions that are caught in getOptional should make clear that many things can go wrong here, and you should really consider a different approach:

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;

public class SortWithReflection
{
    public static void main(String[] args)
    {
        List<Task> tasks = new ArrayList<Task>();
        tasks.add(new Task("AAA", 222));
        tasks.add(new Task("BBB", 333));
        tasks.add(new Task("CCC", 111));

        System.out.println("By getTitle:");
        tasks.stream()
            .sorted(by("getTitle"))
            .forEach(System.out::println);

        System.out.println("By getDate:");
        tasks.stream()
            .sorted(by("getDate"))
            .forEach(System.out::println);

        System.out.println("By getDate, reversed");
        tasks.stream()
            .sorted(by("getDate", Comparator.naturalOrder().reversed()))
            .forEach(System.out::println);

    }

    private static <T> Comparator<T> by(String methodName)
    {
        return by(methodName, Comparator.naturalOrder());
    }

    private static <T> Comparator<T> by(
        String methodName, Comparator<?> downstream)
    {
        @SuppressWarnings("unchecked")
        Comparator<Object> uncheckedDownstream =
            (Comparator<Object>) downstream;
        return (t0, t1) -> 
        {
            Object r0 = getOptional(t0, methodName);
            Object r1 = getOptional(t1, methodName);
            return uncheckedDownstream.compare(r0, r1);
        };
    }

    private static <T> T getOptional(
        Object instance, String methodName)
    {
        try
        {
            Class<?> type = instance.getClass();
            Method method = type.getDeclaredMethod(methodName);
            Object object = method.invoke(instance);
            @SuppressWarnings("unchecked")
            T result = (T)object;
            return result;
        }
        catch (NoSuchMethodException 
            | SecurityException 
            | IllegalAccessException 
            | IllegalArgumentException 
            | InvocationTargetException
            | ClassCastException e)
        {
            e.printStackTrace();
            return null;
        }
    }


    static class Task
    {
        String title;
        Integer date;

        Task(String title, Integer date)
        {
            this.title = title;
            this.date = date;
        }

        String getTitle()
        {
            return title;
        }

        Integer getDate()
        {
            return date;
        }

        @Override
        public String toString()
        {
            return title + ": " + date;
        }
    }

}
Marco13
  • 53,703
  • 9
  • 80
  • 159
2

Using reflection in most cases is the last option.

Your problem could be solved just providing a key extractor to you method instead of digging properties with Reflection API

Check the code below:

import lombok.Data;

import java.time.Instant;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Comparator;
import java.util.List;
import java.util.function.Function;

@Data
public class Task {
    String title;
    Instant date;
    String project;

    public Task(String title, Instant date, String project) {
        this.title = title;
        this.date = date;
        this.project = project;
    }

    @Override
    public String toString() {
        return "Task{" +
                "title='" + title + '\'' +
                ", date=" + date +
                ", project='" + project + '\'' +
                '}';
    }

    public static void sort(Collection<Task> tasks, Function<Task, Comparable> keyExtractor) {
        tasks.stream()
                .sorted(Comparator.comparing(keyExtractor))
                .forEach(System.out::println);
    }

    public static void main(String[] args) {
        List<Task> tasks = new ArrayList<>(3);
        tasks.add(new Task("title1", Instant.now().minusMillis(3), "project3"));
        tasks.add(new Task("title2", Instant.now().minusMillis(1), "project2"));
        tasks.add(new Task("title3", Instant.now().minusMillis(2), "project1"));

        System.out.println("Sorted by title");
        sort(tasks, Task::getTitle);
        System.out.println("Sorted by date");
        sort(tasks, Task::getDate);
        System.out.println("Sorted by project");
        sort(tasks, Task::getProject);

    }

}

The output executing main is:

    Sorted by title
    Task{title='title1', date=2019-10-09T13:42:04.301Z, project='project3'}
    Task{title='title2', date=2019-10-09T13:42:04.303Z, project='project2'}
    Task{title='title3', date=2019-10-09T13:42:04.302Z, project='project1'}
    Sorted by date
    Task{title='title1', date=2019-10-09T13:42:04.301Z, project='project3'}
    Task{title='title3', date=2019-10-09T13:42:04.302Z, project='project1'}
    Task{title='title2', date=2019-10-09T13:42:04.303Z, project='project2'}
    Sorted by project
    Task{title='title3', date=2019-10-09T13:42:04.302Z, project='project1'}
    Task{title='title2', date=2019-10-09T13:42:04.303Z, project='project2'}
    Task{title='title1', date=2019-10-09T13:42:04.301Z, project='project3'}
Ezequiel
  • 3,477
  • 1
  • 20
  • 28
  • Good answer, but `Function` should be `Function>`. Using Comparable without a type will generate a raw type warning and will deactivate generic type checking for that parameter. – VGR Oct 09 '19 at 14:37