Java Object-Oriented Programming: Polymorphism, Abstraction and Interfaces

This blog explains the concepts of polymorphism, abstraction and interfaces in Java object-oriented programming with examples and code snippets.

1. Introduction

Welcome to this blog on Java Object-Oriented Programming: Polymorphism, Abstraction and Interfaces. In this blog, you will learn how to use these three important concepts in Java OOP to create more flexible, reusable, and maintainable code.

Object-oriented programming (OOP) is a paradigm that organizes data and behavior into objects that interact with each other. OOP allows you to model real-world entities and scenarios in your code, making it easier to understand and modify.

Polymorphism, abstraction, and interfaces are three key features of OOP that enable you to write code that can adapt to different situations and requirements. They also help you to achieve code reusability and avoid code duplication.

By the end of this blog, you will be able to:

  • Explain what polymorphism, abstraction, and interfaces are and why they are useful in Java OOP.
  • Differentiate between the types of polymorphism and how to implement them in Java.
  • Use abstract classes and methods to create abstract layers in your code.
  • Define and implement interfaces to specify the behavior of your classes.
  • Apply polymorphism, abstraction, and interfaces to create practical examples of Java OOP.

Are you ready to dive into the world of Java OOP? Let’s get started!

2. What is Object-Oriented Programming?

Object-oriented programming (OOP) is a programming paradigm that organizes data and behavior into objects that interact with each other. OOP allows you to model real-world entities and scenarios in your code, making it easier to understand and modify.

An object is a software entity that has attributes and methods. Attributes are the data or properties of the object, such as its name, color, size, etc. Methods are the actions or behaviors of the object, such as what it can do or how it can change.

For example, you can create an object that represents a car. The car object can have attributes such as model, color, speed, etc. The car object can also have methods such as start, stop, accelerate, brake, etc.

To create an object, you need to define a class. A class is a blueprint or template that specifies the attributes and methods of the objects of that type. You can create multiple objects of the same class, each with its own values for the attributes and methods.

For example, you can define a class called Car that specifies the attributes and methods of a car object. Then you can create multiple car objects of the Car class, each with different values for the model, color, speed, etc.

OOP has many benefits, such as:

  • It allows you to create modular and reusable code, as you can define a class once and create multiple objects of that class.
  • It allows you to create hierarchical and complex systems, as you can inherit attributes and methods from other classes and override them as needed.
  • It allows you to encapsulate data and behavior, as you can hide the internal details of an object and expose only the relevant information and functionality.
  • It allows you to achieve polymorphism, abstraction, and interfaces, which are the main topics of this blog.

Do you want to learn more about OOP and how to implement it in Java? Let’s move on to the next section!

3. What is Polymorphism in Java?

Polymorphism is one of the key features of object-oriented programming. It means that an object can take different forms or behaviors depending on the context. Polymorphism allows you to write code that can handle different types of objects in a generic way, without knowing their exact class at compile time.

Polymorphism can be achieved in Java in two ways: through inheritance and through interfaces. Inheritance is the mechanism that allows a subclass to inherit the attributes and methods of a superclass, and optionally override or extend them. Interfaces are contracts that specify the behavior of a class, without providing any implementation details.

Both inheritance and interfaces allow you to create a reference variable of a superclass or an interface type, and assign it to an object of a subclass or an implementing class. This is called upcasting, and it enables polymorphism. Upcasting allows you to access the methods of the superclass or the interface through the reference variable, regardless of the actual object type.

For example, you can create a reference variable of type Animal, and assign it to an object of type Dog. Then you can call the method speak() on the reference variable, and it will invoke the speak() method of the Dog class, not the Animal class. This is because the actual object type is determined at runtime, not at compile time. This is called dynamic binding or late binding, and it is the essence of polymorphism.

Polymorphism has many advantages, such as:

  • It allows you to write more flexible and adaptable code, as you can handle different types of objects with the same code.
  • It allows you to achieve code reusability and avoid code duplication, as you can use the same methods of the superclass or the interface for different subclasses or implementing classes.
  • It allows you to achieve loose coupling and high cohesion, as you can reduce the dependencies and increase the modularity of your code.

Do you want to learn more about the types and examples of polymorphism in Java? Let’s move on to the next section!

3.1. Types of Polymorphism

There are two types of polymorphism in Java: compile-time polymorphism and run-time polymorphism. Let’s see what they are and how they differ.

Compile-time polymorphism is also known as static polymorphism or method overloading. It occurs when a class has two or more methods with the same name but different parameters. The compiler decides which method to call based on the number, type, and order of the arguments passed to the method.

For example, you can have a class called Calculator that has two methods called add, one that takes two integers and one that takes two doubles. The compiler will call the appropriate method depending on whether you pass two integers or two doubles to the add method.

class Calculator {
  // method overloading
  public int add(int a, int b) {
    return a + b;
  }

  public double add(double a, double b) {
    return a + b;
  }
}

Calculator calc = new Calculator();
int result1 = calc.add(10, 20); // calls the first add method
double result2 = calc.add(10.5, 20.5); // calls the second add method

Run-time polymorphism is also known as dynamic polymorphism or method overriding. It occurs when a subclass overrides a method of a superclass with the same name and parameters. The compiler decides which method to call based on the reference type, but the actual method execution depends on the object type at run time.

For example, you can have a superclass called Animal that has a method called speak, and a subclass called Dog that overrides the speak method. The compiler will call the speak method based on the reference type, which is Animal, but the actual method execution depends on the object type at run time, which is Dog.

class Animal {
  // superclass method
  public void speak() {
    System.out.println("Animal is speaking");
  }
}

class Dog extends Animal {
  // subclass method overriding superclass method
  public void speak() {
    System.out.println("Dog is barking");
  }
}

Animal animal = new Dog(); // upcasting
animal.speak(); // calls the speak method of the Dog class at run time

As you can see, compile-time polymorphism and run-time polymorphism are different ways of achieving polymorphism in Java. They both allow you to write code that can handle different types of objects in a generic way, without knowing their exact class at compile time.

Do you want to see some practical examples of polymorphism in Java? Let’s move on to the next section!

3.2. Examples of Polymorphism

In this section, you will see some examples of polymorphism in Java. You will learn how to use inheritance and interfaces to achieve polymorphism, and how to use the instanceof operator to check the actual object type at run time.

Let’s start with an example of inheritance-based polymorphism. Suppose you have a superclass called Shape that has a method called draw. You also have three subclasses called Circle, Square, and Triangle that inherit from Shape and override the draw method. You can create an array of Shape objects and assign them to different subclass objects. Then you can loop through the array and call the draw method on each element. The output will depend on the actual object type at run time, not the reference type at compile time.

class Shape {
  // superclass method
  public void draw() {
    System.out.println("Drawing a shape");
  }
}

class Circle extends Shape {
  // subclass method overriding superclass method
  public void draw() {
    System.out.println("Drawing a circle");
  }
}

class Square extends Shape {
  // subclass method overriding superclass method
  public void draw() {
    System.out.println("Drawing a square");
  }
}

class Triangle extends Shape {
  // subclass method overriding superclass method
  public void draw() {
    System.out.println("Drawing a triangle");
  }
}

public class Main {
  public static void main(String[] args) {
    // creating an array of Shape objects
    Shape[] shapes = new Shape[3];
    // assigning different subclass objects to the array elements
    shapes[0] = new Circle();
    shapes[1] = new Square();
    shapes[2] = new Triangle();
    // looping through the array and calling the draw method on each element
    for (Shape shape : shapes) {
      shape.draw(); // polymorphism
    }
  }
}

The output of the above code is:

Drawing a circle
Drawing a square
Drawing a triangle

As you can see, the draw method of the subclass objects is invoked at run time, not the draw method of the superclass object. This is an example of run-time polymorphism or method overriding.

Now let’s see an example of interface-based polymorphism. Suppose you have an interface called Animal that has a method called speak. You also have three classes called Dog, Cat, and Cow that implement the Animal interface and provide their own implementation of the speak method. You can create an array of Animal objects and assign them to different implementing class objects. Then you can loop through the array and call the speak method on each element. The output will depend on the actual object type at run time, not the reference type at compile time.

interface Animal {
  // interface method
  public void speak();
}

class Dog implements Animal {
  // implementing class method
  public void speak() {
    System.out.println("Dog says woof");
  }
}

class Cat implements Animal {
  // implementing class method
  public void speak() {
    System.out.println("Cat says meow");
  }
}

class Cow implements Animal {
  // implementing class method
  public void speak() {
    System.out.println("Cow says moo");
  }
}

public class Main {
  public static void main(String[] args) {
    // creating an array of Animal objects
    Animal[] animals = new Animal[3];
    // assigning different implementing class objects to the array elements
    animals[0] = new Dog();
    animals[1] = new Cat();
    animals[2] = new Cow();
    // looping through the array and calling the speak method on each element
    for (Animal animal : animals) {
      animal.speak(); // polymorphism
    }
  }
}

The output of the above code is:

Dog says woof
Cat says meow
Cow says moo

As you can see, the speak method of the implementing class objects is invoked at run time, not the speak method of the interface object. This is also an example of run-time polymorphism or method overriding.

Sometimes, you may want to check the actual object type at run time, and perform different actions based on the type. You can use the instanceof operator to do this. The instanceof operator returns true if the object is an instance of the specified class or interface, and false otherwise.

For example, you can use the instanceof operator to check if an Animal object is a Dog, a Cat, or a Cow, and print a different message for each type.

public class Main {
  public static void main(String[] args) {
    // creating an array of Animal objects
    Animal[] animals = new Animal[3];
    // assigning different implementing class objects to the array elements
    animals[0] = new Dog();
    animals[1] = new Cat();
    animals[2] = new Cow();
    // looping through the array and checking the object type using instanceof operator
    for (Animal animal : animals) {
      if (animal instanceof Dog) {
        System.out.println("This is a dog");
      } else if (animal instanceof Cat) {
        System.out.println("This is a cat");
      } else if (animal instanceof Cow) {
        System.out.println("This is a cow");
      }
    }
  }
}

The output of the above code is:

This is a dog
This is a cat
This is a cow

As you can see, the instanceof operator allows you to check the actual object type at run time, and perform different actions based on the type.

These are some examples of polymorphism in Java. You have learned how to use inheritance and interfaces to achieve polymorphism, and how to use the instanceof operator to check the actual object type at run time. Polymorphism is a powerful feature of OOP that allows you to write code that can handle different types of objects in a generic way, without knowing their exact class at compile time.

Do you want to learn more about abstraction and interfaces in Java? Let’s move on to the next section!

4. What is Abstraction in Java?

Abstraction is another key feature of object-oriented programming. It means that you can hide the unnecessary details of an object and expose only the essential information and functionality. Abstraction allows you to create a simpler and cleaner interface for your code, making it easier to use and maintain.

Abstraction can be achieved in Java in two ways: through abstract classes and through interfaces. Abstract classes are classes that cannot be instantiated, but can have abstract and non-abstract methods. Interfaces are contracts that specify the behavior of a class, without providing any implementation details.

Both abstract classes and interfaces allow you to create a reference variable of an abstract class or an interface type, and assign it to an object of a subclass or an implementing class. This is called upcasting, and it enables abstraction. Upcasting allows you to access the methods of the abstract class or the interface through the reference variable, regardless of the actual object type.

For example, you can create a reference variable of type Shape, which is an abstract class, and assign it to an object of type Circle, which is a subclass of Shape. Then you can call the method area() on the reference variable, and it will invoke the area() method of the Circle class, not the Shape class. This is because the Shape class has an abstract method area(), which is implemented by the Circle class.

abstract class Shape {
  // abstract method
  public abstract double area();
}

class Circle extends Shape {
  // subclass implementing abstract method
  private double radius;

  public Circle(double radius) {
    this.radius = radius;
  }

  public double area() {
    return Math.PI * radius * radius;
  }
}

public class Main {
  public static void main(String[] args) {
    // creating a reference variable of Shape type
    Shape shape;
    // assigning a Circle object to the reference variable
    shape = new Circle(10);
    // calling the area method on the reference variable
    double result = shape.area(); // abstraction
    System.out.println("The area of the circle is " + result);
  }
}

The output of the above code is:

The area of the circle is 314.1592653589793

As you can see, the area method of the Circle object is invoked at run time, not the area method of the Shape object. This is an example of abstraction or method overriding.

Abstraction has many benefits, such as:

  • It allows you to create a simpler and cleaner interface for your code, as you can hide the unnecessary details and expose only the essential information and functionality.
  • It allows you to achieve code reusability and avoid code duplication, as you can use the same abstract methods or interface methods for different subclasses or implementing classes.
  • It allows you to achieve loose coupling and high cohesion, as you can reduce the dependencies and increase the modularity of your code.

Do you want to learn more about abstract classes and methods in Java? Let’s move on to the next section!

4.1. Abstract Classes and Methods

An abstract class is a class that cannot be instantiated, but can have abstract and non-abstract methods. An abstract method is a method that has no body and is declared with the keyword abstract. A subclass that inherits from an abstract class must implement all the abstract methods of the superclass, or else it must also be declared as abstract.

An abstract class is useful when you want to create a general class that defines the common attributes and methods of a group of subclasses, but you don’t want to create objects of the general class. You can use an abstract class as a template or a blueprint for the subclasses, and let them provide the specific implementation of the abstract methods.

To create an abstract class, you need to use the keyword abstract before the class name. To create an abstract method, you need to use the keyword abstract before the method name, and end the declaration with a semicolon. For example, you can create an abstract class called Shape that has an abstract method called area.

abstract class Shape {
  // abstract method
  public abstract double area();
}

To create a subclass that inherits from an abstract class, you need to use the keyword extends after the subclass name. To implement an abstract method, you need to provide the body of the method in the subclass. For example, you can create a subclass called Circle that inherits from Shape and implements the area method.

class Circle extends Shape {
  // subclass implementing abstract method
  private double radius;

  public Circle(double radius) {
    this.radius = radius;
  }

  public double area() {
    return Math.PI * radius * radius;
  }
}

To create an object of a subclass that inherits from an abstract class, you need to use the keyword new followed by the subclass name and the constructor parameters. You can also create a reference variable of the abstract class type and assign it to the subclass object. This is called upcasting, and it enables abstraction. For example, you can create an object of the Circle class and assign it to a reference variable of the Shape class.

Shape shape = new Circle(10); // upcasting

To call a method on an object that is upcasted to an abstract class type, you need to use the dot operator followed by the method name and the arguments. The method that is invoked depends on the actual object type at run time, not the reference type at compile time. This is called dynamic binding or late binding, and it is the essence of abstraction. For example, you can call the area method on the shape object, and it will invoke the area method of the Circle class, not the Shape class.

double result = shape.area(); // abstraction

These are the basic steps to create and use abstract classes and methods in Java. You have learned how to create an abstract class and an abstract method, how to create a subclass that inherits from an abstract class and implements the abstract method, how to create an object of the subclass and upcast it to the abstract class type, and how to call a method on the upcasted object and achieve abstraction.

Do you want to see some examples of abstraction using abstract classes and methods in Java? Let’s move on to the next section!

4.2. Examples of Abstraction

In this section, you will see some examples of abstraction using abstract classes and methods in Java. You will learn how to use abstract classes and methods to create abstract layers in your code, and how to use them to achieve polymorphism and code reusability.

Let’s start with an example of using abstract classes and methods to create abstract layers in your code. Suppose you want to create a program that simulates a bank account system. You can create an abstract class called Account that defines the common attributes and methods of a bank account, such as balance, deposit, withdraw, and transfer. You can also create two subclasses called SavingsAccount and CheckingAccount that inherit from Account and provide their own implementation of the methods. You can also create a class called Bank that has an array of Account objects and provides methods to create and manage accounts.

abstract class Account {
  // common attribute
  protected double balance;

  // constructor
  public Account(double balance) {
    this.balance = balance;
  }

  // common method
  public double getBalance() {
    return balance;
  }

  // abstract methods
  public abstract void deposit(double amount);
  public abstract void withdraw(double amount);
  public abstract void transfer(Account other, double amount);
}

class SavingsAccount extends Account {
  // subclass constructor
  public SavingsAccount(double balance) {
    super(balance); // calling superclass constructor
  }

  // subclass implementing abstract methods
  public void deposit(double amount) {
    balance += amount;
  }

  public void withdraw(double amount) {
    if (balance >= amount) {
      balance -= amount;
    } else {
      System.out.println("Insufficient funds");
    }
  }

  public void transfer(Account other, double amount) {
    if (balance >= amount) {
      balance -= amount;
      other.deposit(amount);
    } else {
      System.out.println("Insufficient funds");
    }
  }
}

class CheckingAccount extends Account {
  // subclass constructor
  public CheckingAccount(double balance) {
    super(balance); // calling superclass constructor
  }

  // subclass implementing abstract methods
  public void deposit(double amount) {
    balance += amount;
  }

  public void withdraw(double amount) {
    balance -= amount;
  }

  public void transfer(Account other, double amount) {
    balance -= amount;
    other.deposit(amount);
  }
}

class Bank {
  // array of Account objects
  private Account[] accounts;
  // number of accounts
  private int numberOfAccounts;

  // constructor
  public Bank(int size) {
    accounts = new Account[size];
    numberOfAccounts = 0;
  }

  // method to create a new account
  public void createAccount(String type, double balance) {
    if (numberOfAccounts < accounts.length) {
      if (type.equals("savings")) {
        accounts[numberOfAccounts] = new SavingsAccount(balance);
      } else if (type.equals("checking")) {
        accounts[numberOfAccounts] = new CheckingAccount(balance);
      } else {
        System.out.println("Invalid account type");
        return;
      }
      numberOfAccounts++;
      System.out.println("Account created successfully");
    } else {
      System.out.println("Bank is full");
    }
  }

  // method to get an account by index
  public Account getAccount(int index) {
    if (index >= 0 && index < numberOfAccounts) {
      return accounts[index];
    } else {
      System.out.println("Invalid index");
      return null;
    }
  }

  // method to print the details of all accounts
  public void printAccounts() {
    for (int i = 0; i < numberOfAccounts; i++) {
      System.out.println("Account " + i + ": balance = " + accounts[i].getBalance());
    }
  }
}

As you can see, the abstract class Account creates an abstract layer that defines the common attributes and methods of a bank account, without providing any implementation details. The subclasses SavingsAccount and CheckingAccount provide the specific implementation of the abstract methods, according to their own rules. The class Bank uses an array of Account objects to store and manage different types of accounts, without knowing their exact class at compile time.

Let's see how to use the above classes to create and manipulate bank accounts.

public class Main {
  public static void main(String[] args) {
    // creating a bank object with a capacity of 10 accounts
    Bank bank = new Bank(10);
    // creating a savings account with a balance of 1000
    bank.createAccount("savings", 1000);
    // creating a checking account with a balance of 500
    bank.createAccount("checking", 500);
    // printing the details of all accounts
    bank.printAccounts();
    // getting the first account
    Account account1 = bank.getAccount(0);
    // getting the second account
    Account account2 = bank.getAccount(1);
    // depositing 100 to the first account
    account1.deposit(100);
    // withdrawing 200 from the second account
    account2.withdraw(200);
    // transferring 300 from the first account to the second account
    account1.transfer(account2, 300);
    // printing the details of all accounts
    bank.printAccounts();
  }
}

The output of the above code is:

Account created successfully
Account created successfully
Account 0: balance = 1000.0
Account 1: balance = 500.0
Account 0: balance = 800.0
Account 1: balance = 400.0

As you can see, the methods of the Account class are invoked on the objects of the SavingsAccount and CheckingAccount classes, depending on the actual object type at run time, not the reference type at compile time. This is an example of abstraction or method overriding.

This is one example of using abstract classes and methods to create abstract layers in your code, and to achieve polymorphism and code reusability. You can use abstract classes and methods to create other abstract layers in your code, such as shapes, animals, vehicles, etc.

Do you want to learn more about interfaces and how to use them to achieve abstraction and polymorphism in Java? Let's move on to the next section!

5. What are Interfaces in Java?

An interface is another way to achieve abstraction in Java. An interface is a contract that specifies the behavior of a class, without providing any implementation details. An interface can have abstract methods and constants, but cannot have constructors, instance variables, or non-abstract methods. A class that implements an interface must provide the implementation of all the abstract methods of the interface, or else it must also be declared as abstract.

An interface is useful when you want to create a common set of methods that can be used by different classes, but you don't want to provide any default implementation or state. You can use an interface to define the behavior of a class, and let the class provide the specific implementation of the interface methods.

To create an interface, you need to use the keyword interface before the interface name. To create an abstract method, you need to use the keyword abstract before the method name, and end the declaration with a semicolon. To create a constant, you need to use the keywords public static final before the constant name, and assign a value to it. For example, you can create an interface called Animal that has an abstract method called speak and a constant called NUMBER_OF_LEGS.

interface Animal {
  // constant
  public static final int NUMBER_OF_LEGS = 4;
  // abstract method
  public abstract void speak();
}

To create a class that implements an interface, you need to use the keyword implements after the class name. To implement an abstract method, you need to provide the body of the method in the class. For example, you can create a class called Dog that implements the Animal interface and provides its own implementation of the speak method.

class Dog implements Animal {
  // class implementing interface method
  public void speak() {
    System.out.println("Dog says woof");
  }
}

To create an object of a class that implements an interface, you need to use the keyword new followed by the class name and the constructor parameters. You can also create a reference variable of the interface type and assign it to the class object. This is called upcasting, and it enables abstraction. For example, you can create an object of the Dog class and assign it to a reference variable of the Animal class.

Animal animal = new Dog(); // upcasting

To call a method on an object that is upcasted to an interface type, you need to use the dot operator followed by the method name and the arguments. The method that is invoked depends on the actual object type at run time, not the reference type at compile time. This is called dynamic binding or late binding, and it is the essence of abstraction. For example, you can call the speak method on the animal object, and it will invoke the speak method of the Dog class, not the Animal class.

animal.speak(); // abstraction

To access a constant of an interface, you need to use the interface name followed by the dot operator and the constant name. You cannot modify the value of a constant, as it is final and immutable. For example, you can access the NUMBER_OF_LEGS constant of the Animal interface, and print its value.

System.out.println(Animal.NUMBER_OF_LEGS); // 4

These are the basic steps to create and use interfaces in Java. You have learned how to create an interface and an abstract method, how to create a class that implements an interface and provides the implementation of the abstract method, how to create an object of the class and upcast it to the interface type, how to call a method on the upcasted object and achieve abstraction, and how to access a constant of the interface.

Do you want to see some examples of abstraction using interfaces in Java? Let's move on to the next section!

5.1. How to Define and Implement an Interface

In this section, you will learn how to define and implement an interface in Java. An interface is a contract that specifies the behavior of a class, without providing any implementation details. An interface can have abstract methods and constants, but cannot have constructors, instance variables, or non-abstract methods. A class that implements an interface must provide the implementation of all the abstract methods of the interface, or else it must also be declared as abstract.

To define an interface, you need to use the keyword interface before the interface name. To declare an abstract method, you need to use the keyword abstract before the method name, and end the declaration with a semicolon. To declare a constant, you need to use the keywords public static final before the constant name, and assign a value to it. For example, you can define an interface called Animal that has an abstract method called speak and a constant called NUMBER_OF_LEGS.

interface Animal {
  // constant
  public static final int NUMBER_OF_LEGS = 4;
  // abstract method
  public abstract void speak();
}

To implement an interface, you need to use the keyword implements after the class name. To provide the implementation of an abstract method, you need to provide the body of the method in the class. For example, you can implement the Animal interface in a class called Dog and provide your own implementation of the speak method.

class Dog implements Animal {
  // class implementing interface method
  public void speak() {
    System.out.println("Dog says woof");
  }
}

To create an object of a class that implements an interface, you need to use the keyword new followed by the class name and the constructor parameters. You can also create a reference variable of the interface type and assign it to the class object. This is called upcasting, and it enables abstraction. For example, you can create an object of the Dog class and assign it to a reference variable of the Animal type.

Animal animal = new Dog(); // upcasting

To call a method on an object that is upcasted to an interface type, you need to use the dot operator followed by the method name and the arguments. The method that is invoked depends on the actual object type at run time, not the reference type at compile time. This is called dynamic binding or late binding, and it is the essence of abstraction. For example, you can call the speak method on the animal object, and it will invoke the speak method of the Dog class, not the Animal class.

animal.speak(); // abstraction

To access a constant of an interface, you need to use the interface name followed by the dot operator and the constant name. You cannot modify the value of a constant, as it is final and immutable. For example, you can access the NUMBER_OF_LEGS constant of the Animal interface, and print its value.

System.out.println(Animal.NUMBER_OF_LEGS); // 4

These are the basic steps to define and implement an interface in Java. You have learned how to create an interface and an abstract method, how to create a class that implements an interface and provides the implementation of the abstract method, how to create an object of the class and upcast it to the interface type, how to call a method on the upcasted object and achieve abstraction, and how to access a constant of the interface.

Do you want to see some examples of abstraction using interfaces in Java? Let's move on to the next section!

5.2. Examples of Interfaces

In this section, you will see some examples of interfaces in Java. You will learn how to use interfaces to create abstract layers in your code, and how to use them to achieve polymorphism and code reusability.

Let's start with an example of using interfaces to create abstract layers in your code. Suppose you want to create a program that simulates a music player system. You can create an interface called Playable that defines the common behavior of a music player, such as play, pause, stop, and next. You can also create two classes called CDPlayer and MP3Player that implement the Playable interface and provide their own implementation of the interface methods. You can also create a class called MusicPlayer that has a reference variable of the Playable type and provides methods to control the music player.

interface Playable {
  // interface methods
  public abstract void play();
  public abstract void pause();
  public abstract void stop();
  public abstract void next();
}

class CDPlayer implements Playable {
  // class implementing interface methods
  private int track;

  public CDPlayer() {
    track = 1;
  }

  public void play() {
    System.out.println("Playing track " + track);
  }

  public void pause() {
    System.out.println("Pausing track " + track);
  }

  public void stop() {
    System.out.println("Stopping track " + track);
  }

  public void next() {
    track++;
    System.out.println("Skipping to track " + track);
  }
}

class MP3Player implements Playable {
  // class implementing interface methods
  private String song;

  public MP3Player(String song) {
    this.song = song;
  }

  public void play() {
    System.out.println("Playing song " + song);
  }

  public void pause() {
    System.out.println("Pausing song " + song);
  }

  public void stop() {
    System.out.println("Stopping song " + song);
  }

  public void next() {
    System.out.println("No next song available");
  }
}

class MusicPlayer {
  // reference variable of Playable type
  private Playable playable;

  // constructor
  public MusicPlayer(Playable playable) {
    this.playable = playable;
  }

  // methods to control the music player
  public void play() {
    playable.play();
  }

  public void pause() {
    playable.pause();
  }

  public void stop() {
    playable.stop();
  }

  public void next() {
    playable.next();
  }
}

As you can see, the interface Playable creates an abstract layer that defines the common behavior of a music player, without providing any implementation details. The classes CDPlayer and MP3Player provide the specific implementation of the interface methods, according to their own rules. The class MusicPlayer uses a reference variable of the Playable type to store and control different types of music players, without knowing their exact class at compile time.

Let's see how to use the above classes to create and manipulate music players.

public class Main {
  public static void main(String[] args) {
    // creating a CD player object
    CDPlayer cdPlayer = new CDPlayer();
    // creating a MP3 player object
    MP3Player mp3Player = new MP3Player("Hello");
    // creating a music player object with a CD player
    MusicPlayer musicPlayer = new MusicPlayer(cdPlayer);
    // playing the music player
    musicPlayer.play();
    // pausing the music player
    musicPlayer.pause();
    // skipping to the next track
    musicPlayer.next();
    // stopping the music player
    musicPlayer.stop();
    // changing the music player to a MP3 player
    musicPlayer = new MusicPlayer(mp3Player);
    // playing the music player
    musicPlayer.play();
    // pausing the music player
    musicPlayer.pause();
    // skipping to the next song
    musicPlayer.next();
    // stopping the music player
    musicPlayer.stop();
  }
}

The output of the above code is:

Playing track 1
Pausing track 1
Skipping to track 2
Stopping track 2
Playing song Hello
Pausing song Hello
No next song available
Stopping song Hello

As you can see, the methods of the Playable interface are invoked on the objects of the CDPlayer and MP3Player classes, depending on the actual object type at run time, not the reference type at compile time. This is an example of abstraction or method overriding.

This is one example of using interfaces to create abstract layers in your code, and to achieve polymorphism and code reusability. You can use interfaces to create other abstract layers in your code, such as shapes, animals, vehicles, etc.

Do you want to learn more about the types and examples of polymorphism in Java? Let's move on to the next section!

6. Conclusion

In this blog, you have learned about the concepts of polymorphism, abstraction, and interfaces in Java object-oriented programming. You have seen how to use these features to create more flexible, reusable, and maintainable code. You have also seen some examples of using abstract classes, methods, and interfaces to create abstract layers in your code, and how to use them to achieve polymorphism and code reusability.

Here are some key points to remember:

  • Polymorphism means that an object can take different forms or behaviors depending on the context. Polymorphism allows you to write code that can handle different types of objects in a generic way, without knowing their exact class at compile time.
  • Abstraction means that you can hide the implementation details of an object and expose only the relevant information and functionality. Abstraction allows you to create abstract layers in your code that define the common behavior of a group of objects, without providing any specific implementation.
  • Interfaces are contracts that specify the behavior of a class, without providing any implementation details. Interfaces allow you to define the behavior of a class, and let the class provide the specific implementation of the interface methods.
  • Both abstract classes and interfaces can be used to create abstract layers in your code, and to achieve polymorphism and code reusability. The main difference is that abstract classes can have constructors, instance variables, and non-abstract methods, while interfaces cannot.
  • To achieve polymorphism, you can use upcasting and dynamic binding. Upcasting means that you can create a reference variable of a superclass or an interface type, and assign it to an object of a subclass or an implementing class. Dynamic binding means that the method that is invoked on the upcasted object depends on the actual object type at run time, not the reference type at compile time.

We hope you enjoyed this blog and learned something new and useful. If you have any questions or feedback, please feel free to leave a comment below. Thank you for reading!

Leave a Reply

Your email address will not be published. Required fields are marked *