Java has compile-time typechecked sum types with exhaustive matching built into the base language.

“Of course it doesn't” you say? “Their absence is one of Java's most glaring deficiencies”? Well, kind of. Read on to see what I mean.

I'll spoil the trick right upfront: the return value of functions with declared checked exceptions is a sum type of the declared return value and each declared exception type. (Mostly. Subtyping presents a wrinkle. But if we don't do that, it won't cause problems.) The compiler will make sure that, at the call site, we write a catch block for each checked exception declared in the method signature. This gives us both multiple exclusive types, and compile-time checking that we've handled all of them.

Of course, there's an issue. (Well. Many issues. But let's start with this one.) Our type here is the return type of a function with declared checked exceptions, which we have no way to directly manipulate. So we're stuck wrapping the “type” with a class containing a method with the desired signature. Let's start sketching out an example of what we want:

public class LeftOrRight { public void get() throws LeftValue, RightValue { } }

We'll have it return void just for uniformity, so all return values are thrown. (Java doesn't make us do anything with the actual return value, so this will improve exhaustiveness as well.)

Okay. Now we need this class to contain the values. Because Java doesn't actually give us sum types, we'll need a field for each possible value, and mutually-exclusive constructors or factory methods to populate them:

public class LeftOrRight { private LeftValue left; private RightValue right; public LeftOrRight(LeftValue left) { this.left = left; } public LeftOrRight(RightValue right) { this.right = right; } public void get() throws LeftValue, RightValue { } }

And then get() just needs to throw whichever value is populated. We'll need to actually set left and right to something, as well, so we don't have “variable might not have been initialized” errors. null is probably our best option here, though we're sort of subverting our original goal.

public class LeftOrRight { private LeftValue left = null; private RightValue right = null; public LeftOrRight(LeftValue left) { this.left = left; } public LeftOrRight(RightValue right) { this.right = right; } public void get() throws LeftValue, RightValue { if (left != null) { throw left; } else { throw right; } } }

(Fun fact: it turns out that we can't implement get() with the ternary operator (as e.g. throw left != null ? left : right) because that whole expression has a single type, which is the nearest common superclass of LeftValue and RightValue, which we didn't declare. Whee!)

Now let's think about what LeftValue and RightValue should look like. We basically just want them to extend Throwable (so they're checked and can be thrown) and contain a variable. Actually, they'll be nearly the same, so we should probably implement a superclass.

private class ThrowableValue extends Throwable { private Object value; public ThrowableValue(Object value) { this.value = value; } public Object getValue() { return this.value; } } public class LeftValue extends ThrowableValue { public LeftValue(Object value) { super(value); } } public class RightValue extends ThrowableValue { public RightValue(Object value) { super(value); } }

(Aah, I love the smell of boilerplate in the morning.)

Okay, cool. Let's try it out. public class Example { public static void main(String[] args) { LeftOrRight leftOrRight = new LeftOrRight(new LeftValue("Hello, world!")); try { leftOrRight.get(); } catch (LeftValue l) { System.out.println("Got left: " + l.getValue()); } } } $ javac Example.java Example.java:6: error: unreported exception RightValue; must be caught or declared to be thrown leftOrRight.get(); ^ 1 error

Neat.

public class Example { public static void main(String[] args) { LeftOrRight leftOrRight = new LeftOrRight(new LeftValue("Hello, world!")); try { leftOrRight.get(); } catch (LeftValue l) { System.out.println("Got left: " + l.getValue()); } catch (RightValue r) { System.out.println("Got right: " + r.getValue()); } } } $ javac Example.java $ java Example Got left: Hello, world!

Neat-o.

A few immediate concerns come to mind.

  • “Aren't exceptions really slow?” If performance is your biggest concern here, I really don't know what to say. Uh, yeah, I guess it's probably slow compared to implementing this in a runtime-checked manner.
  • “Generics?” Unfortunately, no. Generics that extend Throwable are explicitly forbidden, because after generic erasure, the runtime can't tell which generic type we tried to throw or which catch block is supposed to catch it. So…
  • “This is a lot of boilerplate.” Well, it is Java.
  • “This is hardly safe. There's all sorts of room for illegal states to sneak in (like a LeftOrRight with both fields set or unset) that the compiler can't catch.” Yeah, this is all true; we're dependant on LeftOrRight being implemented correctly. However, we do get all the benefits I specified in the intro: the type is declared (as throws LeftValue, RightValue) and the call site is statically checked to handle every possible return type (“checked exception”).

So, that was fun. Would I recommend using this? Probably not. (Definitely not if anyone else ever needs to read the code.) But the capability is interesting, if nothing else, and it's been present in the language basically forever.

Example code is available in Example.java, lightly modified to all go in one file.