JEP draft: Support for primitive types in `instanceof` and type patterns

OwnerAngelos Bimpoudis
Componentspecification / language
Discussionamber dash dev at openjdk dot org
Created2022/06/15 10:05
Updated2022/12/03 17:56


Extend the instanceof operator to support primitive types as well as reference types. Extend type patterns involving primitive types to be consistent with instanceof.



The instanceof operator and the cast operator are closely related; asking whether an expression is an instance of type T asks whether a cast of that expression to T would succeed, and result in a useful value of T (for this purpose, null is considered to not be a useful value, as nearly any subsequent use would result in NPE.) . Prior to adding pattern matching, it was rare to use instanceof not followed by a cast, or a cast not preceded by instanceof. There is usually some chance that the cast can fail, and so we use instanceof to let us prevent that failure, either by not performing the operation that would fail, or by providing a more useful error recovery. In effect, instanceof is the precondition for safe casting.

Java currently only allows reference types for both the target and type of an instanceof expression:

if (e instanceof String) {
     String s = (String) e;
     // use s

However, Java does not currently allow instanceof to take primitive targets or primitive types. Consider this hypothetical code:

int i = ...;
if (i instanceof byte) { // not currently allowed
     byte b = (byte) i;
     // use b

Casting is defined between primitive types (widening, narrowing) and between primitive and reference types (boxing, unboxing), but some casts involving primitive types result in better results than others; casting 0 to byte loses no precision, but casting 500 to byte does, because the higher order bits are discarded.

Today, we express tests of whether a cast would be lossy manually. If we want to know whether an int value fits in a byte, we have to know the range of byte values, and can test for this explicitly:

int i = ...;
 if (i >= -128 && i <= 127) {
     byte b = (byte) i;
     // use b

These manual checks are error-prone (it is easy to get the bounds or the relational operators wrong) and harder to understand. There is no use of instanceof, so the programmer does not immediately recognize the sense of the if block (the numeric comparisons can easily be confused with ordinary business logic).

"Instanceof" allows us to ask the question at a more appropriate level of abstraction: "does this int value fit into a byte." We can extend instanceof to become universally meaningful in a straightforward way: would a cast succeed without error and without loss of precision? Alternately, we can frame this question as: can the expression on the left be exactly represented in the type on the right?

Primitive type patterns

Type patterns allow us to fuse the instanceof check and cast into a single operation. Prior to JEP 405, type patterns were only defined for reference types. JEP 405 gives some meaning to primitive type patterns, but they currently are very inflexible -- they are applicable only to exactly the type they name (meaning they are only useful as unconditional patterns). Once instanceof becomes meaningful for primitive types, this provides an obvious interpretation for primitive type patterns (and reference type patterns as applied to a primitive target): "could I cast the match target to the type in this pattern without error or loss of precision?"

Then primitive type patterns can join their reference counterparts to work as partial patterns as well as unconditional, with the natural semantics:

int i = ...;
if (i instanceof byte b) {
    // use b

And once primitive patterns are able to ask the question "would this cast succeed without loss of precision", we can use them in all contexts that accept patterns, such as switch:

int i = ...;
switch (i) {
    case byte b -> System.out.println("it's a byte");
    case short s -> System.out.println("it's a short");
    default -> System.out.println("it's an int");

A further incentive to extend type patterns to primitive types in this manner is to address an accidental asymmetry around aggregation and deconstruction. Given:

record NumBox(Number n) {...}

we can pass an Integer to the NumBox constructor, and then ask if NumBox contains an Integer:

Integer i = ...
NumBox n = new NumBox(i);
if (n instanceof NumBox(Integer j)) { /* recover original i */ }

But with primitive types, we currently have an asymmetry. Given

record IntBox(int i) { }

we can pass a short to the IntBox constructor:

short s = ...
IntBox b = new IntBox(s);

but we cannot yet correspondingly ask whether the IntBox contains a short:

if (b instanceof IntBox(short t)) { /* recover original s */ }

This asymmetry disappears when we extend type patterns to all types.


We extend instanceof to support primitive types:

    RelationalExpression instanceof Type

by removing the restriction that the relational expression, and the type on the right-hand side, be reference types.

We define instanceof <type> to ask whether we can cast the given expression to the given type without error or loss of precision. An instanceof test whose relational expression is of type S and tests for type T is valid if S can be converted to T in a cast context; if not, a compile-time error results. If the relational expression is a constant expression, a compile-time check for exactness will suffice; otherwise it will require a corresponding run time action. (If the relational expression is null, instanceof continues to evaluate to false.)

We extend the definition of casting to specify whether or not a given cast is exact; an exact cast is one with no loss of precision. Casting 500 from int to short is exact because 500 exists in the set of representable values for both types, but casting 500 to byte is not exact because 500 is not representable in byte. No new conversions are added to casting context, and no new conversion contexts are created; whether instanceof is applicable to a given expression and type is determined entirely by whether there is already a casting conversion. We refine the definition of conversions in casting context to additionally indicate that only if a value is representable in the target type, can be converted exactly. Whether a conversion is known to be exact at compile time or run-time depends on the pair of the types of the conversion and in some occasions on the value of the expression.

The conversions permitted in casting context (JLS 5.5) involving either primitives operands or types are identity conversions (JLS 5.1.1), widening primitive conversions (JLS 5.1.2), narrowing primitive conversions (JLS 5.1.3), widening and narrowing primitive conversions (JLS 5.1.4), boxing conversions (JLS 5.1.7), unboxing conversions (JLS 5.1.8), and specified combinations of these (e.g., boxing conversion followed by a widening reference conversion.)

All of the following would be allowed because the expression operand of instanceof can be converted to the specified type in a casting context. Note that there are no new conversions or contexts here. Asking whether something matches a type pattern is asking whether I can safely cast it:

int i = 0;
Integer ii = 0;

// identity conversion int -> int
if (i instanceof int) { ... }

// widening primitive conversion int -> long
if (i instanceof long) { ... }

// narrowing primitive converion int -> short
if (i instanceof short) { ... }

// Boxing conversion int -> Integer
if (i instanceof Integer) { ... }

// Boxing conversion followed by widening reference conversion int -> Object
if (i instanceof Number) { ... }

// Unboxing conversion Integer -> Int
if (ii instanceof int) { ... }

The semantics of primitive type patterns (and reference type patterns on targets of primitive type) are derived from casting conversions.

A type pattern T t is applicable to a target of type U if a U could be cast to T without an unchecked warning.

A type pattern T t is unconditional on a target of type U if all values of U can be exactly cast to T. This includes widening from one reference type to another, widening from one integral type to another, widening from one floating point type to another, widening from byte, short, or char to a floating point type, widening int to double, and boxing.

A set of patterns containing a type pattern T t is exhaustive on a target of type U if T t is unconditional on U or if there is an unboxing conversion from T to U.

A type pattern T t dominates a type pattern U u, or a record pattern U(...), if T t would be unconditional on a target of type U.

A type pattern T t that does not resolve to an any pattern matches a target u if u instanceof T.

With pattern labels involving record patterns, we allow some patterns to be exhaustive even when they are not unconditional. For example, the following switch is considered exhaustive on Box<Box<String>>, even though it doesn't match new Box(null):

Box<Box<String>> bbs = ...
switch (bbs) {
    case Box(Box(String s)): ...

The pathological value new Box(null) is considered "remainder", and is handled by a synthetic default clause that throws MatchException. Similarly, novel subtypes (those not known at compile time) of sealed types are considered "remainder" at runtime. This accomodation is made because requiring users to specify all possible combinations of pathological values would be tedious and impractical.

Analogously, a type pattern int x is considered exhaustive on Integer, so the following switch is considered exhaustive on Box<Integer>:

Box<Integer> bi = ...
switch (bi) {
    case Box(int i): ...

for the same reason.