The Liskov Substitution Principle in Java

The Liskov Substitution Principle, which provides a lot of details on it. As per the LSP, functions that use references to base classes must be able to use objects of the derived class without knowing it. In simple words, derived classes must be substitutable for the base class. To illustrate the LSP, let’s take an example of rectangles and squares. One tends to establish the ISA relationship, thus, you can say that a square a rectangle. However, there arises a problem (hence, a violation of the Liskov Substitution Principle) which shall be demonstrated with the following code sample. Take a look at code illustration below to understand the Liskov Substitution Principle in detail.

Substitutability is a principle in object-oriented programming introduced by Barbara Liskov in a 1987 conference keynote stating that, if class B is a subclass of class A, then wherever A is expected, B can be used instead:

class A {…}
class B extends A {…}

public void method(A obj) {…}

A a = new B(); // Assignment OK
method(new B()); // Passing as parameter OK

This also applies when the type is an interface, where there doesn’t need to any hierarchical relationship between the objects:

interface Foo {
     void bar();
}

class A implements Foo {
    void bar() {…}
}

class B implements Foo {
    void bar() {…}
}
List foos = new ArrayList<>();
foos.add(new A()); // OK
foos.add(new B()); // OK

Now the list contains objects that are not from the same class hierarchy.

Abstract class and Interface usage: “Is-a” relation vs “Has-a” capability

When to use abstract classes: To implement the same or different behaviour among multiple related objects

Related Article: Packages and Inheritance in Java with Examples

When to use interfaces: to implement a contract by multiple unrelated objects

Abstract classes create “is a” relations while interfaces provide “has a” capability.

This can be seen in the code below:

public class InterfaceAndAbstractClassDemo{
    public static void main(String args[]){

        Dog dog = new Dog("Jack",16);
        Cat cat = new Cat("Joe",20);

        System.out.println("Dog:"+dog);
        System.out.println("Cat:"+cat);

        dog.remember();
        dog.protectOwner();
        Learn dl = dog;
        dl.learn();

        cat.remember();
        cat.protectOwner();

        Climb c = cat;
        c.climb();

        Man man = new Man("Ravindra",40);
        System.out.println(man);

        Climb cm = man;
        cm.climb();
        Think t = man;
        t.think();
        Learn l = man;
        l.learn();
        Apply a = man;
        a.apply();
   }
}

abstract class Animal{
   String name;
   int lifeExpentency;
   public Animal(String name,int lifeExpentency ){
         this.name = name;
         this.lifeExpentency=lifeExpentency;
   }
    public abstract void remember();
    public abstract void protectOwner();

    public String toString(){
        return     this.getClass().getSimpleName()+":"+name+":"+lifeExpentency;
     }
}
class Dog extends Animal implements Learn{
     public Dog(String name,int age){
          super(name,age);
     }
     public void remember(){
          System.out.println(this.getClass().getSimpleName()+" can remember for 5 minutes");
     }
     public void protectOwner(){
         System.out.println(this.getClass().getSimpleName()+ " will protect owner");
     }
     public void learn(){
         System.out.println(this.getClass().getSimpleName()+ " can learn:");
     }
}
class Cat extends Animal implements Climb {
     public Cat(String name,int age){
           super(name,age);
     }
     public void remember(){
          System.out.println(this.getClass().getSimpleName() + " can remember for 16 hours");
     }
     public void protectOwner(){
         System.out.println(this.getClass().getSimpleName()+ " won't protect owner");
     }
     public void climb(){
         System.out.println(this.getClass().getSimpleName()+ " can climb");
     }
}
interface Climb{
     void climb();
}
interface Think {
    void think();
}
interface Learn {
   void learn();
}
interface Apply{
   void apply();
}
class Man implements Think,Learn,Apply,Climb{
String name;
int age;
public Man(String name,int age){
this.name = name;
this.age = age;
}
public void think(){
System.out.println("I can think:"+this.getClass().getSimpleName());
}
public void learn(){
System.out.println("I can learn:"+this.getClass().getSimpleName());
}
public void apply(){
System.out.println("I can apply:"+this.getClass().getSimpleName());
}
public void climb(){
System.out.println("I can climb:"+this.getClass().getSimpleName());
}
public String toString(){
return "Man :"+name+":Age:"+age;
}
}

output:

Dog:Dog:Jack:16
Cat:Cat:Joe:20
Dog can remember for 5 minutes
Dog will protect owner
Dog can learn:
Cat can remember for 16 hours
Cat won't protect owner
Cat can climb
Man :Ravindra:Age:40
I can climb:Man
I can think:Man
I can learn:Man
I can apply:Man

Key notes:

  1. Animal is an abstract class with shared attributes: name and lifeExpectancy and abstract methods: remember() and protectOwner(). Dog and Cat are Animals that have implemented the remember() and protectOwner() methods.
  2. Cat can climb() but Dog cannot. Dog can think() but Cat cannot. These specific capabilities are added to Cat and Dog by implementation.
  3. Man is not an Animal but he can Think , Learn, Apply, and Climb.
  4. Cat is not a Man but it can Climb.
  5. Dog is not a Man but it can Learn
  6. Man is neither a Cat nor a Dog but can have some of the capabilities of the latter two without extending Animal, Cat, or Dog. This is done with Interfaces.
  7. Even though Animal is an abstract class, it has a constructor, unlike an interface.

TL;DR:
Unrelated classes can have capabilities through interfaces, but related classes change the behaviour through extension of base classes.

Consider using abstract classes if…

  1. You want to share code among several closely related classes.
  2. You expect that classes that extend your abstract class have many common methods or fields, or require access modifiers other than public (such as protected and private).
  3. You want to declare non-static or non-final fields.

Consider using interfaces if…

  1. You expect that unrelated classes would implement your interface. For example, many unrelated objects can implement the Serializable interface.
  2. You want to specify the behaviour of a particular data type but are not concerned about who implements its behaviour.
  3. You want to take advantage of multiple inheritance of type

Static Inheritance

Static method can be inherited similar to normal methods, however unlike normal methods it is impossible to create “abstract” methods in order to force static method overriding. Writing a method with the same signature as a static method in a super class appears to be a form of overriding, but really this simply creates a new function hides the other.

public class BaseClass {
public static int num = 5;
public static void sayHello() {
System.out.println("Hello");
}
public static void main(String[] args) {
BaseClass.sayHello();
System.out.println("BaseClass's num: " + BaseClass.num);
SubClass.sayHello();
//This will be different than the above statement's output, since it runs
//A different method
SubClass.sayHello(true);
StaticOverride.sayHello();
System.out.println("StaticOverride's num: " + StaticOverride.num);
}
}
public class SubClass extends BaseClass {
//Inherits the sayHello function, but does not override it
public static void sayHello(boolean test) {
System.out.println("Hey");
}
}
public static class StaticOverride extends BaseClass {
//Hides the num field from BaseClass
//You can even change the type, since this doesn't affect the signature
public static String num = "test";
//Cannot use @Override annotation, since this is static
//This overrides the sayHello method from BaseClass
public static void sayHello() {
System.out.println("Static says Hi");
}
}

Running any of these classes produces the output:

Hello
BaseClass's num: 5
Hello
Hey
Static says Hi
StaticOverride's num: test

Note that unlike normal inheritance, in static inheritance methods are not hidden. You can always call the base sayHello method by using BaseClass.sayHello(). But classes do inherit static methods if no methods with the same signature are found in the subclass. If two method’s signatures vary, both methods can be run from the subclass, even if the name is the same.

Static fields hide each other in a similar way.

Leave a Comment