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.

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?

1 2 3 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?

1 2 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.

1 2 3 4 5 6 7 8 // 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:

1 2 3 4 5 6 7 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:

1 2 3 4 5 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.

1 2 3 4 5 6 7 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.

1 2 3 4 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.

1 2 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.

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.

Java Stack

See also