(Coursenotes for CSC 203 Project-based Object-oriented Programming and Design)

Abstract classes

Overview

We learned about interfaces in Java in the previous lessons. For a recap, please take a look at the Summary from the previous lesson.

Interfaces come with numerous benefits:

  1. They let us introduce a degree of separation between implementation details of different classes in a larger Object-oriented system.
  2. default methods in interfaces let us “pull up” common method implementations into the interface, allowing implementing subclasses to inherit those methods. This saves us from having to duplicate code.

However, interfaces are just one way to achieve polymorphism in Java. In this lesson, we will learn about a related Java construct called abstract classes. Abstract classes are just like regular classes in Java, except they can have abstract methods. There are a few similarities and differences between abstract classes and interfaces in Java.

Differences

Similarities

We’ll motivate the use of abstract classes by first looking at a disadvantage of using interfaces.

A disadvantage of using interfaces

Consider the following interface-based type hierarchy.

In the Shape interface below, we have five abstract methods, each of which needs to be implemented by the four subtypes.

classDiagram
  direction TD
  `interface Shape` <|-- Square : implements
  `interface Shape` <|-- Rectangle : implements
  `interface Shape` <|-- Circle : implements
  `interface Shape` <|-- Triangle : implements

  class `interface Shape` {
    getColor() Color*
    setColor(Color newColor) Color*
    getArea() double*
    getPerimeter() double*
    translate(Point point) void*
  }

  class Square {
    -double sideLength
    -Point center
    -Color color

    getColor() Color
    setColor(Color newColor) Color
    getArea() double
    getPerimeter() double
    translate(Point point) void
  }

  class Rectangle {
    -double width
    -double height
    -Point center
    -Color color
    getColor() Color
    setColor(Color newColor) Color
    getArea() double
    getPerimeter() double
    translate(Point point) void
  }

  class Circle {
    -double radius
    -Point center
    -Color color
    getColor() Color
    setColor(Color newColor) Color
    getArea() double
    getPerimeter() double
    translate(Point point) void
  }

  class Triangle {
    -Point a 
    -Point b 
    -Point c 
    -Color color
    getColor() Color
    setColor(Color newColor) Color
    getArea() double
    getPerimeter() double
    translate(Point point) void
  }

Implementations for getArea, getPerimeter, and translate would likely be different for each subclass, and need to implemented on a per-class basis. For example, calculating the area for a square is different from that of a circle, which is different from that of a triangle, etc.

However, implementations for getColor and setColor are likely be identical for all the subclasses—chances are they have a private Color color instance variable, and they are pretty standard getters and setters.

Because interfaces cannot have instance variables, any code that directly touches those instance variables must be written in the implementing subclasses, even if is the same for all subclasses.

Abstract classes

Abstract classes can help us in these types of situations. Specifically, they are useful when:

Right now, the color instance variable and its getter and setter methods are all being duplicated in the four child classes. If we instead write Shape as an abstract class, we can “pull up” that data and behaviour into a common parent, thereby allowing all the subclasses to re-use that code.

In the new hierarchy below, which uses an abstract class, we can see that the subclasses have reduced in size because they now do not duplicate the shared behaviour.

classDiagram
  direction TD
  `abstract Shape` <|-- Square : extends 
  `abstract Shape` <|-- Rectangle : extends
  `abstract Shape` <|-- Circle : extends
  `abstract Shape` <|-- Triangle : extends

  class `abstract Shape` {
    +Color color
    getColor() Color
    setColor(Color newColor) Color
    
    getArea() double*
    getPerimeter() double*
    translate(Point point) void*
  }

  class Square {
    -double sideLength
    -Point center
    getArea() double
    getPerimeter() double
    translate(Point point) void
  }

  class Rectangle {
    -double width
    -double height
    -Point center
    getArea() double
    getPerimeter() double
    translate(Point point) void
  }

  class Circle {
    -double radius
    -Point center
    getArea() double
    getPerimeter() double
    translate(Point point) void
  }

  class Triangle {
    -Point a 
    -Point b 
    -Point c
    getArea() double
    getPerimeter() double
    translate(Point point) void
  }

Do you see further opportunities for improvement to the structure above? Is there any other duplicated data that can be “moved upward” to reduce code duplication?

Code for the example above

The Shape abstract class

See the Shape abstract class below. Please use the “Walkthrough” button to see notes about important parts of the code below. In particular, there are two new keywords introduced in the code below: abstract and protected.

View in new tab

Since the Shape is abstract, you can’t do this:

// This code won't compile
Shape myShape = new Shape(new Color(255, 0, 0)); // A red....shape? 

Just like an interface, an abstract class is a skeleton for its subclasses — it can’t be used to create objects. It’s a convenient construct for placing common code in one place so that subclasses don’t have to duplicate it.

The Triangle class

Code for the Triangle class is below. Please use the “Walkthrough” button to step through the accompanying notes. Here are some key features to note:

View in new tab

Interfaces or abstract classes?

The key consideration here is that a class can implement at most one abstract class. If your class hierarchy is designed using only abstract classes, you are necessarily going to end up with a tree-like structure, where very child type has exactly one parent type. Sometimes this simplicity is desirable, and sometimes it is too restrictive.

On the other hand, if you create a class hierarchy only interfaces, you end up with a “flatter” hierarchy that is much more flexible, since classes and implement multiple interfaces. This allows you to “mix in” different parent types for individual subclasses, as needed. This flexibility can be a blessing, but can also become difficult to reason about as your project grows.

Or, more likely, you will use some combination of abstract classes and interfaces.

Suppose you have a bunch of interfaces A, B, C, and D. You have created a number of subclasses that implement various combinations of these interfaces, but you find yourself creating some combinations more often than others. For example, you find yourself creating many classes that implement both B and C, and end up duplicating instance variables and methods in those classes. In this case, it may make sense to create an abstract class that implements B, C, and root all of that common data and behaviour in the abstract class.

Yet another possibility is to use neither, at least when you’re working on a smaller system. Sometimes too much polymorphism can be a bad thing. If it’s a simple function and you only do it once, perhaps you don’t need to create a whole type hierarchy to do what you could have done with a “type” variable and an if condition. If you find yourself frequently checking this “type” variable before performing tasks, that’s when you could consider using polymorphism.

Like many problems in software design, there is no “silver bullet” that solves all your problems. You will face tradeoffs and make choices about how to design your systems.