Whenever I tell people I’ve been working with Java I get the same reaction:
“Yuck! Java? Why Java?”
And, admittedly, I had the same reaction — at first. But over time, I’ve come to appreciate Java for its type safety, performance, and rock-solid tooling. I’ve also come to notice that this isn’t the Java I was used to — it’s been steadily improving over the last ten years.
Why Java?
Assuming you haven’t thrown up a little at the thought of using Java in your day-to-day life, let me reiterate that this isn’t the Java you are used to. While Python, Ruby, and Javascript have made waves during the “Dynamic-Typing Revolution”™ (I made that up), Java has quietly been adopting some of the best practices that make dynamic and functional languages so appealing, without sacrificing the many hundred man-years of effort that have made Java and the JVM a world-class development environment. Java is still the world’s most popular programming language with roughly 9 million programmers using Java. It doesn’t make sense to ignore all of that history and development effort because Java is a little too verbose. But just because it’s popular, doesn’t mean it’s right. So, let’s take a moment to look at what makes Java great.
The JVM
The Java Virtual Machine (JVM) has been around for 20 years. In that time it has been deployed in thousands of production systems and received innumerable bug-fixes and performance improvements. The advantages of the JVM’s legacy surface in a number of ways. First, the JVM has excellent support for production logging and production monitoring, allowing you to easily monitor performance metrics down to an individual thread. The JVM also has one of the most highly-optimized garbage collectors in the world and allows you to swap garbage collection algorithms depending on factors such as optimizing throughput. Lastly, the “write once, run anywhere” promise of Java has actually come true — you can easily deploy a Java application on any number of architectures (let’s just all agree that applets never happened). There is a reason very smart people writing new languages like Scala and Clojure have adopted the JVM as their runtime — it provides an unparallelled distribution environment for your code. It just doesn’t make sense to abandon something as rock-solid as the JVM.
Library Support
If you need to get something done, chances are there is a highly useful and well tested Java library to do it for you. Java libraries are generally mature and tuned for actual production usage. Google, Amazon, LinkedIn, Twitter and many foundational Apache projects are heavily committed to Java. By adopting Java you can lean on these libraries and companies to take advantage of the great engineering effort people have already made.
Type Safety
Java’s type system, while verbose at times, allows you to write code that largely “just works”. No more run-time debugging. It allows you to lean on your compiler rather than on unit tests — which are only effective if you already know what bugs you have. Type safety also allows effortless refactoring. Java also supports generics, overcoming one of the chief complaints about Go. Furthermore, libraries such as Guava have standardised some of the best-practices of creating type safe APIs with minimal boilerplate and overhead. Improvements to the Java compiler also mean that you can enjoy type safety while minimizing the amount of boilerplate code required for generics.
Concurrency
Sadly, this sums up the state of concurrency in most dynamic languages.
Most JS/Python/Ruby apps... pic.twitter.com/hkDkjdxpFH
— Reuben Bond (@reubenbond) November 5, 2015
Java, on the other hand, has first class support for multi-threading and concurrency. As of Java 1.7, many concurrent immutable data structures allow you to easily share data among threads. The Akka library takes this a step further, giving you Erlang style Actors for writing concurrent and/or distributed code. I’m not going to claim that Java has a better concurrency story than Go, but being able to manage individual threads gives Java applications asynchronous performance that isn’t possible with Python.
Programming Modern Java
At this point you might have moved from mildly nauseated to mildly curious. So, how can we write Java in 2015? Where to start? First, let’s revisit some of the core language concepts that have been re-imagined in Java 7 and 8.
Iteration
Let’s start with iteration. What if I told you that this is a for
loop in Java 8?
List<String> names = new LinkedList<>(); // compiler determines type of LinkedList
// ... add some names to the collection
names.forEach(name -> System.out.println(name));
Or that the for
keyword has been vastly simplified?
for (String name : names)
System.out.println(name);
Each of these looping constructs makes Java less verbose than the traditional
for
loop you may be used to seeing.
Lambda Functions
The first for
loop above also introduces a new concept to Java: lambda
functions. Lambda functions, denoted by the ->
syntax, are a significant
change to the language and introduce some concepts from functional programming.
Some examples of Lambda functions in Java include the following.
// Lambda Runnable
Runnable r2 = () -> System.out.println("Hello world two!");
// Lambda Sorting
Collections.sort(personList, (Person p1, Person p2) -> p1.getSurName().compareTo(p2.getSurName()))
// Lambda Listener
testButton.addActionListener(e -> System.out.println("Click Detected by Lambda Listener"));
Lambda functions are too broad a topic to cover fully here — this article provides a great starting point for learning more.
Streams
Java 8 also introduces the concept of streams, bringing many of the design features of modern functional languages to Java. Streams are a mechanism for lazily evaluating a sequence of transformations on a collection. As an example, let’s try to count all the names that start with ‘A’. A first approach may be something like the following:
List<String> names = new LinkedList<>();
// ... add some names to the collection
long count = 0;
for (String name : names) {
if (name.startsWith("A"))
++count;
}
With streams, this can be simplified using functional syntax by first converting your collection to a stream and then applying functions:
List<String> names = new LinkedList<>();
// ... add some names to the collection
long count = names.stream()
.filter(name -> name.startsWith("A"))
.count();
Java also supports parallel processing of streams using the parallelStream()
method. Parallel streams allow pipeline operations to be executed concurrently
in separate threads, offering not only improved syntax but improved performance
as well. In most cases, you can simply replace stream()
with
parallelStream()
to achieve parallelism.
Try-With-Resources
Prior to Java 6, opening a File and then reading from it would be done with a
try/finally
block.
static String readFirstLineFromFileWithFinallyBlock(String path)
throws IOException {
BufferedReader br = new BufferedReader(new FileReader(path));
try {
return br.readLine();
} finally {
if (br != null) br.close();
}
}
Unfortunately, it is possible for both readLine
and close
to throw
exceptions. In this case, the exception thrown from readLine
is suppressed and
we never actually know that readLine
failed.
Java 7 introduced the try-with-resources construct as a means to overcome this deficiency.
static String readFirstLineFromFile(String path) throws IOException {
try (BufferedReader br =
new BufferedReader(new FileReader(path))) {
return br.readLine();
}
}
In this example, the BufferedReader
is closed automatically regardless of any
failure conditions. You can also open multiple resources within a try
statement
by separating them with a semicolon.
Multiple Catch
Traditionally, Java only allowed one exception in a catch
block, leading to
duplicate code like this.
catch (IOException ex) {
logger.log(ex);
throw ex;
catch (SQLException ex) {
logger.log(ex);
throw ex;
}
Starting with Java 7, you can catch multiple exceptions within the same block and eliminate that duplicate code.
catch (IOException|SQLException ex) {
logger.log(ex);
throw ex;
}
Numeric Literals
Numeric literals with underscores are a simple addition to the language. They
allow you to use an _
as a visual separator for large numbers. The following
examples should be self explanatory.
int thousand = 1_000;
int million = 1_000_000;
Using Java
After seeing how modern Java syntax simplifies and extends the Java of old, you may be itching to try Java yourself. To aid in that I’ve compiled a list of third-party tools and libraries that make it easy to get started.
Maven
Maven is a Java build system that highly favours convention over configuration. Maven defines the structure of your application and provides many built-in functions for running tests, packaging your application, and deploying your libraries. Using Maven significantly reduces the cognitive overhead of managing Java projects. Maven Central is the PyPI of the Java universe, providing a one-stop shop for published Java libraries.
Core Functionality
Google’s Guava library provides core functionality used by Google for Java development. This includes common functions for working with collections, caching, primitives, concurrency, string processing, I/O and more. Guava is a good case study in how to design a great Java API, providing concrete examples of most of the recommended best practices from Effective Java. Guava has been battle tested through production use at Google and comes with over 286,000 unit tests.
Date/Time Functions
Joda-Time has become the de facto
standard date and time library for Java. In fact, Java 8 adopted Joda-Time
conventions almost verbatim. Going forward, it’s recommended to use the
Date/Time functions in java.time
instead of Joda-Time. However, if you have a
need for an version of Java earlier than 8, Joda-Time provides an unmatched
API.
Distributed Systems
Akka provides abstractions for writing distributed systems using an Erlang-like Actor model. Akka is resilient to many different failures and provides higher level abstractions for writing robust distributed systems.
Web Applications
Need to write a full-fledged web app in Java? Play Framework has you covered. Play provides a scalable and asynchronous framework for writing web applications using non-blocking I/O based on Akka. For a less bleeding edge framework with wide production adoption, try Jetty.
Unit Testing
JUnit is still the standard for writing unit tests. Recent years have seen extensions to JUnit matchers allowing you to make assertions over collections. For example, you can easily assert that a list contains a particular value.
Mocking
Mockito is the standard mocking library for Java. It provides all the functionality you would expect from a mocking library and is invaluable in writing robust tests.
What Still Sucks
So far, I’ve painted a pretty rosy picture of Java, but some things still suck.
It’s Still Java!
Java’s legacy cannot be avoided and the fact that Java remains backwards compatible with its earliest versions means some of the worst parts of the language and standard library are here to stay. The fact that Guava exists to make the Java language more pleasant is testament to the fact that the Java language and APIs can be inconsistent, confusing, and sometimes just plain wrong.
JSON
Java lacks an object literal syntax that maps to JSON (like Python’s dictionary literal syntax). Because of this, mapping from Java objects to JSON and back requires a sometimes confusing set of object instantiations and mappings. There are a variety of JSON libraries competing in this space, with Jackson being the current favorite. Unfortunately, Jackson’s documentation could be considerably improved.
Mocking
Mockito solves a lot of the pain points in testing Java code, however transitioning from the flexibility of a language like Python to the strictness of Java requires you to think more carefully about how to design your classes for mocking.
REPL
One thing I love about Python is the ability to quickly fire up a read-eval-print loop to quickly evaluate new ideas or to test assumptions. Although there has been talk of adding an REPL to the standard Java library, this is something that is currently not supported.
Verbosity
Although advances in the Java compiler have meant less need for explicit type signatures — especially for generics — Java is still more verbose than Python. Getting a project up and running requires more boilerplate and overhead — generally it’s just more work.
Conclusions
Java has had a long and storied history, both good and bad. If you haven’t worked with Java for a few years, maybe now is a good time to try it again for the first time.
Just don’t do this.