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();
}
Example of a standard interface

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
Timer timer = new Timer();
timer.run();

Runnable r1 = new Timer();
r1.run();

Runnable r2 = new Calendar();
r2.run();

The run() method in the Timer class will be called


The run() method in the Timer class will be called


The 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);
}
Code for the Comparator interface

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();
   }
}
Code of the StringLengthComparator class

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();
   }
}
Sorting strings by 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);
    }
}
Sort strings by length

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();
    }
};
Anonymous inner class
Comparator<String> comparator = new StringLengthComparator();

class StringLengthComparator implements Comparator<String>
{
    public int compare (String obj1, String obj2)
    {
        return obj1.length() – obj2.length();
    }
}
StringLengthComparator class

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();
    }
};
Anonymous inner class

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();
   }
};
Anonymous inner class

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.