Java
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Object-Oriented Java
-
Section 4: Collections Framework
-
Section 5: Exception Handling
-
Section 6: Generics
-
Section 7: Functional Java
-
Section 8: Concurrency
-
Section 9: I/O and NIO
-
Section 10: JVM Internals
-
Section 11: Modern Java Features
-
Section 12: Build Tools and Project Structure
-
Section 13: Testing
-
Section 14: Databases and Persistence
-
Section 15: Networking
-
Section 16: Design and Best Practices
-
Section 17: Reflection and Annotations
-
Section 18: Logging and Diagnostics
-
Section 19: Date, Time, and Internationalization
-
Section 20: Java Platform Module System
-
Section 21: Security in Java
-
Section 22: Advanced Collections and Data Structures
-
Section 23: More Concurrency Patterns
-
Section 24: Compression, Files, and System Integration
-
Section 25: GUI Programming
-
Section 26: Practical Projects
-
Section 27: Data Structures and Algorithms
-
Section 28: Interview and Algorithm Practice
-
Section 29: JSON and Data Interchange
-
Section 30: More Concurrency Utilities
-
Section 31: More Collections and Streams Practice
-
Section 32: More File and System Programming
-
Section 33: Standard Library Deep Dive
-
Section 34: More Practice and Drills
-
Section 35: More Testing and Quality
-
Section 36: More Design Patterns and Architecture
-
Section 37: Career and Ecosystem
-
Section 38: More OOP and Architecture Practice
-
Section 39: More Enterprise Concepts
-
Section 40: Advanced JavaFX
-
Section 41: More Interview Practice
247: Common Java Interview Questions on OOP Principles
I've seen this exact bug in a dozen code reviews. A developer is trying to use polymorphism to customize the internal behavior of a base class, but for some reason, the child class's logic is being completely ignored. They usually come to me scratching their head, insisting the code "looks correct."
class PaymentProcessor {
public void processPayment() {
System.out.println("Starting payment flow...");
validateDetails();
System.out.println("Payment complete.");
}
private void validateDetails() {
System.out.println("Performing generic validation...");
}
}
class CreditCardProcessor extends PaymentProcessor {
@Override
private void validateDetails() {
System.out.println("Performing strict Credit Card validation...");
}
}
public class Main {
public static void main(String[] args) {
PaymentProcessor processor = new CreditCardProcessor();
processor.processPayment();
// Expected: "Performing strict Credit Card validation..."
// Actual: "Performing generic validation..."
}
}
The Ghost Overriding Bug
At first glance, it looks like `CreditCardProcessor` is overriding validateDetails(). However, if you try to compile this, the compiler might actually complain that @Override isn't valid here. If you remove the annotation, the code runs, but it calls the base class method instead of the child class method.
Here is the deal: private methods are not polymorphic. In Java, private methods are bound at compile-time (static binding). Because the processPayment() method is defined in the base class and it calls a private method, it is hard-wired to call its own version of that method, regardless of what the actual object instance is at runtime.
Opening the Door with Protected
To fix this, we need to change the visibility. We don't want the whole world to see validateDetails() (that would break encapsulation), but we need the child classes to be able to override it. This is exactly why the protected modifier exists.
class PaymentProcessor {
public void processPayment() {
System.out.println("Starting payment flow...");
validateDetails();
System.out.println("Payment complete.");
}
// Changed from private to protected
protected void validateDetails() {
System.out.println("Performing generic validation...");
}
}
class CreditCardProcessor extends PaymentProcessor {
@Override
protected void validateDetails() {
System.out.println("Performing strict Credit Card validation...");
}
}
Now, when processPayment() is called, Java uses dynamic binding. It looks at the actual object on the heap (which is a CreditCardProcessor) and calls the most specific implementation of validateDetails(). This is the essence of polymorphism in action.
Talking the Talk: The Four Pillars in Interviews
When an interviewer asks you about OOP principles, they aren't just looking for definitions you memorized from a wiki. They want to know if you understand how these concepts prevent bugs. Here is how I suggest you frame your answers.
- Encapsulation: Don't just say "it's hiding data." Say "it's about protecting the internal state of an object to prevent the system from entering an invalid state." Mention that using
privatefields with controlledpublicgetters/setters allows you to add validation logic later without breaking every class that uses your object. - Inheritance: Be careful here. Interviewers love to ask about the "is-a" relationship. I always warn juniors not to over-use inheritance. If you find yourself inheriting just to reuse a few methods, tell the interviewer you'd prefer Composition (the "has-a" relationship). It shows you're a seasoned engineer, not a textbook.
- Polymorphism: Distinguish between Compile-time (Overloading) and Runtime (Overriding). Overloading is just convenience; Overriding is what allows us to build pluggable architectures where the base class doesn't need to know the specifics of the subclasses.
- Abstraction: This is about hiding complexity. Use the example of a
Listinterface. You don't care if it's anArrayListor aLinkedList; you just care that you canadd()andget()elements.
The Great Debate: Interface vs. Abstract Class
This is a staple interview question. The "correct" answer has evolved with Java 8 and 11 (thanks to default methods), but the core distinction remains:
- Abstract Classes: Use these when you have a strong "is-a" relationship and want to share state (fields) and base behavior. An abstract class is a partial blueprint.
- Interfaces: Use these to define a contract or a capability. If you have classes from completely different hierarchies that all need to be "Searchable" or "Serializable," use an interface.
My rule of thumb: If you're wondering which one to use, start with an interface. It's much easier to change a design based on interfaces than it is to untangle a deep, rigid inheritance tree.
📋 Practical Task
Refactoring a Rigid Employee Payroll System
You've inherited a piece of code for a payroll system that is currently a mess. It uses a single Employee class with a bunch of if/else blocks to calculate pay based on the employee type (Full-time vs. Contract). This is a violation of the Open/Closed Principle (one of the SOLID principles that build on OOP).
Your Task:
- Create an interface called
Payablewith a methoddouble calculatePay(). - Create an abstract class
Employeethat implementsPayable. It should have aprivate String nameand aprotected double baseRate, along with a constructor and a getter for the name. - Create two subclasses:
FullTimeEmployee: OverridescalculatePay()to returnbaseRate * 160(standard hours).ContractEmployee: Adds aprivate int hoursWorkedfield and overridescalculatePay()to returnbaseRate * hoursWorked.
- In your
Mainclass, create aList<Payable>containing both types of employees. Iterate through the list and print the name and the calculated pay for each, demonstrating polymorphism.
Constraint: Ensure that baseRate is not accessible from the Main class, but is accessible to the subclasses.
There are no comments for now.