DEV Community

Liu Yongliang
Liu Yongliang

Posted on

Exploring Java Method References - [OOP & Java #13]

Motivation

According to Oracle's the Java tutorials:

You use lambda expressions to create anonymous methods. Sometimes, however, a lambda expression does nothing but call an existing method. In those cases, it's often clearer to refer to the existing method by name. Method references enable you to do this; they are compact, easy-to-read lambda expressions for methods that already have a name.

I am aware of method references and thought that they are what they are: a more succinct way of referring to methods without providing explicit information that the compiler already know. What I did not realize was that method references can be categorized into four different types (summary below). Among the four types, 3 of them are fairly easy to understand:

  • Call to a constructor by doing ClassName::new instead of () -> new ClassName()
  • Call to a static method by doing ClassName::methodName instead of () -> ClassName.methodName()
  • Call to an instance method of a particular object by doing ObjectName::methodName instead of () -> obj.methodName()

What intrigued me was the four conditions: call to an instance method of a particular class. In that case, will JVM be creating an instance of that class and then invoking the instance method via the newly created instance?

Summary table

Type (Reference to) Syntax Example Lambda Equivalent
a static method ContainingClass::staticMethodName Integer::parseInt str -> Integer.parseInt(str)
an instance method of a particular object containingObject::instanceMethodName Instant.now()::isAfter Instant then = Instant.now(); t -> then.isAfter(t)
an instance method of an arbitrary object of a particular type ContainingType::methodName String::toLowerCase str -> str.toLowerCase()
a constructor ClassName::new int[]::new HashMap::new len -> new int[len]; () -> new HashMap()

Note: based on summaries from Oracle's the Java tutorials and Effective Java
Image:
image

The hunt

After some Googling, I found a few articles about method references. So there are in fact a few ways we can do a method reference to an instance method without specifying the instance in the method reference.
Examples:

// Method references and their equivalent lambda expressions
String::toLowerCase // (a) -> a.toLowerCase()
String::compareToIgnoreCase // (a, b) -> a.compareToIgnoreCase(b)
Integer::compareTo // (a, b) -> a.compareTo(b)

Enter fullscreen mode Exit fullscreen mode

In the above cases, it can be seen that the instance method is going to be invoked on the instance that is passed in as the first parameter. So JVM did not magically create an instance just to invoke an instance method on the specified class.

So in fact, this is a case of bound vs unbound.

In bounded reference, the receiving object is specified in the method reference. Bound references are similar in nature to static references: the function object takes the same arguments as the referenced method.

In unbound references, the receiving object is specified when the function object is applied, via an additional parameter before the method’s declared parameters. Unbound references are often used as mapping and filter functions in stream pipelines

In my own words:

  • Bounded means we specify the object that we are going to call the instance method on.
  • Unbounded means we specify that the instance method is going to be called from the first parameter that is passed in, so the first parameter is the object.

Thoughts

The concept of method references is not difficult. I thought it is interesting to write about it because of the process of how I came to learn more about them.

After some initial research, I came across this article which says the following:

String::toLowerCase is an unbound non-static method reference that identifies the non-static String toLowerCase() method of the String class. However, because a non-static method still requires a receiver object (in this example a String object, which is used to invoke toLowerCase() via the method reference), the receiver object is created by the virtual machine. toLowerCase() will be invoked on this object.

The receiver object is created by the virtual machine? I thought to myself: if JVM is going to create an object of the class specified in the method reference in order to call the instance method, how does it know which constructor to invoke? If it simply calls the default constructor, i.e. new ClassName(), what about classes that do not have such a parameterless constructor?

So I opened up jshell and typed in a bunch of classes to verify it:

// driver
class Test { 
    static void print(Function<String, String> fn, String s) {
        System.out.println(fn.apply(s));
    }
}

// test 1
class MyString {
    String toLowerCase(String s) {
        return s.toLowerCase();
    }
}

// test 2
class MyOtherString {
    int one;
    MyOtherString(int one) {
        this.one = one;
    }
    String toLowerCase(String s) {
        return s.toLowerCase();
    }
}

// test
Test.print(String::toLowerCase, "HELLO WORLD");
Test.print(MyString::toLowerCase, "HELLO WORLD");
Test.print(MyOtherString::toLowerCase, "HELLO WORLD");

Enter fullscreen mode Exit fullscreen mode

The error messages for my last two test cases are as follows:

| Error:
| incompatible types: invalid method reference
| unexpected instance method toLowerCase(java.lang.String) found in unbound lookup

Whether my custom classes have a parameterless constructor or not, both calls did not succeed. So it was not a case whereby a magical object of MyString or MyOtherString will be created and does something like magicInstance.toLowerCase("HELLO WORLD)".

To end this little piece of article off, I thought of the quote by Albert Einstein:

“Anyone who has never made a mistake has never tried anything new.”

The process of experimenting with something new or unfamiliar is indeed a great way to learn. At least now I know more about method references and have one article written about it:)

References

Top comments (0)