Java 8 Features with Examples

In this article, we will take a look at Java 8 Features with Examples.

Java 8 features Overview

Some of the key features of Java 8 are

  1. forEach() method in the Iterable interface
  2. Standard and static methods in interfaces
  3. Functional interfaces and lambda expressions
  4. Java Stream API for bulk data operations for collections
  5. Java time API
  6. Collections API improvements
  7. Concurrency API improvements
  8. Java IO improvements

Let’s take a quick look at these Java 8 features. I’ll provide some snippets of code to help you understand the features in an easy way.

1. forEach () method in the Iterable interface

Whenever we need to iterate through a collection, and we need to create an iterator whose only purpose is to iterate. After that, we are looping business logic for each of the items in the collection. We can get ConcurrentModificationException if the iterator is not used correctly.

Java 8 introduced the forEach method in the java.lang.Iterable interface so that we can focus on business logic when writing code. The forEach method takes the java.util.function.Consumer objects as an argument, so that we can have our business logic in a separate place that we can reuse. Let’s look at the Java 8 Features with Examples use of the forEach loop.

package com.HandyOpinion;
 
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.function.Consumer;
import java.lang.Integer;
 
public class Java8_For_Each_Example {
 
    public static void main(String[] args) {
         
        //creating sample Collection
        List<Integer> myList = new ArrayList<Integer>();
        for(int i=0; i<10; i++) myList.add(i);
         
        //traversing using Iterator
        Iterator<Integer> it = myList.iterator();
        while(it.hasNext()){
            Integer i = it.next();
            System.out.println("Iterator Value::"+i);
        }
         
        //traversing through forEach method of Iterable with anonymous class
        myList.forEach(new Consumer<Integer>() {
 
            public void accept(Integer t) {
                System.out.println("forEach anonymous class Value::"+t);
            }
 
        });
         
        //traversing with Consumer interface implementation
        MyConsumer action = new MyConsumer();
        myList.forEach(action);
         
    }
 
}
 
//Consumer implementation that can be reused
class MyConsumer implements Consumer<Integer>{
 
    public void accept(Integer t) {
        System.out.println("Consumer impl Value::"+t);
    }
}

The number of rows may increase, but forEach keeps the iteration logic and business logic in separate places, allowing for more separation of concerns and clean code.

2. Standard and static methods in interfaces

If you read the details of the forEach method carefully, you will find that it is defined in the Iterable interface, but we know that interfaces cannot have method bodies. Starting with Java 8, the interfaces are improved to have a method with implementation. We can use the default and static keywords to interface with the method implementation. forEach method implementation in the repeat interface is like:

default void forEach(Consumer<? super T> action) {
    Objects.requireNonNull(action);
    for (T t : this) {
        action.accept(t);
    }
}

We know that Java does not provide multiple inheritances in classes as this leads to the Diamond problem. How is this handled with interfaces now that interfaces are now similar to abstract classes?

The solution is that in this scenario the compiler throws an exception and we need to provide some implementation logic in the class that implements the interfaces.

package com.HandyOpinion.java8.defaultmethod;
 
@FunctionalInterface
public interface Interface1 {
 
    void method1(String str);
     
    default void log(String str){
        System.out.println("I1 logging::"+str);
    }
     
    static void print(String str){
        System.out.println("Printing "+str);
    }
     
    //trying to override Object method gives compile-time error as
    //"A default method cannot override a method from java.lang.Object"
     
//  default String toString(){
//      return "i1";
//  }
     
}
package com.HandyOpinion.java8.defaultmethod;
 
@FunctionalInterface
public interface Interface2 {
 
    void method2();
     
    default void log(String str){
        System.out.println("I2 logging::"+str);
    }
 
}

Note that both interfaces share a common log () method with implementation logic.

package com.HandyOpinion.java8.defaultmethod;
 
public class MyClass implements Interface1, Interface2 {
 
    @Override
    public void method2() {
    }
 
    @Override
    public void method1(String str) {
    }
 
    //MyClass won't compile without having it's own log() implementation
    @Override
    public void log(String str){
        System.out.println("MyClass logging::"+str);
        Interface1.print("abc");
    }
     
}

As you can see, In the implementation of the MyClass.log() method, Interface1 is used as a static method implementation. Java 8 makes heavy use of the standard and static methods in the collection API, and standard methods were add to keep our code backward compatible.

If a class in the hierarchy has a method with the same signature, the standard methods become obsolete. The object is the base class. So if we have the standard equals(), hashCode() methods in the interface, it becomes unnecessary. For the sake of clarity, interfaces must not have standard object methods.

For more information about interface changes in Java 8, see Changes to the Java 8 Interface.

3. Functional interfaces and lambda expressions

If you notice the interface code above, you will notice the @FunctionalInterface annotation. The new concept introduced in Java 8 was the functional interface. An interface with an abstract mechanism becomes a functional interface. We do not need to use the @FunctionalInterface interpretation to mark an interface as an active interface.

The @FunctionalInterface annotation is a function to prevent the accidental addition of abstract methods in functional interfaces. You can think of it like the @Override annotation, and it’s best to use it. java.lang.Runnable with a single abstract run () method is a great example of a working interface
One of the main advantages of the functional interface is the ability to use lambda expressions to instantiate them. We can instantiate an interface with an anonymous class, but the code looks correct.

Runnable r = new Runnable(){
            @Override
            public void run() {
                System.out.println("My Runnable");
            }};

Since there is only one method in the functional interface, Lambda Feedback can easily implement the method. We just need to provide the arguments method and business logic. For example, we can write the above implementation with the lambda expression like this:

Runnable r1 = () -> {
            System.out.println("My Runnable");
        };

If you only have one statement in the method implementation, we don’t need braces either. For the example above, the anonymous class Interface1 can instantiate with Lambda as follows:

Interface1 i1 = (s) -> System.out.println(s);
         
i1.method1("abc");

Therefore Lambda expressions are easy way to create anonymous classes of functional interfaces. There is no execution time involve in using Lambda impressions, so I will use them with caution as I don’t mind writing a few more lines of code.
A new package, Java.utile.function, with multiple functional interfaces which add to provide Lambda expression and method references to target types.

4. Java Stream API for bulk data operations for collections

A new java.util.stream was added in Java 8 to perform Filter / Map / Collapse type operations on the collection. The Stream API enables sequential and parallel execution. This is one of the best features for me as I work a lot with collections and usually big data, we have to filter them based on certain conditions.
The standard methods stream() and parallelStream() have been added to the collection interface to preserve the stream for sequential and parallel execution. Let’s take a look at another Java 8 Features with Examples.

package com.HandyOpinion;
 
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Stream;
 
public class StreamExample {
 
    public static void main(String[] args) {
         
        List<Integer> myList = new ArrayList<>();
        for(int i=0; i<100; i++) myList.add(i);
         
        //sequential stream
        Stream<Integer> sequentialStream = myList.stream();
         
        //parallel stream
        Stream<Integer> parallelStream = myList.parallelStream();
         
        //using lambda with Stream API, filter example
        Stream<Integer> highNums = parallelStream.filter(p -> p > 90);
        //using lambda in forEach
        highNums.forEach(p -> System.out.println("High Nums parallel="+p));
         
        Stream<Integer> highNumsSeq = sequentialStream.filter(p -> p > 90);
        highNumsSeq.forEach(p -> System.out.println("High Nums sequential="+p));
 
    }
 
}

Output

High Nums parallel=91
High Nums parallel=96
High Nums parallel=93
High Nums parallel=98
High Nums parallel=94
High Nums parallel=95
High Nums parallel=97
High Nums parallel=92
High Nums parallel=99
High Nums sequential=91
High Nums sequential=92
High Nums sequential=93
High Nums sequential=94
High Nums sequential=95
High Nums sequential=96
High Nums sequential=97
High Nums sequential=98
High Nums sequential=99

Note that the parallel processing values are out of order, so parallel processing is very useful when working with large collections.
It is not possible to cover everything about the Stream API in this article. You can read all about the Stream API in the Java 8 Stream API Sample Tutorial.

5. Java time API

It has always been difficult to work with dates, times, and time zones in Java. There was no standard approach or API to date and time in Java. One of the cool additions to Java 8 is the java.time package, which streamlines the work process in Java over time.
Just looking at the Java Time API packages, I can feel that they will be very easy to use. It contains some java.time.format subpackages that provide classes for printing and parsing dates and times, and java.time.zone supports time zones and their rules.
The new time API prefers enumerations over integer constants for months and days of the week. DateTimeFormatter classes are used for converting DateTime objects to strings. For a full tutorial, see the Java Date Time API Sample Tutorial.

6. Collections API improvements

We’ve already seen the forEach () method and Stream API for collections. Some new methods added in the Collection API are:

  • Standard iterator method forEachRemaining (consumer action) to take the specified action on each remaining item until all items have been processed or the action throws an exception.
  • The collection’s default removeIf(predicate filter) method to remove all items from this collection that meet the specified predicate.
  • Collection spliterator() method that returns an instance of spliterator that can be used to iterate through elements sequentially or in parallel.
  • Map methods replaceAll(), compute(), merge().
  • Improved performance of the HashMap class on key collisions

7. Concurrency API improvements

Some important concurrent API improvements are:

  • ConcurrentHashMap methods compute(), forEach(), forEachEntry(), forEachKey(), forEachValue(), merge(), Reduce() and search().
  • CompletableFuture, which can be explicitly completed (set value and status).
  • Executes the newWorkStealingPool () method to create a labor-stealing thread pool that uses all available processors as the target degree of parallelism.

8. Java IO improvements

Some IO improvements I know are:

  • Files.list (Path dir), which returns a slowly filled stream, the elements of which are the entries in the directory.
  • Files.lines (path path) which read all the lines of a file as a stream.
  • Files.find (), which returns a stream that is slowly being filled with Path by looking for files in a file tree that is rooted at a particular startup file.
  • BufferedReader.lines () return a stream of which elements are lines to be read by this BufferedReader.

In this article, we learn about the Java 8 Features with Examples.

Please share this post:
Posts created 48

Ask a Question

This site uses Akismet to reduce spam. Learn how your comment data is processed.

Related Posts

Begin typing your search term above and press enter to search. Press ESC to cancel.

%d bloggers like this: