| Owner | Angelos Bimpoudis |
| Type | Feature |
| Scope | SE |
| Status | Closed / Delivered |
| Release | 23 |
| Component | specification / language |
| Discussion | amber dash dev at openjdk dot org |
| Effort | M |
| Duration | M |
| Relates to | JEP 488: Primitive Types in Patterns, instanceof, and switch (Second Preview) |
| Reviewed by | Alex Buckley, Brian Goetz |
| Endorsed by | Brian Goetz |
| Created | 2022/06/15 10:05 |
| Updated | 2025/02/25 16:30 |
| Issue | 8288476 |
Enhance pattern matching by allowing primitive type patterns in allpattern contexts, and extendinstanceof andswitch to work withall primitive types. This is apreview language feature.
Enable uniform data exploration by allowing type patterns for all types,whether primitive or reference.
Align type patterns withinstanceof, and aligninstanceof with safe casting.
Allow pattern matching to use primitive type patterns in both nested andtop-level contexts.
Provide easy-to-use constructs that eliminate the risk of losing informationdue to unsafe casts.
Following the enhancements toswitch in Java 5 (enumswitch) andJava 7 (stringswitch), allowswitch to process values of anyprimitive type.
Multiple restrictions pertaining to primitive types impose friction when usingpattern matching,instanceof, andswitch. Eliminating these restrictionswould make the Java language more uniform and more expressive.
switch does not support primitive type patternsThe first restriction is that pattern matching forswitch(JEP 441) does not support primitive typepatterns, i.e., type patterns that specify a primitive type. Only type patternsthat specify a reference type are supported, such ascase Integer i orcase String s. (Since Java 21, record patterns(JEP 440) are also supported forswitch.)
With support for primitive type patterns inswitch, we could improvetheswitch expression
switch (x.getStatus()) { case 0 -> "okay"; case 1 -> "warning"; case 2 -> "error"; default -> "unknown status: " + x.getStatus();}by turning thedefault clause into acase clause with a primitivetype 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 inspectthe 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 ...}Another restriction is that record patterns have limited support for primitivetypes. Record patterns streamline data processing by decomposing a record intoits individual components. When a component is a primitive value, the recordpattern must be precise about the type of the value. This is inconvenient fordevelopers and inconsistent with the presence of helpful automatic conversionsin the rest of the Java language.
For example, suppose we wish to process JSON data represented viathese 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, soJsonNumberrepresents a number with adouble component for maximum flexibility.However, we do not need to pass adouble when creating aJsonNumber record; we can pass anint such as30, and the Javacompiler automatically widens theint todouble:
var json = new JsonObject(Map.of("name", new JsonString("John"), "age", new JsonNumber(30)));Unfortunately, the Java compiler is not so obliging if we wish todecompose aJsonNumber with a record pattern. SinceJsonNumberis declared with adouble component, we must decompose aJsonNumber with respect todouble, and convert toint 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 patternsbut are invariant: The primitive type in the pattern must be identical to theprimitive type of the record component. It is not possible to decompose aJsonNumber viainstanceof JsonNumber(int age) and have the compilerautomatically narrow thedouble component toint.
The reason for this limitation is that narrowing might be lossy: The value ofthedouble component at run time might be too large, or have too muchprecision, for anint variable. However, a key benefit of pattern matching isthat it rejects illegal values automatically, by simply not matching. If thedouble component of aJsonNumber is too large or too precise to narrowsafely to anint, theninstanceof JsonNumber(int age) could simply returnfalse, leaving the program to handle a largedouble component in adifferent branch.
This is how pattern matching already works for reference typepatterns. For example:
record Box(Object o) {}var b = new Box(...);if (b instanceof Box(RedBall rb)) ...else if (b instanceof Box(BlueBall bb)) ...else ....Here the component ofBox is declared to be of typeObject, butinstanceof can be used to try to match aBox with aRedBall component oraBlueBall component. The record patternBox(RedBall rb) matches only ifb is aBox at run time and itso component can be narrowed toRedBall;similarly,Box(BlueBall bb) matches only if itso component can be narrowedtoBlueBall.
In record patterns, primitive type patterns should work as smoothly asreference type patterns, allowingJsonNumber(int age) even if thecorresponding record component is a numeric primitive type other thanint. This would eliminate the need for verbose and potentially lossycasts after matching the pattern.
instanceof does not support primitive typesYet another restriction is that pattern matching forinstanceof(JEP 394) does not support primitive typepatterns. Only type patterns that specify a reference type aresupported. (Since Java 21, record patterns are also supported forinstanceof.)
Primitive type patterns would be just as useful ininstanceof as they are inswitch. The purpose ofinstanceof is, broadly speaking, to test whether avalue can be converted safely to a given type; this is why we always seeinstanceof and cast operations in close proximity. This test is critical forprimitive types because of the potential loss of information that can occurwhen converting primitive values from one type to another.
For example, converting anint value to afloat is performed automaticallyby an assignment statement even though it is potentially lossy — and thedeveloper receives no warning of this:
int getPopulation() {...}float pop = getPopulation(); // silent potential loss of informationMeanwhile, converting anint value to abyte is performed with anexplicit cast, but the cast is potentially lossy so it must bepreceded by a laborious range check:
if (i >= -128 && i <= 127) { byte b = (byte)i; ... b ...}Primitive type patterns ininstanceof would subsume the lossy conversionsbuilt into the Java language and avoid the painstaking range checks thatdevelopers have been coding by hand for almost three decades. In other words,instanceof could check values as well as types. The two examples above couldbe rewritten as follows:
if (getPopulation() instanceof float pop) { ... pop ...}if (i instanceof byte b) { ... b ...}Theinstanceof operator combines the convenience of an assignment statementwith the safety of pattern matching. If the input (getPopulation() ori)can be converted safely to the type in the primitive type pattern then thepattern matches and the result of the conversion is immediately available(pop orb). But, if the conversion would lose information then the patterndoes not match and the program should handle the invalid input in a differentbranch.
instanceof andswitchIf we are going to lift restrictions around primitive type patterns then itwould be helpful to lift a related restriction: Wheninstanceof takes atype, rather than a pattern, it takes only a reference type, not a primitivetype. When taking a primitive type,instanceof would check if the conversion issafe but would not actually perform it:
if (i instanceof byte) { // value of i fits in a byte ... (byte)i ... // traditional cast required}This enhancement toinstanceof restores alignment between the semantics ofinstanceof T andinstanceof T t, which would be lost if we allowedprimitive types in one context but not the other.
Finally, it would be helpful to lift the restriction thatswitch can takebyte,short,char, andint values but notboolean,float,double,orlong values.
Switching onboolean values would be a useful alternative to the ternaryconditional operator (?:) because aboolean switch can contain statementsas well as expressions. For example, the following code uses aboolean switchto perform some logging whenfalse:
startProcessing(OrderStatus.NEW, switch (user.isLoggedIn()) { case true -> user.id(); case false -> { log("Unrecognized user"); yield -1; }});Switching onlong values would allowcase labels to belong constants,obviating the need to handle very large constants with separateifstatements:
long v = ...;switch (v) { case 1L -> ...; case 2L -> ...; case 10_000_000_000L -> ...; case 20_000_000_000L -> ...; case long x -> ... x ...;}In Java 21, primitive type patterns are permitted only as nested patterns inrecord patterns, as in:
v instanceof JsonNumber(double a)To support the more uniform data exploration of a match candidatev with patternmatching, we will:
Extend pattern matching so that primitive type patterns are applicable to awider range of match candidate types. This will allow expressions such asv instanceof JsonNumber(int age).
Enhance theinstanceof andswitch constructs to support primitive typepatterns as top level patterns.
Further enhance theinstanceof construct so that, when used for typetesting rather than pattern matching, it can test against all types, not justreference types. This will extendinstanceof's current role, as theprecondition for safe casting on reference types, to apply to all types.
More broadly, this means thatinstanceof can 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 theswitch construct so that it works with all primitivetypes, not just a subset of theintegral primitivetypes.
We achieve these changes by altering a small number of rules in the Javalanguage that govern the use of primitive types:
Remove the restriction against primitive types and primitive type patterns intheinstanceof andswitch constructs;
Extendswitch to handle constant cases for literals of all primitive types;and
Characterize when a conversion from one type to another is safe, whichinvolves knowledge of the value to be converted as well as the source andtarget types of the conversion.
A conversion isexact if no loss of information occurs. Whether a conversionis exact depends on the pair of types involved and on the input value:
For some pairs, it is known at compile time that conversion from the firsttype to the second type is guaranteed not to lose information for anyvalue. The conversion is said to beunconditionally exact. No action isneeded at run time for an unconditionally exact conversion. Examples includebyte toint,int tolong, andString toObject.
For other pairs, a run-time test is needed to check whether the value can beconverted from the first type to the second type without loss of informationor, if a cast were to be performed, without throwing an exception. Theconversion is exact if no loss of information or exception would occur;otherwise, the conversion is not exact. Examples of conversions that mightbe exact arelong toint andint tofloat, where loss of precision isdetected at run time by using numerical equality (==) orrepresentationequivalence, respectively. Converting fromObject toString also needsa run-time test, and the conversion is exact or not exact depending onwhether the input value is dynamically aString.
In brief, a conversion between primitive types is unconditionally exact if itwidens from one integral type to another, or from one floating-point type toanother, or frombyte,short, orchar to a floating-point type, or fromint todouble. Furthermore, boxing conversions and widening referenceconversions are unconditionally exact.
The following table denotes the conversions that are permitted betweenprimitive types. Unconditionally exact conversions are denoted withthe symbol ɛ. The symbol≈ means the identityconversion,ω means a widening primitive conversion,η means a narrowing primitive conversion, andωη means awidening and narrowing primitive conversion. The symbol— means noconversion 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 inJLS §5.5,it can be seen that many of the conversions permitted byω in JLS §5.5are "upgraded" to the unconditionally exactɛ above.
instanceof as the precondition for safe castingType tests withinstanceof are traditionally limited to reference types. Theclassic meaning ofinstanceof is a precondition check that asks: Would it besafe and useful to cast this value to this type? This question is even morepertinent to primitive types than to reference types. For reference types, ifthe check is accidentally omitted then performing an unsafe cast will likely dono harm: AClassCastException will be thrown and the improperly cast valuewill be unusable. In contrast, for primitive types, where there is noconvenient way to check for safety, performing an unsafe cast will likely causesubtle bugs. Instead of throwing an exception, it can silently lose informationsuch as magnitude, sign, or precision, allowing the improperly cast value toflow into the rest of the program.
To enable primitive types in theinstanceof type test operator, we remove therestrictions (JLS §15.20.2)that the type of the left-hand operand must be a reference type and that theright-hand operand must specify a reference type. The type test operatorbecomes
InstanceofExpression: RelationalExpression instanceof Type ...At run time, we extendinstanceof to primitive types by appealing to exactconversions: If the value on the left-hand side can be converted to the type onthe right-hand side via an exact conversion then it would be safe to cast thevalue to that type, andinstanceof reportstrue.
Here are some examples of how the extendedinstanceof can safeguard casting.Unconditionally exact conversions returntrue 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 + 1i 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; // falsef instanceof int; // true (exact)f instanceof double; // true (unconditionally exact)double d = 1000.0d;d instanceof byte; // falsed 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 Java language, nor change existingconversions, nor change which conversions are allowed in existing contexts suchas assignment. Whetherinstanceof is applicable to a given value and type isdetermined by whether a conversion is allowed in a casting context and whetherit is exact. For example,b instanceof char is never allowed ifb is aboolean variable, because there is no casting conversion fromboolean tochar.
instanceof andswitchA type pattern merges a type test with a conditional conversion. This avoidsthe need for an explicit cast if the type test succeeds, while the uncast valuecan be handled in a different branch if the type test fails. When theinstanceof type test operator supported only reference types, it was naturalthat only reference type patterns were allowed ininstanceof andswitch;now that theinstanceof type test operator supports primitive types, it isnatural to allow primitive type patterns ininstanceof andswitch.
To achieve this, we drop the restriction that primitive types cannot be used ina top level type pattern. As a result, the laborious and error-prone code
int i = 1000;if (i instanceof byte) { // false -- i cannot be converted exactly to byte byte b = (byte)i; // potentially lossy ... b ...}can be written as
if (i instanceof byte b) { ... b ... // no loss of information}becausei instanceof byte b means "test ifi instanceof byte and, if so,casti tobyte and bind that value tob".
The semantics of type patterns are defined by three predicates: applicability,unconditionality, and matching. We lift restrictions on the treatmentof primitive type patterns as follows:
Applicability is whether a pattern is legal at compile time. Previously,applicability for a primitive type pattern required that the input expressionhave the exact same type as the type in the pattern. For example,switch (... an int ...) { case double d: ... } was not allowed because the patterndouble was not applicable toint.
Now, a type patternT t is applicable to a match candidate of typeU if aU could be cast toT without an unchecked warning. Sinceint can be casttodouble, thatswitch is now legal.
Unconditionality is whether it is known at compile time that an applicablepattern will match all possible run-time values of the match candidate. Anunconditional pattern requires no run-time checks.
As we extend primitive type patterns to be applicable to more types, we mustspecify on which types they are unconditional. A primitive type pattern fortypeT is unconditional on a match candidate of typeU if the conversionfromU toT is unconditionally exact. This is because an unconditionallyexact conversion is safe regardless of the input value.
Previously, a valuev that is not thenull referencematches a typepattern of typeT ifv can be cast toT without throwing aClassCastException. This definition of matching sufficed when primitivetype patterns had a limited role. Now that primitive type patterns can beused widely, matching is generalized to mean that a value can be cast exactlytoT, which covers throwing aClassCastException as well as potentialloss of information.
Aswitch expression, or aswitch statement whosecase labels arepatterns, is required to beexhaustive: All possible values of the selectorexpression must be handled in theswitch block. Aswitch is exhaustive ifit contains an unconditional type pattern; it can be exhaustive for otherreasons as well, such as covering all possible permitted subtypes of a sealedclass. In some situations, aswitch can be deemed exhaustive even when thereare possible run-time values that will not be matched by anycase; in suchsituations the Java compiler inserts a syntheticdefault clause to handlethese unanticipated inputs. Exhaustiveness is covered in greater detail inPatterns: Exhaustiveness, Unconditionality, and Remainder.
With the introduction of primitive type patterns, we add one new rule to thedetermination of exhaustiveness: Given aswitch whose match candidate is awrapper typeW for some primitive typeP, a type patternT t exhaustsWifT is unconditionally exact onP. In that case,null becomes part ofthe remainder. In the following example, the match candidate is a wrapper typeof the primitive typebyte and the conversion frombyte toint isunconditionally exact. As a result the followingswitch is exhaustive:
Byte b = ...switch (b) { // exhaustive switch case int p -> 0;}This behavior is similar to the exhaustiveness treatment of record patterns.
Just asswitch uses pattern exhaustiveness to determine if the casescover all input values,switch uses dominance to determine if thereare any cases that will match no input values.
One patterndominates another pattern if it matches all the values that theother pattern matches. For example, the type patternObject o dominates thetype patternString s because everything that would matchString s wouldalso matchObject o. In aswitch, it is illegal for acase label with anunguarded type patternP to precede a case label with type patternQ ifPdominatesQ. The meaning of dominance is unchanged: A type patternT tdominates a type patternU u ifT t would be unconditional on a matchcandidate of typeU.
switchWe enhance theswitch construct to allow a selector expression of typelong,float,double, andboolean, as well as the corresponding boxedtypes.
If the selector expression has typelong,float,double, orboolean,any constants used in case labels must have the same type as the selectorexpression, or its corresponding boxed type. For example, if the type of theselector expression isfloat orFloat then anycase constants must befloating-point literals (JLS §3.10.2)of typefloat. This restriction is required because mismatches betweencaseconstants and the selector expression could introduce lossy conversions,undermining programmer intent. The followingswitch is legal, but it would beillegal if the0f constant were accidentally written as0.
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 incase labels is defined in termsofrepresentation equivalence at compile time and run time. It is acompile-time error to use two floating-point literals that are representationequivalent. For example, the followingswitch is illegal because the literal0.999999999f is rounded up to1.0f, creating a duplicatecase label.
float v = ...switch (v) { case 1.0f -> ... case 0.999999999f -> ... // error: duplicate label default -> ...}Since theboolean type has only two distinct values, aswitch that listsboth thetrue andfalse cases is considered exhaustive. The followingswitch is legal, but it would be illegal if there were adefault clause.
boolean v = ...switch (v) { case true -> ... case false -> ... // Alternatively: case true, false -> ...}