Search code examples
kotlinoverridingaccess-modifiers

Why can't I access the public method after overriding a protected method?


I have a question about access modifiers override.

First, let's look at the following code.

fun main() {
    val b: AAA = BBB()

    b.testA()
}

open class AAA {
    fun testA() {
        println("This is testA()")
    }
}

open class BBB : AAA()

result:
This is testA()

No problem with this code, b has printed testA() of class AAA.

Let's see the following code:

fun main() {
    val b: AAA = BBB()

    b.testA()
}

open class AAA {
    open fun testA() {
        println("This is testA()")
    }
}

class BBB : AAA() {
    override fun testA() {
        println("This is overridden testA() ")
    }
}

result:
This is overridden testA()

This code is also no problem.

Because testA() has been overridden, testA() of class BBB is called.

Now, here's the code I'm going to ask.

fun main() {
    val b: AAA = BBB()

    b.testA()
}

open class AAA {
    protected open fun testA() {
        println("This is testA()")
    }
}

class BBB : AAA() {
    public override fun testA() {
        println("This is overridden testA()")
    }
}

This code will not be executed. The IDE complains Cannot access 'testA': it is protected in 'AAA'.

Although testA() of AAA is protected, it was overridden as public in BBB, so I thought that naturally the overridden testA() of BBB would be called like the second code.

What's the problem? Am I missing something? What part of inheritance and access modifiers am I misunderstanding?


Solution

  • Although testA() of AAA is protected, it was overridden as public in BBB, so I thought that naturally the overridden testA() of BBB would be called like the second code.

    The misunderstanding is about when things happen.

    A method call is "dispatched dynamically" with respect to overrides. What this means is that the correct override is chosen at the very last minute when you actually run the code. In your case, the implementation of the method in BBB is used (because b actually points to an instance of BBB, a.k.a an object of type BBB). This is how polymorphism works, and this is why, indeed, you can see This is overridden testA() in your compiling snippets.

    However, the compiler (and the IDE) doesn't run your code, so it can only use information that is known to be true at compile time. What matters in this case is the type of the reference variable b that you use to call the method testA(). In your last snippet, the type of b is explicitly marked as AAA, so the compiler analyses your code based on that, and the method will be resolved to AAA.testA() for the purpose of the compilation. The visibility will also be checked based on this: AAA doesn't have a visible testA method. You can actually see this in action if you try to navigate to the function definition in an IDE (it will lead you to the definition in AAA, not the one in BBB).

    In simpler terms, you can imagine that the compiler sees b as an AAA without any assumptions about the actual value that it will point to when the code runs. This is literally what this means to declare the type AAA explicitly: you're telling the compiler to ignore the extra information that it had about b, and to only consider the API available on all objects of type AAA.

    If you want your last snippet to compile, you would have to declare b of type BBB (or remove the explicit type and let the compiler infer the type to BBB automatically).

    fun main() {
        val b: BBB = BBB() // or simply val b = BBB()
    
        b.testA()
    }