java-foundations 17 min read

Return Values, void, and Composition

Pick a return type, hand a value back, let the caller decide what to do with it

In a nutshell

In the last lesson, values flowed into the method through parameters. Now you learn how a value flows out: through the method’s return type. Every method either:

  • Returns a value of some declared type (int, double, boolean, String, etc.). Inside the body, the keyword return ships a value back to the caller and ends the method.
  • Or is declared void and returns nothing. Its job is the side effect (printing, modifying something, drawing).

Pick the return type before you write the body. Decide whether the caller should be able to use the result. If yes, return it. If the work is just a side effect, write void.

Today in three sentences. Methods that return are reusable. Methods that print are stuck. void is for side effects, not for hiding a missing return type.

After this lesson, you will be able to:

  • Choose between int, double, boolean, String, and void for a given problem statement, and write the matching method header.
  • Use return to both ship a value back and exit the method.
  • Explain why a non-void method must produce a value on every code path, and why ignoring a returned value is almost always a bug.
  • Compose methods so that one method’s return value flows directly into another method’s argument list.

From CSCD 110. Python lets you write def f(x): print(x) and call it as if it returns something — you just get None. Java is stricter. If you call a void method on the right side of an assignment, the compiler refuses. That strictness catches the “I forgot to return” bug at compile time instead of letting you find it at 2 a.m. with a NoneType traceback.


Picking the return type

Before you start writing the body of a method, ask one question: what does this method give back to its caller? The answer is the return type.

Question the method answers Return type
A whole number count? int
A measurement with a fractional part? double
Yes or no? boolean
A piece of text? String
A single character? char
Nothing — just do this side effect void

Worked examples.

“Convert pounds to kilograms.” A measurement, fractional. Return type: double.

public static double poundsToKg(double pounds) {
    return pounds * 0.453592;
}

“Is this score a passing grade?” Yes or no. Return type: boolean.

public static boolean isPassing(int score) {
    return score >= 60;
}

“Translate this score into a letter grade.” A piece of text. Return type: String.

public static String letterGrade(int score) {
    if (score >= 90) return "A";
    if (score >= 80) return "B";
    if (score >= 70) return "C";
    if (score >= 60) return "D";
    return "F";
}

“Print a banner with this title.” Nothing to give back, just a side effect. Return type: void.

public static void printBanner(String title) {
    System.out.println("=== " + title + " ===");
}

A useful trick: write the header first, including the return type, and only then start filling in the body. The header forces you to commit to what the method does, which usually clarifies the body.

Common pitfall: defaulting to void because the body prints. If the body prints, ask whether the caller might also want to use the value (store it, compare it, pass it on). If so, return the value and let the caller decide whether to print. A method that returns is usable everywhere a method that prints is, and in many places where the printing version is not.

Check your understanding. What return type should each method have?

A. A method daysInMonth that takes a month number and returns how many days are in it. B. A method studentGreeting that takes a name and prints “Hi, <name>!” on the screen. C. A method bmi that takes weight in kg and height in m and returns a body-mass-index value. D. A method isVowel that takes a char and answers yes-or-no.

Reveal answer

A. int (a whole-number count). B. void (its job is the side effect of printing). C. double (a fractional measurement). D. boolean (yes or no).


return: a value AND an exit

The keyword return does two things at the same time. It evaluates the expression that follows it, ships that value back to the caller, and immediately exits the method. Any code below the return is unreachable.

public static int absoluteValue(int n) {
    if (n < 0) return -n;     // exits the method right here
    return n;                  // only runs if n >= 0
}

Two consequences worth memorizing.

(1) A non-void method must return a value on every code path. The compiler checks this. If there is a way to walk through the method body without hitting a return, the compiler refuses to compile, with the error missing return statement.

// Will not compile.
public static String letterGrade(int score) {
    if (score >= 90) return "A";
    if (score >= 80) return "B";
    if (score >= 70) return "C";
    // What if score is 50? The method falls off the end with no return.
    // Compiler error: missing return statement.
}

The fix is to make sure every path returns. Either add a final unconditional return, or restructure with if/else if/else:

public static String letterGrade(int score) {
    if (score >= 90) return "A";
    if (score >= 80) return "B";
    if (score >= 70) return "C";
    return "F";   // covers everything < 70
}

(2) Ignoring the returned value of a non-void method silently throws away the work. The compiler does not warn about this. It is one of the easiest bugs to write and one of the hardest to spot in your own code.

public static int square(int n) {
    return n * n;
}

square(5);   // compiles fine. Computes 25. Throws 25 away. Useless.

To use the value, store it, print it, pass it on, or compare it. If you find yourself writing a method call as a stand-alone statement and the method’s return type is not void, double-check that you meant to.

Common pitfall: print-instead-of-return. A method whose only job is to print can be called only for that effect. A method that returns can be called and printed (System.out.println(letterGrade(85))), stored (String g = letterGrade(85)), compared (if (letterGrade(85).equals("B")) ...), or passed on (writeToFile(letterGrade(85))). Default to returning. Let main (or whichever method is in charge) handle the output.

Check your understanding. Which version is more reusable, and why?

// Version A
public static void describe(int score) {
    if (score >= 60) System.out.println("Passing");
    else             System.out.println("Failing");
}

// Version B
public static String describe(int score) {
    if (score >= 60) return "Passing";
    else             return "Failing";
}
Reveal answer

Version B is more reusable. Anywhere you want to print, you can print B (System.out.println(describe(85))). But you can also store B’s result, compare it, write it to a file, or pass it on. Version A is locked into System.out.println. If a future caller wanted to log the result to a file or check it inside an if, they would have to rewrite Version A. The general rule: do the work in the method, do the output in the caller.


void methods: do something, return nothing

A method declared with return type void does not produce a value. Its purpose is the side effect: a print, a write, a draw, a state change. You cannot use a void call on the right side of an assignment, and you cannot use it inside a larger expression.

public static void printRule(int width) {
    for (int i = 0; i < width; i++) {
        System.out.print("-");
    }
    System.out.println();
}

printRule(20);                         // OK. Side effect happens.
String s = printRule(20);              // Compiler error: void cannot be assigned.
System.out.println(printRule(20));     // Compiler error: void in expression.

Inside a void method, you have two options.

(a) Run to the closing brace. The method exits naturally. Most void methods are written this way.

(b) Use return; for an early exit. A bare return (no value) is legal in a void method and means “stop here, do not run the rest.”

public static void greet(String name) {
    if (name == null || name.isEmpty()) {
        System.out.println("(no name given)");
        return;                              // bail out early
    }
    System.out.println("Hello, " + name + "!");
}

What you cannot do is return value; in a void method. That is a compile error: cannot return a value from method whose result type is void.

public static void shout(String word) {
    return word.toUpperCase();   // Compiler error.
}

If a void method wants to give a value back, it should not be void. Change the return type to whatever the value is.

Common pitfall: putting return at the end of every void method out of habit. Functions in some languages (and the way some students were taught) end with return. In Java, a bare return; at the very end of a void method is legal but redundant: the method was about to exit anyway. Save return; for genuine early-exit cases.

Check your understanding. Which of these are legal void method bodies?

A. { System.out.println("hi"); } B. { if (x < 0) return; System.out.println(x); } C. { return 0; } D. { return; return; }

Reveal answer

A and B are legal. C is a compile error: void methods cannot return a value. D compiles, but the second return; is unreachable code (the compiler will issue a warning or an error depending on the IDE settings); after the first return; the method has already exited.


Composition: methods calling methods

Once you have several methods, the call site is itself an expression. You can use the value returned by one method directly as the argument to another. This is composition, and it is the move that lets main shrink down to a few readable lines.

public static double readPositive(Scanner kb) {
    double x = kb.nextDouble();
    while (x <= 0) {
        System.out.println("Please enter a positive number.");
        x = kb.nextDouble();
    }
    return x;
}

public static double circleArea(double radius) {
    return Math.PI * radius * radius;
}

public static void main(String[] args) {
    Scanner kb = new Scanner(System.in);
    System.out.println("Area = " + circleArea(readPositive(kb)));
}

Read the last line of main. The argument to circleArea is the return value of readPositive(kb). The argument to println is the string "Area = " concatenated with the return value of circleArea(...). Three method calls, one statement, no temporary variables.

The trace order matches the three-step protocol from lesson 5b. Java evaluates arguments left to right, fully, before any method body runs:

  1. Evaluate readPositive(kb). The body of readPositive runs to completion and produces, say, 5.0.
  2. The expression now reads circleArea(5.0). Evaluate that. The body of circleArea runs and produces 78.539....
  3. The expression now reads "Area = " + 78.539.... Evaluate the concatenation, then call println with the resulting string.

Composition keeps main flat. Each method does one thing. Each call site reads like a sentence: “Print the area of a circle whose radius the user enters.” The structure of that sentence is the structure of your code.

Common pitfall: nesting too aggressively. Composition reads beautifully when each step has a clear name. It reads horribly when six expressions get jammed onto one line. If a single statement has three or more nested method calls and any of them are not obvious from their name, break it up with named local variables. Readability beats compactness.

Check your understanding. Given the same circleArea and a method square(int n) that returns n * n, what does this print?

System.out.println(square(3) + square(4));
Reveal answer

25. Java evaluates the arguments to + left to right: square(3) returns 9, then square(4) returns 16. The expression becomes 9 + 16 = 25. println prints 25. (Note: this is not the Pythagorean theorem because there is no square root. It is just 9 + 16.)

Check your understanding. Why does this not compile?

public static void greet(String name) {
    System.out.println("Hi, " + name);
}

public static void main(String[] args) {
    System.out.println(greet("Alice"));
}
Reveal answer

greet is declared void, so the call greet("Alice") does not produce a value. But System.out.println(...) requires some value as its argument (typically a String, int, double, etc.). Trying to put a void call where a value is required is a compile error: 'void' type not allowed here. Either let greet print on its own (greet("Alice"); as a stand-alone statement) or change its return type to String and let main print the returned string.


Wrap up and what’s next

Recap.

  • Pick the return type before you write the body. The return type announces what the method gives back.
  • return carries a value back AND ends the method. Non-void methods must return on every code path.
  • Ignoring the return value silently throws away the result. The compiler does not warn. Always store, print, compare, or pass on the value.
  • void is for side effects. return value; in void is a compile error. return; (early exit, no value) is legal.
  • Composition: a method’s return value can flow straight into another method’s argument list. main shrinks. Each method does one thing.

What you can do now. Read a problem statement and write the matching method header without drafting the body. Decide whether a method should be void or return something. Read a chain of method calls and trace it using the three-step protocol from lesson 5b.

Next up: Overloading, Scope, and Decomposition. Two methods can share a name as long as their parameter lists differ. Variables declared inside a method live and die with the call. And the design move at the heart of Week 5: take a verbose main and decompose it into 3 to 5 named methods that each do one thing.


  • Reges & Stepp, Building Java Programs, Chapter 3 sections 3.3 and 3.4 cover return values and void with a Reges-style decomposition example.
  • FAQ entries on missing return statement and the print-vs-return choice.