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.
Learn Advanced Methods & Overloading in our free Java course — a beginner-friendly interactive lesson with worked examples, a practice exercise and a quick…
Part of the free Java course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.
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.
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. int → long ); (3) if still none, try autoboxing ( int → Integer ) 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.
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 .
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.
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).
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.
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 .
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.
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.
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.
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.
Practice quiz
Overloading (same name, different parameter lists) is resolved:
- At runtime, based on the actual object
- Never — it is illegal in Java
- At compile time, based on the arguments
- Only for static methods
Answer: At compile time, based on the arguments. The compiler picks the matching overload from the argument types at compile time. Overriding is the runtime one.
Overriding (a subclass replacing an inherited method with the same signature) is resolved:
- At runtime by the actual object's type
- At compile time by the variable type
- At link time
- By the order of the methods in the file
Answer: At runtime by the actual object's type. Java uses dynamic dispatch: the real object decides which overridden method runs, not the variable's declared type.
A varargs parameter (int... numbers) must be:
- The first parameter
- The only parameter
- Declared final
- The last parameter, and there can be only one
Answer: The last parameter, and there can be only one. Varargs must be the last parameter and a method may have at most one. Inside the method it is just an array.
Given overloads sum() and sum(int... n), what does sum() return?
- null
- 0
- Throws an exception
- 1
Answer: 0. Calling sum() passes an empty int[] to the varargs version; the loop adds nothing, so the total is 0.
Java's parameter passing is best described as:
- Always pass-by-value (the value, or the reference, is copied)
- Pass-by-reference for objects
- Pass-by-reference for primitives
- Pass-by-name
Answer: Always pass-by-value (the value, or the reference, is copied). Java is always pass-by-value. For objects the COPIED value is the reference, so you can mutate but not reassign.
A method does list.add("x") on a passed List, then list = new ArrayList<>(). What does the caller see afterward?
- An empty list
- A NullPointerException
- The list with "x" added; the reassignment is not visible
- The original list unchanged (no "x")
Answer: The list with "x" added; the reassignment is not visible. Mutating the shared object is visible; reassigning the local copy of the reference is not.
You CANNOT overload two methods that differ only in:
- The number of parameters
- Their return type alone
- The types of parameters
- Both number and types
Answer: Their return type alone. Return type alone is not part of the signature for overloading — the parameter lists must differ.
Where does a generic method declare its type parameter?
- After the method name
- Inside the parameter list only
- Generic methods are not allowed
A generic method puts <T> before the return type; the compiler infers T from the argument, no casting needed.
When is a static method the right choice over an instance method?
- When the logic depends on a particular object's fields
- When the result depends only on its arguments (a pure helper)
- Always — static is faster
- Only inside interfaces
Answer: When the result depends only on its arguments (a pure helper). Use static when behaviour does not read or change an object's state; use instance methods when it does.
A recursive method with no reachable base case will:
- Return null
- Loop forever silently
- Throw StackOverflowError
- Be optimized away by the JVM
Answer: Throw StackOverflowError. Java does not optimize tail calls, so endless recursion grows the call stack until it overflows.