Java · lesson 15 of 66
Beginner
17- 1 Introduction to Java
- 2 Setting Up Java
- 3 Data Types in Java
- 4 Operators in Java
- 5 Variables and Operators in Java
- 6 Control Flow in Java
- 7 Arrays in Java
- 8 Functions (Methods) in Java
- 9 Strings in Java
- 10 Methods in Java
- 11 Java OOP — Complete Guide
- 12 Introduction to Object-Oriented Programming in Java
- 13 Encapsulation in Java
- 14 Inheritance in Java
- 15 Polymorphism in Java
- 17 Abstraction in Java
- 18 Error Types in Java
Intermediate
38- 16 Regular Expressions in Java
- 19 Interfaces in Java — A Deep Dive
- 20 Abstract Classes in Java
- 21 Method Overloading in Java
- 22 Composition vs Inheritance in Java
- 23 Generics in Java
- 27 Exception Handling in Java
- 28 Java Streams API
- 29 Collections Framework in Java
- 30 Optional in Java
- 31 File Handling in Java
- 32 Records in Java
- 33 Java 8 Features
- 34 Sealed Classes and Interfaces in Java
- 35 Multithreading in Java
- 37 JDBC — Database Access in Java
- 39 Maven — Java Build and Dependency Management
- 42 Gradle — Modern Java Build Tool
- 44 Unit Testing with JUnit 5 and Mockito
- 45 Logging in Java with SLF4J and Logback
- 49 Spring Boot — Introduction
- 50 Dependency Injection in Spring Boot
- 51 Java Project Ideas
- 52 Building REST APIs with Spring Boot
- 53 Validation in Spring Boot
- 54 Spring Data JPA — Database Access
- 55 Exception Handling in Spring Boot
- 56 Spring Security — Authentication and Authorization
- 57 JWT Authentication in Spring Boot
- 58 Dockerizing Spring Boot Applications
- 59 Java Enums: Beyond Simple Constants
- 60 Inner and Anonymous Classes in Java
- 61 Java Generics: Bounded Types, Wildcards, and Generic Methods
- 62 Java Date/Time API: LocalDate, LocalDateTime, and Duration
- 63 Java NIO.2: Paths, Files, and WatchService
- 64 Java Annotations: Custom Types and Meta-Information
- 65 Java Reflection: Dynamic Type Inspection and Method Invocation
- 66 Java Localization: ResourceBundle and Locale Support
Advanced
11Polymorphism in Java
See Java polymorphism run: overriding and dynamic dispatch, coding to interfaces, why fields and static methods don't dispatch, pattern-matching switch.
By SkillByExample · Updated
Polymorphism means one piece of code works with many types. In Java it mostly means dynamic
dispatch: a call through a supertype reference runs the method of the object’s actual class.
Every example is a single file run with the JDK 21 source launcher (java File.java).
Overriding and dynamic dispatch
import java.util.List;
public class Dispatch {
static class Animal {
String speak() { return "..."; }
String intro() { return getClass().getSimpleName() + " says " + speak(); }
}
static class Dog extends Animal {
@Override String speak() { return "Woof"; }
}
static class Cat extends Animal {
@Override String speak() { return "Meow"; }
}
public static void main(String[] args) {
List<Animal> animals = List.of(new Animal(), new Dog(), new Cat());
for (Animal a : animals) {
System.out.println(a.intro());
}
}
}
$ java Dispatch.java
Animal says ...
Dog says Woof
Cat says Meow
The loop variable is declared Animal, yet each call runs the subclass’s speak. Even inside
Animal.intro(), the call to speak() dispatches on the real object. That is the core trick:
intro was written once and works for subclasses that did not exist when it was written.
@Override catches mistakes
Without @Override, a typo creates a new method instead of overriding one.
public class OverrideCheck {
static class Shape {
double area() { return 0; }
}
static class Square extends Shape {
double side = 3;
double Area() { return side * side; } // capital A: a new method
}
static class Circle extends Shape {
double r = 1;
@Override double area() { return Math.PI * r * r; }
}
public static void main(String[] args) {
Shape[] shapes = {new Square(), new Circle()};
for (Shape s : shapes) {
System.out.printf("%s %.2f%n", s.getClass().getSimpleName(), s.area());
}
}
}
$ java OverrideCheck.java
Square 0.00
Circle 3.14
Square reports 0.00 because Area() never overrode area(). Put @Override on it and the
compiler rejects it with “method does not override or implement a method from a supertype”.
Programming to an interface
Write methods against an interface and they accept every implementation, present or future.
import java.util.List;
public class Payments {
interface PaymentMethod {
String pay(double amount);
}
record Card(String last4) implements PaymentMethod {
public String pay(double amount) { return "charged %.2f to card *%s".formatted(amount, last4); }
}
record Wallet(String email) implements PaymentMethod {
public String pay(double amount) { return "sent %.2f from %s".formatted(amount, email); }
}
static void checkout(PaymentMethod method, double total) {
System.out.println(method.pay(total));
}
public static void main(String[] args) {
checkout(new Card("4242"), 19.99);
checkout(new Wallet("ada@example.com"), 5);
PaymentMethod free = amount -> "nothing to pay for " + amount; // a lambda works too
checkout(free, 0);
}
}
$ java Payments.java
charged 19.99 to card *4242
sent 5.00 from ada@example.com
nothing to pay for 0.0
checkout depends only on the pay contract. Adding a new payment type means a new class, not an
edit to checkout. Because PaymentMethod has a single abstract method, a lambda can implement it.
What does not dispatch: fields and static methods
Only instance methods are polymorphic. Fields and static methods are chosen by the declared type.
public class Hiding {
static class Parent {
String name = "parent field";
static String kind() { return "Parent.kind"; }
String who() { return "Parent.who"; }
}
static class Child extends Parent {
String name = "child field"; // hides, does not override
static String kind() { return "Child.kind"; }
@Override String who() { return "Child.who"; }
}
public static void main(String[] args) {
Parent p = new Child();
System.out.println(p.who());
System.out.println(p.name);
System.out.println(((Child) p).name);
System.out.println(Parent.kind() + " / " + Child.kind());
}
}
$ java Hiding.java
Child.who
parent field
child field
Parent.kind / Child.kind
p.who() dispatches to Child, but p.name reads Parent’s field because p is declared
Parent. Keep fields private and expose them through methods, and this confusion cannot arise.
Overloading is chosen at compile time
Overloads are picked from declared types, which makes them look polymorphic until they are not.
public class StaticVsDynamic {
static class Animal { String sound() { return "generic"; } }
static class Dog extends Animal { @Override String sound() { return "woof"; } }
static String feed(Animal a) { return "feeding an animal that says " + a.sound(); }
static String feed(Dog d) { return "feeding a dog that says " + d.sound(); }
public static void main(String[] args) {
Animal rex = new Dog();
System.out.println(feed(rex));
System.out.println(feed((Dog) rex));
}
}
$ java StaticVsDynamic.java
feeding an animal that says woof
feeding a dog that says woof
The compiler chose feed(Animal) because rex is declared Animal — yet inside it, a.sound()
still dispatched to Dog. Overload selection is static, override selection is dynamic. The
method overloading lesson covers the resolution rules.
Sealed types and pattern-matching switch
Java 21 adds another style: a sealed interface lists its implementations, and a switch over it
must handle every one.
public class Sealed {
sealed interface Shape permits Circle, Rect, Triangle {}
record Circle(double r) implements Shape {}
record Rect(double w, double h) implements Shape {}
record Triangle(double b, double h) implements Shape {}
static double area(Shape s) {
return switch (s) {
case Circle c -> Math.PI * c.r() * c.r();
case Rect r when r.w() == r.h() -> r.w() * r.w(); // squares
case Rect r -> r.w() * r.h();
case Triangle t -> 0.5 * t.b() * t.h();
};
}
public static void main(String[] args) {
for (Shape s : new Shape[] {new Circle(1), new Rect(2, 2), new Rect(2, 5), new Triangle(3, 4)}) {
System.out.printf("%-30s %.2f%n", s, area(s));
}
}
}
$ java Sealed.java
Circle[r=1.0] 3.14
Rect[w=2.0, h=2.0] 4.00
Rect[w=2.0, h=5.0] 10.00
Triangle[b=3.0, h=4.0] 6.00
No default branch is needed: the compiler knows the three permitted types, and if a fourth were
added to permits, this switch would stop compiling until it was handled. Overriding puts the
behaviour inside each type; a sealed switch keeps one operation in one place.
Covariant return types
An override may narrow its return type to a subclass of the original.
public class Covariant {
static class Animal {
Animal reproduce() { return new Animal(); }
}
static class Dog extends Animal {
@Override Dog reproduce() { return new Dog(); } // Dog, not Animal
void wag() { System.out.println("puppy wags"); }
}
public static void main(String[] args) {
Dog puppy = new Dog().reproduce(); // no cast needed
puppy.wag();
Animal a = new Dog();
System.out.println(a.reproduce().getClass().getSimpleName());
}
}
$ java Covariant.java
puppy wags
Dog
Callers that know they have a Dog get a Dog back without casting, while code holding an
Animal still works unchanged. clone() overrides commonly use this.
Practice
- Add a
CowtoDispatchthat says"Moo", without editingAnimalor the loop body.
Solution
import java.util.List;
public class AddCow {
static class Animal {
String speak() { return "..."; }
String intro() { return getClass().getSimpleName() + " says " + speak(); }
}
static class Dog extends Animal { @Override String speak() { return "Woof"; } }
static class Cow extends Animal { @Override String speak() { return "Moo"; } }
public static void main(String[] args) {
for (Animal a : List.of(new Dog(), new Cow())) System.out.println(a.intro());
}
}
$ java AddCow.java
Dog says Woof
Cow says Moo
Extending the hierarchy needed no change to existing code — the open/closed principle in action.
- Fix
OverrideChecksoSquarereally overridesarea(), and confirm with@Override.
Solution
public class OverrideFixed {
static class Shape { double area() { return 0; } }
static class Square extends Shape {
double side = 3;
@Override double area() { return side * side; }
}
public static void main(String[] args) {
Shape s = new Square();
System.out.println(s.area());
}
}
$ java OverrideFixed.java
9.0
With the name corrected, @Override compiles and the call through Shape reaches Square.
- Add a
Hexagon(double side)record to the sealedShapehierarchy (area is3 * sqrt(3) / 2 * side * side). What does the compiler say before you updatearea?
Solution
public class SealedHex {
sealed interface Shape permits Circle, Hexagon {}
record Circle(double r) implements Shape {}
record Hexagon(double side) implements Shape {}
static double area(Shape s) {
return switch (s) {
case Circle c -> Math.PI * c.r() * c.r();
};
}
public static void main(String[] args) {
System.out.println(area(new Hexagon(1)));
}
}
$ java SealedHex.java
SealedHex.java:7: error: the switch expression does not cover all possible input values
return switch (s) {
^
1 error
error: compilation failed
Adding the missing case fixes it:
public class SealedHexFixed {
sealed interface Shape permits Circle, Hexagon {}
record Circle(double r) implements Shape {}
record Hexagon(double side) implements Shape {}
static double area(Shape s) {
return switch (s) {
case Circle c -> Math.PI * c.r() * c.r();
case Hexagon h -> 3 * Math.sqrt(3) / 2 * h.side() * h.side();
};
}
public static void main(String[] args) {
System.out.printf("%.3f%n", area(new Hexagon(1)));
}
}
$ java SealedHexFixed.java
2.598
Exhaustiveness checking turns “forgot to handle the new type” from a runtime bug into a compile error.
- Write a
Notifierinterface withsend(String msg)and two implementations,EmailandSms(SMS truncates to 10 characters). Send the same message through aList<Notifier>.
Solution
import java.util.List;
public class Notify {
interface Notifier { String send(String msg); }
static class Email implements Notifier {
public String send(String msg) { return "email: " + msg; }
}
static class Sms implements Notifier {
public String send(String msg) { return "sms: " + (msg.length() > 10 ? msg.substring(0, 10) : msg); }
}
public static void main(String[] args) {
for (Notifier n : List.of(new Email(), new Sms())) {
System.out.println(n.send("Your order has shipped"));
}
}
}
$ java Notify.java
email: Your order has shipped
sms: Your order
The loop knows only Notifier; each object decides how to deliver the message.