At the end of the previous lesson something happened that we left unexplained. The method printCard(Material m) received a Book, a Magazine or a Dvd indistinctly, and each one responded according to what it really was: the book charged €0.25 per day, the DVD €0.50, the magazine €0.10. And all of that with a single line of calculation code, written only once in Material. That mechanism is called polymorphism —from the Greek for "many forms"— and it is the pillar that justifies the existence of inheritance. Without it, a class hierarchy would be little more than a way of saving lines; with it, it becomes the tool that lets you add new types without modifying existing code. This lesson explains how it works inside, how to use it well, and why ifs and switches on an object's type —the ones that filled your main in module 2— are a symptom of missing polymorphism.
Contents
- Two different polymorphisms
- Declared type versus actual type
- Dynamic dispatch step by step
- The central example:
Materialcalculating fines - Upcasting: the implicit conversion
- Downcasting and
ClassCastException instanceofand the Java 16 patternifs on the type are a design smellstaticmembers are not polymorphic: hiding- Fields and polymorphism: not those either
- Polymorphism with arrays of the superclass
- Common Mistakes and Tips
- Exercises
- Two different polymorphisms
Two forms of polymorphism coexist in Java and it is worth not confusing them:
| Aspect | Compile-time polymorphism | Run-time polymorphism |
|---|---|---|
| Other name | Static, ad hoc | Dynamic, subtype |
| Mechanism | Method overloading | Method overriding |
| Who decides | The compiler | The JVM, at run time |
| What information it decides with | The declared types of the arguments | The actual type of the object |
| Needs inheritance | No | Yes |
| Example | calculateFine(int) and calculateFine(int, double) |
Dvd.getDailyRate() over Material.getDailyRate() |
You already master the first one: you studied it in 03-03 and there is no mystery to it, the compiler looks at the written types and picks. The second is the interesting one and the one that gives the OOP pillar its name; when somebody says "polymorphism" on its own, they almost always mean this one.
- Declared type versus actual type
Every reference in Java has two types that may not coincide:
| Concept | What it is | Who uses it | When it is known |
|---|---|---|---|
| Declared type (static) | The type written to the left of the variable | The compiler, to decide which calls are legal | At compile time |
| Actual type (dynamic) | The class of the object created with new |
The JVM, to decide which code to run | At run time |
And from that come the two rules that govern everything else:
The declared type decides WHAT YOU CAN CALL. The actual type decides WHAT RUNS.
Check it:
Material m = new Dvd("Spring Course", "DVD-0007", 240);
System.out.println(m.getType()); // "DVD" <-- runs Dvd's version
System.out.println(m.getLoanDays()); // 3 <-- runs Dvd's version
System.out.println(m.durationMinutes); // COMPILE ERRORThe object is a DVD and it has its durationMinutes field, but the compiler only sees a Material and Material does not declare that field. The restriction is not arbitrary: the compiler must guarantee that the call is valid for any object that could be there, and that variable could hold a Book.
- Dynamic dispatch step by step
The mechanism by which the JVM picks the right implementation is called dynamic dispatch (or late binding). It works like this:
flowchart TD
A["The compiler sees
m.getDailyRate()
with m of type Material"] --> B{"Does Material declare
that method?"}
B -- "no" --> E["Compile error"]
B -- "yes" --> C["Compiles the call
and leaves it unresolved"]
C --> D["At RUN TIME the JVM
looks at the object's actual type"]
D --> F["Does Dvd override
getDailyRate?"]
F -- "yes" --> G["Runs Dvd.getDailyRate
returns 0.50"]
F -- "no" --> H["Climbs the hierarchy
until it finds the inherited version"]
In two sentences: the compiler verifies that the method exists in the declared type, and the JVM searches for the implementation starting at the object's actual class and climbing the hierarchy until it finds it. That is why Book, which does not override getDailyRate(), ends up running Material's version.
This mechanism has a minimal performance cost (the JVM uses a method table and also optimises aggressively when hot), and in exchange it gives enormous flexibility. In module 10-07 you will see how the JIT compiler even removes that cost when it detects that in practice the same implementation always runs.
- The central example:
Material calculating fines
Material calculating finesThis is the example that sums up the whole module. Look at calculateFine, written only once in Material:
public double calculateFine(int elapsedDays) {
int late = Math.max(0, elapsedDays - getLoanDays());
return Math.min(late * getDailyRate(), MAX_FINE);
}That method does not know —nor does it care— whether the object is a book, a magazine or a DVD. It calls getLoanDays() and getDailyRate(), and dynamic dispatch takes care of each material contributing its own numbers.
Material m1 = new Book("Effective Java", "Joshua Bloch", "978-0000000001", 2018);
Material m2 = new Magazine("Java Magazine", "REV-2024-42", 42, "Bimonthly");
Material m3 = new Dvd("Spring Course", "DVD-0007", 240);
System.out.printf("%-12s %-22s %8s %10s %10s %s%n",
"TYPE", "TITLE", "TERM", "RATE", "FINE 12d", "SEVERITY");
show(m1);
show(m2);
show(m3);/** A single method serves every format, present and future. */
private static void show(Material m) {
System.out.printf("%-12s %-22s %6d d %9.2f %9.2f %s%n",
m.getType(), m.title, m.getLoanDays(), m.getDailyRate(),
m.calculateFine(12), m.classifySeverity(12));
}Output:
TYPE TITLE TERM RATE FINE 12d SEVERITY Book Effective Java 15 d 0.25 0.00 ON TIME Magazine Java Magazine 7 d 0.10 0.50 MINOR DVD Spring Course 3 d 0.50 4.50 SEVERE
Verify it by hand: with 12 elapsed days, the book is still within term (15 days); the magazine is 5 days late (12 − 7) and pays 5 × 0.10 = €0.50, a minor delay because 5 ≤ 7; the DVD is 9 days late (12 − 3) and pays 9 × 0.50 = €4.50, severe because 9 > 7.
Three lines of business code and three different behaviours. And the most valuable property: if tomorrow Nexus Software adds technical maps or robotics kits, show will keep working untouched.
- Upcasting: the implicit conversion
Assigning a subclass object to a superclass reference is called upcasting. It is implicit and always safe, because a Dvd is always a Material:
Dvd dvd = new Dvd("Spring Course", "DVD-0007", 240);
Material m = dvd; // implicit upcasting, no special syntax
// it also happens when passing parameters...
show(dvd); // the parameter is a Material
// ...and when declaring directly
Material other = new Dvd("Docker Course", "DVD-0008", 180);What changes and what does not with upcasting:
| Changes | Does not change |
|---|---|
What the compiler lets you call (only Material's members) |
The object: it is still exactly the same Dvd |
| — | Which implementation runs: Dvd's |
| — | Its own fields: still there, just unreachable through that reference |
It is important to internalise that upcasting transforms nothing: it does not create a new object nor trim the original. It only changes the glasses the compiler looks at it with.
- Downcasting and
ClassCastException
ClassCastExceptionDowncasting is the inverse operation: treating a superclass reference as if it were the subclass. It requires explicit syntax, because it is not always safe:
Material m = new Dvd("Spring Course", "DVD-0007", 240);
Dvd dvd = (Dvd) m; // explicit downcasting
System.out.println(dvd.durationMinutes); // 240: now it can be accessedIf the actual object is not of that type, the program fails at run time:
Material m = new Book("Effective Java", "Joshua Bloch", "978-0000000001", 2018);
Dvd dvd = (Dvd) m; // compiles... and blows up when runException in thread "main" java.lang.ClassCastException:
class com.nexussoftware.bibliotech.domain.Book cannot be cast to
class com.nexussoftware.bibliotech.domain.Dvd
at com.nexussoftware.bibliotech.BiblioTechApp.main(BiblioTechApp.java:42)The compiler accepts the conversion because it could be valid (a Material could be a Dvd), but the real check happens at run time. ClassCastException is studied as an exception in module 6; here it is enough to know that it exists and how to avoid it: by checking the type beforehand with instanceof.
instanceof and the Java 16 pattern
instanceof and the Java 16 patternThe instanceof operator answers the question "is this object of this type?":
Material m = getMaterial();
if (m instanceof Dvd) {
Dvd dvd = (Dvd) m; // downcasting now safe
System.out.println("Duration: " + dvd.durationMinutes + " min");
}That pattern —check, cast, declare a variable— was so repetitive that Java 16 built it into the operator itself. It is instanceof with a type pattern:
if (m instanceof Dvd dvd) { // checks, converts and declares
System.out.println("Duration: " + dvd.durationMinutes + " min");
}The variable dvd only exists where the compiler can guarantee that the check was true, even in compound conditions:
// Works: after && the compiler already knows dvd is valid
if (m instanceof Dvd dvd && dvd.durationMinutes > 120) {
System.out.println("Long DVD: " + dvd.title);
}
// It also works with negation and early exit
if (!(m instanceof Book book)) {
return;
}
System.out.println(book.author); // here book is guaranteedTwo useful details:
null instanceof Anythingreturnsfalse, it never throws an error. That makesinstanceofan implicit null guard.instanceofis also true for superclasses:dvd instanceof Materialistrue.
ifs on the type are a design smell
ifs on the type are a design smellNow the important part of the lesson. Compare these two ways of solving the same problem.
Without polymorphism, as you would have had to write it with what you knew in module 2:
// BEFORE: each format's logic, scattered in a switch on the type
public static double calculateFine(String materialType, int elapsedDays) {
int term;
double rate;
switch (materialType) {
case "BOOK" -> { term = 15; rate = 0.25; }
case "MAGAZINE" -> { term = 7; rate = 0.10; }
case "DVD" -> { term = 3; rate = 0.50; }
default -> { term = 15; rate = 0.25; }
}
int late = Math.max(0, elapsedDays - term);
return Math.min(late * rate, 20.0);
}With polymorphism:
// AFTER: each format knows its rules; the calculation is unique
public double calculateFine(int elapsedDays) {
int late = Math.max(0, elapsedDays - getLoanDays());
return Math.min(late * getDailyRate(), MAX_FINE);
}The comparison, point by point:
| Criterion | switch on the type |
Polymorphism |
|---|---|---|
| Adding an audiobook | Modify the switch... |
Write a new class |
| ...and all the others | And the listing one, the validating one, the printing one... | Nothing else |
| Risk of forgetting a place | High: switches scatter through the code |
None: the compiler gathers everything in the class |
| Where the DVD rule lives | Spread over N methods | In Dvd.java, and only there |
| What happens with an unknown type | It falls into default, silently |
There is no unknown case |
The professional criterion, worth memorising:
If you write an
ifor aswitchthat asks what type an object is in order to decide what to do, a polymorphic method is almost always missing.
This does not mean instanceof is forbidden. There are legitimate uses:
- Implementing
equals(lesson 03-09). - Working with third-party APIs whose hierarchy you cannot modify.
- Retrieving a piece of data specific to a subtype in order to display it, such as a DVD's
durationMinutesin a detailed card, when that data has no equivalent in the others.
The illegitimate case is the one that replaces behaviour that should live in the classes:
// BAD: business behaviour decided from outside
double rate;
if (m instanceof Book) rate = 0.25;
else if (m instanceof Magazine) rate = 0.10;
else if (m instanceof Dvd) rate = 0.50;
else rate = 0.25;
// GOOD
double rate = m.getDailyRate();
static members are not polymorphic: hiding
static members are not polymorphic: hidingHere comes a trap you have to know about so as not to fall into it. static methods are not overridden: they are hidden, and they are resolved with the declared type, not the actual type.
public class Material {
public static String typeDescription() {
return "Generic material";
}
}
public class Dvd extends Material {
public static String typeDescription() { // HIDES, does not override
return "Training DVD";
}
}Material m = new Dvd("Spring Course", "DVD-0007", 240);
System.out.println(m.typeDescription()); // "Generic material" (!)
System.out.println(Material.typeDescription()); // "Generic material"
System.out.println(Dvd.typeDescription()); // "Training DVD"The first line is the surprising one. The object is a Dvd, but since typeDescription() is static, the JVM does no dynamic dispatch: it uses m's declared type, which is Material. It is exactly the opposite of what instance methods do.
A direct contrast, in a table:
| Instance method | static method |
|
|---|---|---|
| Redefining it in the subclass is called | Overriding | Hiding |
| It is resolved with | The object's actual type | The reference's declared type |
Accepts @Override |
Yes | No (compile error) |
| Is it polymorphic | Yes | No |
Acid test: if you add @Override to Dvd's static method, the compiler fails with "method does not override or implement a method from a supertype". One more reason to always write the annotation.
Practical tip: never call a static method through a reference (m.typeDescription()). Always write Material.typeDescription() or Dvd.typeDescription(), and the ambiguity disappears from the code.
- Fields and polymorphism: not those either
The same rule applies to fields: fields are not polymorphic, they are resolved with the declared type.
public class Material {
public String label = "MATERIAL";
}
public class Dvd extends Material {
public String label = "DVD"; // HIDES Material's field
}Dvd dvd = new Dvd("Spring Course", "DVD-0007", 240);
Material mat = dvd; // the SAME memory box
System.out.println(dvd.label); // "DVD"
System.out.println(mat.label); // "MATERIAL" <-- same objectThe object contains both fields at once, and which one you see depends on the glasses you look at it with. It is such an absurd source of errors that the solution is simple: do not declare in a subclass a field with the same name as one in the superclass. And if what you want is a value that varies by type, use an overridable method, like getType() in BiblioTech.
- Polymorphism with arrays of the superclass
Polymorphism shines when you handle many objects uniformly. For that you need to be able to store them together, and for the moment the only tool available is an array:
Provisional note. Arrays have a fixed size and are awkward for a real catalog: you cannot add or remove without creating another array. In module 5
ArrayList,HashMapand the rest of the collections framework will appear, which is the definitive solution. Use the array here as scaffolding to see polymorphism in action.
Material[] catalog = {
new Book("Effective Java", "Joshua Bloch", "978-0000000001", 2018),
new Book("Design Patterns", "Erich Gamma", "978-0000000002", 1994),
new Book("Refactoring", "Martin Fowler", "978-0000000003", 2018),
new Magazine("Java Magazine", "REV-2024-42", 42, "Bimonthly"),
new Dvd("Spring Course", "DVD-0007", 240)
};
System.out.printf("%-12s %-22s %8s %10s %s%n",
"TYPE", "TITLE", "TERM", "FINE 12d", "SEVERITY");
double expectedRevenue = 0.0;
for (Material m : catalog) { // one single line of code...
System.out.printf("%-12s %-22s %6d d %9.2f %s%n",
m.getType(), m.title, m.getLoanDays(),
m.calculateFine(12), m.classifySeverity(12));
expectedRevenue += m.calculateFine(12); // ...and each object applies its own
}
System.out.printf("%nExpected revenue at 12 days: %.2f EUR%n", expectedRevenue);Output:
TYPE TITLE TERM FINE 12d SEVERITY Book Effective Java 15 d 0.00 ON TIME Book Design Patterns 15 d 0.00 ON TIME Book Refactoring 15 d 0.00 ON TIME Magazine Java Magazine 7 d 0.50 MINOR DVD Spring Course 3 d 4.50 SEVERE Expected revenue at 12 days: 5.00 EUR
An array of Material can hold objects of any subclass, and the loop treats them all the same while each one does its own thing. This is, in essence, the reason OOP exists in large software.
One detail to bear in mind: when walking the array, m's declared type is Material, so you can only call what Material declares. If you need the books' author, you will need instanceof with a pattern:
for (Material m : catalog) {
if (m instanceof Book book) {
System.out.println(book.title + " is by " + book.author);
}
}And this is exactly one of the legitimate uses from section 8: extracting a piece of data that only exists in a subtype, not deciding business behaviour.
Common Mistakes and Tips
- Believing that upcasting "converts" the object. It does not touch it. It only limits what the compiler lets you call. The object is still what it was, and its overridden methods keep running.
- Downcasting without checking.
(Dvd) mwith noinstanceofin front is aClassCastExceptionwaiting to happen. Always useif (m instanceof Dvd dvd). - Putting
@Overrideon astaticmethod. It does not compile, and that is its virtue: it warns you that you are not overriding, but hiding. - Calling
staticmethods through an instance.m.typeDescription()misleads about what is going to run. Use the class name. - Redefining a field in the subclass. You end up with two fields of the same name in the same object and results that depend on the declared type. Do not do it.
- Chains of
else if (x instanceof ...). They almost always mean a method is missing from the hierarchy. Before writing the third branch, ask yourself what methodMaterialshould have. - Tip: when you add a subclass, try listing which methods it overrides. If it overrides none, perhaps you did not need a subclass but a field.
- Tip: program against the most general type that serves you. Declare
Material minstead ofDvd dwhen you do not need what is specific to the DVD: your code will work in more cases. This principle is taken to the extreme in module 4 with interfaces. - Tip: use your IDE's gutter marker. IntelliJ and Eclipse show an icon next to overridden methods and let you jump to the actual implementation with a click.
Exercises
Exercise 1: add a type without touching existing code
This exercise checks polymorphism's most valuable property. Nexus Software is adding robotics kits to the catalog: they are lent for 5 days, with a rate of €1.00/day (they are expensive), and they have a partCount field.
- Create
RoboticsKitas a subclass ofMaterial. - Add it to the
catalogarray from section 11. - Run it without modifying a single line of
Material,Book,Magazine,Dvd,showor the listing loop. - Manually verify the fine at 12 elapsed days and the severity.
Then answer: how many files did you have to touch? How many would you have touched with the switch from section 8?
Exercise 2: refactor a switch on the type
A colleague has written this method in BiblioTechApp. Refactor it by removing the switch completely, moving each responsibility to the class it belongs to. State what new methods Material needs and which ones each subclass overrides.
public static String buildNotice(Material m, int elapsedDays) {
String channel;
int noticeDays;
switch (m.getType()) {
case "Book" -> { channel = "email"; noticeDays = 3; }
case "Magazine" -> { channel = "chat"; noticeDays = 1; }
case "DVD" -> { channel = "phone"; noticeDays = 1; }
default -> { channel = "email"; noticeDays = 3; }
}
return "Notice by " + channel + " with " + noticeDays
+ " days' warning. Current fine: "
+ m.calculateFine(elapsedDays) + " EUR";
}Exercise 3: demonstrate hiding versus overriding
Write a test class that demonstrates, in the same run, the three rules from sections 9 and 10:
- An overridden instance method runs the actual type's version.
- A redefined
staticmethod runs the declared type's version. - A redefined field is read according to the declared type, even though the object is the same.
In each case print the declared type, the actual type (with getClass().getSimpleName()) and the value obtained, and explain in writing why result 1 differs from results 2 and 3.
Solutions
Solution 1
package com.nexussoftware.bibliotech.domain;
/** Robotics kit for hands-on training. Expensive and scarce material. */
public class RoboticsKit extends Material {
public static final int KIT_LOAN_DAYS = 5;
public static final double KIT_DAILY_RATE = 1.00;
public final int partCount;
public RoboticsKit(String title, String reference, int partCount) {
super(title, reference, true);
this.partCount = Math.max(0, partCount);
}
@Override
public int getLoanDays() { return KIT_LOAN_DAYS; }
@Override
public double getDailyRate() { return KIT_DAILY_RATE; }
@Override
public String getType() { return "Kit"; }
@Override
public String describe() {
return super.describe() + " - " + partCount + " parts";
}
}You only have to add it to the array:
Material[] catalog = {
new Book("Effective Java", "Joshua Bloch", "978-0000000001", 2018),
new Book("Design Patterns", "Erich Gamma", "978-0000000002", 1994),
new Book("Refactoring", "Martin Fowler", "978-0000000003", 2018),
new Magazine("Java Magazine", "REV-2024-42", 42, "Bimonthly"),
new Dvd("Spring Course", "DVD-0007", 240),
new RoboticsKit("Advanced Arduino Kit", "KIT-0001", 128) // the only new line
};Output (new row marked):
TYPE TITLE TERM FINE 12d SEVERITY Book Effective Java 15 d 0.00 ON TIME Book Design Patterns 15 d 0.00 ON TIME Book Refactoring 15 d 0.00 ON TIME Magazine Java Magazine 7 d 0.50 MINOR DVD Spring Course 3 d 4.50 SEVERE Kit Advanced Arduino Kit 5 d 7.00 MINOR Expected revenue at 12 days: 12.00 EUR
Manual verification, paying attention to the boundary: 12 − 5 = 7 days late; 7 × 1.00 = €7.00, below the €20 cap. And the severity is MINOR, not SEVERE, because the condition is late <= MINOR_THRESHOLD and MINOR_THRESHOLD is exactly 7. It is precisely the kind of boundary value lesson 02-05 taught you to check: thresholds are always tested with the three boundary values (6, 7 and 8), because a < instead of a <= would change this result without any other row of the table noticing.
Answer to the question. You have touched two files: a new one (RoboticsKit.java) and one line in BiblioTechApp to register it. With the switch from section 8 you would have had to locate and modify every method containing a switch on the type —fine calculation, listing, notices, validation—, with the risk of forgetting one and having the kit treated as a book without anyone noticing.
Solution 2
The switch decides two things —the notice channel and the warning days— which are properties of each format. Their home is the classes.
In Material, two new methods with the default value, and a third that composes the message only once:
/** @return preferred channel for warning about this material's due date. */
public String getNoticeChannel() {
return "email";
}
/** @return days of advance warning given before the due date. */
public int getNoticeDays() {
return 3;
}
/** @return text of the due-date notice for this material. */
public String buildNotice(int elapsedDays) {
return String.format("Notice by %s with %d days' warning. Current fine: %.2f EUR",
getNoticeChannel(), getNoticeDays(),
calculateFine(elapsedDays));
}In Magazine:
@Override
public String getNoticeChannel() { return "chat"; }
@Override
public int getNoticeDays() { return 1; }In Dvd:
@Override
public String getNoticeChannel() { return "phone"; }
@Override
public int getNoticeDays() { return 1; }Book overrides nothing: it inherits "email" and 3 days, which were precisely the original switch's default case. And BiblioTechApp loses the method:
Results:
| Material | Before (switch) | After (polymorphism) |
|---|---|---|
Book |
email, 3 days | email, 3 days (inherited) |
Magazine |
chat, 1 day | chat, 1 day (overridden) |
Dvd |
phone, 1 day | phone, 1 day (overridden) |
RoboticsKit |
fell into default without anyone deciding it |
inherits the default explicitly, or overrides it |
Three concrete gains: the silent default disappears, each format gathers all its rules in its own file, and adding a new type no longer forces you to comb the code looking for forgotten switches.
Solution 3
package com.nexussoftware.bibliotech;
class Base {
public String field = "field of Base";
public String instanceMethod() { return "instance of Base"; }
public static String staticMethod() { return "static of Base"; }
}
class Derived extends Base {
public String field = "field of Derived"; // HIDES Base's
@Override
public String instanceMethod() { return "instance of Derived"; }
public static String staticMethod() { return "static of Derived"; } // HIDES
}
public class PolymorphismDemo {
public static void main(String[] args) {
Derived d = new Derived();
Base b = d; // upcasting: SAME object, other glasses
System.out.println("Declared type of b: Base");
System.out.println("Actual type of b: " + b.getClass().getSimpleName());
System.out.println("b == d: " + (b == d));
System.out.println();
// 1. Instance method: the ACTUAL TYPE wins
System.out.println("1) b.instanceMethod() -> " + b.instanceMethod());
System.out.println(" d.instanceMethod() -> " + d.instanceMethod());
// 2. Static method: the DECLARED TYPE wins
System.out.println("2) b.staticMethod() -> " + b.staticMethod());
System.out.println(" d.staticMethod() -> " + d.staticMethod());
// 3. Field: the DECLARED TYPE wins
System.out.println("3) b.field -> " + b.field);
System.out.println(" d.field -> " + d.field);
}
}Output:
Declared type of b: Base Actual type of b: Derived b == d: true 1) b.instanceMethod() -> instance of Derived d.instanceMethod() -> instance of Derived 2) b.staticMethod() -> static of Base d.staticMethod() -> static of Derived 3) b.field -> field of Base d.field -> field of Derived
Explanation. The line b == d gives true: there is a single object and two references, so any difference between the three outputs comes from the declared type, not from the object.
Case 1 uses dynamic dispatch: instance methods are resolved at run time by looking at the actual class, which is Derived in both cases. It is the only one of the three that is polymorphic.
Cases 2 and 3 are resolved at compile time. The compiler replaces b.staticMethod() with Base.staticMethod() and b.field with the field declared in Base, because that is the type the variable was declared with. The JVM never even gets to ask what object is behind it. That is why the same object exhibits two different values for field: the object contains both fields, and each reference reads its own.
The practical moral of the three cases together: polymorphism is exclusively a matter of instance methods. Any other mechanism —static, fields— is resolved by the written type and produces surprises if you try to use it as if it were polymorphic.
Conclusion
You have reached the core of object orientation. You tell the two polymorphisms apart —overloading, resolved by the compiler, and overriding, resolved by the JVM— and you understand the rule that governs them: the declared type decides what you can call, the actual type decides what runs. You have followed dynamic dispatch step by step and seen its practical consequence in the module's central example: a single calculateFine in Material that charges €0.25 to a book, €0.10 to a magazine and €0.50 to a DVD, without a single if on the type. You master implicit and safe upcasting, explicit and risky downcasting with its ClassCastException, and the modern way of protecting yourself with instanceof plus a type pattern (if (m instanceof Dvd dvd)), which checks, converts and declares in one line. You know how to recognise if/switch on the type as a design smell and to refactor them by moving each rule to the class it belongs to. And you know the two exceptions you must always keep in mind: static methods are not overridden, they are hidden, and fields are not polymorphic either; both are resolved by the declared type.
Above all, you have verified in practice the property that justifies the whole scaffolding: adding a robotics kit to the catalog cost one new class and one line, without touching anything that already worked.
One serious weakness remains in the current design, and you will have noticed it: every field is still public. Anyone can write book.available = true skipping lend()'s warnings, and nothing stops assigning a made-up fine to a loan. In the next lesson, Encapsulation, you will close that door: you will make the fields of Book, Employee and Loan private, you will learn the complete table of access modifiers, you will see why putting a setter on every field destroys precisely what you were trying to protect, and you will discover immutability and defensive copies as tools for making your objects' state genuinely their own.
Java Programming Course
Module 1: Introduction to Java
- Introduction to Java
- Setting Up the Development Environment
- Basic Syntax and Structure
- Variables and Data Types
- Operators
- Console Input and Output
- Your First Complete Program: BiblioTech
Module 2: Control Flow
- Conditional Statements
- Loops
- Switch Statements
- Break and Continue
- Debugging and Execution Traces
- Project: The BiblioTech Interactive Menu
Module 3: Object-Oriented Programming
- Introduction to OOP
- Classes and Objects
- Methods
- Constructors
- Inheritance
- Polymorphism
- Encapsulation
- Abstraction
- The Object Class: equals, hashCode and toString
Module 4: Advanced Object-Oriented Programming
- Interfaces
- Abstract Classes
- Inner Classes
- Anonymous Classes
- Lambda Expressions
- Functional Interfaces and Method References
- Enums and Records
Module 5: Data Structures and Collections
- Arrays
- The Collections Framework
- ArrayList
- LinkedList
- HashMap
- HashSet
- Queue and Deque
- Stack
- Sorting and Searching Collections
Module 6: Exception Handling
- Introduction to Exceptions
- The Try-Catch Block
- Throw and Throws
- Custom Exceptions
- The Finally Block
- Try-with-resources and AutoCloseable
- Error Handling Strategies and Logging
Module 7: File Input/Output
- Reading Files
- Writing Files
- File Streams
- BufferedReader and BufferedWriter
- Serialization
- The NIO.2 API: Path and Files
- Interchange Formats: CSV and Properties
Module 8: Multithreading and Concurrency
- Introduction to Multithreading
- Creating Threads
- Thread Lifecycle
- Synchronization
- Concurrency Utilities
- Concurrent Collections and Atomic Variables
- Asynchronous Tasks with CompletableFuture
Module 9: Networking
- Introduction to Networking
- Sockets
- ServerSocket
- DatagramSocket and DatagramPacket
- URL and HttpURLConnection
- The Modern HTTP Client
Module 10: Advanced Topics
- Generics
- Annotations
- Reflection
- Java 8 Features: Streams and Optional
- Dates and Times with java.time
- Java 9 and Beyond
- Memory, Garbage Collection and Performance
Module 11: Java Frameworks and Libraries
- Introduction to Java Frameworks
- Spring Framework
- Hibernate
- JUnit
- Maven
- Advanced Testing with Mockito
- Essential Ecosystem Libraries
