Concepts / Designing Subroutines to Solve Subtasks

Designing Subroutines to Solve Subtasks

A subroutine header fills four slots in order: modifiers, return type, name, and parameter list, and the divisor example fills them with static, void, printDivisors, and int N.

  • Programming

Why Reuse Needs a Subroutine

Imagine a program that needs the same job done in three different places. You could copy the same lines of code three times, or you could write those lines once and let the rest of the program use them whenever they are needed. A subroutine is the second option. It is a block of code that can be called multiple times from different parts of a program.

The important word is called. A subroutine does not run just because it exists in a program. It runs when the program reaches a point where it calls it. Because the same block can be called more than once, a single piece of code can serve many moments in a single run of the program.

Subroutines are easiest to understand when you watch one do a concrete job. This article uses one small task: given a positive whole number, print every number that divides it evenly. The task is narrow on purpose. It has one input, one loop, and one decision, so every line of the subroutine has a clear reason to exist.

The task is also only a piece of a larger program. A program is assembled by breaking a big problem into subtasks, and each subtask becomes a subroutine. Writing the subroutine is one half of the work; deciding which subtasks deserve their own subroutine is the other half. This article concentrates on the first half.

Predicting the Divisor List

Before you look at the program's behavior, pause and form a prediction. Suppose a caller passes the number 12 to the subroutine. Which numbers should appear on the screen, and in what order? Write your answer down, including whether the first line of output is something other than a divisor.

What do you think happens?

For N = 12, which lines appear on the screen, and in what order?

Reveal answer
Answer:

A header line Divisors of 12:, followed by the numbers 1, 2, 3, 4, 6, and 12, one per line, in increasing order.

The loop checks every candidate from 1 up to 12 in order. Each candidate that divides 12 evenly is printed. The header line comes first because the body announces the list before checking candidates.

Reading the Header Line

Every subroutine header follows one template: modifiers, then a return type, then the subroutine name, then a parameter list in parentheses, and finally a body in braces. Writing a subroutine means filling in each slot of this template. The header of the divisor program fills the slots in a specific way.

java
Notes on the code
  • static: modifier; the course works only with static subroutines at this stage
  • void: return type; nothing is handed back to the caller
  • printDivisors: name chosen by the programmer
  • int N: the single parameter, the given integer whose divisors are printed

The modifier is static because the course is working only with static subroutines at this stage. An access modifier such as public or private could be added, but nothing in the task asks for one, so it is left out. The parameter list holds a single entry, an int named N, which represents the given integer whose divisors should be printed. The names N and printDivisors were not given by the problem; the programmer chose them.

The return-type slot describes what, if anything, the subroutine hands back to its caller. The divisor task says only to print the divisors, not to compute and return a value. A subroutine that does not return a value uses void in that slot. The printing happens as a side effect of running the body, and the caller receives nothing back.

Tracing the Loop and the If Test

The body is written from a plain-language algorithm: for each possible divisor D in the range from 1 to N, if D evenly divides N, print D. The phrase that matters is in the range from 1 to N. The loop variable D starts at 1, is tested against N before each pass, and is increased after each pass. Every candidate is checked exactly once, in increasing order.

D = 112 % 1 is 0, printedD = 212 % 2 is 0, printedD = 312 % 3 is 0, printedD = 412 % 4 is 0, printedD = 5remainder 2, skippedD = 612 % 6 is 0, printedD = 7remainder 5, skippedD = 8remainder 4, skippedD = 9remainder 3, skippedD = 10remainder 2, skippedD = 11remainder 1, skippedD = 1212 % 12 is 0, printedD = 13loop test fails, stopD++D++D++D++D++D++D++D++D++D++D++D++
In what order do the loop index and the if test run for N = 12, and which values of the index get printed?

The diagram shows the two decisions that control the output. The loop test decides whether another candidate is examined at all. The divisibility test decides whether a candidate is printed. Every path either prints a value or skips it, then moves to the next candidate, which is why the loop never needs any extra bookkeeping. For N = 12, the values 1, 2, 3, 4, 6, and 12 are printed, and the values 5, 7, 8, 9, 10, and 11 are skipped.

Worked example

Tracing printDivisors for N = 7

What does printDivisors(7) print, and how many candidates are tested?

  1. 1
    Header line

    The body first prints Divisors of 7:.

  2. 2
    Candidates 1 to 7

    The loop tests D = 1, 2, 3, 4, 5, 6, 7 in order, because the loop continues while D is less than or equal to N.

  3. 3
    Divisibility checks

    7 % 1 and 7 % 7 both leave remainder 0, so 1 and 7 are printed. Each of 2 through 6 leaves a nonzero remainder, so those are skipped.

  4. 4
    Loop ends

    When D reaches 8, the loop test fails and the subroutine finishes without returning anything.

Result

The output is the header Divisors of 7:, followed by the lines 1 and 7. Seven candidates were tested.

Who Guarantees a Positive N

Definition

The contract of a subroutine is the statement, usually written as a comment above it, that says what the subroutine does and what it assumes. The body is written under those assumptions and is not obliged to handle inputs that break them.

java
Notes on the code
  • The comment is the contract: it states the job and the assumption.
  • The body never checks whether N is positive, because the contract promises that it is.

The contract also settles who is responsible for what. The subroutine trusts its input, and the caller must make sure the assumption holds before making the call. If the caller breaks the contract, the subroutine is not obligated to produce a sensible answer, and the fault lies with the call site rather than inside the body.

One Subroutine, Several Calls

A subroutine runs when the program calls it, so writing it once lets the program reuse it at many points. The main program below calls printDivisors three times. Each call causes control to leave main, jump into printDivisors, run the body from start to finish, and return to the line after the call.

java
Output
Divisors of 12:
1
2
3
4
6
12
Divisors of 7:
1
7
Divisors of 12:
1
2
3
4
6
12
Notes on the code
  • The same block of code runs three times, with a different value of N each time.
  • The third call repeats the first call and produces the same output, because the body depends only on N.
maincalls three timesprintDivisorssame body each timeprintDivisors(12)returns, nothing sent backprintDivisors(7)returnsprintDivisors(12)returns
When control leaves main, jumps into the subroutine, and returns, how many times does this happen in one run?

The control flow makes the reuse concrete. In one run, the program leaves main three times and enters printDivisors three times. Because the subroutine returns nothing, each return simply resumes main at the next line. The subroutine never needs to know which call invoked it; it only reads the value of N it was given.

Finding Where a Wrong Result Starts

Suppose you expected the divisors of a number to appear in order, but the list stops short of the number itself. A reliable way to find the cause is to follow the loop by hand for that one input. Start with the header, then test each candidate D against the loop bound and then against the divisibility condition, noting every decision. The first point at which your hand trace and the actual output disagree is the failure location.

Hand-tracing works because the body has only a few steps. Compare your trace to the output one line at a time. If the first disagreement appears at a candidate that should have been printed, look at the divisibility test. If the output stops before the largest candidate was tested, look at the loop bound. The earliest mismatch points to the step that produced the unexpected value.

Mistakes When Writing and Calling Subroutines

  • Common mistake

    Expecting a subroutine to run just because it is written in the file

    What it looks like

    Writing printDivisors but never calling it, then wondering why nothing prints.

    Why it fails

    A subroutine runs only when the program reaches a point where it calls it.

    Do this instead

    Place a call such as printDivisors(12) in main or another method at the point where the work is needed.

  • Common mistake

    Copying the same lines of code instead of calling one subroutine

    What it looks like

    Pasting the loop and if test into three places in main to print three lists.

    Why it fails

    Every copy must be updated separately when the logic changes, which invites errors.

    Do this instead

    Write the logic once as a subroutine and call it from each place it is needed.

  • Common mistake

    Expecting void subroutines to hand back a value

    What it looks like

    Writing int x = printDivisors(12); and expecting x to hold a number.

    Why it fails

    A void return type means the subroutine produces its result by printing, not by returning a value to the caller.

    Do this instead

    Call printDivisors(12); on its own line, or change the design if a returned value is needed.

  • Common mistake

    Blaming the subroutine body for a bad argument

    What it looks like

    Calling printDivisors with a negative number and assuming the loop is broken.

    Why it fails

    The contract requires N to be positive, so the fault lies with the caller who broke the assumption.

    Do this instead

    Check the argument at the call site so the contract holds before the call.

  • Common mistake

    Forgetting that the loop checks candidates from 1 up to N, so the largest candidate N is included in the test

How ThreeN2.java Uses Subroutines

The source material points to a concrete program that uses subroutines: ThreeN2.java. It is introduced in Section 4.4 as an improved version of the 3N+1 program, and the source describes it as a program that uses subroutines and prints its output in neat columns. The source gives two reasons for this design. The first is code organization: separating work into named blocks makes the program easier to read and manage. The second is output formatting: the subroutines are part of how the results are printed in neat columns.

The source does not list the individual subroutines inside ThreeN2.java or show their code, so this article does not describe them in detail. When you open the file yourself, look for the places where the program calls a named block of code, and compare them with the places where it prints results. Those call sites show how the subroutines organize the program.

Practice: Predict Before You Trace

MEDIUM

Without running the code, predict the full output of printDivisors(6) followed by printDivisors(9). Write down the header lines, every printed number, and the order they appear in. Then trace the loop for each call and compare your answer with your prediction. Note the first line where they differ, if any.

Hints
  • Start each call with its header line, then test candidates from 1 up to the given N.
  • For 6, the candidates that divide evenly are 1, 2, 3, and 6. For 9, they are 1, 3, and 9.
  • Remember that the two calls are separate runs of the same body, so the second header line starts a new list.

Key Takeaways from Subroutine Design

  1. 1
    A subroutine is a block of code that can be called multiple times from different parts of a program, and it runs only when a call reaches it.
  2. 2
    A subroutine header fills four slots in order: modifiers, return type, name, and parameter list. The divisor example fills them with static, void, printDivisors, and int N.
  3. 3
    The loop over candidates from 1 to N combined with the divisibility test prints each divisor exactly once, in increasing order.
  4. 4
    The comment above a subroutine is its contract. The body assumes the contract holds, so the caller is responsible for satisfying it before the call.

At a glance

  • A subroutine is a reusable block of code that runs when a program calls it, and the same block can be called many times in one run.
  • A subroutine header fills four slots in order: modifiers, return type, name, and parameter list.
  • A loop over candidates from 1 to N with an if divisibility test lists all divisors of N, one check per candidate.
  • The contract above a subroutine states its assumptions; the caller must satisfy them before the call, and the body relies on them.