1. Polymorphism: what it is and why you need it
If you think polymorphism is something from the world of Marvel mutants, I’m sorry to disappoint you: in programming things are much calmer, but no less magical. Polymorphism is the ability of objects with different implementations to respond differently to the same method calls.
Real-life example:
You have a Book class and a Magazine class, both inherited from the abstract class LibraryItem. You want to be able to call the printInfo() method for any library item, and it will output the required information — for a book, that’s the author and title; for a magazine — the issue number and date.
Code example:
abstract class LibraryItem {
String title;
LibraryItem(String title) {
this.title = title;
}
abstract void printInfo();
}
class Book extends LibraryItem {
String author;
Book(String title, String author) {
super(title);
this.author = author;
}
@Override
void printInfo() {
System.out.println("Book: " + title + ", author: " + author);
}
}
class Magazine extends LibraryItem {
int issueNumber;
Magazine(String title, int issueNumber) {
super(title);
this.issueNumber = issueNumber;
}
@Override
void printInfo() {
System.out.println("Magazine: " + title + ", issue: " + issueNumber);
}
}
Now you can create an array of different items and call printInfo() for each:
LibraryItem[] items = {
new Book("Lord of the Flies", "William Golding"),
new Magazine("Science and Life", 5)
};
for (LibraryItem item : items) {
item.printInfo();
}
// Will print:
// Book: Lord of the Flies, author: William Golding
// Magazine: Science and Life, issue: 5
That’s how polymorphism works!
2. Common mistakes with polymorphism
Attempting to call methods that don’t exist on the base type
One of the most common mistakes is trying to call a method that is declared only in a child class via a reference of the base type.
LibraryItem item = new Book("Harry Potter", "J. Rowling");
// item.getAuthor(); // Compilation error! LibraryItem has no getAuthor() method
Java compiles code based on what it sees in the variable’s type (LibraryItem), not the actual object (Book). Therefore, if you need to call a method specific to a book, you must cast:
if (item instanceof Book) {
Book book = (Book) item;
// Now you can call book.getAuthor()
}
Casting without checks
If you are sure an object is a Book but it actually isn’t, you’ll get a ClassCastException at runtime. For example:
LibraryItem item = new Magazine("Forbes", 12);
Book book = (Book) item; // BOOM! ClassCastException
The right way is to always check the type:
if (item instanceof Book) {
Book book = (Book) item;
// OK
} else {
System.out.println("This is not a book!");
}
Not leveraging the benefits of polymorphism
Sometimes developers write code that is tightly coupled to specific types, even though abstractions could be used. For example, if you write:
Book[] books = ...;
for (Book book : books) {
book.printInfo();
}
This only works for books. What if tomorrow you add magazines, newspapers, comics? It’s better to use an array of LibraryItem[] and work with methods of the base class or interface.
3. Abstractions: why they’re needed and how not to mess them up
Abstract classes and interfaces
Abstraction is the art of highlighting what matters and hiding details. In Java, you have abstract classes and interfaces for that.
- Abstract class — a class that cannot be instantiated directly, only extended.
- Interface — a contract: what a class must do, not how it does it.
Mistake 1: Creating an abstract class with no abstract methods
If your abstract class has no abstract methods, think about it — should it really be abstract? Maybe it’s simpler to make a regular class.
abstract class UselessAbstract {
void sayHello() {
System.out.println("Hello!");
}
}
// Better to make a regular class if there are no abstract methods
Mistake 2: Missing implementations of required methods in subclasses
If a class extends an abstract class or implements an interface, it must implement all abstract methods. If you forget, the compiler will remind you — but sometimes methods are implemented “for show” and do nothing. That’s bad for maintainability.
class Magazine extends LibraryItem {
Magazine(String title, int issueNumber) {
super(title);
// ...
}
@Override
void printInfo() {
// Empty! Bad!
}
}
Mistake 3: A hierarchy of abstractions that’s too deep or convoluted
When classes inherit from each other in five–ten levels, it becomes very hard to understand them. It’s better to build “flat” hierarchies where everything is clear.
Bad example:
LibraryItem
|
BookItem
|
PrintedBook
|
IllustratedBook
|
ChildrenIllustratedBook
Complicated, isn’t it? It’s better to limit yourself to two or three levels.
4. Practice: Applying polymorphism and abstractions in a training application
Let’s refine your training library application. Previously, you only had books. Now let’s add magazines and implement a common interface for printed publications.
Declare an abstract class:
abstract class LibraryItem {
protected String title;
public LibraryItem(String title) {
this.title = title;
}
public abstract void printInfo();
}
Add subclasses:
class Book extends LibraryItem {
private String author;
public Book(String title, String author) {
super(title);
this.author = author;
}
@Override
public void printInfo() {
System.out.println("Book: " + title + ", author: " + author);
}
}
class Magazine extends LibraryItem {
private int issueNumber;
public Magazine(String title, int issueNumber) {
super(title);
this.issueNumber = issueNumber;
}
@Override
public void printInfo() {
System.out.println("Magazine: " + title + ", issue: " + issueNumber);
}
}
Use polymorphism:
LibraryItem[] items = {
new Book("Clean Code", "Robert Martin"),
new Magazine("Java World", 3)
};
for (LibraryItem item : items) {
item.printInfo();
}
Add an interface for electronic publications
Suppose some publications can be read online. Let’s introduce an interface:
interface ReadableOnline {
void openOnline();
}
class EBook extends Book implements ReadableOnline {
private String url;
public EBook(String title, String author, String url) {
super(title, author);
this.url = url;
}
@Override
public void openOnline() {
System.out.println("Opening the e-book at: " + url);
}
}
Now you can work with e-books through the interface:
ReadableOnline ebook = new EBook("Java For Dummies", "Barry Burd", "https://example.com/java");
ebook.openOnline();
5. How to avoid problems with polymorphism and abstractions: best practices
- Use interfaces and abstract classes to describe behaviour, not state.
For example, the Printable interface describes the ability to “print”, whereas storing a field String title in an interface is already a bad idea. - Check the object’s type with instanceof before casting.
Especially if the object can be of different types. This will protect you from ClassCastException. - Aim for “flat”, understandable hierarchies.
The simpler the inheritance tree, the easier it is to maintain and extend the code. - Avoid “meaningless” abstractions.
If a class doesn’t contain abstract methods and isn’t intended for inheritance, don’t make it abstract. - Use the @Override annotation whenever you override a method.
This helps the compiler catch signature errors.
6. Typical mistakes when working with polymorphism and abstractions
Error No. 1: Casting without a check
Sometimes you want to “cut a corner” and cast without checking. It may work, or it may cause a sudden crash. Always use instanceof:
if (item instanceof Book) {
Book book = (Book) item;
// ...
}
Error No. 2: Attempting to call a subclass method through a base-type reference
LibraryItem item = new Book("Java", "Author");
item.getAuthor(); // Compilation error: LibraryItem has no such method!
The solution is either to cast, or to add the required method to the base class (if that’s logical).
Error No. 3: Incomplete implementation of an interface or abstract class
If you forget to implement all methods of an interface, the compiler won’t let you build the project. But if you implement “stubs” that do nothing, it will lead to unexpected behaviour.
Error No. 4: An inheritance hierarchy that’s too deep
If you have more than three levels of inheritance, consider whether you can simplify the architecture.
Error No. 5: Violating the single responsibility principle
If an abstraction describes too many responsibilities, it becomes hard to maintain. It’s better to split it into several interfaces or classes.
GO TO FULL VERSION