Draft JEP on Primitive types in patterns, instanceof, and switch

Stephen Colebourne scolebourne at joda.org
Thu Jan 26 22:01:10 UTC 2023


"On Thu, 26 Jan 2023 at 09:49, Angelos Bimpoudis
<angelos.bimpoudis at oracle.com> wrote:
> I would like to share this draft JEP with you about primitive types in patterns, instanceof, and switch:
>
> https://openjdk.org/jeps/8288476
>
> "Enhance pattern matching by allowing primitive types to appear anywhere in patterns. Extend instanceof to support primitive types, and extend switch to allow primitive constants as case labels."
>
> Comments very much welcomed!

I am implacably opposed to this JEP. I think it breaks a fundamental
tenet of the Java language, and it will result in code that is
difficult to read and reason about.

A large part of the Goals and Motivation section are focussed on the
question of how to check and convert from `int` to `byte` or similar.
At no point does the document acknowledge that such conversions are
rare. This isn't a problem people are crying out to be fixed. Instead,
this JEP is merely an attempt to justify utterly nonsensical behaviour
that breaks decades of language principles.

Specifically, the Goal "Provide easy-to-use constructs that eliminate
the risk of losing information due to unsafe casts" does *not* need to
be met by a change to the *language*. Checking whether an `int` can be
converted to a `byte` is indeed a perfectly reasonable question, but
it should be a library method, not a language feature.

In the Motivation section it is claimed that because `byte b = 42`
compiles, it implies that sometimes an `int` can be converted to a
`byte` without a cast. This is nonsense. In this context, `42` is
*not* an `int` at all - it is a literal. There is no conversion here,
`42` is typed as a `byte` because of the assignment. `42` is never, at
any stage, an `int`. At the very least, the JEP should be amended to
remove this part of the Motivation section.

The document proceeds to argue that because switch works with Object
hierarchies:
  Pet p = new Pet(new Dog()); // automatic widening conversion from
Dog to Animal
  switch (p) {
    case Pet(Dog d)    -> ... d ...
    case Pet(Animal a) -> ... a ...
    default            -> ...
  }
that it must therefore be OK to work with primitives:
  int i = methodReturningShort();
  switch (i) {
    case byte  b -> ... b ...;
    case float f -> ... f ...;
    default      -> -1;
  }

There is simply no comparison here. Dog and Animal are subtypes of
Pet, a concept that has been baked into the language since day one,
and is fully understood by all. By contrast, there is absolutely no
subtyping relationship between int, byte and short, and again this
fact has been baked into the language since day one.

There is a *huge* red line being crossed here. Values in Java have two
distinct parts - the type and the value of the instance. Java has
always kept these two things completely separate: General-purpose
language features operate on types and references/null (eg.
instanceof, catch, switch) or expressions (if, for, while). It
requires an expression or an operator for the actual value of the
instance to be considered. The JEP proposes to shatter that boundary,
saying that the language should now examine not only the type but also
the value of the instance in order to determine flow control.

The root cause of the issue here is trying to treat Object hierarchy
conversion and conversion between different primitive types as being
somehow equivalent. They are not. Java does not have a mechanism that
allows a LocalDate to be assigned to a String, even though there is a
perfectly reasonable way to do so. Instead, you have to explicitly
perform the conversion by calling toString(). That is because
`LocalDate` and `String` are separate types with no subtype hierarchy.
Similarly, there is no subtype hierarchy link between `int` and
`long`. That an `int` can be assigned to a `long` is merely a
convenience - it could have required a method call. Critically though,
the convenience conversion is absolute. No runtime check of the value
of the instance is required. This even applies when converting `int`
to `float` which is lossy.

In my view, the only pattern matching checks that make sense here are
those in line with the separation between types and values of
instances. This basic rule implies:
* `int` vs `Integer` and vice versa - OK, as only requires examination
of the type and reference/null
* `int` vs `byte` - not OK, as an expression is required in order to
extract the value of the `int` in order to decide flow control

In summary, if this was simply about adding a niche feature for
checking whether an `int` actually fits in a `byte` I would have no
problem. For example, were the pattern match to be based on an
expression (ie. a method call) then I would have no problem. The key
issue here is the red line being crossed by having a general-purpose
language feature examine the value of the instance outside of an
expression.

thanks
Stephen


More information about the amber-dev mailing list