BT

Facilitating the Spread of Knowledge and Innovation in Professional Software Development

Write for InfoQ

Topics

Choose your language

InfoQ Homepage Articles How Functional is Java 8?

How Functional is Java 8?

Leia em Português

Bookmarks

There's been a lot of talk about how "Java 8 is bringing Functional Programming to Java" - but what does that sentence really mean?

In this article, I'll discuss what it means for a language, or a programming style, to be functional. By looking at the evolution of Java - particularly its type system, we can see how the new features of Java 8, especially lambda expressions, change the landscape, and provide some key benefits of the functional style - before tackling the question: "How Functional is Java 8?"

What is a functional programming language?

At its heart, a functional programming language is one that deals with code in the same way as data. This means that a function should be a first-class value and able to be assigned to variables, passed to functions and so forth.

In fact, many functional languages go even further than this, and see computation and algorithms as more fundamental than the data they operate on. Some of these languages want to disentangle program state from functions (in a way that seems at odds with the desire of object-oriented languages that usually want to bring them closer together).

An example would be the Clojure programming language. Despite running on top of the class-based Java Virtual Machine, Clojure is fundamentally a functional language, and doesn't directly expose classes and objects in the high-level source language (although good interoperability with Java is provided).

A Clojure function, such as the log-processing function shown below, is a first-class citizen, and doesn’t need to be bundled up in a class to exist.

(defn build-map-http-entries [log-file]
(group-by :uri (scan-log-for-http-entries log-file)))

Functional programming is most useful when programs are written in terms of functions that always return the same output for a given input (regardless of any other state present in the running program) and that do not cause  any other effects or change any program state. Functions that obey this are sometimes called "pure" functions, and they behave in the same way that mathematical functions do.

The great advantage that pure functions have is that they are much easier to reason about because their operation does not depend on external state. Functions can easily be combined together - and this can be seen in developer workflow styles such as the REPL (Read, Execute, Print, Loop) style common to Lisp dialects and other languages with strong functional heritage.

Functional Programming in Non-FP languages

Whether a language is functional or not is not a binary condition - instead, languages exist on a spectrum. At the extreme end are languages that basically enforce functional programming, often by prohibiting mutable data structures. Clojure is one example of a language that does not permit mutable data in the accepted sense.

However, there are other languages in which it is common to write programs in a functional style, despite the language not enforcing this. An example would be Scala, which is a blend of object-oriented and functional languages. It permits functions as values, such as:

val sqFn = (x: Int) => x * x

whilst retaining class and object syntax that is very close to that of Java.

At the other extreme it is, of course, possible to write functional programs in completely non-functional languages, such as C, provided that suitable programmer discipline and conventions are maintained.

With this in mind, functional programming should be seen as a function of two factors - one of which is relevant to programming languages, and one to programs written in that language:

1) To what extent does the underlying programming language support, or enforce functional programming?

2) How does this particular program make use of the functional features provided by the language? Does it avoid non-functional features such as mutable state?

Java - some history

Java is an opinionated language - it has been optimized for readability, for accessibility to junior programmers and for long-term stability and supportability. These design decisions have come at a cost - in verbosity, and in a type system that can sometimes seem inflexible when compared to other languages.

However, Java's type system has evolved, albeit relatively slowly, over the history of the language. Let's take a look at some of the forms that it has assumed over the years:

Java's original type system

Java's original type system is now well over 15 years old. It is simple and clear - types are either reference types or primitive types. Reference types are classes, interfaces or arrays.

  • Classes are the heart of the Java platform - a class is the basic unit of functionality that the Java platform will load, or link - and all code that is intended for execution must live inside a class.

  • Interfaces can't be instantiated directly, and instead a class must be defined that implements the API defined by the interface.

  • Arrays hold either primitive types, or instances of classes, or other arrays.

  • The primitive types are all defined by the platform, and new ones can't be defined by the programmer.

From the very earliest days, Java's type system has been insistent on a very important point - every type must have a name that it can be referred by. This is known as "nominative typing" - and Java is a strongly nominatively typed language.

Even the so-called "anonymous inner classes" still have a type by which the programmer must refer to them - the type of the interface that they implement:

Runnable r = new Runnable() { public void run() { System.out.println("Hello World!"); } };

Another way of saying this is that every value in Java is either a primitive or an instance of some class.

Alternatives to Named Types

Other languages do not have this fascination with named types. For example, Java has no equivalent of Scala's concept of a type that implements a specific method (of a specific signature). In Scala, this would be written:

x : {def bar : String}

Remember that Scala indicates the type of a variable on the right (after the : ) so this is read as something like "x is of a type that has a method called bar that returns String". We could use this to define a Scala method like this:

def showRefine(x : {def bar : String}) = { print(x.bar) }

and then, if we define a suitable Scala object like this:

object barBell { def bar = "Bell" }

then calling showRefine(barBell) does the expected thing:

showRefine(barBell) Bell

This is an example of refinement typing. Programmers coming from dynamic languages may be familiar with "duck typing". Structural refinement typing is similar, except that duck typing ("if it walks like a duck, and quacks like a duck, it's a duck") is about the runtime types, whereas these structural refinement types work at compile time.

In languages that fully support structural refinement typing, these refined types can be used anywhere the programmer might expect - such as the type of a parameter to a method). Java, by contrast, does not support this sort of typing (apart from a couple of slightly bizarre edge cases).

The Java 5 type system

The release of Java 5 brought three major new features to the type system - enums, annotations and generic types.

  • Enumerated types (enums) are similar to classes in some respects, but they have the property that only a specified number of instances may exist, and each instance is specified in the class description and distinct. Intended primarily as a "typesafe constant" rather than the then-common practice of using small integers for constants, the enum construction also permits additional patterns that are sometimes extremely useful.

  • Annotations are related to interfaces - the keyword to declare one is @interface - with the initial @ indicating that this is an annotation type. As the name suggests, they're used to annotate elements of Java code with additional information that doesn't affect behavior. Previously, Java had made use of "marker interfaces" to provide a limited form of this metadata, but annotations are considerably more flexible.

  • Java's generics provide parameterized types - the idea that one type can act as a "container" for objects of another type, without regard for the specifics of exactly which type is being contained. The type that fits into the container is often called the type parameter.

Of the features introduced by Java 5, enums and annotations provide new forms of reference type which require special treatment by the compiler, and which are effectively disjoint from the existing type hierarchies.

Generics provide significant additional complexity to Java's type system - not least because they are purely a compile-time feature. This requires the Java developer to be mindful of both a compile-time and a runtime type system that are slightly different from each other.

Despite these changes, Java's insistence on nominative typing remained. Type names now include List<String> (read as: "List-of-String") and Map<Class<?>, CachedObject> ("Map-of-Class-of-Unknown-Type-to-CachedObject"), but these are still named types, and every non-primitive value is still an instance of a class.

Features introduced in Java 6 & 7

Java 6 was essentially a performance and library enhancement release. The only change to the type system was an expansion of the role of annotations, and the release of a capability for pluggable annotation processing. This did not impact most Java developers, and does not really provide for pluggable type systems in Java 6.

Java 7 did not materially change the type system. The only new features, all of them very minor, are:

  • Small improvements in type inference in the javac compiler.
  • Signature polymorphic dispatch - used as an implementation detail for the feature called method handles - which are in turn used to implement lambda expressions in Java 8.
  • Multi-catch provides some small traces of "algebraic data types" - but these are purely internal to javac and are not of any real consequence to the end-user programmer.

The Java 8 type system

Throughout its history, Java has been essentially defined by its type system. It is central to the language and has maintained a strict adherence to nominative typing. From a practical point of view, the Java type system did not change much between Java 5 and Java 7.

At first sight, we might expect Java 8 to change that. After all, a simple lambda expression appears to remove us from nominative typing:

() -> { System.out.println("Hello World!"); }

This is a method, without a name, that takes no parameters and returns void. It's still perfectly statically typed, but is now anonymous.

Have we escaped the Kingdom of the Nouns? Is this actually a new form of type for Java?

The answer is, perhaps unfortunately, no. The JVM, on which Java and other languages run, is very strictly tied to the concept of classes. Classloading is central to the Java platform's security and verification modes. Simply put, it would be very, very difficult to conceive of a type that was not, in some way, represented through a class.

Instead of creating a new kind of type, Java 8 lambda expressions are auto-converted by the compiler to be an instance of a class. The class that they are an instance of is determined by type inference. For example:

Runnable r = () -> { System.out.println("Hello World!"); };

The lambda expression on the right hand side is a perfectly good Java 8 value - but its type is inferred from the value on the left - so it is actually a value of type Runnable. Note however that if a lambda expression is used in an incorrect way, a compiler error will result. Nominative typing is still the Java way, and even the introduction of lambdas has not changed that.

How Functional is Java 8?

Finally, let's turn to the question we posed at the start of the article - "How Functional is Java 8?"

Before Java 8, if a developer wanted to write in a functional style, he or she would have to use nested types (usually anonymous inner classes) as a stand-in for function literals. The default collections libraries would not do the code any favors, and the curse of mutability would be ever-present.

Java 8's lambda expressions do not magically transform it into a functional language. Instead, their effect is to create a still-imperative, still strongly nominatively type language that has better syntax support for lambda expressions as function literals. Simultaneously, the enhancements to the collections libraries have allowed Java developers to start adopting simple functional idioms (such as filter and map) to tidy up otherwise unwieldy code.

Java 8 required the introduction of some new types to represent the basic building blocks of functional pipelines - interfaces such as Predicate, Function and Consumer, in java.util.function. These additions make Java 8 capable of "slightly functional programming" - but Java's need to represent them as types (and their location in a utility package, rather than the language core) speaks to the stranglehold that nominative typing has on the Java language, and how far the language is from the purity of Lisp dialects or other functional languages.

Despite all the above, this small subset of the power of functional languages may well be all that most developers actually need for their day-to-day development. For power users, other languages (on the JVM and elsewhere) still exist, and will doubtless continue to thrive.

About the Author

Ben Evans is the CEO of jClarity, a Java/JVM performance analysis startup. In his spare time he is one of the leaders of the London Java Community and holds a seat on the Java Community Process Executive Committee. His previous projects include performance testing the Google IPO, financial trading systems, writing award-winning websites for some of the biggest films of the 90s, and others.

Rate this Article

Adoption
Style

BT