انتقل إلى المحتوى
أمين أبوزيد
منشورBEG-023-L03

الإرسال الديناميكي من الداخل (مفاهيمي)

  • الدرس 3 من 5
  • 12 دقيقة قراءة
  • مبتدئ
في هذا الدرس 11 موضوعات

هذا الدرس متاح حاليًا باللغة الإنجليزية فقط، ولم تُنشر نسخته العربية بعد.

Lesson Overview

So far the rule has been "the object decides". That is true for overridden instance methods, but it is not true for everything in Java. Some decisions are made by the compiler from the reference type, before the program runs. If you do not know which is which, you will sooner or later be surprised by a program that prints something you did not expect.

This lesson is conceptual: it explains how Java decides which code to run for a call, in two stages, compile time and run time. The run-time stage is called dynamic dispatch. You will also see the places where dispatch does not happen: overloaded methods, fields, and static methods.

Why this matters: the "predict the output" questions in this module's assessment, and many real bugs, come from mixing up overloading with overriding. A clear mental model, not memorized tricks, is what lets you predict the output of unfamiliar code.

This lesson builds on the previous two: reference type versus object type (lesson 1), and what the compiler allows through a parent reference (lesson 2). You also need to know the difference between overloading (same name, different parameters, from the methods module) and overriding (same signature in a subclass, from inheritance).

Learning Objectives

By the end of this lesson you will be able to:

  • Describe the two stages of a method call: what the compiler checks and what the JVM decides at run time.
  • Explain dynamic dispatch, including what happens when the nearest class does not override the method.
  • Predict the result when overloaded methods are called through a parent-typed reference.
  • State that fields and static methods follow the reference type, not the object.
  • Explain why @Override is rejected on a static method.
  • Draw a simple picture of reference, object, class, and method lookup.

Written Explanation

Stage 1: the compiler checks the reference type

When the compiler sees a.monthEnd(), it looks at the declared type of a, say Account, and checks that Account (or one of its parents) has a method named monthEnd that accepts those arguments. If not, you get a "cannot find symbol" error, as in the last lesson. If yes, it records which method signature the call refers to. The compiler never looks at the object, because it does not exist yet.

When there are several methods with the same name (overloads), this is also the stage where the compiler chooses the one that best fits the declared types of the arguments. The Java Language Specification describes the compile-time step this way: it works from the compile-time type of the target and the arguments.

Stage 2: the JVM looks at the real object

When the line actually runs, the JVM follows the reference to the object and asks: what is the real class of this object? It then looks for the method starting in that class:

  1. If that class declares a method that overrides the one the compiler selected, run it.
  2. If not, move up to the parent class and repeat.
  3. Continue up the chain until a version is found.

This procedure is the dynamic method lookup of the Java Language Specification, and the overall behavior is dynamic dispatch. It explains the earlier results: a Truck that extends Car and does not override sound() ran Car's version, because lookup started at Truck, found nothing, and moved up one level.

You can picture it like this:

text
   variable a                 object on the heap            class SavingsAccount
 (type Account)  ------->   [ real class: SavingsAccount ] -> monthEnd()  <- found here, run it
                                                              (else: look in Account)

Many JVM implementations speed this up with an internal table of methods per class (often called a virtual method table), so that the lookup does not search the chain each time. That is an implementation technique you do not control, and the language specification describes the behavior, not the data structure. I did not verify any particular JVM's internals, so treat the table as a picture of "each class knows its methods", not as a rule.

Overloading: decided at compile time

Overloading means several methods with the same name and different parameter lists. Which one is called is fixed by the compiler from the declared types of the arguments. No dispatch happens on the argument's real object.

java
static void show(Account a)        { System.out.println("show(Account)"); }
static void show(SavingsAccount s) { System.out.println("show(SavingsAccount)"); }

Account a = new SavingsAccount();
show(a);    // prints show(Account): the declared type of a is Account

The object is a SavingsAccount, yet the Account version runs, because the compiler picked the signature when it only knew the declared type. Contrast this with overriding, where the receiver object (the thing before the dot) picks the version at run time. One way to remember: overloading chooses by the argument types the compiler can see; overriding chooses by the class of the object the method is called on.

What dispatch does not cover

Dynamic dispatch applies to instance methods. It does not apply to:

  • Fields. A field is looked up by the reference type. If a subclass declares a field with the same name, it hides the parent's field rather than replacing it, and the two fields both exist in the object. Access through an Account reference gives Account's field. (This is another reason to keep fields private: you almost never want this behavior.)
  • Static methods. They belong to the class, not to an object. A subclass static method with the same signature hides the parent's, and the call follows the declared type. The Oracle tutorial states this difference between hiding and overriding. Because static methods are not overridden, @Override on one is a compile error (shown below).
  • Private methods. They are not inherited, so there is nothing to override.
  • Constructors. They are not inherited either.

Examples

Example 1: lookup climbs the class chain (simple)

java
class Account {
    String describe() { return "Account"; }
    String kind() { return "plain"; }
}
class SavingsAccount extends Account {
    @Override String describe() { return "Savings"; }
}
class PremiumSavings extends SavingsAccount {
    @Override String kind() { return "premium"; }
}
// in main:
Account a = new PremiumSavings();
System.out.println(a.describe());
System.out.println(a.kind());

Output:

text
Savings
premium

For describe(): lookup starts at PremiumSavings, which does not declare it, moves to SavingsAccount, which does. For kind(): PremiumSavings declares its own, so it runs at once. At compile time the only requirement was that Account has both methods.

Example 2: overloading versus overriding (practical)

java
class Account { }
class SavingsAccount extends Account { }
class Printer {
    static void show(Account a)        { System.out.println("show(Account)"); }
    static void show(SavingsAccount s) { System.out.println("show(SavingsAccount)"); }
}
// in main:
Account a = new SavingsAccount();
SavingsAccount s = new SavingsAccount();
Printer.show(a);
Printer.show(s);
Printer.show((SavingsAccount) a);

Output:

text
show(Account)
show(SavingsAccount)
show(SavingsAccount)

All three calls pass the same object. The compiler chose each overload from the declared type of the argument: Account, SavingsAccount, and (after the cast) SavingsAccount. This is the surprise to remember: a parent-typed argument selects the parent's overload even when the object is a child. If you want the behavior to depend on the real object, put it in an overridden instance method, as the lessons before did, instead of in overloaded helpers.

Example 3: fields and static methods follow the reference (realistic)

Real code sometimes ends up with a field or static method repeated in a subclass, usually by copy-paste. Here is what that does:

java
class Account {
    String name = "Account-field";
    static String hello() { return "Account.hello"; }
    String who() { return "Account.who"; }
}
class SavingsAccount extends Account {
    String name = "Savings-field";
    static String hello() { return "SavingsAccount.hello"; }
    @Override String who() { return "SavingsAccount.who"; }
}
// in main:
Account a = new SavingsAccount();
System.out.println(a.who());
System.out.println(a.name);
System.out.println(((SavingsAccount) a).name);
System.out.println(a.hello());

Output:

text
SavingsAccount.who
Account-field
Savings-field
Account.hello

Only the instance method who() was dispatched on the object. The field name and the static hello() followed the declared type Account; a cast to SavingsAccount was needed to reach the child's field. Calling a static method through an instance reference like a.hello() is legal but misleading, so call it as Account.hello() to make clear which class it belongs to.

If you try to "override" the static method with the annotation, the compiler refuses:

text
Bad.java:3: error: static methods cannot be annotated with @Override
    @Override static String hello() { return "S"; }
    ^
1 error

Backend Insight

Logging, mapping, and validation helpers are commonly written as overloaded methods (log(Order), log(Payment)). They work well as long as the caller passes the exact type. When a list of mixed objects is processed through a parent type, the compiler can only pick the overload for the parent type, which often means the "generic" version silently runs for everything.

The reliable pattern, which you will use in the next lesson, is to put the behavior in an overridden instance method on the objects themselves, and call that. Overloads then stay what they are meant to be: convenient entry points for different argument types known at compile time.

Common Mistakes

Mistake 1: Confusing overloading with overriding

  • Incorrect: "show(a) will pick show(SavingsAccount) because the object is a savings account."
  • Correct: "show(a) picks the overload from the declared type of a, which is Account."

Overloading is resolved at compile time from declared types. Overriding is resolved at run time from the real object. This is listed as a common mistake for the whole module, and the next lesson's refactoring is the cure.

Mistake 2: Expecting fields to behave like methods

If a parent and child both declare name, reading a.name through a parent reference gives the parent's field. The fix is design, not syntax: do not redeclare fields in subclasses.

Mistake 3: Calling static methods through instances

a.hello() runs the static method of the declared type, not of the object, and readers will assume dispatch. Write Account.hello().

Exercises

Try each exercise yourself before opening the solutions.

Level A: Foundation

A1. For each decision, say whether it is made at compile time or at run time: (a) whether getRate() exists on Account; (b) which overridden monthEnd() runs; (c) which of two overloaded show methods is called; (d) which static method runs for Account.hello().

أظهر الحل A1

A1. (a) compile time. (b) run time. (c) compile time. (d) compile time: it names the class explicitly, and static methods are not dispatched on objects.

A2. Given class P { String m() { return "P"; } }, class Q extends P { @Override String m() { return "Q"; } }, class R extends Q { }, what does P x = new R(); System.out.println(x.m()); print, and why?

أظهر الحل A2

A2. It prints Q. Lookup starts at R, the real class. R does not declare m(), so the search moves to Q, which does.

A3. True or false: a static method with the same signature in a subclass overrides the parent's static method. Explain.

أظهر الحل A3

A3. False. It hides the parent's static method. Which one runs depends on the declared type or class name used in the call, not on any object.

Level B: Application

B1. Predict the six printed lines:

java
class Shape { String name = "shape"; String info() { return "shape " + name; } }
class Square extends Shape {
    String name = "square";
    @Override String info() { return "square " + name; }
}
static String log(Shape s)  { return "log(Shape)"; }
static String log(Square s) { return "log(Square)"; }
// in main:
Shape sh = new Square();
System.out.println(sh.info());
System.out.println(sh.name);
System.out.println(log(sh));
System.out.println(log(new Square()));
Square q = new Square();
Shape alias = q;
System.out.println(log(alias));
System.out.println(alias.info());
أظهر الحل B1

B1. square square, shape, log(Shape), log(Square), log(Shape), square square. The overridden info() is dispatched on the object, and uses the Square field. sh.name follows the reference type Shape. Both log(sh) and log(alias) are chosen from the declared type Shape. I compiled and ran this to confirm.

B2. Predict the four lines:

java
class Base {
    String run() { return helper(1); }
    String helper(int x) { return "Base.helper"; }
}
class Derived extends Base {
    @Override String helper(int x) { return "Derived.helper"; }
    String helper(long x) { return "Derived.helper(long)"; }
}
// in main:
Base b = new Derived();
System.out.println(b.run());
System.out.println(b.helper(1));
System.out.println(((Derived) b).helper(1));
System.out.println(((Derived) b).helper(1L));
أظهر الحل B2

B2. Derived.helper, Derived.helper, Derived.helper, Derived.helper(long). Inside Base.run(), helper(1) has an int argument, so the compiler selects helper(int), and the override in Derived runs because the object is a Derived. The long overload exists only in Derived, so it is reachable only with a Derived-typed reference and a long argument.

B3. In B1, you want log to report log(Square) for alias. Rewrite so it works through a Shape reference without casts.

أظهر الحل B3

B3. Make it an overridden instance method:

java
class Shape  { String logName() { return "log(Shape)"; } }
class Square extends Shape { @Override String logName() { return "log(Square)"; } }
// alias.logName() prints log(Square)

I compiled and ran it.

Level C: Challenge

C1. Describe in four steps what happens, in order, when a.monthEnd() runs with Account a = new SavingsAccount(...): one step for the compiler, and the rest for the JVM.

أظهر الحل C1

C1. (1) Compile time: the compiler checks that Account declares monthEnd() and records that signature. (2) At run time the JVM follows the variable to the object. (3) It finds the object's real class, SavingsAccount. (4) It looks for monthEnd() starting in SavingsAccount (climbing to Account if it were not there) and runs the first one found.

C2. A colleague writes @Override static String hello() in a subclass and says "I want it to be polymorphic". Explain the compiler's reaction and what they should do instead.

أظهر الحل C2

C2. The compiler reports an error ("static methods cannot be annotated with @Override"), because static methods are hidden, not overridden. To get behavior that depends on the real object, make the method an instance method in the parent and override it in the child.

Mini Challenge

Write a small program that makes all of these visible in one run. Use a Shape parent and a Circle child. Give them: (1) an instance method describe() overridden in Circle; (2) a field label redeclared in Circle; (3) a static method create() in both classes; and (4) two overloads of a helper static String inspect(Shape s) and static String inspect(Circle c). In main, create Shape s = new Circle(); and print s.describe(), s.label, s.create(), and inspect(s). Before running, write your predictions.

Check your work:

  • You predicted each line before running and can explain every mismatch.
  • You can say for each line whether compile-time or run-time information decided the result.
  • Bonus: change inspect into an overridden instance method and show how the output changes.

Summary & Next Lesson

Key takeaways

  • A call is resolved in two stages: the compiler checks the reference type and fixes the signature; the JVM then picks the version of an overridden method from the real class of the object (dynamic dispatch).
  • Lookup starts at the object's class and climbs the chain until a version is found.
  • Overloading is chosen at compile time from declared argument types. Overriding is chosen at run time from the receiver object.
  • Fields and static methods follow the reference type. They are hidden, not overridden, and @Override on a static method is a compile error.
  • JVMs often use method tables to speed up lookup, but that is an implementation detail.

Next lesson

You now have the full mental model. The next lesson, "Replacing conditionals with polymorphism", puts it to work: turning a chain of type checks into overriding methods, so that adding a type never touches the calling code.

Self-check

  1. At which stage does the compiler choose between overloaded methods, and using what information?
  2. What does the JVM do if the object's class does not override the method?
  3. A subclass declares the same field name as its parent. Through a parent reference, which field do you read?
  4. Is dynamic dispatch applied to static methods?
  5. show(Account) and show(SavingsAccount) exist. You call show(x) where x is declared Account but holds a SavingsAccount. Which runs?
أظهر الإجابات
  1. At compile time, using the declared types of the arguments.
  2. It continues the lookup in the parent class, and on up the chain, running the first version it finds.
  3. The parent's field: fields follow the reference type.
  4. No. Static methods are hidden, not overridden, and the call follows the declared class.
  5. show(Account), because the overload is chosen from the declared type.

References

  • Oracle, Java Language Specification SE 21, 15.12.4.4 Locate Method to Invoke. Supports the lookup procedure: start at the actual run-time class of the target and climb the superclass chain to the first overriding method, and that casts do not change which method runs.
  • Oracle, Java Language Specification SE 21, Chapter 15, section 15.5 and the table of contents for 15.12. Supports the split between compile-time selection of a method from the compile-time type and run-time selection of the implementation. I did not read the full text of section 15.12.2 on overload resolution; the overload behavior shown is confirmed by compiling and running the examples with JDK 21.
  • Oracle, Java Tutorials, Overriding and Hiding Methods. Supports the distinction between overriding instance methods and hiding static methods.
  • Oracle, Java SE 21 API, Override annotation. Supports the claim that @Override causes a compile error unless the method overrides or implements a supertype method.
  • Not verified from sources: the behavior of fields hiding (confirmed by running the example only), and the existence and name of virtual method tables in particular JVMs (hedged in the text).

التطبيق

طبّق هذا الدرس بمختبر الوحدة وتمارينها واختبارها.

المختبر والتمارين الاختبار

تعليم الدرس كمكتمل اختياري ويمكنك التراجع عنه في أي وقت. يبقى تقدمك محفوظًا في هذا المتصفح.

اكتب للبحث في المستويات والمراحل والوحدات والدروس.