Sunday, November 10, 2019

Java 8 Lambdas - Introduction to Lambdas

Java 8 Lambdas

Lambdas are introduced since Java 8. Beside the lambdas concept, Java 8 offers also a lot of support in order to ease the writing of lambdas.
Lambdas are often referred as expressions because a lambda look like an expression, but they are also referred as functions because in essence the body of a lambda is a function. Lambdas were introduced to allow functional programming which means programming with functions and not programming with objects and their methods. Once lambdas are understood, the code will be more readable and more reusable by reducing the boiler plate code. They also enable parallel processing over Java sets of elements.

The article is designing in 2 chapters.
The first chapter will focus on concept of lambdas by presenting how a lambda is formed of and by applying simple operations with simple lambda expressions. We'll entitle it "Introduction to Lambdas".
The second chapter will try to show the benefits of lambdas especially on Java Collections. We'll entitle it "Benefits of Lambdas".

I. Introduction to Lambdas

1. How a lambda expression is formed ?

The body of a lambda is formed of:
- the left side - the arguments for a lambda function;
- the "->" lambda operator;
- the right side - the lambda function, or the lambda behavior;

For example:
(Integer i) -> System.out.println("printing integer: " + i);
This should be a lambda expression that prints the value of Integer i, which is passed as argument.

Other example:
(int a, int b) -> {return (a + b);};
This should be lambda used to add two ints "a" and "b".

2. The type of a lambda expression
We seen before how a lambda expression looks. But how it can be used?
It may be used by defining its type. The type of a lambda expression is always an interface having defined ONLY a single method which is an abstract method and whose implementation body will represent the right side of a lambda expression. This type is called functional interface, because it consists of an interface to a function.

For example, we have the following classic code:
 public static void main(String[] args) {

  // initialize math inputs
  int x = 15;
  int y = 4;

  // classic instantiation

  // instantiate ClassicMathOperations and call to classic add
  ClassicMathOperations classic = new ClassicMathOperations();
  System.out.println("classic add result:" + classic.add(x, y));
 }

 static class ClassicMathOperations {

  public int add(int a, int b) {
   return a + b;
  }
 }

This code will add two ints "x = 15" and "y = 4" and will out:
> classic add result:19

This code may be transformed using lambdas in the following way:
 public static void main(String[] args) {

  // initialize math inputs
  int x = 15;
  int y = 4;

  // declare LambdaMathOperations type and define lambda add expression
  LambdaMathOperations lambda = (int a, int b) -> {return (a + b);};
  System.out.println("lambda add result:" + lambda.mathOperation(x, y));
 }

 /**
  * the lambda type definition as an interface with ONLY one method definition.
  */
 @FunctionalInterface
 interface LambdaMathOperations {

  int mathOperation(int a, int b);

 }

The above code will out:
> lambda add result:19

We may see here that for the lambda expression "(int a, int b) -> {return (a + b);}", we assigned the type "LambdaMathOperations", which is defined as a functional interface with ONLY one method called "mathOperation". The "mathOperation" will accept 2 arguments: "int a" and "int b" and its result will also be an "int". In this case, Java will analyze the lambda expression and will pass the two arguments "int a" and "int b" to the "mathOperation" method of "LambdaMathOperations" interface and also will define the behaviour of the "mathOperation" method, by passing the right side of the expression "(a + b)".

We may notice that the interface is annotated with "@FunctionalInterface". This annotation is not mandatory, but it will allow us to create a valid functional interface by simply ensuring we have defined the lambda expression type as an interface with ONLY one method defined.

3. Lambda type inference
It is obvious that when we define the "mathOperation" method, we have to provide the type of the 2 input arguments "a" and "b".
As a consequence of this, Java will know the arguments type and we may simplify the lambda declaration by not providing the arguments types:
"LambdaMathOperations lambda = (a, b) -> {return (a + b);};"
Furthermore, when the lambda expression body consists of a single returned line code (in our case it is "{return (a + b);}"), by knowing the "mathOperation" method result type (which is "int"), Java 8 compiler is optimized not to expect in clear the "return" statement and also the accolades. More exactly we may write the lambda expression as:
"(a, b) -> a + b"
Also, when we have to pass ONLY one argument in a lambda expression, the argument parantheses may be ommited, so the lambda expression:
"(Integer i) -> System.out.println("printing integer: " + i)"
, may be written as:
"i -> System.out.println("printing integer: " + i)"

4. Lambda using closures
Having the above code written for lambda expression transformation, we may re-write the code as:
 public static void main(String[] args) {

  // initialize math inputs
  int x = 15;
  int y = 4;

  // declare the LambdaMathOperationsOneArg type and define lambda addition of
  // x and y by using closures
  LambdaMathOperationsOneArg lambdaOneArg = a -> (a + y);
  System.out.println("lambda add result with closures, one arg:" + lambdaOneArg.mathOperation(x));

  // declare the LambdaMathOperationsOneArg type and define lambda addition of
  // x and y by using closures
  LambdaMathOperationsNoArg lambdaNoArg = () -> (x + y);
  System.out.println("lambda add result with closures, no arg:" + lambdaNoArg.mathOperation());
 }

 /**
  * the lambda type definition as an interface with ONLY one method definition.
  */
 @FunctionalInterface
 interface LambdaMathOperationsOneArg {

  int mathOperation(int a);

 }

 /**
  * the lambda type definition as an interface with ONLY one method definition.
  */
 @FunctionalInterface
 interface LambdaMathOperationsNoArg {

  int mathOperation();

 }

The code above will produce the result:
> lambda add result with closures, one arg:19
> lambda add result with closures, no arg:19

The above construction is possible as we already initialized in the same main() method the variables "int x = 15" and "int y = 4". Even if the 2 variables have not the "final" modifier in their declaration, they are considered by Java 8 as effectively final variables. Due to this, we may directly pass these arguments to our lambda body expression.

4. Java 8 Predefined Functional Interfaces
Looking at the code above, some might say that using lambdas is not simplify the code very much, as we still have to define the labda type as a functional interface.
Well, in order to ensure a better support for lambdas, Java 8 has predefined functional interfaces. You may check these functional interfaces by accessing the package "java.util.functions.*".

4.1. java.util.functions.BiFunction
Let's take into consideration the functional interface java.util.functions.BiFunction:
@FunctionalInterface
public interface BiFunction<T, U, R> {
...
    R apply(T t, U u);
...
}
We may see here that BiFunction expects 3 argument types "(T, U, R)", from which "T" and "U" are the types of the input arguments, while "R" is the result type of "apply" method.
So, we may rewrite our code as:
 public static void main(String[] args) {

  // initialize math inputs
  int x = 15;
  int y = 4;

  // replace LambdaMathOperations functionalInterface with already defined
  // BiFunction functional interface and define lambda for add x + y
  BiFunction<Integer, Integer, Integer> biFn = (a, b) -> (a + b);
  System.out.println("lambda add with BiFunction result:" + biFn.apply(x, y));
 }

We see that we are no more dealing with our LambdaOperations functional interface because we replaced it with already predefined Java 9 functional interface java.util.functions.BiFunction.

4.2. java.util.functions.BiConsumer
Let's take into consideration java.util.functions.BiConsumer:
@FunctionalInterface
public interface BiConsumer<T, U> {
...
    void accept(T t, U u);
...
}
We may see here that "BiConsumer" expects 2 argument types "(T, U)", from which "T" and "U" are the types of the input arguments, while the method "accept" has no result type (void result).
So, we may rewrite our code as:
 public static void main(String[] args) {

  // initialize math inputs
  int x = 15;
  int y = 4;

  // replace LambdaMathOperations functionalInterface with already defined
  // BiConsumer functional interface and define lambda for add x + y
  BiConsumer<Integer, Integer> biC = (a, b) -> System.out.println(a + b);
  System.out.print("lambda add with BiConsumer result:");
  biC.accept(x, y);
 }

We see that we are no more dealing with our "LambdaOperations" functional interface because we replaced it with already predefined Java 9 functional interface "java.util.functions.BiConsumer".

4.3 java.util.functions.Function
Let's take into consideration java.util.functions.Function:
@FunctionalInterface
public interface Function<T, R> {
...
    R apply(T t);
...
}
We may see here that "Function" expects 2 argument types "(T, R)", from which "T" is the type of ONLY one input argument, while "R" is the result type of "apply" method.
So, we may rewrite our code as:
 public static void main(String[] args) {

  // initialize math inputs
  int x = 15;
  int y = 4;

  // replace LambdaMathOperationsOneArg functionalInterface with already defined
  // Function functional interface and closures (assumes y is
  // final) and define lambda for add x + y
  Function<Integer, Integer> f = a -> (a + y);
  System.out.println("lambda add with Function result:" + f.apply(x));
 }

We see that we are no more dealing with our "LambdaOperationsOneArg" functional interface because we replaced it with already predefined Java 9 functional interface "java.util.functions.Function". In this example we also passed the "y" effectively final variable.

4.4. java.util.functions.Consumer
Let's take into consideration java.util.functions.Consumer:
@FunctionalInterface
public interface Consumer<T> {
...
    void accept(T t);
...
}
We may see here that "Consumer" expects 1 input argument type "T", while the method "accept" has no result type (void result).
So, we may rewrite our code as:
 public static void main(String[] args) {

  // initialize math inputs
  int x = 15;
  int y = 4;

  // replace LambdaMathOperationsNoArg functionalInterface with already defined
  // Consumer functional interface and closures (assumes x and y are
  // final) and define lambda for add x + y
  Consumer<Integer> c = a -> System.out.println(a);
  System.out.print("lambda add with Consumer result:");
  c.accept(x + y);
 }

We see that we are no more dealing with our "LambdaOperationsNoArg" functional interface because we replaced it with already predefined Java 9 functional interface "java.util.functions.Consumer".

5. Lambda Method Reference

Any lambda expressions with a single method of the form:
"() -> Object.Method()"
, or:
"(s) -> Object.Method(s)"
, or:
"(objectInstance, s) -> objectInstance.Method(s)"
, may be transformed in:
"Object::Method"

In all cases, we observe that the lambda expression is transformed in a reference to a method of an Object.

In case of:
"() -> Object.Method()"
, we'll have the following sample:
public class LambdaOperationsMain {

 public static void main(String[] args) {
  // String operations method reference

  // use LambdaEmpty functional interface and printHello
  LambdaEmpty le = () -> LambdaOperationsMain.printHello();
  le.acceptEmpty();
 }

 static void printHello() {
  System.out.println("lambda String Hello!");
 }

 @FunctionalInterface
 interface LambdaEmpty {

  void acceptEmpty();

 }
}

When we run this, it will output:
> lambda String Hello!

To use method reference, the above code will be:
public class LambdaOperationsMain {

 public static void main(String[] args) {
  // String operations method reference

  // use LambdaEmpty functional interface and printHello with
  // method reference
  LambdaEmpty lRef = LambdaOperationsMain::printHelloReference;
  lRef.acceptEmpty();
 }

 static void printHelloReference() {
  System.out.println("lambda String Hello with method reference!");
 }

 @FunctionalInterface
 interface LambdaEmpty {

  void acceptEmpty();

 }
}

When we run this, it will output:
> lambda String Hello with method reference!

We observe that the lambda expression:
"() -> LambdaOperationsMain.printHello()"
, was transformed in:
"LambdaOperationsMain::printHelloReference"
, by referring the "printHelloReference" method of "LambdaOperationsMain".

Let's consider the case:
"(s) -> Object.Method(s)"
We will took the code presented at "4.4. java.util.functions.Consumer" and we will rewrite it as:

 public static void main(String[] args) {

  // initialize math inputs
  int x = 15;
  int y = 4;

  // replace LambdaMathOperationsNoArg functionalInterface with already defined
  // Consumer functional interface and closures (assumes x and y are
  // final), with method reference and define lambda for add x + y
  Consumer<Integer> c = System.out::println;
  System.out.print("lambda add with Consumer result:");
  c.accept(x + y);
 }

In this case we transformed the lambda expression:
"a -> System.out.println(a)"
, in:
"System.out::println",
by referring the "println" method of "System.out".

Another sample:

 public static void main(String[] args) {

  // initialize input
  String s = "a string";

  // use already defined Function functional interface, with method reference,
  // to print String s length
  Function<String, Integer> fStr = a -> a.length();
  System.out.print("lambda String length with Function result:");
  System.out.println(fStr.apply(s));
 }

The above may be transformed in:

 public static void main(String[] args) {

  // initialize input
  String s = "a string";

  // use already defined Function functional interface, with method reference,
  // to print String s length
  Function<String, Integer> fStrRef = String::length;
  System.out.print("lambda String length with Function and method reference result:");
  System.out.println(fStrRef.apply(s));
 }

, where we transformed the lambda expression:
"a -> a.length()"
, in:
"String::length"
, by referring the "length" method of "String".

For the case:
"(objectInstance, s) -> objectInstance.Method(s)"
, we consider the code:

 public static void main(String[] args) {

  // initialize inputs
  String s = "a string";
  String subStr = "tr";

  // use already defined BiFunction functional interface,
  // to print the index of subStr in str
  BiFunction<String, String, Integer> biFStr = (a, b) -> a.lastIndexOf(b);
  System.out.print("lambda String index occurence with BiFunction result:");
  System.out.println(biFStr.apply(str, subStr));

  // use already defined BiFunction functional interface, with method reference
  // to print the index of subS in s
  BiFunction<String, String, Integer> biFStrRef = String::lastIndexOf;
  System.out.print("lambda String index occurence with BiFunction and method reference result:");
  System.out.println(biFStrRef.apply(str, subStr));
 }

Here we transformed the lambda expression:
"(a, b) -> a.lastIndexOf(b)"
, in:
"String::lastIndexOf"
, by referring the "lastIndexOf" method of "String".

For the overall code of the above, please write me at "vs_octavian[around]yahoo[dot]com".

Next chapter -> II. Benefits of Lambdas

No comments:

Post a Comment