Amazing Java 8 features you need to know Part 1

1. Introduction

Java has changed from its inception numerous times. Be it either API enhancement or compiler changes of generics type systems. We all know one thing is certain that ‘CHANGE IS CONSTANT’.

Yet again Java has evolved and it is fair to ask the question, What’s in it for me? Or Why should I use Java 8? I will answer this question for several features implemented in Java 8. Remember this is not a definitive list. Syntax may look weird but remember the idea of this article is to get you excited about new features. Don’t try to write code while reading this article. It may become challenging. When we discuss these concepts in detail at that time you can code as you read the article.

2. Content

Things we will discuss in this article are as follows:

  1. Internal vs External Iteration
  2. Passing code as behavior
  3. Lambda expressions
  4. High level view of Stream API using Collections
  5. Parallelism

In the next article we will cover Default and Static methods in interface, new Date and time APIs and several other APIs.

Let us get started.

3. Internal vs External Iteration

Iteration means repetition of a sequence of computer instructions for a specified number of times or until a condition is met. Let’s look up iteration with an example.

Given List<Transaction> print the country’s Alpha 3 code where the transaction took place.

for (Transaction transaction : transactions) {
	System.out.println(transaction.country().getAlpha3());
}

We are pulling elements from List and printing them one by one. This is called external iteration. Why should we worry about iteration? Shouldn’t it be handled by the framework itself? 

Java 8 provides us with writing the code as below.

transactions.forEach(new Consumer<Transaction>() {
	@Override
	public void accept(Transaction transaction) {
		System.out.println(transaction.country().getAlpha3());
	}
});

Consumer<T> is an interface that can accept an argument and returns void. forEach method will iterate the List internally.

Yet better we can express this code using lambda expression as follows

txns.forEach((Transaction txn) -> 
					System.out.println(txn.country().getAlpha3()));

This is a new syntax in Java 8 known as lambda expression. It contains the “->” symbol. Left side of the “->” symbol is parameters and the right hand side is the body. lambda expression is applied to interfaces that have one and only one single abstract method. Such interfaces are called Functional Interfaces. As the interface has only one abstract method compiler is able to figure out automatically the type of parameter used so we can rewrite the code as 

txns.forEach(txn -> System.out.println(txn.country().getAlpha3()));

Difference between internal and external iteration?

Internal IterationExternal Iteration
Client controlledFramework controlled
Client needs to use pull mechanism to get data and then process itClient needs to pass behavior
Client has burden to figure out the parallelism details for possibly faster processing of dataFramework will figure out details of parallelism for processing of data

What’s in it for me?

Internal iteration provides an elegant style of code but also has the advantage of speeding up the execution. For external iteration there is no way to process the data in parallel but in internal iteration you are providing the data to the framework and the framework in turn can figure out the details of parallelism, decomposition and so on.

4. Passing code as behavior

As we saw in previous examples we were able to pass lambda as a parameter to the method. Passing lambda can make an API reusable and generic. Let’s take an example and understand it.

Let us take a different example. Our use-case is to find all the even numbers from List and return them. The code is as follows:

List<Integer> evenNumbers(List<Integer> numbers) {
	List<Integer> result = new ArrayList<Integer>();
	for (Integer value : numbers) {
		if (value.intValue() % 2 == 0) {
			result.add(value);
		}
	}
	return Collections.unmodifiableList(result);
}

Let’s say we have a new requirement which says give all positive even numbers. What do we do? We copy the above code and modify it a bit. Below is code for that:

List<Integer> positiveEvenNumbers(List<Integer> numbers) {
	List<Integer> result = new ArrayList<Integer>();
	for (Integer value : numbers) {
		if (value.intValue() > 0 && value.intValue() % 2 == 0) {
			result.add(value);
		}
	}
	return Collections.unmodifiableList(result);
}

This leads to code duplication, not to mention it becomes a nightmare to maintain such a codebase. Right now the example is really simple so it may be hard to understand why it would become unmaintainable. Image requirements are like this:

  1. Get all even numbers
  2. Get all even number greater than 10
  3. Get all even numbers less than 10
  4. Get all odd numbers between 10 and 100.

The solution to such a problem is to use lambda expressions and Predicate<T> functional interface. Predicate<T> interface has only one abstract method that accepts input of type T and returns boolean.

Let us change our method name and method signature. We will accept the Predicate<Integer>. The code is as follows:

List<Integer> filterNumbers(
		List<Integer> numbers, 
		Predicate<Integer> predicate) {
		
	List<Integer> result = new ArrayList<Integer>();
	for (Integer value : numbers) {
		if (predicate.test(value)) {
			result.add(value);
		}
	}
	return Collections.unmodifiableList(result);
}

Now we can call this method as follows:

  • Filter even numbers
    • filterNumbers(numbers, val -> val % 2 == 0);
  • Filter even numbers greater than equal to 0
    • filterNumbers(numbers, val -> val >= 0 && val % 2 == 0);
  • Filter even numbers less than 0
    • filterNumbers(numbers, val -> val < 0 && val % 2 == 0);
  • Filter odd numbers
    • filterNumbers(numbers, val -> val % 2 != 0);

Now we are not only passing the data but also passing the behavior too. A behavior will decide how the data will be used.

What’s in it for me?

Passing the code as behavior makes the code clutter free as well as allows you to focus on implementing business logic more as compared to unnecessary loops.

5. Lambda expressions

Lambda expressions are just like methods. It has parameters, body and can throw exceptions.

Lambda expressions are anonymous in nature and can be passed around the code in a concise manner removing the boiler plate code. Lambda expressions do not have a name and are not associated with the class as methods are. Also it removes the boiler plate code that we deal with anonymous classes.

Below are some examples of lambda expressions. Syntax may not be understandable right now and that is fine. In upcoming articles we will take a deep dive into lambda expressions.

() -> {} // No parameters.
		  
() -> 42; // No parameters. 42 is expression body.
		  
() -> { return 42; } // No parameters. Block body with return.
		  
(int x) -> { // Single declared parameter with body. 
    if(x % 2 == 0) {
        return true; 
    } 
    return false; 
}
		  
(x) -> { // Single inferred parameter with body. 
    if(x % 2 == 0) {
        return true; 
    } 
    return false; 
}
		  
(int x) -> x + 1; // Single declared type. 

(x) -> x + 1; // Single inferred type.
		  
(int x, int y) -> x + y; // Multiple inferred parameters with body.
		  
(int x, y) -> x + y; // Error. Cannot mix inferred and declared type.

Below is the example of Comparator using lambda expressions.

// Before Java 8
Comparator<Transaction> TXN_TIME = new Comparator<Transaction>() {
	@Override
	public int compare(Transaction t1, Transaction t2) {
		return t1.date().compareTo(t2.date());
	}
};

Lambda expression with declared type 

Comparator<Transaction> TXN_TIME = 
 (Transaction t1, Transaction t2) -> t1.date().compareTo(t2.date());

Lambda expression with inferred type 

Comparator<Transaction> TXN_TIME = 
	(t1, t2) -> t1.date().compareTo(t2.date());

See the difference! The lambda expressions are much cleaner, concise and easier to understand.

What’s in it for me?

Lambda expressions express your code in elegant style. Also no more cluttered syntax of anonymous method.

6. High level view of Stream API

Stream is a progression of data elements from some source followed by intermediate and terminal operations.

We won’t dive much into this topic right away as there are multiple articles dedicated to this topic. But just to give you a sneak peek I will provide an example:

Sort US(United States) transactions by time.

Solution prior to Java 8.

List<Transaction> result = new ArrayList<>();

for (Transaction transaction : transactions) {
	if (transaction.country().equals(CountryCode.US)) {
		result.add(transaction);
	}
}

Comparator<Transaction> TXN_TIME = new Comparator<Transaction>() {
	@Override
	public int compare(Transaction t1, Transaction t2) {
		return t1.date().compareTo(t2.date());
	}
};

Collections.sort(result, TXN_TIME);

return result;

Solution in Java 8

List<Transaction> result = 
		transactions
			.stream()
			.filter(txn -> txn.country().equals(CountryCode.US))
			.sorted((t1, t2) -> t1.date().compareTo(t2.date()))
			.collect(Collectors.toList());

return result;

What’s in it for me?

Solution using the Stream API in Java 8 not only looks elegant but it is simple to understand and there are no garbage variables i.e. no temporary Lists or anything like that. In fact, we can read the code like “Filter transactions by country, sort them by date and put them in a List.”

We will look into Stream API’s in detail in upcoming articles.

7. Parallelism

Assume you have millions of transactions in List<Transaction> and you would like to add all the values of transactions. We would usually complete this task using sequential for loop. For loops are inherently sequential. Better way to solve this problem would be to use the Fork-Join framework or RecursiveTask. But that complicates the problem as you need to know the Fork Join framework and its intrinsic details.

In Java 8, we can process this kind of dataset using a method called parallelStream. This method might process data in parallel leveraging multiple cores of the machine. There is no guarantee that parallelStream method will process data in parallel. 

Below is the example of parallelStream method:

BigDecimal total = 
		transactions
		.parallelStream()
		.map(Transaction::amount)
		.reduce(BigDecimal.ZERO, BigDecimal::add);

return total;

What’s in it for me?

Painless parallelism Stream API is an awesome feature, if used properly.

In addition to above features, Java 8 also has Constructor and Method reference, Default and Static methods in interface, new Date and time APIs and several other APIs. We will explore them in upcoming articles.

8. Conclusion

This article describes several different exciting features of Java 8. My personal favorite is lambda expressions and streams. They provide us with writing code in elegant and maintainable way. Boilerplate code like for-each and other loops are not really needed to process collections. In next article we will take a look at Default and Static methods in interface, new Date and time APIs and several other APIs.

Leave a Reply

Your email address will not be published. Required fields are marked *