Skip to content

Method Overriding and Overloading in Java

Question

Compare and contrast method overriding and method overloading in Java. Explain their purposes, implementation requirements, and use cases. How do they contribute to polymorphism and code reusability?

Answer

Method overriding and overloading are two forms of polymorphism in Java that serve different purposes and have distinct implementation requirements.

Method Overriding

  1. Basic Overriding Example

    public class Animal {
        public void makeSound() {
            System.out.println("Some sound");
        }
    }
    
    public class Dog extends Animal {
        @Override
        public void makeSound() {
            System.out.println("Woof!");
        }
    }
    
    public class Cat extends Animal {
        @Override
        public void makeSound() {
            System.out.println("Meow!");
        }
    }
    

  2. Runtime Polymorphism

    public class Shape {
        public double calculateArea() {
            return 0.0;
        }
    }
    
    public class Circle extends Shape {
        private double radius;
    
        @Override
        public double calculateArea() {
            return Math.PI * radius * radius;
        }
    }
    
    public class Rectangle extends Shape {
        private double length;
        private double width;
    
        @Override
        public double calculateArea() {
            return length * width;
        }
    }
    

Method Overloading

  1. Basic Overloading Example

    public class Calculator {
        public int add(int a, int b) {
            return a + b;
        }
    
        public double add(double a, double b) {
            return a + b;
        }
    
        public String add(String a, String b) {
            return a + b;
        }
    }
    

  2. Constructor Overloading

    public class Person {
        private String name;
        private int age;
    
        public Person() {
            this.name = "Unknown";
            this.age = 0;
        }
    
        public Person(String name) {
            this.name = name;
            this.age = 0;
        }
    
        public Person(String name, int age) {
            this.name = name;
            this.age = age;
        }
    }
    

Key Differences

  1. Signature Requirements

    public class OverrideExample {
        // Overriding - Same signature
        public class Parent {
            public void method(int x) {}
        }
    
        public class Child extends Parent {
            @Override
            public void method(int x) {}  // Must match exactly
        }
    
        // Overloading - Different signatures
        public class OverloadExample {
            public void method(int x) {}
            public void method(double x) {}
            public void method(int x, int y) {}
        }
    }
    

  2. Access Modifiers

    public class AccessModifiers {
        public class Parent {
            protected void method() {}
        }
    
        public class Child extends Parent {
            @Override
            public void method() {}  // Can be more accessible
    
            // @Override
            // private void method() {}  // Cannot be less accessible
        }
    }
    

Best Practices

  1. Override Annotation

    public class AnnotationExample {
        public class Parent {
            public void method() {}
        }
    
        public class Child extends Parent {
            @Override  // Good practice - catches errors
            public void method() {}
    
            // Without @Override - might miss typos
            public void metod() {}  // Compiles but doesn't override
        }
    }
    

  2. Covariant Return Types

    public class CovariantReturn {
        public class Animal {
            public Animal create() {
                return new Animal();
            }
        }
    
        public class Dog extends Animal {
            @Override
            public Dog create() {  // Can return subclass
                return new Dog();
            }
        }
    }
    

Common Pitfalls

  1. Accidental Overloading

    public class AccidentalOverload {
        public class Parent {
            public void method(Object obj) {}
        }
    
        public class Child extends Parent {
            public void method(String str) {}  // Overloads, doesn't override
    
            @Override
            public void method(Object obj) {}  // Correct override
        }
    }
    

  2. Static Methods

    public class StaticMethods {
        public class Parent {
            public static void method() {
                System.out.println("Parent");
            }
        }
    
        public class Child extends Parent {
            public static void method() {  // Hides, doesn't override
                System.out.println("Child");
            }
        }
    }
    

Use Cases

  1. Template Method Pattern

    public class TemplateMethod {
        public abstract class Beverage {
            public final void prepare() {
                boilWater();
                brew();
                pourInCup();
                addCondiments();
            }
    
            protected abstract void brew();
            protected abstract void addCondiments();
    
            private void boilWater() {
                System.out.println("Boiling water");
            }
    
            private void pourInCup() {
                System.out.println("Pouring in cup");
            }
        }
    
        public class Coffee extends Beverage {
            @Override
            protected void brew() {
                System.out.println("Dripping coffee");
            }
    
            @Override
            protected void addCondiments() {
                System.out.println("Adding sugar and milk");
            }
        }
    }
    

  2. Builder Pattern

    public class BuilderPattern {
        public class Pizza {
            private String crust;
            private String sauce;
            private String topping;
    
            public static class Builder {
                private Pizza pizza = new Pizza();
    
                public Builder crust(String crust) {
                    pizza.crust = crust;
                    return this;
                }
    
                public Builder sauce(String sauce) {
                    pizza.sauce = sauce;
                    return this;
                }
    
                public Builder topping(String topping) {
                    pizza.topping = topping;
                    return this;
                }
    
                public Pizza build() {
                    return pizza;
                }
            }
        }
    }
    

Testing

  1. Testing Overridden Methods

    @Test
    public void testAnimalSounds() {
        Animal dog = new Dog();
        Animal cat = new Cat();
    
        // Runtime polymorphism
        assertEquals("Woof!", captureOutput(dog::makeSound));
        assertEquals("Meow!", captureOutput(cat::makeSound));
    }
    

  2. Testing Overloaded Methods

    @Test
    public void testCalculator() {
        Calculator calc = new Calculator();
    
        assertEquals(5, calc.add(2, 3));
        assertEquals(5.0, calc.add(2.0, 3.0));
        assertEquals("23", calc.add("2", "3"));
    }