استدعاء واحد وسلوكيات عديدة
في هذا الدرس 11 موضوعات
هذا الدرس متاح حاليًا باللغة الإنجليزية فقط، ولم تُنشر نسخته العربية بعد.
Lesson Overview
Picture a bank teller who presses the same "month-end" button on every account card in a pile. Each card type reacts in its own way: a savings card adds interest, a checking card charges a fee, a plain card does nothing. The teller never looks at the card type first. They just press the button.
Java can work the same way. This is polymorphism, from the Greek for "many forms": one method call, different behavior depending on the real object that receives it. In this lesson you will write exactly that: a pile of accounts, one loop, one call, and each account doing its own thing.
Why this matters: backend code is full of "do this to every item" work (charge every order, notify every user, validate every field), where the items are of different kinds. Polymorphism lets that code stay short and stay unchanged when a new kind appears.
This lesson builds directly on the previous module: classes, extends, and overriding a method with @Override. It also uses arrays and the enhanced for loop. We use arrays for collections of objects; the List type comes in a later module, and everything here works the same with it.
Learning Objectives
By the end of this lesson you will be able to:
- Define polymorphism and say what changes between two runs of the same line of code.
- Distinguish a variable's reference type from the object type it currently holds.
- Predict which version of an overridden method runs for a given reference and object.
- Store objects of different subclasses in one parent-typed array and call a method on each.
- Explain why the caller does not need to know which subclass it holds.
- State that this works for overridden instance methods, and explain what happens when a method is not overridden.
Written Explanation
Two types for every object variable
Consider this line:
Account a = new SavingsAccount("Ana", 1000.0, 0.02);Two types are involved:
- The reference type (also called the declared or static type) is what is written on the left:
Account. It never changes while the program runs, and the compiler uses it to decide which calls are allowed. - The object type (the real or run-time class) is what was created with
new:SavingsAccount. It is fixed when the object is created. A variable can later be pointed at a different object, with a different object type.
The assignment is legal because a SavingsAccount is an Account (the "is-a" relationship from inheritance). A variable of a parent type can refer to an object of any subclass. Converting a child reference to a parent type like this is called upcasting, and Java does it automatically. The next lesson looks at it closely.
The rule
When you call an overridden instance method through a reference, Java runs the version that belongs to the object, not the version that belongs to the reference type.
Account a = new SavingsAccount("Ana", 1000.0, 0.02);
a.monthEnd(); // runs SavingsAccount.monthEnd, not Account.monthEndThe Oracle Java Tutorial describes this as the JVM calling the appropriate method for the object that the variable refers to, rather than the one the variable's declared type suggests, which it names virtual method invocation. The mechanism has a name, dynamic dispatch, and a lesson of its own later in this module.
The decision is made at run time, each time the line executes. That is what makes the same line capable of different behavior:
for (Account a : accounts) {
a.monthEnd(); // one line of source code, three different behaviors
}Nothing about the line changes between iterations. Only the object behind a changes.
What you need for it to work
Three conditions, all of which you already know from inheritance:
- A parent type that declares the method (
AccounthasmonthEnd()). The compiler only allows calls the reference type declares. - Subclasses that override it with the same name and parameters (use
@Overrideso the compiler checks that for you). - A call through a parent reference to an instance method.
If a subclass does not override the method, the object simply uses the inherited version. If nothing overrides it, the parent's version is the only one, and all objects behave the same. That is why Account.monthEnd() can have an empty body: it is the default for account types with nothing special to do.
Why callers do not need to know
Look at the loop again. It only knows two things: "these are accounts" and "accounts have monthEnd()". It does not know, and does not need to know, that one of them is savings. The knowledge about how a savings account ends the month lives in SavingsAccount, next to the interest rate it needs. A consequence you will exploit in the lab at the end of this module: if a new BusinessAccount class appears with its own monthEnd(), the loop works with it immediately, without any edit.
A call inside inherited code is also dispatched
Dispatch applies to every call on an object, including calls made from inside the parent class. When an inherited method calls another method on this, the object's real class still decides which version runs. Example 3 below shows a parent method that calls an overridden method of its subclass.
Examples
Example 1: animals that speak (simple)
class Animal {
String speak() { return "..."; }
}
class Dog extends Animal {
@Override String speak() { return "Woof"; }
}
class Cat extends Animal {
@Override String speak() { return "Meow"; }
}
public class Ex1 {
public static void main(String[] args) {
Animal a = new Dog();
System.out.println(a.speak());
a = new Cat();
System.out.println(a.speak());
a = new Animal();
System.out.println(a.speak());
}
}Output:
Woof
Meow
...The variable a has reference type Animal the whole time. The same call a.speak() printed three different results because the object behind a changed. The third object is a plain Animal, so the parent's version ran.
Example 2: month-end over mixed accounts (practical)
class Account {
private final String owner;
private double balance;
Account(String owner, double balance) { this.owner = owner; this.balance = balance; }
String getOwner() { return owner; }
double getBalance() { return balance; }
void deposit(double amount) { balance += amount; }
void withdraw(double amount) { balance -= amount; }
void monthEnd() { } // default: nothing to do
}
class SavingsAccount extends Account {
private final double rate;
SavingsAccount(String owner, double balance, double rate) {
super(owner, balance);
this.rate = rate;
}
@Override void monthEnd() { deposit(getBalance() * rate); }
}
class CheckingAccount extends Account {
private final double fee;
CheckingAccount(String owner, double balance, double fee) {
super(owner, balance);
this.fee = fee;
}
@Override void monthEnd() { withdraw(fee); }
}
public class Ex2 {
public static void main(String[] args) {
Account[] accounts = {
new SavingsAccount("Ana", 1000.0, 0.02),
new CheckingAccount("Omar", 500.0, 5.0),
new Account("Lina", 200.0)
};
for (Account a : accounts) {
a.monthEnd();
System.out.println(a.getOwner() + ": " + a.getBalance());
}
}
}Output:
Ana: 1020.0
Omar: 495.0
Lina: 200.0One array of type Account[] holds three different kinds of object, and one call produced interest for Ana (1000 plus 2% is 1020), a fee for Omar (500 minus 5 is 495), and nothing for Lina. The loop contains no if, no type check, and no knowledge of the subclasses. (This example uses double for money to keep the focus on polymorphism, as earlier modules did. Real money code needs more care, which comes later in the course.)
Example 3: a parent method that calls an overridden one (realistic)
class Account {
private final String owner;
Account(String owner) { this.owner = owner; }
String label() { return "Account"; }
void printStatement() { System.out.println(label() + " of " + owner); }
}
class SavingsAccount extends Account {
SavingsAccount(String owner) { super(owner); }
@Override String label() { return "Savings account"; }
}
public class Ex3 {
public static void main(String[] args) {
Account a = new SavingsAccount("Ana");
a.printStatement();
new Account("Omar").printStatement();
}
}Output:
Savings account of Ana
Account of OmarprintStatement() is written only once, in Account, and SavingsAccount does not override it. Yet inside it, the call label() (shorthand for this.label()) printed "Savings account" for Ana. The object is a SavingsAccount, so its label() ran even though the call sits in parent code. The Java Language Specification describes this lookup as starting from the actual run-time class of the target object. Statements, receipts, and reports in real systems are often built this way: shared layout in the parent, details in the children.
Backend Insight
Backend code very often processes a collection of things of different kinds in one pass: a batch of payments (card, bank transfer, wallet), a queue of notifications (email, SMS, push), a list of validation rules. Each kind has its own way of doing the job, but the batch processor should not care.
When polymorphism is used well, the processor is a short loop that calls one method, and every kind-specific rule sits inside its own class. New requirements ("we now support wallet payments") become new classes rather than edits to code that already works and has already been tested. That is why teams value it, and why two later topics build on exactly this idea: the Strategy pattern (swap behavior by swapping the object) and dependency inversion (depend on the parent type, not the concrete class). You will meet both in the next level.
Common Mistakes
Mistake 1: Thinking the reference type picks the method
- Incorrect: "
ais declared asAccount, soa.monthEnd()runsAccount.monthEnd()." - Correct: "
ais declared asAccount, so the call is allowed becauseAccounthasmonthEnd(). The version that runs belongs to the real object."
The reference type decides what you may call. The object type decides which version runs.
Mistake 2: Expecting polymorphism without an override
If the subclass method has a different name or different parameters, it does not override anything; it is just another method. Call through the parent reference and the parent's version runs. Using @Override turns this silent problem into a compile error, which is why you always write it.
Mistake 3: Checking the type before every call
if (a instanceof SavingsAccount) { ... } else if (a instanceof CheckingAccount) { ... }This undoes the benefit. If you find yourself writing it to call "the same" action on different kinds, move each branch into an overriding method instead. Lessons 2 and 4 return to this.
Exercises
Try each exercise yourself before opening the solutions. Predict first, then run the code if you want to check.
Level A: Foundation
A1. Given these classes, what does the loop print?
class Vehicle { String sound() { return "generic"; } }
class Car extends Vehicle { @Override String sound() { return "vroom"; } }
class Bike extends Vehicle { @Override String sound() { return "ring"; } }
class Truck extends Car { }
// ...
Vehicle[] vs = { new Car(), new Bike(), new Vehicle(), new Truck() };
for (Vehicle v : vs) System.out.println(v.sound());أظهر الحل A1
A1. It prints vroom, ring, generic, vroom. Truck does not override sound(), so it inherits Car's override, which is the nearest version above it in the class chain.
A2. For the line Vehicle v = new Bike();, name the reference type and the object type. Then say what changes if the next line is v = new Car();.
أظهر الحل A2
A2. Reference type: Vehicle. Object type: Bike. After v = new Car(); the reference type is still Vehicle, but the object type is now Car, so v.sound() returns vroom instead of ring.
A3. In one sentence, explain why an array declared Vehicle[] can hold a Bike.
أظهر الحل A3
A3. A Bike is a Vehicle (it extends it), so a Vehicle-typed slot can refer to it.
Level B: Application
B1. Bird has String move() { return "fly"; }. Write Penguin extends Bird so that a loop over Bird[] birds = { new Bird(), new Penguin() } prints fly and then waddle, without any if.
أظهر الحل B1
B1.
class Penguin extends Bird {
@Override String move() { return "waddle"; }
}The loop for (Bird b : birds) System.out.println(b.move()); prints fly then waddle. I compiled and ran this.
B2. What does this print, and why does the parent's method show the child's fee?
class Parcel {
String describe() { return "parcel, " + fee(); }
String fee() { return "fee 5"; }
}
class Express extends Parcel { @Override String fee() { return "fee 12"; } }
// ...
Parcel p = new Express();
System.out.println(p.describe());أظهر الحل B2
B2. It prints parcel, fee 12. describe() is inherited, but the call to fee() inside it is dispatched on the real object, which is an Express, so Express.fee() runs.
B3. A student writes the following and is surprised that the output is base. Explain the problem and the fix.
class Account { String monthEnd() { return "base"; } }
class Savings extends Account { String monthEnd(int days) { return "savings " + days; } }
// ...
Account a = new Savings();
System.out.println(a.monthEnd());أظهر الحل B3
B3. monthEnd(int days) has different parameters, so it does not override monthEnd(); it is a separate overload. The call a.monthEnd() finds only the parent's no-argument method, and it prints base. Fix: give the subclass the same signature, @Override String monthEnd(). With @Override on the original line, the compiler would have reported the mistake immediately.
Level C: Challenge
C1. A shop has a Ticket class whose int price() returns 10. StudentTicket costs 5 and VipTicket costs 25. Write the three classes and a loop that totals the price of the cart { Ticket, StudentTicket, VipTicket, StudentTicket } with no type checks.
أظهر الحل C1
C1.
class Ticket { int price() { return 10; } }
class StudentTicket extends Ticket { @Override int price() { return 5; } }
class VipTicket extends Ticket { @Override int price() { return 25; } }
// in main:
Ticket[] cart = { new Ticket(), new StudentTicket(), new VipTicket(), new StudentTicket() };
int total = 0;
for (Ticket t : cart) total += t.price();
System.out.println(total); // 45I compiled and ran it: 10 + 5 + 25 + 5 = 45.
C2. Your loop over Ticket[] works. Now a colleague adds a ChildTicket class. In your own words, what must ChildTicket do for the existing loop to price it correctly, and what in the loop must change?
أظهر الحل C2
C2. ChildTicket must extend Ticket and override price() with the same signature (and @Override). The loop does not change at all: it already calls price() on a Ticket reference, and dispatch will pick the new class's version.
Mini Challenge
Model a small "notification" system. Create a parent class Notification with a method String message() that returns "generic". Create at least three subclasses (for example EmailNotification, SmsNotification, PushNotification), each overriding message() to return a different text. In main, build a Notification[] with at least five objects including one plain Notification, loop over it once, and print each message().
Check your work:
- The loop contains one call and no
if,switch, orinstanceof. - Every override has
@Override. - You can say, for each printed line, which class's method produced it.
- Bonus: add a fourth subclass and confirm that you did not change the loop.
Summary & Next Lesson
Key takeaways
- Polymorphism means one method call can run different code depending on the real object.
- Every object variable has a reference type (fixed, written in the code) and an object type (the class created with
new). - The reference type decides which calls are allowed; the object type decides which overridden version runs.
- Callers can work with the parent type only, which keeps loops short and unchanged when new subclasses appear.
- It works for overridden instance methods. A method with a different signature is an overload, not an override.
Next lesson
You can now point a parent reference at a child object. The next lesson, "Upcasting, downcasting, and instanceof", looks at moving between parent and child types: what the parent reference lets you do, how to get back to the child type, and why doing that often is a warning sign.
Self-check
- In
Account a = new SavingsAccount(...), what are the reference type and the object type? - Which
monthEndruns fora.monthEnd(): the reference type's or the object's? - Can polymorphism work if the subclass never overrides the method? What runs?
- Why can the month-end loop avoid any
iforinstanceof? - A subclass declares
monthEnd(int days). Does a call toa.monthEnd()through a parent reference reach it?
أظهر الإجابات
- Reference type
Account, object typeSavingsAccount. - The object's.
- There is no different behavior then: the inherited parent version runs for every object. Polymorphism needs an override to show different behavior.
- Each subclass carries its own version of
monthEnd(), and the call is dispatched on the real object at run time, so the loop needs no type knowledge. - No. A method with different parameters is an overload, not an override, and the call finds only the parent's no-argument method.
References
- Oracle, Java Tutorials, Polymorphism. Supports the rule that the JVM calls the method of the actual object, not of the variable's declared type (virtual method invocation).
- Oracle, Java Language Specification SE 21, 15.12.4.4 Locate Method to Invoke. Supports the statement that dynamic method lookup starts from the run-time class of the target object, including for calls made inside inherited code.
- Oracle, Java SE 21 API, Override annotation. Supports the claim that
@Overridemakes the compiler report an error when a method does not override a supertype method.
التطبيق
طبّق هذا الدرس بمختبر الوحدة وتمارينها واختبارها.
تعليم الدرس كمكتمل اختياري ويمكنك التراجع عنه في أي وقت. يبقى تقدمك محفوظًا في هذا المتصفح.