Migrating from Java 8 to Java 14

Java developers will agree that Java 8 felt like a huge paradigm shift from Java 7. Java 8 was released in March 2014, and most notably introduced functional programming concepts, lambdas, and the Streaming API. Functional Programming in Java was a huge win for a language that already had a reputation for being too imperative. 

List<Integer> numbers = Arrays.asList(1, 2, 3);

// Pre Java 8 - print all items in a list

for (Integer number : numbers) {
    System.out.println(number);
}


// Post Java 8 - print all items in a list

numbers.forEach(n -> System.out.println(n));

// All hail lambda!!!

With method referencing, it’s even more concise.

numbers.forEach(System.out::println);

Java Programmers have been writing code like this for years. Quite frankly, Java 8 has remained the most widely used version of Java even to date. I guess the primary reason for this is there weren’t many notable upgrades to the language for quite some time. 

Java 9 gave us JShell, which I thought was cool. At last, Java programmers had a REPL shell bundled with the JDK with which they could run code snippets. 

Then there was Project Jigsaw which included support for modular Java. By definition, a module is a collection of related code comprising packages, classes, interfaces, etc. Through modular builds, we can compose jars that will run with a smaller memory footprint because they will only include the required modules. This makes it easier to write code that can be scaled down and ported to smaller devices. Nice!

Java as a Language needed to grow and evolve faster in order to keep up with the latest trends in software development. Java 10 through 11 saw some notable updates to the language. We got to see local variable type inference with the var keyword. Finally, Java developers can do something like this:

private void connect() {     
    var socketConnection = new SocketConnection();     
    // more code here.. 
}

Before Java 10, the code above had to be written as:

private void connect() {     
    SocketConnection socketConnection = new SocketConnection();
    // more code here.. 
}

Non-java devs weren’t particularly impressed. Local variable type inference had been a standard in some of the most popular programming languages. Java was late to the party. Still, it was a welcome addition. In a bid to ensure type safety and explicitness, Java often comes off as tautological, verbose. So Java developers were happy with the improvement.

The Java Language was starting to transition to shorter release cycles. In fact, the official statement from Oracle is that release cycles are slated for every 6 months. Meaning we get to enjoy more regular upgrades to our beloved language. At the time of writing this note, Java 15 has been officially released. I haven’t even looked at it yet, but from what I see, it includes a number of preview features from Java 14. 

Although at my place of work, a lot of legacy code is written in Java 8, I have upgraded all my personal projects to Java 14. I took the time to note some of my favorite features since upgrading from Java 8 to Java 14. 

Reactive Streams

Reactive Streams were introduced in Java 9 but started off as an initiative between engineers across major tech companies working in different problem domains. They identified the need for patterns that could promote robustness against workload through non-blocking backpressure. Meaning the consumer of a data stream can control the rate at which it wishes to read data off a stream source. Reactive Streams are an essential part of reactive programming which have been described in the reactive manifesto. You can read up on it to get an understanding of reactive programming and why it is important.  Java 9 implements reactive streams behind 4 interfaces:

  1. Publisher
  2. Subscriber
  3. Subscription
  4. Processor

A publisher is able to publish an arbitrary number of elements to subscribers depending on the rate at which the subscriber demands new data. Hence the subscriber controls the rate of data flow and will only receive new data as it is ready to process it. A subscriber receives an instance of a Subscription soon as it is subscribed to a Publisher. Through the subscription, a subscriber can request new data. The Processor can serve as both a Subscriber and a Publisher. These interfaces exist under the java.util.concurrent.Flow class and they form the basis of reactive programming in Java 9. I hope to do a post soon on reactive programming. It’s a very interesting topic!!

Local Variable Type Inference

Yes! at last, Java devs can now declare local variables using the var keyword. If we’re in a local context, we can use the var keyword to declare a variable and the compiler can deduce the type based on the data assigned to it. 

Records

A record is a data class. At least that’s the idea. A class defined as a record declares an explicit intention to hold data. So as opposed to having a class like this:

class Employee {
    private String firstName;
    private String lastName;

    public Employee(String firstName, String lastName) {         
        this.firstName = firstName;         
        this.lastName = lastName;     
    }

    // getters and setters
} 

Thank God for lombok, we can slap a few annotations on top of the class and avoid the need for constructor initialization or getters and setters. Still, records give us a better alternative. Check this out:

record Employee (
    String firstName,     
    String lastName
){}

Elegant and concise. 

Switch Expressions

Introduced in Java 12. Switch Expressions allow us to utilise switch syntax in assignment statements. Here’s what it looks like:

// Pre Java 12
String sex;
switch (name) {
   case "Julian":
   case "Henry":
     sex = "M";
     break;
   case "Alice":
   case "Ngozi":
     sex = "F";
     break;
   default:
     sex = "Unknown";
}

The above code snippet in Java 12 can be written like this:

// Post Java 12
String sex = switch (name){
    case "Julian", "Henry" -> "M";
    case "Alice", "Ngozi" -> "F";
    default -> "Unknown";
};

Text blocks

Did you ever try to declare a multi-line string in code? Chances are you needed to use the plus (+) sign to concatenate strings across multiple lines including escape sequences to introduce formatting. This produces some ugly looking code which can be hard to read. Text blocks try to address this by introducing a format for easy string expression across multiple lines. 

String html = "<html>\n" +              
    "    <body>\n" +              
    "        <p>Hello, world</p>\n" +  
    "    </body>\n" +              
    "</html>\n";

// With text blocks 
String html = """              
    <html>                  
       <body>                      
          <p>Hello, world</p>                  
       </body>              
    </html>
""";

You can learn more here: https://openjdk.java.net/jeps/355

Helpful Null pointer Errors

Every developer encounters the dreaded NullPointerException from time to time. Typically, it happens when you try to dereference a variable with a null value. Prior to Java 14, we will have a very unhelpful message: null. The developer is left scratching their head on what could have gone wrong. In some cases, it’s fairly easy to determine the culprit and fix it; in other cases, you will need a debugger to step through the code and find the actual source of the null exception. Java 14, makes it easy to identify where a null variable is being dereferenced by analyzing the byte code and extracting a helpful error message for the developer. So instead of a bleak ’null’ message, we can now tell the actual cause of the null pointer. whew!!!

Inbuilt HTTP Client

Java 11 came with a standard implementation of Http Client with support for HTTP/1.1 and HTTP/2. What’s great about this client is its support for synchronous and asynchronous request processing via reactive streams. 

HttpClient client = HttpClient.newHttpClient();

HttpRequest request = HttpRequest.newBuilder()
    .uri(URI.create("http://openjdk.java.net/"))      
    .build();

client.sendAsync(request, BodyHandlers.ofString())      
    .thenApply(HttpResponse::body)      
    .thenAccept(System.out::println)      
    .join();

Managing multiple JDK instances

If you’re going to upgrade your JDK version, then you need a way to manage multiple JDKs on a single machine.  There are a number of ways to accomplish this: 

  1. Using an IDE
  2. On the command-line

Different IDE’s have different ways of managing the applied JDK on a project. You’ll need to research the method applicable to your IDE of choice. 
Sometimes, you want to do Java stuff on the command line. For this, the best tool I found for managing Java versions is SDKMan

SDKMan is a tool that allows installing and managing multiple versions of Java on a single machine. You can use it to manage not only Java, but a bunch of other installations as well. Linux based systems can install sdk man through curl like this:

$ curl -s "https://get.sdkman.io" | bash

After following instructions then Open new terminal and enter command:

$ source "$HOME/.sdkman/bin/sdkman-init.sh"

With SDK Man installed, you can view available java installations by typing command:

$ sdk list java

This will list out all java installations available through sdkman. You can then use the identifier to install the specific version you are insterested in.

$ sdk install java 14.0.2-open

Wrapping Up

A major part of a developer’s job involves keeping abreast of the latest trends in technology. It sucks sometimes, you think you know enough, then a new technology springs up and leaves you out of fashion. Which is why it’s so important you commit to lifelong learning. If you’re a Java developer stuck on Java 8, you need to take time to upgrade your Java. A failure to do so will mean you’re still in the past. It’s time to skill up!!