When I was a youngling learning to code Delphi on the hard knock streets of Joburg… one thing was made very clear by the compiler and teachers were that circular dependencies are wrong… just wrong. No code will compile. I appreciate we are about 800 years from that part and tech has moved on, so it can sort of circular dependencies, but to me, it still feels like a bad design decision.
Which brings me to the Java permits
keyword (also known as Sealed Interfaces), this feature allows one to create an interface and “assign” classes to it and basically get a Union type like TypeScript has; example code (note this is an empty interface - but doesn’t have to be)
sealed interface Animal permits Dog, Cat {}
Now I can use Animal
in my code and do a type check if it is a Dog
or Cat
and things just work. This also means the shape of Dog
and Cat
do not need to match each other (again, a Union type)… coolness. Again an example:
void speak(Animal animal) {
switch (animal) {
case Dog dog -> dog.bark();
case Cat cat -> cat.meow();
}
}
Here is the wild part for me, all the classes “joined” in this interface must also extend that interface… So Dog
is an Animal
, Cat
is an Animal
and Animal
is both Cat
and Dog
. Example:
final class Dog extends Animal {
public String bark() { ... }
}
or
final class Cat extends Animal {
public String meow() { ... }
}
This means Animal
needs to know what Cat
& Dog
are first, to set itself up, but, each of those classes need to know what Animal
is first so that they can use it… it is a circular dependency by design. It seems like a bad - it works… just feels bad. I likely would’ve not had them extend the interface if it was my call. There is a perk to this though, you can force some shared shape in the interface
sealed interface Animal permits Dog, Cat {
boolean isTailWagging;
and then you end up with both classed needing to implement it, à la
final class Dog extends Animal {
public String bark() { ... }
public boolean isTailWagging() {...}
}
or
final class Cat extends Animal {
public String meow() { ... }
public boolean isTailWagging() {...}
}
and then we use that without a type check
void speak(Animal animal) {
if (!animal.isTailWagging()) {
// animal is unhappy so will make a noise
switch (animal) {
case Dog dog -> dog.bark();
case Cat cat -> cat.meow();
}
}
}
Though in my fictional design, one would just have two interfaces - one for the shared shape and one for the union… Anyway, I hope this little tangent into this was interesting, definitely good to see Java getting this sort of functionality (though it did get a while back to be fair), just need to see people using it now!