How do override and access modifiers interact in a three-level class hierarchy?

← Override Mechanics · Ref: Q676

When overriding in multi-level class hierarchies, three rules must hold at every level: the override keyword must be present, the access modifier must be the same or wider, and the signature must match exactly. These rules apply equally to METHODS and OPERATORS.

OVERRIDE ON METHODS AND OPERATORS

The override keyword works identically on both:

  override describe() as pure         method override
  override operator ? as pure         operator override
  override operator $ as pure         operator override

If the parent defines it, the child must say override.

OVERRIDE KEYWORD REQUIRED (E05120)

Any method or operator that replaces a parent version MUST use 'override'. Without it, the compiler reports E05120. This applies to operator ? (inherited from base type), operator $ (if parent defines it), and all other operators.

ACCESS MODIFIER MATCHING (E05130)

An override can maintain or WIDEN access. Narrowing access (public to private) violates Liskov and triggers E05130.

SIGNATURE MATCHING (E05140)

Parameter types and return types must match.

THREE-LEVEL CHAIN

Grandparent declares the contract. Parent overrides it. Grandchild overrides it again. Each level must independently satisfy override, access, and signature rules — for both methods and operators.

See Q570 for override basics. See Q575 for deep override chains. See Q579 for trait override chains.

Example

defines module qa.override.accesshierarchy

  defines class

    <?-
      Level 1: Grandparent declares protected methods.
      These form the contract for all descendants.
    -?>
    Grandparent as abstract

      protected describe()
        <- rtn as String: "grandparent"

      protected classify() as pure abstract
        <- rtn as String?

      default operator ?

    <?-
      Level 2: Parent overrides with matching access.
      Protected stays protected, override keyword present.
    -?>
    Parent extends Grandparent as open

      override protected describe()
        <- rtn as String: "parent"

      override protected classify() as pure
        <- rtn as String: "parent-class"

      default operator ?

    <?-
      Level 3: Child overrides again, maintaining contract.
      Each level independently satisfies all three rules.
    -?>
    Child extends Parent

      override protected describe()
        <- rtn as String: "child"

      override protected classify() as pure
        <- rtn as String: "child-class"

      default operator ?

  defines program

    OverrideAccessHierarchyDemo()
      stdout <- Stdout()

      child <- Child()
      stdout.println(`Override chain verified: ${child?}`)

Common mistakes

E07260 — Without the 'override' keyword, the compiler treats 'protected describe()' as a new method declaration that shadows the parent method, triggering E07260. See ek9 -h E07260 for details.

Incorrect:

protected describe()

Correct:

override protected describe()

E07010 — An override must maintain or widen the access level. Narrowing from protected to private violates Liskov Substitution and triggers E07010. See ek9 -h E07010 for details.

Incorrect:

override private classify() as pure

Correct:

override protected classify() as pure

E05110 — The override signature must match the parent exactly. Adding, removing, or changing parameter types means the method does not actually override the parent and triggers E05110. See ek9 -h E05110 for details.

Incorrect:

override protected describe()
        -> extra as Integer
        <- rtn as String: "child"

Correct:

override protected describe()
        <- rtn as String: "child"
Other ways to ask this
  • What is E05120 method signature mismatch in override?
  • What is E05130 access modifier mismatch in override?
  • What is E05140 covariant return type required in override?
  • How does override protected work in deep class hierarchies?

Coming from another language?

Java: @Override annotation, access can widen (private to protected), signatures must match. Kotlin: 'override' keyword mandatory, access widening allowed. Rust: no inheritance, trait impl must match. Python: no override keyword, duck typing. EK9: 'override' keyword mandatory, access must match or widen, signature must match exactly.

Keywords: access, virtual, override, protected, class, hierarchy, chain, signature, E05140, open, E05130, abstract, E05120, inherit