Java 9 to 19

Java 8 is still extensively used in the industry and many applications will gradually shift to newer Java version, especially the LTS versions.

In this post we will take a look at the evolution happened in Java language from Java 9 to Java 19. Note that each version comes with many improvements, bug fixes and variety of features, we will cover the ones which are majorly used and can impact our day to day developement.

Java 9

Factory methods for collection

    List immutableL = List.of(1, 2, 3);
    Map immutableM = Map.of(1, "ONE", 2, "TWO", 3, "THREE")

JShell: Java Shell, or REPL (Read Evaluate Print Loop) to execute java constructs directly in command line.

Private methods in interface.

This will avoid code duplication and better separation of concern when it comes to implementing default and static methods in interface.

interface Student {
    private String joinNames(String firstName, String lastName) {
        return String.join(firstName, " ",lastName);
    }
    private static String schoolName() {
        return "Some School";
    }

    default String id(String firstName, String lastName) {
        String fullName = joinNames(firstName, lastName);
        return schoolName() + "\n" + fullName;
    }
}

Step in direction to optimize String concatenation.

For the given class,

public class Test {
    public static void main(String[] args) {
        String str = args[0] + " and " + args[1];
    }
}

If we compile and check the bytecode, we can notice significant different in the way concatenation is handled.

In Java 8,

➜  java git:(main) ✗ java -version 
openjdk version "1.8.0_362"
OpenJDK Runtime Environment (build 1.8.0_362-bre_2023_01_22_03_30-b00)
OpenJDK 64-Bit Server VM (build 25.362-b00, mixed mode)
➜  java git:(main) ✗ clear           
➜  java git:(main) ✗ java -version
openjdk version "1.8.0_362"
OpenJDK Runtime Environment (build 1.8.0_362-bre_2023_01_22_03_30-b00)
OpenJDK 64-Bit Server VM (build 25.362-b00, mixed mode)
➜  java git:(main) ✗ javac Test.java
➜  java git:(main) ✗ javap -c  Test 
Compiled from "Test.java"
public class Test {
  public Test();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public static void main(java.lang.String[]);
    Code:
       0: new           #2                  // class java/lang/StringBuilder
       3: dup
       4: invokespecial #3                  // Method java/lang/StringBuilder."<init>":()V
       7: aload_0
       8: iconst_0
       9: aaload
      10: invokevirtual #4                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      13: ldc           #5                  // String  and
      15: invokevirtual #4                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      18: aload_0
      19: iconst_1
      20: aaload
      21: invokevirtual #4                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      24: invokevirtual #6                  // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
      27: astore_1
      28: return
}

In Java 9,

➜  java git:(main) ✗ java -version 
openjdk version "9"
OpenJDK Runtime Environment (build 9+181)
OpenJDK 64-Bit Server VM (build 9+181, mixed mode)
➜  java git:(main) ✗ javac Test.java
➜  java git:(main) ✗ javap -c  Test 
Compiled from "Test.java"
public class Test {
  public Test();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public static void main(java.lang.String[]);
    Code:
       0: aload_0
       1: iconst_0
       2: aaload
       3: aload_0
       4: iconst_1
       5: aaload
       6: invokedynamic #2,  0              // InvokeDynamic #0:makeConcatWithConstants:(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;
      11: astore_1
      12: return
}

Notice the multiple StringBuilder invocations in case of Java 8, which is replaced with makeConcatWithConstants in Java 9.

Java 10

Local variable type interface, use var to declare.

    var i = 1;
    var str = "Hello";
    var student = getStudent();

Static factory methods to create immutable copy of Collection,

    List<String> immutable = List.copyOf(otherList);

orElseThrow() in Optional

    Optional<Object> optional = Optional.ofNullable(null);
    optional.orElseThrow(() -> new RuntimeException("Something went wrong!"));

Java 11 (LTS)

Execute Java file directly without compiling with javac.

java command internally takes care of the compilation.

Some helper methods for String

String str = "Hello";
boolean isBlank = str.isBlank();
str.lines().forEach(System.out::println);
str = str.strip();
// str.stripLeading();
//  str.stripTrailing();

Removed deprecated packages,

java.xml.ws
java.xml.bind
java.activation
java.xml.ws.annotation
java.corba
java.transaction
java.se.ee
jdk.xml.ws
jdk.xml.bind

Make file read and write convenient,

Path path = Files.writeString(Files.createTempFile("temporary", ".txt"), "Something to write!");

String fileContent = Files.readString(path);
System.out.println(fileContent);

Java 12

Switch can be an expression, changes are in preview.

Before Java 12

Animals animal = Animals.COW;

switch (animal) {
    case COW:
    case GOAT:
        System.out.println("Herbivore");
        break;
    case TIGER:
    case LION:
        System.out.println("Carnivore");
        break;
}

Can now be reduced to,

String animalType = switch (animal) {
    case COW, GOAT -> "Herbivore";
    case TIGER,  LION -> "Carnivore";
};

No need to typecast for instanceof,

if(object instanceof String) {
    System.out.println(((String)object).toUpperCase());
}

but now you can do,

if(object instanceof String str) {
    System.out.println(str.toUpperCase());
}

Compare files,

try {
    Path filePath1 = Files.createTempFile("abc1", ".txt");
    Path filePath2 = Files.createTempFile("abc2", ".txt");

    Files.writeString(filePath1, "Hello!");
    Files.writeString(filePath2, "Hello! (Diff)");

    long mismatchIndex = Files.mismatch(filePath1, filePath2);
    if(mismatchIndex == -1) {
        System.out.println("Both files are same!");
    } else {
        System.out.println("Mismatch found at " + mismatchIndex);
    }
} catch (IOException e) {
    throw new RuntimeException(e);
}

Output

Mismatch found at 6

String Identation,

String str = "Hello";
for(int i = 0 ; i < 5; i++) {
    System.out.print(str.indent(i));
}

Output

Hello
 Hello
  Hello
   Hello
    Hello

Convenient method to transform String,

String numbers = "1:ONE,2:TWO,3:THREE";
Map<Integer, String> map = numbers.transform(input -> {
    Map<Integer, String> output =
        Arrays.stream(input.split(","))
            .collect(Collectors.toMap(i -> Integer.parseInt(i.split(":")[0]),
                i -> i.split(":")[1]));
    return output;
});
System.out.println(map);

Java 13

Text block support in String,

String textBlock = """
    I can write anything,
    without adding \\n in the String.
    """;
System.out.println(textBlock);

New Methods in String for format,

String anything = "Hello %d and %s".formatted(1, "ONE");

Intoduced yield in switch case, this will replace the break for cases where we want to return the number. Difference between yeild and return is that yeild will return the value to switch invocation while return will return the value to the caller of the method.

int answer = switch (number) {
    case 1:
    case 3:
    case 5:
    case 7:
        yield number;
    default:
        yield -1;
};

Java 14

Preview of records, a data class.

record Person(String name, int age){}

Can be used,

Person person = new Person("Human", 999);
System.out.printf("Person %s, age %d\n", person.name(), person.age());

Things to note about record

  • Can not extend, can not be extended by class
  • Can not be abstract
  • Allows static fields and methods
  • Instance fields can be declared during initialization.
  • Declared fields are private and final
record Person(String name, int age){
    // int anything = 0; // Not allowed
    static int anything = 0;

    public String personDetails() {
        return String.format("Person %s, age %d\n", name(), age());
    }
    Person {
        if(name == "Human") {
            throw new RuntimeException("Invalid name");
        }
    }
}

Records can implement interfaces,

interface Human {
    public String personDetails();
}

record Person(String name, int age) implements Human{
    public String personDetails() {
        return String.format("Person %s, age %d\n", name(), age());
    }
}

It can support multiple constructors as well,

record Person(String name, int age){
    public Person() {
        this("Human", 9999);
    }
    public Person (int age) {
        this("Human", age);
    }
}

Allow trailing space in text block,

String textBlock = """
    I can write anything,
    without adding \\n in the String.\s\s\s
    """;
System.out.println(textBlock);

Java 15

Preview of sealed classes or interfacse,

to allow only specific types which can extend or implement respectively.

public abstract sealed class Animal permits Herbivore, Carnivore {
}

final class Herbivore extends Animal{}
sealed class Carnivore extends Animal{}

//class Unknown extends Animal{} // Not allowed to extend

Subclass of a sealed class must have either of the following modifiers,

  • sealed : Will allow to be extended further by permitted classes.
  • non-sealed : Will allow to be extended further by any classes.
  • final : Will not allow to be extended further.
public abstract sealed class Animal permits Herbivore, Carnivore, Omnivore {
}

final class Herbivore extends Animal {}
sealed class Carnivore extends Animal permits  Tiger{}
non-sealed class Omnivore extends Animal {}

final class Tiger extends Carnivore{}

Records can implement the sealed interfaces,

sealed interface Food permits Creature {
    void doSomething();
} 
record Creature(String name) implements Food {
    @Override
    public void doSomething() {
        System.out.println("Anything");
    }
}

Java 16

Pattern matching in instanceof no longer makes variable implicitly final

if(object instanceof String) {
    object = String.format("Result %s", object); // Would give compile time error prior to Java 16.
    System.out.println(object.toUpperCase());
}

New Vector API, incubator.

int[] odd = {1, 3, 5, 7};
int[] even = {2, 4, 6, 8};
var vector1 = IntVector.fromArray(IntVector.SPECIES_128, odd, 0);
var vector2 = IntVector.fromArray(IntVector.SPECIES_128, even, 0);
var vector3 = vector1.add(vector2);
System.out.println(vector3);

Output

[3, 7, 11, 15]

Note that, to run the program you will need to add the module otherwise it won’t be visible.

 java --add-modules jdk.incubator.vector JavaMainClass

Java 17 (LTS)

null in switch,

switch (number) {
    case 1, 2, 3 -> System.out.println("Valid");
    case null -> System.out.println("Not available");
    default -> System.out.println("Invalid");
}

Pattern matching in switch,

String value = switch (obj) {
    case Integer i -> "Integer";
    case Long l    -> "Long";
    case Double d  -> "Double";
    case String s  -> "String";
    case null -> "NULL";
    default -> obj.toString();
};

Java 18

Introduce @snippet in JavaDoc to write code in comments,

/**
* {@snippet:
*  int a = 10;
* }
*/
public void testMethod() {

}

Finalization is deprecated.

The use of finalize() method is discouraged and the support will be removed in future.

Java 19

Preview Virtual Threads

lightweight threads which effectively shares the platform thread for optimal hardware utilisation.

ExecutorService executorService = Executors.newVirtualThreadPerTaskExecutor();

Structured Concurrency (Incubator)

Sounds interesting, it enables to consider multiple threads as a unit. This will going to give better control over multithreaded programs.

Basics of Spring Transactional

Why Spring Transaction?

  • Provides consistent model across JTA (Java Transaction API), JPA (Java Persistence API), JDBC (Java Database Connectivity) and JDO (Java Data Objects)
  • Declarative Transaction Management
  • Simple API for programmatic transaction management - overcomes complexity of JTA.
  • Makes the implementation easy to stub and test.

Transaction with JDBC

import java.sql.Connection;

//...
public void deleteSomething() {
    Connection connection = DriverManager.getConnection(host, user, password); 

    try (connection) {
        connection.setAutoCommit(false);
        Statement deleteStatement = connection.createStatement(); 
        String deleteQuery = "DELETE FROM SOMETHING WHERE SOMETHING_ID = 1";
        deleteStatement.executeQuery(deleteQuery); 
        connection.commit();
    } catch (SQLException e) {
        connection.rollback();
    }
}

//...
With raw JDBC we need to handle commit, rollback, savepoint all on our own. Spring takes away majority of boilerplate code so that we can focus on core business functionalities. 

Transaction with Spring

Above function can be implemented in following way with Spring (assuming the transaction management configuration is in place),

@Transactional
public void deleteSomething() {
    somethingRepository.deleteBySomethingId(1);
}

Spring Transaction Manager




Spring transaction manager mainly rely on definition and status. In definition the nature and behaviour of transaction is defined while the status helps to keep track of transaction journey statuses.

Different Supported transaction managers includes,
  •  DataSourceTransactionManager
  •  JtaTransactionManager
  •  HibernateTransactionManager
  •  JdbcTransactionManager
  •  etc.
This Transactional annotation handles everything behind the scene for use. This annotation has two important configurations - Isolation and Propagation, which we will understand in detail.

Propagation

Transcation propagation defines the nature of transaction handling when there are multiple transactional annottated methods are invoked within the same spring transcation context. 

You can suppy the propagation in Transactional annotation,

@Transactional(propagation = Propagation.REQUIRES_NEW)

Following are supported propagation ways,

  • REQUIRED  → Create new transcation or use existing.
  • SUPPORTS  → Work with or without transaction.
  • MANDATORY  → Do not create transaction but fail if transaction do not exist.
  • REQUIRES_NEW  → Create a new transaction.
  • NOT_SUPPORTED  → Execute without transaction, suspend if transaction exists.
  • NEVER  → Execute without transaction, fail if transaction exists.
  • NESTED  → One physical transaction with multiple savepoints to manage subtransactions.
















Isolation

Isolation level configuration determines the visibility of data between transactions in case of concurrent operations on same database resource. It helps to ensure consistency and integrity of the data. Transaction isolation level is subject to underlying database you are using. We will refer to them in generic sense to get glimps of it.

You can suppy the isolation in Transactional annotation,

@Transactional(isolation = Isolation.DEFAULT)

The isolation levels tackles different concurrent transcation phenomena listed below,
  • Dirty Read : Transaction read uncommitted data of other transaction.
  • Non-repeatable read : Re-read the same data which is now modified by other transaction.
  • Phantom Read : Re-execute the query which returns result set which changed by other transcation.
  • Serialization Anomaly : Inconsistent state of data - group of transactions are committed sequentially with all possible ordering.

Following are supported isolations levels in Spring,
  • DEFAULT → User default isolation level of underlying database.
  • READ_UNCOMMITTED → Allows to read uncommitted data of other transaction. It can not prevent problems may arrise due to concurrency.
  • READ_COMMITTED →  Allows to read committed data of other transaction. Prevents dirty read.
  • REPEATABLE_READ → Prevents dirty read and non-repeatable read (and even phantom read) by returning unchanged data in repeatable reads within same transaction.
  • SERIALIZABLE → Executes transactions sequentially. Solves all consurrency problems but performance suffers.

This can be a starting point for spring transaction learnings, if you have understood the propagation and isolation level well, it becomes easier to configure and debug accordingly. 

Code Smells

Code smells are the indicators of potential flaws in the design and structure, not necessarily prevents the functionality but signifies the scope of improvement in implementation.

Code smells are prominent in legacy systems, developers hardly take chance to do major refactoring. However, all of these code smells can be detected and resolved during the development.

Most commonly can be identified by questioning the written code in following way,
  • Is there any duplication?
  • Is it difficult to understand?
  • Is is complicated to explain?
  • Is it difficult to test?
  • Will it be difficult to maintain?

If your answer is 'Yes' to any of these questions, most probably, your code contains one or more code smells.

Let's understand this with following code snippet,

If we check above class closely we can find following issues,

  • Six arguments makes the implementation complex and increases the responsibility of the function.
  • Method expects the caller to send the students in sequence which opens up the window for bug.
  • For new student, method needs to be changed,
  • Return statement is not required.
  • For invalid input method is not raising any exception.
  • The value of 50 is a magic number, difficut to understand what it is. 
  • Print method is again checking the student marks.
  • Subject parameter was not used.
  • ... and so on.
Many of the issues are nowadays identified by the smart IDEs. Out of these issues, few of them are actually code smells. 

Ask beforementioned questions again for this code and you will probably get 'Yes' for all the questions related to complexity, maintainability and readability of the code.

Following code smells are there in above class,
  • Long Parameter List
  • Duplication
  • Unnecessary Comments
  • Unnecessary Boxing-UnBoxing ( double & Double)
  • Unnecessarily Large Method
  • Dead Code 

By questioning your code you can identify such code smells and fix them for the sake of clean coding practice. There are many more code smells which can occur while writing the code. 

References:

Virtual Threads : Java 21

Java 21 came with an exciting feature of Virtual Thread - Lightweight Threads. The idea is to achieve optimal hardware utilisation, which has been a bottleneck for the conventional java.lang.Thread - platform threads.

Platform threads are 1:1 mapped with OS Thread, which makes them powerful to accomplish all possible tasks. java.lang.Thread is a thin layer on actual OS threads, which are quite heavy. The number of thread application can support depends on the hardware capacity of the system.

Let’s say thread memory consumption is 1 MB, to support 1 million threads in concurrent application at the time of heavy load, you will need 1 TB of memory. One thread per request style of implementation also suffer due to this limitation - asynchronous programming can solve this upto some extent but it has its own drawbacks.

Virtual threads effectively shares the platform thread. Rather than holding the platform thread for entire lifetime, it runs short lived tasks and for the needed executions - not while it waits for I/O. This allows insane number of concurrent operations without need of additional threads or hardware capacity. This brings up the new way to deal with concurrency in Java applications.

Virtual threads can be created with java.lang.Thread builder approach,

Runnable function = () -> System.out.println("Something to execute in Virtual Thread!");
Thread virtual = Thread.startVirtualThread(function);

In addition to that, java.util.concurrent.ExecutorService also has factory method for Virtual threads,

ExecutorService virtualExecutor = Executors.newVirtualThreadPerTaskExecutor();

Virtual threads are daemon threads, join() to wait on the main thread. Thread local variables of virtual thread are not accessible to carrier thread or vice versa.

Trunk Based Development

Trunk based branching model seems very inefficient in the first place. Initially, I was feeling that it is not utilizing the branching superpower of a version control system like GIT. You see there are a lot of advantages to using branches, including

  • …you can develop and test multiple things in parallel in separate branches
  • …you can work on multiple releases at the same time by creating a different branch
  • …different developers can work on separate branches and once done with the implementation can raise pull requests for review

All these benefits sound true when you choose branching over the trunk-based approach and coming from the same experience I was skeptical about how this model will help in development. However, I reasonably underestimated it as it offers a lot of positive results in the longer run and I’ll try to list down a few of them below.


What is Trunk Based approach?

Trunk based approach is a practice where all developers push their changes to one “trunk”, the main branch rather than creating a separate branch. Having said that this practice doesn’t stop developers from creating branches, in certain scenarios you can still create branches, but these branches must be short-lived which means the branches must be deleted within a few hours or at max within a day.


Why Trunk Based approach?

Continuous integration and early feedback

All the changes must be production deployable. As soon as the developer pushes the changes to the repository the CI/CD pipelines take care of all the things including static code analysis, scan, security issues, etc., and give immediate feedback to the team. The team won’t need to wait for the final merge and integration before release to make this happen.

Fewer conflicts and Fewer merge

Smaller non-breaking changes are pushed frequently throughout the day such that there are fewer conflicts between the developer changes. A developer ensures to sync at least once a day with the central repository to avoid conflicts as much as possible.

Easy to manage and review

Developers push smaller working changes, frequently, and reviewing smaller changes rather than one pull request with several files is still a better and more efficient option.

Encourages responsible push

Due to continuous integration with all environments including production (with toggle feature), it becomes necessary for developers to be conscious about the code they are pushing to the central repository.

Yet it’s not the best choice in all scenarios, in the following situation this approach may not help that much,

  • Open Source Projects: You won’t expect anyone to just push changes to your trunk directly, going through the pull request way is the best choice.
  • Less experienced developers: When you have more junior members who are still learning, you will need a stringent code review before they sync their changes to the main branch to avoid failures due to minor mistakes.

Conclusion

It is important to choose this based on the nature of your work. For small teams and new projects, this approach can surely help a lot while on the other hand, it can not be as efficient as it seems for legacy systems or teams with less experienced developers.