Java / Basic / Lesson 4
Methods, contracts, and parameter passing
Build small methods with explicit inputs, results, and failure rules. Explain pass-by-value, scope, overloading, and recursive termination.
On this page
Lesson goals
- Define a method contract and meaningful return value.
- Trace copied primitive and reference values.
- Separate overload selection from runtime dispatch.
Specify inputs, output, and failure
A method packages a named operation, but a useful contract explains more than its signature. State accepted inputs, the returned meaning, possible failures, and whether it changes observable state. A quantity-doubling helper can require a nonnegative count, return its doubled count, and reject overflow. Avoid encoding an error as a normal-looking value such as -1 unless the caller contract explicitly reserves it.
Choose names that describe the domain operation rather than the current implementation. A method returning a result is easier to reuse and test than one that always prints it. Keep console interaction at the program boundary and make the calculation a separate operation. This separation is a design tool, not a rule that every method must be pure.
Follow values across a method boundary
Parameters and local variables belong to their invocation scope. Calling twice creates separate local state for each call. return transfers control and optionally a value back to the caller; code after an unconditional return in the same path is unreachable. A non-void method must return a compatible value on all paths that can complete normally.
The example calculates a rectangle area from its two arguments and returns 12. The caller chooses to print it. Multiplication is exact only within the selected type's range; a production contract should specify allowed dimensions or use Math.multiplyExact. Tests should include zero and boundary dimensions and should not depend on hidden global variables that change between calls.
public class Main {
static int area(int width,int height) { return Math.multiplyExact(width,height); }
public static void main(String[] args) throws Exception {
System.out.println(area(3,4));
}
}
12
The two arguments become local parameters width and height. Only the caller performs output.
Explain why reference mutation is visible
Java always passes arguments by value. For a primitive, the copied value is the number or boolean. For a reference, the copied value refers to the same object, so mutating that object can be visible to the caller. Reassigning the parameter itself changes only the local copy of the reference. This distinction resolves the misleading claim that Java passes objects by reference.
The array example changes the first element to 9 and then reassigns its local parameter to a different array. The caller still refers to the original array, now containing 9. To avoid mutation, make and return a copy or use an immutable representation. Document mutation rather than expecting callers to infer it from implementation details.
public class Main {
static void change(int[] a) { a[0]=9; a=new int[]{7}; }
public static void main(String[] args) throws Exception {
int[] values={1};
change(values);
System.out.println(values[0]);
}
}
9
The element update reaches the original array. Assigning a new array to the parameter does not replace the caller's variable.
Keep overloads clear and unsurprising
Overloaded methods share a name but differ in parameter lists. The compiler chooses an applicable overload using declared types and conversion rules; return type alone cannot distinguish overloads. A method accepting int and one accepting long can be convenient, but many similar nullable reference overloads can make a call with null ambiguous. Prefer a clear API over a clever collection of signatures.
Overloading differs from overriding, where a subtype supplies behavior for an inherited instance-method contract. Runtime dispatch applies to that instance-method behavior, not to selecting an arbitrary overload based on the runtime contents of every argument. Keep these two decisions separate when tracing code, and use @Override later to let the compiler check intended overriding.
Bound recursive work and hidden dependencies
A recursive method needs a base case and a measure that moves toward it. Factorial reaches zero for nonnegative inputs only if each call decreases its argument. Negative input without validation can recurse until StackOverflowError. Even mathematically terminating recursion can exceed stack capacity for large input, so choose an iterative design when its resource contract is simpler.
Review a method with independent questions: can its accepted input be stated, can its result be checked, and can its resource use be bounded? Keep state dependencies visible as parameters or object collaborators. Tiny helpers are valuable when they capture a meaningful operation; splitting every arithmetic symbol into a method can instead obscure the reasoning.
Common errors and fixes
missing return statement
- Why it happens
- A non-void method can complete a path without a result.
- How to fix it
- Return a defined value or throw on every applicable path.
- Prevent it next time
- Trace all branches against the result contract.
reference to method is ambiguous
- Why it happens
- Several overloads accept the supplied value under competing rules.
- How to fix it
- Simplify the API or make the intended argument type explicit.
- Prevent it next time
- Avoid unrelated nullable overloads with the same name.
StackOverflowError
- Why it happens
- Recursion lacks progress or exceeds practical stack depth.
- How to fix it
- Validate the domain and use bounded iteration where appropriate.
- Prevent it next time
- Test the base case and maximum supported depth.
Check your understanding
1. A method sets its int parameter to 99. Does the caller's int change?
Show worked solution
No. The parameter received a copied primitive value. Return the new value and assign it in the caller if a replacement is intended. This differs from modifying an element through a copied array reference.
2. Define the contract for an average method accepting an int array.
Show worked solution
State whether null and an empty array are rejected, avoid integer division, choose a sufficiently wide sum, and define whether the array is mutated. Test one value, mixed signs, empty input, and values near the accepted range.
Key takeaways
- Method contracts include failure and mutation.
- All argument passing copies a value, including reference values.
- Recursion needs a base case, progress, and a resource bound.