Method References

If a class has a method that is comparable to the method in a functional interface (i.e. they have the same signature) then we can store a reference to the method in a variable having the Interface type.  This allows us to pass references to methods into other methods that have parameters with functional interface types.

Consider the following class.  It has a static string, a static method, and an instance (non-static) method.  Both methods return a representation of the string.

public class Word {
    private static String word = "undefined";

    public Word (String w) {
        word = w;
    }

    public static String getWord() {
        return word;
    }

    @Override
    public String toString() {
        return "*" + word;
    }
}

Now lets define a functional interface.

interface Printable {
    String getString();
}

The Printable interface has a method that has no arguments and returns a reference to a String.  Notice that this signature is the same as the Word class’ getWord and toString methods.  We say that these methods are therefore comparable to the Printable interface’s method.  Since they are comparable, we can store a reference to getWord and toString methods in variables of type Printable.

In the program below we define a method named print that has one parameter of type Printable. We then call print twice using the :: operator.  The first time we call print we pass in a reference to the Word class’ static method named getWord.  Since the method is static we use the following form to reference the method:

className::methodName

The second time we call print we pass in a reference to an instance of the Word class’ toString method.  Since the method is an instance method we use the following form to reference the method:

objRef::methodName

class Driver {
    public static void print(Printable ref) {
        System.out.println(ref.getString());
    }

    public static void main(String ... args) {
        Word w1 = new Word("hello");
        print(Word::getWord);           // static method

        Word w2 = new Word("world");
        print(w2::toString);            // instance method
    }
}

Passing Instance Methods for Arbitrary Instances

In the above example, when we passed the toString method into print, the argument that we passed included the reference to an instance of the Word class.  This makes sense because toString is an instance method and needs an instance on which to call the method.

It is possible to pass a reference to an instance method without specifying the instance on which it will be invoked.  This allows the method to be invoked on any instance of the class.

Consider the following Num class.

class Num {
    private int num;

    public Num(int n) {
        num = n;
    }

    @Override  
    public String toString() {  
        return Integer.toString(num);  
    }

    public boolean samePolarity(Num j) {
        return num % 2 == j.num % 2;
    }

    @Override
    public boolean equals(Object o) {
        if (!(o instanceof Num)) {
            return false;
        }
        return num == ((Num) o).num;
    }
}

Aside from the usual constructor and overridden toString method, we have two methods (samePolarity and equals) that each take an object as an argument and return a boolean.   Now lets look at the generic functional interface named Test shown below.

interface Test<T> {
    boolean test(T i, T j);
}

The test method in Test has two parameters with the same type and returns a boolean.  At first glance, test does not look comparable to samePolarity and equals since they have different signatures – but they are.

Consider the following snippet of code from a driver.

Num n1 = new Num(1);
Num n2 = new Num(2);
Num n3 = new Num(3);
Test<Num> t = Num::samePolarity;

System.out.println(t.test(n1, n3));
System.out.println(t.test(n2, n1));

The first thee lines create three instances of Num and the forth line store the reference to the samePolarity method using the following form:

className::methodName

Next we call test, passing in two instances of Num, and print the result.

When test is called with two instances of Num, it calls samePolarity on the first instance and passes into the method the second instance.  In other words, the first time test is called, it executes the following:

n1.samePolarity(n3);

The second time test is called it executes the following:

n2.samePolarity(n1);

This demonstrates that we can store a reference to a class’ instance method and invoke it on arbitrary instances of the class.  This allows us to create versatile methods.  Consider the following example.  Here we have a method that takes a reference to an instance method and calls it on each of the elements passed into the method via an array.

public static <T> void print(Test<T> t, T[] arr, T j) {
    for (T i : arr) {
        System.out.println(i + "," + j + ": " + t.test(i,j));
    }
    System.out.println();
}

public static void main(String ... args) {
    Num n1 = new Num(1);
    Num n2 = new Num(2);
    Num n3 = new Num(3);

    Num[] list = {n1, n2, n3};
    print(Num::equals, list, n3);
}

When we call print, we pass in Num::equals, an array of Num references, and n3.  The print method, in its for-loop, effectively makes the following calls

n1.equals(n3)
n2.equals(n3)
n3.equals(n3)

resulting in the following output.

1,3: false
2,3: false
3,3: true

© 2017 – 2019, Eric. All rights reserved.