In this post, we’ll explore the four fundamental pillars of Object-Oriented Programming (OOP) in Java. These core principles help structure code to be modular, reusable, and maintainable. This post serves as an introduction, with upcoming entries diving deeper into each concept with nuanced discussions and examples.
To make it easy to remember, use the acronym “A PIE”: Abstraction, Polymorphism, Inheritance, and Encapsulation.
What Does It Mean to Be Object-Oriented?
Java is often described as an object-oriented language, but it’s not 100% object-oriented. Why? While most elements in Java revolve around objects (like classes, objects, and methods), it also uses primitive types (like int
, boolean
, and double
), which are not objects.
Keeping primitive types in Java was a deliberate design choice. Here’s why:
Memory Efficiency: Primitive types take less memory compared to their object counterparts (like
Integer
orBoolean
).Performance Boost: Operations on primitives are faster since they avoid the overhead of object creation and reference management.
Convenience: Primitive types make code cleaner in simple cases, especially when dealing with arithmetic and logic operations.
In short, Java strikes a balance by providing primitives for performance and memory efficiency while also offering Wrapper Classes (like Integer
) for when you need to treat these values as objects.
1. Abstraction: Hiding Unnecessary Details
Abstraction means hiding internal logic and exposing only the essential features to the user. It allows the user to interact with an object at a high level without worrying about the underlying complexity. Think of it as using an ATM—you just need to enter the amount to withdraw, without knowing how the ATM interacts with your bank to process the transaction.
In Java, abstract classes and interfaces help achieve abstraction by defining essential methods and leaving the internal details either to the child classes or within the parent class but hidden from the user.
Example:
abstract class Payment {
// A method with concrete logic, hidden from the user.
private void authenticate() {
System.out.println("Authenticating payment...");
}
// Abstract method that child classes must implement.
abstract void processPayment(double amount);
// Public method exposing only the necessary details.
public void makePayment(double amount) {
authenticate(); // Hidden complexity
processPayment(amount); // Exposed to child classes
System.out.println("Payment completed.");
}
}
// Concrete class implementing the abstract method.
class CreditCardPayment extends Payment {
@Override
void processPayment(double amount) {
System.out.println("Processing credit card payment of ₹" + amount);
}
}
public class TestAbstraction {
public static void main(String[] args) {
Payment payment = new CreditCardPayment(); // Polymorphism in action.
payment.makePayment(1000.00); // Only high-level interaction.
}
}
Explanation:
-
Where is the complexity hidden?
- The
authenticate()
method represents internal logic (e.g., user validation, encryption) that is private and hidden from both the child class and the user. - The
makePayment()
method is the only public method available to the user, providing a simple way to interact with the payment system.
- The
-
How does the abstract class help?
- It forces child classes (like
CreditCardPayment
) to implement the core logic (processPayment()
), but the child class doesn’t need to know about authentication logic—it’s handled in the parent class.
- It forces child classes (like
-
What does the user see?
- From the user’s perspective, they only interact with
makePayment()
—they don’t care about authentication or how the credit card payment works internally.
- From the user’s perspective, they only interact with
2. Polymorphism: Same Action, Different Forms
Polymorphism allows an object to behave differently in different situations. Java supports two types of polymorphism:
1. Compile-Time Polymorphism (Method Overloading): Achieved by defining multiple methods with the same name but different parameters.
Example:
class Calculator {
// Compile-time polymorphism (Overloading)
int add(int a, int b) {
return a + b;
}
int add(int a, int b, int c) {
return a + b + c;
}
public static void main(String[] args) {
Calculator calc = new Calculator();
System.out.println(calc.add(2, 3)); // Output: 5
System.out.println(calc.add(2, 3, 4)); // Output: 9
}
}
2. Runtime Polymorphism (Method Overriding): Achieved when a subclass provides its specific implementation of a method declared in the parent class.
Example:
class Animal {
void sound() {
System.out.println("Animals make sounds.");
}
}
class Dog extends Animal {
@Override
void sound() {
System.out.println("Dog barks.");
}
}
public class TestPolymorphism {
public static void main(String[] args) {
Animal animal = new Dog(); // Runtime polymorphism
animal.sound(); // Output: Dog barks
}
}
Explanation:
Compile-Time Polymorphism is demonstrated by overloading the add()
method, while Runtime Polymorphism is shown by overriding the sound()
method.
The sound()
method behaves differently based on the object type. Although animal
is of type Animal
, at runtime, the overridden method in Dog
is executed.
3. Inheritance: Code Reusability through Parent-Child Relationship
Inheritance allows a class (child) to reuse the properties and behavior of another class (parent). This promotes code reusability and establishes an IS-A relationship between classes. Java doesn’t support multiple inheritance through classes to avoid ambiguity but allows it through interfaces.
Example:
class Animal {
void eat() {
System.out.println("This animal eats food.");
}
}
class Dog extends Animal {
void bark() {
System.out.println("Dog barks.");
}
}
public class TestInheritance {
public static void main(String[] args) {
Dog dog = new Dog();
dog.eat(); // Inherited method from Animal
dog.bark(); // Dog’s own method
}
}
Explanation:
In this example:
Dog
inherits fromAnimal
, meaning the dog can both eat and bark.This demonstrates code reuse—we didn’t need to rewrite the
eat()
method for the Dog class.
4. Encapsulation: Protecting Data with Access Control
Encapsulation means bundling the data (fields) and the methods that manipulate it into a single unit (class). It also ensures data-hiding by making fields private and exposing them through getters and setters.
Example:
class Student {
private String name;
// Getter
public String getName() {
return name;
}
// Setter
public void setName(String name) {
this.name = name;
}
}
public class TestEncapsulation {
public static void main(String[] args) {
Student student = new Student();
student.setName("John"); // Setting data through setter
// Accessing data through getter
System.out.println(student.getName());
}
}
Explanation:
The
name
field is private, meaning it can’t be accessed directly from outside the class.Access is provided through public getters and setters, enforcing data-hiding.
Conclusion
Java’s OOP principles—Abstraction, Polymorphism, Inheritance, and Encapsulation—form the foundation for writing modular, maintainable, and efficient code. With these concepts in hand, you’ll be better prepared to design and understand complex systems.
In upcoming posts, we’ll dive deeper into each of these principles with more nuanced examples, best practices, and interview-focused tips. Stay tuned!
Related Posts
Happy Coding!