Local Class Improvements - Sip of Java

There have been several changes in recent releases of Java that improve the ability to define classes and concepts locally. Most of these changes come from clean-up work while implementing other features. Defining and scoping a concept to only where it’s relevant can improve readability and prevent bugs from being introduced. They can also be a source of bugs if not used properly. Let’s look at three improvements that allow developers to define classes or concepts within a method.

Non-Denotable Types

Local-Variable Type-Inference, var, was introduced in JDK 10. var can improve the experience of working with local variables by saving time when defining them. Less covered, with the introduction of var, the ability to retain type information of non-denotable types. Let’s look at the below example:

public void printNamesNicely() {
    var data = """
               Billy,Korando
               """;
    try (var scanner = new Scanner(data)) {
        while (scanner.hasNextLine()) {
            var line = scanner.nextLine().split(",");
            var person = new Object() {
                String fName = line[0];
                String lName = line[1];
                void prettyPrint() {
                    System.out.println("%s, %s".formatted(fName, lName));
                }
            };
        person.prettyPrint();
        }
    }
}                                              

Here the usage of var captures the type information of the anonymous class, specifically prettyPrint(). Using Object as the type for the variable person meant prettyPrint() would have been lost, and a compiler error on the line person.prettyPrint();. Using var retains the type information of the anonymous class, allowing prettyPrint() to be referenced later in the method printNamesNicely(). var’s ability to retain type information can also be helpful when working with capture types and intersection types.

Expanded Local Class Types

Before JDK 16, only a class could be defined within the body of a method. With the addition of local records, this restriction was lifted so that other class types, abstract, interface, and enum, can also be defined within the body of a method. In the below example, enum Size is defined within the body of sizeFinder(String):

public String sizeFinder(String mySize) {
	enum Size {
		SMALL("S"), MEDIUM("M"), LARGE("L"), EXTRA_LARGE("XL");
		private String shortSize;
		private Size(String shortSize) {
			this.shortSize = shortSize;
		}

		public static Size lookupByShortSize(String shortSize) {
			for (int i = 0; i < Size.values().length; i++) {
				if (shortSize.equals(Size.values()[i].shortSize)) {
					return Size.values()[i];
				}
			}
			throw new IllegalArgumentException("Size not found!");
		}
	}
	return Size.lookupByShortSize(mySize).name();
} 

enum Size provides additional context to the meaning of the String values; S, M, L, and XL. If the only area where these values are used is within sizeFinder(String), then defining enum Size within sizeFinder(String) limits the cognitive load for developers maintaining the application. A developer would need only be concerned with this information when interacting with the method sizeFinder(String).

Static Members for Inner and Local Classes

Also added with JDK 16 was the lifting of the restriction that inner classes and local classes could not have a static member. This makes it possible to maintain a method-local state between invocations, as demonstrated in the below example:

public void recordUsers(String user) {
	class Holder {
		static List<String> userRecord = new ArrayList<>();
	}
	Holder.userRecord.add(user);
	System.out.println("User: " + user + " interaction has been recorded");
	System.out.println("All users:" + Holder.userRecord.toString());
}

The local class Holder contains the static member List<String> userRecord. Each time recordUsers(String) the passed-in argument is persisted to the List<String> userRecord.

With Great Power Comes Responsibility

These changes can improve the expressibility of a Java application or allow if desired, a tighter coupling of the definition of a concept to where it’s used within an application. These changes could also be misused to undermine readability, lead to thread safety problems, or other potential issues.

When considering using these features, weigh the potential risks versus the benefits.

Additional Reading

Happy coding!