Java 8 ☕: Key Features in a Nutshell

Java 8 introduced a variety of enhancements to the programming language. In this blog let's understand the Java 8 features. Discuss their significance

  1. Functional Interfaces and Lambda Expressions(λ)

    Functional Interface: An interface that contain only one abstract method but you can have any number of concrete ,default,static methods.

interface Calculator {
    int calculate(int x, int y);
}

examples: Runnable ,Callable,Supplier,Consumer, Comparable etc..

Lambda Expressions: It is an anonymous function , i.e, a function with no name, no type and no modifiers. They brought functional programming style closer to java and helps to write more concise and readable code.

syntax:

(parameters) -> { body } ; "-> " Separates the parameters from impl

lambda expressions provide implementation to the abstract method of a functional interface. They leverage automatic type inference

Using anonymous inner classes to provide an implementation for the abstract method of a functional interface.

Calculator c = new Calculator(){
            @Override
            public int calculate(int x, int y) {
                return x + y;
            }};

Using Lambda Expressions:

Calculator cal = (x, y) -> x + y;
cal.calculate(10, 20);// calling a Lambda expression using FI reference variable

Commonly used functional interfaces:

Predicate<T> , Function<T, R> , Consumer<T> ,Supplier<T>

public interface Predicate {       public interface Function<T,R> {
  boolean test(T t);                    R appply(T t);
}                                   }

public interface Consumer {        public interface Supplier {
  void accept(T,t);                      T get(); 
}                                   }
  1. Default and static methods in Interfaces

Java 8, interfaces are enhanced to have a method with implementation. We can use default and static keyword to create interfaces with method implementation.

why default and static methods were introduced ?

  1. Static methods in interfaces allow to define utility methods related to the interface. These methods can provide common functionality that is useful across multiple implementations of the interface.

  2. Default methods enable you to add new functionality to existing interfaces and ensure backward compatibility. They allow interfaces to evolve over time by adding new methods without breaking existing implementations.

    Note :
    1. Static methods can be accessed using interface name and they are not inherited by implementation class and cannot be overridden
    2. Default methods can be accessed using reference variable of implementation class and can be overridden

     public interface Calculator {
    
         // Static method 
         static String getDefaultMode() {
             return "Basic";
         }
    
         // Default method 
         default void displayMode() {
             System.out.println("Calculator Mode: " + getDefaultMode());
         }
    
         // Abstract method
         int calculate(int x, int y);
     }
    
  3. Method References

    The double colon (::) operator in Java is used for method references, providing a concise way to refer to methods without invoking them directly. It comes in four types: static method reference, instance method reference, reference to an instance method of an arbitrary object, and constructor reference. Use of method references contributes to writing cleaner, more expressive, and maintainable code in Java, particularly in the context of functional programming paradigms.

// Reference to a static method    
Syntax: ContainingClass::staticMethodName

Calculator calculator = (x,y) -> x+y; // using lambda
Calculator calculator = Integer::sum;  

//Reference to an instance method of a particular object
Syntax: containingObject::instanceMethodName 

 Calculator c = new BasicCalculator();
 Runnable mode = c::displayMode;  

 String str = "Chess";
 Supplier<Integer> lengthSupplier = str::length;  
 Integer length = lengthSupplier.get();

//Reference to an instance method of an arbitrary object of a particular type
Syntax : ContainingType::methodName

List<String> list = Arrays.asList("King", "Queen", "Rook");
list.forEach(System.out::println);

String[] stringArray = { "King", "Queen", "Rook", "Bishop","Knight", "Pawn"};
Arrays.sort(stringArray, String::compareToIgnoreCase);

//Reference to a constructor
Syntax : ClassName::new

Supplier<ArrayList<String>> arrayListSupplier = ArrayList::new;
  1. ForEach

    The forEach method was introduced to the Iterable interface, allowing for a more concise and readable way to iterate over collections like lists, sets, and maps. The forEach method accepts a lambda expression or a method reference as its argument, which is executed for each element in the collection.

    Syntax:
    void forEach(Consumer<? super T> action);

List<String> chessPieces = Arrays.asList("King", "Queen", "Bishop", "Knight", "Rook", "Pawn");
// Using forEach to print each chess piece name
chessPieces.forEach(piece -> System.out.println(piece));
  1. Optional

    The Optional class was introduced to address the problem of dealing with null values in a more concise and expressive manner. It is a container class that may or may not contain a non-null value. Using Optional, you can prevent NullPointerExceptions and write more robust and readable code

    • Creating Optional :
    // Empty Optional
    Optional<String> emptyOptional = Optional.empty();

    //Throws Null Pointer Exception when input is null
    Optional<String> nonNullOptional = Optional.of("NonNull-Input"); 

    //Returns an empty Optional if Value is Null
    Optional<String> nullableOptional = Optional.ofNullable(null);
  1. Stream API

The Stream API introduced in Java 8 , Stream is a sequence of elements which provides a new way to process data in a functional style, allowing you to perform operations on collections of objects. Streams allow you to express complex data processing queries concisely, which can lead to more readable and maintainable code

  1. Creating a stream :
// Using of Creates a stream from a sequence of elements
Stream<String> stream = Stream.of("King", "Queen", "Rook", "Bishop");

// Using stream() -- Converts a collection or array into a stream.
Stream<String> stream = Arrays.asList("King", "Queen", "Rook").stream();

// Generates an infinite stream using a Supplier.
Stream<String> infiniteStream = Stream.generate(() -> "atom").limit(5);

// Generates an infinite stream by applying a Unary Funcation repeatedly.
Stream<Integer> infiniteStream = Stream.iterate(0, n -> n + 2).limit(5);
  1. Intermediate Operations
// Filters the elements based on a predicate.
Stream.of("King", "Queen", "Rook", "Bishop").filter(s -> s.startsWith("K"))

//Transforms each element using the given function
Stream.of("King", "Queen", "Rook", "Bishop").map(String::toUpperCase)

//Truncates the stream to a maximum size.
Stream.of("King", "Queen", "Rook", "Bishop").limit(2);

//Flattens the elements of a stream of streams into a single stream.
Stream<List<String>> stream = Stream.of(Arrays.asList("King", "Queen"), Arrays.asList("Rook", "Bishop"));
stream.flatMap(Collection::stream).forEach(System.out::println);
  1. Short-Circuiting Operations

Stream.of("King", "Queen", "Rook").anyMatch(s -> s.equals("Queen"));

Stream.of("King", "Queen", "Rook").findFirst();

Stream.of("King", "Queen", "Rook").findAny();
  1. Terminal Operations
//Performs an action for each element
Stream<String> stream = Stream.of("King", "Queen", "Rook", "Bishop");
stream.forEach(System.out::println);

//Collects the elements into a collection
Stream<String> stream = Stream.of("King", "Queen", "Rook", "Bishop");
List<String> collected = stream.collect(Collectors.toList());

//Reduces the elements to a single value
Stream<Integer> stream = Stream.of(1, 2, 3, 4, 5);
Optional<Integer> reduced = stream.reduce((a, b) -> a + b);

Parallel Streams

A parallel stream can leverage the power of multi-core processors to perform operations concurrently. Instead of processing elements sequentially, a parallel stream divides the workload among multiple threads, allowing for potentially faster execution, especially for computationally intensive tasks or operations that can be performed independently on different elements.

//Creating a parallell stream

List<String> list = Arrays.asList("King", "Queen", "Rook", "Bishop");

// Sequential stream
Stream<String> sequentialStream = list.stream();

// Parallel stream
Stream<String> parallelStream = list.parallelStream();

Note: When using parallel streams, ensure thread-safe operations, expect potential element order changes, and consider overhead that may not always yield faster results.

Base64

Represents binary data in an ASCII string format, In Java 8, the java.util.Base64 class was introduced to provide native support for Base64 encoding and decoding operations.

//Encoding
Base64.getEncoder().encodeToString("Chess Game".getBytes());

//Decoding 
String encodedString = "Q2hlc3MgR2FtZQ==";
new String(Base64.getDecoder().decode(encodedString));

Date-Time API (java.time Package)

Due to the limitations of the java.util.Date and java.util.Calendar classes. The Date-Time API introduced in Java 8, located in the java.time package, provides a comprehensive and easy-to-use API for date and time manipulation.

  • LocalDate : Represents a date without time or timezone information

    Example: 2024-04-06

  • LocalTime : Represents a time without date or timezone information

    Example: 14:30:45

  • LocalDateTime : Represents a date and time without timezone information

    Example: 2024-04-06T14:30:45

  • ZonedDateTime : Represents a date and time with timezone information

    Example: 2024-04-06T19:00:45+05:30[Asia/Kolkata]

  • Instant : Represents an instantaneous point on the time-line

    Example: 2024-04-06T13:30:45Z

  • Duration : Represents a time-based amount of time

    Example : P1Y

  •   LocalDate start = LocalDate.of(2023, 4, 6);
      LocalDate end = LocalDate.of(2024, 4, 6);
      Period period = Period.between(startDate, endDate);
    
  • Period : Represents a date-based amount of time

    Example : PT8784H

  •   Instant start = Instant.parse("2023-04-06T10:18:30.00Z");
      Instant end = Instant.parse("2024-04-06T10:18:30.00Z");
      Duration duration = Duration.between(start, end);
    
  • DateTimeFormatter : Formats and parses date-time objects.

    Example: "yyyy-MM-dd HH:mm:ss"