This is the 2nd entry in my series about my Java wishlist, the Java features which I would like to see most. Some are already on the JEP list, some are not. The previous entry is:
Now lets talk about union types. When programming some non-trivial code in Typescript, I quickly learned to like union types, something like
type Stuff = number | boolean | { option: string }
And the meaning is pretty much what you expect. When you write
function f(stuff: Stuff): void { const lcase = stuff.option.toLowerCase(); }
the compiler immediately complains that at this point in the
code stuff
is not necessarily an object with
an option
field, but may be a number or boolean as
well. You are forced, for good, to first verify what you have, before
you can access the field. Something like
if ( typeof stuff === 'object' ) { const lcase = stuff.option.toLowerCase(); }
And ideally provide some code then to handle the other cases too. Though in this particular setting the compiler won't complain if you don't.
The union type Stuff
quite simply defines that any value
of this type is one of the given types and before accessing such a
value, code must be provided which can tell what it actually is.
Compare this with Java exception handling:
} catch (InterruptedException | ParseException e) { if (e instanceof ParseException ee) { System.err.println(ee.getErrorOffset()); } }
It looks much the same: Exception e
may be either of two
exception types and access to the method specific
to ParseException
is only possible after we have made
sure at which type exception we are looking.
But exceptions seem to be the only place where we can use this. But with the new pattern matching for switch expressions we may also write
private static void f(Object value) { switch (value) { case Integer i -> System.out.println("got Integer: " + i); case String s -> System.out.println("got String \"" + s + "\""); default -> System.out.println( "cannot prevent to get weird type: " + value.getClass()); } }
Sadly, the best we can do is declare the value
parameter
as Object
. This forces us to add the default
case with error handling. But, worse, the function prototype does not
advertise that this method is only designed to handle Integer
and String
values. It would just be great to write
private static void f(Integer | String value) {
similar to the catch
clause syntax.
Under some circumstances, the new sealed classes come to the rescue, as, depending on use case, we may be able to use something like:
sealed static class IorS permits Int, Str {}; static final class Int extends IorS { public int value; } static final class Str extends IorS { public String value; } private static void f(IorS value) { switch (value) { case Int i -> System.out.println("got Integer: " + i.value); case Str s -> System.out.println("got String \"" + s.value + "\""); case IorS weird -> System.err.println("weird" + weird); } }
When using this, we have to obey some rules, though:
I am not a language designer, so I can't tell how difficult this would be to implement or if it is possible at all without introducing extremely ugly cases. But well: wishes. :-)