Java SE 8 may be one of the most profound Java releases in its 14-year history. When Java was released in 1996 and adopted by the masses in the form of applets (and later Servlets, JSP, and even user interfaces), the consumer computer world was a different place. Most of us had a single CPU in our computers with a single core. Java worked well on a single CPU and, as time and technology evolved, we developed ways to manage concurrency. Java was multi-threaded from its inception, and it implemented a lock-based synchronization strategy for managing access to shared data.
But that was then, this is now. Most of us have multiple CPUs with multiple cores on our desktops, and our servers have even more power. Furthermore, with the advent of the cloud, distributed and parallel computing have grown at an unprecedented rate. In order to stay current with the times, parallel computing requires a paradigm shift away from traditional object-oriented programming to a more functional model. Thus, in the past few years we have seen the evolution of new functional programming languages like Scala and Haskel, as well as the reemergence of older functional programming languages like Erlang.
So what is to become of Java? Java has had a rich history, but if it is to continue to be the language of choice of the mainstream commercial world, it must evolve. And with Java 8, evolve it has!
This article is not exhaustive in its review of the new features added to Java SE 8 (you can read a more exhaustive article series here), but it highlights the specific subset of features that modernize the Java programming language with functional programming concepts to allow it meet the demands of modern computing. In other words, if you are going to stay relevant as a Java programmer in the coming years, then these are the new features in Java that you need to understand.
Java Methods and Functions
We are familiar with passing variables and values to methods, but Java 8 permits methods and functions to be passed to methods as well. This allows you to write a generic method and pass in code that allows it to build a specific result. For example, consider a list of cars. If we want to filter the list of cars to include only coupes or only sedans, we can define methods that perform this check for us. Listing 1 shows a Car class with two static methods that determine if a car is a coupe or a sedan.
public class Car {
public static Boolean isSedan( Car car ) {
return car.getType().equals( "sedan" );
}
public static Boolean isCoupe( Car car ) {
return car.getType().equals( "coupe" );
}
}
Listing 1 shows a snippet from the Car class that contains two methods which, when provided with a Car, interpret the type: isSedan() returns true if the type is a "sedan" and isCoupe() returns true if the type is a "coupe". Listing 2 shows the filter() method from a CarInventory class that manages a list of cars.
public class CarInventory {
public List<Car> filter( Predicate<Car> p ) {
List<Car> results = new ArrayList<Car>();
for( Car car : carList ) {
if( p.test( car ) ) {
results.add( car );
}
}
return results;
}
}
The filter() method iterates over a list of cars and builds a result that matches the predicate condition. This probably leads to the question, what is a Predicate? The Predicate interface is defined in the java.util.function package and looks similar to Listing 3.
public interface Predicate<T> {
public boolean test( T t );
}
A predicate, in mathematics, is a function that accepts a value and returns true or false. Predicate<Car> could likewise have been written as Function<Car,Boolean>, but Predicate<Car> is more succinct.
Finally, Listing 4 shows how you can pass the correct Car method to the CarInventory filter() method.
CarInventory carInventory = new CarInventory();
List<Car> coupes = carInventory.filter( Car::isCoupe );
List<Car> sedans = carInventory.filter( Car::isSedan );
Using the "::" operator, we are able to pass a method to the filter() method and, as we say in Listing 2, that method will be executed inside the filter() method itself.
It does become burdensome to write static methods in our classes just to be passed as predicates, so we have the option to instead create anonymous functions, which are also called lambdas. Lambdas, in general are defined as follows:
( Input values ) -> Expression that optionally produces a response
Given an input, a lambda can do something, which may produce an output. Predicates are special types of lambdas that are of the following form:
For example, we could retrieve our coupes and sedans as follows:
List<Car> coupes = carInventory.filter( ( Car c ) -> c.getType().equals( "coupe" ) );
List<Car> sedans = carInventory.filter( ( Car c ) -> c.getType().equals( "sedan" ) );
These expressions read as follows: Given a Car c, return true of the type is a "coupe" (or “sedan”). This is functionally equivalent to passing the method (Car::isCoupe).