extends
and super
Inheritance is a fundamental concept in object-oriented programming that allows one class to inherit fields and methods from another. In Java, inheritance is expressed using the extends
keyword, enabling you to create hierarchical relationships between classes and promote code reuse.
Inheritance allows a new class (called a child or subclass) to acquire properties and behaviors from an existing class (called the parent or superclass). This helps avoid duplication and makes your code more organized.
extends
to Define a Child ClassThe extends
keyword indicates that a class inherits from another:
public class Animal {
public void eat() {
System.out.println("This animal eats food.");
}
}
public class Dog extends Animal {
public void bark() {
System.out.println("The dog barks.");
}
}
Here, Dog
inherits the eat()
method from Animal
and also adds its own method bark()
.
public class Main {
public static void main(String[] args) {
Dog dog = new Dog();
dog.eat(); // Inherited from Animal
dog.bark(); // Defined in Dog
}
}
Output:
This animal eats food.
The dog barks.
class Animal {
public void eat() {
System.out.println("This animal eats food.");
}
}
class Dog extends Animal {
public void bark() {
System.out.println("The dog barks.");
}
}
public class Main {
public static void main(String[] args) {
Dog dog = new Dog();
dog.eat(); // Inherited from Animal
dog.bark(); // Defined in Dog
}
}
super
Keyword: Accessing Parent Class Memberssuper
allows subclasses to refer to their superclass, which is useful in two common scenarios:
When a subclass has a constructor, it can call the parent class constructor using super(...)
. This initializes the parent part of the object properly.
public class Animal {
String name;
public Animal(String name) {
this.name = name;
}
}
public class Dog extends Animal {
public Dog(String name) {
super(name); // Calls Animal's constructor
}
public void printName() {
System.out.println("Dog's name: " + name);
}
}
Usage:
Dog dog = new Dog("Buddy");
dog.printName(); // Output: Dog's name: Buddy
If a subclass overrides a method, it can still access the original version using super.methodName()
.
public class Animal {
public void sound() {
System.out.println("Animal makes a sound");
}
}
public class Dog extends Animal {
@Override
public void sound() {
super.sound(); // Calls Animal's sound()
System.out.println("Dog barks");
}
}
Usage:
Dog dog = new Dog();
dog.sound();
Output:
Animal makes a sound
Dog barks
class Animal {
String name;
public Animal(String name) {
this.name = name;
}
public void sound() {
System.out.println("Animal makes a sound");
}
}
class Dog extends Animal {
public Dog(String name) {
super(name);
}
public void printName() {
System.out.println("Dog's name: " + name);
}
@Override
public void sound() {
super.sound(); // Calls Animal's sound()
System.out.println("Dog barks");
}
}
public class Main {
public static void main(String[] args) {
Dog dog1 = new Dog("Buddy");
dog1.printName(); // Output: Dog's name: Buddy
dog1.sound(); // Output: Animal makes a sound
// Dog barks
}
}
extends
to create a subclass that inherits from a superclass.super(...)
to invoke a parent class constructor inside a subclass constructor.super.methodName()
to call a parent’s method that has been overridden.Try creating your own class hierarchies using extends
and practice using super
to access parent class constructors and methods. This foundational skill is key to mastering Java’s object-oriented programming.
In object-oriented programming, method overriding allows a subclass to provide a specific implementation of a method that is already defined in its superclass. This lets subclasses customize or extend behavior inherited from parent classes, enabling polymorphism and flexible code design.
When a subclass defines a method with the same signature (name, return type, and parameters) as a method in its superclass, it overrides that method. Calls to the method on an object of the subclass will execute the subclass’s version instead of the superclass’s.
@Override
AnnotationThe @Override
annotation is optional but highly recommended. It tells the compiler that you intend to override a method, enabling it to check your method signature for correctness and avoid mistakes such as misspellings or incorrect parameters.
Example:
@Override
public void draw() {
// subclass-specific code
}
If the method doesn't correctly override a superclass method, the compiler will raise an error.
public
method cannot be overridden with private
).Shape
, Circle
, and Rectangle
public class Shape {
public void draw() {
System.out.println("Drawing a generic shape");
}
}
public class Circle extends Shape {
@Override
public void draw() {
System.out.println("Drawing a circle");
}
}
public class Rectangle extends Shape {
@Override
public void draw() {
System.out.println("Drawing a rectangle");
}
}
Because all these classes share the same method signature, you can write code that treats all shapes uniformly while still calling their specific implementations:
public class Main {
public static void main(String[] args) {
Shape[] shapes = { new Shape(), new Circle(), new Rectangle() };
for (Shape shape : shapes) {
shape.draw(); // Calls the appropriate method based on the object’s actual type
}
}
}
Output:
Drawing a generic shape
Drawing a circle
Drawing a rectangle
This demonstrates polymorphism — the ability of different objects to respond uniquely to the same method call.
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 Rectangle extends Shape {
@Override
public void draw() {
System.out.println("Drawing a rectangle");
}
}
public class Main {
public static void main(String[] args) {
Shape[] shapes = { new Shape(), new Circle(), new Rectangle() };
for (Shape shape : shapes) {
shape.draw(); // Polymorphic call
}
}
}
@Override
annotation to help catch errors and improve readability.Experiment by creating your own class hierarchy with overridden methods and observe how Java chooses which method to call at runtime. This technique is foundational for advanced Java programming.
instanceof
and Type CastingIn Java’s inheritance hierarchy, objects of subclasses can be treated as instances of their superclass, but sometimes you need to identify an object’s actual type at runtime or convert references between classes. Java provides the instanceof
operator for type checking and supports type casting to convert references safely.
instanceof
?The instanceof
operator tests whether an object is an instance of a specified class or interface. It returns true
if the object can be safely treated as that type, otherwise false
.
Syntax:
objectRef instanceof ClassName
Example:
if (obj instanceof Dog) {
System.out.println("obj is a Dog");
}
Assigning a subclass object to a superclass reference is called upcasting and happens automatically (implicitly):
Dog dog = new Dog();
Animal animal = dog; // Upcasting Dog to Animal
Upcasting is safe because a Dog
is an Animal
, so you can use the animal
reference to access only the members defined in Animal
.
Assigning a superclass reference back to a subclass reference is called downcasting and requires an explicit cast because it’s potentially unsafe:
Animal animal = new Dog();
Dog dog = (Dog) animal; // Downcasting Animal to Dog
If animal
actually refers to a Dog
, the cast works. If it refers to a different subclass or a plain Animal
, a ClassCastException
will be thrown at runtime.
instanceof
to Ensure Safe DowncastingBefore downcasting, you should check the object’s type using instanceof
to avoid exceptions:
if (animal instanceof Dog) {
Dog dog = (Dog) animal; // Safe downcast
dog.bark();
} else {
System.out.println("The animal is not a dog");
}
class Animal {
public void makeSound() {
System.out.println("Animal makes a sound");
}
}
class Dog extends Animal {
public void bark() {
System.out.println("Dog barks");
}
}
class Cat extends Animal {
public void meow() {
System.out.println("Cat meows");
}
}
public class Main {
public static void main(String[] args) {
Animal myAnimal = new Dog(); // Upcasting
myAnimal.makeSound(); // Animal method or overridden version
// myAnimal.bark(); // Compile error: method not visible via Animal reference
if (myAnimal instanceof Dog) {
Dog myDog = (Dog) myAnimal; // Downcasting after check
myDog.bark();
}
Animal anotherAnimal = new Cat();
if (anotherAnimal instanceof Dog) {
Dog dog = (Dog) anotherAnimal; // This block won't execute
} else {
System.out.println("Not a Dog, cannot cast");
}
}
}
Output:
Animal makes a sound
Dog barks
Not a Dog, cannot cast
instanceof
check, or risk a runtime ClassCastException
.instanceof
; it can indicate design issues. Prefer polymorphism (overriding methods) to handle different behaviors.instanceof
returns false
if the object is null
.instanceof
to check an object’s actual type at runtime safely.instanceof
.Try experimenting by creating your own class hierarchy and practice safe casting with instanceof
. Understanding these concepts is vital for mastering Java’s polymorphic behaviors.
Java’s power in object-oriented programming largely comes from its ability to support polymorphism, allowing a single reference type to point to objects of multiple types. The core mechanism enabling this is called dynamic method dispatch. This runtime process decides which overridden method implementation to execute based on the actual object type, not the declared reference type.
Dynamic method dispatch is Java’s way of selecting the correct version of an overridden method when a superclass reference points to a subclass object. At compile time, the compiler checks that the method exists in the reference type, but the actual method called at runtime depends on the object’s real class.
This makes Java’s method calls dynamic and flexible.
Consider a simple hierarchy of shapes:
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 Rectangle extends Shape {
@Override
public void draw() {
System.out.println("Drawing a rectangle");
}
}
Here, all subclasses override the draw()
method to provide their specific drawing behavior.
public class Main {
public static void main(String[] args) {
Shape shape;
shape = new Shape();
shape.draw(); // Calls Shape's draw()
shape = new Circle();
shape.draw(); // Calls Circle's draw()
shape = new Rectangle();
shape.draw(); // Calls Rectangle's draw()
}
}
Output:
Drawing a generic shape
Drawing a circle
Drawing a rectangle
Despite shape
being declared as type Shape
, the runtime object type determines which draw()
method runs.
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 Rectangle extends Shape {
@Override
public void draw() {
System.out.println("Drawing a rectangle");
}
}
public class Main {
public static void main(String[] args) {
Shape shape;
shape = new Shape();
shape.draw(); // Drawing a generic shape
shape = new Circle();
shape.draw(); // Drawing a circle
shape = new Rectangle();
shape.draw(); // Drawing a rectangle
}
}
If Java used the reference type to decide which method to call (static dispatch), the output would always be:
Drawing a generic shape
Drawing a generic shape
Drawing a generic shape
This would ignore the subclasses’ custom behavior, making polymorphism impossible.
Dynamic dispatch enables:
Shape[] shapes = { new Circle(), new Rectangle(), new Shape() };
for (Shape s : shapes) {
s.draw(); // Calls the appropriate draw() method dynamically
}
Output:
Drawing a circle
Drawing a rectangle
Drawing a generic shape
This pattern is common in frameworks, GUI toolkits, and APIs where objects behave differently but share a common interface.
Experiment by creating your own class hierarchy with overridden methods and try calling those methods through superclass references. Watch how Java dynamically selects the correct implementation — this is the heart of polymorphism in Java!