Object-Oriented Programming in Java
Inheritance and Polymorphism
Inheritance allows classes to derive properties and behaviors from parent classes, promoting code reusability and hierarchical organization. Polymorphism enables objects to be treated as instances of their parent class, allowing for flexible, interchangeable code that adapts at runtime. Developers reach for these concepts when building modular systems where shared logic needs to be specialized across different but related object types.
Understanding Inheritance Basics
Inheritance is the mechanism by which one class acquires the non-private members of another. By using the 'extends' keyword, a subclass gains access to the state and behavior of the superclass, which promotes the 'DRY' (Don't Repeat Yourself) principle. When you define a base class, you identify common attributes that all specific entities in your domain will share. The subclass inherently possesses these, allowing you to focus on defining only the differences or unique enhancements. The underlying reason this works is the hierarchical structure enforced by the compiler: a subclass is essentially an 'is-a' version of its parent. This relationship allows developers to model real-world taxonomy effectively. Because the subclass carries the blueprint of the parent, memory allocation includes fields from both classes, ensuring that the child object is fully functional and contains all necessary data structures to operate within the context of the superclass. This structure is fundamental to establishing predictable relationships between objects in your codebase.
class Employee {
protected String name; // Accessible to subclasses
public Employee(String name) { this.name = name; }
}
// Developer inherits state from Employee
class Developer extends Employee {
private String language;
public Developer(String name, String language) {
super(name); // Must call parent constructor
this.language = language;
}
}
// Usage: new Developer("Alice", "Java");Method Overriding and Dispatch
Overriding occurs when a subclass provides a specific implementation for a method already defined in its parent class. This is not just about changing code; it is about establishing behavioral specialization within the same method signature. When the Java Virtual Machine executes an overridden method, it employs dynamic method dispatch. This mechanism examines the actual object type at runtime—not the variable type—to decide which version of the method to trigger. The runtime environment follows a lookup table associated with the object's class to find the most specific implementation. This behavior is crucial because it allows the base class to define a contract or an interface that guarantees certain operations exist, while the subclasses define how those operations are performed in their specific contexts. By overriding methods, you ensure that the system behaves correctly according to the specific nature of the object, even if that object is being referenced by a more generic superclass variable type throughout the application logic.
class Shape {
public void draw() { System.out.println("Drawing shape"); }
}
class Circle extends Shape {
@Override // Compiler verifies this overrides a parent method
public void draw() { System.out.println("Drawing circle"); }
}
// Runtime behavior determined by object instance, not variable type.Polymorphism Through Reference Types
Polymorphism allows a single interface to control access to different underlying data types. Specifically, a parent class reference can hold an instance of any of its subclasses. This works because the compiler only checks if the method exists in the reference type, but the execution environment triggers the implementation found in the actual object type. This 'type-widening' is safe because a subclass is guaranteed to contain everything the superclass declares. By coding to a superclass reference, your logic becomes decoupled from the specific implementation details of concrete subclasses. This is a massive advantage when designing APIs or library code that needs to operate on groups of objects without knowing their exact types at design time. It enables extensibility; you can introduce new subclasses into your system later without modifying the code that processes the parent references, provided the external contract defined by the parent class remains consistent and meaningful for the new types introduced.
Shape s1 = new Circle(); // Polymorphic reference
Shape s2 = new Shape();
// We treat all shapes as Shape, but they behave as their true type
Shape[] shapes = {s1, s2};
for (Shape s : shapes) {
s.draw(); // Dynamic dispatch happens here
}The Role of the 'super' Keyword
The 'super' keyword serves as a explicit reference to the immediate parent class instance. It is essential when you need to access parent members that have been hidden or overridden in the subclass. Without 'super', the subclass would lack a way to trigger the original behavior of a method once that method has been overridden. Furthermore, 'super' is required for constructor chaining. In Java, a subclass constructor must always ensure the parent class is initialized before it adds its own fields. If you do not call 'super()' explicitly, the compiler inserts a call to the default no-argument constructor of the parent. Understanding this ensures your objects are initialized in a stable state from the top of the inheritance chain down to the leaf node. Using 'super' properly allows for 'additive' design, where a subclass performs its specific tasks and then invokes the parent's logic to handle the foundational requirements, effectively layering functionality during initialization and execution sequences.
class Logger {
void log() { System.out.print("Base Log "); }
}
class FileLogger extends Logger {
void log() {
super.log(); // Execute parent logic
System.out.println("to file"); // Add specific logic
}
}Abstract Classes and Enforcement
An abstract class acts as a partially implemented template that cannot be instantiated directly. Its purpose is to define a common protocol for a family of related subclasses while forcing those subclasses to provide concrete implementations for mandatory behaviors. By declaring a method as 'abstract', you force the compiler to reject any subclass that does not explicitly implement that method. This is the ultimate tool for architectural enforcement in an object-oriented system. It bridges the gap between inheritance and interfaces, providing both state management (through shared fields) and behavioral contracts (through abstract methods). When you design with abstract classes, you are setting boundaries for future developers, ensuring that every type in that family fulfills the necessary requirements. This pattern works because it forces a structure upon the inheritance tree, guaranteeing that polymorphic calls will always succeed because the required methods are guaranteed to exist in every subclass that extends the abstract base.
abstract class Worker {
abstract void performTask(); // No body, must be implemented
}
class Robot extends Worker {
void performTask() { System.out.println("Assembling..."); }
}
// Worker w = new Worker(); // Illegal: abstract class cannot be instantiatedKey points
- Inheritance allows a subclass to reuse fields and methods defined in a superclass.
- The 'extends' keyword establishes an 'is-a' relationship between two classes.
- Polymorphism enables the use of a parent class reference to point to a subclass instance.
- Method overriding allows a subclass to provide its own specific logic for an inherited method.
- Dynamic method dispatch ensures that the actual object instance determines which method implementation executes at runtime.
- The 'super' keyword provides a mechanism to access parent members and invoke parent constructors.
- Abstract classes serve as blueprints that prevent direct instantiation while enforcing specific method implementations in subclasses.
- Coding to the most general reference type possible maximizes the flexibility and extensibility of an application.
Common mistakes
- Mistake: Attempting to access a subclass-specific method via a superclass reference. Why it's wrong: The compiler only knows about methods defined in the declared type. Fix: Use explicit type casting after verifying the object type with 'instanceof'.
- Mistake: Overloading a method instead of overriding it. Why it's wrong: Changing the method signature (parameters) creates a new overloaded method, meaning runtime polymorphism won't trigger. Fix: Use the @Override annotation to ensure the compiler catches signature mismatches.
- Mistake: Assuming private methods are inherited and overridden. Why it's wrong: Private methods are hidden from subclasses; creating a method with the same name in the subclass is just shadowing, not overriding. Fix: Change the modifier to protected or public if polymorphic behavior is required.
- Mistake: Failing to define a no-argument constructor in a parent class when subclass constructors call 'super()'. Why it's wrong: If you define a custom constructor in the parent, the compiler won't provide the default one, causing a compilation error. Fix: Explicitly define a no-arg constructor or call the super constructor with arguments.
- Mistake: Misunderstanding that static methods are not polymorphic. Why it's wrong: Static methods are bound at compile time based on the reference type, not the object type. Fix: Use instance methods if you need behavior that depends on the actual object created at runtime.
Interview questions
What is the primary purpose of inheritance in Java?
Inheritance in Java allows a class to acquire the properties and behaviors of another class, facilitating code reusability and establishing an 'is-a' relationship. By extending a base class using the 'extends' keyword, a subclass inherits non-private fields and methods. This is fundamental because it allows developers to create a hierarchy of classes, reducing redundancy and making it easier to maintain and update shared logic across related components in a project.
How does method overriding work, and why is it important?
Method overriding occurs when a subclass provides a specific implementation for a method that is already defined in its parent class. To ensure correctness, the method must have the same name, parameters, and return type. It is crucial because it allows a subclass to modify the behavior of inherited methods to suit its specific needs. For example, if you have a class 'Shape' with a 'draw()' method, a 'Circle' subclass can override 'draw()' to render a circular shape instead of a generic one.
Explain the concept of polymorphism and how it is achieved in Java.
Polymorphism, meaning 'many forms,' allows objects to be treated as instances of their parent class while executing the specific behavior defined by their actual class. In Java, this is achieved through method overriding (runtime polymorphism) and method overloading (compile-time polymorphism). By using a parent reference to hold a child object, such as 'Animal myPet = new Dog();', we can invoke methods on 'myPet', and Java will resolve the appropriate method call at runtime based on the actual object type.
Compare 'Inheritance' and 'Composition' as approaches to code reuse in Java.
Inheritance represents an 'is-a' relationship, where a subclass is a specialized version of the parent class, exposing the parent's protected members. Composition represents a 'has-a' relationship, where a class contains an instance of another class as a field. Composition is often preferred over inheritance because it offers greater flexibility; it allows for dynamic behavior changes at runtime and avoids the tight coupling and 'fragile base class' problems that can arise when deep inheritance hierarchies are used.
What is the significance of the 'super' keyword in Java?
The 'super' keyword in Java is a reference variable used to refer to the immediate parent class object. It is essential when a subclass overrides a parent method but still needs to access the original logic or when it needs to explicitly call a parent constructor. For instance, in a constructor, 'super()' must be called as the first statement to ensure the parent state is initialized properly before the subclass adds its own custom initialization logic.
How do abstract classes and interfaces differ in their application of polymorphism?
Abstract classes and interfaces both facilitate polymorphism but serve different architectural purposes. An abstract class is used when subclasses share a common identity and implementation details, using the 'extends' keyword to enforce a single inheritance. An interface defines a contract of behaviors that unrelated classes can implement, supporting multiple inheritance of type. While abstract classes can hold state (instance variables), interfaces primarily define method signatures that implementing classes must fulfill, enabling a decoupled, plug-and-play architecture.
Check yourself
1. Given class A and class B extends A, what happens when you execute: A obj = new B(); obj.doWork(); if doWork() exists in both classes?
- A.The method in A is called because the reference type is A.
- B.The method in B is called because of runtime polymorphism.
- C.A compilation error occurs because A does not know about B's methods.
- D.Both methods are executed sequentially.
Show answer
B. The method in B is called because of runtime polymorphism.
Java uses dynamic method dispatch for instance methods. The method in B is called because the actual object is of type B. A is incorrect because dynamic dispatch ignores the reference type for instance methods. C is wrong because A already contains the method definition (if it didn't, the code wouldn't compile). D is wrong because polymorphism selects only one implementation.
2. Which of the following best describes the purpose of the 'super' keyword in a constructor?
- A.To invoke an overloaded constructor within the same class.
- B.To access a private field in the parent class.
- C.To initialize the inherited portion of the object by calling the parent constructor.
- D.To prevent the parent class from being instantiated.
Show answer
C. To initialize the inherited portion of the object by calling the parent constructor.
The super() call ensures the parent part of the object is set up correctly before the subclass logic runs. A is wrong because that describes 'this()'. B is wrong because 'super' cannot access private members. D is wrong because abstract classes or access modifiers handle instantiation, not the 'super' keyword.
3. What is the primary difference between an interface and an abstract class?
- A.Abstract classes can have constructors, while interfaces cannot.
- B.Interfaces can only contain static final variables.
- C.A class can only extend one abstract class but can implement multiple interfaces.
- D.All of the above.
Show answer
D. All of the above.
All three statements are fundamental truths about Java design: abstract classes allow state (via constructors), interfaces define contracts, and multiple inheritance of interfaces is allowed. This makes D the only comprehensive answer.
4. If a subclass overrides a method that returns a String, what return type is allowed in the overriding method?
- A.Only String.
- B.Only Object.
- C.Any type, as long as it is a subclass of String.
- D.Any primitive type.
Show answer
C. Any type, as long as it is a subclass of String.
Java supports covariant return types. You can return a more specific type (a subclass of the original return type), but you cannot change it to a completely unrelated type. A is too restrictive, B is the opposite of covariant, and D is invalid as primitives cannot replace objects in overriding.
5. What happens if you define a static method in a subclass with the same signature as a static method in the superclass?
- A.It overrides the superclass method.
- B.It results in a compile-time error.
- C.It hides the superclass method, but doesn't override it.
- D.The program crashes at runtime.
Show answer
C. It hides the superclass method, but doesn't override it.
Static methods are hidden, not overridden. This means the version called depends solely on the reference type used. A is wrong because static methods do not support polymorphism. B is wrong because hiding is legal in Java. D is wrong because this is a standard language feature, not a bug.