Java 8 Lambda Expressions
Java 8 Lambda Expressions
This is the most asked interview question on Java 8. Many of the java developers do not work on java 8.
Below are the java 8 features:
1. Functional Interface: Each functional interface has a single abstract method, called the functional
method, implementation can be provided using the lambda expressions.
2. Lambda Expressions: It is a feature derived from the functional programming. It is a function that does
not belong to any class.
3. Optional: Instead of using null values Optional class is used for representing Optional values.
4. Stream api
5. Spliterator
6. Method References
7. New Date and Time API.
2 Important Topics
Java 8 introduced several key features that support functional programming paradigms. Here are the main
topics related to functional programming in Java 8:
1. Lambda Expressions:
o Lambda expressions provide a clear and concise way to represent instances of functional
interfaces (interfaces with a single abstract method).
o Syntax: (parameters) -> expression or (parameters) -> { statements; }.
2. Functional Interfaces:
2
o A functional interface is an interface with exactly one abstract method.
o Java 8 introduced the @FunctionalInterface annotation to ensure that an interface meets the
criteria of a functional interface.
o Examples: Runnable, Comparator, and custom interfaces like MyFunctionalInterface.
3. Method References:
o Method references provide a way to refer to methods without invoking them.
o Types:
o Example:
5. Default Methods:
o Default methods allow interfaces to have methods with implementation without affecting the
classes that implement the interface.
o This feature was introduced to support backward compatibility while evolving the API.
o Example:
interface MyInterface {
default void myDefaultMethod() {
System.out.println("Default method implementation");
}
}
6. Optional Class:
3
o The Optional class is a container object which may or may not contain a non-null value.
o Example:
8. Parallel Streams:
o Parallel streams enable parallel processing of data, which can lead to performance
improvements on multi-core processors.
o Example:
9. CompletableFuture:
o CompletableFuture is used for asynchronous programming and provides a way to handle
future results in a non-blocking manner.
o It supports chaining and combining multiple asynchronous operations.
o Example:
These features collectively enhance Java's ability to support functional programming styles, making
it easier to write more concise, readable, and maintainable code.
4
3 What is and why Lambda?
Lambda expressions are the method without name i.e Anonymous method. In other words, Lambda
expression is a function that can be passed around and referenced as an object.
It is replacement of anonymous classes. In case of anonymous classes, a new .class file gets compiled /
created, while in case with using Lambda expressions no extra file gets created.
A.
1. Enables functional programming.
2. Readable and con size code.
3. Easier-to-use APIs and library.
4. Enables support for parallel processing.
4 Functional interfaces?
A Functional Interface is an Interface which allows only one Abstract method within the Interface scope.
There are some predefined functional interfaces in Java like Predicate, consumer, supplier etc. The return
type of a Lambda function (introduced in JDK 1.8) is an also functional interface.
Java 8 introduced several built-in functional interfaces in the java.util.function package to support functional
programming. These interfaces represent common function types and can be used with lambda
expressions, method references, and streams. Below are the main functional interfaces categorized by their
purpose:
In Java, the java.util.function package contains several core functional interfaces that represent
common function types. These interfaces are used extensively with lambda expressions and method
references. Here are the most important ones:
4.1.1 1. Consumer<T>
5
o BiConsumer<T, U> (2 inputs)
o DoubleConsumer, IntConsumer, LongConsumer (primitive versions)
4.1.2 2. Supplier<T>
Primitive Specializations
6
IntSupplier, LongSupplier, DoubleSupplier
IntFunction, LongFunction, DoubleFunction
ToIntFunction, ToLongFunction, ToDoubleFunction
IntPredicate, LongPredicate, DoublePredicate
4.1.7 Examples
// Consumer
Consumer<String> print = s -> System.out.println(s);
// Supplier
Supplier<Double> random = Math::random;
// Function
Function<String, Integer> length = String::length;
// Predicate
Predicate<Integer> isEven = n -> n % 2 == 0;
// Runnable
Runnable task = () -> System.out.println("Running!");
These interfaces are foundational for Java's functional programming features (lambdas, streams, etc.).
_____________________________
4.2 What is Consumer Interface?
The Consumer Interface is a part of the java.util.function package which has been introduced since Java
8, to implement functional programming in Java. It represents a function which takes in one argument and
produces a result. However these kind of functions don’t return any value.
Hence this functional interface which takes in one generic namely:-
T: denotes the type of the input argument to the operation
The lambda expression assigned to an object of Consumer type is used to define its accept() which
eventually applies the given operation on its argument. Consumers are useful when it not needed to return
any value as they are expected to operate via side-effects.
4.2.1 Functions in Consumer Interface
This method accepts one value and performs the operation on the given argument
7
// Java Program to demonstrate
// Consumer's accept() method
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.function.Consumer;
dispList.accept(list);
}
}
Output:
10
hi_suffix
ki_suffix
4.2.3 andThen()
It returns a composed Comsumer wherein the parameterized Consumer will be executed after the first one.
If evaluation of either function throws an error, it is relayed to the caller of the composed operation.
Note: The function being passed as the argument should be of type Consumer.
Syntax:
Parameters: This method accepts a parameter after which is the Consumer to be applied after the current
one.
Return Value: This method returns a composed Consumer that first applies the current Consumer first and
then the after operation.
8
Exception: This method throws NullPointerException if the after operation is null.
Below is the code to illustrate andThen() method:
Program 1:
public static void main(String[] args) {
consumer1.andThen(consumer2).andThen(consumer3).andThen(consumer4).andThen(consumer5).andThen(printCon
sumer).accept(sb);
Program 2:
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.function.Consumer;
9
// using addThen()
modify.andThen(dispList).accept(list);
;
}
}
Output:
426
______________________________
Example 3:
import java.util.function.Consumer;
orderProcessingPipeline.accept(order);
}
}
// Supporting classes
class Order {
private String id;
private double amount;
10
private Customer customer;
class Customer {
private String email;
private boolean isPremium;
// Getters
public String getEmail() { return email; }
public boolean isPremium() { return isPremium; }
}
Output:
11
4.3 What is predicate Interface?
Predicate in general meaning is a statement about something that is either true or false. In programming,
predicates represent single argument functions that return a boolean value.
Predicates in Java are implemented with interfaces. Predicate<T> is a generic functional interface
representing a single argument function that returns a boolean value. It is located in
the java.util.function package. It contains a test(T t) method that evaluates the predicate on the given
argument.
In Java we do not have standalone functions. Furthermore, methods are not first-class citizens. (They
cannot be added to collections or passed to methods as parameters.) Therefore, we define interfaces and
create objects from these interfaces. Such objects can be then passed to methods such as Iterables.filter().
With Java lambdas it is much easier to work with predicates.
import java.util.List;
import java.util.function.Predicate;
@Override
public boolean test(Integer v) {
Integer five = 5;
@Override
public boolean test(Integer v) {
12
Integer five = 5;
com/zetcode/JavaPredicateEx2.java
package com.zetcode;
import java.util.List;
import java.util.function.Predicate;
nums.stream().filter(btf).forEach(System.out::println);
}
}
The example filters integer values; this time we use Java lambda expression, which makes the code much
shorter.
Predicate<Integer> btf = n -> n > 5;
This is a one-liner that creates the predicate.
13
4.3.3 Java Predicate example II
@Override
public String toString() {
return "Country{" + "name=" + name +
", population=" + population + '}';
}
}
import java.util.ArrayList;
import java.util.List;
import java.util.function.Predicate;
14
countries.add(new Country("India", 1342512000));
countries.add(new Country("Latvia", 1978000));
countries.add(new Country("Vietnam", 95261000));
countries.add(new Country("Sweden", 9967000));
countries.add(new Country("Iceland", 337600));
countries.add(new Country("Israel", 8622000));
countries.stream().filter(p1).forEach(System.out::println);
}
}
In the example, we create a list of countries. We filter the list by the country name and population.
The predicate returns true for countries that start with 'I' and their population is over ten million.
Country{name=Iran, population=80840713}
Country{name=India, population=1342512000}
Two countries from the list fulfill the conditions: Iran and India.
4.3.4 Java IntPredicate
IntPredicate represents a predicate of one int-valued argument. This is the int-consuming primitive type
specialization of Predicate<E>.
com/zetcode/IntPredicateEx.java
package com.zetcode;
import java.util.Arrays;
import java.util.function.IntPredicate;
int[] nums = { 2, 3, 1, 5, 6, 7, 8, 9, 12 };
Arrays.stream(nums).filter(p).forEach(System.out::println);
}
}
The example filters an array of int values with filter() and IntPredicate.
int nums[] = { 2, 3, 1, 5, 6, 7, 8, 9, 12 };
An IntPredicate is created; it returns true for int values bigger than five.
15
Arrays.stream(nums).filter(p).forEach(System.out::println);
We create a stream from the array and filter the elemetnts. The filter() method receives the predicate as a
parameter.
4.3.5 Composing predicates
com/zetcode/JavaPredicateCompose.java
package com.zetcode;
import java.util.Arrays;
import java.util.function.IntPredicate;
Arrays.stream(nums).filter(p1.and(p2)).forEach(System.out::println);
System.out.println("**********");
IntPredicate p3 = n -> n == 6;
IntPredicate p4 = n -> n == 9;
Arrays.stream(nums).filter(p3.or(p4)).forEach(System.out::println);
}
}
Arrays.stream(nums).filter(p1.and(p2)).forEach(System.out::println);
We combine two predicates with the and() method; we get integers that are bigger than three and smaller
than nine.
IntPredicate p3 = n -> n == 6;
IntPredicate p4 = n -> n == 9;
Arrays.stream(nums).filter(p3.or(p4)).forEach(System.out::println);
With the or() method, we get values that are equal either to six or nine.
5
6
7
8
16
**********
6
9
This is the output.
4.3.6 Negating predicates
The negate() method returns a predicate that represents the logical negation of the given predicate.
com/zetcode/JavaPredicateNegate.java
package com.zetcode;
import java.util.Arrays;
import java.util.function.IntPredicate;
Arrays.stream(nums).filter(p).forEach(System.out::println);
System.out.println("**********");
Arrays.stream(nums).filter(p.negate()).forEach(System.out::println);
}
}
We have a predicate that returns true for values bigger than five.
Arrays.stream(nums).filter(p).forEach(System.out::println);
Arrays.stream(nums).filter(p.negate()).forEach(System.out::println);
With the negate() method, we get the opposite: values lower or equal to four.
6
7
8
9
12
**********
2
3
1
5
17
This is the output of the example.
4.3.7 Java predicate as method parameter
com/zetcode/PredicateMethodParam.java
package com.zetcode;
import java.util.List;
import java.util.function.Predicate;
import java.util.stream.Collectors;
_______________________________
_______________
package com.anurag.example.lambda_exp;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.function.Predicate;
interface Condition {
boolean test(Person p);
}
18
class Person {
String name;
String lname;
int age;
//Step 1: sort
//Collections.sort(persons, (Person o1, Person o2) ->
o1.getName().compareTo(o2.getName()));
//or
Collections.sort(persons, (o1, o2) -> o1.getName().compareTo(o2.getName()));
//Step 2: Create a method which prints all the elements in the list.
//printAll(persons);
//or
printConditionally(persons, p -> true);
19
private static void printAll(List<Person> persons) {
for (Person person : persons) {
System.out.println(person);
}
}
}
then
# You can use any interface from the pkg java.util.function, like Consumer interface
_________________________________________
4.5 Handling exception in lambda expression
20
- using java.util.function BiConsumer interface
try {
consumer.accept(t, u);
} catch (ArithmeticException e) {
System.out.println("Exception caught.");
}
};
}
21
5 Method references?
5.1 Introduction
If we only use a method of an object in another method, we still have to pass the full object as an
argument. Wouldn't it be more practical to just pass the method as an argument? For example:
isFull(list.size);
In Java 8, thanks to lambda expressions, we can do something like this. We can use methods as if
they were objects, or primitive values.
A method reference is the shorthand syntax for a lambda expression that executes just ONE method.
Here's the general syntax of a method reference:
Object :: methodName
We know that we can use lambda expressions instead of using an anonymous class. But sometimes,
the lambda expression is really just a call to some method, for example:
22
To make the code clearer, you can turn that lambda expression into a method reference:
Consumer<String> c = System.out::println;
In a method reference, you place the object (or class) that contains the method before the :: operator
and the name of the method after it without arguments.
So to use a method reference, you first need a lambda expression with one method. And to use a
lambda expression, you first need a functional interface, an interface with just one abstract method.
In other words:
Instead of using
AN ANONYMOUS CLASS
you can use
A LAMBDA EXPRESSION
And if this just calls one method, you can use
A METHOD REFERENCE
23
Let's begin by explaining the most natural case, a static method.
Class::staticMethod
Notice that between a static method and a static method reference, instead of the . operator, we use
the :: operator, and that we don't pass arguments to the method reference.
In general, we don't have to pass arguments to method references. However, arguments are treated
depending on the type of method reference.
In this case, any arguments (if any) taken by the method are passed automatically behind the curtains.
Where ever we can pass a lambda expression that just calls a static method, we can use a method
reference. For example, assuming this class:
class Numbers {
public static boolean isMoreThanFifty(int n1, int n2) {
return (n1 + n2) > 50;
}
public static List<Integer> findNumbers(
List<Integer> l, BiPredicate<Integer, Integer> p) {
List<Integer> newList = new ArrayList<>();
for(Integer i : l) {
if(p.test(i, i + 10)) {
newList.add(i);
}
}
return newList;
}
}
24
return Numbers.isMoreThanFifty(i1, i2);
}
});
Where an instance of an object is passed, and one of its methods is executed with some optional(s)
parameter(s).
ObjectType::instanceMethod
This time, the conversion is not that straightforward. First, in the method reference, we don't use the
instance itself—we use its type.
Second, the other argument of the lambda expression, if any, is not used in the method reference, but
it's passed behind the curtains like in the static method case.
class Shipment {
public double calculateWeight() {
double weight = 0;
// Calculate weight
return weight;
}
}
25
}
In this example, we don't pass any arguments to the method. The key point here is that an instance of
the object is the parameter of the lambda expression, and we form the reference to the instance
method with the type of the instance.
Here's another example where we pass two arguments to the method reference.
Java has a Function interface that takes one parameter, a BiFunction that takes two parameters, but
there's no TriFunction that takes three parameters, so let's make one:
Now assume a class with a method that takes two parameters a return a result, like this:
class Sum {
Integer doSum(String s1, String s2) {
return Integer.parseInt(s1) + Integer.parseInt(s1);
}
}
We can wrap the doSum() method within a TriFunction implementation by using an anonymous class:
26
//Or by using a lambda expression:
TriFunction<Sum, String, String, Integer> lambda =
(Sum s, String arg1, String arg2) -> s.doSum(arg1, arg1);
System.out.println(lambda.apply(new Sum(), "1", "4"));
Here:
The first type parameter of TriFunction is the object type that contains the method to
execute.
The second type parameter of TriFunction is the type of the first parameter.
The third type parameter of TriFunction is the type of the second parameter.
The last type parameter of TriFunction is the return type of the method to execute. Notice
how this is omitted (inferred) in the lambda expression and the method reference.
It may seem odd to just see the interface, the class, and how they are used with a method
reference; but this becomes more evident when you see the anonymous class or even the
lambda version.
From:
To
Sum::doSum
obj::instanceMethod
This time, an instance defined somewhere else is used, and the arguments (if any) are passed behind
the curtains like in the static method case.
27
For example, assuming these classes:
class Car {
private int id;
private String color;
// More properties
// And getter and setters
}
class Mechanic {
public void fix(Car c) {
System.out.println("Fixing car " + c.getId());
}
}
The key, in this case, is to use any object visible by an anonymous class/lambda expression and pass
some arguments to an instance method of that object.
Consumer<String> c = System.out::println;
c.accept("Hello");
28
(args) -> new ClassName(args)
ClassName::new
The only thing this lambda expression does is to create a new object and we just reference a
constructor of the class with the keyword new. Like in the other cases, arguments (if any) are not
passed in the method reference.
Most of the time, we can use this syntax with two (or three) interfaces of the java.util.function package.
If the constructor takes an argument, we can use the Function interface. For example:
29
return new Locale(lang, country);
}
};
Locale loc = f.apply("en","UK");
If you have a constructor with three or more arguments, you would have to create your own functional
interface.
You can see that referencing a constructor is very similar to referencing a static method. The
difference is that the constructor "method name" is new.
5.7 Conclusion
Many of the examples presented here are very simple and they probably don't justify the use of
lambda expressions or method references.
As mentioned at the beginning, use method references if they make your code CLEARER.
For example, you can avoid the one method restriction by grouping all your code in a static method, and
create a reference to that method instead of using a class or a lambda expression with many lines.
But the real power of lambda expressions and method references comes when they are combined with
another new feature of Java 8; streams.
6 Spliterator?
Java Spliterator interface is an internal iterator that breaks the stream into the smaller parts. These smaller
parts can be processed in parallel.
In real life programming, we may never need to use Spliterator directly. Under normal operations, it will
behave exactly same as Java Iterator.
+ Spliterator Syntax
- Spliterator<T> spliterator = list.spliterator();
30
The Java collection classes provide default stream() and parallelStream() methods which internally use
the Spliterator through the call to the spliterator(). It helps in processing the collection data in parallel.
Collection.java
1. int characteristics() : returns the list of characteristics of the spliterator. It can be any of ORDERED,
DISTINCT, SORTED, SIZED, NONNULL, IMMUTABLE, CONCURRENT, and SUBSIZED.
2. long estimateSize() : returns an estimate of the number of elements that would be encountered by a
forEachRemaining() traversal, or returns Long.MAX_VALUE if infinite, unknown, or too expensive to
compute.
3. default void forEachRemaining(Consumer action) : performs the given action for each remaining
element, sequentially in the current thread, until all elements have been processed or the action
throws an exception.
4. default Comparator getComparator() : if the spliterator’s source is SORTED by a Comparator,
returns that Comparator.
5. default long getExactSizeIfKnown() : returns estimateSize() if this Spliterator is SIZED, else -1.
6. default boolean hasCharacteristics(int characteristics) : returns true if the dpliterator’s
characteristics() contain all of the given characteristics.
7. boolean tryAdvance(Consumer action) : if a remaining element exists, performs the given action
on it, returning true; else returns false.
8. Spliterator trySplit() : if the spliterator can be partitioned, returns a Spliterator covering elements,
that will, upon return from this method, not be covered by this Spliterator.
31
6.3 Java Spliterator Example
Spliterator example
ArrayList<String> list = new ArrayList<>();
if (spliterator.hasCharacteristics(Spliterator.ORDERED)) {
System.out.println("ORDERED");
}
if (spliterator.hasCharacteristics(Spliterator.DISTINCT)) {
System.out.println("DISTINCT");
}
if (spliterator.hasCharacteristics(Spliterator.SORTED)) {
System.out.println("SORTED");
}
if (spliterator.hasCharacteristics(Spliterator.SIZED)) {
System.out.println("SIZED");
}
if (spliterator.hasCharacteristics(Spliterator.CONCURRENT)) {
System.out.println("CONCURRENT");
}
if (spliterator.hasCharacteristics(Spliterator.IMMUTABLE)) {
System.out.println("IMMUTABLE");
}
if (spliterator.hasCharacteristics(Spliterator.NONNULL)) {
System.out.println("NONNULL");
}
if (spliterator.hasCharacteristics(Spliterator.SUBSIZED)) {
System.out.println("SUBSIZED");
}
Program Output.
Console
32
true
ORDERED
SIZED
SUBSIZED
Java example to get the size of backing collection i.e. number of elements to iterate by spliterator.
Spliterator example
ArrayList<String> list = new ArrayList<>();
list.add("A");
list.add("B");
list.add("C");
list.add("D");
System.out.println(spliterator.estimateSize());
System.out.println(spliterator.getExactSizeIfKnown());
Program Output.
Console
4
4
Spliterator example
SortedSet<String> set = new TreeSet<>( Collections.reverseOrder() );
set.add("A");
set.add("D");
set.add("C");
set.add("B");
System.out.println(set);
System.out.println(set.spliterator().getComparator());
33
Program Output.
Console
[D, C, B, A]
java.util.Collections$ReverseComparator@7852e922
Java example to split the elements to two groups and iterate independently.
Spliterator example
ArrayList<String> list = new ArrayList<>();
list.add("A");
list.add("B");
list.add("C");
list.add("D");
list.add("E");
list.add("F");
spliterator1.forEachRemaining(System.out::println);
System.out.println("========");
spliterator2.forEachRemaining(System.out::println);
Program Output.
Console
D
E
F
========
A
B
C
34
6.3.5 Java example to perform hasNext() and next() operations in single statement using
forEachRemaining() method.
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
Key Points:
forEachRemaining() internally uses hasNext() and next() to process all remaining elements
It provides a more concise way to iterate when you want to perform the same operation on all
elements
The method takes a Consumer functional interface as parameter
After calling forEachRemaining(), the iterator will be exhausted (no more elements)
Output:
Traditional approach:
Apple
Banana
35
Cherry
Date
Using forEachRemaining():
Apple
Banana
Cherry
Date
This approach is particularly useful when you want to perform the same operation on all remaining elements
of the iterator.
1. Use in API: Iterator is used for Collection API while Spliterator is used for Stream API
2. Parallel programming: Iterator can be used for iterating the elements in Collection in sequential order
while Spliterator can be used for iterating the Stream elements in parallel or sequential order.
Till Java 7, the collections framework relied on the concept of external iteration, where a Collection
provides, by implementing Iterable, a means to enumerate its elements i.e. Iterator, and clients use this to
step sequentially through the elements of a collection. For example, if we wanted to get all strings in
uppercase, we would write:
36
OR we can write like this:
Above both code snippets are for external iteration. External iteration is straightforward enough, but it has
several problems:
1) Java’s for-each loop/iterator is inherently sequential, and must process the elements in the order
specified by the collection.
2) It limits the opportunity to manage the control flow, which might be able to provide better performance by
exploiting reordering of the data, parallelism, short-circuiting, or laziness.
Sometimes the strong guarantees of the for-each loop (sequential, in-order) are desirable, but often are just
an disadvantage to performance. The alternative to external iteration is internal iteration, where instead of
controlling the iteration, client let it handle by library and only provide the code which must be executed for
all/some of data elements.
37
8 Stream API
A Stream in Java can be defined as a sequence of elements from a source that supports aggregate
operations on them. The source here refers to a Collections or Arrays who provides data to a Stream.
Stream keeps the ordering of the data as it is in the source. The aggregate operations or bulk
operations are operations which allow us to express common manipulations on stream elements easily
and clearly.
Before going ahead, it is important to learn that Java 8 Streams are designed in such a way that most of
the stream operations returns streams only. This help us creating chain of the stream operations. This is
called as pipe-lining. I will use this term multiple times in this post, so keep it in mind.
All of us have watch online videos on youtube or some other such website. When you start watching video,
a small portion of file is first loaded into your computer and start playing. You don’t need to download
complete video before start playing it. This is called streaming. I will try to relate this concept with respect to
collections and differentiate with Streams.
At the basic level, the difference between Collections and Streams has to do with when things are
computed. A Collection is an in-memory data structure, which holds all the values that the data structure
currently has—every element in the Collection has to be computed before it can be added to the Collection.
A Stream is a conceptually fixed data structure, in which elements are computed on demand. This
gives rise to significant programming benefits. The idea is that a user will extract only the values they
require from a Stream, and these elements are only produced—invisibly to the user—as and when required.
This is a form of a producer-consumer relationship.
In java, java.util.Stream represents a stream on which one or more operations can be performed.
Stream operations are either intermediate or terminal. While terminal operations return a result of a
certain type, intermediate operations return the stream itself so you can chain multiple method calls in
a row. Streams are created on a source, e.g. a java.util.Collection like lists or sets (maps are not
supported). Stream operations can either be executed sequential or parallel.
Based on above points, if we list down the various characteristics of Stream, they will be as follows:
38
8.2 Different ways to create streams
Below is the most popular different ways to build streams from collections.
8.2.2 Stream.of(arrayOfElements)
8.2.3 List.stream()
// generate
Stream<Date> stream = Stream.generate(Date::new);
stream.forEach(System.out::println);
39
// iterate
List<Integer> evens = Stream.iterate(0, n -> n + 2)
.limit(10)
.collect(Collectors.toList());
System.out.println(evens); // [0, 2, 4, 6, 8, 10, 12, 14, 16, 18]
}
}
//OR
There are some more ways apart from above list such as using Stream.Buider or using intermediate
operations. We will learn about them in separate posts time to time.
Please note that it is not a true conversion. It’s just collecting the elements from the stream into a collection
or array.
8.3.1 Convert Stream to List – Stream.collect( Collectors.toList() )
40
List<Integer> list = new ArrayList<Integer>();
for(int i = 1; i< 10; i++){
list.add(i);
}
Stream<Integer> stream = list.stream();
Integer[] evenNumbersArr = stream.filter(i -> i%2 == 0).toArray(Integer[]::new);
System.out.print(evenNumbersArr);
}
}
There are plenty of other ways also to collect stream into set, map or into multiple ways. Just go
through Collectors class and try to keep them in mind.
Stream abstraction have a long list of useful functions for you. I am not going to cover them all, but I plan
here to list down all most important ones, which you must know first hand.
Before moving ahead, lets build a collection of String beforehand. We will build out example on this list, so
that it is easy to relate and understand.
Intermediate operations return the stream itself so you can chain multiple method calls in a row. Let’s learn
important ones.
8.5.1 Stream.filter()
Filter accepts a predicate to filter all elements of the stream. This operation is intermediate which enables
us to call another stream operation (e.g. forEach) on the result.
Output:
Amitabh
Aman
8.5.2 Stream.map()
The intermediate operation map converts each element into another object via
the given function. The following example converts each string into an upper-
41
cased string. But you can also use map to transform each object into another
type.
Output:
AMITABH
AMAN
8.5.3 Stream.sorted()
Sorted is an intermediate operation which returns a sorted view of the stream. The elements are sorted in
natural order unless you pass a custom Comparator.
memberNames.stream().sorted()
.map(String::toUpperCase)
.forEach(System.out::println);
Output:
AMAN
AMITABH
LOKESH
RAHUL
SALMAN
SHAHRUKH
SHEKHAR
YANA
Keep in mind that sorted does only create a sorted view of the stream without manipulating the ordering of
the backed collection. The ordering of memberNames is untouched.
8.5.4 Terminal operations
This method helps in iterating over all elements of a stream and perform some operation on each of them.
The operation is passed as lambda expression parameter.
memberNames.forEach(System.out::println);
8.5.6 Stream.collect()
42
System.out.print(memNamesInUppercase);
8.5.6.2.2 Example:
Use Case:
Initializes a mutable container (e.g., StringBuilder, ArrayList, HashMap).
Called once per thread in parallel streams.
8.5.6.2.4 Example:
Use Case:
Processes each stream element and updates the container.
43
Example: Adding strings to a List, summing numbers, etc.
8.5.6.2.6 Example:
Use Case:
Combines results from multiple threads (e.g., merging two StringBuilder objects).
Unused in sequential streams but required syntactically.
<R> R collect(
Supplier<R> supplier,
BiConsumer<R, ? super T> accumulator,
BiConsumer<R, R> combiner
);
class SumHolder {
int sum = 0;
}
44
(h1, h2) -> h1.sum += h2.sum // Combiner
);
System.out.println(sumHolder.sum); // Output: 10
System.out.println(lengthMap);
// Output: {5=[Apple], 6=[Banana, Cherry], 4=[Date]}
Use Use
Scenario collect() Collectors.toList()
Need a custom container (e.g., StringBuilder, ✅ ❌
Stats)
Parallel stream processing ✅ ✅ (built-in support)
Simple collection (e.g., List, Set) ❌ ✅
45
8.5.7 Stream.match()
Various matching operations can be used to check whether a certain predicate matches the stream. All of
those operations are terminal and return a boolean result.
System.out.println(matchedResult);
matchedResult = memberNames.stream()
.allMatch((s) -> s.startsWith("A"));
System.out.println(matchedResult);
matchedResult = memberNames.stream()
.noneMatch((s) -> s.startsWith("A"));
System.out.println(matchedResult);
Output:
true
false
false
8.5.8 Stream.count()
Count is a terminal operation returning the number of elements in the stream as a long.
System.out.println(totalMatched);
Output: 2
8.5.9 Stream.reduce()
This terminal operation performs a reduction on the elements of the stream with the given function. The
result is an Optional holding the reduced value.
reduced.ifPresent(System.out::println);
46
Output: Amitabh#Shekhar#Aman#Rahul#Shahrukh#Salman#Yana#Lokesh
Though, stream operations are performed on all elements inside a collection satisfying a predicate, It is
often desired to break the operation whenever a matching element is encountered during iteration. In
external iteration, you will do with if-else block. In internal iteration, there are certain methods you can use
for this purpose. Let’s see example of two such methods:
This will return true once a condition passed as predicate satisfy. It will not process any more elements.
System.out.println(matched);
Output: true
It will return first element from stream and then will not process any more element.
System.out.println(firstMatchedName);
Output: Lokesh
With the Fork/Join framework added in Java SE 7, we have efficient machinery for implementing parallel
operations in our applications. But implementing this framework is itself a complex task; and if not done
47
right; is a source of complex multi-threading bugs having potential to crash the application. With the
introduction of internal iteration, we got the possibility of operations to be done in parallel.
To enable parallelism, all you have to do is to create a parallel stream, instead of sequential stream. And to
surprise you, this is really very easy. In any of above listed stream examples, anytime you want to particular
job using multiple threads in parallel cores, all you have to call method parallelStream() method instead of
stream() method.
A key driver for this work is making parallelism more accessible to developers. While the Java platform
provides strong support for concurrency and parallelism already, developers face unnecessary impediments
in migrating their code from sequential to parallel as needed. Therefore, it is important to encourage idioms
that are both sequential- and parallel-friendly. This is facilitated by shifting the focus towards describing
what computation should be performed, rather than how it should be performed.
It is also important to strike the balance between making parallelism easier but not going so far as to make it
invisible. Making parallelism transparent would introduce non-determinism and the possibility of data races
where users might not expect it.
This is all what I wanted to share regarding basics of Stream abstraction introduced in java 8. I will be
talking about various other things related to Streams in future posts.
Happy Learning !!
Read More:
48
8.8 Stream Operations
filter()
map()
flatMap()
distinct()
sorted()
peek()
limit()
skip()
forEach()
forEachOrdered()
toArray()
reduce()
collect()
min()
max()
count()
anyMatch()
allMatch()
noneMatch()
findFirst()
findAny()
49
9 Default and Static Methods in Interfaces
9.1 Why Introduced?
interface Vehicle {
// Regular abstract method
void start();
// Default method
default void honk() {
System.out.println("Default honk sound");
}
}
// Usage:
Vehicle car = new Car();
car.honk(); // "Default honk sound"
interface MathOperations {
static int add(int a, int b) {
return a + b;
}
}
// Usage:
int sum = MathOperations.add(5, 3); // 8
50
5. Class implementation wins
6. Sub-interface can override
7. Explicit override required if ambiguous:
interface A { default void foo() {} }
interface B { default void foo() {} }
class C implements A, B {
@Override public void foo() {
A.super.foo(); // Explicitly choose A's implementation
}
}
10 Optional Class
10.1 What is Optional class and what is it’s use?
Java SE 8 introduces new class in util package i.e java.util.Optional . In a nutshell, you can view Optional as
a single value container which either contains the value or not (then it is called as empty).
It is used to avoid NullPointerException. This Optional class concept is inspired from Haskell and Scala.
Optional<Soundcard> sc = Optional.empty();
Optional<Soundcard> sc = Optional.of(soundcard);
The Optional class have various utilities method such as isPresent() which help coders to avoid making use
of null value checks.
51
10.2 Purpose
import java.util.Optional;
import java.util.function.Supplier;
// =================================================================
// 2. VALUE ACCESS EXAMPLES
// =================================================================
52
} catch (RuntimeException e) {
System.out.println("orElseThrow() example: " + e.getMessage());
}
// Output: orElseThrow() example: Value not present
// =================================================================
// 3. CONDITIONAL ACTIONS
// =================================================================
// =================================================================
// 4. TRANSFORMATION AND CHAINING
// =================================================================
// =================================================================
// 5. PRACTICAL USE CASES
// =================================================================
System.out.println("\nPractical examples:");
53
.filter(s -> !s.isEmpty())
.map(String::toUpperCase)
.orElse("DEFAULT");
System.out.println("Method chaining result: " + result);
// Possible Outputs:
// If getExternalValue() returns non-null: Method chaining result: VALID VALUE
// If getExternalValue() returns null: Method chaining result: DEFAULT
1. Optional Creation:
o Optional.empty() - Explicit empty container
2. Value Access:
o orElse() - Simple default value
3. Conditional Operations:
o ifPresent() - Consumer-based action
4. Transformation:
o map() - Value transformation
54
o flatMap() - Flatten nested Optionals
5. Practical Patterns:
o Safe null handling in method chains
Use as return type for methods that might not return value
Never use for class fields or method parameters
Avoid Optional.get() without checking isPresent()
11 java.time API
11.1 Core Classes
// With timezone
ZonedDateTime zoned = ZonedDateTime.now(ZoneId.of("Europe/Paris"));
// Duration (time-based)
Duration duration = Duration.between(startTime, endTime);
// Period (date-based)
Period period = Period.between(startDate, endDate);
11.1.1 Formatting
55
12 CompletableFuture
// Remove conditionally
list.removeIf(s -> s.equals("b")); // ["a", "c"]
// Map enhancements
Map<String, Integer> map = new HashMap<>();
map.putIfAbsent("a", 1);
map.compute("a", (k, v) -> v + 1); // a=2
map.merge("a", 1, Integer::sum); // a=3
56
14.2 Repeating Annotations
@Repeatable(Schedules.class)
@interface Schedule {
String time();
}
@interface Schedules {
Schedule[] value();
}
@Schedule(time = "10:00")
@Schedule(time = "15:00")
class Meeting {}
// Shorthand
String joined = String.join("-", "A", "B", "C"); // A-B-C
57