🃏 Instance Methods vs Variables and Static Methods

Rule: Instance methods are overridden, while variables and static methods are hidden.

  • The method invoked depends on the actual object type (runtime)
  • The field accessed depends on the reference type (compile-time)
class Parent {
    String role = "Parent";
    static String familyName() { return "Smith"; }
    String introduce() { return "I am a Parent"; }
}

class Child extends Parent {
    String role = "Child";                      // Field hiding
    static String familyName() { return "Johnson"; }  // Method hiding
    String introduce() { return "I am a Child"; }     // Method overriding
}

Parent member = new Child();
System.out.println(member.role);        // Parent (field access - compile-time)
System.out.println(member.familyName()); // Smith (static method - compile-time)
System.out.println(member.introduce());  // I am a Child (instance method - runtime)

💡 Learning Tip: Remember “HIDE vs OVERRIDE” - static methods and fields are HIDDEN (reference type matters), instance methods are OVERRIDDEN (object type matters).

Q: Does overriding a method replace the original method call even if the reference is of parent type?
A: Yes — overridden instance methods use the object type at runtime (dynamic dispatch). Static methods use the reference type (they are hidden, not overridden).


🃏 Constructor Chaining and super()

Rule: If a constructor does not explicitly call super() or this(), the compiler inserts super() only if the superclass has a no-arg constructor.

class Ancestor {
    Ancestor(String msg) {
        System.out.println("Ancestor: " + msg);
    }
    // No no-arg constructor available!
}

class Parent extends Ancestor {
    // ❌ This would cause compile error:
    // Parent() {} // Implicit super() call fails
    
    // ✅ Must explicitly call super with argument:
    Parent() {
        super("Default parent message"); // Explicit call required
    }
    
    Parent(String name) {
        super("Parent: " + name);       // Explicit call required
    }
}

class Child extends Parent {
    Child() {
        // ✅ Implicit super() works - Parent has no-arg constructor
        System.out.println("Child constructor");
    }
}

Constructor execution order:

Child child = new Child();
// Output:
// Ancestor: Default parent message
// Child constructor

💡 Learning Tip: “No free lunch” - if parent needs arguments, children must provide them explicitly.


🃏 equals() Method Behavior

When a class does not override equals() from Object, .equals() compares references, just like ==.

class Person {
    String name;
    Person(String name) { this.name = name; }
    // No equals() override - inherits Object.equals()
}

Person a = new Person("John");
Person b = new Person("John");
Person c = a;

System.out.println(a.equals(b));  // false - different objects
System.out.println(a == b);       // false - different objects  
System.out.println(a.equals(c));  // true - same reference
System.out.println(a == c);       // true - same reference

// Compare with String (which DOES override equals):
String s1 = new String("hello");
String s2 = new String("hello");
System.out.println(s1.equals(s2)); // true - content comparison
System.out.println(s1 == s2);      // false - different objects

Examples of classes that DON’T override equals():

  • StringBuilder - reference comparison only
  • StringBuffer - reference comparison only
  • Most custom classes (unless explicitly overridden)

💡 Learning Tip: Classes that don’t override equals() are doing reference comparison. StringBuilder is a famous example!


🃏 protected Access Across Packages

  • Same package: accessible anywhere
  • Different package: only accessible from subclass, and only via subclass reference (not parent reference)
// File: family/Parent.java
package family;

public class Parent {
    protected void guide() { System.out.println("Parent guidance"); }
    protected String advice = "Listen to your parents";
}

// File: extended/Child.java  
package extended;
import family.Parent;

public class Child extends Parent {
    void test() {
        // ✅ Accessing through subclass (this):
        guide();                    // OK - implicit this.guide()
        this.guide();              // OK - explicit this
        System.out.println(advice); // OK - inherited field
        
        // ✅ Accessing through subclass reference:
        Child child = new Child();
        child.guide();             // OK - subclass reference
        
        // ❌ Accessing through parent reference (different package):
        Parent parent = new Parent();
        // parent.guide();         // Compile error!
        // parent.advice;          // Compile error!
        
        // ✅ But this works (casting):
        Parent parentRef = new Child();
        // parentRef.guide();      // Still compile error - reference type matters
    }
}

💡 Learning Tip: Protected across packages = “Family only, and only through your own family line.”


🃏 Static Field Access and Class Initialization

Rule: Accessing a static field only initializes the class that declares the field, not the class through which it’s accessed.

  • Class initialization is triggered by accessing a field declared by that class.
  • Inherited static fields do not trigger subclass initialization.
  • The reference used (Child.familyName) doesn’t matter - only the declaring class matters.
class Parent { 
    static String familyName = "Johnson"; 
}

class Child extends Parent {
    static { 
        System.out.print("Child initialized"); 
    }
}

public class FamilyTest {
    public static void main(String[] args) {
        System.out.println(Child.familyName);  // Accesses inherited field
    }
}

// Output: Johnson
// NOT: Child initializedJohnson

💡 Learning Tip: Remember “DECLARES WINS” - only the class that declares the static field gets initialized, even when accessed through a subclass reference.

Q: Does accessing Child.familyName initialize the Child class if familyName is declared in Parent?
A: No — only Parent gets initialized because Parent declares the field. Child inherits it but doesn’t declare it.


🃏 StringBuilder Reference Behavior

Java is pass-by-value for references. You get a copy of the reference, not the reference itself.

public class StringBuilderExample {
    static void modifyContent(StringBuilder sb) {
        sb.append(" modified");     // ✅ Modifies the object - caller sees this
        System.out.println("Inside method after append: " + sb);
    }
    
    static void reassignReference(StringBuilder sb) {
        sb.append(" first");        // ✅ Modifies original object
        sb = new StringBuilder("completely new");  // ❌ Only changes local copy of reference
        sb.append(" content");      // ❌ Modifies the new object, not original
        System.out.println("Inside method after reassign: " + sb);
    }
    
    public static void main(String[] args) {
        StringBuilder original = new StringBuilder("start");
        
        modifyContent(original);
        System.out.println("After modifyContent: " + original);  // "start modified"
        
        reassignReference(original);  
        System.out.println("After reassignReference: " + original);  // "start modified first"
        // Note: "completely new content" is lost!
    }
}

💡 Learning Tip: You can change the object’s content through the reference, but you can’t change where the original reference points.


🃏 Method Overriding Rules

Rule: Method overriding follows the “IS-A substitution” principle with specific visibility and exception rules.

  • Return type: Must be same type or covariant (subtype)
  • Access modifier: Must be same or more accessible
  • Exceptions: Can only throw same, fewer, or more specific checked exceptions
class Animal {
    protected Animal reproduce() throws IOException {
        return new Animal();
    }
    
    void makeSound() throws Exception {
        System.out.println("Animal sound");
    }
}

class Dog extends Animal {
    // ✅ Covariant return type (Dog is subtype of Animal)
    @Override
    public Dog reproduce() throws FileNotFoundException {  // More specific exception
        return new Dog();
    }
    
    // ✅ More accessible (protected -> public)
    @Override
    public void makeSound() {  // Fewer exceptions (Exception -> none)
        System.out.println("Woof!");
    }
}

// ❌ These would cause compile errors:
class BadDog extends Animal {
    // ❌ Less accessible (protected -> private)
    // private Animal reproduce() { return new BadDog(); }
    
    // ❌ Broader exception (IOException -> Exception)  
    // Animal reproduce() throws Exception { return new BadDog(); }
    
    // ❌ Different return type (Animal -> String)
    // String reproduce() { return "puppy"; }
}

Access modifier rules:

// Accessibility levels (most to least restrictive):
// private -> default -> protected -> public

class Parent {
    protected void method() {}
}

class Child extends Parent {
    public void method() {}      // ✅ OK - more accessible
    // private void method() {}  // ❌ Error - less accessible
}

💡 Learning Tip: Remember “OVERRIDE = UPGRADE” - you can make methods more accessible, return more specific types, and throw fewer/more specific exceptions, but never the reverse.

Q: Can an overriding method throw a broader checked exception than the parent method?
A: No — overriding methods can only throw the same, fewer, or more specific checked exceptions. Broader exceptions would violate the substitution principle.


🃏 Abstract Classes and Methods

Rule: Abstract classes cannot be instantiated and may contain both abstract and concrete methods.

  • Abstract methods must be implemented by concrete subclasses
  • Abstract classes can have constructors, fields, and concrete methods
  • A class with any abstract method must be declared abstract
abstract class Shape {
    protected String color;
    
    // Constructor in abstract class
    protected Shape(String color) {
        this.color = color;
    }
    
    // Abstract method - no implementation
    public abstract double getArea();
    public abstract double getPerimeter();
    
    // Concrete method - has implementation
    public String getColor() {
        return color;
    }
    
    public void displayInfo() {
        System.out.println("Color: " + color + ", Area: " + getArea());
    }
}

class Circle extends Shape {
    private double radius;
    
    public Circle(String color, double radius) {
        super(color);  // Call parent constructor
        this.radius = radius;
    }
    
    // Must implement all abstract methods
    @Override
    public double getArea() {
        return Math.PI * radius * radius;
    }
    
    @Override
    public double getPerimeter() {
        return 2 * Math.PI * radius;
    }
}

// Usage:
// Shape shape = new Shape("red");     // ❌ Cannot instantiate abstract class
Shape circle = new Circle("blue", 5);  // ✅ OK - concrete subclass
circle.displayInfo();                  // Uses both inherited and overridden methods

Abstract class inheritance rules:

abstract class Animal {
    abstract void makeSound();
    void sleep() { System.out.println("Sleeping..."); }
}

abstract class Mammal extends Animal {
    // Can choose not to implement makeSound() - remains abstract
    abstract void giveBirth();  // Add new abstract method
}

class Dog extends Mammal {
    // Must implement ALL abstract methods from hierarchy
    @Override
    void makeSound() { System.out.println("Woof!"); }
    
    @Override
    void giveBirth() { System.out.println("Giving birth to puppies"); }
}

💡 Learning Tip: Think “ABSTRACT = BLUEPRINT” - defines the structure but leaves implementation details to subclasses. Like architectural blueprints, you can’t build from them directly but they guide construction.

Q: Can an abstract class have a constructor?
A: Yes — abstract classes can have constructors that are called when concrete subclasses are instantiated via super().


🃏 Interface Implementation Rules

Rule: Interfaces define contracts that implementing classes must fulfill with specific rules for method implementation.

  • All interface methods are implicitly public abstract (unless default/static)
  • Implementing classes must provide public implementations
  • Interfaces can have default methods, static methods, and private methods (Java 8+)
interface Flyable {
    // Implicitly public abstract
    void fly();
    void land();
    
    // Default method (Java 8+)
    default void glide() {
        System.out.println("Gliding smoothly");
    }
    
    // Static method (Java 8+)
    static void checkWeather() {
        System.out.println("Weather is good for flying");
    }
    
    // Private method (Java 9+) - helper for default methods
    private void prepareForFlight() {
        System.out.println("Pre-flight check complete");
    }
}

class Bird implements Flyable {
    // Must be public - cannot reduce visibility
    @Override
    public void fly() {
        System.out.println("Bird is flying");
    }
    
    @Override
    public void land() {
        System.out.println("Bird is landing");
    }
    
    // Can override default method (optional)
    @Override
    public void glide() {
        System.out.println("Bird glides with grace");
    }
}

// ❌ This would cause compile error:
class BadBird implements Flyable {
    // ❌ Cannot reduce visibility (public -> protected)
    // protected void fly() { System.out.println("Flying"); }
}

Multiple interface implementation:

interface Swimmable {
    void swim();
    default void dive() { System.out.println("Diving deep"); }
}

interface Walkable {
    void walk();
    default void run() { System.out.println("Running fast"); }
}

class Duck implements Flyable, Swimmable, Walkable {
    @Override
    public void fly() { System.out.println("Duck flies"); }
    
    @Override
    public void land() { System.out.println("Duck lands"); }
    
    @Override
    public void swim() { System.out.println("Duck swims"); }
    
    @Override
    public void walk() { System.out.println("Duck walks"); }
    
    // Inherits default methods: glide(), dive(), run()
}

Default method conflict resolution:

interface A {
    default void method() { System.out.println("A"); }
}

interface B {
    default void method() { System.out.println("B"); }
}

class Implementation implements A, B {
    // ❌ Compile error without explicit resolution
    // Must override to resolve conflict
    @Override
    public void method() {
        A.super.method();  // Call A's version
        // or B.super.method();  // Call B's version
        // or provide own implementation
    }
}

💡 Learning Tip: Think “INTERFACE = CONTRACT” - defines what must be done (public methods) but allows flexibility in how (default methods provide optional behavior).

Q: What happens if a class implements two interfaces with conflicting default methods?
A: Compile error — the class must override the conflicting method to explicitly resolve which implementation to use or provide its own.