Java8 Features

Java8 Features

Being a java developer from more than a decade now, I want to share my views on the biggest java release Java8 of the history. Java has done significant changes in the language in java8 release and that will change the way, how we code today at some extent.

Why Java8? and the answer is SPEED.

Here are some of the significant features of Java8.

Interface : Interfaces are added with static and default method which will be concrete methods. These are introduced in a backward compatible way so that it will not break the implementation classes. However its still different from abstract class, As abstract classes can have state, but Interface doesn't and the new default and static methods are only meant to be called. eg.

public interface InterfaceDefaultStatic { 
    default void defaultMethod(){ 
        System.out.println("A default method"); 
    } 
    static void staticMethod () {
        System.out.println("A static method"); 

    }
}

Several default methods are added in collection interface to support new java8 features and hence default methods are introduced instead of reinventing the wheel.

Classes implementing multiple interfaces (which has the same signature default methods) will not compile.

Functional Interfaces : Interfaces with zero or more default methods, zero or more static methods and exactly one abstract method with class level @FunctionalInterface annotation are functional interface. Lambda can be used to implement these functional interfaces. In Java8, existing Interfaces with one abstract method are annotated with @FunctionalInterface annotation hence these can be instantiated using Lambda or Method References. We can find several new functional interface in java.util.function package.

Lambda Expression : Lambda ( - > ) is a anonymous function, which can be used to implement functional interfaces. eg.

Runnable runnable = () -> System.out.println("new thread"); 
Thread th = new Thread(runnable);
Or
Thread th = new Thread(() -> System.out.println("new thread"));

Here lambda is implementing Runnable interface. Lambda are glue code and as a good practice should be one liner.

Method References : Method references is a way to reference a method which can replace lambda expression implementation of a functional interface. This is basically important when we have a bigger lambda implementation or a reusable functionality.

In other words functional interfaces can be instantiated using method references. A method call having functional interface as a parameter can accept method references. eg.

Predicate<Integer> evenPredicate = n -> n % 2 ==0;
//OR
Predicate<Integer> evenPredicate = ClassName::isEvenStaticMethod; 
// isEven is static method with one Integer parameter returning boolean
// OR
Predicate<Integer> evenPredicate = instance::isEvenInstanceMethod; 
// isEven is instance method with one Integer parameter returning boolean


public boolean isEvenInstanceMethod(Integer n){return n % 2 == 0;}
public static boolean isEvenStaticMethod(Integer n){return n % 2 == 0;}

Stream : Stream API are introduced in java8 to perform functional operations on collections/Arrays. Collection interface/Arrays are added with a default stream() method. stream method creates a stream object of the source collection but keeps the source collection object intact. The created stream object is NOT reusable and can only be used once. So if we need to perform another computation on the same collection, we need to create another stream object. The basic difference between stream and collection is, stream is about computation and collection is about data.

Stream comes with several methods which can be grouped into intermediate and terminal operations. All the intermediate operation returns stream object and intermediate operations together creates a pipeline. Stream pipelines are lazily evaluated. So when intermediate operations are called on streams, java runtime just create a pipeline. So java doesn't start computation on a stream until terminal operation is called.

Most of the time we perform three basic type of operations on streams and those are filter, map and reduce. Here filter and map are intermediate operation and reduce is terminal operation.

filter operation filter's a given collection, map transform the collection elements and does not guarantee the same object in response and reduce is a reduce operation eg.

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 1, 2, 3, 4, 5, 6);
System.out.println(numbers.stream()
						  .filter(n -> n % 2 == 0) // block odd number and pass even number to map
						  .map(n -> String.valueOf(n))
						  .reduce("", (carry, str) -> carry.concat(str)));
// OR with method references
System.out.println(numbers.stream()
		 	  			  .filter(ClassName::isEven) // Intermediate Op
		 	  			  .map(String::valueOf) // Intermediate op
		 	  			  .reduce("", String::concat)); // Terminal op

Here stream are being processed sequentially and once the terminal operation reduce called, each element of the collection starts flowing through the stream pipeline one by one.

In other words, java existing loops and streams will have same 12 iteration for the given list.

Intermediate operation operates within their swimlane and doesn't look up and down. filter operation takes a number and let go the even number but block the odd number. it doesn't know up and down. map operation receives only even numbers and it also doesn't look up and down, it just picks the number and transform it into String.

Terminal operation reduce, cuts across the swim lanes and concat all the input received. hence all the even number are concatenated.

Parallel Stream : Parallel Stream is a way to perform the stream computation in parallel. If you have noticed the stream computational operations above are working in a stateless way and each method in pipeline is just taking an element, doing its job and passing it on to the next method in pipeline. So if we execute the stream computation in parallel using the parallel stream, then the parallel stream will actually make use of all your system processing power to execute the computation in parallel and obviously the computation will be lot more faster than the sequential execution. The amazing thing is developer don't need to do anything and everything will be done out of the box via java's fork join framework released as part of Java7. Here is an example.

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 1, 2, 3, 4, 5, 6);
// Sequential Processing
long start = System.nanoTime();
System.out.println(
		      numbers.stream()
		    	     .filter(ClassName::isEven)
		             .mapToInt(ClassName::expensiveComputation)
		             .sum());
long end = System.nanoTime();
System.out.println("Total Time Taken Sequential => "+ (end-start)/1.0e9 +" seconds");
// And With Parallel Stream Processing
start = System.nanoTime();
System.out.println(
		      numbers.parallelStream()
		    	     .filter(ClassName::isEven)
		    	     .mapToInt(ClassName::expensiveComputation)
		    	     .sum());
end = System.nanoTime();
System.out.println("Total Time Taken in Parallel => "+ (end-start)/1.0e9 +" seconds");


public static int expensiveComputation(int number) {
		try {
			Thread.sleep(1000);
		} catch (Exception ex) {
		}
		return number * 3;
}

//OUTPUT - on my mac its 3 times faster in parallel stream
72
Total Time Taken Sequential => 6.018370897 seconds
72
Total Time Taken in Parallel => 2.011356256 seconds


To be more specific, Fork Join Pool default parallelism is 1 less than number of #cores in the system. Here my macbook is having 4 #cores, so parallelism was 3. However we can configure it as java system property. eg.

-Djava.util.concurrent.ForkJoinPool.common.parallelism=5

// Same above program results after configuring parallelism.
72
Total Time Taken Sequential => 6.026331705 seconds
72
Total Time Taken in Parallel => 1.008625642 seconds

Shared Immutability with Parallel Streams :We need to make sure, when we write lambda in the intermediate and terminal operations, we can share the objects with the elements coming in the pipeline but we should NOT be doing shared mutability. eg.

//WRONG
List<Integer> EvensList = new ArrayList<>();

numbers.stream()
	       .filter(e -> e % 2 == 0)
	       .forEach(e -> EvensList.add(e));
System.out.println(EvensList);
//CORRECT WAY
numbers.stream()
           .filter(e -> e % 2 == 0)
           .collect(toList());
System.out.println(EvensList);

// Here both code snippet will give the same result but the first one is NOT OK
// Sharing is fine but we need to make sure we shouldn't have shared mutability.

Spliterator : We have just seen that, The powerful stream api can do the computation in parallel. But to support parallel execution of the pipeline, data elements from the source collection must be spilt over multiple threads into sub collections. The spliterator has trysplit method which returns a spliterator of sub collection of the original collection and the original collection skips this sub collection. and hence recursively spliterator can be broken and processed in parallel.

Internal Iterators : Till java7 all the loops java developers has written, were externally iterated and externally managed. that means, Its programmer's responsibility to write the loop and manage the flow.

In Java8, the concepts of internal iterator has been introduced which makes us to focus only on what need to be done with the elements of collection. It simplifies the loop implementation and improve code readability. eg

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

//external iterators
for(int i = 0; i < numbers.size(); i++) {
      System.out.println(numbers.get(i));
}
	    
//OR
for(int e : numbers) {
	 System.out.println(e);
}
// Using Internal Iterators
numbers.forEach(value -> System.out.println(value));
//OR
numbers.forEach(System.out::println);

It also gives more control to java, in case of parallel processing of collections via parallel stream. Parallel stream is making use of internal iterators to process the collections in parallel.

Optional : No more null pointer exceptions in java. The Optional type is a way to deal with null values. it has utility methods to handle null values and have option the provide the alternative value.

Optional<String> optionalString = Optional.ofNullable("not null");
System.out.println(optionalString.orElse("else value"));
// with null value
// Here the parameter of ofNullable can be a method being called.
Optional<String> optionalString = Optional.ofNullable(null);
System.out.println(optionalString.orElse("else value"));

// Output #1 with not null value
not null
// Output #2 with null value
else value


Sandeep Patil

Software Architect & Project Manager | 20+ Years in Custom Development, Java, Spring Boot, AWS, PCF | Proven Leader in Building Scalable Solutions & High-Performance Teams

8y

Nice Article Rajendra

Tushar Solanki

Lead Engineer @Barclays

8y

Nice document

To view or add a comment, sign in

More articles by Rajendra Pawar

Insights from the community

Others also viewed

Explore topics