In philosophical terms, Abstraction is the process of understanding behaviour or structure of a real-life object. For example: When we think about a car, we remember an abstract concept of a car and its functionalities. This is why we can recognize object like car, even if it is different from any car we have seen before. In other words, we develop concepts of everyday objects through the process of abstraction, where we eliminate unnecessary details and focus on essential attributes and behaviour.
Abstraction is also a common feature in real life applications. For example:
Object-Oriented Programming (OOP) uses abstraction to separate the interface of an object from its implementation. It defines external behavior of an object and encapsulates its internal workings. This allows developers to interact with objects based on their intended behavior, without understanding the details of how the behavior is achieved.
In other words, abstraction in OOP enables the hiding of the internal details of an object from the outside world, so that the focus is on what the object does, rather than how it does it.
In Java, we implement abstraction using abstract classes and interfaces.
An abstract class is a class that is declared with the abstract keyword and may contain both abstract methods (methods without body) and non-abstract methods. Abstract classes cannot be instantiated, but can be subclassed. To implement abstraction using abstract classes:
Example Java code
abstract class Shape {
abstract void draw();
void fillColor(String color) {
System.out.println("Filling color " + color + " for shape");
}
}
class Circle extends Shape {
void draw() {
System.out.println("Drawing Circle");
}
}
class Driver {
public static void main(String[] args) {
Shape shape = new Circle();
shape.draw();
shape.fillColor("Red");
}
}
Explore this blog for more details: Abstract Class in Java
In Java, we can also achieve abstraction using interfaces, which serve as blueprints for classes and define methods that must be implemented. Interfaces are reference types that consist of constants, method signatures, default methods, and static methods.
Example Java code
public interface Shape {
int SIDES = 4;
void draw();
default void fillColor(String color) {
System.out.println("Filling color " + color + " for shape");
}
static void printSides() {
System.out.println("Number of sides: " + SIDES);
}
}
class Square implements Shape {
public void draw() {
System.out.println("Drawing Square");
}
}
class Driver {
public static void main(String[] args) {
Shape shape = new Square();
shape.draw();
shape.fillColor("Red");
Shape.printSides();
}
}
Explore this blog for more details: Interface in Java
In C++, abstraction is implemented using header files, access specifiers, and abstract classes.
An abstract class in C++ is a base class that cannot be instantiated as an object. It contains at least one pure virtual function, which is declared using the "virtual" keyword and the "= 0" notation in the function declaration. The purpose of an abstract class is to provide a common interface for its derived classes.
Example C++ code
class Shape {
public:
virtual void Draw() = 0;
};
class Circle : public Shape {
public:
void Draw() { cout << "Drawing Circle" << endl; }
};
class Square : public Shape {
public:
void Draw() { cout << "Drawing Square" << endl; }
};
int main() {
Shape *shape = new Circle();
shape->Draw();
shape = new Square();
shape->Draw();
return 0;
}
In summary, an abstract class is a blueprint for its derived classes, providing a common interface and enforcing the implementation of certain functions. We will discuss abstraction in C++ in a separate blogs later.
Object-oriented programming can be seen as an attempt to abstract both data and control. So there are two types of abstraction in OOPS: Data abstraction and Control abstraction.
In object-oriented programming, data abstraction refers to the concept of separating the properties of a data type from its implementation details. To achieve this, we define data types within a class, design operations to interact with the data type, and keep the implementation details hidden from the outside world. By doing this, the data of an object becomes inaccessible to the outside world, creating data abstraction.
If necessary, we can provide access to the object's data through specific methods, which can serve as an interface for the client. The operations that interact with the data within the class can be either kept private or made public, depending on the requirement.
class DataAbstraction {
private int data;
public DataAbstraction(int data) {
this.data = data;
}
public int getData() {
return data;
}
public void setData(int data) {
this.data = data;
}
// Other private or public methods working on data
}
Here is another example: Suppose we define an abstract data type called Dictionary, where each key is associated with a unique value and we can access values based on their keys. This data structure may be implemented using a hash table, a binary search tree, or even a simple array. For the client code, the abstract properties are the same in each case.
// Dictionary Interface
interface Dictionary {
void put(String key, Integer value);
Integer get(String key);
}
// HashTable implementation of Dictionary
class HashTable implements Dictionary {
private Map<String, Integer> map = new HashMap<>();
public void put(String key, Integer value) {
map.put(key, value);
}
public Integer get(String key) {
return map.get(key);
}
}
// BinarySearchTree implementation of Dictionary
class BinarySearchTree implements Dictionary {
private class Node {
String key;
Integer value;
Node left, right;
Node(String key, Integer value) {
this.key = key;
this.value = value;
}
}
private Node root;
public void put(String key, Integer value) {
root = put(root, key, value);
}
private Node put(Node node, String key, Integer value) {
// implementation code
}
public Integer get(String key) {
Node node = get(root, key);
return node == null ? null : node.value;
}
private Node get(Node node, String key) {
// implementation code
}
}
// Driver code
public class Main {
public static void main(String[] args) {
Dictionary dict = new HashTable();
dict.put("A", 1);
dict.put("B", 2);
System.out.println("Value of key 'B': " + dict.get("B"));
dict = new BinarySearchTree();
dict.put("A", 1);
dict.put("B", 2);
System.out.println("Value of key 'B': " + dict.get("B"));
}
}
In the above code, we defined an abstract data type Dictionary that allows associating a unique value with a key and accessing values based on their keys. Here client code interacts with the Dictionary interface and is unaware of the implementation details.
We have provided two implementations of the interface, HashTable and BinarySearchTree, each with its own implementation of the abstract operations. Here client code can switch between implementations by simply changing the object it interacts with.
In object-oriented programming, control abstraction is the abstraction of actions. Often, we don't need to provide details about all the methods of an object. In other words, when we hide the internal implementation of the different methods related to the client operation, it creates control abstraction.
interface Bank {
double getBalance();
void deposit(double amount);
void withdraw(double amount);
}
class SavingsAccount implements Bank {
private double balance;
public SavingsAccount(double balance) {
this.balance = balance;
}
@Override
public double getBalance() {
return balance;
}
@Override
public void deposit(double amount) {
balance = updateBalance(balance, amount, true);
}
@Override
public void withdraw(double amount) {
balance = updateBalance(balance, amount, false);
}
private double updateBalance(double balance, double amount, boolean deposit) {
if (deposit) {
return balance + amount;
} else {
return balance - amount;
}
}
}
class Driver {
public static void main(String[] args) {
Bank account = new SavingsAccount(1000.0);
System.out.println("Initial balance: " + account.getBalance());
account.deposit(500.0);
System.out.println("Balance after deposit: " + account.getBalance());
account.withdraw(200.0);
System.out.println("Balance after withdrawal: " + account.getBalance());
}
}
In the above example, client code can simply create an instance of the SavingsAccount class and use it to perform banking operations without having to know how operations are done internally. The updateBalance method is a private method that performs actual update of the balance and is only accessible within the class. This creates control abstraction.
We can also understand control abstraction at a lower level. A software code is a collection of methods written in a programming language. Many times, some methods are similar and repeated multiple times. The idea of control abstraction is to identify these methods and combine them into a single unit. Note: Control abstraction is one of the primary purposes of using programming languages. Computer machines understand operations at a very low level, such as moving some bits from one memory location to another location and producing the sum of two sequences of bits. Programming languages allow this to be done at a higher level.
Here is another example.
abstract class Shape {
abstract double calculateArea();
}
class Circle extends Shape {
private double radius;
Circle(double radius) {
this.radius = radius;
}
@Override
double calculateArea() {
return Math.PI * radius * radius;
}
}
class Square extends Shape {
private double side;
Square(double side) {
this.side = side;
}
@Override
double calculateArea() {
return side * side;
}
}
public class ControlAbstractionExample {
public static void main(String[] args) {
Shape shape = new Circle(5);
System.out.println("Area of Circle: " + shape.calculateArea());
shape = new Square(4);
System.out.println("Area of Square: " + shape.calculateArea());
}
}
The internal implementation of the calculateArea() method for each shape is hidden from the client, which creates control abstraction. The client only sees the calculateArea() method and doesn't need to know the specific details of how it's implemented for each shape. This allows us to change the implementation of the calculateArea() method for each shape without affecting the client code.
A well-designed code with proper use of abstraction follows the Principle of Least Astonishment: “A component of a system should behave in a way that most users will expect it to behave. The behavior should not surprise users”.
But the critical question is: How to identify and expose that expected behavior to the users? How to handle their implementation details? At this stage, the next pillar of object-oriented programming comes into the picture: encapsulation!
Difference between Abstraction and Encapsulation
In object oriented programming, Abstraction is the practice of only exposing the necessary details to the client or user. This means that when a client uses a class, they don't need to know the inner workings of the class's operations. This decouples the user of the object from its implementation, making it easier to understand and maintain. If there is a change in an operation, only the inner details of the related method need to be updated.
Enjoy learning, Enjoy OOPS!