CodeGym /Courses /JAVA 25 SELF /Problems of polymorphism and abstractions

Problems of polymorphism and abstractions

JAVA 25 SELF
Level 23, Lesson 3
Available

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.

1
Task
JAVA 25 SELF, level 23, lesson 3
Locked
One method for any file 🎬
One method for any file 🎬
1
Task
JAVA 25 SELF, level 23, lesson 3
Locked
Virtual Canvas: Variety of Shapes 🎨
Virtual Canvas: Variety of Shapes 🎨
1
Task
JAVA 25 SELF, level 23, lesson 3
Locked
Smart Home: Boiling Kettle 💡
Smart Home: Boiling Kettle 💡
1
Task
JAVA 25 SELF, level 23, lesson 3
Locked
IT Company: Programmer who works and reports 👨‍💻
IT Company: Programmer who works and reports 👨‍💻
Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION