DEV Community

Cover image for Concrete Vs Abstract Vs Interface - [OOP & Java #4]
Liu Yongliang
Liu Yongliang

Posted on • Edited on

Concrete Vs Abstract Vs Interface - [OOP & Java #4]

Let's explore the relationship between concrete class, abstract class, and interfaces.

Suppose we would like to create a representation of a laptop.

class Laptop {
  String screen;
  int batteryHour;

  Laptop(String screen, int batteryHour) {
    this.screen = screen;
    this.batteryHour = batteryHour;
  }

  void moveCursor(){
    System.out.println("Using the trackpad to move the cursor");
  }
}
Enter fullscreen mode Exit fullscreen mode

The Laptop class contains a constructor that takes in a string representing the resolution of its screen and an integer representing the hours that its battery can last. It also has a moveCursor method for illustration purposes later on.

Laptop ex1 = new Laptop("1080P",5);
Laptop ex2 = new Laptop("4k", 8);
Enter fullscreen mode Exit fullscreen mode

A typical issue we often face is that sometimes we need to create something related to the existing objects, like a desktop in this case.
Given that a desktop also has a screen, we can simply and intuitively make our laptop class the parent of the desktop class, like so:

class Desktop extends Laptop {
  Desktop(String screen, int batteryHour){
    super(screen, batteryHour);
  }
}
Enter fullscreen mode Exit fullscreen mode

If you read the book "Thinking Fast And Slow", you will know that system 1, or the lazy, quick response system of our brain, often make mistakes.

This is one such instance. So, DO NOT in any circumstances, subclass an object simply because they share some properties. There are many reasons why the above solution might violate the Liskov Substitution Principle (LSP). One obvious point is that the batteryHour does not make sense to a Desktop.

Both laptops and desktops are computers. They share a lot of similarities. However, you would agree with me that we do not typically say that a desktop is a laptop or vice versa. This is a tell-tale sign that they are not suitable for a parent-child relationship.


Concrete Class

Knowing that they are both computers leads us to the first valid design:

// Have Computer as a Concrete parent class
class Computer {
  String screen;
  void moveCursor(){
    System.out.println("moving the cursor?");
  }
}

class Laptop extends Computer {
  int batteryHour;
  Laptop(String screen, int batteryHour) {
    super.screen = screen;
    this.batteryHour = batteryHour;
  }

  @Override
  void moveCursor(){
    System.out.println("Using the trackpad to move the cursor");
  }
}

class Desktop extends Computer {
  Desktop(String screen){
    super.screen = screen;
  }

  @Override
  void moveCursor(){
    System.out.println("Using the mouse to move the cursor");
  }   
}
Enter fullscreen mode Exit fullscreen mode

Observations

  • Define a Computer class as the parent of these two child class
  • For properties and methods that are common, they should be extracted into the parent class.
  • Methods can be shared among the children, and by including them in the parent class, we make sure that all its children will have those methods. However, we may or may not be able to fill in the implementation of a method accurately at the parent class level. This means each child class will need to override this method to include its own implementation. Note that this behavior is not enforced by the compiler, meaning forgetting to override a method when you meant to do so will result in an unexpected runtime error. For example, desktop users typically do not have/use a trackpad to control their cursor. Therefore, moveCursor method has to be overridden at the child class level for Desktop. Yet, to omit the overriding is perfectly acceptable behavior to the compiler.
  • It is also possible to instantiate the parent class since it is concrete. But, it may not make sense to do so. In our case, if we call new Computer().moveCursor(), the output may not be meaningful.

Abstract Class

The second approach is to create Computer as an abstract class.

abstract class Computer {
  String screen;
  abstract void moveCursor();
}

class Laptop extends Computer {
  int batteryHour;
  Laptop(String screen, int batteryHour) {
    super.screen = screen;
    this.batteryHour = batteryHour;
  }

  @Override
  void moveCursor(){
    System.out.println("Using the trackpad to move the cursor");
  }
}

class Desktop extends Computer {
  Desktop(String screen){
    super.screen = screen;
  }

  @Override
  void moveCursor(){
    System.out.println("Using the mouse to move the cursor");
  }   
}
Enter fullscreen mode Exit fullscreen mode
Abstract class
  • Cannot be instantiated
  • Contains abstract methods that do not have implementation details and must be implemented by its subclass, will be enforced by the compiler
  • Can contain properties
  • Can contain concrete methods with implementations as well

This pretty much solves the few issues we have with concrete class. If the parent class does not need to be instantiated, it will be safe to say that letting the parent class be abstract is usually better. Now, the drawback of the second approach, at least for Java, is that a class can only inherit from one parent class.

Let say you want to have a Chargeable class for objects that can be charged:

abstract class Chargeable {
  abstract Chargeable charge();
}
Enter fullscreen mode Exit fullscreen mode

Then you are faced with a dilemma. Given that a class can only subclass one parent class, which one to choose?

Side note on multiple inheritance
  • You may want to inherit all methods of different parents, but not all properties.
  • Will PineapplePen be a logical object to you?
class PineapplePen extends Pineapple, Pen{
//...
}
Enter fullscreen mode Exit fullscreen mode

Interfaces

The next option in line is interfaces:

abstract class Computer {
  String screen;
  abstract void moveCursor();
}

interface Chargeable {
  void charge();
}

class Laptop extends Computer implements Chargeable {
  int batteryHour;
  Laptop(String screen, int batteryHour) {
    super.screen = screen;
    this.batteryHour = batteryHour;
  }

  @Override
  void moveCursor(){
    System.out.println("Using the trackpad to move the cursor");
  }

  @Override
  public void charge(){
    // pretend that it does something
    System.out.println("Charging the laptop");
  }
}

class Desktop extends Computer implements Chargeable {
  Desktop(String screen){
    this.screen = screen;
  }

  @Override
  void moveCursor(){
    System.out.println("Using the mouse to move the cursor");
  }

  @Override
  public void charge(){
    // pretend that it does something
    System.out.println("Providing power to the desktop");
  }
}   
Enter fullscreen mode Exit fullscreen mode

Interfaces have the following characteristics:

  • Methods are implicitly abstract and public
  • Cannot be instantiated
  • Only constant variables allowed (static final) within interfaces
  • Interface can extend multiple interfaces
  • A class can implement multiple interfaces

In the final code snippet, there is a combination of concrete class, abstract class, and interface at work. There is no dominance of one over the other. Rather, exploring situations in which one of them will be most helpful will provide us the knowledge to weigh our options and choose the right one when designing our program.


Summary

Concrete Class Abstract Class Interface
Implementation Actual Actual
Abstract
Abstract
Instantiatable Yes No No

Till next time...

Top comments (0)