Showing posts with label OOP Concepts. Show all posts
Showing posts with label OOP Concepts. Show all posts

Tuesday, December 9, 2025

4 Surprising Truths About Java Type Casting You Probably Don't Know

 

Most Java developers use type casting every day. It's a fundamental tool for working with class hierarchies and polymorphism. But what's really happening "under the hood"? When you cast an object, are you creating a new one? The answer might surprise you and change how you debug polymorphism and avoid subtle, surprising bugs.

This article reveals four counter-intuitive but critical truths about type casting, based on a deep dive into its internal mechanics.

1. Casting Creates a New Reference, Not a New Object

This is the most fundamental misconception about type casting. Let's set the record straight from the start. When you cast an object, you are not creating a new object in memory. You are not converting the existing object into something else. You are simply creating a new reference variable of a different type that points to the exact same existing object.

Consider this common example:

String s = new String("Durga");
Object o = (Object)s;

After this code runs, how many objects are in memory? Only one: the String object containing "Durga". What you have are two different reference variables, s (of type String) and o (of type Object), both pointing to that single object.

This is why the feature is called "type casting," not "object casting." You are changing the type of the reference, which acts like a label, not the object itself.

Strictly speaking, through type casting we are not creating any new object. For the existing object, we are providing another type of reference variable; that is, we are performing type casting but not object casting.

2. Parent References Can't Access Child-Specific Methods

Creating a new reference type for an object isn't just a cosmetic change; it has practical consequences for what you can do with that object. When you hold a child object using a parent reference, you limit your view of that object. Through the parent reference, you can only access the methods and variables defined in the parent class.

This is a cornerstone of polymorphism: you treat a Child object as a Parent to work with a generic interface, intentionally ignoring its specialized methods.

Let's look at the code. First, here are our class definitions:

class Parent {
    public void M1() {
        System.out.println("Parent Method");
    }
}

class Child extends Parent {
    public void M2() {
        System.out.println("Child-Specific Method");
    }
}

Now, let's see what happens when we use a Parent reference to hold a Child object:

Child c = new Child();
Parent p = c; // This is an implicit upcast, same as Parent p = (Parent)c;

p.M1(); // Valid - M1() is defined in the Parent class.
// p.M2(); // Compile-Time Error!

The call to p.M2() fails because the compiler only sees the Parent reference type, which has no knowledge of an M2() method. Even though the actual object in memory is a Child and does have that method, the reference type p prevents you from accessing it.

A parent reference can be used to hold a child object, but by using that reference, we can't call child-specific methods. We can call only methods available in the parent class.

3. Method Resolution Depends on Whether It’s static or Not

Here’s where the real magic—and confusion—lies. The way Java decides which method to run depends entirely on whether that method is an instance method or a static method. The distinction boils down to a core Java concept: runtime resolution (late binding) vs. compile-time resolution (early binding).

For Instance Methods (Overriding), It's All About the Object

Instance method overriding is the heart of polymorphism. In this case, method resolution is always based on the runtime object's actual type, regardless of the reference type you use to call it. This is late binding, where the JVM waits until runtime to look at the actual object in memory and check its virtual method table to decide which version of the method to execute.

Consider this class hierarchy where each class overrides M1():

class A {
    public void M1() { System.out.println("A"); }
}
class B extends A {
    public void M1() { System.out.println("B"); }
}
class C extends B {
    public void M1() { System.out.println("C"); }
}

Now, watch what happens when we cast the reference but the object stays the same:

C c = new C();

c.M1();                 // Output: C
((B)c).M1();            // Output: C
((A)(B)c).M1();         // Output: C

Even when we cast the reference to B and then to A, the output is always C. This is because the runtime object never changes—it's always a C object—and for overridden instance methods, the object's type is all that matters.

For static Methods (Hiding), It's All About the Reference

For static methods (a concept known as method hiding), the resolution is the complete opposite. It is always based on the reference type, not the underlying object. This is early binding. The compiler determines which method to call at compile-time based purely on the reference it sees, hard-wiring the call. This behavior is non-polymorphic and a frequent source of confusion for developers who expect it to behave like instance methods.

Here are the same classes, but with M1() declared as static:

class A {
    public static void M1() { System.out.println("A"); }
}
class B extends A {
    public static void M1() { System.out.println("B"); }
}
class C extends B {
    public static void M1() { System.out.println("C"); }
}

As a senior instructor, I must point out that calling a static method on an instance reference (c.M1()) is considered bad practice, precisely because it creates this confusion. You should always call them using the class name (C.M1()). But for this demonstration, let's see the result:

C c = new C();

c.M1();                 // Output: C (Reference is type C)
((B)c).M1();            // Output: B (Reference is type B)
((A)(B)c).M1();         // Output: A (Reference is type A)

Here, changing the reference type through casting directly changes which method is called. The output C, B, A proves that for static methods, the compiler resolves the call based on the reference, completely ignoring the runtime object's actual type.

4. Variable Resolution is Always Based on the Reference Type

The final truth follows the same logic as static methods: compile-time resolution. When it comes to accessing variables, the reference is king. The compiler decides which variable to access based only on the reference type, not the runtime object's type.

When a subclass declares a variable with the same name as one in a superclass, it's called variable shadowing. The specific variable being accessed is determined at compile-time based on the reference type, not the object's type.

Let's use our A, B, and C class hierarchy again, where each class defines an integer x:

class A {
    int x = 777;
}
class B extends A {
    int x = 888;
}
class C extends B {
    int x = 999;
}

Now, let's access x through different reference types pointing to the same C object:

C c = new C();

System.out.println(c.x);                 // Output: 999 (Reference is type C)
System.out.println(((B)c).x);            // Output: 888 (Reference is type B)
System.out.println(((A)(B)c).x);         // Output: 777 (Reference is type A)

Just like with static methods, the cast directly controls which x variable is accessed. The compiler binds the access to the variable declared in the reference's class.

Variable resolution is always based on reference type, but not based on the runtime object.

Conclusion

Type casting in Java is not about object conversion; it's about changing your perspective on an existing object by applying a different label—a new reference type. This new label comes with its own set of rules that are mostly resolved at compile-time. Understanding the difference between compile-time and runtime resolution is the key to mastering these behaviors.

The Golden Rule of Resolution

In Java, the runtime object's type only determines which overridden instance method is called. For everything else—static methods, all variables, and compile-time method availability checks—the reference type is all that matters.

Now that you see casting as a way to control access and perspective, how will this change the way you design and interact with your class hierarchies?

Saturday, December 6, 2025

4 Surprising Rules About Java Method Overriding You Might Not Know

 

When we first learn about inheritance in Java, method overriding seems straightforward. A parent class defines a method, but the child class isn't satisfied with the implementation and decides to provide its own. Much like a child might disagree with a parent's choice of who they should marry(), the child class can override the parent's logic with its own. But while the concept is simple, the rules governing it are full of surprising subtleties that can trip up even experienced developers.

Let's dive into four rules of method overriding that separate the experts from the novices—and see how they can make you a more effective Java programmer.

--------------------------------------------------------------------------------

1. It’s All Decided at Runtime (And That’s Why It’s Called Polymorphism)

The first crucial rule is that the decision of which overridden method to execute is handled by the JVM at runtime, not by the compiler. This distinction is the very foundation of polymorphism in Java.

Consider a common scenario where a parent reference holds a child object:

Parent p1 = new Child();
p1.marry(); // At runtime, the JVM executes the Child's version.

When you call a method like p1.marry(), the compiler and the JVM have distinct jobs:

  • Compiler's Role: The compiler only looks at the reference type, which is Parent. It checks if a marry() method exists in the Parent class. If it does, the code compiles successfully.
  • JVM's Role: At runtime, the JVM looks at the actual object that p1 is pointing to, which is a Child object. It then checks if the Child class has an overridden version of the marry() method. If it does, the child's method is executed.

This separation of concerns is a common source of confusion, but understanding it is key to mastering polymorphism and debugging unexpected runtime behavior. This runtime resolution is why overriding is a cornerstone of dynamic behavior in Java.

In overriding method resolution is always based on runtime object... that's why overriding is also known as runtime polymorphism or dynamic polymorphism or late binding.

While the JVM handles which method to run, the rules for what that method can return are surprisingly flexible.

--------------------------------------------------------------------------------

2. Return Types Don't Always Have to Be Identical (Welcome to Covariant Returns)

A common misconception is that the return type of an overriding method must be identical to the parent method's return type. While this was the strict rule until Java 1.4, the introduction of Java 1.5 brought more flexibility.

The modern rule introduces the concept of covariant return types. This means an overriding method in a child class can have a return type that is a subtype of the parent method's return type.

For example, if a parent method returns Object, a child can override it to return a String, because String is a subtype of Object. Crucially, this relationship is one-way. A method returning String cannot be overridden to return Object, as that would be broadening the return type, which is not allowed.

There's a critical limitation to remember: this flexibility only applies to object types, not primitives. If the parent method's return type is double, the overriding child method's return type must also be double.

...child class method return type need not be same as parent method return type it's child type also allowed.

Just as return types have rules, so do the access modifiers you can apply to an overridden method.

--------------------------------------------------------------------------------

3. You Can’t Weaken Access, But You Can Strengthen It

The rules for access modifiers in overriding are strict but logical: you cannot reduce the scope of the access modifier, but you can increase it.

For instance, if a method in the parent class is declared public, any overriding method in a child class must also be public. You cannot make it protected or default, as that would weaken its accessibility. If you try, the compiler will stop you with a clear error message:

attempting to assign weaker access privileges; was public

However, you are free to increase the scope. A protected method in a parent class can be overridden by a public method in a child class. The hierarchy of access is private -> default -> protected -> public. When overriding, you can only move from left to right in this hierarchy (e.g., protected to public), never from right to left.

Note that private methods are not included here because they are not inherited by child classes and therefore cannot be overridden at all.

This leads us to our final, and perhaps most surprising, rule.

--------------------------------------------------------------------------------

4. You Can Override a Concrete Method with an Abstract One

This last rule is perhaps the most counter-intuitive. It is perfectly valid in Java to override a non-abstract (fully implemented) method from a parent class with an abstract method in the child class.

From a design perspective, this is a powerful tool to control an inheritance hierarchy. But why would you ever want to do this? The primary reason is to force the next level of child classes to provide a completely new implementation. By declaring the method as abstract in the child, you effectively prevent any of its own subclasses (the "grandchildren") from inheriting and using the original implementation from the "grandparent" class.

...we can stop the availability of parent method implementation to the next level child classes.

Of course, the reverse is more common: an abstract method in a parent must be implemented by a child class. The only exception is if the child class is also declared abstract, which effectively pushes the implementation responsibility further down the inheritance chain.

--------------------------------------------------------------------------------

Conclusion

Method overriding is a powerful feature that enables polymorphism and flexible code design in Java. However, its power comes from a set of nuanced rules that go far beyond simply redefining a method. Understanding these details—from runtime method resolution and covariant returns to the strict rules on access modifiers—is a hallmark of a seasoned Java developer.

Which of these overriding rules has surprised you or caused a bug in your own code?

Featured Post

How LLMs Really Work: The Power of Predicting One Word at a Time

  1.0 Introduction: The Intelligence Illusion The most profound misconception about modern AI is that it understands . While models like Cha...

Popular Posts