头图

[Note] This article is translated from: What's New Between Java 11 and Java 17?

On September 14th, Java 17 was released. It's time to take a closer look at the changes since the last LTS version (ie Java 11). We first briefly introduce the licensing model, and then focus on some of the 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 support until September 2023, and extended support until September 2026. In addition, in Java 11, Oracle JDK is no longer free for production and commercial use. A new Java version is released every 6 months, the so-called non-LTS release, from Java 12 up to and including Java 16. However, these are all production-ready versions. The only difference from the LTS version is that support ends when the next version is released. E.g. Java 12 support ended when Java 13 was released. When you want to maintain support, you more or less have to upgrade to Java 13. This may cause some problems when some of your dependencies are not yet ready for Java 13. In most cases, for production use, the company will wait for the LTS version. But even so, some companies are reluctant to upgrade. A recent Snyk survey showed that only 60% of people use Java 11 in production, and it has been 3 years since Java 11 was released! 60% of companies are still using Java 8. Another interesting thing worth noting is that the next LTS version will be Java 21, which will be released in 2 years. A good overview of whether the library has problems in Java 17 can be found at here .

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, it is again allowed to use the Oracle JDK version for production and commercial use free of charge. In the same Snyk survey, someone pointed out that the Oracle JDK version is only used by 23% of users in a 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 affects upgrading to the next LTS version.

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. For a complete list of changes in each version since Java 11, the Oracle release notes provide a good overview.

In the following sections, some changes will be explained through examples, but it is mainly up to you to experiment with these new features to become 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

To make Java more readable and more concise, many improvements have been made. The text block definitely makes the code more readable. First, let's look at the problem. Suppose you need some JSON string into your code and you need to print it. There are several problems with this code:

  • Escaping double quotes;
  • String concatenation to make it more or less readable;
  • Copying and pasting JSON is a labor-intensive task (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 ending three double quotation marks cannot be on the same line as the beginning. First, just print an empty block. In order to visualize what is happening, the text is printed between the two double tubes.

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

The output is:

||||

The JSON part in question can now be written as follows, which makes it more readable. There is no need to escape the 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 preceding output, there are no leading spaces. However, in the code, there are spaces in front. How to determine the stripping of the preceding spaces? First, move the three double quotation marks at the end 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 indicate 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 previous 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 the switch and use these return values in assignments, etc. A classic switch is shown here, in which, according to a given Fruit enumeration value, some operations need to be performed. Ignore break deliberately.

    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 the method.

oldStyleWithoutBreak(Fruit.APPLE);

This will print each case, because there is no break statement, the case is invalid.

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 result, but now the code is less readable.

Common fruit

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

    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 not so wordy anymore, and the result is the same.

Switch expressions can also return a value. In the example above, you can return String values and assign them to the variable text. After this, you can print the text variable. Don't forget to add a semicolon after the last case bracket.

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

And, even shorter, the above content can be rewritten in one sentence. 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";
            });
    }

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 denote a case block, and use the yield keyword when returning a 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 bit different, and two print statements are executed.

the given fruit was: APPLE
Common fruit

It's also cool that you can use the yield keyword in the "old" switch syntax. 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. Currently, you need to create GrapeClass to generate constructor, getter, hashCode, equals, and toString, for example, using IDE's auto-generated functions, or you can use Lombok to achieve the same purpose. In the end, you will 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 +
                '}';
    }

}

Perform some tests using the GrapeClass class described above. 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 is much simpler. You create a record and indicate what the fields should be, and then you are 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 above test rewritten with records 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 the recorded copy should end with the same copy. It is a bad practice to add additional functions, such as grape1.nbrOfPits() in order to do some processing and return a different value from the initial nbrOfPits. Although this is allowed, you should not do it.

The constructor 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 feature. Inside the constructor, the field values are still null, but when the record is printed, they are assigned a value. Verification 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

Sealed classes will give you more control over which classes can extend your class. Sealed classes may be more of a function useful to library owners. A class is in Java 11 final or can be extended. If you want to control which classes can extend your super class, you can put all classes in the same package and give the super class package visibility. Everything is now under your control, but it is no longer possible to access the superclass from outside the package. Let's see how this works through an example.

In the 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 the package
Create a SealedClasses.java file with problemSpace method in com.mydeveloperplanet.myjava17planet. As you can see, you can create instances for Apple, Pear, and Apple, and you can assign Apple to Fruit. In addition, you can also 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). When assigning Apple to Fruit and creating the Avocado class, the above code will no longer compile. 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 the package
In com.mydeveloperplanet.myjava17planet.sealed, sealed versions of Fruit, Apple and Pear have been created. The only thing to do is to add the sealed keyword to the Fruit class and use the permits keyword to indicate which classes can extend this Sealed class. Subclasses need to indicate whether they are final, sealed or non-sealed. The superclass has no control over whether and how the subclass 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 FruitSealed to be extended. However, it is allowed to extend AppleSealed 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. Instanceof pattern matching

It is usually necessary to check whether the object belongs to a certain type. If it is, the first thing to do is to cast the object into 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 instanceof's pattern matching, the above can be rewritten as follows. As you can see, variables can be created in the instanceof check, and the extra lines for creating new variables and transforming objects are no longer needed.

    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 carefully at the scope of the variable. It should not be ambiguous. In the code below, 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 scope. If the object is not of type GrapeClass, RuntimeException is thrown. In this case, the print statement is never reached. In this case, the grape variable can also be used, because the compiler must know that the 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

The 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 chain method resolves to null. You must find yourself through 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
荷兰语替换英语的 LONG 格式:
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 mode B is added for formatting DateTime, which indicates the date and time period according to the Unicode standard.

Use the default Chinese locale to print a few moments 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:

上午
下午
晚上
晚上
午夜

Now use the Dutch locale:

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 nights in the UK start at 23 o'clock and nights in the Netherlands start at 01 o'clock. It may be cultural differences ;-).

’s ochtends
’s middags
’s avonds
middernacht
’s nachts

10. Stream.toList()

In order to convert Stream to List, you need to use the Collectors.toList() method of collect. This is very verbose, as shown in the example below.

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

11. Conclusion

In this article, you took a quick look at some of the features that have been added since the last LTS version, Java 11. Now it's up to you to consider your plan 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!


信码由缰
65 声望8 粉丝

“码”界老兵,分享程序人生。