What is OOP?
OOP (Object-Oriented Programming) is a way of writing computer programs by thinking of everything as objects, just like in real life. Imagine you have a toy box full of different toys. Each toy has its own shape, color, and things it can do. In OOP, we treat things in a program like those toys, each one has its own properties (like color and size) and actions (like making a sound or moving).
Imagine we have a "car" in game. A car has color (red, blue, etc.) and speed (fast, slow), a car can drive and brake, a sports car can be a special type of car that is even faster. OOP helps us build programs in a way that makes everything easy to organize and reuse, just like sorting your toys into different boxes.
We use OOP because it makes coding more organized, reusable, and easier to manage. It may still sound a bit confusing at first, but we will go through the four main ideas of OOP below along with examples to make it easier to understand.
- ✅ Inheritance
- ✅ Abstraction
- ✅ Polymorphism
- ✅ Encapsulation
The main difference between OOP and non-OOP
The non-OOP is also called Procedural Programming. Let's take a look at the difference.
1. Structure
OOP: Code is organized into objects that bundle data (variables) and behavior (methods) together.
Non-OOP: Code is organized into functions that operate on data, without bundling them together.
2. Code Reusability
OOP: Uses inheritance, allowing new classes to reuse existing code. You don't have to repeatedly build from the scratch.
Non-OOP: Requires copying and pasting code or writing separate functions, making reuse harder.
3. Data Handling
OOP: Uses encapsulation, keeping data inside objects and controlling access.
Non-OOP: Data is often stored in global variables, which can be changed by any function, increasing the risk of errors.
4. Flexibility
OOP: Uses polymorphism, allowing different objects to behave differently while following the same structure.
Non-OOP: Functions must be rewritten or modified separately for different behaviors.
Non-OOP (Procedural) Approach:
// Procedural way: Using functions and separate data
String carColor = "Red";
int carSpeed = 100;
void drive() {
System.out.println("The car is driving at " + carSpeed + " km/h.");
}
OOP Approach:
// OOP way: Using a class to bundle data and behavior
class Car {
String color;
int speed;
void drive() {
System.out.println("The " + color + " car is driving at " + speed + " km/h.");
}
}
With OOP, we can create multiple Car objects with different properties, making the code scalable and reusable. In conclusion, Non-OOP is simpler and works well for small programs, while OOP is better for large, complex programs because it helps organize, reuse, and maintain code efficiently.
Now let's take a look at the four main ideas of OOP.
Inheritance
Inheritance is a fundamental concept in OOP that allows a class (child class or subclass) to acquire the properties and behaviors of another class (parent class or superclass). It promotes code reusability and makes programs more structured and maintainable.
1. In Java, the extends keyword is used to inherit from a class
class Stricker {
void punch() {
System.out.println("Punch...");
}
}
class KickBoxer extends Stricker {
void kick() {
System.out.println("Kick...");
}
}
KickBoxer.punch(); // Punch...
KickBoxer.kick(); // Kick...
The KickBoxer class inherits the punch() method from Stricker and also defines its own method kick().
2. The super keyword is used to refer to the parent class
The super keyword is used to explicitly refer to the direct parent class in Java. It serves two main purposes:
-
- Accessing fields and methods of the parent class
-
- Calling the parent class's constructor (must be the first line in the child’s constructor)
For example,
class Stricker {
void punch() {
System.out.println("Punch...");
}
}
class KickBoxer extends Stricker {
void punchAndKick() {
super.punch();
System.out.println("Kick...");
}
}
KickBoxer.punchAndKick() // Punch...\nKick...
When a child class is instantiated, the parent class’s constructor is automatically called first. However, this applies only to the default (no-argument) constructor of the parent class.
If the parent class has a non-default constructor (one with parameters), we must explicitly call it using super(arguments).
class Parent {
Parent() { // Default constructor
System.out.println("Parent...");
}
}
class Child extends Parent {
Child() { // Default constructor
System.out.println("Child...");
}
}
public class Main {
public static void main(String[] args) {
Child obj = new Child(); // Creating an instance of Child
}
}
// Output
// Parent...
// Child...
3. Java’s Supreme Class: Object
In Java, every class implicitly extends the Object class, even if it is not explicitly mentioned. This makes Object the highest superclass in Java, serving as the root of the class hierarchy. Since all Java classes inherit from Object, they automatically have access to its built-in methods, such as toString(), equals(), and hashCode().
Benefit of inheritance
-
- Code Reusability – Avoids writing duplicate code.
-
- Improved Maintainability – Changes in the parent class automatically apply to child classes.
-
- Logical Organization – Helps structure code in a hierarchical manner.
However, Java does not support multiple inheritance (inheriting from multiple classes) to prevent complexity and ambiguity. Instead, interfaces can be used.
Abstraction
Abstraction is the process of hiding implementation details from the user and only showing essential features. It helps in reducing complexity by focusing on what an object does rather than how it does it. In Java, abstraction is achieved using abstract classes and interfaces.
1. Using Abstract classes
An abstract class is a class that cannot be instantiated (you cannot create an object of it). It may contain abstract methods (methods without implementation) that must be implemented by subclasses.
abstract class Vehicle { // Abstract class
abstract void start(); // Abstract method (no implementation)
void stop() { // Concrete method (has implementation)
System.out.println("Vehicle is stopping...");
}
}
class Car extends Vehicle {
@Override
void start() {
System.out.println("Car is starting with a key...");
}
}
public class Main {
public static void main(String[] args) {
Vehicle myCar = new Car(); // Upcasting
myCar.start(); // Calls overridden method
myCar.stop(); // Calls inherited method
}
}
Vehicle is an abstract class that defines an abstract method start(), forcing subclasses to provide their own implementation. Car extends Vehicle and implements the abstract method. The stop() method in Vhicle is a concrete method that can be used by all subclasses.
2. Using interfaces
An interface is a blueprint of a class that contains only abstract methods (before Java 8) and default/static methods (from Java 8 onwards). Here is the comparison between default and static:

Unlike abstract classes, a class can implement multiple interfaces by using implements keyword. In addition, interfaces doesn't have constructors because it is not initiable.
interface Vehicle {
// Default method (can be overridden)
default void start() {
System.out.println("Vehicle is starting...");
}
// Static method (cannot be overridden)
static void service() {
System.out.println("Vehicle service in progress...");
}
}
class Car implements Vehicle {
@Override
public void start() { // Overriding the default method
System.out.println("Car is starting with a key...");
}
}
public class Main {
public static void main(String[] args) {
Car car = new Car();
car.start(); // Calls overridden method
// Calling static method from interface
Vehicle.service();
}
}
// Output
// Car is starting with a key...
// Vehicle service in progress...
Benefit of abstraction
-
- Hides Complexity – Users only interact with necessary functionality, not the internal logic.
-
- Improves Maintainability – Changing internal implementation does not affect external users.
-
- Enhances Code Reusability – Abstract classes and interfaces promote reusability across different classes.
-
- Supports Loose Coupling – Classes depend on abstract definitions, making the system more modular and flexible.
Polymorphism
The term "polymorphism" comes from Greek, meaning "many forms". It refers to the ability of objects to take on multiple forms or behaviors depending on the context. In simpler terms, polymorphism allows a single interface or method to operate on different types of data or objects. Polymorphism can be categorized into two main types: compile-time polymorphism and run-time polymorphism.
1. Compile-time (Static) Polymorphism
This is achieved through method overloading. The method to be executed is determined at compile time based on the number or types of arguments.
class MathOperations {
// Method overloading (same name, different parameters)
int add(int a, int b) {
return a + b;
}
double add(double a, double b) {
return a + b;
}
}
public class Main {
public static void main(String[] args) {
MathOperations math = new MathOperations();
System.out.println(math.add(5, 10)); // Calls int add(int, int)
System.out.println(math.add(5.5, 10.5)); // Calls double add(double, double)
}
}
2. Run-time (Dynamic) Polymorphism
This is achieved through method overloading and inheritance. The method to be executed is determined at runtime based on the object's actual type.
class Animal {
void sound() {
System.out.println("Animal makes a sound");
}
}
class Dog extends Animal {
@Override
void sound() {
System.out.println("Dog barks");
}
}
class Cat extends Animal {
@Override
void sound() {
System.out.println("Cat meows");
}
}
public class Main {
public static void main(String[] args) {
Animal myAnimal = new Animal(); // Parent class object
Animal myDog = new Dog(); // Child class object
Animal myCat = new Cat(); // Child class object
myAnimal.sound(); // "Animal makes a sound"
myDog.sound(); // "Dog barks" (Runtime polymorphism)
myCat.sound(); // "Cat meows" (Runtime polymorphism)
}
}
How Polymorphism works
- Inheritance: A subclass inherits methods and properties from a superclass.
- Method Overriding: The subclass provides its own implementation of a method already defined in the superclass.
- Upcasting: A reference of the superclass type can point to an object of the subclass. (e.g. Animal myDog = new Dog();)
- Dynamic Method Dispatch: At runtime, the JVM determines which version of the overridden method to call based on the actual object type.
Benefit of polymorphism
-
- Code Reusability: Write generic code that works with different types of objects.
-
- Flexibility: Easily extend and modify code without changing existing logic.
-
- Simplified Code: Reduces the need for repetitive code by using a single interface for multiple forms.
-
- Maintainability: Makes code easier to maintain and understand.
Encapsulation
Encapsulation is the concept of bundling the data (variables) and methods (functions) that operate on the data into a single unit or class. More importantly, it is used to restrict direct access to some of an object's components and protect the object's integrity by preventing unintended or harmful modifications. In Java, it is implemented through the use of access modifiers, which control how the members (fields and methods) of a class are accessed from outside the class.
1. Use access modifiers to protect important data
The table below represents the access range of each access modifier in Java.

The private modifier restricts access to the class member (fields, methods) only to the class where they are defined. No other class can access them. It is used for encapsulation, ensuring that the fields or methods cannot be accessed or modified outside the class.
2. Access private data using public methods
Public methods, often called getter and setter methods, are used to provide access to private variables. Getters are used to retrieve the values of variables, and setters are used to modify them. By using them, developers can enforce rules or validation before changing or retrieving data, allowing for better control over the object’s state.
public class Phone {
private String name;
private int version;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getVersion() {
return version;
}
public void setVersion(int version) {
if (version > 0) {
this.version = version'
}
}
}
In this example, the Phone class encapsulates the data (name and version). The name and version fields are private, meaning they cannot be accessed directly from outside the class. Instead, public getter and setter methods are used to interact with these fields.
The setVersion method includes a validation check to ensure that the version is positive before it is set.
Benefit of inheritance
-
- Code Reusability – Avoids writing duplicate code.
-
- Improved Maintainability – Changes in the parent class automatically apply to child classes.
-
- Logical Organization – Helps structure code in a hierarchical manner.