Lambda expression
Lambda expressions are introduced in Java 8 and are touted to be the biggest feature of Java 8 - a compact way of passing around behavior. Lambda expression facilitates functional programming, and simplifies the development a lot.
Lambdas allows to specify a block of code which should be executed later. If a method expects a functional interface as parameter it is possible to pass in the lambda expression instead.
A lambda expression is characterized by the following syntax
parameter -> expression body (int a, int b) -> {return a + b}
Following are the important characteristics of a lambda expression
An interface with exactly one abstract method is called Functional Interface. @FunctionalInterface annotation is added so that we can mark an interface as functional interface.
The Java compiler automatically identifies functional interfaces. The only requirement is that they have only one abstract method. However, is possible to capture the design intent with a @FunctionalInterface
annotation.
Several default Java interfaces are functional interfaces:
public class Example { interface MathOperation { int operation(int a, int b); } public static void main(String args[]){ // different forms of notation MathOperation addition = (a, b) -> a + b; MathOperation multiplication = (int a, int b) -> { return a * b; }; System.out.println("5 + 5 = " + addition.operation(5, 5)); System.out.println("5 * 5 = " + multiplication.operation(5, 5)); } }
Following are the important points to be considered in the above example.
We can use new feature of Java 8 method reference for referencing to static methods, instance methods, constructors using new operator. Method references help to point to methods by their names. A method reference is described using ::
(double colon) symbol.
Let's look into an example of method referencing to get a more clear picture.
import java.util.List; import java.util.ArrayList; public class Example { public static void main(String args[]){ List<String> planets = new ArrayList<String>>(); planets.add("Mercury"); planets.add("Venus"); planets.add("Earth"); planets.add("Mars"); planets.forEach(System.out::println); } }
Difference between a lambda expression and a closure
The Java programming language supports lambdas but not closures. A lambda is an anonymous function, e.g., it can be defined as parameter. Closures are code fragments or code blocks which can be used without being a method or a class. This means that a closure can access variables not defined in its parameter list and that it can also be assigned to a variable.
Let’s look at some use case examples of java lambda expression.
(List list) -> list.isEmpty()
() -> new Movie()
(Movie m) -> { System.out.println(m.getYear()); }
(String s) -> s.length()
(int a, int b) -> a * b
(Movie m1, Movie m2) -> m1.getYear().compareTo(m2.getMovie())
Stream API
Stream API is a new abstract layer introduced in Java 8. Using stream, you can process data in a declarative way similar to SQL statements. Stream API represents a sequence of elements from a source, which supports different kind of operations to perform computations upon those elements. Following are the characteristics of a Stream.
Collections
, Arrays
, or I/O
resources as input source.filter
, map
, limit
, reduce
, find
, match
, and so on.Collections
where explicit iteration is required.Stream<Integer> stream = Stream.of(1,2,3,4,5,6,7,8,9); stream.forEach(p -> System.out::println); IntStream stream = "abcde".chars(); stream.forEach(p -> System.out::println);
Collections vs Streams. Collections are in-memory data structures which hold elements within it. Each element in the collection is computed before it actually becomes a part of that collection. On the other hand Streams are fixed data structures which computes the elements on-demand basis. The Java 8 Streams can be seen as lazily constructed Collections, where the values are computed when user demands for it.
Stream operations are either intermediate or terminal. Intermediate operations return a stream so we can chain multiple intermediate operations without using semicolons. Terminal operations are either void or return a non-stream result. Methods filter
, map
and sorted
are intermediate operations whereas forEach
is a terminal operation.
Most stream operations accept some kind of lambda expression parameter, a functional interface specifying the exact behavior of the operation.
With Java 8, Collection
interface has two methods to generate a Stream.
stream()
returns a sequential stream considering collection as its source.parallelStream()
returns a parallel Stream considering collection as its source.// example 1 List<String> strings = Arrays.asList("a", "", "b", "c", "f","", "g"); List<String> filtered = strings.stream().filter(string -> !string.isEmpty()).collect(Collectors.toList()); // example 2 String[] in = {"aa", "", "bbb", "ccc", "f","", "gg"}; String[] out = Arrays.stream(in).filter(s -> s.length() > 2).toArray(String[]::new); System.out.println(out[0]);
The limit()
method is used to reduce the size of the stream. The following code segment shows how to print 10 random numbers using limit()
.
Random random = new Random(); random.ints().limit(10).forEach(System.out::println);
The map()
method is used to map each element to its corresponding result. In other words, for each item in the collection you create a new object based on that item. The following code segment prints unique squares of numbers using map()
.
List<Integer> numbers = Arrays.asList(7, 6, 2, 1, 4, 9, 8); List<Integer> squares = numbers.stream().map(i -> i * i).distinct().collect(Collectors.toList());
The filter()
method is used to eliminate elements based on a criteria. The following code segment prints a count of empty strings using filter()
.
List<String> strings = Arrays.asList("a", "", "b", "c", "f","", "g"); int count = strings.stream().filter(string -> string.isEmpty()).count(); strings.stream().filter(string -> string.startsWith("a"));
The method forEach()
is used to iterate each element of the stream. The following code segment shows how to print 10 random numbers using forEach()
.
Random random = new Random(); random.ints().limit(10).forEach(System.out::println);
The sorted()
method is used to sort the stream. The following code segment shows how to print 10 random numbers in a sorted order.
Random random = new Random(); random.ints().limit(10).sorted().forEach(System.out::println);
Collectors are used to combine the result of processing on the elements of a stream. Collectors can be used to return a list or a string.
List<String> strings = Arrays.asList("a", "", "b", "c", "f","", "g"); List<String> filtered = strings.stream().filter(string -> !string.isEmpty()).collect(Collectors.toList()); System.out.println("Filtered list: " + filtered); String merged = strings.stream().filter(string -> !string.isEmpty()).collect(Collectors.joining(", ")); System.out.println("Merged string: " + merged);
Statistics collectors are introduced to calculate all statistics when stream processing is being done.
List<Integer> numbers = Arrays.asList(7, 6, 2, 1, 4, 9, 8); IntSummaryStatistics stats = numbers.stream().mapToInt((x) -> x).summaryStatistics(); System.out.println("Highest number in List : " + stats.getMax()); System.out.println("Lowest number in List : " + stats.getMin()); System.out.println("Sum of all numbers : " + stats.getSum()); System.out.println("Average of all numbers : " + stats.getAverage());
Various matching operations (allMatch
, anyMatch
, noneMatch
) can be used to check whether a certain predicate matches the stream. All of those operations are terminal and return a boolean result.
List<String> strings = Arrays.asList("a", "", "b", "c", "f","", "g"); boolean matched = strings.stream().anyMatch((s) -> s.startsWith("c"));
The reduce()
method performs a reduction on the elements of the stream with the given function. The result is an Optional
holding the reduced value.
The Optional class acts as a wrapper around a value that may or may not be null, and is used to reduce the frequency of NullPointerException in applications that take advantage of it.
List<String> strings = Arrays.asList("a", "", "b", "c", "f","", "g"); Optional<String> reduced = strings.stream().reduce((s1, s2) -> s1 + "-" + s2); reduced.ifPresent(System.out::println);
The findFirst()
method will return first element from stream and then will not process any more element.
List<String> strings = Arrays.asList("a", "", "b", "c", "f","", "g"); String first = strings.stream().filter((s) -> s.startsWith("b")).findFirst().get(); System.out.println(first);
Besides regular object streams Java 8 ships with special kinds of streams for working with the primitive data types int
, long
and double
. As you might have guessed it's IntStream
, LongStream
and DoubleStream
.
IntStreams
can replace the regular for-loop utilizing IntStream.range()
IntStream.range(1, 7).forEach(System.out::println); LongStream longStream = LongStream.rangeClosed(1, 3);
The range(int startInclusive, int endExclusive)
method creates an ordered stream from first parameter to the second parameter. It increments the value of subsequent elements with the step equal to 1. The result doesn’t include the last parameter, it is just an upper bound of the sequence.
The rangeClosed(int startInclusive, int endInclusive)
method does the same with only one difference – the second element is included. These two methods can be used to generate any of the three types of streams of primitives.
Useful links