Skip to main content
    Courses/Java/Advanced Methods

    Lesson 15 • Advanced

    Advanced Methods & Overloading

    Go beyond the basics: by the end you'll know exactly when Java chooses one method over another, why your method sometimes can't change a value, and how to design clean, flexible method signatures.

    What You'll Learn in This Lesson

    • Tell overloading apart from overriding — and predict which runs
    • Write varargs methods that accept any number of arguments
    • Create generic methods that work for any type without casting
    • Decide between static and instance methods with confidence
    • Use default and private interface methods, plus method references
    • Explain Java's pass-by-value rule and build immutable classes

    📚 Before You Start

    This lesson assumes you're comfortable with:

    • Methods — parameters, return types, and calling methods.
    • Generics — type parameters like <T>.
    • Interfaces — contracts a class promises to fulfil.

    💡 A Real-World Analogy

    Picture a coffee machine with one big "Brew" button. Press it with no cup setting and it makes a default coffee; press it after choosing "espresso" or "latte" and it does something different. The button's name never changes — what changes is the inputs. That's overloading: one method name, many parameter lists.

    Now imagine you buy a fancier machine from the same brand. The "Brew" button is in the same place and you press it the same way, but the newer machine replaces the old behaviour with a better one. That's overriding: same button, same signature, but a subclass swaps in new behaviour. Keep this picture in mind — most of this lesson is just these two ideas plus the rules around them.

    1️⃣ Overloading & Varargs — One Name, Many Shapes

    Overloading means you write several methods with the same name but different parameter lists (a different number of parameters, or different types). The compiler reads your arguments and quietly picks the best match — it decides at compile time, before the program ever runs.

    The matching happens in three steps: (1) look for an exact type match; (2) if none, try widening (e.g. intlong); (3) if still none, try autoboxing (intInteger) and varargs last. Return type alone is not enough to tell two overloads apart.

    Varargs (Type... name) let a method accept zero or more arguments. Inside the method, name is simply an array. The rule: varargs must be the last parameter, and a method can have only one.

    Read every comment in the worked example below — the result of each line is stated inline.

    Overloading and Varargs (worked example)
    public class Main {
        // OVERLOADING: same name, different parameter lists (compile-time choice)
        static int area(int side) {              // 1 arg  -> square
            return side * side;
        }
        static int area(int w, int h) {          // 2 args -> rectangle
            return w * h;
        }
        static double area(double radius) {      // double -> circle
            return Math.PI * radius * radius;
        }
    
        // VARARGS: zero or more ints. Must be the LAST parameter.
        static int sum(int... numbers) {         // "numbers" is really an int[]
            int total = 0;
            for (int n : numbers) total += n;    // loop over however many were passed
            return total;
        }
    
        public static void main(String[] args) {
            System.out.println("square(5)      = " + area(5));        // 25  -> int side
            System.out.println("rectangle(4,3) = " + area(4, 3));     // 12  -> int w,h
            System.out.printf("circle(2.0)    = %.2f%n", area(2.0));  // 12.57 -> double
    
            System.out.println("sum()          = " + sum());          // 0  args -> 0
            System.out.println("sum(1,2,3)     = " + sum(1, 2, 3));    // 3 args -> 6
            System.out.println("sum(10,20,30,40) = " + sum(10, 20, 30, 40)); // 100
        }
    }
    Output
    square(5)      = 25
    rectangle(4,3) = 12
    circle(2.0)    = 12.57
    sum()          = 0
    sum(1,2,3)     = 6
    sum(10,20,30,40) = 100
    This is real code — run it for free atonecompiler.com/javaor in your own editor.

    2️⃣ Overriding — Same Signature, Chosen at Runtime

    Overriding is different. A subclass provides its own version of a method it inherited, using the exact same signature. Java decides which version to run by looking at the actual object at runtime, not the type of the variable holding it. This is what makes polymorphism work.

    Always add the @Override annotation. It's not required, but it makes the compiler check that you really are overriding something — if you typo the name or parameters, you get an error instead of an accidental new method.

    In the example, the loop variable is typed Animal, yet each object reports its own sound. That's the heart of the overloading-vs-overriding distinction: overloading is resolved by arguments at compile time; overriding is resolved by the object at runtime.

    Overriding and runtime dispatch (worked example)
    public class Main {
        // A base class with a method subclasses can replace.
        static class Animal {
            String speak() { return "..."; }            // default behaviour
        }
    
        static class Dog extends Animal {
            @Override                                    // OVERRIDING: same signature
            String speak() { return "Woof"; }            // replaces Animal.speak()
        }
    
        static class Cat extends Animal {
            @Override
            String speak() { return "Meow"; }
        }
    
        public static void main(String[] args) {
            // The variable type is Animal, but the OBJECT decides which speak() runs.
            Animal[] zoo = { new Dog(), new Cat(), new Animal() };
            for (Animal a : zoo) {
                // Runtime dispatch: Java looks at the real object, not the variable type.
                System.out.println(a.getClass().getSimpleName() + " says " + a.speak());
            }
        }
    }
    Output
    Dog says Woof
    Cat says Meow
    Animal says ...
    This is real code — run it for free atonecompiler.com/javaor in your own editor.

    🎯 Your Turn #1 — Overload with Varargs

    Fill in the two blanks so join works both with exactly two strings and with any number of strings. The expected output is shown in the comments — run it to check yourself.

    Your Turn #1
    public class Main {
    
        // 🎯 YOUR TURN — fill in the blanks marked with ___
    
        // 1) Overload "join" so this version handles TWO strings.
        static String join(String a, String b) {
            return a + " " + b;
        }
    
        // 2) Add a VARARGS overload that joins ANY number of strings with a space.
        //    👉 the parameter type is "String..." and it must be the last parameter
        static String join(String... parts) {
            return String.join(" ", ___);     // 👉 pass the varargs array here
        }
    
        public static void main(String[] args) {
            System.out.println(join("Hello", "World"));          // two-arg version
            System.out.println(join("a", "b", "c", "d"));        // varargs version
            System.out.println(join());                          // varargs, zero args
    
            // ✅ Expected output:
            // Hello World
            // a b c d
            // (an empty line)
        }
    }
    Output
    Hello World
    a b c d
    
    This is real code — run it for free atonecompiler.com/javaor in your own editor.

    3️⃣ Generic Methods & Static vs Instance

    A generic method declares its own type parameter in angle brackets before the return type: static <T> T firstOf(T[] items). The T is a placeholder that the compiler fills in from the argument you pass, so the same method works for String[], Integer[], or anything else — with no casting and full type safety.

    A static method belongs to the class, so you call it on the class name and it can't touch any object's fields. An instance method belongs to a particular object created with new, so it can read and change that object's state. A handy rule: if the method's result depends only on its arguments, make it static; if it needs the object's data, make it an instance method.

    The Counter in the example shows the difference between a static field (shared by every object) and an instance field (one copy per object).

    Generic method, static and instance (worked example)
    public class Main {
        // GENERIC METHOD: <T> before the return type declares a type parameter.
        // Works for any type, no casting, no Object.
        static <T> T firstOf(T[] items) {
            return items[0];                       // returns the same type T it received
        }
    
        // STATIC method: belongs to the class. No object needed to call it.
        static int square(int n) { return n * n; }
    
        // Tiny counter to contrast INSTANCE state with STATIC (shared) state.
        static class Counter {
            static int created = 0;                // STATIC: shared by ALL counters
            int id;                                // INSTANCE: one per object
            Counter() { id = ++created; }          // each new object bumps the shared count
        }
    
        public static void main(String[] args) {
            String[] names = { "Ana", "Ben", "Cy" };
            Integer[] nums = { 7, 8, 9 };
            System.out.println("firstOf(names) = " + firstOf(names)); // Ana
            System.out.println("firstOf(nums)  = " + firstOf(nums));  // 7
    
            // Static method: call on the class, no object.
            System.out.println("square(6)      = " + Main.square(6)); // 36
    
            Counter a = new Counter();
            Counter b = new Counter();
            Counter c = new Counter();
            System.out.println("ids: " + a.id + ", " + b.id + ", " + c.id); // 1, 2, 3
            System.out.println("Counter.created = " + Counter.created);     // 3 (shared)
        }
    }
    Output
    firstOf(names) = Ana
    firstOf(nums)  = 7
    square(6)      = 36
    ids: 1, 2, 3
    Counter.created = 3
    This is real code — run it for free atonecompiler.com/javaor in your own editor.

    4️⃣ Default/Private Interface Methods & Method References

    Interfaces used to hold only abstract methods. Since Java 8, an interface can include a default method — a method with a body that every implementing class inherits for free. This lets you add new behaviour to an interface without breaking the classes that already implement it. Java 9 added private interface methods: helpers that a default method can call but that stay hidden from implementers and callers.

    A method reference (the :: operator) is a compact way to write a lambda that just calls an existing method. String::toUpperCase means s -> s.toUpperCase(), and System.out::println means x -> System.out.println(x). Use a method reference whenever a lambda would only forward its argument to one method — it reads more cleanly.

    Default/private methods and method references (worked example)
    import java.util.List;
    import java.util.function.Function;
    
    public class Main {
        interface Greeter {
            String name();                          // abstract: each class supplies it
    
            // DEFAULT method: a body lives in the interface. Implementers inherit it
            // for free but may override it.
            default String greet() {
                return banner() + "Hello, " + name() + "!";
            }
    
            // PRIVATE interface method: a helper for the default method only.
            // Not visible to implementers or callers.
            private String banner() {
                return ">> ";
            }
        }
    
        // This class only writes name() — greet() comes from the interface.
        static class English implements Greeter {
            public String name() { return "Sam"; }
        }
    
        public static void main(String[] args) {
            System.out.println(new English().greet());   // >> Hello, Sam!
    
            // METHOD REFERENCE: String::toUpperCase is shorthand for s -> s.toUpperCase()
            Function<String, String> shout = String::toUpperCase;
            System.out.println(shout.apply("hi"));        // HI
    
            // Static method reference passed straight to forEach.
            List.of("Ana", "Ben").forEach(System.out::println); // prints each name
        }
    }
    Output
    >> Hello, Sam!
    HI
    Ana
    Ben
    This is real code — run it for free atonecompiler.com/javaor in your own editor.

    5️⃣ Pass-by-Value — Why Your Method Can't Change That Number

    This trips up almost everyone. Java is always pass-by-value. When you call a method, Java copies each argument into the method's parameter.

    For a primitive (int, double, boolean…), the value is copied. The method works on its own copy, so changes never reach the caller's variable. For an object, the value that's copied is the reference — the address of the object. Both the caller and the method now point at the same object, so if the method mutates that object (adds to a list, changes a field), the caller sees it. But if the method reassigns its parameter to a brand-new object, only the local copy changes — the caller is unaffected.

    One sentence to remember: Java copies the reference, not the object — so you can change what the object contains, but not which object the caller's variable points to.

    Pass-by-value: primitives vs objects (worked example)
    import java.util.ArrayList;
    import java.util.List;
    
    public class Main {
        // Java is ALWAYS pass-by-value. For a primitive, the VALUE is copied.
        static void tryToChange(int x) {
            x = 999;                    // changes only this local copy
        }
    
        // For an object, the REFERENCE (an address) is copied — both point at the
        // same object, so mutating it is visible to the caller.
        static void addItem(List<String> list) {
            list.add("added inside");   // mutates the SHARED object
        }
    
        // Reassigning the copied reference does NOT affect the caller's variable.
        static void reassign(List<String> list) {
            list = new ArrayList<>();   // local copy now points elsewhere
            list.add("ignored");        // caller never sees this
        }
    
        public static void main(String[] args) {
            int n = 5;
            tryToChange(n);
            System.out.println("primitive n = " + n);   // 5 — unchanged
    
            List<String> items = new ArrayList<>();
            addItem(items);
            System.out.println("after addItem  = " + items); // [added inside]
    
            reassign(items);
            System.out.println("after reassign = " + items); // [added inside] (same)
        }
    }
    Output
    primitive n = 5
    after addItem  = [added inside]
    after reassign = [added inside]
    This is real code — run it for free atonecompiler.com/javaor in your own editor.

    6️⃣ Immutability & the Builder Pattern

    An immutable object can never change after it's created. You make a class immutable by marking every field final, setting them once in the constructor, and providing no setters. Immutable objects are easy to reason about and safe to share between threads — nobody can secretly change them under you.

    But a constructor with many parameters is hard to read (new Pizza("large", true, false, true)— which boolean is which?). The builder pattern fixes this: a small helper class with one method per option, each returning this so calls chain fluently, ending in build(). The result reads like a sentence and produces a fully-formed immutable object.

    Immutable class with a builder (worked example)
    public class Main {
        // IMMUTABLE class: all fields final, set once in the constructor, no setters.
        // Once built, a Pizza can never change — safe to share freely.
        static final class Pizza {
            private final String size;
            private final boolean cheese;
            private final boolean pepperoni;
    
            private Pizza(Builder b) {              // only the Builder can construct one
                this.size = b.size;
                this.cheese = b.cheese;
                this.pepperoni = b.pepperoni;
            }
    
            @Override public String toString() {
                return size + " pizza" +
                       (cheese ? " +cheese" : "") +
                       (pepperoni ? " +pepperoni" : "");
            }
    
            // BUILDER: each method returns "this" so calls chain fluently.
            static class Builder {
                private String size = "medium";
                private boolean cheese = false;
                private boolean pepperoni = false;
    
                Builder size(String s)   { this.size = s; return this; }
                Builder cheese()         { this.cheese = true; return this; }
                Builder pepperoni()      { this.pepperoni = true; return this; }
                Pizza build()            { return new Pizza(this); }
            }
        }
    
        public static void main(String[] args) {
            Pizza order = new Pizza.Builder()
                .size("large")
                .cheese()
                .pepperoni()
                .build();
            System.out.println(order);              // large pizza +cheese +pepperoni
    
            Pizza plain = new Pizza.Builder().build();
            System.out.println(plain);              // medium pizza
        }
    }
    Output
    large pizza +cheese +pepperoni
    medium pizza
    This is real code — run it for free atonecompiler.com/javaor in your own editor.

    🎯 Your Turn #2 — Recursive Countdown

    Fill in the two blanks so the method counts down and then prints Liftoff!. Every recursive method needs a base case (when to stop) and a step that moves toward it.

    Your Turn #2
    public class Main {
    
        // 🎯 YOUR TURN — finish the recursive method
    
        // Count down from n to 1, then print "Liftoff!".
        static void countdown(int n) {
            if (n ___ 0) {            // 👉 base case: stop when n reaches 0 (use <=)
                System.out.println("Liftoff!");
                return;
            }
            System.out.println(n);
            countdown(n ___ 1);       // 👉 recurse toward the base case (subtract 1)
        }
    
        public static void main(String[] args) {
            countdown(3);
    
            // ✅ Expected output:
            // 3
            // 2
            // 1
            // Liftoff!
        }
    }
    Output
    3
    2
    1
    Liftoff!
    This is real code — run it for free atonecompiler.com/javaor in your own editor.

    🧩 Mini-Challenge — Average of Any Scores

    No blanks this time — just a comment outline. Write a varargs method average from scratch, handle the empty case so you never divide by zero, and print the two results. The expected output is in the comments.

    Mini-Challenge
    public class Main {
    
        // 🎯 MINI-CHALLENGE: average of any number of scores
        // 1. Write a static method  double average(int... scores)
        //    - if no scores were passed, return 0.0 (avoid divide-by-zero!)
        //    - otherwise add them all and divide by scores.length
        // 2. In main, print average(80, 90, 100) and average()
        //
        // ✅ Expected output:
        // 90.0
        // 0.0
    
        // your code here
    }
    This is real code — run it for free atonecompiler.com/javaor in your own editor.

    Common Errors (and How to Fix Them)

    • Ambiguous overload. error: reference to add is ambiguous — happens when two overloads match equally well, e.g. calling add(1, 2) when both add(int, long) and add(long, int) exist. Fix: remove one overload, or cast an argument to force a single match, e.g. add(1L, 2).
    • Only the return type differs. error: method add(int,int) is already defined — you can't overload by return type alone. Fix: make the parameter lists actually different, or give the methods different names.
    • Varargs + generics warning. warning: [unchecked] Possible heap pollution from parameterized vararg type — a method like static <T> List<T> of(T... items) can't create the array safely. Fix: if the method only reads the array, annotate it @SafeVarargs (on a static, final, or private method); otherwise accept a List<T> instead.
    • Thinking Java is pass-by-reference. Reassigning a parameter inside a method (list = new ArrayList<>();) and expecting the caller to see it. Fix: remember Java copies the reference — mutate the existing object, or return the new object and let the caller reassign.
    • Recursion with no reachable base case. Exception in thread "main" java.lang.StackOverflowError — each call adds a stack frame and you never stop. Fix: add a base case that's actually reached, ensure each call moves toward it, and for very deep cases switch to a loop.

    📋 Quick Reference

    ConceptLooks LikeKey Idea
    Overloadingadd(int), add(double)Same name, different params; chosen at compile time
    Overriding@Override speak()Subclass, same signature; chosen at runtime
    Varargssum(int... n)Zero or more args; must be last parameter
    Generic method<T> T firstOf(T[])Type param before return type; no casting
    Static methodMath.max(a, b)Belongs to the class; no object needed
    Default methoddefault String greet()Interface method with a body; inherited free
    Method referenceString::toUpperCaseShorthand for a lambda that calls one method
    Pass-by-valuemethod(x)Java copies the value (or the reference)
    Buildernew Builder().size("L").build()Fluent construction of an immutable object

    Frequently Asked Questions

    What is the difference between overloading and overriding in Java?

    Overloading means several methods share one name but have different parameter lists; the compiler picks the right one at compile time based on the arguments. Overriding means a subclass replaces an inherited method using the exact same signature; Java picks the right one at runtime based on the actual object. A quick test: different parameters in the same class is overloading; same signature in a subclass is overriding.

    Is Java pass-by-reference or pass-by-value?

    Java is always pass-by-value. For primitives, the value itself is copied, so the method cannot change the caller's variable. For objects, the value that gets copied is the reference (an address), so the method can mutate the shared object, but reassigning the parameter to a new object does not affect the caller. Java never passes the variable itself, which is what true pass-by-reference would do.

    Why does varargs cause an 'unchecked generic array creation' warning?

    Java implements varargs by creating an array, but it cannot create an array of a generic type safely, so a method like void of(T... items) produces an unchecked warning at the call site. If you have verified the method only reads the array and never stores anything unsafe into it, you can suppress it with @SafeVarargs on a static, final, or private method. Otherwise, accept a List<T> instead of varargs.

    When should I use a static method instead of an instance method?

    Use a static method when the logic does not depend on any object's state, such as a pure helper like Math.max or a utility that only works with its arguments. Use an instance method when the behaviour reads or changes the fields of a particular object. A static method belongs to the class and is called on the class name; an instance method needs an object created with new.

    How do I avoid a StackOverflowError with recursion?

    Make sure every recursive method has a base case that is actually reachable and that each call moves closer to it, so the recursion terminates. Java does not optimise tail calls, so very deep recursion still grows the call stack and can overflow even with a correct base case. For large inputs, rewrite the recursion as a loop or use an explicit Deque as your own stack.

    🎉 Lesson Complete!

    Great work! You can now tell overloading from overriding, write varargs and generic methods, choose between static and instance methods, lean on default and private interface methods plus method references, explain Java's pass-by-value rule, and build clean immutable objects with a builder.

    Next up: OOP Design Patterns — apply these method skills to Singleton, Factory, Builder, and Decorator patterns.

    Sign up for free to track which lessons you've completed and get learning reminders.

    Previous

    Cookie & Privacy Settings

    We use cookies to improve your experience, analyze traffic, and show personalized ads. You can manage your preferences below.

    By clicking "Accept All", you consent to our use of cookies for analytics and personalized advertising. You can customize your preferences or reject non-essential cookies.

    Privacy PolicyTerms of Service

    Install LearnCodingFast

    Learn faster with the app on your home screen.