Hacker Newsnew | past | comments | ask | show | jobs | submitlogin

The "Sealed classes" feature, as described here, just feels all wrong to me.

They are saying that if you have a (normal) interface, anyone can create a new class implementing it. So if you do

    if (x instanceof Foo) {
        ...
    } else if (x instanceof Bar) {
        ...
    } ...
then your code will break at runtime if someone adds a new class, as that code won't expect it. So the article is saying the solution is to use the new "sealed" interfaces feature, so nobody can create any new classes implementing that interface, and your "if" statement will not break.

Surely object-oriented programming already thought about that and already solved that? (I know object-oriented programming is out of vogue at the moment, but Java is an object-oriented language.)

The solution there is to add a method to the interface, and all your classes implement that. Then rather than having a massive if/switch statement with all the options, you call the method.

That is better than preventing people from extending your code, it allows them to extend your code. They just have to implement the method. And the compiler will force them to do that, so they can't even accidentally forget.

The example given of color spaces (RGB, CMYK etc.) is a great example. I can absolutely imagine writing code which uses color spaces, but then some user or client having a need to use a weird obscure color space I haven't thought of. I wouldn't want to restrict my code to saying "these are the color spaces I support, due to this massive if/switch statement listing them all, the code is written in such a way that you can't extend it".



> The solution there is to add a method to the interface, and all your classes implement that.

What if you don't know all methods that you will need in advance?

That is the problem and this problem is solved by sealed classes. However, by doing so they introduce a new problem: what if you need more extending classes and you don't know all of them in advance? Which raises the question: is there a way to achieve both?

This problem is called expression problem. [1]

There are (statically typed) languages that are able to solve the expression problem, Java is one of them [2]. However, unfortunately the way to do that in Java is (still) very complex and unergonomic, hence rarely used. Languages like Haskell or, if you want to stay in the JVM world, Scala do much better here.

[1] https://en.wikipedia.org/wiki/Expression_problem [2] https://koerbitz.me/posts/Solving-the-Expression-Problem-in-...


The solution with sealed classes also allows anyone to extend the code, but in a different dimension than the solution with an interface method.

The solution with interface method and virtual call is very inflexible when you want to add new operations instead of adding new classes. If you want to just add one new operation, then you have to go to all the implementations and add new methods. And you possibly break the implementations you don't have access to. And all those methods must be defined in a single class, even if they are unrelated to each other. This seriously degrades code readability (and performance as well - those vcalls are not free either).

The sealed class extends much better in this case. You just add a new switch in one place and done. No breaking of backwards compatibility.

This is the famous expression problem.

https://pkolaczk.github.io/in-defense-of-switch/


  > If you want to just add one new operation, then you have to go to all the implementations and add new methods.
not necessarily, if you have extension methods (kotlin, swift) you have the option to extend the interface and only override the specific implementation when needed


I understand what you're recommending, and I've seen Bob Martin talk about it extensively (polymorphic dispatch instead of instanceof), but it's something I disagree with.

To do this kind of polymorphic dispatch, objects have to deal with multiple concerns within themselves.

In a video game, a Car might have .render(), .collide(), .playSound(). Later on you can add a Dog, which also has those three methods, and you don't need to edit/recompile the Renderer, the PhysicsEngine, and the SoundEngine. And other programmers can add additional entities like this without introducing bugs into my precious code! What's there not to love?

Well, now both my Car and my Dog need to know about graphics, physics, and sound. And these entities don't exist in isolation. Cars and Dogs need to be rendered in the right order (maybe occluding one another). They'll definitely need to check for collisions with each other. And (something which has actually happened to me in a Game Jam) my sound guy is going to need to step into all my objects to add their sound behaviours.

I would much rather work in the Physics.collideAll() method, and have it special-case (using instanceof) when I'm thinking about physics, and work in the Graphics.renderAll() method when I'm thinking about graphics.

A more common example I see in day-to-day backend Java web dev: when I'm sitting in the (REST) Controller deciding how to convert my Java objects into HTTP responses, I much prefer it if I can consider them all in one method, and map out {instanceof Forbidden} to 403, {instanceof NotFound} to 404, etc., rather than putting getCode() (and other REST-specific stuff) into the Java classes themselves.


The OO solution is to have a PhysicalObject class that dog and car both inherit from (or use via composition).


Then there's even more scattering around of the logic.

Good way:

  PhysicsEngine {
    List<Entities> entities;
    doCollisions() {
      // Logic involving instanceof
    }
  }
Bad way:

  PhysicsEngine {
    List<Entities> entities;
    doCollisions() {
      // Delegate to whomever.
      entities.forEach(e -> e.collide());
    }
  }

  Dog {
    collide() {
      // What the hell can I do here?
      // I don't know about the rest of the world
    }
  }

  Car {
    collide() {
      // What the hell can I do here?
      // I don't know about the rest of the world
    }
  }

Worse way:

  PhysicsEngine {
    List<Entities> entities;
    doCollisions() {
      // Delegate to whomever.
      entities.forEach(e -> e.collide());
    }
  }

  Dog : PhysicalObject {
    collide() {
      super.collide();
    }
  }

  Car : PhysicalObject {
    collide() {
      super.collide();
    }
  }

  PhysicalObject  {
    collide() {
      // Not only do I not know about the rest of the world
      // I don't even know how *I* collide, because what am I?
    }
  }


It doesn't always make sense to allow extending -- String is `final` for a reason (and one might even argue that final should be the default, and one should explicitly mark with `open` classes that can be subclassed).

The stereotypical FP example for sum types are a List -- there you only have an Element<T>(T head, List<T> tail) and a Nil(). There is no point extending it, it would, in fact, result in incorrect code in conjunction with all the functions that operate on Lists.

Also, the Visitor pattern, which is analogous to pattern matching is very verbose and depends on a hack with the usual method dispatch semantics of Java. I do think that pattern matching is several times more readable here.


In a world of untrusted code and SecurityManagers, it was critical that strings be immutable so a data race couldn’t bypass a policy decision. The JVM doesn’t have immutable arrays, so string methods carefully protected the embedded array from tampering, and couldn’t be overridden.

Most classes don’t have this problem. The original authors of a class don’t know what I’m trying to do, and they don’t bear consequences if I (a consenting adult) get it wrong.


There are valid use cases for that.

Consider a security interface of some sort, e.g. such that validates a security token.

With a normal interface, it is easy to implement it and ignore the token (allow all), siphon off the token, add a backdoor, etc. If a class doing that is somehow injected where a security check is done, it can compromise security.

Now with a sealed interface, there cannot be new, unanointed implementations. If you get an object that claims to implement that interface, it's guaranteed to be one if the a real, vetted implementation that does the actual security check, not anything else. You've just got rid from a whole class of security bugs and exploits.




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: