3

I recently put my hands on Java 8 and tried using Method References. I was trying different kinds of Method references and got stuck in the type "Reference to an Instance Method of an Arbitrary Object of a Particular Type".

String[] arr = {"First", "Second", "Third", "Fourth"};

Arrays.sort(arr, String::compareToIgnoreCase);

This works perfectly well. But when I try to refer a method of a user defined class through its type :

Demo2[] arr = {a, b};

Arrays.sort(arr, Demo2::compare);

This displays compile-time error as "Non-static method cannot be referenced from a static context".

Here's the Demo2 class :

public class Demo2 implements Comparator<Demo2> {
    Integer i;

    Demo2(Integer i1){
        i = i1;
    }

    public Integer getI() {
        return i;
    }

    @Override
    public int compare(Demo2 o1, Demo2 o2) {
        return o1.getI().compareTo(o2.getI());
    }
}
  • 2
    Look at the declaration of the `String` `compareToIgnoreCase` method. – greg-449 Aug 22 '16 at 07:49
  • 2
    In general, if you are declaring a class as a `Comparator` of itself, as opposed to `Comparable`, your design is faulty. `Comparator` is supposed to be used for comparing objects which are not naturally comparable. As an object, it is separate from the objects it compares, and only contains state that helps it in the comparison. – RealSkeptic Aug 22 '16 at 07:53
  • This seems a pretty standard confusion between `Comparator` and `Comparable`. – Louis Wasserman Aug 22 '16 at 19:27

3 Answers3

5

As greg-449 pointed to you, your code has a bug.

By making a method reference like YourObjet::yourMethod you make a static reference to the method of the instance. So the method will be called for each object and thus the signature needs to be different than the earlier

A code that will compile will be of the following form :

Demo2[] demos = {a, b};
Arrays.sort(demos, Demo2::compareTo);

public class Demo2 {
    Integer i;

    Demo2(Integer i1){
        i = i1;
    }

    public Integer getI() {
        return i;
    }

    public int compareTo(Demo2 other) {
        return this.getI().compareTo(other.getI());
    }
}

But as RealSkeptic pointed out, this is not the correct way to implement and objet comparison. You should give the Arrays.sort method a comparator instead :

 Arrays.sort(demos, (obj1, obj2) -> obj1.getI().compareTo(obj2.getI()));
N00b Pr0grammer
  • 4,503
  • 5
  • 32
  • 46
loicmathieu
  • 5,181
  • 26
  • 31
  • 2
    Actually, the use of `compareTo` in your example is perfectly fine, it is not actually part of any interface (though if you declared `Demo2 implements Comparable` it would also be fine, though then you wouldn't need `Arrays.sort(array,comparator)`). My comment was about abusing the `Comparator` interface when someone actually means `Comparable`. – RealSkeptic Aug 22 '16 at 08:40
3

The Comparator interface required for Arrays.sort(T[],Comparator<T>) has a method that accepts two object references of the same type T, and returns an integer.

There is a bit of "magic" in method references. What Java does is wrap the method in such a way that it will fit the interface requirement.

The interface, of course, doesn't require a static method. But the wrapping can create a method that calls a static method, as in the Tutorial's first example:

public static int compareByAge(Person a, Person b) {
    return a.birthday.compareTo(b.birthday);
}

It wraps it in such a way that you get something similar to

new Comparator<Person>() {
    @Override
    public int compare(Person a, Person b) {
        return Person.compareByAge(a,b);
    }
}

Which satisfies the interface.

But in the example in the section "Reference to an Instance Method of an Arbitrary Object of a Particular Type", it wraps it differently. It needs a method that receives two strings, but it has a method that only receives one. This is how String::compareToIgnoreCase is defined:

public int compareToIgnoreCase(String str)

But in this case, it's an instance method. What Java does now is, because this method belongs to an object of type String, and accepts an object of type String it is easy to build a "wrap" around it that makes it into a method that accepts two objects, much like the lambda expression:

(String a, String b) -> {
    return a.compareToIgnoreCase( b );
}

Or, if we imagine a formal wrapping as a Comparator:

new Comparator<String>() {
    @Override
    public int compare(String a, String b) {
        return a.compareToIgnoreCase(b);
    }
}

So, the fact that it's an instance method that belongs to type T, accepts type T and returns int allows Java to wrap it appropriately so it fits the Comparator interface.

But int compare(Demo2 o1, Demo2 o2) doesn't fit that pattern. It accepts two parameters. If a method accepts two parameters, it must be a static method to fit the wrapping rules - there is no way to pass the "this" object into the Comparator interface. So it tries to wrap it as a static method, and fails, as it is not a static method.

Bottom line: you got the error, because for this particular type of method reference, you need an instance method with only one parameter of the same type as the class.

As @Holger mentions in a comment, if you have a new class you are building, you shouldn't put a comparison method in it specifically for this sort of sorting task. If the class has a natural ordering, make it Comparable and use Arrays.sort(Object[]). If it doesn't, and you need to sort it sometimes based on any of its attributes, use a lambda expression or Comparator.comparing(Demo2::getI) which makes better use of an existing getter for the specific purpose of comparison.

RealSkeptic
  • 33,993
  • 7
  • 53
  • 79
1

As a convention, Comparator<T> is implemented with a lambda expression and it looks very odd to implement it in a class.

    Demo2[] array = new Demo2[2];
    array[0] = new Demo2(12);
    array[1] = new Demo2(32);

    Comparator<Demo2> demo2Comparator = (e1,e2)->e1.getI().compareTo(e2.getI());
    Arrays.sort(array, demo2Comparator);
Jude Niroshan
  • 4,280
  • 8
  • 40
  • 62