avatarVinotech

Summary

The provided content is a comprehensive guide to Java 8 features, including Stream API, lambda expressions, functional interfaces, method references, default methods, Optional class, Date/Time API, forEach method, StringJoiner class, and Spliterator interface, with examples and explanations for each concept.

Abstract

The text serves as an in-depth tutorial on Java 8's new and advanced features, focusing on functional programming paradigms. It begins by introducing lambda expressions as a foundation for writing more concise and readable code. The guide then delves into functional interfaces, which are key to using lambda expressions effectively, and illustrates their usage with examples like Predicate, Function, and Consumer. Method references are presented as a shorthand notation for lambda expressions that directly call a method. The concept of default methods is explained as a way to add new methods to interfaces without breaking existing implementations. The Optional class is discussed as a means to handle null values and avoid NullPointerException. The Date/Time API section covers the LocalDate, LocalTime, LocalDateTime, ZonedDateTime, Period, and Duration classes, providing insights into handling date and time in Java 8. The forEach method is explained with various use cases, including iterating over collections and streams. The StringJoiner class is introduced for string concatenation with delimiters. Finally, the Spliterator interface is described for efficient traversal and partitioning of elements in a collection, particularly useful for parallel processing.

Opinions

  • The author emphasizes the importance of lambda expressions for simplifying code and enabling functional programming in Java.
  • Functional interfaces are highlighted as a critical component in the Java 8 framework, facilitating the use of lambda expressions and method references.
  • The use of method references is recommended for cleaner and more readable code when a lambda expression is just a method call.
  • Default methods are seen as a significant enhancement for maintaining backward compatibility when evolving interfaces.
  • The Optional class is strongly advocated for as a tool to mitigate the risk of null values and enhance code safety.
  • The Date/Time API is presented as a robust and intuitive solution for date and time manipulation, replacing the older, error-prone date and calendar classes.
  • The forEach method is portrayed as a versatile tool for iterating over collections, with a distinction made between forEach and forEachOrdered for parallel streams.
  • The StringJoiner class is commended for its utility in concatenating strings with optional prefixes, delimiters, and suffixes.
  • Spliterator is regarded as an advanced feature for developers who need to optimize parallel processing of large datasets.

Java 8 Interview Questions and Answer

πŸ‘ Clap for the story to help this article be featured

Stream API Interview Questions GITHUB Link https://github.com/vinosubi/Streams-Example-part1

Source code download : https://github.com/vinosubi/Streams-Example-part1/archive/refs/heads/master.zip

Topics Covered

  1. Lambda Expressions
  2. Functional Interfaces
  3. built-in functional interfaces [ Predicate, BiPredicate, Function, BiFunction, Consumer, BiConsumer, Supplier, UnaryOperator, BinaryOperator ] With examples
  4. Method references [ Reference to a Static Method, Reference to an Instance Method of a Particular Object, Reference to an Instance Method of an Arbitrary Object of a Particular Type, Reference to a Constructor ]
  5. Default methods
  6. Static methods
  7. Optional Class (Optional.empty(),Optional.of(), Optional.ofNullable(), isPresent(), ifPresent(), get(), orElse(), orElseGet(), orElseThrow() )
  8. Date/Time API [ LocalDate class, LocalTime class, LocalDateTime class, ZonedDateTime class, Period class, Duration Class, Temporal adjusters ].
  9. forEach() method [ Iterating a Map, Iterating a List, Iterating a Set, Iterating a Stream, Using forEach with Consumer, Exception Handling with forEach, forEach vs forEachOrdered ]
  10. Stringjoiner class
  11. Spliterator interface
  12. Nashorn JavaScript engine
  13. Stream API

What are Lambda Expressions? A lambda expression is an anonymous function; that is, a function without a name. It allows for functional programming in Java, enabling developers to pass functions as arguments to methods or be stored in a variable.

(parameters) -> expression or (parameters) -> { statements; }

Let’s break it down:

  • Parameters: These are the inputs to the lambda expression. If there are no parameters, you can simply use empty parentheses ().
  • Arrow token (->): This separates the parameters from the body of the lambda.
  • Body: This is the code that gets executed when the lambda is invoked. It can be a single expression or a block of statements.

Without lambda expressions:

List<String> names = Arrays.asList("Peter", "Paul", "Mary");
for (String name : names) {
    System.out.println(name);
}

With lambda expressions:

List<String> names = Arrays.asList("Peter", "Paul", "Mary");
names.forEach(name -> System.out.println(name));

Using Lambda Expressions with Functional Interfaces

@FunctionalInterface
interface Greeting {
    void sayHello(String name);
}

public class LambdaExample {
    public static void main(String[] args) {
        Greeting greeting = (name) -> System.out.println("Hello, " + name);
        greeting.sayHello("World");
    }
}

In this example, Greeting is a functional interface with a single method sayHello. We use a lambda expression to provide an implementation for this method.

Benefits of Lambda Expressions

Lambda expressions bring several benefits to Java programming:

  1. Conciseness: They reduce the boilerplate code, making your programs shorter and easier to read.
  2. Readability: Code written using lambda expressions is more straightforward and looks closer to natural language.
  3. Functional Programming: They enable a more functional programming style, which is beneficial for processing collections and streams of data.

What is Functional Interfaces

  1. A Functional Interface is an interface that has only one abstract method.
  2. Instances of functional interfaces can be created with the help of lambda expressions, method references and constructor references.
  3. @FunctionalInterface Annotation: While it’s not mandatory, it’s a good practice to annotate functional interfaces with @FunctionalInterface. This annotation helps the compiler catch any accidental addition of multiple abstract methods to the interface.
  4. A functional interface can have any number of default methods.
  5. A functional interface can also contain any number of static methods.

Let’s Understand It With an Example:

@FunctionalInterface
public interface ExampleInterface {
    int randomCalculate(int a, int b);

    default void print(int result) {
        System.out.println(result);
    }
}

The above example interface contains one abstract method and one default method.

Now let’s understand how lambda expression can create instances of functional interface and how to call the implemented method.

Example : Functional interface using a lambda expression.

import java.util.Random;

public class TestFunctionalInterface {

    public static void main(String[] args) {

  
      // functional interface using a lambda expression.
        ExampleInterface exampleInterface = (a, b) -> {
            int randomCal = a * b / 20;
            Random random = new Random();
            randomCal = randomCal + random.nextInt(1000);
            return randomCal;
        };

        exampleInterface.print(exampleInterface.randomCalculate(10, 20));
    }
}

TestFuntionalInterface this class is a driver class that has a main method and inside this method, a lambda expression exampleInterface has implemented the abstract method from ExampleInterface . Using this lambda the function definition can be called(e.g. exampleInterface.randomCalculate(10, 20)). Similarly, the default method of the functional interface can also be called using the same lambda exampleInterface.(e.g. exampleInterface.print()).

Example : Functional interface using a method reference.

@FunctionalInterface
public interface ExampleInterface {
    int randomCalculate(int a, int b);

    default void print(int result) {
        System.out.println(result);
    }
}


public class TestFunctionalInterface {

    // Static method matching the signature of randomCalculate
    public static int calculate(int a, int b) {
        int randomCal = a * b / 20;
        Random random = new Random();
        randomCal = randomCal + random.nextInt(1000);
        return randomCal;
    }

    public static void main(String[] args) {

        // Functional interface using a method reference.
        ExampleInterface exampleInterface = TestFunctionalInterface::calculate;

        exampleInterface.print(exampleInterface.randomCalculate(10, 20));
    }
}

Example : Functional interface using a constructor reference.

import java.util.function.BiFunction;

// Class that performs the calculation
class Calculation {
    private int a;
    private int b;

    public Calculation(int a, int b) {
        this.a = a;
        this.b = b;
    }

    public int performCalculation() {
        int randomCal = a * b / 20;
        Random random = new Random();
        randomCal = randomCal + random.nextInt(1000);
        return randomCal;
    }
}

// Functional interface for factory method
@FunctionalInterface
interface CalculationFactory {
    Calculation create(int a, int b);
}

public class TestConstructorReference {

    public static void main(String[] args) {
        // Constructor reference
        CalculationFactory factory = Calculation::new;

        // Use the factory to create an instance and perform the calculation
        Calculation calculation = factory.create(10, 20);
        System.out.println(calculation.performCalculation());
    }
}

Common Functional Interfaces: Java 8 introduced several built-in functional interfaces in the java.util.function package, including:

  • Predicate<T>: Represents a boolean-valued function of one argument.
  • BiPredicate<T, U>: Takes two arguments and returns a boolean.
  • Function<T, R>: Represents a function that accepts one argument and produces a result.
  • BiFunction<T, U, R>: Takes two arguments and returns a result.
  • Consumer<T>: Represents an operation that accepts a single input argument and returns no result.
  • BiConsumer<T, U>: Takes two arguments and performs an operation, returning nothing (void).
  • Supplier<T>: Represents a supplier of results.
  • UnaryOperator<T>: Represents an operation on a single operand that produces a result of the same type as its operand.
  • BinaryOperator<T>: Represents an operation upon two operands of the same type, producing a result of the same type as the operands.

Example for all function interface

class Employee {
    private String name;
    private int id;

    public Employee(String name, int id) {
        this.name = name;
        this.id = id;
    }

    // Getters and toString method
    public String getName() {
        return name;
    }

    public int getId() {
        return id;
    }

    @Override
    public String toString() {
        return "Employee{name='" + name + "', id=" + id + "}";
    }
}

1. Supplier<T>:

  • Abstract method: T get()
  • This represents a supplier of results, providing a value without taking any input.

Example : The Supplier functional interface is used to represent a function that does not take any arguments and produces a result. It is often used for lazy evaluation or to supply instances of a certain object.

public class Main {
    public static void main(String[] args) {
        // Supplier that supplies an Employee object
        Supplier<Employee> employeeSupplier = () -> new Employee("John Doe", 123);

        // Get the Employee object from the Supplier
        Employee employee = employeeSupplier.get();

        // Output the Employee object
        System.out.println(employee);
    }
}

// output
Employee{name='John Doe', id=123}

Predicate<T>:

  • Abstract method: boolean test(T t)
  • It represents a predicate/condition, a boolean-valued function that checks a condition on an input.
  • It is used for filtering or testing elements based on a condition.

Example : The Predicate interface represents a function that takes a single input and returns a boolean value. It is commonly used for filtering or matching conditions.

Let’s say you have an Employee object, and you want to use a Predicate to check certain conditions, such as whether the employee's ID is greater than a certain value or if the employee's name starts with a specific letter.

public class Main {
    public static void main(String[] args) {
        Employee employee = new Employee("Alice Smith", 102);

        // Predicate to check if the employee's ID is greater than 100
        Predicate<Employee> isIdGreaterThan100 = emp -> emp.getId() > 100;

        // Predicate to check if the employee's name starts with "A"
        Predicate<Employee> isNameStartsWithA = emp -> emp.getName().startsWith("A");

        // Using the predicates
        boolean result1 = isIdGreaterThan100.test(employee);
        boolean result2 = isNameStartsWithA.test(employee);

        // Output the results
        System.out.println("Is employee ID greater than 100? " + result1);
        System.out.println("Does employee name start with 'A'? " + result2);
    }
}

// output
Is employee ID greater than 100? true
Does employee name start with 'A'? true

Function<T, R>

  • Abstract method: R apply(T t)
  • This represents a function that accepts one argument and produces a result.
  • It is commonly used for mapping or transforming input to output.

Example : The Function<T, R> interface represents a function that takes an input of type T and returns a result of type R. This interface is useful when you want to transform an input object into another object or value.

public class Main {
    public static void main(String[] args) {
        Employee employee = new Employee("Bob Johnson", 201);

        // Function that takes an Employee and returns a formatted String
        Function<Employee, String> employeeToStringFunction = emp -> 
            "Employee Details: Name - " + emp.getName() + ", ID - " + emp.getId();

        // Apply the function to the Employee object
        String employeeDetails = employeeToStringFunction.apply(employee);

        // Output the result
        System.out.println(employeeDetails);
    }
}

//output
Employee Details: Name - Bob Johnson, ID - 201

Consumer<T>:

  • Abstract method: void accept(T t)
  • This represents an operation that accepts a single input and returns no value or result.
  • It is used for performing actions on input values, such as printing, saving, or processing.

Example : The Consumer<T> interface represents an operation that takes a single input argument of type T and performs some action on it but does not return any result. It's often used for operations like printing, logging, or modifying an object.

Here’s an example using the Consumer<T> interface with an Employee object. Let's create a Consumer that prints out the details of an Employee and another Consumer that gives a raise to the employee by modifying their salary.

public class Main {
    public static void main(String[] args) {
        Employee employee = new Employee("Sarah Connor", 303, 50000);

        // Consumer to print Employee details
        Consumer<Employee> printEmployeeDetails = emp -> System.out.println(emp);

        // Consumer to give a raise by increasing salary by 10%
        Consumer<Employee> giveRaise = emp -> emp.setSalary(emp.getSalary() * 1.10);

        // Apply the Consumers
        printEmployeeDetails.accept(employee);  // Print details before raise
        giveRaise.accept(employee);             // Give a raise
        printEmployeeDetails.accept(employee);  // Print details after raise
    }
}

// output
Employee{name='Sarah Connor', id=303, salary=50000.0}
Employee{name='Sarah Connor', id=303, salary=55000.0}

UnaryOperator<T>:

  • Abstract method: T apply(T t)
  • This is a special case of the Function interface where the input and output types are the same.
  • It is used for operations that transform an input into an output of the same type.

Example : The UnaryOperator<T> interface is a specialized version of the Function<T, R> interface where both the input and output are of the same type. It's often used for operations that map an object to another object of the same type, such as updating or transforming a value.

Here’s an example using the UnaryOperator<T> interface with an Employee object. We'll create a UnaryOperator that increases the employee's salary by a certain percentage.

import java.util.function.UnaryOperator;

class Employee {
    private String name;
    private int id;
    private double salary;

    public Employee(String name, int id, double salary) {
        this.name = name;
        this.id = id;
        this.salary = salary;
    }

    // Getters and setters
    public String getName() {
        return name;
    }

    public int getId() {
        return id;
    }

    public double getSalary() {
        return salary;
    }

    public void setSalary(double salary) {
        this.salary = salary;
    }

    @Override
    public String toString() {
        return "Employee{name='" + name + "', id=" + id + ", salary=" + salary + "}";
    }
}

public class Main {
    public static void main(String[] args) {
        Employee employee = new Employee("John Doe", 101, 40000);

        // UnaryOperator to give a 10% raise
        UnaryOperator<Employee> giveRaise = emp -> {
            emp.setSalary(emp.getSalary() * 1.10);
            return emp;
        };

        // Apply the UnaryOperator
        Employee updatedEmployee = giveRaise.apply(employee);

        // Output the updated Employee details
        System.out.println(updatedEmployee);
    }
}

// output
Employee{name='John Doe', id=101, salary=44000.0}

BinaryOperator:

  • Abstract method: T apply(T t, T u)
  • It is a special case of the BiFunction interface where the two input arguments and the output are of the same type.
  • It is used for binary operations that combine two inputs into a single result.

Example : The BinaryOperator<T> interface is a specialized version of the BiFunction<T, T, T> interface, where both the operands and the result are of the same type. It's commonly used for operations that combine two values of the same type into a single result of that type.

Let’s create an example using the BinaryOperator<T> interface with an Employee object. Suppose you want to combine two Employee objects and decide which one has the higher salary. The BinaryOperator will return the Employee with the higher salary.

import java.util.function.BinaryOperator;

class Employee {
    private String name;
    private int id;
    private double salary;

    public Employee(String name, int id, double salary) {
        this.name = name;
        this.id = id;
        this.salary = salary;
    }

    // Getters
    public String getName() {
        return name;
    }

    public int getId() {
        return id;
    }

    public double getSalary() {
        return salary;
    }

    @Override
    public String toString() {
        return "Employee{name='" + name + "', id=" + id + ", salary=" + salary + "}";
    }
}

public class Main {
    public static void main(String[] args) {
        Employee employee1 = new Employee("Alice Smith", 201, 60000);
        Employee employee2 = new Employee("Bob Johnson", 202, 75000);

        // BinaryOperator to determine which Employee has a higher salary
        BinaryOperator<Employee> higherSalary = (emp1, emp2) -> emp1.getSalary() > emp2.getSalary() ? emp1 : emp2;

        // Apply the BinaryOperator
        Employee result = higherSalary.apply(employee1, employee2);

        // Output the Employee with the higher salary
        System.out.println("Employee with the higher salary: " + result);
    }
}

// output

Employee with the higher salary: Employee{name='Bob Johnson', id=202, salary=75000.0}

What is Method References?

Method references are shorthand notations for lambda expressions that directly call a method. They provide a way to refer to methods without invoking them. Method references simplify the code and make it more concise, especially when the lambda expression contains only a method call.

In Java, there are four types of method references:

  1. Reference to a Static Method: A method reference to a static method uses the syntax `ClassName::staticMethodName`. It refers to a static method of the specified class.
  2. Reference to an Instance Method of a Particular Object: A method reference to an instance method of a particular object uses the syntax `objectReference::instanceMethodName`. It refers to an instance method of the specified object.
  3. Reference to an Instance Method of an Arbitrary Object of a Particular Type: A method reference to an instance method of an arbitrary object of a particular type uses the syntax `ClassName::instanceMethodName`. It refers to an instance method of the first argument of the specified type.
  4. Reference to a Constructor: A method reference to a constructor uses the syntax `ClassName::new`. It refers to the constructor of the specified class.

Example: Reference to a Static Method

import java.util.Arrays;
import java.util.List;

public class MethodReferenceExample {

    public static void main(String[] args) {
        List<String> names = Arrays.asList("Alice", "Bob", "Charlie");

        // Using lambda expression
        names.forEach(name -> System.out.println(name));

        // Using method reference
        names.forEach(System.out::println);
    }
}

Example 2 :

public class MathUtils {
    public static int multiply(int x, int y) {
        return x * y;
    }
}

public class Main {
    public static void main(String[] args) {
        // Method reference to MathUtils.multiply
        MyFunction multiply = MathUtils::multiply;

        // Use the method reference
        int result = multiply.apply(4, 5);
        System.out.println(result); // Output: 20
    }
}

Example : Reference to an Instance Method

public class MyClass {
    public void instanceMethod(String message) {
        System.out.println(message);
    }
}
import java.util.function.Consumer;

public class Main {
    public static void main(String[] args) {
        MyClass myClassInstance = new MyClass();
        
        // Using a lambda expression
        Consumer<String> lambdaConsumer = (message) -> myClassInstance.instanceMethod(message);
        lambdaConsumer.accept("Hello from lambda!");

        // Using method reference
        Consumer<String> methodRefConsumer = myClassInstance::instanceMethod;
        methodRefConsumer.accept("Hello from method reference!");
    }
}

// output
Hello from lambda!
Hello from method reference!

Example: Reference to a Constructor

import java.util.function.Supplier;

class Person {
    private String name;

    public Person(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }
}

public class MethodReferenceExample {

    public static void main(String[] args) {
        // Using lambda expression
        Supplier<Person> personSupplier1 = () -> new Person("Alice");
        Person alice = personSupplier1.get();

        // Using method reference
        Supplier<Person> personSupplier2 = Person::new;
        Person bob = personSupplier2.get();

        System.out.println(alice.getName()); // Output: Alice
        System.out.println(bob.getName());   // Output: null
    }
}

What is Default method?

Why Default method? Adding one or more new methods to interface will result in all the implementing classes will have to provide implementation for new method which creates a major issue while maintaining backward compatibility. This problem usually occurs in java version prior to java 8.

  1. Default methods were introduced in Java 8 as a new feature for interfaces.
  2. Allow the addition of new methods to interfaces without breaking existing implementations
  3. Implementers can choose whether to use the default method implementation present in interface or provide their own implementation(@Override).

𝑾𝒉𝒂𝒕 𝒆𝒍𝒔𝒆 !? 𝑾𝒆 𝒄𝒂𝒏 𝒖𝒔𝒆 π’”π’•π’‚π’•π’Šπ’„ π’Žπ’†π’•π’‰π’π’…π’” , π’Šπ’• 𝒅𝒐𝒆𝒔 𝒕𝒉𝒆 π’”π’‚π’Žπ’† π’•π’‰π’Šπ’π’ˆ .

No , static methods cannot be overrided , otherwise we can override default methods .

Example 1 :

interface Printer {
    default void print() {
        System.out.println("Printing in black and white");
    }
}

class BasicPrinter implements Printer {
    // Uses the default implementation
}

class ColorPrinter implements Printer {
    // Overrides the default implementation
    @Override
    public void print() {
        System.out.println("Printing in color");
    }
}

public class Main {
    public static void main(String[] args) {
        Printer basicPrinter = new BasicPrinter();
        Printer colorPrinter = new ColorPrinter();
        
        basicPrinter.print(); // Output: Printing in black and white
        colorPrinter.print(); // Output: Printing in color
    }
}

In this example:

  1. The Printer interface provides a default implementation for the print() method.
  2. BasicPrinter implements Printer but doesn't override print(). It uses the default implementation as-is.
  3. ColorPrinter also implements Printer but overrides print() with its own implementation.

Example 2 :

interface Vehicle {
    void start();

    default void stop() {
        System.out.println("Vehicle stopped");
    }
}

class Car implements Vehicle {
    public void start() {
        System.out.println("Car started");
    }
}

public class Main {
    public static void main(String[] args) {
        Car car = new Car();
        car.start(); // Output: Car started
        car.stop();  // Output: Vehicle stopped (default implementation from Vehicle interface)
    }
}

What is Static method? Static methods in interfaces can be called directly on the interface without requiring an instance of a class implementing the interface. These methods are commonly used for utility functions related to the interface.

Example :

interface MathOperations {
    static int add(int a, int b) {
        return a + b;
    }

    static int multiply(int a, int b) {
        return a * b;
    }
}

public class Main {
    public static void main(String[] args) {
        int sum = MathOperations.add(5, 3); // Using static method directly from interface
        int product = MathOperations.multiply(5, 3); // Using static method directly from interface

        System.out.println("Sum: " + sum); // Output: Sum: 8
        System.out.println("Product: " + product); // Output: Product: 15
    }
}

Static methods never Override

In Java interfaces, static methods cannot be overridden by implementing classes. When a class implements an interface that includes a static method, it can’t provide its implementation of that static method or override it.

What is Optional Class? Java 8 introduced the Optional class as a way to handle null values avoid NullPointerException.

Methods of Optional:

Here’s a set of examples using an Employee object to demonstrate the usage of the Optional class methods in Java:

Example : Employee Class

public class Employee {
    private String name;
    private Integer id;

    public Employee(String name, Integer id) {
        this.name = name;
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public Integer getId() {
        return id;
    }

    @Override
    public String toString() {
        return "Employee{" +
                "name='" + name + '\'' +
                ", id=" + id +
                '}';
    }
}

1. Optional.empty()

Represents an empty Optional instance, meaning no value is present.

Optional<Employee> emptyOptional = Optional.empty();
System.out.println(emptyOptional);  // Output: Optional.empty

2. Optional.of()

Creates an Optional with a non-null value. Throws NullPointerException if the argument is null.

Employee emp = new Employee("John", 1001);
Optional<Employee> empOptional = Optional.of(emp);
System.out.println(empOptional);  // Output: Optional[Employee{name='John', id=1001}]

3. Optional.ofNullable()

Creates an Optional that may or may not hold a non-null value. Doesn't throw an exception if the argument is null.

Employee emp = null;
Optional<Employee> empOptional = Optional.ofNullable(emp);
System.out.println(empOptional);  // Output: Optional.empty

emp = new Employee("John", 1001);
empOptional = Optional.ofNullable(emp);
System.out.println(empOptional);  // Output: Optional[Employee{name='John', id=1001}]

4. isPresent()

Returns true if there is a value present, otherwise false.

Optional<Employee> empOptional = Optional.ofNullable(new Employee("John", 1001));
System.out.println(empOptional.isPresent());  // Output: true

empOptional = Optional.empty();
System.out.println(empOptional.isPresent());  // Output: false

5. ifPresent()

Performs the given action if a value is present.

Optional<Employee> empOptional = Optional.ofNullable(new Employee("John", 1001));
empOptional.ifPresent(e -> System.out.println("Employee is present: " + e));
// Output: Employee is present: Employee{name='John', id=1001}

empOptional = Optional.empty();
empOptional.ifPresent(e -> System.out.println("Employee is present: " + e));
// Output: (Nothing is printed because the Optional is empty)

6. get()

Returns the value if present; otherwise, throws NoSuchElementException.

Optional<Employee> empOptional = Optional.ofNullable(new Employee("John", 1001));
Employee emp = empOptional.get();
System.out.println(emp);  // Output: Employee{name='John', id=1001}

empOptional = Optional.empty();
try {
    empOptional.get();  // Throws NoSuchElementException
} catch (NoSuchElementException e) {
    System.out.println("No value present!");  // Output: No value present!
}

7. orElse()

Returns the value if present; otherwise, returns the specified default value.

Optional<Employee> empOptional = Optional.ofNullable(null);
Employee emp = empOptional.orElse(new Employee("Default", 9999));
System.out.println(emp);  

// Output: Employee{name='Default', id=9999}

8. orElseGet()

Returns the value if present; otherwise, returns the result produced by the given supplier.

Optional<Employee> empOptional = Optional.ofNullable(null);
Employee emp = empOptional.orElseGet(() -> new Employee("Default", 9999));
System.out.println(emp);  

// Output: Employee{name='Default', id=9999}

9. orElseThrow()

Returns the value if present; otherwise, throws an exception created by the provided supplier.

Optional<Employee> empOptional = Optional.ofNullable(null);
try {
    Employee emp = empOptional.orElseThrow(() -> new IllegalArgumentException("No Employee found"));
} catch (IllegalArgumentException e) {
    System.out.println(e.getMessage());  // Output: No Employee found
}

Date/Time API in Java 8

Java 8 introduced a powerful Date/Time API within java.time package to handle date and time-related operations.

LocalDate class

The LocalDate class represents a date without a timezone in the format yyyy-mm-dd.

An instance of the current date can be created using the LocalDate.now() method.

LocalDate today = LocalDate.now();   // 2023-08-28

By using of or parse method we can create an instance of a LocalDate for a specific date.

LocalDate localDate = LocalDate.of(2023, 08, 31);  // 2023-08-31

LocalDate localDate = LocalDate.parse("2023-09-20"); // 2023-09-20

We can add/subtract days, months, and years from an instance of LocalDate.

LocalDate yesterday = LocalDate.now().plusDays(1);     // 2023-08-27
LocalDate tomorrow  = LocalDate.now().minusDays(1);    // 2023-08-29

LocalDate lastMonth = LocalDate.now().minusMonths(1);  //2023-07-28
LocalDate nextMonth = LocalDate.now().plusMonths(1);   //2023-09-28

LocalDate lastYear  = LocalDate.now().minusYears(1);  //2022-08-28
LocalDate nextYear  = LocalDate.now().plusYears(1);   //2024-08-28

We can derive the day of the week, day, month, and year from an instance of LocalDate.

DayOfWeek day = LocalDate.now().getDayOfWeek();   //MONDAY


int day     = LocalDate.parse("2023-01-12").getDayOfMonth();   // 12
Month month = LocalDate.parse("2023-01-12").getMonth();        // JANUARY
int year    = LocalDate.parse("2023-01-12").getYear();         // 2023

We can check if a particular year is a leap year by doing the following.

boolean isLeap  = LocalDate.now().isLeapYear();                 //false
boolean isLeap1 = LocalDate.parse("2024-01-12").isLeapYear();   //true

Comparison of two dates can be done as follows.

LocalDate date1 = LocalDate.parse("2022-08-12");
LocalDate date2 = LocalDate.parse("2023-01-09");

// check if date1 is before date2
boolean isBefore = date1.isBefore(date2);   //true

// check if date1 is after date2
boolean isAfter = date1.isAfter(date2);     //false

LocalTime class

The LocalTime class represents time without a date.

An instance of the current time can be created using the LocalTime.now() method.

LocalTime now = LocalTime.now();       // 14:07:12.503202341 

By using of or parse method we can create an instance of a LocalTime for a specific time.

LocalTime eightFifteen = LocalTime.parse("08:15");  // 08:15
LocalTime eightFifty   = LocalTime.of(8, 50);       // 08:50

Similar to LocalDate, we can add/subtract minutes, seconds, and hours from an instance of a LocalTime.

LocalTime timeInOneHour   = LocalTime.now().plus(1, ChronoUnit.HOURS);  //15:16:05.836826479 
LocalTime timeInOneMinute = LocalTime.now().plusMinutes(1);             //14:17:05.836826479

We can obtain seconds, minutes, and hours from a specific instance of a LocalTime.

//Local Time : 14:21:22.383214776

int hour = LocalTime.now().getHour();        //14
int minutes = LocalTime.now().getMinute();   //21
int seconds = LocalTime.now().getSecond();`  //22

We can compare two times as follows.

LocalTime time1 = LocalTime.parse("02:23");
LocalTime time2 = LocalTime.parse("23:13");

// check if time1 is before time2
boolean isBefore = time1.isBefore(time2);   // true

// check if time1 is after time2
boolean isAfter = time1.isAfter(time2);    // false

LocalDateTime class

LocalDateTime class is used to represent date and time without a time zone.

Similar operations as LocalDate and LocalTime can be performed for this class too.

//Get the current date and time

LocalDateTime now = LocalDateTime.now(); //2023-08-28T14:31:21.433439004
//Create an instance using of and parse
LocalDateTime parseDateTime = LocalDateTime.parse("2023-09-29T15:20");  //2023-09-29T15:20
LocalDateTime givenDateTime = LocalDateTime.of(2023, Month.SEPTEMBER, 4, 02, 02); //2023-09-04T02:0

We can add specific time and date units like hours, minutes, seconds, months, years, and days using plus and minus methods.

LocalDateTime yesterday         = LocalDateTime.now().plusDays(1);
LocalDateTime timebeforeOneHour = LocalDateTime.now().minusHours(1);

We can get the specific day, month, year, second, minute, hour from an instance of a LocalDateTime class.

 //LocalDateTime : 2023-08-28T14:40:11.302826996

int hour     = LocalDateTime.now().getHour();          // 14
int minutes  = LocalDateTime.now().getMinute();        // 40
int seconds  = LocalDateTime.now().getSecond();        // 11
            
int day      = LocalDateTime.now().getDayOfMonth();    // 28
Month month  = LocalDateTime.now().getMonth();         // AUGUST
int year     = LocalDateTime.now().getYear();          // 2023

We can also compare two LocalDateTime instances using isAfter and isBefore methods.

ZonedDateTime class

ZonedDateTime is provided by Java8 to handle time-zone-specific dates and times.

We can get the available ZoneIds as follows.

Set<String> availableZoneIds = ZoneId.getAvailableZoneIds();

The zone Ids would be as Asia/Aden, America/Cuiaba, etc.

We can convert the LocalDateTime to a specific zone.

LocalDateTime now = LocalDateTime.now(); //2023-08-28T15:00:30.487070279

ZonedDateTime zonedDT = ZonedDateTime.of(now, ZoneId.of("Africa/Nairobi"));  //2023-08-28T15:00:30.487070279+03:00[Africa/Nairobi]

The ZonedDateTime also provides the parse method to get time-zone-specific date-time.

ZonedDateTime zonedDT = ZonedDateTime.parse("2019-12-20T10:15:30+05:00[Asia/Karachi]");

Similar to the previously mentioned classes, we can execute plus/minus operations, get different time and date units, and compare two instances using the ZonedDateTime class as well.

Period Class

The Period class is used to modify the values of a given date and to get the difference between to given class.

The following code illustrates how you can manipulate an instance of LocalDate:

// Current Local Date : 2023-08-28

// Add a period of 6 days
LocalDate futureDate = LocalDate.now().plus(Period.ofDays(6));  // 2023-09-03

// Subtract a period of 2 weeeks
LocalDate pastDate = LocalDate.now().minus(Period.ofWeeks(2));  //2023-08-14

// Add period of 2 months and 15 days
LocalDate specificDate = LocalDate.now().plus(Period.ofMonths(2).plusDays(15)); //2023-11-12

We can get the difference between two dates in different date units using Period class.

LocalDate date1 = LocalDate.of(2022, Month.AUGUST, 12);   // 2022-08-12
LocalDate date2 = LocalDate.of(2023, Month.DECEMBER, 02); // 2023-12-02

// Difference between two dates
Period period = Period.between(date1, date2);    //P1Y3M20D


int days = period.getDays();       // 20
int months = period.getMonths();   // 3
int years = period.getYears();     // 1
boolean isNegative = period.isNegative(); // false
boolean isZero = period.isZero();         // false

P1Y3M20D represents a Period of 1 Year, 3 Months, and 20 Days.

We can use the ChronoUnit class to obtain the difference between two dates in a specific unit such as day, month, or year.

long days = ChronoUnit.DAYS.between(date1, date2);     // 477
long months = ChronoUnit.MONTHS.between(date1, date2); // 15
long years = ChronoUnit.YEARS.between(date1, date2);   // 1

Duration Class

Similar to Period Class, Duration Class is used to modify the values of a given time or to find the difference between two times.

// Current Local Time: 15:39:40.053910730

// Add a duration of 8 hours
LocalTime futureTime = LocalTime.now().plus(Duration.ofHours(8));  //23:39:40.063033292

// Subtract 1 day
LocalTime pastTime = LocalTime.now().minus(Duration.ofDays(1));   //15:39:40.063269984

// Add 4 Days and 3 hours
LocalTime specificTime = LocalTime.now().plus(Duration.ofDays(4).plusHours(3)); //15:39:40.063269984

We can get the duration between two given times as follows.

LocalTime time1   = LocalTime.of(10, 30); // 10:30
LocalTime time2   = LocalTime.of(12, 28); // 12:28

// get the difference between times
Duration duration = Duration.between(time1, time2);  //PT1H58M

long seconds = duration.getSeconds(); // 7080
long minutes = duration.toMinutes();  // 118
long hours = duration.toHours();      // 1

We can use the ChronoUnit class to obtain the difference between two times in a specific unit such as seconds, minutes, or hours.

long seconds = ChronoUnit.SECONDS.between(time1, time2); // 7080
long minutes = ChronoUnit.MINUTES.between(time1, time2); // 118
long hours   = ChronoUnit.HOURS.between(time1, time2); // 1

Temporal Adjusters

Temporal adjusters are useful when carrying out diverse calculations involving dates and times. We can use them to manipulate temporal objects effectively and address queries such as discovering the first day of the Month or finding the next Sunday of the month etc.

LocalDate givenDate= LocalDate.parse("2023-08-28")

givenDate.with(TemporalAdjusters.lastDayOfMonth()) //2023-08-31
givenDate.with(TemporalAdjusters.firstDayOfMonth()) //2023-08-01
givenDate.with(TemporalAdjusters.firstDayOfNextMonth()) //2023-09-01

Some of the other Temporal Adjusters are :

  • next(DayOfWeek.TUESDAY)
  • dayOfWeekInMonth
  • firstDayOfNextYear
  • firstDayOfYear
  • lastDayOfNextMonth
  • lastDayOfNextYear
  • lastDayOfYear

forEach() Method in Java 8

The forEach() method is part of the Iterable interface and is commonly used with collections (such as List, Set, etc.) and streams in Java. It takes a Consumer as an argument and applies that consumer's action to each element in the collection.

Example : forEach

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);

// Using forEach to double each number and print the result
numbers.forEach(num -> System.out.println(num * 2));

Example : normal for loop:

import java.util.Arrays;
import java.util.List;

public class Main {
    public static void main(String[] args) {
        List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);

        // Normal for loop to double each number and print the result
        for (int i = 0; i < numbers.size(); i++) {
            int num = numbers.get(i);
            System.out.println(num * 2);
        }
    }
}
  1. Iterating a Map
  2. Iterating a List
  3. Iterating a Set
  4. Iterating a Stream
  5. Using forEach with Consumer
  6. Exception Handling with forEach
  7. forEach vs forEachOrdered

Iterating a Map

Map<String, Integer> map = new HashMap<>();
map.put("apple", 1);
map.put("banana", 2);
map.put("orange", 3);

// Java 8 forEach
map.forEach((key, value) -> System.out.println(key + " -> " + value));
// Java 7 loop
for (Map.Entry<String, Integer> entry : map.entrySet()) {
    System.out.println(entry.getKey() + " -> " + entry.getValue());
}

Iterating a List

List<String> list = Arrays.asList("apple", "banana", "orange");

// Java 8 forEach
list.forEach(item -> System.out.println(item));
// Java 7 loop
for (String item : list) {
    System.out.println(item);
}

Iterating a Set

Set<Integer> set = new HashSet<>(Arrays.asList(1, 2, 3));
// Java 8 forEach
set.forEach(item -> System.out.println(item));
// Java 7 loop
for (Integer item : set) {
    System.out.println(item);
}

Iterating a Stream

List<String> list = Arrays.asList("apple", "banana", "orange");

// Java 8 forEach on Stream
list.stream().forEach(item -> System.out.println(item));
// Java 7 loop on Stream
for (String item : list) {
    System.out.println(item);
}

Using forEach with Null Check

list.forEach(item -> {
    if (item != null) {
        // Perform some action
    }
});

Exception Handling with forEach

list.forEach(item -> {
    try {
        // Perform some operation that may throw an exception
    } catch (Exception e) {
        // Handle the exception
    }
});

forEach vs forEachOrdered

list.stream().forEach(item -> System.out.println(item)); // May not maintain order
list.stream().forEachOrdered(item -> System.out.println(item)); // Maintains order

StringJoiner

The stringjoiner class was introduced in java8. string joiner is a final class and is present in Java. util. package

The StringJoiner class was introduced to provide a convenient way to join strings with a delimiter. It is particularly useful when you need to concatenate multiple strings with a separator between them.

import java.util.StringJoiner;

public class StringJoinerExample {
    public static void main(String[] args) {

        StringJoiner stringJoiner = new StringJoiner(", ");
        stringJoiner.add("Bade Miyan Chote Miyan");
        stringJoiner.add("Maidaan");
        stringJoiner.add("Dukaan");
        stringJoiner.add("The Lost Girl");

        String hindiMovies = stringJoiner.toString();
        System.out.println(hindiMovies);

    }
}

// output
Bade Miyan Chote Miyan, Maidaan, Dukaan, The Lost Girl

You can also specify a prefix and suffix for the joined string by providing them as arguments to the StringJoiner constructor.

import java.util.StringJoiner;

public class StringJoinerExample {
    public static void main(String[] args) {

        StringJoiner playingXI = new StringJoiner(", ", "[", "]");
        playingXI.add("Quinton de Kock");
        playingXI.add("KL Rahul");
        playingXI.add("Devdutt Padikkal");
        playingXI.add("Marcus Stoinis");
        playingXI.add("Nicholas Pooran");
        playingXI.add("Ayush Badoni");
        playingXI.add("Krunal Pandya");
        playingXI.add("Ravi Bishnoi");

        String playerNames = playingXI.toString();
        System.out.println(playerNames);
    }
}

// Output
[Quinton de Kock, KL Rahul, Devdutt Padikkal, Marcus Stoinis, Nicholas Pooran, Ayush Badoni, Krunal Pandya, Ravi Bishnoi]

Spliterator

The Spliterator interface in Java 8 provides a way to traverse and partition elements in a collection.

It’s part of the Java collections framework and is designed to work with streams for efficient parallel execution.

Basic Functionality

  • Traversal: Spliterator allows traversal of elements in a collection.
  • Partitioning: It can partition a portion of elements for parallel processing, which is especially useful for parallel streams.
  • Characteristics: Spliterator defines characteristics like SIZED, SORTED, CONCURRENT, and others, which describe the properties of the underlying data source and how it can be processed.
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import java.util.Spliterator;

public class SpliteratorExample {

    public static void main(String[] args) {

        List<String> listNames = List.of("kuna", "runa", "kuna", "kanha");

//        Iterator<String> iterator = listNames.iterator();
//        while (iterator.hasNext()) {
//            System.out.println(iterator.next());
//        }
        Spliterator<String> spliterator = listNames.stream().sorted().spliterator();
        spliterator.forEachRemaining(System.out::println);
    }

}

Let’s see an example of using Spliterator with a List to process elements in parallel:

import java.util.ArrayList;
import java.util.List;
import java.util.Spliterator;

public class SpliteratorExample {
    public static void main(String[] args) {
        List<String> fruits = new ArrayList<>();
        fruits.add("Apple");
        fruits.add("Banana");
        fruits.add("Orange");
        fruits.add("Mango");

        // Get a Spliterator for the list
        Spliterator<String> spliterator = fruits.spliterator();

        // Split the Spliterator for parallel processing
        Spliterator<String> split1 = spliterator.trySplit();
        Spliterator<String> split2 = spliterator.trySplit();

        // Process elements in parallel
        System.out.println("Split 1:");
        split1.forEachRemaining(System.out::println);

        System.out.println("Split 2:");
        split2.forEachRemaining(System.out::println);

        // Process remaining elements
        System.out.println("Remaining:");
        spliterator.forEachRemaining(System.out::println);
    }
}
Split 1:
Apple
Banana
Split 2:
Orange
Mango
Remaining:

In this above example

  • We create a List of fruits and obtain a Spliterator using the spliterator() method.
  • We then use trySplit() to split the Spliterator into multiple parts for parallel processing.
  • Each split is processed using forEachRemaining(), and then the remaining elements are processed.

What is Nashorn?

Nashorn is a JavaScript engine introduced in Java 8, allowing developers to execute JavaScript code within the Java Virtual Machine (JVM).

Nashorn is a JavaScript engine for the JVM that is released with Java 8. Nashorn comprises of an embedded JavaScript interpreter and a command line tool. The objective of Nashorn is to implement a high-performance JavaScript runtime in Java with a native JVM. Using Nashorn, a developer can embed JavaScript in a Java application and can also invoke Java methods and classes from the JavaScript code providing a seamless integration between the two languages.

Executing JavaScript from Java

import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
import javax.script.ScriptException;

public class NashornExample {
    public static void main(String[] args) {
        // Create a ScriptEngineManager
        ScriptEngineManager scriptEngineManager = new ScriptEngineManager();
        
        // Get Nashorn ScriptEngine
        ScriptEngine nashorn = scriptEngineManager.getEngineByName("nashorn");
        
        // JavaScript code as a String
        String script = "var greeting = 'Hello, Nashorn'; greeting";
        
        try {
            // Evaluate the JavaScript code
            Object result = nashorn.eval(script);
            
            // Print the result
            System.out.println(result); // Output: Hello, Nashorn
        } catch (ScriptException e) {
            e.printStackTrace();
        }
    }
}

Stream API with Examples

What is the Stream API? the Stream API is used to process collections of objects. A stream is a sequence of objects that supports various methods which can be pipelined to produce the desired result.

Types of Streams:

  1. Sequential Streams: These process elements in a single thread, ensuring predictable order and simplicity.
  2. Parallel Streams: Parallel streams, as the name suggests, process elements in parallel using multiple threads. They are designed to take advantage of modern multi-core processors and are especially useful for computationally intensive tasks.

Stream Operations:

Stream operations are divided into two categories: intermediate and terminal operations.

Intermediate Operations:

  1. filter(Predicate<T> predicate) - Filters elements based on a given predicate.
  2. map(Function<T, R> mapper) - Transforms each element using a given function.
  3. flatMap(Function<T, Stream<R>> mapper) - Transforms each element and flattens the result into a single stream.
  4. distinct() - Removes duplicate elements.
  5. sorted() - Sorts elements using their natural order or a custom comparator.
  6. peek(Consumer<T> action) - Performs an action on each element without changing the stream.
  7. limit β€” The limit operation sets a boundary on how many elements you want from the stream
  8. skip(int n)discards the first three elements, as indicated n

Terminal Operations:

  1. forEach(Consumer<T> action) - Applies an action to each element.
  2. collect(Collector<T, A, R> collector) - Collects the stream into a collection.
  3. reduce(T identity, BinaryOperator<T> accumulator) - Combines elements in a stream.
  4. count() - Counts the number of elements in the stream.
  5. min(Comparator<T> comparator) - Finds the minimum element.
  6. max(Comparator<T> comparator) - Finds the maximum element.
  7. anyMatch(Predicate<T> predicate) - Checks if any element matches a predicate.
  8. allMatch(Predicate<T> predicate) - Checks if all elements match a predicate.
  9. noneMatch(Predicate<T> predicate) - Checks if no element matches a predicate.
  10. findFirst() - Finds the first element in the stream.
  11. findAny() - Finds any element in the stream.

Here as you see some of the operations takes the Consumer, Predicate, Function as parameters. They are functional interfaces. If have not got chance read them, I recommend read this blog here for understanding them.

Stream API Interview Questions GITHUB Link https://github.com/vinosubi/Streams-Example-part1

Example :

Employee Class

class Employee {
    private int id;
    private String name;
    private int age;
    private double salary;
    private String department;

    // Constructor, getters, and setters
    public Employee(int id, String name, int age, double salary, String department) {
        this.id = id;
        this.name = name;
        this.age = age;
        this.salary = salary;
        this.department = department;
    }

    public int getId() { return id; }
    public String getName() { return name; }
    public int getAge() { return age; }
    public double getSalary() { return salary; }
    public String getDepartment() { return department; }

    @Override
    public String toString() {
        return "Employee{" +
                "id=" + id +
                ", name='" + name + '\'' +
                ", age=" + age +
                ", salary=" + salary +
                ", department='" + department + '\'' +
                '}';
    }
}

Employee List Example

List<Employee> employees = Arrays.asList(
    new Employee(1, "John", 28, 3000.00, "HR"),
    new Employee(2, "Jane", 32, 4000.00, "Finance"),
    new Employee(3, "Jack", 24, 2500.00, "IT"),
    new Employee(4, "Jill", 29, 3500.00, "HR"),
    new Employee(5, "Jerry", 40, 5000.00, "Finance")
);
  1. Filter() Filters the elements of the stream based on a given condition. The resulting stream contains only the elements that match the condition.
List<Employee> hrEmployees = employees.stream()
    .filter(e -> e.getDepartment().equals("HR"))
    .collect(Collectors.toList());

System.out.println(hrEmployees);

// output
[Employee{id=1, name='John', age=28, salary=3000.0, department='HR'}, 
 Employee{id=4, name='Jill', age=29, salary=3500.0, department='HR'}]

2. Map()

Transforms each element in the stream by applying a function to it. The resulting stream contains the transformed elements.

List<String> employeeNames = employees.stream()
    .map(Employee::getName)
    .collect(Collectors.toList());

System.out.println(employeeNames);

//output
[John, Jane, Jack, Jill, Jerry]

3. FlatMap

Flattens a stream of collections into a single stream. Each collection is expanded into a stream of its elements.

List<List<String>> departments = Arrays.asList(
    Arrays.asList("HR", "Finance"),
    Arrays.asList("IT", "Marketing")
);

List<String> departmentList = departments.stream()
    .flatMap(Collection::stream)
    .collect(Collectors.toList());

System.out.println(departmentList);

// output
[HR, Finance, IT, Marketing]

4. Distinct

Removes duplicate elements from the stream. The resulting stream contains only unique elements.

List<String> distinctDepartments = employees.stream()
    .map(Employee::getDepartment)
    .distinct()
    .collect(Collectors.toList());

System.out.println(distinctDepartments);

// output
[HR, Finance, IT]

5. Sorted

Sorts the elements in the stream based on a given comparator. The resulting stream is ordered according to the comparator.

List<Employee> sortedByName = employees.stream()
    .sorted(Comparator.comparing(Employee::getName))
    .collect(Collectors.toList());

System.out.println(sortedByName);

//output
[Employee{id=3, name='Jack', age=24, salary=2500.0, department='IT'}, 
 Employee{id=2, name='Jane', age=32, salary=4000.0, department='Finance'}, 
 Employee{id=4, name='Jill', age=29, salary=3500.0, department='HR'}, 
 Employee{id=1, name='John', age=28, salary=3000.0, department='HR'}, 
 Employee{id=5, name='Jerry', age=40, salary=5000.0, department='Finance'}]

6. Peek

Performs an action for each element of the stream, mainly for debugging purposes. It returns the same stream, allowing further operations.

employees.stream()
    .peek(e -> System.out.println("Processing: " + e.getName()))
    .collect(Collectors.toList());

//output
Processing: John
Processing: Jane
Processing: Jack
Processing: Jill
Processing: Jerry

7. Limit

Limits the number of elements in the stream to a given size. The resulting stream contains only the first N elements.

List<Employee> limitedEmployees = employees.stream()
    .limit(2)
    .collect(Collectors.toList());

System.out.println(limitedEmployees);

//output
[Employee{id=1, name='John', age=28, salary=3000.0, department='HR'}, 
 Employee{id=2, name='Jane', age=32, salary=4000.0, department='Finance'}]

8. Skip

Skips the first N elements in the stream. The resulting stream contains the remaining elements after skipping the first N.

List<Employee> skippedEmployees = employees.stream()
    .skip(2)
    .collect(Collectors.toList());

System.out.println(skippedEmployees);

//output
[Employee{id=3, name='Jack', age=24, salary=2500.0, department='IT'}, 
 Employee{id=4, name='Jill', age=29, salary=3500.0, department='HR'}, 
 Employee{id=5, name='Jerry', age=40, salary=5000.0, department='Finance'}]

9. ForEach

Performs an action for each element of the stream. It does not return a value, as it is a terminal operation.

employees.stream()
    .forEach(e -> System.out.println(e.getName()));

//output
John
Jane
Jack
Jill
Jerry

10. Reduce

Combines the elements of the stream into a single result by repeatedly applying a binary operator.

double totalSalary = employees.stream()
    .map(Employee::getSalary)
    .reduce(0.0, Double::sum);

System.out.println(totalSalary);

// output
18000.0

11. Count Counts the number of elements in the stream. The result is a long value representing the count.

long count = employees.stream().count();
System.out.println(count);

//output
5

12. Min Finds the minimum element in the stream according to a given comparator. It returns an Optional that contains the minimum element if it exists.

Employee youngestEmployee = employees.stream()
    .min(Comparator.comparing(Employee::getAge))
    .orElse(null);

System.out.println(youngestEmployee);

// output
Employee{id=3, name='Jack', age=24, salary=2500.0, department='IT'}

13. Max

Finds the maximum element in the stream according to a given comparator. It returns an Optional that contains the maximum element if it exists.

Employee highestPaidEmployee = employees.stream()
    .max(Comparator.comparing(Employee::getSalary))
    .orElse(null);

System.out.println(highestPaidEmployee);

//output
Employee{id=5, name='Jerry', age=40, salary=5000.0, department='Finance'}

14. AnyMatch

Checks if any element in the stream matches the given condition. It returns a boolean value.

boolean anyOver30 = employees.stream()
    .anyMatch(e -> e.getAge() > 30);

System.out.println(anyOver30);

// output
true

15. AllMatch

Checks if all elements in the stream match the given condition. It returns a boolean value.

boolean allOver20 = employees.stream()
    .allMatch(e -> e.getAge() > 20);

System.out.println(allOver20);

//output
true

16. NoneMatch

Checks if no elements in the stream match the given condition. It returns a boolean value.

boolean noneOver50 = employees.stream()
    .noneMatch(e -> e.getAge() > 50);

System.out.println(noneOver50);

// output
true

17. FindFirst

Finds the first element in the stream. It returns an Optional that contains the first element if it exists.

Employee firstEmployee = employees.stream()
    .findFirst()
    .orElse(null);

System.out.println(firstEmployee);

// output
Employee{id=1, name='John', age=28, salary=3000.0, department='HR'}

18. FindAny

Finds any element in the stream. It returns an Optional that contains any element if it exists.

Employee anyEmployee = employees.stream()
    .findAny()
    .orElse(null);

System.out.println(anyEmployee);

// output
Employee{id=1, name='John', age=28, salary=3000.0, department='HR'}
Java8
Java Interview Questions
Java8 Stream Api
Recommended from ReadMedium