Java 8 has introduced a new feature called the ‘Method Reference Operator’. In this article, we’ll be explaining what method references are, as well as the different types of method references available. In order to understand them, you’ll need some basic knowledge of lambda expressions and functional interfaces.
A brief introduction to Method References
In Java 8, the method reference operator is used to refer to an existing method in a class and is represented by two double colons (::). Previously, if you wanted to implement an interface prior to Java 8, you’d need to create a class that implements the interface and overrides the abstract method within the same interface; resulting in a lot of boilerplate code.
Fortunately, Java 8 introduced functional interfaces and lambda expressions to overcome this issue. Now when a functional interface is implemented (via a lambda expression), it gets rid of all the boilerplate code and helps keep it concise. The best part? Method references go one step further and help reduce the code within a lambda expression. Think about it. Sometimes, your class may already have a method that has the same code that you’d like to specify in a lambda expression. Instead of having to rewrite it using a lambda expression, you can instead choose to use the method reference operator and keep things extra concise.
How Method References work
Consider the following functional interface:
@FunctionalInterface
public interface Calculator {
public int calculate(int num1,int num2);
}
The code above is an example of a functional interface called Calculator. It has a single method called calculate that accepts two integer values and returns an integer.
Now consider the following class:
public class MethodReferenceDemo {
public static void main(String[] args) {
Calculator cal1 = (num1,num2) -> num1+num2;
int additionResult = cal1.calculate(4, 6);
System.out.println("Result of addition:"+additionResult);
}
}
The MethodReferenceDemo class has a main method that implements the Calculator interface via a lambda expression. The lambda expression simply returns the result of adding the two input numbers.
Now suppose the MethodReferenceDemo class already has a method like so:
public static int multiply(int num1,int num2) {
return num1*num2;
}
And suppose you want to use the Calculator interface to multiply the input arguments. Using a lambda expression, you’ll need to write code as follows:
Calculator cal2 = (num1,num2) -> num1*num2;
int multiplicationresult = cal2.calculate(4,6);
System.out.println("Result of multiplication:"+multiplicationresult);
This code is similar to the previous code written for addition, meaning that the lambda expression multiples the input arguments and returns the result; However, there is already a multiply method in the code that does the same thing and the lambda expression unnecessarily repeats the code in the multiply method. When this happens, the method reference operator can be used in order to refer the existing method.
The above code can be re-written as follows:
Calculator cal2 = MethodReferenceDemo::multiply;
This means that the multiply method from the MethodReferenceDemo class should be used to implement the calculate method in the Calculator interface. Compared to the code that uses a lambda expression, this code is more concise and is easy to both read and understand.
Types of Method References
There are four types of method references:
Static Method References
A static method reference occurs when a static method of a class is referenced via the method reference operator.
Its syntax is as follows:
classname::staticMethodName
Here, classname refers to the name of the class whose method is being invoked and staticMethodName refers to the method itself that is being invoked. Since a static method of a class is being referenced, the class name needs to be specified in static method reference.
In this example, the MethodReferenceDemo class has a static method called multiply which is referenced via the method reference operator.
Instance Method References
An instance method reference happens when an instance method is referenced via the method reference operator.
Its syntax looks like so:
objectName::instancemethodname
objectName refers to the object whose method is being invoked and instanceMethodName refers to the method being invoked. Since an instance method of a class is being referenced, an object needs to be specified in instance method references.
We can change the code above like so:
public class InstanceMethodReferenceDemo {
public int multiply(int num1,int num2) {
return num1*num2;
}
public static void main(String[] args) {
InstanceMethodReferenceDemo obj = new InstanceMethodReferenceDemo();
Calculator cal2 = obj::multiply;
int multiplicationResult = cal2.calculate(4, 6);
System.out.println("Result of multiplication:"+multiplicationResult);
}
}
Now, the multiply method is no longer a static method. Instead, it’s an instance method. In the main method, an object obj of type InstanceMethodReferenceDemo is created. The method reference operator uses this obj to refer to the multiply method.
When this code is executed, it will print the following output:
Result of multiplication:24
Reference to an Arbitrary Method
This happens when the code references an instance method of a class, but not on any specific object via the method reference operator.
The syntax for this looks like so:
classname::instancemethodname
Here, classname refers to the name of the class whose method is being invoked and instanceMethodName refers to the method being invoked.
The following code demonstrates this:
public class Animal {
private String name;
private String food;
public Animal(String name,String food) {
this.name = name;
this.food = food;
}
//getters and setters
public void print() {
System.out.println(name+" eats "+food);
}
}
This is an Animal class that has two fields corresponding to name and food. It has a print method that prints the animal name and the food that it eats.
Now consider the following code:
public class ArbitraryMethodReferenceDemo {
public static void main(String[] args) {
List<Animal> animals = new ArrayList<Animal>();
animals.add(new Animal("cat","fish"));
animals.add(new Animal("dog","bone"));
animals.add(new Animal("cow","grass"));
animals.forEach(animal -> animal.print());
}
Within the example above, a List of animal objects is created and initialised to some values. The forEach method is invoked on the animals list which invokes the print method on each animal in the list.
This code can be re-written using an arbitrary method reference:
animals.forEach(Animal::print);
In the above example, a method reference is used instead of a lambda expression. The method reference indicates that the print method from the Animal class should be invoked on each animal object. Since the print method is not invoked on a specific object, but on all objects, the class name is specified in the method reference.
When this code is executed, it will print the following output:
cat eats fish
dog eats bone
cow eats grass
Constructor References
Constructor references occur when the constructor of a class is referenced via the method reference operator.
The syntax for this looks like the following:
Classname::new
Classname refers to the name of the class whose constructor is being invoked.
Consider the following code:
public class Fruit {
private String name;
private int calorieCount;
public Fruit(String name) {
super();
this.name = name;
this. calorieCount = 0;
}
This is a Fruit class with two fields: name and calorieCount. It has a constructor which initialises the name field to the value passed in and the calorieCount to 0.
Now consider the following:
public class ConstructorReferenceDemo {
public static void main(String[] args) {
List<String> fruitNames = Arrays.asList("Mango","Banana","Apple","Orange");
List<Fruit> fruits = fruitNames.stream().map(str -> new Fruit(str)).collect(Collectors.toList());
fruits.forEach(fruit -> System.out.println("Fruit name is "+fruit.getName()));
}
}
Here, an ArrayList is initialised to some String values (which are names of fruits). The code uses the Stream map operation to convert each String to a Fruit object via a lambda expression and this same lambda expression then uses the Fruit constructor to create a Fruit object using the String fruit name. Finally, the collect method converts the fruit objects into a List.
The code can be re-written using a constructor reference as follows:
List<Fruit> fruitsList = fruits.stream().map(Fruit::new).collect(Collectors.toList());
In the example above, instead of using a lambda expression, a constructor reference is used to refer to the Fruit constructor. So the Fruit constructor will be invoked with each String in the input stream and a new Fruit object will be returned.
When the code is executed, it will print the following output:
Fruit name is Mango
Fruit name is Banana
Fruit name is Apple
Fruit name is Orange
The following code also demonstrates a constructor reference using a Supplier interface:
Supplier<Date> dateGenerator = () -> new Date();
Supplier is an in-built functional interface that does not accept any arguments but can return an object of any data type. Here, it returns a Date instance. This code can be re-written using a constructor reference as follows:
Supplier<Date> dateGenerator = Date::new;
Instead of using a lambda expression, the Date class constructor is referenced via a constructor reference in the example above.
Lambda expressions were introduced to get rid of the amount of boilerplate code involved in implementing an interface. Method references are designed to make life easier and they’re a great tool to use for reducing the code required to be written in a lambda expression. Ultimately, method references make code concise and increase the readability. It’s a win-win.
Like what you've read? If you're looking for a new technical role, make sure to say hi!