Parameters, Return Types, and Method Overloading

8 min read

A method's parameters are the inputs it expects; the return type is what it gives back. Both are typed at compile time, so the compiler refuses to call a method with the wrong shape — a giant safety net for any test framework with more than a few helpers. This lesson covers parameters in detail, including the difference between primitive and object arguments, and method overloading — defining several methods with the same name but different parameter lists. Selenium's findElement(...), JUnit's assertEquals(...), and almost every helper in Rest Assured are overloaded; once you've written your own, the docs for those libraries stop looking magic.

Parameters with multiple types

A method can take any number of parameters of any types:

public static void logTestResult(String testName, boolean passed, long durationMs) {
    String status = passed ? "✅ PASS" : "❌ FAIL";
    System.out.println(status + " " + testName + " (" + durationMs + "ms)");
}

Calling it:

logTestResult("Login", true, 1450);
logTestResult("Checkout", false, 3120);

Output:

✅ PASS Login (1450ms)
❌ FAIL Checkout (3120ms)

The parameter list is comma-separated Type name pairs. The order matters — call sites must pass arguments in the same order as the declaration. Java has no equivalent to Python's keyword arguments — logTestResult(passed=true, ...) doesn't exist. If your method takes more than three or four parameters, that's usually a sign you should bundle them into an object (chapter 4).

Return types — what you can give back

Anything Java can hold:

public static int countFailures(boolean[] results) { ... }            // primitive
public static double calculatePassRate(int p, int t) { ... }          // primitive
public static String formatDuration(long ms) { ... }                  // String
public static boolean validateResponse(int code, String body) { ... } // boolean
public static void logResult(String message) { ... }                  // nothing — void

The compiler checks every return against the declared type. return "1450"; from a method declared int won't compile. This sounds restrictive; it catches surprisingly many real bugs early.

Inside the body you can have multiple return statements as long as every code path eventually reaches one:

public static String classify(int code) {
    if (code >= 500) return "server error";
    if (code >= 400) return "client error";
    if (code >= 200) return "success";
    return "unknown";        // catch-all so every path returns
}

If you forget the trailing return "unknown";, the compiler complains: missing return statement.

Pass by value, even for objects

Java is pass-by-value, but for objects the value passed is a reference (a pointer). The practical consequences confuse a lot of beginners:

public static void incrementInside(int x) {
    x = x + 1;           // changes the LOCAL copy
}
 
public static void addRow(int[] rows) {
    rows[0] = 999;       // mutates the array the reference points at
}
 
public static void replaceArray(int[] rows) {
    rows = new int[]{99, 99, 99};  // local reassignment, has NO effect outside
}
 
public static void main(String[] args) {
    int n = 5;
    incrementInside(n);
    System.out.println(n);                  // 5  — primitive copied, original unchanged
 
    int[] data = {1, 2, 3};
    addRow(data);
    System.out.println(data[0]);            // 999 — same array, mutated through the reference
 
    replaceArray(data);
    System.out.println(data[0]);            // still 999 — `data` outside still points at the old array
}

Output:

5
999
999

The rule:

  • Primitives (int, boolean, double, etc.) are copied. Modifying the parameter inside the method has no effect outside.
  • Objects (arrays, String, custom classes) — the reference is copied, so both names point at the same object. Mutating the object's fields/elements is visible outside; reassigning the parameter to a new object is not.

This rule explains why mutating an array passed into a helper method changes it for the caller too — Selenium's WebElement lists, Rest Assured response objects, and your own test data classes all behave the same way.

Method overloading — same name, different parameters

Java lets you define multiple methods with the same name as long as their parameter lists differ. Calling code picks the right one based on the argument types:

public static String formatDuration(long ms) {
    return ms + "ms";
}
 
public static String formatDuration(long ms, boolean verbose) {
    if (verbose) return ms + " milliseconds";
    return ms + "ms";
}
 
public static String formatDuration(double seconds) {
    return seconds + "s";
}

Calling:

System.out.println(formatDuration(1450));        // "1450ms"        — long version
System.out.println(formatDuration(1450, true));  // "1450 milliseconds" — long+boolean version
System.out.println(formatDuration(1.45));        // "1.45s"          — double version

Output:

1450ms
1450 milliseconds
1.45s

The compiler resolves the call at compile time based on the static types of the arguments, not at runtime. If two overloads are equally valid, you get a compile error and have to disambiguate (usually with a cast).

Two facts that trip people up:

  • Overloads must differ in parameter types (or count). You cannot overload on return type alone — int foo() and String foo() is a compile error.
  • Overloads with different names are not overloads at all. findById(...) and findByCss(...) are simply two separate methods; the overloading mechanism is only invoked when the names match.

Overloading vs separate methods

Method overloading vs separate-name methods

Overloaded — same name, different parameters

  • findElement(String selector)

  • findElement(By locator)

  • findElement(WebElement parent, String selector)

  • Caller writes findElement(...) — compiler picks the right one based on argument types

  • Used for: same logical action, different input shapes

Separate methods — different names

  • findById(String id)

  • findByCss(String css)

  • findByXpath(String xpath)

  • Caller picks which to call by name — no compiler resolution

  • Used for: actions that conceptually do different things

Selenium uses overloading for findElement because every variant does the same job (find one element); the only thing that changes is how you describe the element. Use overloading for genuine variants, not as a way to cram unrelated methods under one name.

Real overloading — defaults via overloads

Java has no default parameter values (Python and Kotlin do). The standard workaround is to overload with shorter parameter lists that delegate to the longest one:

public class UserFactory {
 
    public static String createUser(String email, String role, boolean isVerified) {
        return "User{email=" + email + ", role=" + role + ", verified=" + isVerified + "}";
    }
 
    public static String createUser(String email, String role) {
        return createUser(email, role, true);          // default: verified
    }
 
    public static String createUser(String email) {
        return createUser(email, "MEMBER", true);      // default: MEMBER role, verified
    }
 
    public static void main(String[] args) {
        System.out.println(createUser("alice@x.com"));
        System.out.println(createUser("bob@x.com", "ADMIN"));
        System.out.println(createUser("guest@x.com", "GUEST", false));
    }
}

Output:

User{email=alice@x.com, role=MEMBER, verified=true}
User{email=bob@x.com, role=ADMIN, verified=true}
User{email=guest@x.com, role=GUEST, verified=false}

Each overload calls the next-longer one with sensible defaults. The "real work" lives in one method; the others are thin wrappers. This is the JUnit pattern (assertEquals(expected, actual) vs assertEquals(expected, actual, message)) and it's how almost every Java test library handles optional parameters.

⚠️ Common mistakes

  • Trying to overload on return type. int run() and String run() is a compile error — the parameter lists are identical. Overloading needs different parameter types or counts. If you want both shapes, use different names: runAndCount() and runAndDescribe().
  • Assuming objects are pass-by-reference. Reassigning a parameter inside a method (rows = new int[]{...};) has no effect outside; only mutating the existing object does. Forget this and you'll write a "helper" that "doesn't update the caller" and tear your hair out.
  • Mixing up == for primitives and objects in parameter checks. if (env == "staging") inside a method has the same trap as in lesson 2.1 — use .equals(...). Methods don't change the rules, they just spread the rules across more files.

🎯 Practice task

Build a typed test helpers class with overloads. 25-30 minutes.

  1. Create TestUtils.java.
  2. Write three overloads of assertEquals:
    • public static void assertEquals(int expected, int actual)
    • public static void assertEquals(String expected, String actual) ← use .equals() for the comparison!
    • public static void assertEquals(int expected, int actual, String message) — adds a label to the failure print
  3. Each overload should print OK if the values match (use == for ints, .equals() for Strings), or FAIL: expected X, got Y (and include the message in the third overload).
  4. Write three overloads of createUser:
    • createUser(String email) — defaults role to "MEMBER" and verified to true
    • createUser(String email, String role) — defaults verified to true
    • createUser(String email, String role, boolean verified) — the full version The first two should call the third (chained overloads, like the example above). Have the full version print and return a description string.
  5. In main, call each overload at least once. Force one assertion to fail on purpose to see the FAIL output.
  6. Stretch: add a method incrementCounters(int[] counters) that increments every element by 1 in place. Call it and confirm the array passed in is mutated (because arrays are passed by reference). Then write a sibling method replaceCounters(int[] counters) that does counters = new int[]{0,0,0}; and confirm — to your surprise — the caller's array is not replaced. Tracing through that difference is the fastest way to internalise pass-by-value-of-references.

You now have parameters, return types, overloads, and Java's argument-passing rules nailed. Lesson 3 introduces arrays — the simplest container Java offers and the foundation every collection in chapter 6 builds on.

// tip to track lessons you complete and pick up where you left off across devices.