2019独角兽企业重金招聘Python工程师标准>>>
Introduction
With the release of JDK 8 planned for 2013, Oracle has a pretty fixed idea of what will be included. Speaking at QCon London earlier this year, Simon Ritter outlined the new features that will be part of JDK 8, which include modularity (Project Jigsaw), JRockit/Hotspot convergence, annotations on types, and Project Lambda.
From a language perspective, perhaps the most important change is Project Lambda, which includes support for lambda expressions, virtual extension methods and better support for multicore platforms in the form of parallel collections.
Most of these features are already available in many other JVM languages, including Scala. Moreover, many of the approaches taken in Java 8 are surprisingly similar to those used in Scala. As a consequence, playing with Scala is a good way of getting a feeling for what programming with Java 8 will be like.
In this article we will explore Java 8’s new features, using both Java’s proposed syntax and Scala. We will cover lambda expressions, higher-order functions, parallel collections and virtual extension methods aka traits. Besides this, we will provide insights into the new paradigms integrated in Java 8, such as functional programming.
The reader will experience how the new concepts incorporated in Java 8 - that are already available in Scala - are not mere bells and whistles, but could introduce a true paradigm shift, which will offer great possibilities and may profoundly change the way we write software.
Lambda Expressions/Functions
Java 8 will include lambda expressions, finally! Lambda expressions have been available in the form of Project Lambda since 2009. At that time, lambda expressions were still referred to as Java Closures. Before we jump into some code examples, we will explain why lambda expressions will be a very welcome tool in the Java programmers tool belt.
Motivation for Lambda Expressions
A common use of lambda expressions is in GUI development. In general, GUI programming resolves around connecting behaviour to events. For example, if a user presses a button (an event), your program needs to execute some behaviour. This might be the storage of some data in a datastore. In Swing, for example, this is done using ActionListeners
:
class ButtonHandler implements ActionListener {
public void actionPerformed(ActionEvent e) {
//do something
}
}
class UIBuilder {
public UIBuilder() {
button.addActionListener(new ButtonHandler());
}
}
This example shows the use of the class ButtonHandler
as a callback replacement. The class ButtonHandler
is only there to hold a single method: actionPerformed
, defined in the ActionListener
interface. We could simplify this code a bit by using anonymous inner classes:
class UIBuilder {
public UIBuilder() {
button.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent event) {
//do something
}
}
}
}
This code is somewhat cleaner. When we look more closely at the code, we still create an instance of a class just to call a single method. These kinds of problems are exactly the ones solved by introducing lambda expressions.
Lambda Expressions as Functions
A lambda expression is a function literal. It defines a function with input parameters and function body. The syntax of the lambda expression in Java 8 is still under discussion, but will look something like this:
(type parameter) -> function_body
A concrete example is:
(String s1, String s2) -> s1.length() - s2.length();
This lambda expression calculates the difference in length between two strings. There are some extensions to this syntax, like avoiding the type definition for the arguments, as we will see later on, and supporting multi-line definitions by using { and } to group statements.
The Collections.sort()
method would be an ideal usage example for this expression. It enables us to sort a collection of Strings
based on their lengths:
List list = Arrays.asList("looooong", "short", "tiny" );
Collections.sort(list, (String s1, String s2) -> s1.length() - s2.length());
> "tiny", "short", "looooong".
So, instead of feeding the sort
method with an implementation of Comparator
, as we would have to do with current Java, passing the above lambda expression is all that is needed to achieve the same result.
Lambda Expression as Closures
Lambda expressions have some interesting properties. One is that they are closures. A closure allows a function to access variables outside its immediate lexical scope.
String outer = "Java 8"
(String s1) -> s1.length() - outer.length()
The example shows that the lambda expression has access to the String outer
, which is defined outside its scope. For inline usage scenarios closure can be very handy.
Type Inference also for Lambda Expressions
Type inference, which was introduced in Java 7, also applies to lambda expressions. Type inference, in a nutshell, means that a programmer can omit the type definition everywhere where the compiler can ‘infer’ or deduce the type by itself.
If type inference was used for the sorting lambda expression, it could be written as follows:
List list = Arrays.asList(...);
Collections.sort(list, (s1, s2) -> s1.length() - s2.length());
As you can see, the types for the parameters s1
and s2
are omitted. Because the compiler knows that the list contains a collection of Strings
it knows that a lambda expression used as comparator must also have two parameters of type String
. Consequently, the types do not need to be declared explicitly, even though you are free to do so.
The main advantage of type inference is reduction of boilerplate. If the compiler can infer the type for us, why should we define them ourselves?
Hello Lambda Expressions, Goodbye Anonymous Inner Classes
Let’s see how lambda expressions and type inference help to simplify the callback example we discussed initially:
class UIBuilder {
public UIBuilder() {
button.addActionListener(e -> //process ActionEvent e)
}
}
Instead of defining a class to hold our callback method, we now directly pass a lambda expression into the addActionListener
method. Besides the reduction lot of boilerplate code and increased readability, it lets us directly express the only thing we are really interested in: handling the event.
Before we unravel more advantages of lambda expressions, we first take a look at the lambda expressions counterpart in Scala.
Lambda Expressions in Scala
Functions are the basic building blocks of a style of programming called functional programming. Scala combines object orientation, known from Java, and functional programming. In Scala, a lambda expression is a basic building block called a ‘function’ or ‘function literal’. Functions in Scala are first class citizens. They can be assigned to vals
or vars
(final or non final variables), they can be passed as an argument to another function, and they can be combined to form new functions.
In Scala a function literal is written as follows:
(argument) => //function body
For example, the previous Java lambda expression calculating the difference of length of two strings, is written in Scala like so:
(s1: String, s2: String) => s1.length - s2.length
In Scala, function literals are also closures. They can access variables defined outside their own lexical scope.
val outer = 10
val myFuncLiteral = (y: Int) => y * outer
val result = myFuncLiteral(2)
> 20
This example would result in 20. As you see, we assigned a function literal to a variable named myFuncLiteral
.
The syntactical and semantic similarities between Java 8’s lambda expression and Scala’s function are remarkable. Semantically they are exactly the same, whereas syntactically the only differences are the symbol for the arrow (Java8: -> Scala: =>) and the shorthand notation, which is not covered here.
Higher-Order Functions as Reusable Building Blocks
The great advantage of function literals is that we can pass them around as any other literals, like a String
or an arbitrary Object
. This offers a wide range of possibilities and allows for highly compact and reusable code constructs.
Our First Higher-Order Function
When we pass a function literal to a method, we basically have a method that accepts a method. Such methods are called higher-order functions. The addActionListener
method in the previous Swing code example is exactly one of those. We also can define our own higher-order functions, which can offer us a lot of benefit. Let’s look at a simple example:
def measure[T](func: => T):T = {
val start = System.nanoTime()
val result = func
val elapsed = System.nanoTime() - start
println("The execution of this call took: %s ns".format(elapsed))
result
}
In this example, we have a measure
method, which measures the time needed to execute the function literal callback called func
. The signature of func
is that it does not take any parameters and returns a result of the generic type T. As you can see, functions in Scala do not necessarily need parameters even though they can - and most often also will - have parameters.
Now we can pass any function literal (or method) in the measure method:
def myCallback = {
Thread.sleep(1000)
"I just took a powernap"
}
val result = measure(myCallback);
> The execution of this call took: 1002449000 ns
What we have done, from a conceptual point of view, is separating the concern of measuring the length of a method call from the actual computation. We created two reusable code constructs (the measure part and the callback part) that are very loosely coupled, similar to an interceptor.
Reuse through Higher-Order Functions
Let’s look at another hypothetical example where two reusable constructs are slightly tighter coupled:
def doWithContact(fileName:String, handle:Contact => Unit):Unit = {
try{
val contactStr = io.Source.fromFile(fileName).mkString
val contact = AContactParser.parse(contactStr)
handle(contact)
} catch {
case e: IOException => println("couldn't load contact file: " + e)
case e: ParseException => println("couldn't parse contact file: " + e)
}
}
The method doWithContact
reads a contact from a file, such as a vCard or similar, and offers it to a parser which converts it to a contact domain object. The contact domain object is then passed to the function literal callback handle
, which does with the contact domain object whatever the function dictates. The doWithContact
method as well as the function literal returns the type Unit
, which is the equivalent of a void
method in Java.
Now we can define various callbacks that can be passed to the doWithContact
method:
val storeCallback = (c:Contact) => ContactDao.save(c)
val sendCallback = (c:Contact) => {
val msgBody = AConverter.convert(c)
RestService.send(msgBody)
}
val combinedCallback = (c:Contact) => {
storeCallback(c)
sendCallback(c)
}
doWithContact("custerX.vcf", storeCallback)
doWithContact("custerY.vcf", sendCallback)
doWithContact("custerZ.vcf", combinedCallback)
The callback can also be passed inline:
doWithContact("custerW.vcf", (c:Contact) => ContactDao.save(c))
Higher-Order Functions in Java 8
The Java 8 equivalent would look very similar - using the current syntax proposal:
public interface Block {
void apply(T t);
}
public void doWithContact(String fileName, Block block) {
try{
String contactStr = FileUtils.readFileToString(new File(fileName));
Contact contact = AContactParser.parse(contactStr);
block.apply(contact);
} catch(IOException e) {
System.out.println("couldn't load contact file: " + e.getMessage());
} catch(ParseException p) {
System.out.println("couldn't parse contact file: " + p.getMessage());
}
}
//usage
doWithContact("custerX.vcf", c -> ContactDao.save(c))
Benefit of Higher-Order Functions
As you can see, functions helped us to cleanly separate the concern of creating a domain object from processing it. By doing so, new ways of handling contact domain objects can easily be plugged in without being coupled to the logic that creates the domain object.
As a result, the benefit we gain from higher-order functions is that our code stays DRY (Don’t Repeat Yourself) so that the programmer can optimally profit from code reuse on a very granular level.
Collections and Higher-Order Functions
Higher-order functions provide a very efficient way of dealing with collections. Because almost every program makes use of collections, efficient collection handling can be a great help.
Filtering Collections: Before and After
Let’s look at a common use case involving collections. Say we want to apply a calculation to every element of a collection. For example, we have a list of Photo objects and we want to filter out all Photos of a certain size.
List input = Arrays.asList(...);
List output = new ArrayList();
for (Photo c : input){
if(c.getSizeInKb() < 10) {
output.add(c);
}
}
This code contains a lot of boilerplate, such as creating the result collection and adding the new elements to the list. An alternative is the use of a Function class, to abstract over function behaviour:
interface Predicate {
boolean apply(T obj);
}
Which results in code written like this using Guava
final Collection input = Arrays.asList(...);
final Collection output =
Collections2.transform(input, new Predicate(){
@Override
public boolean apply(final Photo input){
return input.getSizeInKb() > 10;
}
});
This code reduces the boilerplating somewhat, but it still is messy, verbose code. If we translate this code to Scala or Java 8, we get a taste of the power and elegance of lambda expressions.
Scala:
val photos = List(...)
val output = photos.filter(p => p.sizeKb < 10)
Java 8:
List photos = Arrays.asList(...)
List output = photos.filter(p -> p.getSizeInKb() < 10)
Both of these implementations are elegant, and very succinct. Note that they both make use of type inference: the function parameter p
of type Photo
is not explicitly defined. As you can see in Scala type inference is a standard feature.
Function Chaining in Scala
So far we have saved at least six lines of code and even improved readability. The fun starts when we chain several higher-order functions. To illustrate that, let’s create a Photo
class and add additional properties in Scala:
case class Photo(name:String, sizeKb:Int, rates:List[Int])
Without knowing much Scala, what we have done is declare the class Photo
with three instance variables, name, sizeKb
and rates
. The rates will contain the user ratings for this image, from 1 to 10. Now we can create an instance of the Photo
class, which is done as follows:
val p1 = Photo("matterhorn.png", 344, List(9,8,8,6,9))
val p2 = ...
val photos = List(p1, p2, p3, ...)
With this list of Photos, it is now quite easy to define various queries by chaining multiple higher-order functions after each other. Suppose we have to extract the file names of all the pictures whose file size is greater than 10MB. The first question is how do we transform a list of Photos into a list of filenames? To achieve that we use one of the most powerful higher-order functions, called map:
val names = photos.map(p => p.name)
The map
method transforms each element of a collection to the type defined in the function passed to it. In this example we have a function that receives a Photo
object and returns a String
, which is the filename of the Image.
By using map
we can solve the given task by chaining the map
method after the filter
method:
val fatPhotos = photos.filter(p => p.sizeKb > 10000).map(p => p.name)
We do not have to be afraid of NullPointerExceptions
, because each method (filter, map
, etc.) always returns a collection, which can be empty but never null
. So if the photos collection was empty right from the start, the result of the computation would still be an empty collection.
Function chaining is also referred to as ‘function composition’. Using function composition we can shop in the Collections API to look for the building blocks by which our problem can be solved.
Let’s consider a more advanced example:
Task: "Return the names of all photos whose average rating is higher than 6, sorted by the total amount of ratings given:"
val avg = (l:List[Int]) => l.sum / l.size
val minAvgRating = 6
val result = photos.filter(p => avg(p.ratings) >= minAvgRating)
.sortBy(p => p.ratings.size)
.map(p => p.name)
To achieve this task we rely on the sortBy
method, which expects a function that takes the element type of the collection as input (here Photo
) and returns an object of type Ordered
(here an Int
). Because a List
does not offer an average method, we defined the function literal avg
that calculates the average of the given List
of Ints
for us in the anonymous function passed to the filter
method.
Function Chaining in Java 8
It is not clear yet which higher-order functions the collection classes of Java 8 will offer. Filter
and map
will most probably be supported. Consequently, the first shown chained example will likely appear in Java 8 as follows:
List photos = Arrays.asList(...)
List output = photos.filter(p -> p.getSizeInKb() > 10000)
.map(p -> p.name)
Again, it is remarkable that there is almost no syntactic difference compared to the Scala version.
Higher-order functions in combination with collections are extremely powerful. They are not only very concise and readable, but also save us a lot of boilerplate code, with all the benefits thereof, like fewer tests and fewer bugs. However, there’s more...
Parallel Collections
So far we have not touched one of the most important advantages of higher-order functions in collections. Besides conciseness and readability, higher-order functions add a very important layer of abstraction. In all the previous examples we haven’t seen any loops. Not even once have we had to iterate over a collection to filter, map or sort elements. Iterating is hidden from the user of the collection, or in other words abstracted away.
This additional abstraction layer is key to leveraging multicore platforms, because the underlying implementation of the loop can choose by itself how to iterate over the collection. Consequently, iterating can not only be done sequentially but also in parallel. With the advance of multicore platforms, leveraging parallel processing is no longer a nice-to-have. Today’s programming languages need to be able to cope with the demands of parallel platforms.
In theory, we all could write our own parallel code. In practice that’s not a good idea. First of all, writing robust parallel code, especially with shared state and intermediate results that need to be merged, is extremely hard. Second, ending up with many different implementations wouldn’t be desirable. Even though Java7 has Fork/Join, the problem to decompose and reassemble the data is left to the client, which is not the abstraction level we are looking for. And third, why bother if we already have the answer in the form of functional programming?
Thus, let very smart people write the parallel iteration code once and abstract over their usage by means of higher-order functions.
Parallel Collections in Scala
Let’s look at a simple Scala example, which makes use of parallel processing:
def heavyComputation = "abcdefghijk".permutations.size
(0 to 10).par.foreach(i => heavyComputation)
We first define the method heavyComputation
, which performs - well - a heavy computation. On a quad core laptop this expression takes about 4 seconds to perform. We then instantiate a range collection (0 to 10
) and invoke the par
method. The par
method returns a parallel implementation, which offers the exact same interfaces as its sequential counterpart. Most Scala collection types have such a par
method. From a usage point of view that’s all there is to it.
Let’s see what performance gains we can achieve on a quad core computer. In order to gain insights we reuse the measure method from further above:
//single execution
measure(heavyComputation)
> The execution of this call took: 4.6 s
//sequential execution
measure((1 to 10).foreach(i => heavyComputation))
> The execution of this call took: 46 s
//parallel execution
measure((1 to 10).par.foreach(i => heavyComputation))
> The execution of this call took: 19 s
What might be surprising at first glance is that the parallel execution is only about 2.5 times as fast, even though we are using four cores. The reason is that parallelism comes with the price of additional overhead, in the form of threads that need to be started and intermediate results that need to merged. Therefore, it is not a good idea to use parallel collections by default, but only apply them for known heavy computations.
Parallel Collections in Java 8
In Java 8 the proposed interface for parallel collection again is almost identical to Scala:
Array.asList(1,2,3,4,5,6,7,8,9.0).parallel().foreach(int i ->
heavyComputation())
The paradigm is exactly the same as in Scala, with the sole difference that the method name to create a parallel collection is parallel()
instead of par
.
All at Once: A Larger Example
To recap higher-order functions / lambda expression in combination with parallel collections, let’s look at a larger example in Scala, which combines many of the concepts introduced in the sections above.
For this example we chose a random site, which offers a great variety of beautiful nature wallpapers. We will write a program, that strips all the wallpaper image urls from this page and downloads the images in parallel. Besides the Scala core libraries, we use two other ones, Dispatch for http communication and Apache’s FileUtils, to simplify certain tasks. Be prepared to see some Scala concepts that were not explained in this article, but whose intention should be more or less understandable.
import java.io.File
import java.net.URL
import org.apache.commons.io.FileUtils.copyURLToFile
import dispatch._
import dispatch.tagsoup.TagSoupHttp._
import Thread._
object PhotoScraper {
def main(args: Array[String]) {
val url = "http://www.boschfoto.nl/html/Wallpapers/wallpapers1.html"
scrapeWallpapers(url, "/tmp/")
}
def scrapeWallpapers(fromPage: String, toDir: String) = {
val imgURLs = fetchWallpaperImgURLsOfPage(fromPage)
imgURLs.par.foreach(url => copyToDir(url, toDir))
}
private def fetchWallpaperImgURLsOfPage(pageUrl: String): Seq[URL] = {
val xhtml = Http(url(pageUrl) as_tagsouped)
val imgHrefs = xhtml \\ "a" \\ "@href"
imgHrefs.map(node => node.text)
.filter(href => href.endsWith("1025.jpg"))
.map(href => new URL(href))
}
private def copyToDir(url: URL, dir: String) = {
println("%s copy %s to %s" format (currentThread.getName, url, dir))
copyURLToFile(url, new File(toDir, url.getFile.split("/").last))
}
Code explanation
The scrapeWallpapers
method processes the control flow, which is fetching the image URLs from the html and downloading each of them.
By means of the fetchWallpaperImgURLsOfPage
, all the wallpaper image URLs are screen scraped from the html.
The Http
object is a class of the HTTP dispatch library, which provides a DSL around Apache’s httpclient library. The method as_tagsouped
converts html in xml, which is a built-in datatype in Scala.
val xhtml = Http(url(pageUrl) as_tagsouped)
From the html, in the form of xhtml, we then retrieve the relevant hrefs of the images we want to download:
val imgHrefs = xhtml \\ "a" \\ "@href"
Because XML is native in Scala we can use the xpath-like expression \\ to select the nodes we are interested in. After we have retrieved all hrefs we need to filter out all image URLs and convert the hrefs to URL objects. In order to achieve that, we chain several higher-order functions of Scala’s Collection API like map and filter
. The result is a List of image URLs.
imgHrefs.map(node => node.text)
.filter(href => href.endsWith("1025.jpg"))
.map(href => new URL(href))
The next step is to download each image in parallel. To achieve parallelism, we turn the list of image names into a parallel collection. Consequently, the foreach
method starts several threads to loop through the collection simultaneously. Each thread will eventually call the copyToDir
method.
imgURLs.par.foreach(url => copyToDir(url, toDir))
The copyToDir
method makes use of Apache Common’s FileUtils. The static method copyURLToFile
of the FileUtil
class is statically imported and therefore can be called directly. For clarity we also print the name of the thread that performs the task. When executed in parallel, it will illustrate that multiple threads are busy processing.
private def copyToDir(url: URL, dir: String) = {
println("%s copy %s to %s" format (currentThread.getName, url, dir))
copyURLToFile(url, new File(toDir, url.getFile.split("/").last))
}
This method also illustrates that Scala is fully interoperable with existing Java libraries.
Scala’s functional features, and the resulting benefits such as higher-order functions on collections and the "free" parallelism, make it possible to accomplish parsing, IO and conversion of data in parallel with only a few lines of code.
Virtual Extension Methods/Traits
Virtual extension methods in Java are similar to ‘traits’ in Scala. What are traits, exactly? A trait in Scala offers an interface, and optionally includes an implementation. This structure offers great possibilities. This becomes clear when we start composing classes with traits.
Just like Java 8, Scala doesn’t support multiple inheritance. In both Java and Scala a subclass can only extend a single superclass. With traits however, inheritance is different. A class can "mix in" multiple traits. An interesting fact is that the class gains both the type and all methods and state of the mixed in trait(s). Traits are therefore also called mixins, as they mix in new behaviour and state into a class.
The question that remains is: if traits support some form of multiple inheritance, won’t we suffer from the notorious "diamondproblem"? The answer, of course, is no. Scala defines a clear set of precedence rules that determine when and what is executed within the multiple inheritance hierarchy. This is independent of the number of traits mixed in. These rules provide us with the full benefits of multiple inheritance without any of the problems associated with it.
If Only I Had a Trait
The following example shows a familiar code-snippet for Java developers:
class Photo {
final static Logger LOG = LoggerFactory.getLogger(Photo.class);
public void save() {
if(LOG.isDebugEnabled()) {
LOG.debug("You saved me." );
}
//some more useful code here ...
}
}
Logging is thought of as a cross cutting concern, from a design perspective. However, this is hardly noticeable in the daily practice of Java development. Each class, again and again, declares a logger. We also keep checking whether a log level is enabled, using for example isDebugEnabled
(). This is a clear violation of DRY: Don’t Repeat Yourself.
In Java, there is no way to validate that a programmer declares the log level check or use the proper logger associated with the class. Java developers got so used to this practice, it’s considered a pattern now.
Traits offer a great alternative to this pattern. If we put the logging functionality into a trait, we can mix-in this trait in any class we’d like. This enables access for the class to the cross-cutting concern ‘logging’, without limiting the possibilities of inheriting from another class.
Logging as a Trait, a Solution to the Logging Problem
In Scala, we would implement the Loggable
trait like the following code snippet:
trait Loggable {
self =>
val logger = Slf4jLoggerFactory.getLogger(self.getClass())
def debug[T](msg: => T):Unit = {
if (logger.isDebugEnabled()) logger.debug(msg.toString)
}
}
Scala defines a trait using the keyword ‘trait’
. The body of a trait can contain anything an abstract class is allowed to contain, like fields and methods. Another interesting part in the logging example is the use of self =>
. The logger should log the class that mixes in the Loggable
trait, not Loggable itself. The self =>
syntax, called self-type in Scala, enables the trait to get a reference to the class that mixes it in.
Note the use of the parameterless function msg: => T
as input parameter for the debug method. The main reason why we use the isDebugEnabled()
check is to make sure that the String
that is logged is only computed if the debug level is enabled. So if the debug
method would only accept a String
as input parameter, the log message would always get computed no matter whether the debug loglevel is enabled or not, which is not desirable. By passing the parameterless function msg: => T
instead of a String
however, we get exactly what we want: the msg function that will return the String to be logged is only invoked when the isDebugEnabled
check succeeds. If the isDebugEnabled
check fails the msg
function is never called and therefore no unnecessary String
is computed.
If we want to use the Loggable
trait in the Photo class, we need to use extends
to mix in the trait:
class Photo extends Loggable {
def save():Unit = debug("You saved me");
}
The ‘extends’
keyword gives the impression that Photo
inherits from Loggable
, and therefore cannot extend any other class. This is not the case. The Scala syntax demands that the first keyword for mixing in or extending a class is ‘extends’
. If we want to mix in multiple traits, we have to use the keyword ‘with’
for each trait after the first. We will see more examples using ‘with’
further below.
To show that this actually works, we call the method save()
on Photo
:
new Photo().save()
18:23:50.967 [main] DEBUG Photo - You saved me
Adding more Behaviours to your Classes
As we discussed in the previous paragraph, a class is allowed to mix in multiple traits. So besides logging, we can also add other behaviour to the Photo
class. Let’s say we want to be able to order Photos
based on their file size. Luckily for us, Scala offers a number of traits out of the box. One of these traits is the Ordered[T]
trait. Ordered
is similar to the Java interface Comparable
. The big, and very important, difference is that the Scala version offers an implementation as well:
class Photo(name:String, sizeKb:Int, rates:List[Int]) extends Loggable with
Ordered[Photo]{
def compare(other:Photo) = {
debug("comparing " + other + " with " + this)
this.sizeKb - other.sizeKb
}
override def toString = "%s: %dkb".format(name, sizeKb)
}
In the example above, two traits are mixed in. Besides the previously defined Loggable
trait, we also mix in the Ordered[Photo]
trait. The Ordered[T]
trait requires the implementation of the compare(type:T)
method. This is still very similar to Java’s Comparable
.
Besides the compare
method, the Ordered
trait also offers a number of different methods. These methods can be used to compare objects in various ways. They all make use of the implementation of the compare method.
val p1 = new Photo("Matterhorn", 240)
val p2 = new Photo("K2", 500)
p1 > p2
> false
p1 <= p2
> true
Symbolic names like > and <= etc. are not special reserved keywords in Scala like in Java. The fact that we can compare objects using >, <= etc. is due to the Ordered
trait that implements the methods with these symbols.
Classes that implement the Ordered
trait can be sorted by every Scala collection. By having a collection populated with Ordered
objects, ‘sorted’
can be called on the collection, which sorts the objects according to the order defined in the compare
method.
val p1 = new Photo("Matterhorn", 240)
val p2 = new Photo("K2", 500)
val sortedPhotos = List(p1, p2).sorted
> List(K2: 500kb, Matterhorn: 240kb)
The Benefits of Traits
The above examples reveal that we are able to isolate generic functionality in a modular fashion using traits. We can plug the isolated functionality in every class if needed. To equip the Photo
class with logging functionality, we mixed in the Loggable
trait, for ordering the Ordered
trait. These traits are reusable in other classes.
Traits are a powerful mechanism to create modular and DRY (Don’t Repeat Yourself) code using built-in language features, without having to rely on extra technology complexity like aspect oriented programming.
The Motivation for Virtual Extension Methods
The Java 8 specification defines a draft for virtual extension methods. Virtual extension methods will add default implementations to new and/or existing methods of existing interfaces. Why is that?
For many existing interfaces it would be very beneficial to have support for lambda expressions in the form of higher-order functions. As an example, let’s consider the java.util.Collection
interface. It would be desirable for the java.util.Collection
interface to provide a forEach(lambdaExpr)
method. If such a method was added to the interface without a default implementation, all implementing classes would have to provide one. It is self-evident that this would lead to a very challenging compatibility hell.
That’s why the JDK team came up with virtual extension methods. With this feature a forEach
method, for example, could be added to java.util.Collection
including a default implementation. Consequently, all implementing classes will automatically inherit this method and its implementation. By doing so, their API is able to evolve in a completely non-intrusive way, which is exactly what virtual extension methods intend to achieve. If the implementing class is not satisfied with the default implementation, it simply can override it.
Virtual Extension Methods vs Traits
The primary motivator for virtual extension methods is API evolution. A welcome side effect is that they offer a form of multiple inheritance, which is limited to behaviour. Traits in Scala not only provide multiple inheritance of behaviour, but also of state. Besides state and behaviour inheritance, traits offer a means to get a reference to the implementing class, shown with the ‘self‘
field in the Loggable
example.
From a usage point of view, traits offer a richer set of features than virtual extension methods. However, their motivation is different: in Scala traits were always intended as modular building blocks that offer multiple inheritance ‘without the issues’, whereas virtual extension methods primarily have to enable API evolution and in the second place ‘multiple behaviour inheritance’.
The Loggable and Ordered Traits in Java 8
In order to see what we can achieve with virtual extension methods, let’s try to implement the Ordered
and Loggable
trait in Java 8.
The Ordered
trait could be fully implemented with virtual extension methods, because no state is involved. As mentioned the counterpart of Scala’s Ordered
trait is java.lang.Comparable
in Java. The implementation would look as follows:
interface Comparable {
public int compare(T that);
public boolean gt(T other) default {
return compare(other) > 0
}
public boolean gte(T other) default {
return compare(other) >= 0
}
public boolean lt(T other) default {
return compare(other) < 0
}
public boolean lte(T other) default {
return compare(other) <= 0
}
}
We added new comparison methods to the existing Comparable
interface (‘greater than’, ‘greater than or equals’, ‘less than’, ‘less than or equals’ identical to those found in the Ordered trait: >, >=, <, <=). The default implementation, marked with the default
keyword, forwards all calls to the existing abstract compare method. The result is that an existing interfaces is enriched with new methods, without the need for classes implementing Comparable
to implement these methods as well. The Ordered
trait in Scala looks quite similar to this implementation.
If
the Photo
class implemented Comparable
, we also would be able to perform comparison operations with those newly added methods:
Photo p1 = new Photo("Matterhorn", 240)
Photo p1 = new Photo("K2", 500)
p1.gt(p2)
> false
p1.lte(p2)
> true
The Loggable
trait could not be fully implemented with virtual extension methods, but almost:
interface Loggable {
final static Logger LOG = LoggerFactory.getLogger(Loggable.class);
void debug(String msg) default {
if(LOG.isDebugEnabled()) LOG.debug(msg)
}
void info(String msg) default {
if(LOG.isInfoEnabled()) LOG.info(msg)
}
//etc...
}
In this example, we add log methods, like debug, info
etc., to the Loggable
interface, that by default delegate their calls to the static Logger
instance. What we miss is a means to get a reference to the implementing class. Because this mechanism is lacking, we use the Loggable
interface as logger, which would log all statements under Loggable instead of the implementing class. Because of this limitation, virtual extension methods are less suitable for such a usage scenario.
To sum up, traits and virtual extension methods both provide multiple inheritance for behaviour. Traits also provide multiple inheritance for state and a means to acquire a handle to the implementing class.
Conclusion
Java 8 is going to provide a variety of new language features, with the potential to fundamentally change the way we write applications. Especially the functional programming constructs, such as lambda expressions, can be considered a paradigm shift. This shift provides great new possibilities for more concise, compact and easy to understand code.
In addition, lambda expressions are key to enabling parallel processing.
As explained in this article all these features are already available in Scala. Developers who want to try them out can explore early builds of Java 8 on most platforms. Alternatively, we recommend taking a look at Scala as a way of preparing for the paradigm shifts to come.
Appendix
Information on Java 8 mainly stems from the following presentations:
Java 7 and 8: WhereWe'veBeen, WhereWe'reGoing
VirtualExtensionMethods