-
JEP
-
Resolution: Unresolved
-
P4
-
None
-
None
-
Angelos Bimpoudis
-
Feature
-
Open
-
SE
-
-
M
-
M
Summary
Enhance pattern matching by allowing primitive types in all pattern contexts,
and extend instanceof and switch to work with all primitive types. This is a
preview language feature.
History
This feature was originally proposed by JEP 455 (JDK 23) and re-previewed by JEP 488 (JDK 24) and JEP 507 (JDK 25) without change.
We here propose to preview it for a fourth time with two changes: Enhance the
definition of unconditional exactness, and apply
tighter dominance checks in switch constructs. These changes
enable the compiler to identify a wider range of coding errors, although a small
number of switch constructs that were previously legal will now be rejected.
Goals
Enable uniform data exploration by allowing type patterns for all types, whether primitive or reference.
Align type patterns with
instanceof, and aligninstanceofwith safe casting.Allow pattern matching to use primitive types in both nested and top-level pattern contexts.
Provide easy-to-use constructs that eliminate the risk of losing information due to unsafe casts.
Following the enhancements to
switchin Java 5 (enumswitch) and Java 7 (stringswitch), allowswitchto process values of any primitive type.
Non-Goals
- It is not a goal to add new kinds of conversions to the Java language.
Motivation
Multiple restrictions pertaining to primitive types impose friction when using
pattern matching, instanceof, and switch. Eliminating these restrictions
would make the language more uniform and more expressive.
Pattern matching for switch does not support primitive type patterns
The first restriction is that pattern matching for switch
(JEP 441) does not support primitive type
patterns, i.e., type patterns that specify a primitive type. The only type
patterns supported in switch are those that specify a reference type, such as
case Integer i or case String s, and those that specify a record
(JEP 440), such as case Point(int x, int
y).
With support for primitive type patterns in switch, we could improve
the switch expression
switch (x.getStatus()) {
case 0 -> "okay";
case 1 -> "warning";
case 2 -> "error";
default -> "unknown status: " + x.getStatus();
}
by turning the default clause into a case clause with a primitive
type pattern that exposes the matched value:
switch (x.getStatus()) {
case 0 -> "okay";
case 1 -> "warning";
case 2 -> "error";
case int i -> "unknown status: " + i;
}
Supporting primitive type patterns would also allow guards to inspect the matched value:
switch (x.getYearlyFlights()) {
case 0 -> ...;
case 1 -> ...;
case 2 -> issueDiscount();
case int i when i >= 100 -> issueGoldCard();
case int i -> ... appropriate action when i > 2 && i < 100 ...
}
Record patterns have limited support for primitive types
Another restriction is that record patterns have limited support for primitive types. Record patterns streamline data processing by decomposing a record into its individual components. When a component is a primitive value, the record pattern must be precise about the type of the value. This is inconvenient for developers and inconsistent with the presence of helpful automatic conversions in the rest of the language.
For example, suppose we wish to process JSON data represented via these record classes:
sealed interface JsonValue {
record JsonString(String s) implements JsonValue { }
record JsonNumber(double d) implements JsonValue { }
record JsonObject(Map<String, JsonValue> map) implements JsonValue { }
}
JSON does not distinguish integers from non-integers, so JsonNumber represents
a number with a double component for maximum flexibility. We do not need to
pass a double when creating a JsonNumber record; we can pass an int such
as 30, and the compiler automatically widens the int to double:
var json = new JsonObject(Map.of("name", new JsonString("John"),
"age", new JsonNumber(30)));
Unfortunately, the compiler is not so obliging when decomposing a JsonNumber
with a record pattern. Since JsonNumber is declared with a double component,
we must decompose a JsonNumber with respect to double, and convert to int
manually:
if (json instanceof JsonObject(var map)
&& map.get("name") instanceof JsonString(String n)
&& map.get("age") instanceof JsonNumber(double a)) {
int age = (int)a; // unavoidable (and potentially lossy!) cast
}
In other words, primitive type patterns can be nested inside record patterns
but are invariant: The primitive type in the pattern must be identical to the
primitive type of the record component. It is not possible to decompose a
JsonNumber via instanceof JsonNumber(int age) and have the compiler
automatically narrow the double component to int.
The reason for this limitation is that narrowing might be lossy: The value of
the double component at run time might be too large, or have too much
precision, for an int variable. However, a key benefit of pattern matching is
that it rejects illegal values automatically, by simply not matching. If the
double component of a JsonNumber is too large or too precise to narrow
safely to an int, then instanceof JsonNumber(int age) could simply return
false, leaving the program to handle a large double component in a
different branch.
With support for primitive type patterns, we could lift this limitation. Pattern
matching could safeguard against a possibly lossy narrowing conversion of a
value to a primitive type, both at the top level and when nested inside record
patterns. Since any double can be converted to an int, the primitive type
pattern int a would be applicable to the corresponding component of
JsonNumber of type double. If, and only if, the double component can be
converted to an int without loss of information, then instanceof would match
the pattern and the if-branch would be taken, with the local variable a in
scope:
if (json instanceof JsonObject(var map)
&& map.get("name") instanceof JsonString(String n)
&& map.get("age") instanceof JsonNumber(int a)) {
... n ...
... a ...
}
This would enable nested primitive type patterns to work as smoothly as nested reference type patterns.
Pattern matching for instanceof does not support primitive type patterns
Yet another restriction is that pattern matching for instanceof
(JEP 394) does not support primitive type
patterns. The only type patterns supported in instanceof are those that
specify a reference type or a record.
Primitive type patterns would be just as useful in instanceof as they are in
switch. The purpose of instanceof is, broadly speaking, to test whether a
value can be converted safely to a given type; this is why we always see
instanceof and cast operations in close proximity. This test is critical for
primitive types because of the potential loss of information that can occur
when converting primitive values from one type to another.
For example, converting an int value to a float is performed automatically
by an assignment statement even though it is potentially lossy — and we receive
no warning of this:
int getPopulation() {...}
float pop = getPopulation(); // silent potential loss of information
Meanwhile, converting an int value to a byte is performed with an explicit
cast, but the cast is potentially lossy, so it must be preceded by a laborious
range check:
if (i >= -128 && i <= 127) {
byte b = (byte)i;
... b ...
}
Primitive type patterns in instanceof would subsume the lossy conversions
built into the language and avoid the painstaking range checks that we have been
coding by hand for three decades. In other words, instanceof could check
values as well as types. The two examples above could be rewritten as:
if (getPopulation() instanceof float pop) {
... pop ...
}
if (i instanceof byte b) {
... b ...
}
The instanceof operator combines the convenience of an assignment statement
with the safety of pattern matching. If the input can be converted safely to the
type in the primitive type pattern then the pattern matches and the result of
the conversion is immediately available. But, if the conversion would lose
information then the pattern does not match and the program should handle the
invalid input in a different branch.
Primitive types in instanceof and switch
If we are going to lift restrictions around primitive type patterns then it
would be helpful to lift a related restriction: When instanceof takes a type,
rather than a pattern, it takes only a reference type, not a primitive type. If
instanceof could take a primitive type then it would check if the conversion
is safe, but would not actually perform it:
if (i instanceof byte) { // value of i fits in a byte
... (byte)i ... // traditional cast required
}
This enhancement to instanceof restores alignment between the semantics of
instanceof T and instanceof T t, which would be lost if we allowed primitive
types in one context but not the other.
Finally, it would be helpful to lift the restriction that switch can take
byte, short, char, and int values but not boolean, float, double,
or long values.
Switching on boolean values would be a useful alternative to the ternary
conditional operator (?:) because a boolean switch can contain statements
as well as expressions. For example, the following code uses a boolean switch
to perform some logging when false:
startProcessing(OrderStatus.NEW, switch (user.isLoggedIn()) {
case true -> user.id();
case false -> { log("Unrecognized user"); yield -1; }
});
Switching on long values would allow case labels to be long constants,
obviating the need to handle large constants with separate if statements:
long v = ...;
switch (v) {
case 1L -> ...;
case 2L -> ...;
case 10_000_000_000L -> ...;
case 20_000_000_000L -> ...;
case long x -> ... x ...;
}
Description
Currently, primitive type patterns are permitted only as nested patterns in record patterns, and only when they name the type of the match candidate exactly, as in:
v instanceof JsonNumber(double a)
To support more uniform data exploration of a match candidate v with pattern
matching, we will:
Extend pattern matching so that primitive type patterns are applicable to a wider range of match candidate types. This will allow expressions such as
v instanceof JsonNumber(int age).Enhance the
instanceofandswitchconstructs to support primitive type patterns as top level patterns.Further enhance the
instanceofconstruct so that, when used for type testing rather than pattern matching, it can test against all types, not just reference types. This will extendinstanceof's current role, as the precondition for safe casting on reference types, to apply to all types.More broadly, this means that
instanceofcan safeguard all conversions, whether the match candidate is having its type tested (e.g.,x instanceof int, ory instanceof String) or having its value matched (e.g.,x instanceof int i, ory instanceof String s).Further enhance the
.switchconstruct so that it works with all primitive types, not just a subset of the
We will implement these changes by altering a small number of rules in the language that govern the use of primitive types, and by characterizing when a conversion from one type to another is safe — which involves knowledge of the value to be converted as well as the source and target types of the conversion.
This is a preview language feature, disabled by default
To try out the changes described here, you must enable
:Compile your program with
javac --release 26 --enable-preview Main.java, and run it withjava --enable-preview Main; or,When using the source code launcher, run your program with
java --enable-preview Main.java; orWhen using
jshell, start it withjshell --enable-preview.
Safety of conversions
A conversion is exact if no loss of information occurs at run time; otherwise, it is inexact. Whether a conversion is exact or inexact depends on the source type, the target type, and, possibly, the input value.
Two examples of conversions that are exact or inexact depending on the input
value are long to int (a narrowing primitive conversion) and int to
float (a widening primitive conversion), both of which lose precision for some
input values. Another example is a conversion from Object to String, which
is exact or inexact depending on whether the input value is dynamically a
String, though this conversion cannot lose precision. In all these cases, we
need to test at run time whether the conversion, if it were to be performed,
would be exact, i.e., whether the value can be converted from the source type to
the target type without loss of information, or, if a cast were to be performed,
without throwing an exception. (The exactness of a long to int conversion is
tested with numerical equality (==), while the exactness of an int to
float conversion is tested using representation equivalence.)
In contrast, for some conversions we know at compile time that the conversion will not lose information at run time regardless of the input value. No run-time test is needed for such a conversion, which is said to be unconditionally exact. There are two categories of such conversions:
A type-based unconditionally exact conversion is one that widens from one integral type to another, or from one floating-point type to another, or from
byte,short, orcharto a floating-point type, or frominttodouble, or is a boxing conversion (e.g.,inttoInteger), or a widening reference conversions (e.g.,StringtoObject).A value-based unconditionally exact conversion is one in which the input value is a constant expression and
The conversion is either a narrowing primitive conversion or one of the widening primitive conversions that can lose precision, e.g.,
inttofloat, andA compile-time test determines that converting the value results in no loss of information.
For example, narrowing the
intvalue42tobyteis unconditionally exact, as is widening theintvalue4096tofloat.
The following table denotes the conversions that are permitted between primitive
types. Unconditionally exact conversions for values that are not constant
expressions are denoted with the symbol ɛ. The symbol ≈ means the
identity conversion, ω means a widening primitive conversion,
η means a narrowing primitive conversion, and ωη means a widening
and narrowing primitive conversion. The symbol — means no conversion is
allowed.
| To → | byte | short | char | int | long | float | double | boolean |
|---|---|---|---|---|---|---|---|---|
| From ↓ | ||||||||
| byte | ≈ | ɛ | ωη | ɛ | ɛ | ɛ | ɛ | — |
| short | η | ≈ | η | ɛ | ɛ | ɛ | ɛ | — |
| char | η | η | ≈ | ɛ | ɛ | ɛ | ɛ | — |
| int | η | η | η | ≈ | ɛ | ω | ɛ | — |
| long | η | η | η | η | ≈ | ω | ω | — |
| float | η | η | η | η | η | ≈ | ɛ | — |
| double | η | η | η | η | η | η | ≈ | — |
| boolean | — | — | — | — | — | — | — | ≈ |
Comparing this table to its equivalent in
JLS §5.5,
we can see that many of the conversions permitted by ω in JLS §5.5 are
upgraded to the unconditionally exact ɛ above.
instanceof as the precondition for safe casting
Type tests with instanceof are traditionally limited to reference types. The
classic meaning of instanceof is a precondition check that asks: Would it be
safe to cast this value to this type? This question is even more pertinent to
primitive types than to reference types. For reference types, if the check is
accidentally omitted then performing an unsafe cast will likely do no harm: A
ClassCastException will be thrown and the improperly cast value will be
unusable. In contrast, for primitive types, where there is no convenient way to
check for safety, performing an unsafe cast will likely cause subtle bugs.
Instead of throwing an exception, it can silently lose information such as
magnitude, sign, or precision, allowing the improperly cast value to flow into
the rest of the program.
To enable primitive types in the instanceof type test operator, we remove the
restrictions (JLS §15.20.2)
that the type of the left-hand operand must be a reference type and that the
right-hand operand must specify a reference type. The syntax of the type test
operator becomes
InstanceofExpression:
RelationalExpression instanceof Type
...
At run time, we extend instanceof to primitive types by appealing to exact
conversions: If the value on the left-hand side can be converted to the type on
the right-hand side via an exact conversion then it would be safe to cast the
value to that type, and instanceof reports true.
Here are some examples of how the extended instanceof can safeguard casting.
Unconditionally exact conversions return true regardless of the input value;
all other conversions require a run-time test whose result is shown.
byte b = 42;
b instanceof int; // true (unconditionally exact)
int i = 42;
i instanceof byte; // true (exact)
int i = 1000;
i instanceof byte; // false (not exact)
int i = 16_777_217; // 2^24 + 1
i instanceof float; // false (not exact)
i instanceof double; // true (unconditionally exact)
i instanceof Integer; // true (unconditionally exact)
i instanceof Number; // true (unconditionally exact)
float f = 1000.0f;
f instanceof byte; // false
f instanceof int; // true (exact)
f instanceof double; // true (unconditionally exact)
double d = 1000.0d;
d instanceof byte; // false
d instanceof int; // true (exact)
d instanceof float; // true (exact)
Integer ii = 1000;
ii instanceof int; // true (exact)
ii instanceof float; // true (exact)
ii instanceof double; // true (exact)
Integer ii = 16_777_217;
ii instanceof float; // false (not exact)
ii instanceof double; // true (exact)
We do not add any new conversions to the language, nor change existing
conversions, nor change which conversions are allowed in existing contexts such
as assignment. Whether instanceof is applicable to a given value and type is
determined by whether a conversion is allowed in a casting context and whether
it is exact. For example, b instanceof char is never allowed if b is a
boolean variable, because there is no casting conversion from boolean to
char.
Primitive type patterns in instanceof and switch
A type pattern merges a type test with a conditional conversion. This avoids
the need for an explicit cast if the type test succeeds, while the uncast value
can be handled in a different branch if the type test fails. When the
instanceof type test operator supported only reference types, it was natural
that only reference type patterns were allowed in instanceof and switch;
now that the instanceof type test operator supports primitive types, it is
natural to allow primitive type patterns in instanceof and switch.
To achieve this, we drop the restriction that primitive types cannot be used in a top level type pattern. As a result, the laborious code
int i = 1000;
if (i instanceof byte) { // false -- i cannot be converted exactly to byte
byte b = (byte)i;
... b ...
}
can be written as
if (i instanceof byte b) {
... b ...
}
because i instanceof byte b means "test if i instanceof byte and, if so,
cast i to byte and bind that value to b".
The semantics of type patterns are defined by three predicates: applicability, unconditionality, and matching. We lift restrictions on the treatment of primitive type patterns as follows:
Applicability is whether a pattern is legal at compile time.
Previously, for a primitive type pattern to be applicable required that the match candidate have the exact same type as the type in the pattern. For example,
switch (... an int ...) { case double d: ... }was not allowed because the patterndoublewas not applicable toint.Now, a primitive type pattern for type
Tis applicable to a match candidate of typeUif aUcould be cast toTwithout an unchecked-cast warning. Sinceintcan be cast todouble, thatswitchis now legal.Unconditionality is whether we can determine at compile time that an applicable pattern will match all possible run-time values of the match candidate. An unconditional pattern requires no run-time checks.
Previously, primitive type patterns were applicable only to match candidates of the same type, so all such patterns were unconditional.
Now, a primitive type pattern for type
.Tis unconditional on a match candidate of typeUif the conversion fromUtoTisFor conditional type patterns, matching is whether the required run-time checks succeed.
Previously, a non-
nullvaluevwould match a type pattern of typeTwhenvcould be cast toTwithout throwing aClassCastException. This definition sufficed when primitive type patterns had a limited role.Now that primitive type patterns can be used widely, we generalize matching so that a non-
nullvaluevmatches a type pattern of typeTifvcan be cast exactly toT. This ensures that no information is lost when matching a primitive type pattern.
Exhaustiveness
A switch expression, or a switch statement whose case labels are
patterns, is required to be exhaustive: All possible values of the selector
expression must be handled in the switch block. A switch is exhaustive if
it contains an unconditional type pattern; it can be exhaustive for other
reasons as well, such as covering all possible permitted subtypes of a sealed
class. In some situations, a switch can be deemed exhaustive even when there
are possible run-time values that will not be matched by any case; in such
situations the compiler inserts a synthetic default clause to handle
these unanticipated inputs. Exhaustiveness is covered in greater detail in
Patterns: Exhaustiveness, Unconditionality, and Remainder.
With the introduction of primitive type patterns, we add one new rule to the
determination of exhaustiveness: Given a switch whose match candidate is a
wrapper type W for some primitive type P, a type pattern T t exhausts W
if T is unconditionally exact on P, in which case null becomes part of the
remainder. For example:
Byte b = ...
switch (b) { // exhaustive switch
case int p -> 0;
}
Here the match candidate is a wrapper type of the primitive type byte and the
conversion from byte to int is unconditionally exact. As a result, the
switch is exhaustive. This behavior is similar to the treatment of
exhaustiveness in record patterns.
Dominance
Just as switch uses exhaustiveness to determine if the cases cover all input
values, switch uses dominance to determine if there are any cases that will
match no input values. That is, exhaustiveness ensures that no possible input is
left unhandled (the cases are sufficient) while dominance ensures that no case
is unreachable (all cases are necessary).
One pattern dominates another pattern if it matches all the values that the
other pattern matches. For example, the type pattern Object o dominates the
type pattern String s because everything that would match String s would
also match Object o. In a switch, it is illegal for a case label with an
unguarded type pattern P to precede a case label with type pattern Q if P
dominates Q, because Q would have nothing left to match..
Currently, the
covers only reference type patterns. We expand it to cover primitive type patterns by saying that a type patternT t dominates a type pattern U u if T t would
unconditionally match any candidate of type U. As a result, e.g., the type
pattern long q dominates the type pattern int i.
In a switch, it is common to have some case labels that are type patterns,
e.g., case int i, and some case labels that are constants, e.g,. case 42.
Just as we validate case labels that are type patterns by using dominance
determined by type-based unconditional exactness, we can additionally
validate case labels that are constants by using dominance determined by
value-based unconditional exactness. For example:
int j = ...;
switch (j) {
case float f -> {}
case 16_777_216 -> {} // error: dominated since 16_777_216 can be
// converted unconditionally exactly to float
default -> {}
}
byte x = ...;
switch (x) {
case short s -> {}
case 42 -> {} // error: dominated since 42 can be
// converted unconditionally exactly to short
}
In a switch, an unconditional pattern for the type of the
selector expression makes the switch exhaustive, since it matches all possible
selector values. Such a pattern dominates all following case labels, if any.
It is, therefore, now illegal for a case label with an unconditional pattern
to be followed by any other case labels, since those labels are dominated. For
example:
int x = ...;
switch (x) {
case int _ -> {} // unconditional pattern
case float _ -> {} // error: dominated
}
Expanded primitive support in switch
We enhance the switch construct to cover the remaining primitive types, namely
long, float, double, and boolean, as well as the corresponding boxed
types.
If the selector expression has type long, float, double, or boolean,
any constants used in case labels must have the same type as the selector
expression, or its corresponding boxed type. For example, if the type of the
selector expression is float or Float then any case constants must be
floating-point literals (JLS §3.10.2)
of type float. This restriction is required because mismatches between case
constants and the selector expression could introduce lossy conversions,
undermining programmer intent. The following switch is legal, but it would be
illegal if the 0f constant were accidentally written as 0.
float v = ...
switch (v) {
case 0f -> 5f;
case float x when x == 1f -> 6f + x;
case float x -> 7f + x;
}
The semantics of floating-point literals in case labels is defined in terms
of representation equivalence at both compile time and run time. It is a
compile-time error to use two floating-point literals that are representation
equivalent. For example, the following switch is illegal because the literal
0.999999999f is rounded up to 1.0f, creating a duplicate case label.
float v = ...
switch (v) {
case 1.0f -> ...
case 0.999999999f -> ... // error: duplicate label
default -> ...
}
Since the boolean type has only two distinct values, a switch that lists
both the true and false cases is considered exhaustive. The following
switch is legal, but it would be illegal if there were a default clause.
boolean v = ...
switch (v) {
case true -> ...
case false -> ...
}
Risks and Assumptions
The greater use of unconditionality to validate case labels constitutes a
source-incompatible change to the language: Some switch constructs that
compiled previously will now produce compile-time errors. For example:
interface A {}
interface B {}
A a = ...;
switch (a) {
case A _ -> {} // unconditional pattern
case B _ -> {} // error: dominated
}
The first pattern does not dominate the second pattern because interfaces A and
B are unrelated. However, the type of the match candidate, i.e., the selector
expression a, is A, so the first pattern is unconditional and case B _ is
never reached.
Previously, this switch was legal but misleading because maintainers might
expect case B _ sometimes to be reached. (Perhaps their expectation was valid
in the past if, e.g., the interfaces had a common ancestor and the selector
expression was slightly different.) To avoid misleading maintainers, compiling
this switch will now result in an error.
Future Work
Having regularized the language's rules around type comparisons and pattern
matching, we may then consider introducing constant patterns. At present, in a
switch, constants can only appear as case constants, e.g., the 42 in this
code:
short s = ...
switch (s) {
case 42 -> ...
case int i -> ...
}
Constants cannot appear in record patterns, which limits the usefulness of
pattern matching. For example, the following switch is not possible:
record Box(short s) {}
Box b = ...
switch (b) {
case Box(42) -> ... // Box(42) is not a valid record pattern
case Box(int i) -> ...
}
Thanks to the applicability rules defined here, constants could be allowed to
appear in record patterns. In a switch, case Box(42) would mean case
Box(int i) when i == 42, since 42 is a literal of type int.
- relates to
-
JDK-8349215 JEP 507: Primitive Types in Patterns, instanceof, and switch (Third Preview)
-
- Closed
-