skip to content

Java makes methods overridable by default, while C++, C# and Kotlin require the base author to opt in with virtual or open. Argue both positions: what does opt-in overridability buy, and what does it cost consumers of a library who need an extension point the author did not provide?

level: principalimportance: should knowfreq 26%

answer

  1. C++ non-virtual default = zero-overhead; forgotten virtual destructor = undefined behaviour
  2. C# virtual/override/new/sealed override -- nothing implicit
  3. Kotlin classes AND members final; all-open plugin exists for Spring
  4. JIT devirtualizes via CHA + inline caches -- final as perf hint is folklore
  5. Swift: extension methods dispatch statically, cannot be overridden

basics

~20 s

Opt-in overridability (C++ virtual, C# virtual, Kotlin open) makes the base author declare every extension point, so nothing is accidentally part of the contract. It costs downstream users adaptability: what the author did not open cannot be adapted, mocked or wrapped, so they extract interfaces, fork, or use bytecode tricks.

solid answer

~50 s

**For opt-in.** C++ started here for cost: dispatch means a vtable pointer and blocked inlining, incompatible with zero-overhead abstraction. C# added `virtual`/`override`/`new` to make every hook deliberate. Kotlin made classes *and* members final unless `open`, following Effective Java's "design and document for inheritance or else prohibit it". Every method you did not open is a method you can still change. **Against.** Sealed surfaces make consumers powerless. C# and Kotlin codebases extract interfaces largely so mocking frameworks have something to intercept, and Kotlin ships an `all-open` compiler plugin whose only purpose is to undo the default for Spring proxies -- a language default so strict that a framework needs a plugin to reverse it. Java's counter-argument is that the JIT devirtualizes via class-hierarchy analysis and inline caches, so the runtime cost largely evaporates; the price is that every public non-final method is a contract forever. Swift shows a third failure: methods added in an extension dispatch statically and cannot be overridden.

code

kotlin · 7 lines
kotlin
class Repo { fun find(id: Long): Row? = TODO() }

class FakeRepo : Repo()       // error: Repo is final and cannot be inherited

// Required opt-in:
open class Repo2 { open fun find(id: Long): Row? = TODO() }
// or apply the all-open compiler plugin so Spring can proxy annotated classes

go deeper

for a junior

Know that the default differs by language and that C++, C# and Kotlin require an explicit keyword before a method can be overridden.

for a middle

Explain the mechanics on both sides -- C++ vtable cost, C#'s override/new pair, Kotlin's open -- and why frameworks that generate subclass proxies collide with the strict default.

for a senior

Weigh the maintenance argument against the testability and adaptability cost, and describe the non-virtual interface idiom as the way to stay closed without being unextendable.

for a principal

Set the policy: opt-in for published libraries where every open method is a versioned promise, pragmatic openness inside a single repository, and dismiss the performance argument on JIT runtimes while keeping it for ahead-of-time compilation.

## The decision Every class-based language must choose a default for whether a method participates in dynamic dispatch. It is one flag in a specification and it reshapes how libraries are written, tested and evolved. ## The case for opt-in, language by language **C++** chose non-virtual by default for cost. A class with no virtual functions has no vtable pointer, fits in the size its members imply, and its calls inline. That is the zero-overhead principle: you do not pay for dispatch you did not ask for. The famous consequence is the base destructor -- delete a derived object through a base pointer whose destructor is not virtual and the behaviour is undefined -- which is a direct tax on making the safe thing opt-in. C++11 added `override` and `final` because the silent-failure modes of the default were costing more than they saved. **C#** kept non-virtual by default and went further, requiring `override` at the override site and `new` to declare a deliberately shadowing member, plus `sealed override` to stop further overriding partway down. Nothing is accidental in either direction. **Kotlin** made classes final too, not just members. The stated rationale is Effective Java's Item 19: design and document for inheritance, or prohibit it. An unopened method has no subclass contract, so its behaviour, its self-calls and even its existence remain the author's to change. **Swift** is subtler: methods declared in a class body are dynamically dispatched, but methods added in an *extension* are dispatched statically and cannot be overridden. The same declaration in two syntactic positions gets different dispatch, which is opt-in virtuality expressed through file layout rather than a keyword. ## The case against Sealing shifts risk from the author to the consumer, and the consumer usually has fewer options. - **Testing.** Mocking frameworks that work by subclassing cannot intercept a non-virtual member. This is the practical reason C# codebases extract an interface for almost every collaborator: the interface exists to restore a seam that the language default removed. Kotlin teams hit it the same way and reach for `mockk`'s inline instrumentation or open up types under test. - **Frameworks.** Spring generates proxies by subclassing, so Kotlin classes are unusable as beans by default. The response was the `all-open` compiler plugin, which opens annotated classes automatically. When an ecosystem ships a plugin whose entire purpose is to reverse a language default, the default is contested, not settled. - **Adaptation.** If a library author did not open a method and you need to change its behaviour, your remaining options are wrapping the whole type (only works if it implements an interface), forking, or bytecode manipulation. Java's default at least leaves the door unlocked; whether that door should have been unlocked is exactly the argument. ## Java's rebuttal The cost argument, which motivated C++, is weak on a JIT runtime. The compiler performs class hierarchy analysis: if only one implementation of a method is loaded, the call is devirtualized and inlined, guarded by a check that deoptimizes if a second implementation appears. Polymorphic call sites get inline caches. So the throughput difference between virtual-by-default and final-by-default is small in practice, and `final` as a performance hint is largely folklore. What remains is the *contract* argument, and there Java is genuinely exposed. Any public non-final method is an extension point whether or not you designed one, which means its self-call pattern is observable, its behaviour is depended upon, and a constructor-time call to it can reach a subclass override. Java's `sealed` classes and interfaces exist to give authors control over hierarchy shape, and record and enum types are implicitly final, which shows the language moving toward closure where it can. ## The dynamic end of the spectrum Python and Ruby have no sealing at all: everything is overridable and, beyond that, replaceable at runtime. Ruby's `prepend` inserts a module ahead of a class, so a third party can wrap any method without subclassing. The defence is convention -- Python's double-underscore name mangling, a leading underscore signalling privacy -- and the culture accepts that consumers can and will reach in. Total flexibility, zero enforceable contract. ## Making the call For a library published to strangers, opt-in is the defensible default: every open method is a promise you must keep across versions, and closed methods are your only freedom to evolve. For application code inside one repository -- where you can change every caller in the same change -- the argument mostly evaporates, and the cost of ceremony is real. Two practices soften the tradeoff regardless of default: the non-virtual interface idiom (a public non-overridable method that calls a protected overridable hook, so the author controls the sequencing and the subclass supplies only the variable step), and separating the *client* interface from the *specialization* interface so the extension points are explicitly named. Both let you be closed by default without being unextendable.

  • Is marking methods final a meaningful performance optimization on a JIT runtime?
    Rarely. The JIT performs class hierarchy analysis and devirtualizes monomorphic call sites regardless of the final keyword, guarding the assumption and deoptimizing if a second implementation loads; polymorphic sites use inline caches. Final is best justified as a design statement about the contract, not as a speed knob. The situation differs in ahead-of-time compiled languages without whole-program visibility, where sealing genuinely enables direct calls and inlining.
  • Kotlin closes classes by default, yet Spring applications work. How, and what does that tell you?
    The all-open compiler plugin opens classes carrying configured annotations, and the kotlin-spring preset applies it to Spring's stereotypes, so proxy generation by subclassing still works. It tells you the strict default collides with a major ecosystem's mechanism, and that the collision was resolved by tooling rather than by either side changing position -- which is the strongest evidence that the default is a genuine trade rather than a settled best practice.
  • How does the non-virtual interface idiom let an author be closed by default without being unextendable?
    The public method is non-overridable and owns the sequencing -- argument checks, locking, logging, invariant assertions -- and it calls a protected, overridable hook for the single varying step. Subclasses can only change the step the author designated, and the author keeps the right to change everything around it. It makes the specialization interface explicit and separate from the client interface, which is the underlying point in any language.

Opt-in overridability is a building with a few marked service hatches; virtual-by-default is a building where every wall is a possible doorway. The first is easier to renovate, the second easier for tenants to adapt -- and someone will knock through a wall you were planning to move.

saying these in an interview costs you the question

  • Presenting one default as objectively correct rather than a trade between author freedom and consumer adaptability
  • Claiming final gives a meaningful speedup on a JIT runtime
  • Not knowing that a non-virtual base destructor in C++ makes deletion through a base pointer undefined behaviour
  • Assuming Swift methods are always dynamically dispatched, including those declared in extensions
  • Arguing for sealing everything without addressing testability and the seams mocking frameworks need

context