Unleashing the power of Java 8: The Ultimate Guide to Its Key Features

Keval Padsumbiya
Javarevisited
Published in
7 min readFeb 16, 2023
Java 8

Java 8 introduced several new features that helped to make it more versatile and flexible. Java 8 feature make functional programming possible. Here are some of the key features of Java 8:

  1. Lambda Expressions
  2. Functional Interfaces
  3. Stream API
  4. Default Methods
  5. Method References
  6. Optional
  7. Date/Time API

In this story we will learn lambda expressions, functional interfaces and stream API in detail with examples.

1. Lambda Expressions

Java 8 | Lambda Expressions

Using lambda expressions we can represent a block of code that can be executed later.

Lambda expressions are used to define anonymous functions, which can be passed as method arguments or can be returned from methods just like other object.

Lambda expression syntax:

(parameters) -> { statements };

The -> symbol separates the parameters from the body of the Lambda expression.

Examples:

(i) no arguments

()-> System.out.println("lambda expression with no arguments");

(ii) one or more arguments

// lambda expression with one argument
(a) -> System.out.println(String.format("Value of a is: %s", a);

// lambda expression with multiple arguments
(a, b) -> a + b;

// also argument type can be provided explicitely
(Integer a, Integer b) -> a + b;

(iii) more than one statements

(Integer a, Integer b) -> {  // parentheses needed for multiple statements
System.out.println(String.format("Returning maximum from %s and %s", a, b));
return (a > b) a : b;
};

Let’s see below example in which lambda expression is used to find maximum of two numbers:

// functional interface, we will see below in detail
interface MaxFinder {
int findMax(int a, int b);
}

public class LambdaExpressionExample {
public static void main(String[] args) {
// return maximum of two numbers using lambda expression
MaxFinder maxFinder = (a, b) -> (a > b) ? a : b;

System.out.println(maxFinder.findMax(5, 10));
System.out.println(maxFinder.findMax(15, 5));
}
}

Output of above program:

Output

Now, let’s see how lambda expression can be used with forEach loop:

public class LambdaExpressionExample {
public static void main(String[] args) {
List<Integer> nums = new ArrayList<>();
nums.add(1);
nums.add(2);
nums.add(3);
nums.add(4);
nums.forEach(num -> System.out.println(String.format("Square of %s is %s", num, num * num)));
}
}

Output of above program:

Output

2. Functional Interfaces

Java 8 | Functional Interfaces

An interface that has exactly one abstract method is called a functional interface and also named as Single Abstract Method Interfaces (SAM Interfaces).

Functional Interfaces are the foundation for Lambda expressions and it allows writing concise and expressive code by removing the need for anonymous inner classes.

Java 8 provides number of built in functional interfaces, some of them are Predicate, Function, Consumer and Supplier.

You can refer above simple example where we have defined a functional interface MaxFinder.

@FunctionalInterface annotation is used to represent a functional interface.

(i) Predicate

Predicate interface has a single abstract method test(T t) that takes a single argument and returns a boolean value.

Generally it is used to filter values or to test whether a value satisfies a condition.

T: type of the input argument of the method

Here is an example using Predicate:

Predicate<Integer> isEligibleForVoting = (age) -> age >= 18;
boolean eligible = isEligibleForVoting.test(22); // true

(ii) Function

Function interface has a single abstract method apply(T t) that takes a single argument and returns a object.

Generally it is used in transforming or mapping values from one type to another.

Here is an example using Function:

Function<Integer, Integer> findSquare = (num) -> (num*num);
int square = findSquare.apply(5); // 25

(iii) Consumer

Consumer interface has a single abstract method accept(T t) that takes a single argument and returns no result.

Generally it is used for performing side effects like printing or logging.

Here is an example using Consumer:

Consumer<String> printValue = (x) -> System.out.println(x);
printValue.accept("Java 8 features!"); // prints "Java 8 features!"

(iv) Supplier

Supplier interface has a single abstract method get() that takes no argument and returns a result.

Generally it is used for lazy initialization or for generating random values.

Here is an example using Supplier:

Supplier<Integer> getRandomInteger = () -> (int) (Math.random() * 500);
int randomInteger = getRandomInteger.get(); // random integer between 0 and 500

Now, let’s see below example using above all 4 functional interfaces:

public class FunctionalInterfacesExample {

public static void main(String[] args) {
// Using Predicate functional interface
Predicate<Integer> isEligibleForVoting = (age) -> age >= 18;
boolean eligible = isEligibleForVoting.test(22); // true
System.out.println(String.format("Eligible for voting: %s", eligible));

// Using Function functional interface
Function<Integer, Integer> findSquare = (num) -> (num * num);
int square = findSquare.apply(5); // 25
System.out.println(String.format("Square of 5: %s", square));

// Using Consumer functional interface
Consumer<String> printValue = (val) -> System.out.println(val);
printValue.accept("Java 8 features!"); // prints "Java 8 features!"

// Using Supplier functional interface
Supplier<Integer> getRandomInteger = () -> (int) (Math.random() * 500);
int randomInteger = getRandomInteger.get(); // random integer between 0 and 500
System.out.println(String.format("Random number: %s", randomInteger));
}

}

Output of above program:

Output

3. Stream API

Java 8 | Stream API

Java 8 Stream API is a powerful tool for processing collections of data.

Using Stream API complex data manipulation tasks can be performed in more concise and expressive way than it can be done with conditional statements and traditional loops.

A Stream is an ordered sequence of elements which can be processed sequentially or parallelly.

With the help of functional interfaces and methods we can create, transform, and process streams.

Stream of a collection can be created using the method stream().

The Streams API is a new addition to the Java Collections Framework that provides a declarative way to process collections of objects. Streams allow you to perform various operations, such as filtering, mapping and reducing on collections of objects in a more concise and readable way than traditional for-loops.

The Stream API consists of three types of operations:

(i) Intermediate Operations:

Intermediate operations are used to transform or filter the elements of a stream.

They return a new stream that further cab be processed by intermediate or terminal operations.

Most frequently used intermediate operations are:

  • filter(): used to filter values based on a condition
  • sorted(): used to sort elements
  • distinct(): used to find distinct elements
  • map(): used to map value from one type to another

(ii) Terminal Operations:

Terminal operations are used to produce a final result from a stream.

They are called after all intermediate operations which have been applied to the stream.

Most frequently used terminal operations are:

  • collect(): used to return outcome of the intermediate operations
  • reduce(): used to reduces the elements of a stream into a single element.
  • forEach(): used to iterate through every element in the stream
  • count(): returns the count of elements in this stream

(iii) Short-Circuiting Operations:

Short-circuting operations are used to produce a result early, without processing the entire stream.

Java 8 stream intermediate and terminal operations both can be short-circuiting.

Most frequently used terminal operations are:

  • limit(): used to return stream as soon as reaches to given limit value
  • anyMatch(): used to check if any elements of this stream match the provided predicate.
  • findFirst(): returns an Optional describing the first element of the stream, or an empty Optional if the stream is empty
  • findAny(): returns an Optional describing some element of the stream, or an empty Optional if the stream is empty.

Now, let’s see example using stream:

public class StreamAPIExample {

public static void main(String[] args) {
List<Integer> nums = Arrays.asList(3, 1, 4, 2, 6, 8, 5, 7);
System.out.print("Given numbers list: ");
nums.forEach(num -> System.out.print(String.format("%s, ", num)));
System.out.println();

// sort list of numbers
List<Integer> sortedNums = nums.stream().sorted().collect(Collectors.toList());
System.out.print("Sorted numbers are: ");
sortedNums.forEach(num -> System.out.print(String.format("%s, ", num)));
System.out.println();

// filter all numbers less than 6
List<Integer> filteredNums = nums.stream()
.filter(i -> i < 6)
.collect(Collectors.toList());
System.out.print("Numbers less then 5 are: ");
filteredNums.forEach(num -> System.out.print(String.format("%s, ", num)));
System.out.println();

// odd numbers
long oddNumsCount = nums.stream()
.filter(i -> i % 2 == 1).count();
System.out.println(String.format("Total odd numbers are: %s", oddNumsCount));

// print first 2 even numbers found from list while iterating stream
System.out.print("first 2 even numbers found from list while iterating stream: ");
nums.stream()
.filter(num -> num % 2 == 0)
.limit(2)
.forEach(num -> System.out.print(String.format("%s, ", num)));
System.out.println();

// find squares of numbers
List<Integer> squares = nums.stream()
.map(num -> (num * num))
.collect(Collectors.toList());
System.out.print("Squares of numbers are: ");
squares.forEach(square -> System.out.print(String.format("%s, ", square)));
}

}

Output of above program:

Output

Thank you for reading, hope this story helped you in learning Java 8 features.

Please follow if you like the article.

Subscribe to stay tuned for upcoming articles.

--

--

Keval Padsumbiya
Javarevisited

Competitive Programmer and Software Engineer with experience in Java, Spring Boot, AWS, Jooq, SQL, Terraform etc