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.
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 or Boolean).
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.
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.
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. } }
Where is the complexity hidden?
How does the abstract class help?
What does the user see?
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.
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. } }
2. Runtime Polymorphism (Method Overriding): Achieved when a subclass provides its specific implementation of a method declared in the parent class.
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 } }
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.
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.
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 } }
In this example:
Dog inherits from Animal, 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.
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.
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. } }
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.
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!
Java Fundamentals
Array Interview Essentials
Java Memory Essentials
Java Keywords Essentials
Collections Framework Essentials
Happy Coding!
The above is the detailed content of Cracking OOP in Java: A PIE You'll Want a Slice Of. For more information, please follow other related articles on the PHP Chinese website!