Understanding Lambda/Closure, Part 8 - Method References and Constructor References



Sorting a list of names by length may be done as follows:

List<String> names = Arrays.asList("Justin", "Monica", "Irene", "caterpillar");
Collections.sort(names, new Comparator<String>() {
    public int compare(String s1, String s2) {
        return s1.length() - s2.length();
    }
});

The code, however, seems unclear even if you just look at the body of the compare method. You may also want to take different sorting strategies. So, you define several static methods in a StringOrder class:

public class StringOrder {
    public static int byLength(String s1, String s2) {
        return s1.length() - s2.length();
    }

    public static int byLexicography(String s1, String s2) {
        return s1.compareTo(s2);
    }
   
    public static int byLexicographyIgnoreCase(String s1, String s2) {
        return s1.compareToIgnoreCase(s2);
    }
   
    ...
}

Now, you can rewrite the sorting code as follows:

Collections.sort(names, new Comparator<String>() {
    public int compare(String s1, String s2) {
        return StringOrder.byLength(s1, s2);
    }
});

It seems clearer about what the code does. Using JDK8 Lambda can make this code more concise.

Collections.sort(names, (s1, s2) -> NameOrder.byLength(s1, s2) );

A wise guy may find that, the byLength method has the same signature as the compare method of Comparator except the method name. We know that a lambda expression is an anonymous method (function) and the body part of a lambda expression is the method implementation of a functional interface. Since we just pass the parameters s1 and s2 to the byLength method, can we reuse the implementation of the byLength method directly? Yes, JDK8 provides method references for that.

Collections.sort(names, NameOrder::byLength);

Keeping compliance with the existing APIs is one of the chief considerations when adopting lambda in Java. Besides functional interfaces, method references also play an important part while reusing the existing APIs. You can reuse the existing method implementation to avoid writing lambda expressions everywhere. The above is one kind of method references - referring to a static method. You can also refer to an instance method of an arbitrary object of a particular type. For example, sorting a list of names lexicographically may be done as follow:

Collections.sort(names, NameOrder::byLexicography);

From previous sections, we know NameOrder::byLexicography will refer to the implementation of the byLexicography method. The above code has the same effect as the following:

Collections.sort(names, (s1, s2) -> s1.compareTo(s2));

We may find that, in the body part of the lambda expression, the first parameter is the receiver and the remaining parameter is the argument of the compareTo method. In this situation, we may refer to the compareTo method of String directly, such as the following:

Collections.sort(names, String::compareTo);

Similarly, sorting a list of names lexicographically but ignoring case differences may be done by referring to a static method:

Collections.sort(names, NameOrder::byLexicographyIgnoreCase);

Again, in the method implementation of byLexicographyIgnoreCase, the first parameter is the receiver and the remaining parameter is the argument of compareToIgnoreCase. We can refer to the compareToIgnoreCase method of String directly.

Collections.sort(names, String::compareToIgnoreCase);

It's easy to observe that, method references not only avoid rewriting lambda expressions but also make the code more concise. Besides the above two method references, we can also refer to an instance method of a particular object. For example, suppose that you're designing software for filtering the applicants of a job vacancy. You have the following two classes:

public class JobVacancy {
    ...
    public int bySeniority(JobApplicant ja1, JobApplicant ja2) {
        ...
    }
   
    public int byEducation(JobApplicant ja1, JobApplicant ja2) {
        ...
    }
    ...
}

public class JobApplicant {
    ...
}

You're using JDK8 and writing a lambda expression when sorting the applicants.

List<JobApplicant> applicants = ...;
JobVacancy vacancy = ...;
Collections.sort(applicants, (ja1, ja2) -> vacancy.bySeniority(ja1, ja2));

The lambda expression captures the object referred by vacancy. The bySeniority method has the same signature as the compare method of Comparator. In this case, we can refer to the bySeniority method of the vacancy object directly.

Collections.sort(applicants, vacancy::bySeniority);

Besides method references, JDK8 also provides constructor references. You may ask "Constructors? Do they have a return type?" Yes, they have! Every constructor has a return type - the defining class itself. For example, suppose that you have the following interfaces:

public interface Part {
    ...
}

public interface Material {
    ...
}

public interface PartFactory {
    Part createPart(Material material);
}

You write several implementations for these interfaces.

public class PartImpl implements Part {
    public PartImpl(Material material) {
        ...
    }
}

public class MaterialImpl implements Material {
    ...
}

public PartFactoryImpl implements PartFactory {
    public Part createPart(Material material) {
        return new PartImpl(material);
    }
}

Then you may use the following code to create an instance of Part:

PartFactory factory = new PartFactoryImpl();
Part part = factory.createPart(new MaterialImpl());

The implementation of the createPart method only uses the constructor to create the instance of Part. Using JDK8 can save you the time of writing the PartFactoryImpl class. You can refer to the constructor of the PartImpl class directly.

PartFactory factory = PartImpl::new;
Part part = factory.createPart(new MaterialImpl());

If a class has multiple constructors, the method signature of the functional interface is used in the same way that a constructor invocation is resolved.

Finally, we reach the end of Understanding Lambda/Closure. "Wait! Where are the discussions about default methods? Aren't they parts of Project Lambda?"

Yes, default methods are really parts of Project Lambda, but they're about evolving the existing APIs. Default methods really relax the restrictions on interface, make defensive API evolution with Java interfaces easily, and open more possibilities for reusing workflow. But they also bring back several complexities of multiple inheritances. When discussing how to evolving the existing APIs, we may also smell something functional programming. I think opening a new series of articles to discuss all these funny topics would be a good idea, so I leave them for the next series Functional Programming for Java Developers. Just stay tuned.