Background of OOP
The history of object-oriented programming dates back to the 1960s, but the paradigm gained significant popularity and widespread adoption in the 1980s and 1990s. Here's a brief timeline of OOP adoption:
Timeline of OOP adoption
Early Concepts (1960s - 1970s): In the 1960s, researchers at the Norwegian Computing Center created the Simula programming language, which introduced the concept of classes and objects, laying the foundation for OOP. Simula was designed for simulations and pioneered encapsulation and inheritance.
Smalltalk (1970s): In the 1970s, Smalltalk, developed by Alan Kay and his team at Xerox PARC, became a highly influential OOP language. It featured a fully object-oriented environment, with everything treated as an object. Smalltalk emphasized simplicity, message passing, and interactive development.
C++ Emergence (1980s): Bjarne Stroustrup's C++, developed in the 1980s, extended C with object-oriented features. It introduced classes, inheritance, polymorphism, and encapsulation, blending procedural and OOP approaches.
OOP Mainstream Adoption (1990s): During the 1990s, OOP gained popularity with the rise of Java and C#. Java, designed by James Gosling, offered platform independence and security, making OOP more accessible. Microsoft's C#, influenced by Java and C++, became a core language for .NET development.
OOP continues to evolve, with new paradigms and design patterns complementing traditional practices. It remains a powerful approach, promoting code reusability, maintainability, and modularity in software development.
Classes and Objects
In object-oriented programming (OOP), classes and objects are the building blocks that form the basis of the paradigm. They allow developers to model real-world entities and define their properties and behaviors in a structured and reusable way.
A class is a blueprint or a template that defines the structure and behavior of objects. It describes the properties (attributes) and actions (methods) that the objects of that class will possess. Think of a class as a cookie cutter and the objects as the cookies cut from that template. It provides a way to create multiple instances of objects with the same characteristics and behaviors defined by the class.
For example, consider a class named "Car". The Car class might have attributes like "color," "make," and "model," and methods like "start_engine()" and "drive()". The class itself doesn't represent any specific car; it just describes what attributes and methods a car object should have. The illustration below shows what a Car class should look like:
A car class with attributes and methods
An object is an instance of a class based on the class blueprint. It represents a real-world entity or concept that adheres to the structure defined by the class. When an object is instantiated, it gets its own attributes and can perform the actions defined by the class's methods.
Continuing with the "Car" example, when we create an object from the Car class, we might create a specific car instance like "my_car" with the color "red," make "Toyota," and model "Corolla." The "my_car" object will have its own unique values for the attributes "color," "make," and "model," but it will possess the same methods like "start_engine()" and "drive()" as defined in the Car class.
Objects are where the real action happens in OOP. They can interact with each other, communicate by passing messages, and encapsulate their data to ensure data security and integrity.
Cornerstones of Object-Oriented Programming
There are four principles of object-oriented programming:
Cornerstones of OOP
Now, we are going to explain each of these principles with examples.
It is one of the four cornerstones of object-oriented programming. Encapsulation is the concept of bundling data (attributes) and methods that operate on that data within a single unit called an object. The internal state of the object is hidden from the outside world, and access to the data is controlled through well-defined interfaces (public methods). Encapsulation promotes data security, abstraction, and code maintainability.
Consider a television remote control as a real-life example of encapsulation. It provides a user-friendly interface to operate the television without exposing the complex internal workings of the TV itself.
How does encapsulation work?
- You achieve encapsulation by declaring the class's data members (attributes) as private and providing public methods (getters and setters) to access and modify those attributes.
- The private access modifier restricts direct access to the attributes outside the class. Only the methods within the class have access to these private members.
- Public methods are the interface through which external code interacts with the object's attributes. This way, the internal implementation details are hidden, and the object's integrity is maintained.
Implementing encapsulation in Java
Let's demonstrate encapsulation in Java with a simple class representing a "Person":
- The Person class has private attributes name and age, which cannot be accessed directly from outside the class.
- We have provided public getter methods, getName() and getAge() to allow external code to read (access) the private attributes.
- The setter methods setName() and setAge() are also public, allowing external code to modify (mutate) the private attributes. The setAge() method has a validation check to ensure the age is not negative.
- The sayHello() method is a public method that can be accessed from outside the class, and it provides a simple behavior to introduce the person.
In the above example, we encapsulate the Person class by keeping its attributes private and providing public methods for accessing and modifying those attributes. This way, the internal state of the object is protected, and external code interacts with the object through controlled access points, promoting data security and abstraction.
Abstraction is the second cornerstone of OOP that allows you to simplify complex systems by representing essential features while hiding unnecessary details. It focuses on "what" an object does rather than "how" it does it. In many object-oriented programming languages, we can achieve abstraction through the concept of abstract classes and interfaces, defining common characteristics for a group of related objects.
In the context of abstraction, we focus on the essential features and functionalities of an object while hiding unnecessary details. The “volume” button on a television remote control is a good example of abstraction as it hides the internal complexity of the TV circuitry and provides a simplified interface for users to interact with the TV.
How does abstraction work?
Abstraction is achieved by using abstract classes and interfaces.
An abstract class is a class that cannot be instantiated directly, but it can have abstract (unimplemented) methods and concrete (implemented) methods.
Abstract methods are declared without a body and are intended to be implemented by the subclasses.
Concrete methods in an abstract class provide a default implementation that can be used by all subclasses.
- An interface is a collection of abstract methods and constant variables. Interfaces cannot be instantiated; instead, classes implement interfaces to provide specific behavior.
Abstract classes and interfaces allow you to define a common set of methods that must be implemented by the subclasses, promoting a consistent interface across different objects.
Implementing abstraction in Java
Let's demonstrate abstraction in Java with an abstract class representing a "Shape" and an interface representing a "Drawable" object:
- The Shape class is an abstract class. It has an abstract method area(), which must be implemented by its subclasses. It also has a concrete method displayColor() that provides a default implementation.
- The Drawable interface declares a single method draw(), which must be implemented by any class that implements the interface.
- The Circle class extends the Shape abstract class and implements the Drawable interface. It provides specific implementations for the area() method and the draw() method.
In the above example, we demonstrate abstraction by creating an abstract class Shape and an interface Drawable. The Circle class extends the Shape abstract class and implements the Drawable interface, providing specific implementations for the required methods. By using abstraction, we can represent shapes generically and define common behavior across different objects, allowing for a more straightforward and maintainable code structure.
The third cornerstone of OOP is Inheritance, which allows a class (subclass or derived class) to inherit properties and behaviors from another class (superclass or base class). Inheritance promotes code reuse and creates a hierarchical relationship between classes. The subclass can extend and specialize the functionality of its superclass, gaining access to its attributes and methods.
How does inheritance work?
- You can use the keyword “extends” in Java language to establish an inheritance relationship between the superclass and the subclass. The subclass follows the superclass, and it can access the superclass's non-private members directly.
- The subclass can override the superclass's methods to provide its specific implementation (method overriding).
- Whenever we come across an IS-A relationship between objects, we can use inheritance.
- Inheritance takes into account the superclass's access modifiers (e.g., public, private, protected). Members of the superclass who are public become public members of the subclass. Private members are not directly available in the subclass, but they can be accessed indirectly via the superclass's public or protected methods.
Types of inheritance
Different object-oriented programming languages support different types of inheritance, however, for this article, we will focus on the types of inheritance supported by Java language.
There are generally five types of inheritance which are explained below.
A subclass extends only one superclass. This is the standard type of inheritance supported in Java. Below is an example of single inheritance:
Let's demonstrate the implementation details of the above example:
In the above example, we demonstrate single inheritance by creating a Dog class that extends the Animal class. The Dog class inherits the eat() method from Animal. Through inheritance, we promote code reuse and build a hierarchical structure of classes, making the code more organized and maintainable.
Multiple Inheritance (Interface-based)
A class can implement multiple interfaces, allowing it to inherit behavior from multiple sources. Although Java doesn't support multiple inheritance with classes (i.e., extending multiple classes), it achieves multiple inheritance through interfaces. Below is one such example:
Interface-based inheritance (Multiple inheritance)
Let's show you how to implement the above scenario, where we have two interfaces, Swimmable and Flyable, and a class Duck that implements both interfaces to achieve multiple inheritance.
In the above example, the Duck class implements both the Swimmable and Flyable interfaces. By doing so, the Duck class inherits behavior from both interfaces, allowing it to perform swimming and flying actions. This demonstrates the concept of multiple inheritance using interfaces in Java.
Using interfaces for multiple inheritance ensures a clear separation of concerns and avoids the diamond problem (ambiguity that can occur with multiple inheritance in some languages). Developers can implement different interfaces to provide specific behavior to a class while maintaining the benefits of reusability.
A subclass inherits from another subclass, creating a chain of inheritance is called multilevel inheritance. Let's create a scenario with three classes: Animal, Mammal, and Dog, where Animal is the superclass of Mammal, and Mammal is the superclass of Dog.
Here is the implementation of the above class diagram.
In the above example, the Dog class inherits behavior from the Animal and Mammal classes, forming a multilevel inheritance relationship. Multilevel inheritance is useful when you have classes that exhibit a hierarchical relationship, with each subclass specializing and adding new behavior to the existing hierarchy. It allows you to create a well-organized code structure by reusing code from the parent classes, promoting code reusability and maintainability.
In hierarchical inheritance, multiple subclasses inherit from the same superclass, forming a hierarchical structure. As you can see below, two subclasses, Car and Motorcycle, both inherit from the Vehicle class.
The implementation of the above design is shown below:
In the above example, both the subclasses inherit behavior from the common superclass, forming a hierarchical inheritance relationship. Hierarchical inheritance is beneficial when you have multiple classes that share common attributes or behaviors from a single superclass. It allows you to establish a clear and organized class hierarchy, promoting code reusability and making the code structure easier to manage and maintain.
A combination of multiple inheritance and multilevel inheritance involving both classes and interfaces is called hybrid inheritance. In the below example, a superclass Animal, a subclass Mammal inheriting from Animal, and a class Bird implementing an interface Flyable are shown. The Bat class is a subclass of Mammal, which, in turn, is a subclass of Animal. The Bat class also implements the Flyable interface. This forms a hybrid inheritance structure, combining single inheritance (through classes) and multiple inheritance (through the interface).
The implementation of the above design is shown below:
Hybrid inheritance allows for greater flexibility and code reuse, as classes can inherit behavior from multiple sources, and interfaces provide a way to enforce common behavior across unrelated classes. However, developers must be cautious when using hybrid inheritance to avoid potential complications like the diamond problem, where multiple inheritance with classes can lead to ambiguity. Nevertheless, hybrid inheritance remains a powerful feature that enhances the expressiveness and extensibility of object-oriented programs.
Polymorphism is the fourth cornerstone of object-oriented programming that allows objects to take on multiple forms or behave differently based on the context. It enables code to work with objects of different classes in a unified way, promoting flexibility and code reusability.
Types of Polymorphism
There are two types of polymorphism.
- Compile-time Polymorphism (Method Overloading)
- Runtime Polymorphism (Method Overriding)
Compile-time Polymorphism (Method Overloading)
Method overloading allows a class to have multiple methods with the same name but different parameters. The compiler determines the appropriate method to call based on the number or type of arguments passed during compile-time. Method overloading provides the ability to use descriptive method names for similar operations.
Let's check out an example implementation of method overloading in Java:
In the above example, the MathOperations class has multiple add() methods with different parameter types (int, double, and String). During compile-time, the compiler determines which version of the add() method to call based on the arguments provided.
Runtime Polymorphism (Method Overriding)
Method overriding occurs when a subclass provides a specific implementation for a method that is already defined in its superclass. The decision of which method to call is made during runtime based on the actual type of the object rather than the reference type.
Runtime polymorphism allows for dynamic dispatch, where the correct method implementation is determined based on the actual object's type.
Let's check out an example implementation of method overriding in Java:
In the above example, we have a hierarchy of classes with the Animal class as the superclass and Dog and Cat as its subclasses. Both Dog and Cat override the sound() method defined in the Animal class with their specific sound implementations. When we create objects of Dog or Cat and call the sound() method, the decision of which version of the method to execute is made during runtime based on the actual object's type. This is an example of dynamic dispatch, which is a feature of runtime polymorphism.
In this article, we presented a primer on object-oriented programming. We went briefly through the history of OOP and then explained its building blocks i.e., classes and objects, which allow developers to model real-world entities. We then provided a detailed walkthrough of the four principles (cornerstones) of OOP with real-life examples and implementation code for greater understanding.
This primer will help to brush up your knowledge of OOP, which is necessary for preparing yourself for the OOD/LLD interview. We hope you enjoyed reading this primer on OOP as much as we did writing it.