Topic Note Part 6: Polymorphism¶
Course: Java Programming Masterclass - Tim Buchalka (Udemy)
Section: 08. Advanced OOP Techniques
Status: Complete
Learning Objectives¶
- Understand what polymorphism means and how it works in Java
- Master the difference between compile-time and runtime types
- Learn to write polymorphic code using inheritance
- Implement factory methods for object creation
- Understand type casting and its implications
- Use
instanceofoperator and pattern matching (JDK 16+) - Apply Local Variable Type Inference (
varkeyword)
What is Polymorphism?¶
Definition¶
Polymorphism literally means "many forms." In Java, it allows us to:
- Write code that calls a method
- Have different behavior execute for different objects at runtime
- Use a single reference type to work with multiple object types
The Core Concept¶
Movie movie = new Adventure("Star Wars"); // Declared: Movie, Actual: Adventure
movie.watchMovie(); // Calls Adventure's watchMovie(), not Movie's!
The declared type (compıle-time) is Movie, but the actual type (runtime) is Adventure. Java automatically calls the method on the actual runtime object.
How Polymorphism Works¶
flowchart TD
A[Code calls movie.watchMovie] --> B{What is the RUNTIME type?}
B --> C[Movie]
B --> D[Adventure]
B --> E[Comedy]
B --> F[ScienceFiction]
C --> G[Execute Movie.watchMovie]
D --> H[Execute Adventure.watchMovie]
E --> I[Execute Comedy.watchMovie]
F --> J[Execute ScienceFiction.watchMovie]
Movie Class Hierarchy Example¶
The Base Class: Movie¶
public class Movie {
private String title;
public Movie(String title) {
this.title = title;
}
public void watchMovie() {
// getClass().getSimpleName() returns the RUNTIME class name
String instanceType = this.getClass().getSimpleName();
System.out.println(title + " is a " + instanceType + " film");
}
}
The Subclasses: Adventure, Comedy, ScienceFiction¶
class Adventure extends Movie {
public Adventure(String title) {
super(title);
}
@Override
public void watchMovie() {
super.watchMovie(); // Call parent's method first
System.out.printf("..%s%n".repeat(3),
"Pleasant Scene",
"Scary Music",
"Something Bad Happens");
}
public void watchAdventure() {
System.out.println("Watching an Adventure!");
}
}
class Comedy extends Movie {
public Comedy(String title) {
super(title);
}
@Override
public void watchMovie() {
super.watchMovie();
System.out.printf("..%s%n".repeat(3),
"Something funny happens",
"Something even funnier happens",
"Happy Ending");
}
public void watchComedy() {
System.out.println("Watching a Comedy!");
}
}
class ScienceFiction extends Movie {
public ScienceFiction(String title) {
super(title);
}
@Override
public void watchMovie() {
super.watchMovie();
System.out.printf("..%s%n".repeat(3),
"Bad Aliens do Bad Stuff",
"Space Guys Chase Aliens",
"Planet Blows Up");
}
public void watchScienceFiction() {
System.out.println("Watching a Science Fiction Thriller!");
}
}
Polymorphism in Action¶
// Same declared type (Movie), different runtime types
Movie movie1 = new Movie("Generic Film");
Movie movie2 = new Adventure("Star Wars");
Movie movie3 = new Comedy("Airplane");
Movie movie4 = new ScienceFiction("The Matrix");
// Each call executes DIFFERENT behavior!
movie1.watchMovie(); // Movie.watchMovie()
movie2.watchMovie(); // Adventure.watchMovie()
movie3.watchMovie(); // Comedy.watchMovie()
movie4.watchMovie(); // ScienceFiction.watchMovie()
Output:
Generic Film is a Movie film
Star Wars is a Adventure film
..Pleasant Scene
..Scary Music
..Something Bad Happens
Airplane is a Comedy film
..Something funny happens
..Something even funnier happens
..Happy Ending
The Matrix is a ScienceFiction film
..Bad Aliens do Bad Stuff
..Space Guys Chase Aliens
..Planet Blows Up
Factory Methods¶
What is a Factory Method?¶
A factory method is a static method that returns instances of objects. It hides the details of object creation from calling code.
public class Movie {
// ... fields and constructor ...
public static Movie getMovie(String type, String title) {
return switch (type.toUpperCase().charAt(0)) {
case 'A' -> new Adventure(title);
case 'C' -> new Comedy(title);
case 'S' -> new ScienceFiction(title);
default -> new Movie(title);
};
}
}
Using the Factory Method¶
// Calling code doesn't need to know about subclasses!
Movie movie = Movie.getMovie("S", "Star Wars");
movie.watchMovie();
// Output:
// Star Wars is a ScienceFiction film
// ..Bad Aliens do Bad Stuff
// ..Space Guys Chase Aliens
// ..Planet Blows Up
Benefits of Factory Methods¶
- Encapsulation: Subclass details hidden from caller
- Flexibility: Easy to add new types without changing caller code
- Centralized creation: All instantiation logic in one place
- Polymorphism-friendly: Returns parent type, actual type varies
flowchart LR
A[Calling Code] --> B[Movie.getMovie type, title]
B --> C{Type?}
C -->|A| D[new Adventure]
C -->|C| E[new Comedy]
C -->|S| F[new ScienceFiction]
C -->|default| G[new Movie]
D --> H[Returns Movie reference]
E --> H
F --> H
G --> H
Compile-Time vs Runtime Types¶
Understanding the Difference¶
| Aspect | Compile-Time Type | Runtime Type |
|---|---|---|
| When determined | At compilation | During execution |
| Also called | Declared type | Actual type |
| What decides | Variable declaration | new statement |
| Method resolution | Checks if method exists | Determines which version runs |
Example Breakdown¶
Movie movie = new Adventure("Star Wars");
// ↑ ↑
// │ └── Runtime type (Adventure)
// └── Compile-time type (Movie)
What the Compiler Sees vs What JVM Executes¶
Movie movie = new Adventure("Jaws");
// Compile-time: Compiler checks if Movie has watchMovie() ✓
// Runtime: JVM executes Adventure's watchMovie()
movie.watchMovie(); // Adventure's version runs!
Method Visibility Based on Declared Type¶
Movie movie = new Adventure("Jaws");
movie.watchMovie(); // ✅ Works - watchMovie() is on Movie
movie.watchAdventure(); // ❌ COMPILE ERROR - watchAdventure() not on Movie!
The compiler only knows about methods on the declared type (Movie), even though the runtime type (Adventure) has additional methods.
Type Casting¶
Why Cast?¶
When you need to access methods specific to a subclass:
Movie movie = Movie.getMovie("A", "Jaws");
// Can't call watchAdventure() on Movie reference
// movie.watchAdventure(); // ❌ Won't compile
// Solution: Cast to Adventure
Adventure adventure = (Adventure) movie;
adventure.watchAdventure(); // ✅ Works!
Casting Risks¶
Movie movie = Movie.getMovie("C", "Airplane"); // Actually a Comedy
// Dangerous: Casting to wrong type!
Adventure adventure = (Adventure) movie; // ❌ Runtime Exception!
ClassCastException
Casting to the wrong type compiles successfully but throws a ClassCastException at runtime. Always verify the type before casting!
The Object Reference Problem¶
Object movie = Movie.getMovie("C", "Airplane");
movie.watchMovie(); // ❌ COMPILE ERROR - watchMovie() not on Object!
When declared as Object, only Object's methods are available (like toString(), equals(), etc.).
Local Variable Type Inference (var)¶
What is var?¶
Introduced in Java 10, var lets the compiler infer the type:
var movie = Movie.getMovie("C", "Airplane");
// ↑
// └── Compiler infers: Movie (from method return type)
How Type Inference Works¶
// Method signature determines inferred type
public static Movie getMovie(String type, String title) { ... }
var movie = Movie.getMovie("C", "Airplane"); // Inferred as Movie
movie.watchMovie(); // ✅ Works - Movie has watchMovie()
movie.watchComedy(); // ❌ Error - Movie doesn't have watchComedy()
Direct Instantiation with var¶
var plane = new Comedy("Airplane"); // Inferred as Comedy
plane.watchMovie(); // ✅ Works
plane.watchComedy(); // ✅ Works - plane is a Comedy reference!
When using new ClassName(), the type is inferred as that specific class.
Limitations of var¶
// ❌ Can't use in field declarations
// private var name; // Won't compile
// ❌ Can't use in method parameters
// public void process(var item) {} // Won't compile
// ❌ Can't use in method return types
// public var getItem() {} // Won't compile
// ❌ Must have initializer
// var x; // Won't compile - can't infer type
// ❌ Can't assign null
// var x = null; // Won't compile - can't infer type from null
Runtime Type Checking¶
Method 1: Using getClass()¶
Object unknownObject = Movie.getMovie("C", "Airplane");
if (unknownObject.getClass().getSimpleName().equals("Comedy")) {
Comedy c = (Comedy) unknownObject;
c.watchComedy();
}
Method 2: Using instanceof Operator¶
Object unknownObject = Movie.getMovie("A", "Jaws");
if (unknownObject instanceof Adventure) {
Adventure a = (Adventure) unknownObject;
a.watchAdventure();
}
The instanceof operator checks if an object is an instance of a specific type.
Method 3: Pattern Matching for instanceof (JDK 16+)¶
Object unknownObject = Movie.getMovie("S", "Star Wars");
// Old way (pre-JDK 16):
if (unknownObject instanceof ScienceFiction) {
ScienceFiction scifi = (ScienceFiction) unknownObject;
scifi.watchScienceFiction();
}
// NEW way (JDK 16+) - Pattern Matching:
if (unknownObject instanceof ScienceFiction scifi) {
// scifi is already typed and ready to use!
scifi.watchScienceFiction();
}
Pattern Matching Benefits
- No explicit cast needed
- Variable (
scifi) is automatically typed - Cleaner, more readable code
- Variable scope is limited to the
ifblock
Complete Example¶
Object unknownObject = Movie.getMovie("C", "Airplane");
if (unknownObject.getClass().getSimpleName().equals("Comedy")) {
Comedy c = (Comedy) unknownObject;
c.watchComedy();
} else if (unknownObject instanceof Adventure) {
((Adventure) unknownObject).watchAdventure(); // Cast inline
} else if (unknownObject instanceof ScienceFiction scifi) {
scifi.watchScienceFiction(); // Pattern matching
}
Polymorphism Challenge: Car Classes¶
The Car Base Class¶
public class Car {
private String description;
public Car(String description) {
this.description = description;
}
public void startEngine() {
System.out.println("Car -> startEngine");
}
protected void runEngine() {
System.out.println("Car -> runEngine");
}
public void drive() {
System.out.println("Car -> driving, type is " +
getClass().getSimpleName());
runEngine();
}
}
Subclasses with Overridden Methods¶
class GasPoweredCar extends Car {
private double avgKmPerLiter;
private int cylinders = 6;
public GasPoweredCar(String description,
double avgKmPerLiter, int cylinders) {
super(description);
this.avgKmPerLiter = avgKmPerLiter;
this.cylinders = cylinders;
}
@Override
public void startEngine() {
System.out.printf("Gas -> All %d cylinders are fired up, Ready!%n",
cylinders);
}
@Override
protected void runEngine() {
System.out.printf("Gas -> usage exceeds the average: %.2f%n",
avgKmPerLiter);
}
}
class ElectricCar extends Car {
private double avgKmPerCharge;
private int batterySize = 6;
public ElectricCar(String description,
double avgKmPerCharge, int batterySize) {
super(description);
this.avgKmPerCharge = avgKmPerCharge;
this.batterySize = batterySize;
}
@Override
public void startEngine() {
System.out.printf("BEV -> switch %d KWh battery on, Ready!%n",
batterySize);
}
@Override
protected void runEngine() {
System.out.printf("BEV -> usage under the average: %.2f%n",
avgKmPerCharge);
}
}
class HybridCar extends Car {
private double avgKmPerLiter;
private int cylinders = 6;
private int batterySize;
public HybridCar(String description, double avgKmPerLiter,
int cylinders, int batterySize) {
super(description);
this.avgKmPerLiter = avgKmPerLiter;
this.cylinders = cylinders;
this.batterySize = batterySize;
}
@Override
public void startEngine() {
System.out.printf("Hybrid -> %d cylinders are fired up, Ready!%n",
cylinders);
System.out.printf("Hybrid -> switch %d KWh battery on, Ready!%n",
batterySize);
}
@Override
protected void runEngine() {
System.out.printf("Hybrid -> usage below the average: %.2f%n",
avgKmPerLiter);
}
}
Polymorphic Method¶
public class Main {
public static void main(String[] args) {
Car ferrari = new GasPoweredCar("2022 Ferrari 296 GTS", 15.4, 6);
Car tesla = new ElectricCar("2022 Tesla Model 3", 568, 75);
Car ferrariHybrid = new HybridCar("2022 Ferrari SF90", 16, 8, 8);
runRace(ferrari);
runRace(tesla);
runRace(ferrariHybrid);
}
// Polymorphic method - works with ANY Car subclass!
public static void runRace(Car car) {
car.startEngine();
car.drive();
}
}
Key Insight: Indirect Polymorphism¶
public void drive() {
System.out.println("Car -> driving, type is " + getClass().getSimpleName());
runEngine(); // ← Calls the OVERRIDDEN version!
}
Even though drive() is NOT overridden, it calls runEngine(), which IS overridden. The JVM resolves to the subclass's version!
// Output for GasPoweredCar:
Gas -> All 6 cylinders are fired up, Ready!
Car -> driving, type is GasPoweredCar
Gas -> usage exceeds the average: 15.40
Key Takeaways¶
Polymorphism Summary¶
- "Many Forms": Same method call, different behavior based on object type
- Enabled by inheritance: Subclasses override parent methods
- Resolution at runtime: JVM decides which method to execute
- Works with parent references:
Car car = new ElectricCar(...)
When to Use Polymorphism¶
- When writing code that should work with any subclass
- When you want to add new types without changing existing code
- When implementing factory patterns
- When designing flexible, extensible systems
Type Handling Rules¶
| Scenario | Solution |
|---|---|
| Call method on any subclass | Use parent reference, call overridden method |
| Call subclass-specific method | Cast to specific type (after checking) |
| Check object's actual type | Use instanceof (prefer pattern matching) |
| Let compiler infer type | Use var keyword |
Quick Reference¶
Polymorphism Checklist¶
- Base class defines method to be overridden
- Subclasses override with
@Overrideannotation - Use parent class as reference type for flexibility
- Factory methods return parent type
- Cast only after type checking
Pattern Matching Syntax (JDK 16+)¶
if (object instanceof SpecificType variableName) {
// variableName is already typed as SpecificType
variableName.specificMethod();
}
Questions Explored¶
- What is polymorphism and how does it work?
- What's the difference between compile-time and runtime types?
- How do I write code that works with any subclass?
- What is a factory method and why use it?
- When and how should I cast objects?
- How do I check an object's runtime type safely?
- What is
varand when can I use it?
Related Notes¶
| Part | Topic | Link |
|---|---|---|
| 1 | Classes, Objects & Encapsulation | ← Part 1 |
| 2 | Inheritance & Method Overriding | ← Part 2 |
| 3 | Strings & StringBuilder | ← Part 3 |
| 4 | Composition | ← Part 4 |
| 5 | Encapsulation | ← Part 5 |
| 6 | Polymorphism | You are here |
Last Updated: 2026-01-26