GFG Day 17: Introduction to Java 8+ Features

sri parthusri parthu
12 min read

Java 8 Feature Overview

  • Java 8 (released on 8 March 2014) introduced major updates to the Java programming language and platform. It brought functional programming, stream processing, lambda expressions, and more.

  • Before Java 8, Java was mostly object-oriented with very limited support for functional-style coding. This version changed that by introducing a whole new programming paradigm, along with improvements in APIs and performance.

Key Features Introduced in Java 8

1. Lambda Expressions

  • Allows writing anonymous methods/functions.

  • Syntax: (parameters) -> { body }

  • Makes code concise and readable.

(a, b) -> a + b

2. Functional Interfaces

  • An interface with only one abstract method.

  • Annotated with @FunctionalInterface.

  • Introduced several built-in functional interfaces like Predicate, Function, Consumer, and Supplier. You can also create your own.

@FunctionalInterface
interface MyFunction {
    void apply();
}

3. Stream API

  • Enables functional-style operations on collections (map, filter, reduce).

  • Supports lazy evaluation and parallel processing.

list.stream().filter(x -> x > 10).forEach(System.out::println);

4. New Date and Time API

  • Replaces old java.util.Date and Calendar.

  • Immutable, thread-safe, and more readable.

LocalDate today = LocalDate.now();
LocalTime time = LocalTime.now();

5. Optional Class

  • Used to avoid null pointer exceptions.

  • Wraps values that may or may not be present.

Optional<String> name = Optional.ofNullable(null);
System.out.println(name.orElse("Default"));

6. Default Methods in Interfaces

  • Interfaces can now have method implementations using default keyword.
interface A {
    default void show() {
        System.out.println("Hello from default method");
    }
}

7. Static Methods in Interfaces

  • Interfaces can now have static methods.
interface MathUtils {
    static int add(int a, int b) {
        return a + b;
    }
}

8. Method References

  • Shorthand for calling a method using ::.
list.forEach(System.out::println);

9. forEach() Method

  • Introduced in the Iterable interface, this method lets you loop through collections more easily using lambda expressions or method references.
list.forEach(item -> System.out.println(item));

10. Collection API Enhancements

Java 8 added several convenient methods to existing collection classes, like removeIf, replaceAll, and more, to simplify operations on collections.

11. Concurrency Enhancements

Java 8 improved concurrency with the introduction of the CompletableFuture class and other enhancements, making asynchronous programming easier and more manageable.


Java Lambda Expressions

  • A lambda expression introduced in Java 8. It is a short block of code that takes in parameters and returns a value. It is similar to a function, but it does not need a name and can be implemented directly where it is needed.

Key Functionalities of Lambda Expression

  • Functional interfaces are implemented by Lambda Expressions since they are the only abstract function that they implement. The following features are offered by Java 8's addition of lambda expressions.

    • Functional Interfaces: A functional interface is an interface that contains only one abstract method.

    • Code as Data: Treat functionality as a method argument.

    • Class Independence: Create functions without defining a class.

    • Pass and Execute: Pass lambda expressions as objects and execute on demand.

Example: Implementing a Functional Interface with Lambda

The below Java program demonstrates how a lambda expression can be used to implement a user-defined functional interface.

interface FuncInterface
{
    // An abstract function
    void abstractFun(int x);

    // A non-abstract (or default) function
    default void normalFun()
    {
       System.out.println("Hello");
    }
}

class Test
{
    public static void main(String args[])
    {
        // lambda expression to implement above functional interface. 
        FuncInterface fobj = (int x)->System.out.println(2*x);

        // This calls above lambda expression and prints 10.
        fobj.abstractFun(5);
    }
}

Output

10

Structure of Lambda Expression

  • The diagram below demonstrates the structure of a lambda expression:

lambda expression

Syntax of Lambda Expressions

(argument list) -> { body of the expression }

Components:

  • Argument List: Parameters for the lambda expression

  • Arrow Token (->): Separates the parameter list and the body

  • Body: Logic to be executed.

Types of Lambda Parameters

  • There are three Lambda Expression Parameters are mentioned below:

    1. Lambda with Zero Parameter

    2. Lambda with Single Parameter

    3. Lambda with Multiple Parameters

1. Lambda with Zero Parameter

Syntax:

() -> System.out.println("Zero parameter lambda");

Example: The below Java program demonstrates a lambda expression with zero parameters.

@FunctionalInterface
interface ZeroParameter {
    void display();
}

public class Main {
    public static void main(String[] args)
    {
        // Lambda expression with zero parameters
        ZeroParameter zeroParamLambda = ()
            -> System.out.println(
                "This is a zero-parameter lambda expression!");

        // Invoke the method
        zeroParamLambda.display();
    }
}

Output

This is a zero-parameter lambda expression!

2. Lambda with a Single Parameter

Syntax:

(p) -> System.out.println("One parameter: " + p);
  • It is not mandatory to use parentheses if the type of that variable can be inferred from the context.

Example: The Java program that follows shows how to utilize lambda expressions with an ArrayList in two different situations.

  • We are iterating through an ArrayList and printing each element using a lambda function.

  • To choose output an even amount of entries from an ArrayList, we are use a lambda expression with a condition.

import java.util.ArrayList;

class Test {

    public static void main(String args[])
    {
        // Creating an ArrayList with elements
        // {1, 2, 3, 4}
        ArrayList<Integer> al = new ArrayList<Integer>();
        al.add(1);
        al.add(2);
        al.add(3);
        al.add(4);

        // Using lambda expression to print all elements of al
        System.out.println("Elements of the ArrayList: ");
        al.forEach(n -> System.out.println(n));

        // Using lambda expression to print even elements of al
        System.out.println(
            "Even elements of the ArrayList: ");
        al.forEach(n -> {
            if (n % 2 == 0)
                System.out.println(n);
        });
    }
}

Output

Elements of the ArrayList: 
1
2
3
4
Even elements of the ArrayList: 
2
4

Note: The lambda expression in the foreach() method is used in the example above, and it functions internally with the consumer functional interface. A single parameter is taken by the Consumer interface, which then acts upon it.

3. Lambda Expression with Multiple Parameters

Syntax:

(p1, p2) -> System.out.println("Multiple parameters: " + p1 + ", " + p2);

Example: The Java program below shows how to construct a functional interface using lambda expressions to carry out simple arithmetic operations.

@FunctionalInterface
interface Functional {
    int operation(int a, int b);
}

public class Test {

    public static void main(String[] args) {

        // Using lambda expressions to define the operations
        Functional add = (a, b) -> a + b;
        Functional multiply = (a, b) -> a * b;

        // Using the operations
        System.out.println(add.operation(6, 3));  
        System.out.println(multiply.operation(4, 5));  
    }
}

Output

9
10

Note: Functions and lambda expressions are similar in that they both take parameters.

Common Built-in Functional Interfaces

  • Comparable<T>: int compareTo(T o);

  • Comparator<T>: int compare(T o1, T o2);

These are commonly used in sorting and comparisons.

Note: Other commonly used interface include Predicate<T>, it is used to test conditions, Function<T, R>, it represent a function that take the argument of type T and return a result of type R and Supplier<T>, it represent a function that supplies results.

  1. () -> {};

  2. () -> "CoreJava";

  3. () -> { return "CoreJava";};

  4. (Integer i) -> {return "CoreJava" + i;}

  5. (String s) -> {return "CoreJava";};

4 and 5 are invalid lambdas, the rest are valid. Details:

  1. This lambda has no parameters and returns void. It’s similar to a method with an empty body: public void run() { }.

  2. This lambda has no parameters and returns a String as an expression.

  3. This lambda has no parameters and returns a String (using an explicit return statement, within a block).

  4. return is a control-flow statement. To make this lambda valid, curly braces are required as follows: (Integer i) -> { return "CoreJava" + i; }.

  5. β€œCoreJava” is an expression, not a statement. To make this lambda valid, you can remove the curly braces and semicolon as follows: (String s) -> "CoreJava. Or if you prefer, you can use an explicit return statement as follows: (String s) -> { return "CoreJava"; }.


Java Functional Interfaces

  • In Java, a functional interface is one that has just one abstract method. Functional interfaces can have only one abstract method but several static or default methods.

  • From Java 8 onwards, lambda expressions and method references can be used to represent the instance of a functional interface.

Example: Using a Functional Interface with Lambda Expression

public class Main {

    public static void main(String[] args) {

        // Using lambda expression to implement Runnable
        new Thread(() -> System.out.println("New thread created")).start();
    }
}

output

New thread created

Explanation:

  • The above program demonstrates the use of a lambda expression with the Runnable functional interface.

  • Runnable has one abstract method, run(), so it qualifies as a functional interface.

  • Lambda ()-> System.out.println("New thread created") defines the run() method.

  • new Thread(). start() starts a new thread that executes the lambda body

Note: A f*unctional interface can also extend another functional interface.*

@FunctionalInterface Annotation

  • The @FunctionalInterface annotation makes sure that there can't be more than one abstract method on the functional interface. The compiler indicates the presence of many abstract methods with a "Unexpected @FunctionalInterface annotation" warning. But using this annotation is not required.

Note: Although @FunctionalInterface annotation is not required, using it is a good idea. By ensuring that there is just one abstract method in the interface, it helps identify errors early on.

Example: Defining a Functional Interface with @FunctionalInterface Annotation

@FunctionalInterface

interface Square {
    int calculate(int x);
}

class Main {
    public static void main(String args[]) {
        int a = 5;

        // lambda expression to define the calculate method
        Square s = (int x) -> x * x;

        // parameter passed and return type must be same as defined in the prototype
        int ans = s.calculate(a);
        System.out.println(ans);
    }
}

Output

25

Explanation:

  • Square is a functional interface with a single method calculate(int x).

  • A lambda expression (int x) -> x * x is used to implement the calculate method.

  • Lambda takes x as input and returns x * x.

Java Functional Interfaces Before Java 8

  • Before Java 8, we had to implement these interfaces or make anonymous inner class objects. An illustration of the Runnable interface's implementation before lambda expressions were introduced can be found below.

Example: Java program to demonstrate functional interface

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

        // create anonymous inner class object
        new Thread(new Runnable() {
            @Override public void run()
            {
                System.out.println("New thread created");
            }
        }).start();
    }
}

Output

New thread created

Built-In Java Functional Interfaces

  • Many interfaces have been transformed into functional interfaces since Java SE 1.8. @FunctionalInterface is used to annotate each of these interfaces. The following are these interfaces:

    • Runnable: This interface only contains the run() method.

    • Comparable: This interface only contains the compareTo() method.

    • ActionListener: This interface only contains the actionPerformed() method.

    • Callable: This interface only contains the call() method.

1. Consumer

  • The functional interface's consumer interface is the one that only takes one argument or one that has been gentrified. There is no return value on the user interface. It gives no results. The Consumer DoubleConsumer, IntConsumer, and LongConsumer all have functional variations. Primitive values are accepted as arguments in these variations.

  • In addition to these variations, there is another version of the Consumer interface called Bi-Consumer.

Syntax / Prototype of Consumer Functional Interface:

Consumer<Integer> consumer = (value) -> System.out.println(value);
  • This Java Consumer functional interface implementation outputs the value that was supplied to the print statement as a parameter. Java's Lambda function is used in this implementation.

2. Predicate

  • One argument's boolean-valued function is represented by the Predicate interface. It is commonly utilized to stream filtering procedures.

  • Just like the Consumer functional interface, Predicate functional interface also has some extensions. These are IntPredicate, DoublePredicate and LongPredicate. These types of predicate functional interfaces accept only primitive data types or values as arguments.

Syntax:

public interface Predicate<T> {    boolean test(T t);}
  • The Java predicate functional interface can also be implemented using Lambda expressions.
Predicate predicate = (value) -> value != null;

3. Function

  • In Java, a function is a kind of functional interface that takes in a single parameter and, after the necessary processing, returns a value. Primitive types like double, int, and long frequently use a variety of instrumental function interface versions.

Syntax:

Function<Integer, Integer> function = (value) -> value * value;
  • Bi-Function: The Bi-Function is substantially related to a Function. Besides, it takes two arguments, whereas Function accepts one argument.

  • Unary Operator and Binary Operator: There are also two other functional interfaces which are named as Unary Operator and Binary Operator. They both extend the Function and Bi-Function respectively, where both the input type and the output type are same.

4. Supplier

  • The Supplier functional interface is also a type of functional interface that does not take any input or argument and yet returns a single output.

  • The different extensions of the Supplier functional interface hold many other suppliers functions like BooleanSupplier, DoubleSupplier, LongSupplier and IntSupplier. The return type of all these further specializations is their corresponding primitives only.

Syntax:

Supplier<String> supplier = () -> "Hello, World!";

Example: Using Predicate Interface to Filter Strings

import java.util.*;
import java.util.function.Predicate;

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

        // create a list of strings
        List<String> n = Arrays.asList(
            "Java", "CoreJava", "Java1", "Core", "Java2");

        // declare the predicate type as string and use lambda expression to create object
        Predicate<String> p = (s) -> s.startsWith("CJ");

        // Iterate through the list
        for (String st : n) {

            // call the test method
            if (p.test(st))
                System.out.println(st);
        }
    }
}

Functional Interfaces Table

Functional InterfacesDescriptionMethod
RunnableIt represents a task that can be executed by a thread.void run()
ComparableIt compares two objects for ordering.int compareTo(T o)
ActionListenerIt handles an action event in event-driven programming.void actionPerformed(ActionEvent e)
CallableIt represents a task that can return a result or throw an exception.V call() throws Exception
ConsumerIt accepts a single input argument and returns no result.void accept(T t)
PredicateIt accepts a single argument and returns a boolean result.boolean test(T t)
FunctionIt accepts a single argument and returns a result.R apply(T t)
SupplierIt does not take any arguments but provides a result.T get()
BiConsumerIt accepts two arguments and returns no result.void accept(T t, U u)
BiPredicateIt accepts two arguments and returns a boolean result.boolean test(T t, U u)
BiFunctionIt accepts two arguments and returns a result.R apply(T t, U u)
UnaryOperatorThis is a special case of Function, where input and output types are the same.T apply(T t)
BinaryOperatorThis is a special case of BiFunction, where input and output types are the same.T apply(T t1, T t2)

Happy Learning

Thanks For Reading! :)

SriParthu πŸ’πŸ’₯

0
Subscribe to my newsletter

Read articles from sri parthu directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

sri parthu
sri parthu

Hello! I'm Sri Parthu! 🌟 I'm aiming to be a DevOps & Cloud enthusiast πŸš€ Currently, I'm studying for a BA at Dr. Ambedkar Open University πŸ“š I really love changing how IT works to make it better πŸ’‘ Let's connect and learn together! πŸŒˆπŸ‘‹