5

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?

2

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).

0

The compiler can only talk about objects in terms of their interface/contract. However the Java runtime always uses references to refer to objects at runtime. Note the difference between the static environment for the compiler, and the dynamic environment for the runtime.

e.g.

Square square = new Square();
square.setSideLength(2);

Shape shape = square; // assuming Square is a Shape
shape.size() == 4;

so the compiler won't let me setSideLength() on an 'shape', since setSideLength() is a method on the declared Square.

But when I call size() on the shape reference, which method to call is determined at runtime. e.g. imagine this:

List<Shape> shapes = mixtureOfSquaresAndTrianglesAndCircles;
shapes.get(2).area(); // I can call this regardless of the underlying being a triangle, square, circle etc.
  • So in the code above. If I call a.m2(). It would not compile? Since a doesn't have the method m2()? – fizzix Apr 13 '16 at 16:38
  • @fizzix Yes, a.m2() won't compile in your example. Not because a doesn't "have" method m2() (it does have it, since a is an instance of B), but because you only told the compiler enough information to know it's an A. Note that if you casted a to class B, you'd be effectively telling the compiler "I assure you this a is an instance of B", and it'd let you call method m2, like this: ((B) a).m2(). – Andres F. Apr 13 '16 at 21:58

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.