In a keynote address that I once co-presented with Martin Fowler, he made a perceptive observation:
The legacy of Java will be the platform, not the language.
The original engineers of Java technology made a brilliant decision to separate the language from the runtime, ultimately enabling more than 200 languages to run on the Java platform. This architecture is crucial for the platform's long-term vitality, because computer programming languages typically have short lifespans. Since 2008, the annual JVM Language Summit, hosted by Oracle, has given implementers of alternative languages on the JVM an opportunity to collaborate openly with Java platform engineers.
Welcome to the Java.next column series. In it, I profile three modern JVM languages — Groovy, Scala, and Clojure — that offer an interesting mix of paradigm, design choice, and comfort factor. I won't spend time here providing deep descriptions of each language; those are available on their respective websites (see Resources). But language-community websites — whose primary purpose is evangelism — lack objective information or examples of tasks a language is ill-suited for. The substantive comparisons I'll perform in this series are intended to help fill that void. This article sets the stage with an overview of the Java.next languages and the benefits of learning about them.
Beyond Java
The Java language rose to prominence through what Bruce Tate, in his book Beyond Java (see Resources), calls a perfect storm: the combined factors of the rise of the web, the unsuitability of existing web technologies for various reasons, and the rise of multitiered application development by enterprises. Tate also argues that this perfect storm was a unique series of events, and that no other language will ever rise to the same relative prominence in the same way.
The Java language has proved quite elastic in capabilities, but its syntax and inherent paradigms have long-understood limitations. Despite the promising changes that are coming to the language, the syntax simply can't support some important future goals, such as elements of functional programming. But if you're trying to find a single new language to displace Java, you're searching for the wrong thing.
Polyglot programming
Polyglot programming — a term that I resurrected and repopularized in a 2006 blog entry (see Resources) — is based on the realization that no single language is suitable for solving every problem. Some languages have built-in perspectives or features that fit specific problems better. For example, as sophisticated as Swing is, developers find it incredibly cumbersome to write Swing UIs in Java, because it requires type declarations, clumsy anonymous inner classes for behavior, and other friction. Using a language that's better-suited to building UIs, such a Groovy with its SwingBuilder
facilities (see Resources), makes building Swing applications much more palatable.
The proliferation of languages run on the JVM makes the idea of polyglot programming all
the more compelling, because you can mix and match while maintaining the same underlying byte code and libraries. For example, SwingBuilder
doesn't replace Swing; it layers atop the existing Swing API. Of course, for a long time, developers have been mixing languages outside the JVM — for example, by using SQL and JavaScript for specialized purposes — but it is becoming more prevalent within the JVM boundaries. Many ThoughtWorks projects incorporate multiple languages, and all the tools developed by ThoughtWorks Studios use mixed languages.
Even if Java remains your primary development language, learning how alternative languages work enables you to incorporate them strategically. Java will remain an important part of the JVM ecosystem, but ultimately more as the platform's assembly language — a place you go purely for performance reasons or to meet specialized requirements.
Evolution
In the early 1980s, when I was in university, we used a development environment called Pecan Pascal. Its unique feature was that the same Pascal code could run on either the Apple II or IBM PC. The Pecan engineers achieved this feat by using something mysterious called "byte code." Developers compiled their Pascal code to this "byte code," which ran on a "virtual machine" written natively for each platform. It was a hideous experience! The resulting code was achingly slow even for simple class assignments. The hardware at the time just wasn't up to the challenge.
A decade after Pecan Pascal, Sun released Java using the same architecture, straining but succeeding in mid-1990s hardware environments. It also added other developer-friendly features such as automatic garbage collection. Having worked in languages like C++, I never want to code in a non-garbage-collected language again. I'd rather spend time at a higher level of abstraction thinking about ways to solve complex business problems, not complicated plumbing problems like memory management.
One of the reasons that computer languages typically don't have long lives is the rate of innovation in language and platform design. As our platforms have become more powerful, they can handle more busywork. For example, Groovy's memoization feature (added in 2010) caches the results of function calls. Rather than hand-writing caching code, potentially introducing bugs, you need merely call the memoize()
method, as shown in Listing 1:
Listing 1. Memoizing a function in Groovy
def static sum = { number -> factorsOf(number).inject(0, {i, j -> i + j}) } def static sumOfFactors = sum.memoize()
In Listing 1, the results from the sumOfFactors
method are automatically cached. You can also customize the caching behavior with alternative methods, memoizeAtLeast()
and memoizeAtMost()
. Clojure also includes memoization, and it's trivial to implement in Scala. Advanced features such as memoization that exist in next-generation languages (and in some Java frameworks) will gradually find their way into the Java language. The next release of Java will add higher-order functions, making memoization much easier to implement. By studying next-generation Java languages, you get a sneak peak into future Java features.
Groovy, Scala, Clojure
Groovy is twenty-first-century Java syntax — espresso instead of regular coffee. Groovy's design goals are to update and remove friction from Java's syntax while supporting the primary paradigms in the Java language. Thus, Groovy "knows" about such things as JavaBeans, simplifying property access. Groovy incorporates new features at a rapid rate, including some important functional features I'll highlight in future installments. Groovy is still primarily an object-oriented, imperative language. Two fundamental differences from Java are that Groovy is dynamically rather than statically typed, and that its metaprogramming capabilities are significantly better.
Scala is a language designed from the ground up to take advantage of the JVM, but with a completely redesigned syntax. Scala is a strongly, statically typed language — more stringently typed than Java yet less obnoxious about it — that supports both object-oriented and functional paradigms, favoring the latter. For example, Scala prefers val
declarations, yielding an immutable variable (similar to flagging a parameter as final
in Java) to var
s, which create more-familiar mutable variables. Scala provides a bridge from what you likely are (an object-oriented imperative programmer) to what you likely should be (a more functional programmer) by supporting both paradigms deeply.
Clojure is the most radical syntactic departure from the other languages, being a dialect of Lisp. A strongly, dynamically typed language (like Groovy), it reflects opinionated design decisions. Although Clojure allows you full and deep interoperability with legacy Java, it isn't trying to build a bridge from the older paradigm. For example, Clojure is unapologetically functional and supports object orientation to allow interoperability. Yet it supports all the features that object-oriented programmers are accustomed to, such as polymorphism — but in a functional rather than object-oriented manner. Clojure is designed around a few core engineering principles, such as Software Transactional Memory, that break old paradigms in favor of new capabilities.
Paradigms
Outside of syntax, the most interesting differences among these languages concern typing and the underlying primary paradigm: functional or imperative.
Static vs. dynamic typing
Static typing in programming languages refers to explicit type declarations, such as Java's int x;
declaration. Dynamic typing refers to languages that don't require type information for declaration. All the languages under consideration are strongly typed languages, meaning that your code can reflect on the type after assignment.
Java's type system has been widely criticized as providing many of the inconveniences of static typing without enough of the benefits. For example, before the limited type inferencing currently available, Java required developers to repeat type declarations on both sides of assignments. Scala is more statically typed than Java but much less cumbersome in daily use, because it makes heavy use of type inferencing.
Groovy has one behavior that at first glance seems to bridge the static vs. dynamic divide. Consider the simple collection factory shown in Listing 2:
Listing 2. Groovy collection factory
class CollectionFactory { def List getCollection(description) { if (description == "Array-like") new ArrayList() else if (description == "Stack-like") new Stack() } }
The class in Listing 2 acts as a factory, returning either of the two List
interface implementers — ArrayList
or Stack
— based on the passed description
parameter. To Java developers, this appears to ensure that whatever is returned meets that contract.
However, the two unit tests in Listing 3 reveal a complication:
Listing 3. Testing collection types in Groovy
@Test void test_search() { List l = f.getCollection("Stack-like") assertTrue l instanceof java.util.Stack l.push("foo") assertThat l.size(), is(1) def r = l.search("foo") } @Test(expected=groovy.lang.MissingMethodException.class) void verify_that_typing_does_not_help() { List l = f.getCollection("Array-like") assertTrue l instanceof java.util.ArrayList l.add("foo") assertThat l.size(), is(1) def r = l.search("foo") }
In the first unit test in Listing 3, I retrieve a Stack
via the factory, verify that it is indeed a Stack
, then perform Stack
-like operations such as push()
, size()
, and search()
. However, in the second unit test, I must guard the test with a MissingMethodException
expected exception to pass the test. When I retrieve my Array-like
collection into a variable typed as a List
, I can verify that I do indeed receive a list in return. However, when I try to call the search()
method, it triggers an exception because ArrayList
doesn't include a search()
method. Thus, the declaration provided no compile-time protection ensuring that the method invocation was correct.
Although this might seem like a bug, it's proper behavior. Types in Groovy only ensure the validity of the assignment statement. For example, in Listing 3, if I returned something that didn't implement the List
interface, a runtime exception (GroovyCastException
) would be triggered. This places Groovy firmly in the strongly, dynamically typed family with Clojure.
However, recent changes to the language have made the static vs. dynamic divide in Groovy even blurrier. Groovy 2.0 added a @TypeChecked
annotation, which lets you make ad hoc decisions about the strictness of type checking, at the class or method level. Listing 4 illustrates this annotation:
Listing 4. Type checking via annotation
@TypeChecked @Test void type_checking() { def f = new CollectionFactory() List l = f.getCollection("Stack-like") l.add("foo") def r = l.pop() assertEquals r, "foo" }
In Listing 4, I add the @TypeChecked
annotation, which verifies both assignment and subsequent method invocation. For example, the code in Listing 5 will no longer compile:
Listing 5. Type checking preventing invalid method invocation
@TypeChecked @Test void invalid_type() { def f = new CollectionFactory() Stack s = (Stack) f.getCollection("Stack-like") s.add("foo") def result = s.search("foo") }
In Listing 5, I must add the type cast for the return from the factory to allow me to call Stack
's search()
method. This facility comes with restrictions: Many of Groovy's dynamic features won't work when static typing is enabled. However, this example illustrates Groovy's continuing evolution in bridging the static vs. dynamic divide.
All of these languages have quite powerful metaprogramming facilities, so more-stringent typing can be added after the fact. For example, several side projects exist to add selective typing to Clojure. Generally, though, if selective typing is optional, it's not part of the type system; it's a verification mechanism.
Imperative vs. functional
The other main comparison axis is imperative vs. functional. Imperative programming focuses on step-by-step instructions, in many cases mimicking ancient low-level hardware conveniences. Functional programming focuses more on functions as first-class constructs and tries to minimize state transitions and mutability.
Groovy, being heavily inspired by Java, is primarily an imperative language. But from the outset, many functional language features were included, and more have been added over time.
Scala bridges these two paradigms, supporting both. While preferring (and encouraging) functional programming, Scala still supports object-orientation and imperative programming. Thus, using Scala properly requires a disciplined team to ensure that you don't mix and match paradigms haphazardly, which is always a danger in multiparadigm languages.
Clojure is unapologetically functional. It supports object orientation to allow it to easily interact with other JVM languages, but it's not trying to be a bridge. Rather, the opinionated design of Clojure makes a statement about what the designer thinks are good engineering practices. Those decisions have far-reaching implications, enabling Clojure to solve some nagging problems in the Java world (such as concurrency) in groundbreaking ways.
Many of the shifts in thinking required to learn these new languages will come from this imperative/functional divide, and it's one of the richest areas of exploration in this series.
Conclusion
Developers live in an increasingly polyglot world where multiple languages are employed to solve problems. Learning to leverage new languages effectively helps you determine when this approach is appropriate. Even if you don't leave Java, it will gradually incorporate features from next-generation JVM languages; looking at them now gives you a sneak peak at the future of the Java language.
In the next Java.next installment, I'll start the comparison by looking at what Groovy, Scala, and Clojure all have in common.
Resources
Learn
- Java.next: Common ground in Groovy, Scala, and Clojure, Part 1 (Neal Ford, developerWorks, March 2013): Address the inability to overload operators in the Java language with the Java.next languages: Groovy, Scala, and Clojure).
- Java.next: Common ground in Groovy, Scala, and Clojure, Part 2 (Neal Ford, developerWorks, April 2013): Reduce boilerplate and complexity with the syntactic conveniences offered by Java.next languages.
- Java.next: Common ground in Groovy, Scala, and Clojure, Part 3 (Neal Ford, developerWorks, May 2013): Compare Groovy, Scala, and Clojure improvements for exceptions, statements versus expressions, and edge cases around
null
code. - Groovy: Groovy is a dynamic language for the JVM. Groovy builders such as
SwingBuilder
simplify the building of complex objects. - Scala: Scala is a modern, functional language on the JVM.
- Clojure: Clojure is a modern, functional Lisp that runs on the JVM.
- Beyond Java (Bruce Tate, O'Reilly Media, 2005): Tate's book discusses many of the shortcomings of Java and its ecosystem.
- Polyglot Programming: Read Neal Ford's 2006 blog entry.
- Explore alternative languages for the Java platform: Follow this knowledge path to investigate developerWorks content about various alternative JVM languages. It includes an excellent podcast interview with Dr. Venkat Subramaniam that discusses the merits of polyglot programming.
- Language designer's notebook: In this developerWorks series, Java Language Architect Brian Goetz explores some of the language design issues that have presented challenges for the evolution of the Java language in Java SE 7, Java SE 8, and beyond.
- Functional thinking: Explore functional programming in Neal Ford's column series on developerWorks.
- Browse the technology bookstore for books on these and other technical topics.
- developerWorks Java technology zone: Find hundreds of articles about every aspect of Java programming.
Get products and technologies
- Download IBM product evaluation versions or explore the online trials in the IBM SOA Sandbox and get your hands on application development tools and middleware products from DB2®, Lotus®, Rational®, Tivoli®, and WebSphere®.
Discuss
- Check out developerWorks blogs and get involved in the developerWorks community.
Comments
Dig deeper into Java technology on developerWorks
- Overview
- New to Java programming
- Technical library (articles and more)
- Forums
- Blogs
- Communities
- Downloads and products
- Open source projects
- Standards
- Events
BlueMix Developers Community
Get samples, articles, product docs, and community resources to help build, deploy, and manage your cloud apps.
developerWorks Labs
Experiment with new directions in software development.
DevOps Services
Software development in the cloud. Register today to create a project.
IBM evaluation software
Evaluate IBM software and solutions, and transform challenges into opportunities.