Skip to Content
Course content

247: Common Java Interview Questions on OOP Principles

Click on the "Edit" button in the top corner of the screen to edit your slide content.

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 private fields with controlled public getters/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 List interface. You don't care if it's an ArrayList or a LinkedList; you just care that you can add() and get() 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:

  1. Create an interface called Payable with a method double calculatePay().
  2. Create an abstract class Employee that implements Payable. It should have a private String name and a protected double baseRate, along with a constructor and a getter for the name.
  3. Create two subclasses:
    • FullTimeEmployee: Overrides calculatePay() to return baseRate * 160 (standard hours).
    • ContractEmployee: Adds a private int hoursWorked field and overrides calculatePay() to return baseRate * hoursWorked.
  4. In your Main class, create a List<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.

Rating
0 0

There are no comments for now.

to be the first to leave a comment.