What’s New Between Java 11 and Java 17?

Java 17 was released on September 14. It’s time to take a closer look at what’s changed since the last LTS release, namely Java 11. We’ll start with a brief overview of the licensing model and then highlight some of the changes between Java 11 and Java 17, mainly through examples. Enjoy!

1. Introduction

First, let’s take a closer look at the Java licensing and support model. Java 17 is an LTS (long term support) release, just like Java 11. Java 11 begins a new release rhythm. Java 11 support until September 2023, extended support until September 2026. Also, in Java 11, the Oracle JDK is no longer free for production and commercial use. A new Java release, known as a non-LTS release, is released every six months, from Java 12 up to and including Java 16. However, these are production ready releases. The only difference from the LTS release is that it supports termination with the next release. For example. Java 12 support ends with the release of Java 13. When you want to maintain support, you more or less have to upgrade to Java 13. This can cause problems when some of your dependencies are not ready for Java 13. In most cases, for production purposes, companies will wait for the LTS version. But even so, some companies are reluctant to upgrade. A recent Snyk survey shows that only 60% of people are using Java 11 in production, and that’s three years since Java 11 was released! 60% of companies are still using Java 8. Another interesting thing to note is that the next LTS release will be Java 21, which is due in 2 years. A good overview of whether libraries are a problem in Java 17 can be found here.

With the release of Java 17, the Oracle licensing model changed. Java 17 is released under a new NFTC (Oracle Free Terms and Conditions) license. As a result, the Oracle JDK version is once again allowed for production and commercial use at no cost. In the same Snyk survey, it was noted that the Oracle JDK version was only used by 23% of users in production. Please note that support for the LTS release will end one year after the next LTS release. It will be interesting to see how this affects upgrading to the next LTS release.

What has changed between Java 11 and Java 17? A complete list of JEP (Java Enhancement Proposals) can be found on the OpenJDK website. Here, you can read the details of each JEP. The Oracle release notes provide a good overview of the complete list of changes for each release since Java 11.

Some of the changes are explained by examples in the following sections, but it’s up to you to experiment with the new features to get familiar with them. All of the resources used in this article are available on GitHub.

The last thing is that Oracle has released dev.java, so don’t forget to take a look.

2. Text Blocks

Many improvements have been made to make Java more readable and concise. Blocks of text definitely make code more readable. First, let’s look at the problem. Suppose you need some JSON strings in your code and you need to print it. There are several problems with this code:

  • Escape of double quotes;
  • String concatenation to make it more or less readable;
  • Copying and pasting JSON can be labor-intensive (your IDE might help you with that).
    private static void oldStyle(a) {
        System.out.println(""" ************* * Old Style * *************""");
        String text = "{\n" +
                      " \"name\": \"John Doe\",\n" +
                      " \"age\": 45,\n" +
                      " \"address\": \"Doe Street, 23, Java Town\"\n" +
                      "}";
        System.out.println(text);
    }
Copy the code

The output from the above code is well-formed JSON.

{
    "name": "John Doe"."age": 45."address": "Doe Street, 23, Java Town"
}
Copy the code

A text block is defined with three double quotes, where the last three double quotes cannot be on the same line as the starting one. First, just print an empty block. To visualize what is happening, the text is printed between two double tubes.

    private static void emptyBlock(a) {
        System.out.println(""" *************** * Empty Block * ***************""");
        String text = """""";
        System.out.println("|" + text + "|");
    }
Copy the code

The output is:

||||
Copy the code

The JSON part in question can now be written as follows for better readability. There is no need to escape the double quotes, it looks like it will be printed.

    private static void jsonBlock(a) {
        System.out.println(""" ************** * Json Block * **************""");
        String text = """{"name":"John Doe","age": 45,"address":"Doe Street, 23, Java Town"}""";
        System.out.println(text);
    }
Copy the code

The output, of course, is the same.

{
    "name": "John Doe"."age": 45."address": "Doe Street, 23, Java Town"
}
Copy the code

In the previous output, there is no preceding space. However, in the code, there are Spaces in front of it. How do I determine to strip the space before it? First, move the three closing double quotes more to the left.

    private static void jsonMovedBracketsBlock(a) {
        System.out.println(""" ***************************** * Json Moved Brackets Block * *****************************""");
        String text = """{"name":"John Doe","age": 45,"address":"Doe Street, 23, Java Town"}""";
        System.out.println(text);
    }
Copy the code

Output now prints two Spaces before each line. This means that the three closing double quotes indicate the beginning of a block of text.

{
    "name": "John Doe"."age": 45."address": "Doe Street, 23, Java Town"
}
123
Copy the code

What happens when you move the three closing double quotes to the right?

    private static void jsonMovedEndQuoteBlock(a) {
        System.out.println(""" ****************************** * Json Moved End Quote Block * ******************************""");
        String text = """{"name":"John Doe","age": 45,"address":"Doe Street, 23, Java Town"}""";
        System.out.println(text);
    }
Copy the code

The spacing ahead is now determined by the first non-space character in the text block.

{
    "name": "John Doe"."age": 45."address": "Doe Street, 23, Java Town"
}
Copy the code

3. Switch expression

Switch expressions will allow you to return values from the Switch and use them in assignments, etc. A classic Switch is shown here, where, given a Fruit enumeration value, some action needs to be performed. “Break” was deliberately ignored.

    private static void oldStyleWithoutBreak(FruitType fruit) {
        System.out.println(""" *************************** * Old style without break * ***************************""");
        switch (fruit) {
            case APPLE, PEAR:
                System.out.println("Common fruit");
            case ORANGE, AVOCADO:
                System.out.println("Exotic fruit");
            default:
                System.out.println("Undefined fruit"); }}Copy the code

Call the method using APPLE.

oldStyleWithoutBreak(Fruit.APPLE);
Copy the code

This will print each case, since without a break statement, the case will be invalidated.

Common fruit
Exotic fruit
Undefined fruit
Copy the code

Therefore, it is necessary to add a break statement to each case to prevent such invalidation.

    private static void oldStyleWithBreak(FruitType fruit) {
        System.out.println(""" ************************ * Old style with break * ************************""");
        switch (fruit) {
            case APPLE, PEAR:
                System.out.println("Common fruit");
                break;
            case ORANGE, AVOCADO:
                System.out.println("Exotic fruit");
                break;
            default:
                System.out.println("Undefined fruit"); }}Copy the code

Running this method gives you the results you need, but now the code is a little less readable.

Common fruit
Copy the code

This can be solved by using the Switch expression. Replace the colon (:) with the arrow (->) and be sure to use expressions in case. The default behavior of a Switch expression is that there is no failure, so a break is not required.

    private static void withSwitchExpression(FruitType fruit) {
        System.out.println(""" ************************** * With switch expression * **************************""");
        switch (fruit) {
            case APPLE, PEAR -> System.out.println("Common fruit");
            case ORANGE, AVOCADO -> System.out.println("Exotic fruit");
            default -> System.out.println("Undefined fruit"); }}Copy the code

This is not so verbose, but the result is the same.

The Switch expression can also return a value. In the example above, you can return String values and assign them to the variable TEXT. After that, you can print the text variable. Don’t forget to add a semicolon after the last case parentheses.

    private static void withReturnValue(FruitType fruit) {
        System.out.println(""" ********************* * With return value * *********************""");
        String text = switch (fruit) {
            case APPLE, PEAR -> "Common fruit";
            case ORANGE, AVOCADO -> "Exotic fruit";
            default -> "Undefined fruit";
        };
        System.out.println(text);
    }
Copy the code

And, even shorter, the above can be rewritten in a single statement. Whether this is more readable than the above is up to you.

    private static void withReturnValueEvenShorter(FruitType fruit) {
        System.out.println(""" ********************************** * With return value even shorter * **********************************""");
        System.out.println(
            switch (fruit) {
                case APPLE, PEAR -> "Common fruit";
                case ORANGE, AVOCADO -> "Exotic fruit";
                default -> "Undefined fruit";
            });
    }
Copy the code

What do you do when you need to do more than one thing in a case? In this case, you can use square brackets to represent the case block and use the yield keyword when returning the value.

    private static void withYield(FruitType fruit) {
        System.out.println(""" ************** * With yield * **************""");
        String text = switch (fruit) {
            case APPLE, PEAR -> {
                System.out.println("the given fruit was: " + fruit);
                yield "Common fruit";
            }
            case ORANGE, AVOCADO -> "Exotic fruit";
            default -> "Undefined fruit";
        };
        System.out.println(text);
    }
Copy the code

The output is now a little different, with two print statements executed.

the given fruit was: APPLE
Common fruit
Copy the code

The yield keyword you can use in “old” switch syntax is also cool. We don’t need a break here.

    private static void oldStyleWithYield(FruitType fruit) {
        System.out.println(""" ************************ * Old style with yield * ************************""");
        System.out.println(switch (fruit) {
            case APPLE, PEAR:
                yield "Common fruit";
            case ORANGE, AVOCADO:
                yield "Exotic fruit";
            default:
                yield "Undefined fruit";
        });
    }
Copy the code

4. She kept a record of her work.

Records will allow you to create immutable data classes. Currently, you need, for example, to create GrapeClass using the IDE’s auto-generating functions to generate constructors, getters, hashCode, equals, and toString, or you can use Lombok for the same purpose. Eventually, you’ll end up with boilerplate code, or your project will end up relying on Lombok.

public class GrapeClass {

    private final Color color;
    private final int nbrOfPits;

    public GrapeClass(Color color, int nbrOfPits) {
        this.color = color;
        this.nbrOfPits = nbrOfPits;
    }

    public Color getColor(a) {
        return color;
    }

    public int getNbrOfPits(a) {
        return nbrOfPits;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null|| getClass() ! = o.getClass())return false;
        GrapeClass that = (GrapeClass) o;
        return nbrOfPits == that.nbrOfPits && color.equals(that.color);
    }

    @Override
    public int hashCode(a) {
        return Objects.hash(color, nbrOfPits);
    }

    @Override
    public String toString(a) {
        return "GrapeClass{" +
                "color=" + color +
                ", nbrOfPits=" + nbrOfPits +
                '} '; }}Copy the code

Perform some tests using the GrapeClass class above. Create two instances, print them, compare them, create a copy and compare this as well.

    private static void oldStyle(a) {
        System.out.println(""" ************* * Old style * *************""");
        GrapeClass grape1 = new GrapeClass(Color.BLUE, 1);
        GrapeClass grape2 = new GrapeClass(Color.WHITE, 2);
        System.out.println("Grape 1 is " + grape1);
        System.out.println("Grape 2 is " + grape2);
        System.out.println("Grape 1 equals grape 2? " + grape1.equals(grape2));
        GrapeClass grape1Copy = new GrapeClass(grape1.getColor(), grape1.getNbrOfPits());
        System.out.println("Grape 1 equals its copy? " + grape1.equals(grape1Copy));
    }
Copy the code

The output of the test is:

Grape 1 is GrapeClass{color=java.awt.Color[r=0,g=0,b=255], nbrOfPits=1}
Grape 2 is GrapeClass{color=java.awt.Color[r=255,g=255,b=255], nbrOfPits=2}
Grape 1 equals grape 2? false
Grape 1 equals its copy? true
Copy the code

GrapeRecord has the same functionality as GrapeClass, but is much simpler. You create a record and indicate what the fields should be, and you’re done.

record GrapeRecord(Color color, int nbrOfPits) {}Copy the code

A record can be defined in its own file, but because it is so compact, it can be defined where needed. The test rewritten with the record above becomes the following:

    private static void basicRecord(a) {
        System.out.println(""" **************** * Basic record * ****************""");
        record GrapeRecord(Color color, int nbrOfPits) {}
        GrapeRecord grape1 = new GrapeRecord(Color.BLUE, 1);
        GrapeRecord grape2 = new GrapeRecord(Color.WHITE, 2);
        System.out.println("Grape 1 is " + grape1);
        System.out.println("Grape 2 is " + grape2);
        System.out.println("Grape 1 equals grape 2? " + grape1.equals(grape2));
        GrapeRecord grape1Copy = new GrapeRecord(grape1.color(), grape1.nbrOfPits());
        System.out.println("Grape 1 equals its copy? " + grape1.equals(grape1Copy));
    }
Copy the code

The output is the same as above. It is important to note that copies of records should end with the same copy. It’s not a good idea to add extra functionality, such as grape1.nbrofpits (), in order to do some processing and return a different value than the original nbrOfPits. Although this is allowed, you should not do it.

Constructors can be extended with some field validation. Notice that assigning parameters to record fields occurs at the end of the constructor.

    private static void basicRecordWithValidation(a) {
        System.out.println(""" ******************************** * Basic record with validation * ********************************""");
        record GrapeRecord(Color color, int nbrOfPits) {
            GrapeRecord {
                System.out.println("Parameter color=" + color + ", Field color=" + this.color());
                System.out.println("Parameter nbrOfPits=" + nbrOfPits + ", Field nbrOfPits=" + this.nbrOfPits());
                if (color == null) {
                    throw new IllegalArgumentException("Color may not be null");
                }
            }
        }
        GrapeRecord grape1 = new GrapeRecord(Color.BLUE, 1);
        System.out.println("Grape 1 is " + grape1);
        GrapeRecord grapeNull = new GrapeRecord(null.2);
    }
Copy the code

The output from the above tests shows you this functionality. Inside the constructor, the field values are still null, but they are assigned a value when the record is printed. Validation does what it should do, too, and throws an IllegalArgumentException when the color is null.

Parameter color=java.awt.Color[r=0,g=0,b=255], Field color=null
Parameter nbrOfPits=1, Field nbrOfPits=0
Grape 1 is GrapeRecord[color=java.awt.Color[r=0,g=0,b=255], nbrOfPits=1]
Parameter color=null, Field color=null
Parameter nbrOfPits=2, Field nbrOfPits=0
Exception in thread "main" java.lang.IllegalArgumentException: Color may not be null
    at com.mydeveloperplanet.myjava17planet.Records$2GrapeRecord.<init>(Records.java:40)
    at com.mydeveloperplanet.myjava17planet.Records.basicRecordWithValidation(Records.java:46)
    at com.mydeveloperplanet.myjava17planet.Records.main(Records.java:10)
Copy the code

5. Sealed Classes

Sealed classes give you more control over which classes can extend your classes. The sealed class is probably more of a useful feature for the library owner. A class may be extended in Java 11 Final. If you want to control which classes can extend your superclass, you can put all the classes in the same package and give the superclass package visibility. Everything is now under your control, but it is no longer possible to access the superclass from outside the package. Let’s go through an example to see how this works.

In the package. Com mydeveloperplanet. Myjava17planet. Creating a nonsealed has public visibility abstract class Fruit. In the same package, you created the final Apple and Pear classes, both of which extend Fruit.

public abstract class Fruit {}public final class Apple extends Fruit {}public final class Pear extends Fruit {}Copy the code

In the package com. Mydeveloperplanet. Myjava17planet SealedClasses. Create a with problemSpace method in the Java file. As you can see, you can create instances for Apple, Pear, and Apple, and you can assign apples to Fruit. You can also create a Avocado class that extends Fruit.

public abstract sealed class FruitSealed permits AppleSealed.PearSealed {}public non-sealed class AppleSealed extends FruitSealed {}public final class PearSealed extends FruitSealed {}Copy the code

Suppose you don’t want someone to extend Fruit. In this case, you can change the visibility of Fruit to default visibility (remove the public keyword). This code will no longer compile when assigning an Apple to Fruit and creating the Avocado class. The latter is needed, but we do want to be able to assign an Apple to a Fruit. This can be addressed in Java 17 with sealed classes.

In the package. Com mydeveloperplanet. Myjava17planet. Sealed, created the seal version of the Fruit, Apple and Pear. The only thing you need to do is add the Sealed keyword to the Fruit class and use the permits keyword to indicate which classes can extend the Sealed class. Subclasses need to specify whether they are final, sealed, or non-sealed. Superclasses have no control over whether or how subclasses can be extended.

public abstract sealed class FruitSealed permits AppleSealed.PearSealed {}public non-sealed class AppleSealed extends FruitSealed {}public final class PearSealed extends FruitSealed {}Copy the code

In the sealedClasses method, AppleSealed can still be assigned to FruitSealed, but Avocado does not allow FruitSealed extensions. However, AppleSealed is allowed to be extended because this subclass is indicated to be non-sealed.

    private static void sealedClasses(a) {
        AppleSealed apple = new AppleSealed();
        PearSealed pear = new PearSealed();
        FruitSealed fruit = apple;
        class Avocado extends AppleSealed {};
    }
Copy the code

6. Pattern matching of Instanceof

It is often necessary to check whether an object belongs to a certain type, and if so, the first thing to do is cast the object to a new variable of that particular type. An example can be seen in the following code:

private static void oldStyle(a) {
System.out.println(""" ************* * Old Style * *************""");
Object o = new GrapeClass(Color.BLUE, 2);
if (o instanceof GrapeClass) {
GrapeClass grape = (GrapeClass) o;
System.out.println("This grape has " + grape.getNbrOfPits() + " pits."); }}Copy the code

The output is:

This grape has 2 pits.
Copy the code

Using pattern matching with Instanceof, the above can be rewritten as follows. As you can see, variables can be created in the Instanceof check, and additional lines for creating new variables and converting objects are no longer required.

    private static void patternMatching(a) {
        System.out.println(""" ******************** * Pattern matching * ********************""");
        Object o = new GrapeClass(Color.BLUE, 2);
        if (o instanceof GrapeClass grape) {
            System.out.println("This grape has " + grape.getNbrOfPits() + " pits."); }}Copy the code

The output is of course the same as above.

It is important to look carefully at the range of variables. It should not be ambiguous. In the code below, the condition after && is evaluated only if the Instanceof check results in true. So it’s allowed. Will && changed to | | will not compile.

    private static void patternMatchingScope(a) {
        System.out.println(""" ******************************* * Pattern matching scope test * *******************************""");
        Object o = new GrapeClass(Color.BLUE, 2);
        if (o instanceof GrapeClass grape && grape.getNbrOfPits() == 2) {
            System.out.println("This grape has " + grape.getNbrOfPits() + " pits."); }}Copy the code

The code below shows another example of scope. If the object is not of GrapeClass type, a RuntimeException is thrown. In this case, the print statement is never reached. In this case, you can also use the Grape variable, because the compiler must know that Grape exists.

    private static void patternMatchingScopeException(a) {
        System.out.println(""" ********************************************** * Pattern matching scope test with exception * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *""");
        Object o = new GrapeClass(Color.BLUE, 2);
        if(! (oinstanceof  GrapeClass grape)) {
            throw new RuntimeException();
        }
        System.out.println("This grape has " + grape.getNbrOfPits() + " pits.");
    }
Copy the code

7. Useful null pointer exception

A useful NullPointerException will save you some valuable analysis time. The following code causes a NullPointerException.

public class HelpfulNullPointerExceptions {

    public static void main(String[] args) {
        HashMap<String, GrapeClass> grapes = new HashMap<>();
        grapes.put("grape1".new GrapeClass(Color.BLUE, 2));
        grapes.put("grape2".new GrapeClass(Color.white, 4));
        grapes.put("grape3".null);
        var color = ((GrapeClass) grapes.get("grape3")).getColor(); }}Copy the code

For Java 11, the output will show the line number on which NullPointerException occurred, but you don’t know which chained method resolves to NULL. You have to find yourself through debugging.

Exception in thread "main" java.lang.NullPointerException
        at com.mydeveloperplanet.myjava17planet.HelpfulNullPointerExceptions.main(HelpfulNullPointerExceptions.java:13)
Copy the code

In Java 17, the same code produces the following output showing exactly where NullPointerException occurred.

Exception in thread "main" java.lang.NullPointerException: Cannot invoke "com.mydeveloperplanet.myjava17planet.GrapeClass.getColor()" because the return value of "java.util.HashMap.get(Object)" is null
    at com.mydeveloperplanet.myjava17planet.HelpfulNullPointerExceptions.main(HelpfulNullPointerExceptions.java:13)
Copy the code

#8. Compact NumberFormat support a factory method has been added to NumberFormat to format numbers in a compact, human-readable form according to the Unicode standard. The SHORT style is shown in the code below:

        NumberFormat fmt = NumberFormat.getCompactNumberInstance(Locale.ENGLISH, NumberFormat.Style.SHORT);
        System.out.println(fmt.format(1000));
        System.out.println(fmt.format(100000));
        System.out.println(fmt.format(1000000));
Copy the code

The output is:

1K
100K
1M
Copy the code

LONG format style:

fmt = NumberFormat.getCompactNumberInstance(Locale.ENGLISH, NumberFormat.Style.LONG);
System.out.println(fmt.format(1000));
System.out.println(fmt.format(100000));
System.out.println(fmt.format(1000000));
Copy the code

The output is:

1 thousand 100 thousand 1 millionCopy the code
fmt = NumberFormat.getCompactNumberInstance(Locale.forLanguageTag("NL"), NumberFormat.Style.LONG);
System.out.println(fmt.format(1000));
System.out.println(fmt.format(100000));
System.out.println(fmt.format(1000000));
Copy the code

The output is:

1 duizend
100 duizend
1 miljoen
Copy the code

9. Added daily cycle support

A new schema B was added for formatting DateTime, which indicates the date period according to the Unicode standard.

Print several times of the day using the default Chinese locale:

System.out.println("""
 **********************
 * Chinese formatting *
 **********************""");
DateTimeFormatter dtf = DateTimeFormatter.ofPattern("B");
System.out.println(dtf.format(LocalTime.of(8, 0)));
System.out.println(dtf.format(LocalTime.of(13, 0)));
System.out.println(dtf.format(LocalTime.of(20, 0)));
System.out.println(dtf.format(LocalTime.of(23, 0)));
System.out.println(dtf.format(LocalTime.of(0, 0)));
Copy the code

The output is:

Morning, afternoon, evening, midnightCopy the code

Now in Dutch

System.out.println("""
 ********************
 * Dutch formatting *
 ********************""");
dtf = DateTimeFormatter.ofPattern("B").withLocale(Locale.forLanguageTag("NL"));
System.out.println(dtf.format(LocalTime.of(8, 0)));
System.out.println(dtf.format(LocalTime.of(13, 0)));
System.out.println(dtf.format(LocalTime.of(20, 0)));
System.out.println(dtf.format(LocalTime.of(0, 0)));
System.out.println(dtf.format(LocalTime.of(1, 0)));
Copy the code

The output is as follows. Please note that English night starts at 23:00 and Dutch night starts at 01:00. It could be cultural differences; -).

Ochtends's Middags' Avonds Middernacht's NachtsCopy the code

10. Stream.toList()

To convert a Stream to a List, you need to use the Collectors. ToList () method of collect. This is very verbose, as shown in the following example.

private static void oldStyle() { System.out.println(""" ************* * Old style * *************"""); Stream<String> stringStream = Stream.of("a", "b", "c"); List<String> stringList = stringStream.collect(Collectors.toList()); for(String s : stringList) { System.out.println(s); }}Copy the code

In Java 17, a toList method was added to replace the old behavior.

private static void streamToList() { System.out.println(""" ***************** * stream toList * *****************"""); Stream<String> stringStream = Stream.of("a", "b", "c"); List<String> stringList = stringStream.toList(); for(String s : stringList) { System.out.println(s); }}Copy the code

Conclusion 11.

In this article, you took a quick look at some of the features that have been added since the last LTS release, Java 11. It’s now up to you to think about your plans to migrate to Java 17 and learn more about these new features and how you can apply them to your daily coding habits. Hint: IntelliJ will help you solve this problem!