In philosophical terms, Abstraction is a 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 internal 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 OOPS 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.
When object data is hidden from the outside world, it creates data abstraction. If required, access to that data is provided through well-defined public methods, which are often called accessors and mutators (or getters and setters). These methods act as an interface between the object and the external world, providing controlled access to the object's data.
In object-oriented programming, data abstraction provides a clear separation between the internal implementation and external usage of an object. It helps developers change the internal representation or data structure of an object without affecting the code that interacts with it. So in simple words: Data abstraction promotes code modularity and improves code maintainability.
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
}
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. For example, a software code is a collection of methods and many times, some of these 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.
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 putValue(String key, Integer value);
Integer getValue(String key);
}
// HashTable implementation of Dictionary
class HashTable implements Dictionary {
private Map<String, Integer> map = new HashMap<>();
public void putValue(String key, Integer value) {
map.put(key, value);
}
public Integer getValue(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 putValue(String key, Integer value) {
root = put(root, key, value);
}
private Node put(Node node, String key, Integer value) {
// implementation code
}
public Integer getValue(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.putValue("A", 1);
dict.putValue("B", 2);
System.out.println("Value of key 'B': " + dict.getValue("B"));
dict = new BinarySearchTree();
dict.putValue("A", 1);
dict.putValue("B", 2);
System.out.println("Value of key 'B': " + dict.getValue("B"));
}
}
Control abstraction is present in the above example because the client code interacts with the Dictionary interface, which defines the actions that can be performed (putValue and getValue). The client code doesn't need to know the internal implementation details of the methods in the HashTable and BinarySearchTree classes. In simple terms, it abstracts the control flow of these methods and provides a simple way of accessing and manipulating values in the dictionary.
Data abstraction is also present in the above example because it hides the internal implementation of the data structures. The client code only accesses the data through the defined methods, without knowing how the data is stored internally (e.g., using a HashMap or a binary search tree). In simple terms, the internal implementation of the data structure is abstracted away. This provides a higher-level view and allows flexibility in choosing different data structure implementations 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!