Java 8 Tutorial

Java 8 Tutorial with Features

Java 8 is a revolutionary release of the Java Programming language. It was released on Mar 18, 2014, and was the stable release of the Java programming language and includes a huge upgrade to its model and a coordinated evolution of the JVM, Java language, and libraries. It includes several features for productivity, ease of use, improved polyglot programming, security, and improved performance. This Java 8 tutorial is completely based on new features of Java 8.

Java 8 Tutorial

Objective

In this Java 8 tutorial, our main objective is to explain the basic-to-advanced features of Java 8 and their usage simply and intuitively.

Prerequisites

We are starting this Java 8 tutorial from scratch, so, you are not required to have any prior knowledge. But, It is good to have basic knowledge of the Java programming language.

In this Java 8 tutorial, we have included the following topics:

  1. What is Java
  2. How to Download & Install Java 8
  3. What’s new in Java 8
  4. Java 8 Features
  5. Default Methods in Java 8
  6. Functional interface in Java 8
  7. Lambda Expressions in Java 8
  8. Lambda Scopes in Java 8
  9. Streams in Java 8
  10. Parallel Streams in Java 8
  11. Date/Time API in Java 8
  12. Map in Java 8
  13. Annotations in Java 8
  14. Collection API Improvements in Java 8
  15. Concurrency API Improvements
  16. IO improvements in Java 8
  17. Conclusion

Apart from the above tutorials, you can check out our separate Java Tutorial. This tutorial is based on Java 8. all the tutorials and practicals are performed using the JDK 8.

Let’s get started with Java 8. before diving into the tutorial, let’s see a brief introduction of Java technology.

What is Java

Java is a high-level programming language used for developing multiple platform applications such as mobile applications, desktop applications, web applications, games, and many more.

The Java programming language is a platform-independent programming language means we can write the Java program on one platform and can compile it on other platforms. For example, if we write a Java program on Windows PC, then we can easily run it on other platforms such as Linux, macOS, etc., without making any changes in it.

Java is a platform-independent language; which is the main reason behind the development and popularity of Java.

See more about what is Java.

How to Download & Install Java 8

The Java Development Kit(JDK) allows us to code and run Java programs & applications. In order to run a Java program, we must need to have any version of JDK. In this Java 8 tutorial, we are discussing how to download and install the JDK 8 (Java 8). We may have multiple versions of Java on our machine.

The following are the steps to download and install Java 8 on our machine. However, these steps will work the same for the other Java versions.

Step1: Click on this link, it will take you to Oracle’s official download page. Scroll down the page and search for the Java 8 download link.

Click on the JDK Download link.

Step2: Select the required JDK version

Now, select your required JDK version as per system configuration.

Now, click on the download link.

Step3: Accept the license agreement

When we click on the download link, it will open a pop-up window to accept the license agreement.

Select the license agreement; the download link will be highlighted. Now, click on this link.

It will start downloading the selected JDK version.

Step4: Run the Installer File

Once the download is completed. Run the installer file and follow some basic prompts.

It will take a while to install the JDK on your system. For the windows-based systems, we are required to set the environment variables.

Step5: Set the Environment variable

The environment variables are PATH and CLASSPATH. The PATH variables represent the location of executable Java files javac, java, etc. However, we can run a Java program without specifying the Path. But, we will have to specify the full Path (The location where Java is installed) whenever we run a program.

To set the path, right-click on My Computer and select the Properties, and navigate to the Advanced system setting.

Now, click on the Environment Variable.

Here, click on the New button and type the PATH in the variable name field, and in the variable value field, copy the location of the bin folder of the JDK.

Usually, the bin folder can be found under the directory C: Program Files-> Java-> JDK 1.8 -> bin location.

Click Ok to continue.

Step6: Test the JDK

Now, open the command prompt and type the below command to test the installed Java version:

java -version

It will display the installed version of Java.

For macOS: Visit How to install Java on Mac

For Linux: Visit How to install Java on Linux

Now, we have successfully installed Java on our machine. Let’s move a step forward to our main topic Java 8. Let’s discuss what’s new in Java 8.

What’s New in Java 8 || Java 8 Features

Java 8 is a powerful and stable release of Oracle’s Java programming languages. It includes some updated features and bug-fixes of the earlier version of Java.

Java 8 Features

Some key highlight’s of Java 8 are as following:

  • Lambda expressions
  • Method references
  • Default Methods (Defender methods)
  • A new Stream API.
  • Optional Class
  • Collectors class
  • A new Date/Time API.
  • Nashorn, the new JavaScript engine
  • Removal of the Permanent Generation
  • Functional interfaces
  • Base64 Encode Decode
  • Static methods in interface
  • ForEach() method
  • Parallel array sorting
  • Type and Repeating Annotations
  • IO Enhancements
  • Concurrency Enhancements
  • JDBC Enhancements etc.

In this Java 8 tutorial, we are going to discuss all the above features. Let’s discuss them in detail:

Default Methods in Java 8

Before Java 8, interfaces could have only abstract methods. The implementation of these methods was defined in a separate class. So, if we want to add a new method is in an interface, then we must provide its implementation code in the class implementing the same interface. Java 8 overcomes this issue, it introduced the concept of default methods which permits the interfaces to have methods with implementation. It will not affect the classes that implement the interface.

In a nutshell, Java 8 allows us to add non-abstract method implementations to interfaces by using the default keyword. This process is also termed as Extension Methods.

Consider the below example:

Slidable.java:

package java8.javasterling;
public interface Slidable {
	    default void slide(){
	        System.out.println("I am Sliding");
	    }
	}

From the above code, the Slidable interface defines a method slide() and provided a default implementation as well. If any class will implement the Slidable interface, then it is not necessary to define the slide() method instead, it can directly call the slide method by using the classinstance.Slide() statement.

Consider the below class example:

Demo.java:

package java8.javasterling;

public class Demo implements Slidable {      
	    public static void main(String[] args){
	        Demo d = new Demo();
	        d.slide();
	    }
	}

Output:

From the above code, we can see the Demo class is implementing the Slidable interface, so by default it can use the behaviour of the default method slide().

If the Demo class willingly wants to customize the behavior of the slide() method then it can provide its own custom implementation and override the method.

Functional interface in Java 8

The functional interface is also known as Single Abstract Method Interfaces (SAM Interfaces). The functional or Single Abstract Method Interfaces allow exactly one method inside them. In Java 8, We can use an annotation @FunctionalInterface with the interfaces for handling the compile-time errors while violating the contracts of Functional Interface.

The Functional Interface can have any number of default, static methods but can contain only one abstract method. Methods of object class can also be declared within the Functional Interface. It is useful for achieving functional programming in Java.

Consider the below example:

FunctionaInterfaceDemo.java:

package java8.javasterling;
@FunctionalInterface  
interface MyInterface{  
    void msg(String msg);  
}  
public class FunctionalInterfaceDemo implements MyInterface{  
    public void msg(String msg){  
        System.out.println(msg);  
    }  
    public static void main(String[] args) {  
        FunctionalInterfaceDemo fie = new FunctionalInterfaceDemo();  
        fie.msg("Hello Readers");  
    }  
}  

Output:

Below are some consideration points about the functional interface in Java:

  • We can omit the @FunctionalInterface annotation, still it will be a valid interface. These annotations are only used for informing the compiler to enforce the single method implementation inside the interface.
  • We can add several default methods in the functional interface as they are not abstract.
  • If an interface declares an abstract method overriding one of the public methods of java.lang.Object class, then it will not be counted as the interface’s abstract method. This means any implementation of the interface will have an implementation from java.lang.Object or elsewhere.

Consider the below code:

@FunctionalInterface
public interface MyInterface
{
    public void firstTask();
 
    @Override
    public String toString();                //Overridden method from java.lang.Object class
 
    @Override
    public boolean equals(Object obj);        //Overridden method from java.lang.Object class
}

The above code is a valid example of the functional interface.

Lambda Expressions in Java 8

Lambda Expression is a new feature of Java 8. You must overhear about lambda expression because it is one of the best features of Java 8. many of us must be familiar with lambda expression if we have some exposure to other programming languages like Scala.

In Java, a lambda is like an anonymous function, which means it has no name and is not bounded to an identifier. The lambda function is written exactly where they needed like a parameter to some other function.

It provides a clear and concise way to represent one method interface using an expression. It is very useful in the collection library. It helps to iterate, filter, and extract data from collection.

Generally, it is used for the implementation of a functional interface and reduces the line of codes. By using the lambda expression, we are not required to define the method again for the implementation. It is treated like a function by the Java compiler so, the Java compiler does not create a class file for the Lambda expression.

Below is the syntax of the lambda expression:

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

The Lambda expression looks like as follows:

(a, b) -> a + b  //This expression takes two parameters and returns their sum.

Based on the type of a and b, the above method may be used in multiple places, the parameters can match to other data types such as int, integer, or String. Based on context, it will add two integers or concat two strings.

Let’s understand it with an example:

Consider the below example using the Lambda Expression:

LambdaExpressionDemo.java:

package java8.javasterling;

interface Runnable{  
    public void run();  
}  
public class LambdaExpressionDemo {  
    public static void main(String[] args) {  
        int speed=10;   
        //Using lambda  Expression
        Runnable r=()->{  
            System.out.println(" I am Running with the Speed of "+speed);  
        };  
        r.run();  
    }  
}  

Output:

Now understand the same example without lambda expression:

Without using the Lambda Expression

LambdaExpressionDemo1.java:

package java8.javasterling;

interface Runnable1{  
    public void run();  
}  
public class LambdaExpressionDemo1 {  
    public static void main(String[] args) {  
        int width=10;  
  
        //without lambda, The Runnable implementation using anonymous class  
        Runnable1 r=new Runnable1(){  
            public void run(){System.out.println("I am running with the speed of "+width);}  
        };  
        r.run();  
    }  
}  

Output:

Lambda Scopes in Java 8

we can easily access the scope variable from lambda expression. It is much similar to anonymous objects. We can access final variables, instance fields, and static variables from the local outer scope.

Accessing the local Variables:

The final local variables can be read from the outer scope of lambda expressions. Consider the below example:

final int n = 1;
Converter<Integer, String> stringConverter =
        (from) -> String.valueOf(from + n);

stringConverter.convert(2); 

In the above code, unlike to anonymous objects, It is not necessary to to declare the variable ‘n’ as final; that would also be a valid code:

int n = 1;
Converter<Integer, String> stringConverter =
        (from) -> String.valueOf(from + n);

stringConverter.convert(2); 

However, the variable n must be declared as final for the code to compile. Consider the below code:

int n = 1;
Converter<Integer, String> stringConverter =
        (from) -> String.valueOf(from + num);
n = 3;

The above code will not compile.

Accessing Default Interface:

The Default methods cannot be accessed from within lambda expressions.

Accessing the fields and static variables:

We are allowed to read and write access to instance fields and static variables from within the lambda expressions.

Streams in Java 8

Stream is one of the fascinating features of Java 8. it a new feature in Java 8. the Stream API in Java is java.util.stream; it holds classes, interfaces, and enum to allow functional programming in Java. To use stream, we have to import the java.util.stream package.

Stream API provides a mechanism for processing a set of data in different ways that may be filtering, transformation, or any other way. It supports different types of iteration where we can define the set of operations to be processed, operations to be performed on each item, and where the output of these processes to be stored.

Some important features of Stream API are as follows:

  • Stream is not used to store elements; instead, it is used to convey elements from a source like an array, or other data structure, I/O channel using a pipeline of computational operations.
  • Stream’s nature is functional; the operation performed does not modify its source. Let’s consider a scenario, if we have to filter a Stream, then filtering a Stream from a collection will produce a new Stream without the filtered elements, it will not remove elements from the source collection.
  • Stream is lazy in nature; it evaluates code only when required.
  • The elements of a stream are only visited once during the life of a stream. A new Stream must be regenerated to revisit the same elements of the source.

Consider the below example for filtering the collection using Stream:

package java8.javasterling;

import java.util.*;  
import java.util.stream.Collectors;  
class Items{  
    int id;  
    String iName;  
    float iprice;  
    public Items(int id, String iname, float iprice) {  
        this.id = id;  
        this.iName = iName;  
        this.iprice = iprice;  
    }  
}  
public class JavaStreamDemo {  
    public static void main(String[] args) {  
        List<Items> cart = new ArrayList<Items>();  
        //Adding Products  
        cart.add(new Items(1,"Dell Laptop",25000f));  
        cart.add(new Items(2,"iPad",30000f));  
        cart.add(new Items(3,"mac",28000f));  
        cart.add(new Items(4,"Samsung Note",28000f));  
        cart.add(new Items(5,"Air Conditioner",90000f));  
        List<Float> itemPriceList2 =cart.stream()  
                                     .filter(p -> p.iprice > 30000)// filtering data  
                                     .map(p->p.iprice)        // fetching price  
                                     .collect(Collectors.toList()); // collecting as list  
        System.out.println(itemPriceList2);  
    }  
}  

Output:

From the above example, we are filtering data using stream API. We can see that code is optimized and maintained. It will provide fast execution to the program.

Parallel Streams in Java 8

Since Java 8, parallel streams are supported by Java. It is useful for utilizing the core of the processor. Normally, Java code supports one stream of processing executed sequentially. Whereas, using parallel Streams. We can divide the code into multiple streams that will be executed parallelly on the separate cores of the processor and will produce the final result as the combination of the individual outcomes. However, we can not control the order of execution.

Therefore, it is recommended to use the parallel streams in case where we have not required the order of execution. Also, the result is unaffected and the state of one element does not affect the other as well as the source of the data also remains unaffected.

We can create parallel streams using the following two ways in Java:

  1. Using parallel method on a streams
  2. Using parallelStream() on a collection

Using parallel() method on a stream

The parallel() method is a method of BaseStream interface; it returns an equivalent parallel stream.

Let’s understand it with an example:

The parallel() method of the BaseStream interface returns an equivalent parallel stream. Let us explain how it would work with the help of an example.

Below is a simple example of printing the numbers from 1 to 5:

ParalleStreamDemo.java:

package java8.javasterling;
import java.util.stream.IntStream;

public class ParallelStreamDemo {

    public static void main(String[] args) {

        System.out.println("Normal Execution...");
      
        IntStream range = IntStream.rangeClosed(1, 5);
        range.forEach(System.out::println);

        System.out.println("Parallel Execution...");

        IntStream range2 = IntStream.rangeClosed(1, 5);
        range2.parallel().forEach(System.out::println);

    }
}

Output:

Normal Execution...
1
2
3
4
5
Parallel Execution...
3
2
5
1

Using the parallelStream() on a Collection

The parallelStream() method of the Collection interface returns a feasible stream with the collection as the source. Let’s understand it’s working with an example:

Below is a simple example for printing the letters from a to z using the parallelStream() on a collection:

ParallelStreamDemo1.java:

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

public class ParallelStreamDemo1 {
    public static void main(String[] args) {
        System.out.println("Normal Execution...");
        List<String> alpha = getData();
        alpha.stream().forEach(System.out::print);
        System.out.println("\n" + "Parallel Execution...");
        List<String> alpha2 = getData();
        alpha2.parallelStream().forEach(System.out::print);    
    }

    private static List<String> getData() {

        List<String> alpha = new ArrayList<>();

        int n = 97;  // 97 = a , 122 = z
        while (n <= 122) {
            char c = (char) n;
            alpha.add(String.valueOf(c));
            n++;
        }
        return alpha;
    }
    }

Output:

Normal Execution...
abcdefghijklmnopqrstuvwxyz
Parallel Execution...
qwhirxglmjkdefbcayztopnsuv

Date/Time API Changes in Java 8

Java 8 introduced a new Date and Time API (Classes) called JSR-310 (ThreeTen), which provides a new way to handle dates in Java programs.

Dates

The Date class has improved in Java 8. The new classes that are deliberated to replace the Date class are LocalDate, LocalTime, and LocalDateTime.

  • The LocalDate class is meant for a date; it does not support the representation of time and time-zone.
  • The LocalTime Class is meant for a time; it does not support a date or time-zone.
  • The LocalDateTime class is meant for date-time; it does not support date or time-zone.

We can also use the date functionality with zone information using the additional three classes, which are OffsetDate, OffsetTime, and OffsetDateTime. We can also represent the timezone offset in “+05:30” or “Europe/Paris” formats by using another ZoneId class.

The following are the implementation of Date API:

LocalDate localDate = LocalDate.now();
LocalTime localTime = LocalTime.of(12, 20);
LocalDateTime localDateTime = LocalDateTime.now(); 
OffsetDateTime offsetDateTime = OffsetDateTime.now();
ZonedDateTime zonedDateTime = ZonedDateTime.now(ZoneId.of("Europe/Paris"));

Timestamp and Duration

The Instant class is used to represent the specific timestamp at any moment. It represents an instant time to an accuracy of nanoseconds. The operations on an Instant class included comparison to another Instant and add or subtract a duration.

The following are the implementation of the Instant class:

Instant instant = Instant.now();
Instant instant1 = instant.plus(Duration.ofMillis(5000));
Instant instant2 = instant.minus(Duration.ofMillis(5000));
Instant instant3 = instant.minusSeconds(10);

There is another new concept is added in Java 8, which is the Duration class. It is used to represent the time duration between two timestamp stamps:

The following are the implementation of the Duration class:

Duration duration = Duration.ofMillis(5000);
duration = Duration.ofSeconds(60);
duration = Duration.ofMinutes(10);

for the small unit of time we can use Duration, but, what about long durations?. Here, Java 8 provides a Period class to interact with humans for long periods. In the Period class, we can define Days, Months, etc.

The following are the implementation of the Period class:

Period period = Period.ofDays(6);
period = Period.ofMonths(6);
period = Period.between(LocalDate.now(), LocalDate.now().plusDays(60));

Map in Java 8

The Map converts each element into another object using the given function in intermediate operation. We can also use the map to transform each object into another type of object. In the case of a generic type of resulting stream, it depends on the generic type of the function passed to the map.

Consider the below example for converting the each strings into upper-case string:

stringCollection
    .stream()
    .map(String::toUpperCase)
    .sorted((a, b) -> b.compareTo(a))
    .forEach(System.out::println);

The above example is converting each string into an upper-cased string.

Annotations in Java 8

Java 8 supports two types of annotations, which are type and repeatable.

Type Annotations in Java 8

In the early Java versions, we can apply annotations only to declarations. Since Java 8, we can apply annotations in any type of use, which means we can use annotations anywhere where we use a type. For a scenario, consider you want to avoid NullPointerException in your Java program, you can declare a string variable as follows:

@NonNull String str;

The following are some examples of the typed annotations:

@NonNull List<String>  
List<@NonNull String> str  
Arrays<@NonNegative Integer> sort  
@Encrypted File file  
@Open Connection connection  
void divideInteger(int a, int b) throws @ZeroDivisor ArithmeticException  

The typed annotations are used to support improved analysis of the Java programs and provide stronger type checking.

Repeating annotations in Java 8

the repeating annotations are useful for reusing the annotations for the same class. We can repeat an annotation anywhere that we would use a standard annotation.

The repeating annotations are stored in an annotation container for compatibility reasons. The container annotation is auto-generated by the Java Compiler.

The following two declarations are essential for the compiler to auto-generate the container annotation:

  • Declare a repeatable annotation type
  • Declare the containing annotation type

The repeatable annotation is declared as follows:

@Repeatable(Employee.class)  
@interfaceEmployee{  
    String name();  
    double sal();  
}  

The containing the annotation type is declared as follows:

@interfaceGames{  
    Employee[] value();  
}

“ The compiler will throw an error if the same annotation is applied to a declaration without first declaring it as repeatable. ”

Collection API Improvements in Java 8

We have already discussed Stream API for collections. Few more new methods are added in Collection API are given below:

  • Iterator default method forEachRemaining(Consumer action) to perform the given action for each remaining element until all elements have been processed or the action throws an exception.
  • Collection default method removeIf(Predicate filter) to remove all of the elements of this collection that satisfy the given predicate.
  • Collection spliterator() method returning Spliterator instance that can be used to traverse elements sequentially or parallel.
  • Map replaceAll(), compute(), merge() methods.
  • Performance Improvement for HashMap class with Key Collisions

Concurrency API Improvements

The following are some important concurrent API enhancements:

ConcurrentHashMap

  • compute()
  • forEach()
  • forEachEntry()
  • forEachKey()
  • forEachValue()
  • merge()
  • reduce() and search() methods.

CompletableFuture that may be explicitly completed by setting its value and status.

Executors newWorkStealingPool() method to create a work-stealing thread pool using all available processors as its target parallelism level.

IO Improvements

  • Files.list(Path dir)
  • Files.lines(Path path)
  • Files.find()
  • BufferedReader.lines()

Conclusion:

In this Java 8 tutorial, we have discussed all the major improvements and new features of Java 8. for more details, visit enhancements in Java SE 8.