How does Java 17 compare to Java 11?

[note] this article is translated from: 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 the changes since the last LTS version, Java 11. We first briefly introduce the license model, and then focus on some changes between Java 11 and Java 17, mainly through examples. Enjoy it!

1. Introduction

     First, let's take a closer look at the Java licensing and support model. Java 17 is an lts (long-term support) version, just like Java 11. Java 11 has begun a new release rhythm. Java 11 is supported until September 2023 and extended support until September 2026. In addition, in Java 11, Oracle JDK is no longer free for production and commercial purposes. A new Java version is released every six months, the so-called non lts release, from Java 12 to and including Java 16. However, these are production ready versions. The only difference from the LTS version is that it supports ending when the next version is released. For example. Support for Java 12 ends when Java 13 is released. When you want to maintain support, you must more or less 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, the company 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 use Java 11 in production, which has been three years since the release of Java 11! 60% of companies still use Java 8. Another interesting thing to note is that the next lts version will be Java 21, which will be released within two years. A good overview of whether the library has problems in Java 17 can be found in here Found.

     With the introduction of Java 17, the Oracle licensing model has changed. Java 17 is released under the new NFTC (Oracle free terms and conditions) license. Therefore, the Oracle JDK version is again allowed to be used for production and commercial purposes free of charge. In the same Snyk survey, it was pointed out that the Oracle JDK version is only used by 23% of users in the production environment. Please note that support for the LTS version will end one year after the next lts version is released. It will be interesting to see how this will affect upgrading to the next lts version.

     What has changed between Java 11 and Java 17? The complete list of JEP (Java enhancement proposal) 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 version since Java 11.

     In the next section, some changes will be explained by examples, but it's up to you to experiment with these new features to get familiar with them. All the resources used in this article can be found on GitHub.

     The last thing is that Oracle 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. Text blocks undoubtedly make the code more readable. First, let's look at the problem. Suppose you need some JSON strings into your code and you need to print it. There are several problems with this Code:

  • Escape of double quotation marks;
  • String connection to make it more or less readable;
  • Copying and pasting JSON is a labor-intensive job (your IDE may help you solve this problem).
    private static void oldStyle() {
        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);
    }

     The output of the above code is well formed JSON.

{
    "name": "John Doe",
    "age": 45,
    "address": "Doe Street, 23, Java Town"
}

     The text block is defined by three double quotation marks, and the three double quotation marks at the end cannot be on the same line as the beginning. First, just print an empty block. To visualize what happened, the text was printed between two tubes.

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

     The output is:

||||

The problematic JSON part can now be written as follows, which is more readable. You don't need to escape double quotes, it looks like it will be printed.

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

     The output is of course the same.

{
    "name": "John Doe",
    "age": 45,
    "address": "Doe Street, 23, Java Town"
}

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

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

     The output now prints two spaces before each line. This means that the three double quotes at the end represent the beginning of the text block.

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

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

    private static void jsonMovedEndQuoteBlock() {
        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);
    }

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

{
    "name": "John Doe",
    "age": 45,
    "address": "Doe Street, 23, Java Town"
}

3. Switch expression

     Switch expressions will allow you to return values from switch and use them in assignments, etc. A classic switch is shown here, where some operations need to be performed according to the given Fruit enumeration value. Deliberately ignored break.

    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");
        }
    }

     Use APPLE to call this method.

oldStyleWithoutBreak(Fruit.APPLE);

     This will print each case, because without a break statement, the case will fail.

Common fruit
Exotic fruit
Undefined fruit

     Therefore, it is necessary to add a break statement in each case to prevent this failure.

    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");
        }
    }

     Running this method will give you the desired results, but the code is now slightly less readable.

Common fruit

     This can be solved by using Switch expressions. Replace colon with arrow (- >)( 😃 And make sure to use expressions in case. The default behavior of Switch expressions is no failure, so there is no need to break.

    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");
        }
    }

     This is no longer so wordy. The result is the same.

     A Switch expression can also return a value. In the above example, 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 parenthesis of the last case.

    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);
    }

     What's more, the above content can be rewritten with one statement. Whether it is more readable than the above depends on 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";
            });
    }

     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 the keyword yield 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);
    }

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

the given fruit was: APPLE
Common fruit

     You can use the yield keyword in the "old" switch syntax. It's also cool. There's no need to 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";
        });
    }

4. Records

     Records will allow you to create immutable data classes. At present, you need to create a GrapeClass, for example, using the IDE's auto generation function to generate constructors, getter s, hashcodes, equals, and toString, or you can use Lombok to do the same. Finally, you'll get some boilerplate code, or your project will eventually rely 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() {
        return color;
    }

    public int getNbrOfPits() {
        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() {
        return Objects.hash(color, nbrOfPits);
    }

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

}

     Use the GrapeClass class described above to perform some tests. Create two instances, print them, compare them, create a copy, and compare this as well.

    private static void oldStyle() {
        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));
    }

     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

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

record GrapeRecord(Color color, int nbrOfPits) {
}

     A record can be defined in its own file, but because it is very compact, it is also possible to define it where needed. The test rewritten with record above becomes as follows:

    private static void basicRecord() {
        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));
    }

     The output is the same as above. It is important to note that copies of records should end with the same copy. Adding additional features, such as grape1.nbrOfPits() to do some processing and return a value different from the initial nbrOfPits, is a bad practice. Although this is allowed, you should not do so.

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

    private static void basicRecordWithValidation() {
        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);
    }

     The output of the above test shows you this function. Inside the constructor, the field values are still null, but when the record is printed, they are assigned a value. Validation also does what it should do 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)

5. Sealed Classes

     Sealing classes gives you better control over which classes can extend your classes. Sealed classes may be more like a function useful to library owners. A class can be extended in Java 11 final. If you want to control which classes can extend your superclass, you can put all classes in the same package and give visibility to the superclass package. Now everything is under your control, but it is no longer possible to access superclasses from outside the package. Let's take an example to see how this works.

     In package
Create an abstract class Fruit with public visibility in com.mydeveloperplanet.myjava17planet.nonsealed. In the same package, the final classes Apple and Pear are created, both of which extend Fruit.

public abstract class Fruit {
}
public final class Apple extends Fruit {
}
public final class Pear extends Fruit {
}

     In package
Create a SealedClasses.java file with the problemSpace method in com.mydeveloperplanet.myjava17planet. As you can see, you can create instances for Apple, Pear, and Apple, and assign Apple to Fruit. In addition, you can create an 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 {
}

     Suppose you don't want someone to extend Fruit. In this case, you can change the visibility of Fruit to the default visibility (remove the public keyword). The above code will not be compiled when Apple is assigned to Fruit and Avocado classes are created. The latter is needed, but we do want to be able to assign an Apple to a Fruit. This can be solved in Java 17 with sealed classes.

In package
com.mydeveloperplanet.myjava17planet.sealed, sealed versions of Fruit, Apple, and Pear are created. The only thing to do is to add the sealed keyword to the Fruit class and use the permissions keyword to indicate which classes can extend the sealed class. Subclasses need to indicate whether they are final, sealed or non sealed. Superclasses cannot control 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 {
}

     In the sealedClasses method, AppleSealed can still be assigned to FruitSealed, but Avocado does not allow extension of FruitSealed. However, AppleSealed is allowed to be extended because this subclass is indicated as unsealed.

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

6. Pattern matching of instanceof

     It is usually necessary to check whether the object belongs to a certain type. If so, the first thing to do is to cast the object to a new variable of that specific type. You can see an example in the following code:

private static void oldStyle() {
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.");
 }
 }

     The output is:

This grape has 2 pits.

     Using the pattern matching of 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 transforming objects are no longer required.

    private static void patternMatching() {
        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.");
        }
    }

     The output is of course the same as above.

     It is important to look closely at the range of variables. It should not be ambiguous. In the following code, the condition after & & will only be evaluated when the instanceof check result is true. So this is allowed. Changing & & to ðž“œ will not compile.

    private static void patternMatchingScope() {
        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.");
        }
    }

     The following code shows another example of a scope. If the object is not of type GrapeClass, 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() {
        System.out.println("""
                **********************************************
                * Pattern matching scope test with exception *
                **********************************************""");
        Object o = new GrapeClass(Color.BLUE, 2);
        if (!(o instanceof  GrapeClass grape)) {
            throw new RuntimeException();
        }
        System.out.println("This grape has " + grape.getNbrOfPits() + " pits.");
    }

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();
    }
}

     For Java 11, the output will show the line number where the NullPointerException occurred, but you don't know which chained method resolves to null. You must find yourself by debugging.

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

     In Java 17, the same code produces the following output, which shows exactly where the 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)

#8. Streamlined digital format 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 format style is shown in the following code:

        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));

     The output is:

1K
100K
1M

     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));

     The output is:

1 thousand
100 thousand
1 million
 Dutch for English LONG Format:
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));

     The output is:

1 duizend
100 duizend
1 miljoen

9. Added daily cycle support

     A new pattern B has been added to format DateTime, which indicates the date time period according to the Unicode standard.

     Use the default Chinese language environment to print several times of the day:

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)));

     The output is:

morning
 afternoon
 night
 night
 mid-night

     Now use the Dutch local environment:

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)));

     The output is as follows. Please note that the British night starts at 23 o'clock and the Dutch night starts at 01 o'clock. It may be cultural differences; -).

's ochtends
's middags
's avonds
middernacht
's nachts

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);
        }
    }

     In Java 17, a toList method is 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);
        }
    }

11. Conclusion

     In this article, you took a quick look at some of the features added since the last LTS version of Java 11. Now it's up to you to consider 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. Tip: IntelliJ will help you solve this problem!

Keywords: Java Oracle

Added by alexander.s on Fri, 15 Oct 2021 23:14:31 +0300