JDK new features:修订间差异

来自WHY42
Riguz留言 | 贡献
标签2017版源代码编辑
Riguz留言 | 贡献
标签2017版源代码编辑
第457行: 第457行:
==Local-Variable Type Inference ==
==Local-Variable Type Inference ==
Since {{Label |red| JDK 11}} Preview {{Label |yellow| JDK 10}}
Since {{Label |red| JDK 11}} Preview {{Label |yellow| JDK 10}}
<syntaxhighlight lang="java">
var greetingMessage = "Hello!";
</syntaxhighlight>
The type of the declared variables is inferred at compile time.


==Allow private methods in interfaces ==
==Allow private methods in interfaces ==

2024年10月28日 (一) 08:53的版本

New Language Features

Pattern Matching for switch

Since JDK 21 Preview JDK 20 JDK 19 JDK 18 JDK 17

Previously, switch was very limited: the cases could only test exact equality, and only for values of a few types: numbers, Enum types and Strings.

This feature enhances switch to work on any type and to match on more complex patterns.

These additions are backwards compatible, switch with the traditional constants work just as before, for example, with Enum values:

enum Status { Normal, Deleted }
public int getStatus(Status s) {
    return switch(s) {
        case Normal -> 0;
        case Deleted -> -1;
    };
}

However, now it also works with type patterns introduced by JEP 394: Pattern Matching for instanceof:

class Item {}
class Text extends Item {}
class Image extends Item {
    public String getImageType() { return "png"; }
}

public void printItem(Item item) {
    var msg = switch(item) {
        case Text ignored -> "Text";
        case Image ignored -> "Image";
        default -> "Unknow item";
    };
    System.out.println(msg);
}

A pattern supports guards, written as type pattern when guard expression:

var msg = switch(item) {
    case Text ignored -> "Text";
    case Image img when img.size > 1024 -> "Large Image";
    case Image ignored -> "Image";
    default -> "Unknow item";
};

Similarly to the type patterns in if conditions, the scope of the pattern variables are flow sensitive. Generally it works just as you'd expect, but there are many rules and edge cases involved.

Switch expressions always had to be exhaustive, in other words, they have to cover all possible input types. This is still the case with the new patterns, the Java compiler emits an error when the switch is incomplete.

Switch can now also match null values. Traditionally, when a null value was supplied to a switch, it threw a NullPointerException. For backwards compatibility, this is still the case when there's no explicit null pattern defined. However, now an explicit case for null can be added:

var msg = switch(item) {
    // without this, a NullPointerException throws if item is null.
    case null -> "Empty"; 
    case Text ignored -> "Text";
    case Image img when img.size > 1024 -> "Large Image";
    case Image ignored -> "Image";
    default -> "Unknow item";
};

This feature synergizes well with enums, Sealed Classes and generics. If there's only a fixed set of possible alternatives that exist, the default case can be omitted. Also, this feature helps a lot to maintain the integrity of the codebase when the domain is extended — for example, with a new constant in the enum. Due to the exhaustiveness check, all related switch expressions will yield a compiler error where the default case is not handled.

sealed interface I<T> permits A, B {}
final class A<X> implements I<String> {}
final class B<Y> implements I<Y> {}

static int testGenericSealedExhaustive(I<Integer> i) {
    return switch (i) {
        case B<Integer> bi -> 42;
    };
}

Exhaustiveness is checked at compile time but if at runtime a new implementation pops up (e.g. from a separate compilation), the compiler also inserts a synthetic default case that throws a MatchException.

The compiler also performs the opposite of the exhaustiveness check: it emits an error when a case completely dominates another.

    case Image ignored -> "Image";
    // Label is dominated by a preceding case label 'Image ignored'
    case Image img when img.size > 1024 -> "Large Image";

For readability reasons, the dominance checking forces constant case labels to appear before the corresponding type-based pattern.

Record Patterns

Since JDK 21 Preview JDK 20 JDK 19

Record Patterns extends the pattern matching capabilities of Java beyond simple type patterns to match and deconstruct Record values. It supports nesting to enable declarative, data focused programming.

interface Point { }
record Point2D(int x, int y) implements Point { }
enum Color { RED, GREEN, BLUE }
record ColoredPoint(Point p, Color c) { }

Object r = new ColoredPoint(new Point2D(3, 4), Color.GREEN);

Note, that while we use a Point2D to construct r, ColoredPoint accepts other Point implementations as well.

Without pattern matching, we'd need two explicit type checks to detect if object r is a ColoredPoint that holds Point2D, and also quite some work to extract the values x, y, and c:

if (r instanceof ColoredPoint) {
  ColoredPoint cp = (ColoredPoint) r;
  if (cp.p() instanceof Point2D) {
    Point2D pt = (Point2D) cp.p();
    int x = pt.x();
    int y = pt.y();
    Color c = cp.c();

    // work with x, y, and c
  }
}

Using type patterns for instanceof makes things a bit better as there's no longer a need for the casts, but conceptually the same thing needs to be done as previously:

if (r instanceof ColoredPoint cp && cp.p() instanceof Point2D pt) {
    int x = pt.x();
    int y = pt.y();
    Color c = cp.c();

    // work with x, y, and c
}

However this can be greatly simplified with Record Patterns that offer a concise way to test the shape of the data and to deconstruct it:

if (r instanceof ColoredPoint(Point2D(int x, int y), Color c)) {
  // work with x, y, and c
}

Patterns can be nested, which allows easy navigation in the objects, allowing the programmer to focus on the data expressed by those objects. Also it centralizes error handling. A nested pattern will only match if all subpatterns match. There's no need to manually handle each individual branch manually.

Record patterns also support not just instanceof but the Switch expression as well:

var length = switch (r) {
	case ColoredPoint(Point2D(int x, int y), Color c) -> Math.sqrt(x*x + y*y);
	case ColoredPoint(Point p, Color c) -> 0;
}

Unnamed Patterns and Variables (Preview)

Preview JDK 21

In Java 8, the compiler emitted a warning when '_' was used as an identifier. Java 9 took this a step further making the underscore character illegal as an identifier, reserving this name to have special semantics in the future. The goal of these actions was to prepare the stage for unnamed variables, which is a preview feature in Java 21.

var _ = mySet.add(x); // ignore the return value

try {
   // ...
} catch (Exception _) { // ignore the exception object
   // ...
}

list.stream()
  .map((_) -> /* ... */) // ignore the parameter
  .toList();

A related feature is called unnamed patterns, which can be used in pattern matching to ignore subpatterns.

if (r instanceof ColoredPoint(Point(int x, int y), _)) {
  // ... x ... y ...
}

String Templates (Preview)

Preview JDK 21

String Templates are an extension to the single-line String literals and Text Blocks, allowing String interpolation and much more.

var name = "Duke";
var info = STR."My name is \{name}";

In addition to STR, there are a few more built-in processors. One is FMT, which is just like STR but accepts format specifiers that appear left to the embedded expression as specified by java.util.Formatter:

double value = 17.8D;
var result = FMT."Value is %7.2f\{value}";

Finally, there's the RAW processor which can be used to create StringTemplate objects, to be processed later by an other processor:

StringTemplate template = RAW."Hello \{name}";
// ...
String result = STR.process(template);

Unnamed Classes and Instance Main Methods (Preview)

Preview JDK 21

void main() {
  System.out.println("Hello, World!");
}

Sealed Classes

Since JDK 17 Preview JDK 15 JDK 16

Sealed Classes have been added to the Java Language. Sealed classes and interfaces restrict which other classes or interfaces may extend or implement them.

Sealed Classes were proposed by JEP 360 and delivered in JDK 15 as a preview feature. They were proposed again, with refinements, by JEP 397 and delivered in JDK 16 as a preview feature. Now in JDK 17, Sealed Classes are being finalized with no changes from JDK 16.

For further details, see JEP 409.[1]

Goals of sealed classes:

  • Allow the author of a class or interface to control which code is responsible for implementing it.
  • Provide a more declarative way than access modifiers to restrict the use of a superclass.
  • Support future directions in pattern matching by providing a foundation for the exhaustive analysis of patterns.

Option1: define inner classes:

public abstract sealed class Chinese {
    final class SimplifiedChinese extends Chinese{}
    final class TradictionalChinese extends Chinese{}
}

// a sealed class could not be extended, otherwise you get an compiler error:
// public class Other extends Chinese {}
// 'Other' is not allowed in the sealed hierarchy

Option2:

public abstract sealed class Language permits English, Chinese{
}
// the permitted class is either final, sealed or non-sealed
public final class English extends Language{
}


Difference between sealed/final:

  • both sealed/final class could not be extended again
  • final class does not have sub classes
  • sealed class contains sub classes
  • if you want a sub class to be extended by the user, make it non-sealed

(The final modifier can be considered a special case of sealing, where extension/implementation is prohibited completely. That is, final is conceptually equivalent to sealed plus a permits clause which specifies nothing, though such a permits clause cannot be written.)

Another example:

public sealed class Shape
    permits Circle, Quadrilateral, WeirdShape {...}

public final class Circle extends Shape {...}

public sealed class Quadrilateral extends Shape
    permits Rectangle, Parallelogram {...}
public final class Rectangle extends Quadrilateral {...}
public final class Parallelogram extends Quadrilateral {...}

public non-sealed class WeirdShape extends Shape {...}


Record Classes

Since JDK 16 Preview JDK 14 JDK 15

Record Classes introduce a new type declaration to the language to define immutable data classes. Instead of the usual ceremony with private fields, getters and constructors, it allows us to use a compact syntax:

public record Point(int x, int y) { }

var point = new Point(1, 2);
point.x(); // returns 1
point.y();

Record Classes intended to be transparent carriers of their shallowly immutable data. To support this design they come with a set of restrictions. Fields of a Record Class are not only final by default, it's not even possible to have any non-final fields.

Finally, Record Classes can't extend other classes, they can't declare native methods, and they are implicitly final and can't be abstract.

Similarly to getters, hashCode, equals and toString methods are provided by default considering all fields; these methods can also be explicitly defined.

Pattern Matching for instanceof

Since JDK 16 Preview JDK 14 JDK 15

The pattern is a combination of a test (obj instanceof String) and a pattern variable (s).

if (obj instanceof String s && s.length() > 5) {
  // use s
}

Early returns and exceptions can also guarantee matches:

private static int getLength(Object obj) {
  if (!(obj instanceof String s)) {
    throw new IllegalArgumentException();
  }

  // s is in scope - if the instanceof does not match
  //      the execution will not reach this statement
  return s.length();
}

Text Blocks

Since JDK 15 Preview JDK 13 JDK 14

Java 15 introduced multi-line string literals called Text Blocks:

String html = """
          <html>
            <body>
              <p>Hello, world</p>
            </body>
          </html>
          """;

System.out.println(html);

For each line-break in the source code, there will be a \n character in the result. This can be prevented by ending the line with the \ character, which can be useful in case of very long lines that you'd like to split into two for keeping the source code readable.

String singleLine = """
          Hello \
          World
          """;

Text Blocks can be aligned with neighboring Java code because incidental indentation is automatically removed. The compiler checks the whitespace used for indentation in each line to find the least indented line, and shifts each line to the left by this minimal common indentation. This means that if the closing """ is in a separate line, the indentation can be increased by shifting the closing token to the left.

String noIndentation = """
          First line
          Second line
          """;

String indentedByToSpaces = """
          First line
          Second line
        """;

Helpful NullPointerExceptions

Since JDK 15 Preview JDK 14

Traditionally, experiencing a NullPointerException was like this:

node.getElementsByTagName("name").item(0).getChildNodes().item(0).getNodeValue();

Exception in thread "main" java.lang.NullPointerException
        at Unlucky.method(Unlucky.java:83)

From the exception it's not obvious which method returned null in this case. For this reason many developers used to spread such statements over multiple lines to make sure they'll be able to figure out which step led to the exception.

From Java 15, there's no need to do that because NPE's describe which part was null in the statement. (Also, in in Java 14 you can enable it with the -XX:+ShowCodeDetailsInExceptionMessages flag.)

Exception in thread "main" java.lang.NullPointerException:
  Cannot invoke "org.w3c.dom.Node.getChildNodes()" because
  the return value of "org.w3c.dom.NodeList.item(int)" is null
        at Unlucky.method(Unlucky.java:83)

Switch Expressions

Since JDK 14 Preview JDK 12 JDK 13

The good old switch got a facelift in Java 14. While Java keeps supporting the old switch statement, it adds the new switch expression to the language:

int numLetters = switch (day) {
    case MONDAY, FRIDAY, SUNDAY -> 6;
    case TUESDAY                -> 7;
    default      -> {
        String s = day.toString();
        int result = s.length();
        yield result;
    }
};

The most striking difference is that this new form can be used as an expression.

However, there are some other, more subtle differences between switch expressions and switch statements.

First, for switch expressions cases don't fall-through. So no more subtle bugs caused by missing breaks. To avoid the need for fall-through, multiple constants can be specified for each case in a comma separated list.

Second, each case has its own scope.

String s = switch (k) {
    case  1 -> {
        String temp = "one";
        yield temp;
    }
    case  2 -> {
        String temp = "two";
        yield temp;
    }
    default -> "many";
}

Third, cases of a switch expression are exhaustive. This means that for String, primitive types and their wrappers the default case always has to be defined.

int k = 3;
String s = switch (k) {
    case  1 -> "one";
    case  2 -> "two";
    default -> "many";
}

For enums either a default case has to be present, or all cases have to be explicitly covered.

Switch expression can not only used with the lambda-like arrow-form cases. The old switch statement with its colon-form cases can also be used as an expression using yield:

int result = switch (s) {
    case "foo":
    case "bar":
        yield 2;
    default:
        yield 3;
};

Local-Variable Type Inference

Since JDK 11 Preview JDK 10

var greetingMessage = "Hello!";

The type of the declared variables is inferred at compile time.

Allow private methods in interfaces

Since JDK 9

Diamond operator for anonymous inner classes

Since JDK 9

Allow effectively-final variables to be used as resources in try-with-resources statements

Since JDK 9


Underscore is no longer a valid identifier name

Since JDK 9


Improved Warnings

Since JDK 9


[2] [3]