Bits of Java – Episode 21: Lambdas Expressions and Functional Interfaces

This week we are going to discuss about lambdas expressions in Java. They are a way to write code in a more functional way, and they could be quite useful when you need to perform just a simple operation, and you do not want to write a dedicated method just for that.

One of the simplest lambda expression you can write is:

s -> System.out.println(s)

In here you are just saying something like “Giving the parameter s, performs the action System.out.println(s). So, what comes before the arrow are the lambda parameters, and what comes after is the lambda body.

There are a few syntax rules to keep in mind when working with lambdas:

  • If you specify the type of the parameter, or if you have multiple parameters, then you need to enclose them within brackets:
(String s) -> System.out.println(s)
(a, b) -> a+b
  • If you specify the type for one of the parameters, then you need to do the same for the other parameters as well:
(int a, double b) -> a+b
  • If the lambda body requires more than one statement, then you have to enclose it within curly brackets and use the return statement:
(int a, double b) -> {
  a += 7;
  return a + b;
}
  • If you use var instead of the type for a lambda parameter, then you have to do the same for the other parameters as well:
(var a, var b) -> a+b

There are also some limitations on the use of variables within a lambda body. In particular, while class and instance variables can be called from within a lambda body without any issue, local variables can be called only if they are effectively final. This means that, or they are marked with the final specifier, or they act as if they were, in the sense that their value is never changed after initialization.

public static void main(String[] args) {
    String msg = "A message";  //effectively final
    Predicate<String> pred = (String s) -> s.length() < msg.length();

    String name = "Ilenia"; //NOT effectively final
    //DOES NOT COMPILE!!
    Consumer<String> cons = (String s) -> System.out.println(s + " " + name);
    name = "Andrea"; //name changes its value here!!
}

The type of the pred and cons variable used in the previous example are two of the Java built-in functional interfaces. What are these things exactly? They are interfaces which define one and only one abstract method. They can have also other members defined, but one and only one abstract method!

So, for instance, Predicate is defined as:

public interface Predicate<T> {
    boolean test(T obj);
}

So, what you are doing when defining a Predicate through a lambda expression is simply providing an implementation for such an abstract method, in a short way. The parameter of the lambda expression is evaluated at runtime and we can do something like:

public static void main(String[] args) {
    String msg = "A message";  
    Predicate<String> pred = (String s) -> s.length() < msg.length();
    boolean res1 = pred.test("This message is longer!"); //false
    boolean res2 = pred.test("Short"); //true
}

Consumer is another built-in Java functional interface, and its abstract method takes one parameter of generic type T and returns nothing.

public interface Consumer<T> {
    public void accept(T obj);
}

There are other useful built-in functional interfaces in Java. Among them, we could mention Supplier, which takes no input parameters, but returns an object of type T.

public interface Supplier<T> {
    public T get();
}

Another one is Comparator. This takes two input parameters and returns an int, which represents the result of the comparison between the two parameters.

public interface Comparator<T> {
    public int compare(T obj1, T obj2);
}

To compare two objects, the convention is to return a positive number whether the first object is larger, a negative number in the other case, and zero whether the two objects are the same based on the comparison criterion.

Then, of course, you can also make your own functional interfaces. If you are not sure whether an interface you have defined can be used as functional interface, just try to add the annotation @FunctionalInterface to it. If the compiler complains, then you are probably not working with a valid functional interface.

@FunctionalInterface //DOES NOT COMPILE!!
public interface NotValid {
    //two abstract methods
    boolean areWeSure(String msg);
    void print(String msg);
}
@FunctionalInterface //OK!!
public interface NotValid {
    //one abstract method
    void print(String msg);
}

And that’s it for today!

Next week we will discuss some of the differences between Comparator, that we saw today, and Comparable!

by Ilenia Salvadori