Java's Dynamic Method Selection Explained

This article is fairly under-construction...

Java's dynamic method selection is painful to understand if you don't get its design methodology.

We're gonna go through a few simple code snippets to make sure we can understand it with keeping some really simple principles in mind.

Suppose we have the following interface and class implementation:

public interface Animal {
    default void greet(Animal a) {
        System.out.println("greet :: Animal!");
    }

    default void sniff(Animal a) {
        System.out.println("sniff :: Animal!");
    }

    default void flatter(Animal a) {
        System.out.println("flatter :: Animal!");
    }
}

public class Dog implements Animal {
    public void sniff(Animal a) {
        System.out.println("sniff :: Dog!");
    }

    public void flatter(Dog d) {
        System.out.println("flatter :: Dog!");
    }
}

Let's assume we have the following lines of code in a valid main function that can be run directly. What is the expected output for the program?

Animal a = new Dog();
Dog d = new Dog();
a.greet(d);

It should be not hard for you to convince yourself a line with greet :: Animal! will be printed out. OK, what about the following line?

d.flatter(d);
d.flatter(a);

The answer is

flatter :: Dog!
flatter :: Animal!

You should be able to notice that the flatter method in class Dog is actually overloading the flatter method in interface Animal rather than overriding it (the method signatures are different!). But how does Java handle all these complicated things?

Be relaxed, let's understand and remember the following principles:

  • If we have a variable has static type X and dynamic type Y, then if Y overrides the method, Y's method is used instead.
  • At compile time, the compiler will verify X has a method that can handle the given parameter. If multiple methods can handle, it records the most specific one.
  • At runtime, if Y overrides the recorded signature, use the overridden method.

We provide a more concrete example and comments for a better understanding of these principles.

public class Main {
    public static void main(String[] args) {
        // static: Animal
        // dynamic: Dog
        Animal a = new Dog();
        // static: Dog
        // dynamic: Dog
        Dog d = new Dog();

        // compile: Animal's `greet(Animal a)` is recorded
        // run: not found Dog's `greet(Animal a)`
        // result: Animal.greet
        a.greet(d);
        // compile: Animal's `sniff(Animal a)` is recorded
        // run: found Dog's `sniff(Animal a)`
        // result: Dog.sniff
        a.sniff(d);
        // compile: Dog's `sniff(Animal a)` is recorded
        // run: keep Dog's `sniff(Animal a)`
        // result: Dog.sniff
        d.sniff(a);
        // compile: Animal's flatter(Animal a) is recorded
        // run: not found Dog's flatter(Animal a)
        // result: Animal.flatter
        a.flatter(d);
        // compile: Animal's flatter(Animal a) is recorded
        // run: not found Dog's flatter(Animal a)
        // result: Animal.flatter
        a.flatter(a);
        // compile: Dog's flatter(Dog d) is recorded
        // run: keep Dog's flatter(Dog d)
        // result: Dog.flatter
        d.flatter(d);
        // compile: Animal's flatter(Animal a) is recorded,
        // as Dog does have a flatter method but not with the same signature.
        // run: not found Dog's flatter(Animal a)
        // result: Animal.flatter
        d.flatter(a);
    }
}

It worth to note that although in the example above Animal is an interface, the exact same idea can be applied for extends as well. Another example is provided here for a practice purpose.

public class Main {
    public static void main(String[] args) {
        // static: Bird
        // dynamic: Falcon
        Bird bird = new Falcon();
        // static: Falcon
        // dynamic: Falcon
        Falcon falcon = (Falcon) bird;

        // compile: Bird's `gulgate(Bird b)` is recorded
        // run: not found Falcon's `gulgate(Bird b)`
        // result: Bird.gulgate
        bird.gulgate(bird);

        // compile: Bird's `gulgate(Bird b)` is recorded
        // run: not found Falcon's `gulgate(Bird b)`
        // result: Bird.gulgate
        bird.gulgate(falcon);

        // compile: Bird's `gulgate(Bird b)` is recorded
        // as Falcon does have a gulgate method but not with the same signature.
        // run: not found Falcon's `gulgate(Bird b)`
        // result: Bird.gulgate
        falcon.gulgate(bird);

        // compile: Falcon's `gulgate(Falcon f)` is recorded
        // run: keep Falcon's `gulgate(Falcon f)`
        // result: Falcon.gulgate
        falcon.gulgate(falcon);
    }
}


class Bird {
    public void gulgate(Bird b) {
        System.out.println("Bird Gul!");
    }
}

class Falcon extends Bird {
    public void gulgate(Falcon f) {
        System.out.println("Falcon Gul!");
    }
}

Reference

  • UCB's CS61B - Inheritance1 Study Guide
  • Runtime Polymorphism or Dynamic Method Dispatch

你可能感兴趣的:(Java's Dynamic Method Selection Explained)