Inconsistent open/final defaults between classes and functions

Classes and functions are final by default in Kotlin. It’s debatable, but that’s another topic.

Both classes and functions can be marked abstract or open to allow for inheritance and overriding. Makes sense so far.

But when an open function is overridden, it still remains open unless explicitly marked as final. On the other hand, when an open (or abstract) class is extended, it becomes final unless explicitly marked as open (or abstract).

What’s the rationale for this inconsistency? The only thing I can think of is that since the class is final by default, it essentially makes all its methods effectively final, so there is no need to make them really final. But that just doesn’t seem to be convincing enough.

Example. I want to use the Template Method pattern, at the same time extending an existing abstract class:

abstract class LibraryClass {
    abstract fun doSomething()
}

abstract class MyTemplateClass : LibraryClass() {
    override fun doSomething() {
        // some init
        reallyDoSomething()
        // some cleanup
    }

    abstract fun reallyDoSomething()
}

Now doSomething() is still open and someone may (by incident or purpose) override it, even though the Template Method pattern is intended to avoid overriding of concrete methods and calling super. For this code to work as intended, I need to explicitly add final to the overriding function.

Is it just an oversight in language design or I am missing something here?

5 Likes

Well, I also thought about it as a little strange. If there was a need to override the same method several times I think that an explicit open keyword for each time would be a better option.

You should take a look at “Design and document for inheritance or else prohibit it” of “Effective Java”.

override implies open like abstract requires open, you still have to mark overrideable methods using override or open, if you don’t mark a method then it is final by default, similarly if you don’t mark a class then it is final by default.

I think you miss the point. It makes perfect sense to use final by default. What doesn’t is that when you extend an open class, it becomes final by default, but when you override an open method, it remains open by default.

3 Likes

Kotlin requires to mark every overridden method with override, this is different behaviour than class.
class does not require a modifier, so it is final.
fun requires override and override implies open, like abstract implies open.

Why don’t require the open override modifier?
As described di Effective Java, we have to pay attention to design a class for inheritance, so we have to design entry points using open or abstract.
Kotlin’s designer supposed that these entry points remain unchanged in subclasses, so we have to mark methods required by inheritance and notified any changes to other developers.
Instead class extension does not require any modifier, so the default behavior remains final.

4 Likes

So it’s purely a syntax thing?

Because sure, there is no override modifier, but there is that little colon that has essentially the same meaning for functions.

The “entry point” reasoning sort of makes sense to me, but I’m still not fully convinced. Back to my initial example, suppose a library designer made an abstract method. Surely it’s a clear indication that this method is an entry point and must be overridden by every concrete subclass. So far so good. But when I extend that class, and override that method, is it still an entry point or not, design-wise? The method is no longer abstract, it’s concrete, it does certain things, so whatever inheritance logic the author of the original library had in store for this method, it no longer applies to my concrete implementation. It’s now up to me to think carefully how my class may (or may not) be extended, and what should be done with that method implementation. And more often than not, that method should not be overridden any more, simply because overriding concrete methods is generally a bad idea. The only notable exception I can think of is empty concrete methods that serve as entry points for descendants (such as the ones in classes like MouseAdapter).

Surely it’s a non-issue most of the time, simply because if I just extend a library class like that, without giving a second thought about inheritance, then my class will end up final (by default) and therefore nobody can extend it and override its methods anyway.

Every class extends Any, so the little colon is not enough.

I want to suppose that these entry-point don’t change in each subclass.

Please consider deeper this statement, why I should not override an open concrete method?

I add equals, hashCode, toString, compareTo, close and many other domains-specific methods.
In short, your list may be incomplete.

This is what Joshua Bloch teaches and Kotlin implements.

1 Like

Ah, yes, I see now. I was only considering class-specific methods, when it’s the norm that a method is either abstract or empty, and gets overridden exactly once. But when considering methods such as equals() or other methods widely used across many classes, everything changes.

Makes sense then.