DEV Community

Discussion on: Why it is not possible to extend a final class in Java?

Collapse
 
cjbrooks12 profile image
Casey Brooks

The final keyword is much more related to the intent behind that class than any physical restriction from Java or the JVM. As others have mentioned, it is technically possible to extend a final class, but it is difficult and dangerous to do so.

In Java, a final class is an opt-in feature, meaning the developer chose to make this class final. They looked at the class and the way it is intended to be used, and decided it makes more sense for it to not be extended. There are several reasons why they might have made this choice:

  • Performance. Especially in low-level platform classes, there are a lot of optimizations that can be done if the specific behavior of a given class is completely known. By forcing these classes to be final, other code can make certain assumptions that improve performance.
    • Example: a field will never be null and so we don't need a null-check. If a class can be extended, we no longer have that guarantee, and to ensure safety we would otherwise have to do a null check every time we wished to use a certain variable.

  • Ensure Correct Framework Usage. When working with a large codebase, you will inevitably be faced with a situation where a given task could be implemented in multiple different ways. By making many of the core framework classes final, the original developer is actually helping guide new developers into the correct/intended way to use the framework.
    • Example (from Android): I am trying to add a Fragment to a particular ViewPager. The Android ViewPager accepts any Fragment instance, but our application logic requires an instance of FragmentProvider (a custom class), not Fragment. This FragmentProvider class is final, and contains methods for loading/unloading fragments and tying them into our custom application lifecycle, which is functionality built on top of the normal ViewPager. Our ViewPager adapter only accepts a list of FragmentProvider as input, so there is only one way to add a new Fragment to do it, and that one way is perfectly in-line with the original developer's intention.

  • Protect Brittle Code. - Another situation which arises in large codebases is the inevitably-brittle class, which is large, filled with spaghetti code, and is quite buggy. By making this class final, we can lock it down and force all consumers to use that class exactly as it is, which makes it a bit easier to wrangle the damage that this class can cause. If it could be extended, not only do we have to make sure that this class itself isn't crashing, but that all classes that extend it aren't also crashing in their own code, and also that the particular combination of overridden methods isn't causing strange, unexpected behavior, both within that class and within the classes that depend on it.

  • Prevent Inheritance-hell. No framework was ever built with the expectation that a class would be extended 10, 20, 30 or more times. It expects it would be extend once, maybe twice. But as the framework grows in size, it is all too easy for extended classes to be extended, and those to be extended, and so on. Having a shallow inheritance tree is better for performance and for understandability, and so a class might make itself final to try and enforce this. It might be at the very first level, so that a class is never extended, or maybe after a couple subclasses have been created, all those will be made final to effectively stop extension-growth beyond that level or in a particular sub-tree. All this helps to keep the code clear and easy to navigate.
    • Anti-example (from Android): The Context base class in Android is a god-class that knows entirely too much, and is fragmented across many different implementations through a deep and confusing hierarchy. An Application is a Context. An Activity is a ContextThemeWrapper which is a ContextWrapper which is a Context. And supporting older versions of android, an AppCompatActivity has 9 extended classes before reaching Context. This makes it confusing to reason about which code is actually being run, and in what order, and it would have probably been a better idea to have made just a few Context-type classes (just Context, Application, and Activity) and have the rest of the needed behavior implemented through composition.