We said in the previous chapter that lambdas allow functional programming. Also, when lambdas are understood, the code with lambdas will be more readable and more reusable by reducing the boiler plate code. They also enable parallel processing.
Beside these, there are benefits that are consequences of the functional programmimg model of lambdas.
1. Lambdas involved in earlier Java versions
As we said in the first chapter, a functional interface is an interface with ONLY one method.
This means that we may always use the earlier Java interfaces that need to implement ONLY one method.
For example, let's consider java.lang.Thread. We know that when we define a thread, we have to implement its Runnable interface.
The Runnable interface has ONLY one abstract method, namely "run()".
The next code will show how to use lambdas with threads.
Consider the classic way:
Thread thread = new Thread(new Runnable() {
public void run() {
System.out.println("Hello");
}
});
thread.start();
This code may be transformed by using lambdas in:
Thread thread = new Thread(() -> System.out.println("Hello"))
thread.start();
Let's also consider the sorting of a java.util.List. In order to sort the list, we need the Comparator interface. We know that Comparator should implement the "compare()" method.
The next code will show how to sor list with lambdas.
Consider the classic way:
List<String> list = Arrays.asList("str3", "str1", "str4", "str2");
Collections.sort(list, new Comparator<String>() {
@Override
public int compare(String s1, String s2) {
return s1.compareTo(s2);
}
});
This code may be transformed by using lambdas in:
List<String> list = Arrays.asList("str3", "str1", "str4", "str2");
Collections.sort(list, (s1, s2) -> s1.compareTo(s2));
There exists also other interfaces, for example java.awt.event.ActionListener. This interface must implement the "actionPerformed()" method.
2. Java 8 streams support
In order to better support lambdas and as a consequence of how functional programming was implemented, java 8 added several features over collections of elements.
Java 8 Streams should not be confused with Java I/O streams. Streams should be considered as wrappers around a set of elements (collections, arrays, aso), allowing us to operate with that data source and making bulk processing convenient and fast.
A stream does not store data and, in that sense, is not a data structure. It also never modifies the underlying data source.
Streams supports functional-style operations (this they support lambdas), on streams of elements, such as map-reduce transformations on collections.
2.1. Define a stream
A Stream may be applied in these forms:
"Stream.of(T t)"
, where T is the type of stream elements, or:
"Stream.of (T ... values)"
, where you may stream more than 1 type of stream elements,
"Stream.builder().accept(T t).accept(U u)...",
where we use Builder to stream more than 1 type of elements (note here the Consumer "accept" method), or:
"listInstance.stream()",
where we stream a list.
2.2. Stream Pipelines
In order to use streaming over a collection of elements, we deal with stream operations which are methods of intermediate type, in the sense they return a result, or of terminal type, in the sense they return void.
A stream pipeline consists of a stream source, followed by zero or more intermediate operations and a terminal operation.
Integer count = list.stream().filter(s -> "str5".equals(s)).count();
Here we have a stream pipeline consisting of the "filter()" intermediate operation and followed by "count()" terminal operation.
Any of the stream pipeline operations waits for the preceeding operation to finish, except for the short-circuit operations that we'll discuss later.
2.3. Stream Operations
forEach()
This is a terminal operation loops over the elements of a stream.
Let's consider the following classic code:
List<String> list = Arrays.asList(new String[] { "str5", "other5", "str1", null, "str5", "other2", "str3", null, "str5", "str6", "other4" });
// classic prints the elements of list
System.out.println("classic initial list");
for (String s : list) {
System.out.println(s);
}
This may be transformed in:
list = Arrays.asList(new String[] { "str5", "other5", "str1", null, "str5", "other2", "str3", null, "str5", "str6", "other4" });
//print all with forEach Consumer
System.out.println("lambda sorted list with forEach Consumer");
list.forEach(s -> System.out.println(s));
, and further more, by using method reference in:
//print all with forEach Consumer and method reference
System.out.println("lambda sorted list with forEach Consumer and reference");
list.forEach(System.out::println);
Why this is a terminal operation ? We see that the forEach() argument is a Consumer and we know that Consumer is a functional interface that accepts 1 argument and returns void.
sorted()
This sorts the stream elements based on the passed Comparator. Notice that Comparator may be considered a functional interface, as we have to implement ONLY one method, the "compare()" method.
Let's consider the following classic code:
Collections.sort(list, Comparator.nullsFirst(new Comparator<String>() {
@Override
public int compare(String arg0, String arg1) {
return arg0.compareTo(arg1);
}
}));
for (String s : list) {
System.out.println(s);
}
This may be transformed in:
// first sort the list
list = list.stream().sorted(Comparator.nullsFirst((arg0, arg1) -> arg0.compareTo(arg1))).collect(Collectors.toList());
// second print the list with forEach Consumer and method reference
list.forEach(System.out::println);
When defining the comparator, we apply the "Comparator.nullsFirst()" in order to avoid the nulls.
We see here that we used the intermediate operation "sorted()" that also returns a stream of elements, followed by "collect()" operation which will discuss later.
Other stream operations that use the Comparator interface are min() and max().
collect()
It is used to assign the result of a stream() operation to an object defined by Collector interface. "collect()" is an intermediate operation, in the sense we may continue the pipeline with other operations.
Consider the example given at "sorted()" operation. in this case, the "collect()" operation simply collects a stream of elements and converts it into a "java.util.List" by specifying "Collectors.toList()".
There are also "Collectors.toMap()", "Collectors.toSet()" and a lot of methods of this final class.
However, when we want to collect to an array, we should use the "toArray()" operation which may be considered as a specific form of "collect()", returning the results to an array:
// using streams over an array of strings
String[] strs = list.stream().toArray(String[]::new);
Stream.of(strs).forEach(System.out::println);
filter()
It uses the Predicate functional interface to filter the elements of a stream.
You may apply more than a filter operation, as a filter() operation will result also in a stream.
Consider the following code:
// re-init the list
list = Arrays.asList(new String[] { "str5", "other5", "str1", null, "str5", "other2", "str3", null, "str5", "str6", "other4" });
// sort the list
list.stream().sorted(Comparator.nullsFirst((arg0, arg1) -> arg0.compareTo(arg1))).filter(Objects::nonNull).forEach(System.out::println);
This will out:
> lambda list not nulls, with stream()
> other2
> other4
> other5
> str1
> str3
> str5
> str5
> str5
> str6
We see here that null object values were omitted from the list by filtering the initial list.
Another sample:
// prints only elements with str5 and Function
// lambda type is java.util.function.Function
System.out.println("lambda list startsWith str");
list.stream().filter(s -> s != null && s.startsWith("str")).forEach(System.out::println);;
This will out:
> lambda list startsWith str
> str1
> str3
> str5
> str5
> str5
> str6
Here, from the initial list, there were filtered only the strings starting with "str".
anyMatch()
This is a boolean operation terminal operation that checks any occurence in elements of a collection
Consider the code:
//print if list contains str5 with stream()
String str = "str5";
System.out.println("check lambda list contains str5, with stream()");
System.out.println(list.stream().anyMatch(str::equals));
This will out:
> lambda list contains only str5, with stream()
> true
There is also "allMatch()" boolean terminal operation that checks all elements for an occurence.
map()
It accepts a Function fnctional interface as parameter and converts a stream of a type T elements to a stream of type U elements.
Consider the code:
// re-init the list
list = Arrays.asList(new String[] { "str5", "other5", "str1", null, "str5", "other2", "str3", null, "str5", "str6", "other4" });
// prints elements of list toUpperCase, with map
System.out.println("lambda otherList, elements toUpperCase, with map");
List<String> otherList = list.stream().sorted(Comparator.nullsFirst((arg0, arg1) -> arg0.compareTo(arg1))).filter(Objects::nonNull).map(String::toUpperCase).collect(Collectors.toList());
otherList.forEach(System.out::println);
Here we transformed the initial list of strings in a new list of strings toUpperCase.
Let's suppose the code:
// prints the length of elements of otherList, with map
System.out.println("lambda anotherList, elements length, with map");
List<Integer> anotherList = otherList.stream().map(String::length).collect(Collectors.toList());
anotherList.forEach(System.out::println);
Here, we used map to transform from a list of strings into a list of integers.
It may also be used as a kind of "filter()":
// prints a new list containing str5, with map
System.out.println("lambda str5List, new list containing str5, with map");
List<String> str5List = list.stream().map(s -> "str5".equals(s) ? s : null).filter(Objects::nonNull).collect(Collectors.toList());
str5List.forEach(System.out::println);
Here, we returned a new list consisting of elements of the list equal with "str5".
distinct()
It eliminates duplicates in a stream:
// distinct
System.out.println("lambda otherList, distinct elements");
otherList = list.stream().distinct().collect(Collectors.toList());
otherList.forEach(System.out::println);
findFirst()
Returns the first element in a stream as an Optional object:
// find first
System.out.println("lambda list, findFirst occurence of str5");
String strFindFirst = list.stream().filter(s -> "str5".equals(s)).findFirst().orElse(null);
System.out.println(strFindFirst);
Here, we return the first occurence string if it is found or null if it is not found at all.
skip() and limit()
These is a short-circuiting operation, allowing to skip over some elements of a stream and limit the results.
Consider the following code:
// skip, limit
System.out.println("lambda initial list, used for skip and limit");
list.forEach(System.out::println);
System.out.println("lambda list, skip 4, limit 2");
List<String> skippedList = list.stream().skip(4).limit(2).collect(Collectors.toList());
skippedList.forEach(System.out::println);
The output will be:
> lambda initial list, used for skip and limit
> str5
> other5
> str1
> null
> str5
> other2
> str3
> null
> str5
> str6
> other4
> lambda list, skip 4, limit 2
> str5
> other2
So, we skipped 4 elements from list and returned the next 2 elements.
The short-circuiting operations are often used to complete infinite streams.
The short-circuiting will not be applied for "sorted()", so you'll have to "collect()" first and then reuse.
As these short-circuit operations applies on number of stream elements, they should count those elements, so they must interfer with the previous operations from the stream pipeline. For example, "limit()" will not wait for a "filter()" to finish, because it should know at any time the number of elements "filter()" returned.
There are many other stream operations, but we'll not focus on all of them.
2.4 Optional object
Optional container was added to better support the functional programming in the sense that it may be considered as a wrapper on a java object with some methods that test that wrapped object for nulls.
We consider an example in which the lambda expression was set as a parameter for a method:
public static void main(String[] args) {
...
// prints only not null elements of the list with Function
// lambda type matched is java.util.function.Function
System.out.println("lambda list not nulls, with Function");
printList(list, s -> s != null ? s : null);
...
}
/**
* prints the list with forEach, Optional object, with Consumer and Function functional interfaces
* @param list
* @param p
*/
static void printList(List<String> list, Function<String, String> f) {
// we use Optional to test if s is null
// if s is not null then we use the Optional.ifPresent Consumer
list.forEach(s -> Optional.ofNullable(f.apply(s)).ifPresent(System.out::println));
}
The code above uses the functional interface Function to test the elements of a list for null values.
The output will be:
> lambda list not nulls, with Function
> str5
> other5
> str1
> str5
> other2
> str3
> str5
> str6
> other4
2.5. Stream Specializations
Stream is a stream of object references. However, there are also the IntStream, LongStream, and DoubleStream – which are primitive specializations for int, long and double respectively.
These specialized streams do not extend Stream but extend BaseStream on top of which Stream is also built.
We will not focus on these particular cases of streams.
2.6. Parallel Streams
When we deal with multi-core processors, we should consider parallel streams. They look like a sequential stream, but some aspects should be considered.
The parallel streams should be considered only in cases when we deal with multicore processors. Only in these cases we may expect an increase of speed of execution when using parallel streams.
As parallel streams implies execution on many threads, the parallel stream code should be thread-safe, meaning the pipeline should not allow intermediary data modification or intermediary different data results, as the intermediary data may be used as input by the following execution thread.
For example:
//print count of filtered list values that are equal to str5 with stream() and forEach Consumer
System.out.println("lambda count when list contains only str5, with parallelStream()");
System.out.println(list.parallelStream().filter(s -> "str5".equals(s)).count());
3. Treating exceptions in lambdas
Taken into consideration the example of Comparator above, we have the following code:
// print all with stream(), Collectors and forEach Consumer, with Exception handling
System.out.println("lambda sorted list, with stream, catch exception");
list.stream().sorted((arg0, arg1) -> {
try {
return arg0.compareTo(arg1);
} catch (Exception e) {
return -1;
}
}).collect(Collectors.toList()).forEach(System.out::println);
So, treating exception may be done in the body of the lambda expression.
5. Final words
There are many other things to say about streams, the rest should be discoverable by each of us. Why so? Because lambdas were introduced to ease the way of writing code.
For the overall code of the above, please write me at "vs_octavian[around]yahoo[dot]com".
Previous chapter -> I. Introduction to Lambdas
Wednesday, December 11, 2019
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
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
Subscribe to:
Posts (Atom)