Mastering Inheritance and Polymorphism in Java
- Understanding Inheritance
Inheritance is a fundamental mechanism in Object-Oriented Programming (OOP) that allows a new class to adopt the attributes and behaviors of an existing class. This promotes code reusability and establishes a hierarchical relationship between classes. The existing class is often referred to as the Superclass (or Parent class), while the new class is the Subclass (or Child class). This relationship is typically described as an is-a relationship.
1.1 Syntax and Basic Example
In Java, the extends keyword is used to establish inheritance.
// Base class
class LivingBeing {
String speciesName;
int lifespan;
public void breathe() {
System.out.println(speciesName + " is breathing.");
}
}
// Derived class
class Mammal extends LivingBeing {
public void walk() {
System.out.println(speciesName + " is walking.");
}
}
public class InheritanceTest {
public static void main(String[] args) {
Mammal human = new Mammal();
human.speciesName = "Homo Sapiens";
human.lifespan = 80;
// Accessing inherited method
human.breathe();
// Accessing own method
human.walk();
}
}
1.2 Accessing Parent Members (super keyword)
The super keyword is essential for referencing the immediate parent class. It is commonly used to access parent attributes or methods that are hidden by child class declarations.
class Parent {
String data = "Parent Data";
public void display() {
System.out.println("Parent Display");
}
}
class Child extends Parent {
String data = "Child Data";
public void showInfo() {
System.out.println("Child data: " + this.data);
// Accessing parent's version of 'data'
System.out.println("Parent data: " + super.data);
// Calling parent's method explicitly
super.display();
}
}
1.3 Method Overriding
Method overriding occurs when a subclass provides a specific implementation for a method already defined in its superclass. The method signature (name and parameters) must be identical.
class Device {
public void powerOn() {
System.out.println("Generic device powering on.");
}
}
class Smartphone extends Device {
@Override
public void powerOn() {
System.out.println("Smartphone booting up with logo.");
}
}
1.4 Constructor Chaining
In Java, the constructor of the parent class is always executed before the constructor of the child class. If you do not explicitly call a parent constructor using super(), the compiler inserts a call to the no-argument constructor of the parent class automatically.
class Base {
Base(String id) {
System.out.println("Base initialized with ID: " + id);
}
}
class Derived extends Base {
Derived() {
// Must explicitly call super because Base has no default constructor
super("XYZ-123");
System.out.println("Derived initialized.");
}
}
1.5 Inheritance Rules
- Single Inheritance: A class can only extend one direct superclass.
- Multilevel Inheritance: A class can be a parent of another class, which in turn is a parent of a third class (e.g., A -> B -> C).
- Private Members: Subclasses inherit private fields, but they cannot access them directly. They must use public getter/setter methods.
- Access Modifiers
Java defines access levels that control where members can be accessed.
| Modifier | Class | Package | Subclass (Diff Package) | World |
|---|---|---|---|---|
| public | Yes | Yes | Yes | Yes |
| protected | Yes | Yes | Yes | No |
| default | Yes | Yes | No | No |
| private | Yes | No | No | No |
- The Object Class
All classes in Java implicitly extend java.lang.Object. This root class provides standard methods that every object inherits.
3.1 Common Object Methods
- toString(): Returns a string representation of the object. It is good practice to override this for debugging.
- equals(Object obj): Compares the equality of objects. By default, it checks reference equality (==), but it is often overridden to check logical equality (e.g., comparing field values).
- hashCode(): Returns a hash code value for the object. If two objects are equal according to
equals(), they must have the same hash code. - getClass(): Returns the runtime class of the object, useful for reflection.
class User {
private String userId;
private int level;
public User(String userId, int level) {
this.userId = userId;
this.level = level;
}
@Override
public String toString() {
return "User{id='" + userId + "', level=" + level + "}";
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof User)) return false;
User user = (User) o;
return level == user.level && userId.equals(user.userId);
}
}
- Polymorphism
Polymorphism allows objects of different subclasses to be treated as objects of a common superclass. It enables one interface to be used for a general class of actions.
4.1 Principles of Polymorphism
To achieve polymorphism, you need:
- Inheritance: A relationship between a parent and child class.
- Method Overriding: The child class must redefine a method from the parent.
- Upcasting: A parent class reference variable pointing to a child class object.
4.2 Upcasting and Dynamic Method Dispatch
When a parent reference points to a child object, the call to an overridden method is determined at runtime (Dynamic Method Dispatch). The decision of which method to execute is based on the actual object in the heap, not the reference type.
class Shape {
public void draw() {
System.out.println("Drawing a generic shape.");
}
}
class Circle extends Shape {
@Override
public void draw() {
System.out.println("Drawing a Circle.");
}
}
class Square extends Shape {
@Override
public void draw() {
System.out.println("Drawing a Square.");
}
}
public class PolyDemo {
public static void render(Shape s) {
// Polymorphic behavior: The specific draw() method is called
// based on whether 's' is a Circle or Square at runtime.
s.draw();
}
public static void main(String[] args) {
Shape sh1 = new Circle(); // Upcasting
Shape sh2 = new Square(); // Upcasting
render(sh1); // Output: Drawing a Circle.
render(sh2); // Output: Drawing a Square.
}
}
4.3 Downcasting and instanceof
While upcasting is automatic, downcasting (converting a parent reference back to a child type) must be explicit. To avoid ClassCastException, use the instanceof operator to check the actual type before casting.
public class CastingDemo {
public static void main(String[] args) {
Shape myShape = new Circle();
if (myShape instanceof Circle) {
Circle specificCircle = (Circle) myShape;
System.out.println("Successfully downcasted to Circle.");
} else if (myShape instanceof Square) {
Square specificSquare = (Square) myShape;
}
}
}
4.4 Member Access in Polymorphism
- Methods: "Compile-time check, Runtime execution." The compiler checks the reference type, but the JVM executes the method of the actual object type.
- Variables: Variables are not polymorphic. Access is determined by the reference type (the left side of the assignment), not the object type.