1. Interfaces
To understand what lambda functions are, you first need to understand what interfaces are. So, let's recall the main points.
An interface is a variation of the concept of a class. A heavily truncated class, let's say. Unlike a class, an interface cannot have its own variables (except static ones). You also cannot create objects whose type is an interface:
- You can't declare variables of the class
- You can't create objects
Example:
interface Runnable
{
void run();
}
Using an interface
So why is an interface needed? Interfaces are only used together with inheritance. The same interface can be inherited by different classes, or as is also said — classes implement the interface.
If a class implements an interface, it must implement the methods declared by but not implemented by the interface. Example:
interface Runnable
{
void run();
}
class Timer implements Runnable
{
void run()
{
System.out.println(LocalTime.now());
}
}
class Calendar implements Runnable
{
void run()
{
var date = LocalDate.now();
System.out.println("Today: " + date.getDayOfWeek());
}
}
The Timer
class implements the Runnable
interface, so it must declare inside itself all the methods that are in the Runnable
interface and implement them, i.e. write code in a method body. The same goes for the Calendar
class.
But now Runnable
variables can store references to objects that implement the Runnable
interface.
Example:
Code | Note |
---|---|
|
The run() method in the Timer class will be calledThe run() method in the Timer class will be calledThe run() method in the Calendar class will be called
|
You can always assign an object reference to a variable of any type, as long as that type is one of the object's ancestor classes. For the Timer
and Calendar
classes, there are two such types: Object
and Runnable
.
If you assign an object reference to an Object
variable, you can only call the methods declared in the Object
class. And if you assign an object reference to a Runnable
variable, you can call the methods of the Runnable
type.
Example 2:
ArrayList<Runnable> list = new ArrayList<Runnable>();
list.add (new Timer());
list.add (new Calendar());
for (Runnable element: list)
element.run();
This code will work, because the Timer
and Calendar
objects have run methods that work perfectly well. So, calling them is not a problem. If we had just added a run() method to both classes, then we wouldn't be able to call them in such a simple way.
Basically, the Runnable
interface is only used as a place to put the run method.
2. Sorting
Let's move on to something more practical. For example, let's look at sorting strings.
To sort a collection of strings alphabetically, Java has a great method called Collections.sort(collection);
This static method sorts the passed collection. And in the process of sorting, it performs pairwise comparisons of its elements in order to understand whether elements should be swapped.
During sorting, these comparisons are performed using the compareTo
() method, which all the standard classes have: Integer
, String
, ...
The compareTo() method of the Integer class compares the values of two numbers, while the compareTo() method of the String class looks at the alphabetical order of strings.
So a collection of numbers will be sorted in ascending order, while a collection of strings will be sorted alphabetically.
Alternative sorting algorithms
But what if we want to sort strings not alphabetically, but by their length? And what if we want to sort numbers in descending order? What do you do in this case?
To handle such situations, the Collections
class has another sort()
method that has two parameters:
Collections.sort(collection, comparator);
Where comparator is a special object that knows how to compare objects in a collection during a sort operation. The term comparator comes from the English word comparator, which in turn derives from compare, meaning "to compare".
So what is this special object?
Comparator
interface
Well, it's all very simple. The type of the sort()
method's second parameter is Comparator<T>
Where T is a type parameter that indicates the type of the elements in the collection, and Comparator
is an interface that has a single int compare(T obj1, T obj2);
method
In other words, a comparator object is any object of any class that implements the Comparator interface. The Comparator interface looks very simple:
public interface Comparator<Type>
{
public int compare(Type obj1, Type obj2);
}
The compare()
method compares the two arguments that are passed to it.
If the method returns a negative number, that means obj1 < obj2
. If the method returns a positive number, that means obj1 > obj2
. If the method returns 0, that means obj1 == obj2
.
Here's an example of a comparator object that compares strings by their length:
public class StringLengthComparator implements Comparator<String>
{
public int compare (String obj1, String obj2)
{
return obj1.length() – obj2.length();
}
}
To compare string lengths, simply subtract one length from the other.
The complete code for a program that sorts strings by length would look like this:
public class Solution
{
public static void main(String[] args)
{
ArrayList<String> list = new ArrayList<String>();
Collections.addAll(list, "Hello", "how's", "life?");
Collections.sort(list, new StringLengthComparator());
}
}
class StringLengthComparator implements Comparator<String>
{
public int compare (String obj1, String obj2)
{
return obj1.length() – obj2.length();
}
}
3. Syntactic sugar
What do you think, can this code be written more compactly? Basically, there is only one line that contains useful information — obj1.length() - obj2.length();
.
But code can't exist outside a method, so we had to add a compare()
method, and in order to store the method we had to add a new class — StringLengthComparator
. And we also need to specify the types of the variables... Everything seems to be correct.
But there are ways to make this code shorter. We've got some syntactic sugar for you. Two scoops!
Anonymous inner class
You can write the comparator code right inside the main()
method, and the compiler will do the rest. Example:
public class Solution
{
public static void main(String[] args)
{
ArrayList<String> list = new ArrayList<String>();
Collections.addAll(list, "Hello", "how's", "life?");
Comparator<String> comparator = new Comparator<String>()
{
public int compare (String obj1, String obj2)
{
return obj1.length() – obj2.length();
}
};
Collections.sort(list, comparator);
}
}
You can create an object that implements the Comparator
interface without explicitly creating a class! The compiler will create it automatically and give it some temporary name. Let's compare:
Comparator<String> comparator = new Comparator<String>()
{
public int compare (String obj1, String obj2)
{
return obj1.length() – obj2.length();
}
};
Comparator<String> comparator = new StringLengthComparator();
class StringLengthComparator implements Comparator<String>
{
public int compare (String obj1, String obj2)
{
return obj1.length() – obj2.length();
}
}
The same color is used to indicate identical code blocks in the two different cases. The differences are quite small in practice.
When the compiler encounters the first block of code, it simply generates a corresponding second block of code and give the class some random name.
4. Lambda expressions in Java
Let's say you decide to use an anonymous inner class in your code. In this case, you will have a block of code like this:
Comparator<String> comparator = new Comparator<String>()
{
public int compare (String obj1, String obj2)
{
return obj1.length() – obj2.length();
}
};
Here we combine the declaration of a variable with the creation of an anonymous class. But there is a way to make this code shorter. For example, like this:
Comparator<String> comparator = (String obj1, String obj2) ->
{
return obj1.length() – obj2.length();
};
The semicolon is needed because here we have not only an implicit class declaration, but also the creation of a variable.
Notation like this is called a lambda expression.
If the compiler encounters notation like this in your code, it simply generates the verbose version of the code (with an anonymous inner class).
Note that when writing the lambda expression, we omitted not only the name of the Comparator<String>
class, but also the name of the int compare()
method.
The compile will have no problem determining the method, because a lambda expression can be written only for interfaces that have a single method. By the way, there is a way to get around this rule, but you'll learn about that when you start to study OOP in greater depth (we're talking about default methods).
Let's look at the verbose version of the code again, but we'll gray out the part that can be omitted when writing a lambda expression:
Comparator<String> comparator = new Comparator<String>()
{
public int compare (String obj1, String obj2)
{
return obj1.length() – obj2.length();
}
};
It seems that nothing important was omitted. Indeed, if the Comparator
interface has only the one compare()
method, the compiler can entirely recover the grayed-out code from the remaining code.
Sorting
By the way, now we can write the sorting code like this:
Comparator<String> comparator = (String obj1, String obj2) ->
{
return obj1.length() – obj2.length();
};
Collections.sort(list, comparator);
Or even like this:
Collections.sort(list, (String obj1, String obj2) ->
{
return obj1.length() – obj2.length();
}
);
We simply immediately replaced the comparator
variable with the value that was assigned to the comparator
variable.
Type inference
But that's not all. The code in these examples can be written even more compactly. First, the compiler can determine for itself that the obj1
and obj2
variables are Strings
. And second, the curly braces and return statement can also be omitted if you only have a single command in the method code.
The shortened version would be like this:
Comparator<String> comparator = (obj1, obj2) ->
obj1.length() – obj2.length();
Collections.sort(list, comparator);
And if instead of using the comparator
variable, we immediately use its value, then we get the following version:
Collections.sort(list, (obj1, obj2) -> obj1.length() — obj2.length() );
Well, what do you think of that? Just one line of code with no superfluous information — only variables and code. There's no way to make it shorter! Or is there?
5. How it works
In fact, the code can be written even more compactly. But more on that later.
You can write a lambda expression where you would use an interface type with a single method.
For example, in the code Collections.sort(list, (obj1, obj2) -> obj1.length() - obj2.length());
, you can write a lambda expression because the sort()
method's signature is like this:
sort(Collection<T> colls, Comparator<T> comp)
When we passed the ArrayList<String>
collection as the first argument to the sort method, the compiler was able to determine that the type of the second argument is Comparator<String>
. And from this, it concluded that this interface has a single int compare(String obj1, String obj2)
method. Everything else is a technicality.
GO TO FULL VERSION