extends
In Java, the extends
keyword is used to create a subclass (also called a derived class) that inherits fields and methods from a superclass (also called a base class). This is the foundation of inheritance, one of the core principles of object-oriented programming.
class Animal {
void speak() {
System.out.println("The animal makes a sound.");
}
}
class Dog extends Animal {
void bark() {
System.out.println("The dog barks.");
}
}
In this example, Dog
is a subclass of Animal
. It inherits the speak()
method from Animal
, and adds a new method bark()
.
When a class extends another:
super()
.public class Main {
public static void main(String[] args) {
Dog d = new Dog();
d.speak(); // Inherited from Animal
d.bark(); // Defined in Dog
}
}
class Animal {
void speak() {
System.out.println("The animal makes a sound.");
}
}
class Dog extends Animal {
void bark() {
System.out.println("The dog barks.");
}
}
public class Main {
public static void main(String[] args) {
Dog d = new Dog();
d.speak(); // Inherited from Animal
d.bark(); // Defined in Dog
}
}
Using extends
helps avoid code duplication by centralizing shared behavior in a base class. Subclasses can:
This approach supports the "is-a" relationship, where Dog
is an Animal
, making the code more organized, modular, and reusable.
Use inheritance thoughtfully, ensuring it models a logical hierarchy. When used well, extends
promotes clean, maintainable code through shared behavior and abstraction.
Method overriding allows a subclass to provide a specific implementation of a method that is already defined in its superclass. This is a key aspect of polymorphism in Java, enabling objects to exhibit different behaviors based on their actual type at runtime.
class Animal {
void speak() {
System.out.println("The animal makes a sound.");
}
}
class Dog extends Animal {
@Override
void speak() {
System.out.println("The dog barks.");
}
}
In this example, Dog
overrides the speak()
method inherited from Animal
. The @Override
annotation tells the compiler that this method is intended to override a superclass method, helping catch errors such as mismatched method signatures.
When you call an overridden method on a superclass reference that points to a subclass object, Java decides at runtime which version of the method to execute—this is called dynamic dispatch:
public class Main {
public static void main(String[] args) {
Animal myPet = new Dog(); // Upcasting
myPet.speak(); // Outputs: "The dog barks."
}
}
Although myPet
is declared as type Animal
, it actually refers to a Dog
object. The overridden speak()
method in Dog
is executed.
class Animal {
void speak() {
System.out.println("The animal makes a sound.");
}
}
class Dog extends Animal {
@Override
void speak() {
System.out.println("The dog barks.");
}
}
public class Main {
public static void main(String[] args) {
Animal myPet = new Dog(); // Upcasting
myPet.speak(); // Outputs: The dog barks.
}
}
private
, static
, or final
.Method overriding supports polymorphism, allowing one interface (e.g., Animal
) to represent multiple behaviors (e.g., Dog.speak()
, Cat.speak()
).
This makes your code:
In essence, overriding allows subclasses to customize or extend behaviors, making your codebase better organized and more powerful.
super
Keyword UsageThe super
keyword in Java is used to refer to the immediate superclass of a class. It is particularly useful in two main situations:
super()
If a subclass needs to initialize part of the parent class, it can call the superclass constructor using super()
.
class Animal {
Animal(String name) {
System.out.println("Animal constructor: " + name);
}
}
class Dog extends Animal {
Dog() {
super("Buddy"); // Calls Animal(String) constructor
System.out.println("Dog constructor");
}
}
When new Dog()
is called, it first invokes the Animal
constructor through super("Buddy")
, then proceeds with the rest of the Dog
constructor. This call must be the first statement in the subclass constructor.
super.methodName()
A subclass can override a method but still invoke the parent version using super.methodName()
:
class Animal {
void speak() {
System.out.println("Animal speaks");
}
}
class Dog extends Animal {
@Override
void speak() {
super.speak(); // Calls Animal's speak method
System.out.println("Dog barks");
}
}
This is helpful when the subclass wants to extend rather than completely replace the behavior of the superclass method.
class Animal {
Animal(String name) {
System.out.println("Animal constructor: " + name);
}
void speak() {
System.out.println("Animal speaks");
}
}
class Dog extends Animal {
Dog() {
super("Buddy"); // Calls Animal(String) constructor
System.out.println("Dog constructor");
}
@Override
void speak() {
super.speak(); // Calls Animal's speak method
System.out.println("Dog barks");
}
}
public class Main {
public static void main(String[] args) {
Dog dog = new Dog();
dog.speak();
}
}
super
Using super
is important when you:
However, overusing super
can indicate tight coupling. It's best used deliberately, where superclass behavior is truly required in the subclass.
Object slicing is a concept that arises when a subclass object is referenced by a superclass variable. In Java, all objects are accessed through references, so the term "object slicing" is often used more conceptually than literally—as Java does not physically slice objects like some other languages (e.g., C++). However, the effect is similar: when you use a superclass reference to hold a subclass object, you lose direct access to subclass-specific members.
class Animal {
void speak() {
System.out.println("Animal speaks");
}
}
class Dog extends Animal {
void bark() {
System.out.println("Dog barks");
}
}
public class Main {
public static void main(String[] args) {
Dog myDog = new Dog();
Animal myAnimal = myDog; // Polymorphic assignment
myAnimal.speak(); // Works fine, Animal method accessible
// myAnimal.bark(); // Compile-time error: method not found in Animal
}
}
Here, myAnimal
holds a reference to a Dog
object, but only the methods declared in Animal
are accessible. The subclass-specific bark()
method cannot be called directly from the myAnimal
reference.
This limitation is due to static typing: the compiler checks methods based on the reference type, not the actual object type. Although dynamic dispatch ensures overridden methods are correctly called at runtime, subclass-specific members require casting to access.
This behavior promotes type safety but restricts access to subclass features unless explicitly cast, reflecting a trade-off between flexibility and safety in Java's polymorphism model. Understanding this helps avoid confusion when working with inheritance and references.
final
)In Java, the final
keyword can be used to prevent inheritance and method overriding, helping enforce design constraints and improve code safety.
final
classA final
class cannot be subclassed. This means no class can extend it:
public final class ImmutableClass {
// Class content
}
// The following will cause a compile-time error:
// public class ExtendedClass extends ImmutableClass { }
Making a class final
is useful when you want to create immutable or security-sensitive classes that shouldn't be altered by inheritance.
final
methodA final
method cannot be overridden by subclasses, even if the class itself is not final
:
class Base {
public final void display() {
System.out.println("Base display method");
}
}
class Derived extends Base {
// The following will cause a compile-time error:
// public void display() { System.out.println("Derived display"); }
}
Using final
on methods ensures critical behavior remains unchanged in subclasses.
final class ImmutableClass {
public void show() {
System.out.println("ImmutableClass method");
}
}
// Uncommenting this will cause a compile-time error:
// class ExtendedClass extends ImmutableClass {}
class Base {
public final void display() {
System.out.println("Base display method");
}
}
class Derived extends Base {
// This override would cause a compile-time error:
// public void display() {
// System.out.println("Derived display");
// }
}
public class Main {
public static void main(String[] args) {
ImmutableClass imm = new ImmutableClass();
imm.show();
Derived d = new Derived();
d.display();
}
}
Restricting inheritance or overriding with final
helps maintain design stability, preventing unintended modifications or misuse. For example, security-sensitive classes like java.lang.String
are final
to guarantee consistent, reliable behavior.
At the same time, overusing final
can reduce flexibility, so it's best applied thoughtfully, balancing safety and extensibility.
instanceof
OperatorThe instanceof
operator in Java is used to test whether an object is an instance of a specific class or interface before performing operations like casting. This helps avoid ClassCastException
at runtime by ensuring the cast is safe.
Here is a typical example where instanceof
is used to check the type of an object before casting it:
Object obj = "Hello, Java!";
if (obj instanceof String) {
String str = (String) obj; // Safe cast after check
System.out.println("String length: " + str.length());
} else {
System.out.println("Not a String object");
}
In this example, obj
is declared as Object
but actually references a String
. The instanceof
check confirms this before casting, preventing runtime errors.
instanceof
Enables Safe PolymorphismIn polymorphic code, you often have variables of a superclass or interface type pointing to subclass instances. The instanceof
operator allows you to:
This improves flexibility by letting a single reference hold various subclass types, while still enabling safe and controlled behavior specific to those types.
instanceof
(Java 16)Starting with Java 16, instanceof
was enhanced with pattern matching, which simplifies type checks and casts into one step:
if (obj instanceof String str) {
System.out.println("String length: " + str.length());
}
Here, if obj
is a String
, it's automatically cast and assigned to str
, removing the need for a separate cast line. This feature improves readability and reduces boilerplate.
public class InstanceofExample {
public static void main(String[] args) {
Object obj = "Hello, Java!";
// Traditional usage
if (obj instanceof String) {
String str = (String) obj; // Safe cast after check
System.out.println("Traditional: String length = " + str.length());
} else {
System.out.println("Traditional: Not a String object");
}
// Enhanced pattern matching (Java 16+)
if (obj instanceof String str) {
System.out.println("Pattern matching: String length = " + str.length());
}
}
}
Using instanceof
judiciously enhances type safety and clarity in polymorphic code. While it helps prevent errors, excessive use may indicate design issues—sometimes better addressed with polymorphic methods or interfaces. However, combined with Java's newer pattern matching, instanceof
remains a powerful tool for safe and readable type-dependent logic.