Java – Understanding polymorphism and interface in Java

interfacesjavapolymorphism

I'm reading though some notes. And I'm not understanding the following two statements.

  1. Polymorphism means that it is always the class of the actual object at run time that determines which method will be called at run time.

    class A{
        public void m1(){
        System.out.println(" A");
        }
    } 
    
    class B extends A{
        public void m1(){
        System.out.println(" B");
        }
    
        public void m2(){
        System.out.println(" m2");
        }
    } 
    
    B b = new B();
    A a = new B();
    // will print B
    b.m1();
    // will also print B because a refers to an object of class B.
    a.m1();
    
  2. While accessing a method or variable, the compiler will only allow you to access a method or variable that is visible through the class of the reference.

    interface Flyer{
        String getName();
    } 
    class Bird implements Flyer{
    ...
        public String getName(){
            return name;
        }
    
        public String getNativeOf(){
        return nativeOf;
        }
    }
    
    Flyer f = new Bird();
    f.getName(); //valid
    f.getNativeOf(); //will not compile because the class of reference f is Flyer and not Bird.
    ((Bird) f).getNativeOf(); //valid because class of the object referred to by f is Bird.
    

So is it by actual object or reference?

Best Answer

Both. You're somewhat conflating the notions of method/field availability by type and late binding. They are separate, though related.

... actual object at runtime determines which method ...

I might restate this as "whose" method instead of "which" method to be more specific. The method name + signature that will be called is determined at compile time and fixed, but the particular implementation (e.g. base or some override) of that method is determined at runtime. This is also sometimes called late binding.

(In addition to name + signature, the method that will be called will also be directly related to the method of the class that originally introduced it, e.g. it will be that method itself, or an override of that in a subclass.)

Take the situation where we have:

class A {
     void foo ();
}
class B extends A { ... }
class C extends B { ... }
class D extends A { ... }
...
class U {
    void method ( A a ) {
        a.foo ();
    }
}
...
U u = new U();
u.method ( new A () );
u.method ( new B () );
u.method ( new C () );
u.method ( new D () );

We know that method will call an existing method named foo with no parameters, which was introduced by class A. However, it may call A's foo, B's foo, C's foo, or D's foo . Here the language runtime (Virtual Machine) executing method determines whose foo to call using late binding.

The access issue is a compile time determination made using static analysis and the type system rules (of the particular language involved). Using types at compile time, languages can prevent certain illegal program states (with a compile time error instead of a runtime error), such as there is no foo to call.

You should also note that there are fundamentally several types of cast. Upcasts (casting from B to A, or C to B, or C to A) are safe and can be validated at compile time. Down casts (e.g. from A to B, or A to D) are not always necessarily compile time analyzable, and therefore are designed into the language to perform a runtime check at the point of cast (before things go any further and make use of the casted value).

As a result, you can get an illegal cast exception, at runtime. By having the language be defined so that the compiler and runtime conspire to check for illegal casts, they don't have to check for method not found at runtime no matter what true class the object is: this because it knows the cast worked, therefore the method exists (reflection notwithstanding; also DLL versioning complicate things, but there is an additional load-time check that complements the compile time check and both together replace the need for a runtime check for methods existing or not.).

Though in your particular example with Flyer and Bird, a static analysis could have elided the runtime cast check in ((Bird) f), in general this is not always possible. Your example goes on to call a method (only) available in Bird.

Note that by definition of the byte-code instructions, this method invocation (getNativeOf()) can only be performed (i.e. can only be reached by flow of control) if the runtime cast succeeds, as a failure of the cast will throw an exception thus altering the normal flow of control and preventing the method invocation from being reached.

(In addition that the compiler won't generate the following, a specially concocted byte code program attempting to access getNativeOf() from a Flyer object without a necessary protective runtime cast to Bird will fail a load-time verification check! Though this happens post compile time, it is also a form of static analysis, in other words, is only done once at runtime (known as load time) rather than each time the code is used.)

Following the cast, the compiler will generate the method invocation assuming the cast succeeds (knowing that if we are at this point, the cast must have succeeded), and as such knows at compile time the name + signature (and introducing class) of what method to call. The resulting actual method called still uses late binding at runtime to find whose implementation or override of that method is appropriate for the true type of the object (optimization notwithstanding, but any optimization has to give the same results as if late binding were used).