The Math class and its constants, composing method calls the way you compose mathematical functions, writing methods that return a value, and the incremental method that keeps you from ever debugging more than a line or two at a time. Follows Think Java 2e, Chapter 4 (Methods and Testing), Sections 4.6-4.9, pp. 58-64, cross-referenced against The Java Tutorials — Returning a Value from a Method.
Subject: Java · 65 slides · code lesson
Open the interactive version of this deck
Title
Think Java 2e · Chapter 4 · Methods and Testing
Sections 4.6-4.9 · pp. 58-64
Objectives
This lesson follows Think Java 2e, Chapter 4 (Methods and Testing), Sections 4.6-4.9, pp. 58-64. Everything on these slides can be checked against those pages.
1. Use Math methods and constants, and say why PI has no parentheses.
2. Compose method calls, passing one call's result as another's argument.
3. Recognise the 'cannot find symbol' error caused by omitting a class name.
4. Write a value-returning method: declare a return type and use a return statement.
5. Develop a method incrementally from a stub, checking intermediate values as you go.
6. Explain what scaffolding is and why you remove it.
Warm-up
One idea from the previous lesson, because this one removes its main limitation.
Discussion prompt
Why can a method not change its caller's variables? And given that, how could a method ever be useful for computing something the caller needs?
Hint: Parameter passing is an assignment. What direction does information travel?
Answer:
Because parameter passing copies the value into a new variable in a new frame. Changing the copy leaves the original alone, and when the method returns, its frame and everything in it disappears.
So information travels in through arguments. To get information back out, the method has to return a value and the caller has to catch it — which is what this lesson is about.
Concept
Every method you have written so far was void. This lesson replaces that word with a real type, so a method can compute a value and give it to whoever called it — which is what makes methods composable.
Figure (svg): A diagram showing a value passed into a method as a parameter and a result returned back to the caller
Downey & Mayfield, Think Java, 2nd edition (Green Tea Press / O'Reilly, 2020) — Think Java 2e, Chapter 4 (Methods and Testing), Sections 4.6-4.9, pp. 58-64 — Sections 4.6-4.9, printed pages 58-64.
Section
Section 4.6
Concept
The Java library contains thousands of classes you can use. The Math class provides common mathematical operations — and because it is in java.lang, you do not have to import it.
double root = Math.sqrt(17.0);
double angle = 1.5;
double height = Math.sin(angle);
double degrees = 90;
double radians = degrees / 180.0 * Math.PI;| expression | what it gives | note |
|---|---|---|
| Math.sqrt(17.0) | the square root of 17 | a method — it has parentheses |
| Math.sin(angle) | the sine of 1.5 | the argument must be in RADIANS |
| Math.PI | an approximation of pi | a constant — no parentheses |
| degrees / 180.0 * Math.PI | 90 degrees as radians | divide by 180 and multiply by pi |
Values for the trigonometric functions — sin, cos and tan — must be in radians. That is the single most common source of wrong answers when people first use the Math class.
Notation
Math.PI and Math.sqrt look similar and behave completely differently. The parentheses are the tell, as they were in Lesson 3a.
Annotate
PI is in capital letters, following the constant convention from Lesson 3a. Java does not recognise Pi, pi or pie — the capitalisation is part of the name.PI is the name of a constant, not a method, so it does not have parentheses. Writing Math.PI() is an error. The same is true of Math.E, which approximates Euler's number.sqrt and round are methods, so they take parentheses and arguments. Reading the name is not enough — the parentheses are what distinguish the two kinds of thing.toRadians and toDegrees so you do not have to write the division and multiplication by hand.Math.round returns a long, not an int. A long is like an int but bigger: an int uses 32 bits and holds up to about 2 billion; a long uses 64 and holds about 9 quintillion.The habit worth forming: take a minute to read the documentation for a class before using it. The easiest way to find it is a web search for Java and the class name.
Worked example
Do the conversion by hand first, then with the library method, and check they agree. Checking a library call against something you computed yourself is a habit worth keeping.
double degrees = 90;
double byHand = degrees / 180.0 * Math.PI;
double byLibrary = Math.toRadians(degrees);
System.out.println(byHand);
System.out.println(byLibrary);| step | expression | value |
|---|---|---|
| divide by 180 | 90 / 180.0 | 0.5 |
| multiply by pi | 0.5 * 3.14159... | 1.5707963267948966 |
| the library version | Math.toRadians(90) | 1.5707963267948966 |
| compare | — | identical |
Write 180.0 rather than 180.
Why: Lesson 2b's rule: with two ints, 90 / 180 would be 0, and the whole answer would be zero.
Multiply by Math.PI.
Why: No parentheses — it is a constant, not a method.
Compare against Math.toRadians.
Why: The library method does exactly this calculation, and agreeing with it confirms you did it right.
Prefer the library method from now on.
Why: It says what it means, and it cannot be mistyped as a wrong formula.
Verify: Both lines should print 1.5707963267948966 — half of pi, as expected for 90 degrees.
Why: If your hand version printed 0.0, you wrote 180 instead of 180.0 and integer division gave 0 before the multiplication ever happened.
Elimination
One of these treats a constant as a method.
Eliminate the wrong options
Rule out the three that are fine.
Survives elimination: B
Why: PI is the name of a constant rather than a method, so it does not take parentheses. Writing Math.PI() asks Java to call a method that does not exist. The parentheses are the difference between naming a stored value and invoking a piece of behaviour — the same distinction as System.out versus System.out.println().
Concept
Math.round returns a long, which is a type you have not met. It is worth knowing what it is and why it exists.
long x = Math.round(Math.PI * 20.0); // 63, rounded up from 62.8319| step | value |
|---|---|
| Math.PI * 20.0 | 62.83185307179586 |
| Math.round(...) | 63 — rounded to the NEAREST integer |
| the type | long |
| type | bits | largest value | roughly |
|---|---|---|---|
| int | 32 | 2 to the 31st, minus 1 | about 2 billion |
| long | 64 | 2 to the 63rd, minus 1 | about 9 quintillion |
Note the contrast with Lesson 3b: a (int) cast truncates toward zero, so (int) 62.83 would be 62. Math.round rounds to the nearest, giving 63. Two different operations, and choosing between them is a decision you should make deliberately.
Trap
The assumption. Math.sin(90) should give 1, because the sine of 90 degrees is 1.
System.out.println(Math.sin(90));| what you meant | what Java computed | result |
|---|---|---|
| sin of 90 degrees | sin of 90 RADIANS | 0.8939966636005579 |
| expected 1.0 | — | a plausible-looking wrong answer |
The result is a number between -1 and 1, so nothing looks obviously broken. This is a logic error of the worst kind: no message, and output that passes a glance.
Convert to radians first.
System.out.println(Math.sin(Math.toRadians(90))); // 1.0
// or by hand:
System.out.println(Math.sin(90 / 180.0 * Math.PI));| expression | value |
|---|---|
| Math.toRadians(90) | 1.5707963267948966 |
| Math.sin(1.5707963...) | 1.0 |
A good habit when a Math method returns a surprising number: check the units before checking anything else. Trigonometric functions take radians in essentially every programming language, not just Java.
Prediction
The same value, two ways of making it a whole number.
double d = 2.7;
System.out.println((int) d);
System.out.println(Math.round(d));| operation | behaviour | result |
|---|---|---|
| (int) d | truncates toward zero | 2 |
| Math.round(d) | rounds to the nearest | 3 |
Predict first
What two values are printed?
Correct: 2 then 3
Why: A cast to int always rounds toward zero, discarding the fractional part, so 2.7 becomes 2. Math.round rounds to the nearest integer, so 2.7 becomes 3. Choosing between them is a decision about what you want, and the two agree only when the value has no fractional part.
Matching
Two constants and two methods.
Match the pairs
Why: The first is a constant and the others are methods. Note that toRadians(180) gives pi itself, which is a useful check on your understanding of the conversion: half a turn in degrees is pi in radians.
Socratic
It could have returned an int. Ask why it does not.
Discussion prompt
Math.round(double) returns a long rather than an int. Given that a long holds far larger values than an int, why would the designers choose that — and what would go wrong with an int?
Hint: How large can a double be?
Answer:
A double can hold values far larger than any int — well beyond 2 billion. If round returned an int, rounding a large double would have nowhere to put the answer, and would have to fail or silently give a wrong one.
Returning a long moves that problem much further away without eliminating it entirely. It is a good small example of a design principle worth noticing: choose the return type that can hold every answer the method might produce, not the one that is most convenient at the call site.
The practical consequence for you: int n = Math.round(2.7); will not compile, because assigning a long to an int is a narrowing conversion. You need long n, or an explicit cast.
Section
Section 4.7
Concept
You have learned to evaluate expressions like the sine of pi over two: first evaluate the argument, then the function itself. Java methods compose the same way — you can use any expression as an argument, as long as its value has the right type.
double x = Math.cos(angle + Math.PI / 2.0);
double y = Math.exp(Math.log(10.0));
double z = Math.pow(2.0, 10.0); // 1024.0| expression | innermost first | then |
|---|---|---|
| Math.cos(angle + Math.PI / 2.0) | divide PI by 2, add angle | take the cosine of the sum |
| Math.exp(Math.log(10.0)) | log base e of 10 | raise e to that power — giving 10 back |
| Math.pow(2.0, 10.0) | two arguments, no nesting | 2 raised to the 10th, which is 1024.0 |
In Java the log method always uses base e. Some Math methods take more than one argument: Math.pow raises its first argument to the power of its second.
Picture it
Exactly the process you use for the logarithm of one over the sine of pi over two: evaluate the innermost argument, then the function around it, and repeat.
Figure (svg): A trace strip reducing a nested Math expression from the inside out in four steps
That last value is worth a comment: the cosine of pi over two is exactly zero mathematically, and Java gives a number around 10 to the minus 17. That is Lesson 2b's rounding error, appearing exactly where you were told to expect it.
Worked example
You can take the result of one method and pass it straight to another. Trace what happens, and in what order.
double x = Math.exp(Math.log(10.0));| step | expression | value |
|---|---|---|
| 1 | Math.log(10.0) | 2.302585092994046 |
| 2 | Math.exp(2.302585092994046) | 10.000000000000002 |
| 3 | assigned to x | 10.000000000000002 |
Evaluate the innermost call first.
Why: Math.log(10.0) gives the log base e of 10.
Use its result as the argument to the outer call.
Why: That value is passed to Math.exp, which raises e to that power.
Note the type must match at each step.
Why: log returns a double and exp requires a double, so the composition is legal.
Observe the answer.
Why: Mathematically exp(log(10)) is exactly 10; Java gives 10.000000000000002.
Verify: Print x and expect a value extremely close to 10 but not exactly 10.
Why: Two lessons meet here: composition works exactly as in mathematics, and floating-point arithmetic is approximate. If you had tested x == 10.0 it would have been false — which is Lesson 2b's warning about comparing doubles.
Ranking
For Math.sqrt(Math.pow(3.0, 2.0) + Math.pow(4.0, 2.0)).
Put in order
Why: Arguments are evaluated before the method that uses them, innermost first. Both pow calls must finish before their results can be added, and the addition must finish before sqrt can be given its argument. The answer, 5.0, is the hypotenuse of a 3-4-5 triangle — which is the same example the next idea uses.
Concept
When using Math methods, beginners often leave off the word Math. The resulting error message is confusing until you know what it is telling you.
double x = pow(2.0, 10.0); // no Math.| line of the message | what it means |
|---|---|
| Error: cannot find symbol | a name could not be resolved |
| symbol: method pow(double,double) | specifically, a method with this signature |
| location: class Test | it looked in YOUR class and did not find it |
The last two lines are the useful hint. If you do not specify a class name when referring to a method, the compiler looks in the current class by default — so it searched Test.java for a pow method and found none. Writing Math.pow tells it where to look.
Trap
Two mistakes with identical-looking messages. Knowing which one you have is the whole diagnosis.
double x = pow(2.0, 10.0); // forgot the class name
Scanner in = new Scanner(System.in); // forgot the import| mistake | what the message says | the giveaway |
|---|---|---|
forgot Math. | symbol: method pow — location: class Test | it names a METHOD and YOUR class |
| forgot the import | symbol: class Scanner | it names a CLASS |
Both say cannot find symbol. The lines beneath say which kind of name went missing, and that decides whether you need a class prefix or an import statement.
Read the symbol: and location: lines, and fix accordingly.
double x = Math.pow(2.0, 10.0); // class name supplied
import java.util.Scanner; // at the top of the file
Scanner in = new Scanner(System.in);| symbol: says | you need |
|---|---|
| method ... location: class YourClass | a class name in front of the method call |
| class Something | an import for that class |
| variable something | a declaration, or a fix to a typo in the name |
Three causes, one message, and the symbol: line distinguishes them every time. This is Lesson 3a's point about vocabulary paying off: the message names the kind of thing it could not find.
Prediction
One word is missing.
double x = sqrt(16.0);| line of the message | content |
|---|---|
| Error | cannot find symbol |
| symbol | method sqrt(double) |
| location | class Test |
Predict first
What is wrong, and what does the message's location: line tell you?
Correct: The class name Math is missing; the compiler searched the current class by default
Why: If you do not specify a class name when referring to a method, the compiler looks in the current class — which is exactly what location: class Test is reporting. Writing Math.sqrt(16.0) tells it where the method actually lives. Math needs no import because it is in java.lang.
Fill the middle
Compute the square root of 2 raised to the 10th power.
Fill in the blanks
double x = Math.sqrt(Math.pow(2.0, 10.0));
Why: Math.pow(2.0, 10.0) gives 1024.0, and Math.sqrt of that gives 32.0. The inner call is evaluated first and its result becomes the outer call's argument — which works because pow returns a double and sqrt requires one.
Explain it to yourself
State the rule that makes composition possible.
Discussion prompt
You can write Math.exp(Math.log(10.0)). In one sentence, say what property of a method call makes it legal to put one inside another — and say which methods this would NOT work for.
Hint: What does an argument have to be?
Answer:
A value-returning method call is an expression, and any expression whose value has the right type can be used as an argument. So a call that produces a double can go wherever a double is required.
It would not work for a void method. Math.exp(printTwice("hi")) is meaningless, because printTwice produces no value — there is nothing for exp to receive. That is the practical difference between the two kinds of method, and it is why the next section matters.
Section
Section 4.8
Concept
When you invoke a void method, the invocation is usually on a line by itself. When you invoke a value-returning method, you have to do something with the result — assign it to a variable, or use it as part of an expression.
public static double calculateArea(double radius) {
double result = Math.PI * radius * radius;
return result;
}| difference from a void method | in this example |
|---|---|
| declares the type of the return value | double where you are used to seeing void |
| uses at least one return statement | return result; |
| the caller must use the result | double area = calculateArea(5.0); |
The last line is a new form of the return statement meaning return immediately from this method, and use the following expression as the return value.
Picture it
When main invokes calculateArea, the value 5.0 is assigned to the parameter radius; calculateArea then returns 78.54, which is assigned to the variable area.
Figure (svg): A stack diagram showing main with area, and calculateArea below with radius 5.0 and result 78.54
Two crossings of the boundary, in opposite directions. The argument goes down into the new frame as a parameter; the return value comes back up. Nothing else passes between them.
Worked example
The expression you return can be arbitrarily complex, so the method can be written more concisely. Both versions are correct, and the choice is about debugging.
// with a temporary variable
public static double calculateArea(double radius) {
double result = Math.PI * radius * radius;
return result;
}
// more concisely
public static double calculateArea(double radius) {
return Math.PI * radius * radius;
}| version | advantage |
|---|---|
with result | you can print it or inspect it in a debugger before returning |
| concise | fewer lines, nothing to name |
| both | identical behaviour and identical return value |
Replace void with the type you will return.
Why: double, because the area of a circle is a floating-point value.
Compute the value.
Why: Math.PI * radius * radius — pi r squared, using the constant rather than a magic 3.14159.
Return it.
Why: The type of the expression must match the return type you declared.
Decide whether to keep the temporary variable.
Why: Think Java's advice: temporary variables like result often make debugging easier, especially when stepping through with a debugger.
Verify: Call it with 5.0 and expect approximately 78.53981633974483.
Why: Check the value independently: pi times 25 is about 78.54. Testing a method means knowing the right answer before you run it — a point the next idea makes central.
Discrimination
Decide from what the method is for, before looking at any code.
Sort into buckets
Sort each method by the kind you would write.
Concept
When you declare that the return type is double, you are making a promise that this method will eventually produce a double value. The compiler holds you to it.
| what you write | what the compiler does |
|---|---|
| return type double, return a double | accepts it |
| return type double, return with no expression | error — a value was promised |
| return type double, return a String | error — wrong type |
| return type void, return a value | error — nothing was promised |
| return type double, no return statement at all | error — missing return statement |
The type of the expression in the return statement must match the return type of the method itself. This is the same bargain as declaring a variable's type: you say what will be there, and the compiler checks every place it could go wrong.
Trap
The mistake. Calling it like a void method and expecting something to happen.
double radius = 5.0;
calculateArea(radius); // the value is computed and thrown away
System.out.println(radius); // still 5.0 — nothing changed| step | what happens |
|---|---|
| calculateArea(radius) | computes 78.54 |
| the return value | returned to the caller |
| the caller | does nothing with it — the value is discarded |
| radius | unchanged, because a method cannot change its caller's variables |
This compiles and runs. It is a logic error: the work is done and the answer is thrown away, and nothing anywhere reports a problem.
Catch the return value in a variable, or use it in an expression.
double radius = 5.0;
double area = calculateArea(radius); // caught in a variable
System.out.println(area);
System.out.println(calculateArea(3.0) * 2); // used in an expression| how the result is used | legal? |
|---|---|
| assigned to a variable | yes — the usual way |
| used as part of a larger expression | yes |
| passed as an argument to another method | yes — this is composition |
| ignored entirely | legal, but almost always a mistake |
This is the counterpart of Lesson 4a's rule. A method cannot change its caller's variables, so the only way information comes back is the return value — and only if the caller does something with it.
Prediction
Watch what the caller does with the result.
public static int doubled(int n) {
return n * 2;
}
public static void main(String[] args) {
int x = 5;
doubled(x);
System.out.println(x);
}| step | x | the returned value |
|---|---|---|
| int x = 5; | 5 | — |
| doubled(x) | 5 | 10 — returned |
| the caller ignores it | 5 | discarded |
| println(x) | 5 | — |
Predict first
What is displayed?
Correct: 5
Why: The method computes 10 and returns it, but the caller does nothing with the value, so it is discarded. x is unchanged because a method cannot modify its caller's variables. To see 10 you would write x = doubled(x); — the assignment in main is what actually changes x.
Fill the middle
A method that returns the larger of two integers is not possible yet — but the square is.
Fill in the blanks
public static int square(int n) return} n * n;
}
Why: The method produces an int, so int replaces void in the header as a promise about what will come back. The return statement then delivers on that promise, and the type of the expression n * n — an int times an int — matches what was declared.
Missing information
It declares a return type but the compiler rejects it.
Discussion prompt
public static double half(double x) { double h = x / 2.0; } does not compile. What is missing, what exactly does the compiler say, and why is this an error rather than a warning?
Hint: The header made a promise.
Answer:
The return h; statement is missing, and the compiler says missing return statement. The header promised a double and no path through the method produces one.
It is an error rather than a warning because the caller is entitled to rely on the promise. double y = half(4.0); has to receive a value — if the method could finish without producing one, there would be nothing to assign, and Java has no way to represent 'no answer' for a double.
This is a good example of a type system doing real work: the declaration is checked at both ends. The method must produce what it promised, and the caller can rely on getting it.
Section
Section 4.9
Concept
People often make the mistake of writing a lot of code before they try to compile and run it — and then spend far too long debugging. A better approach is incremental development.
Figure (svg): Three boxes stating the key aspects of incremental development: small changes, intermediate variables, and consolidating only at the end
This is the same principle Section 1.9 gave for debugging, applied to writing rather than to fixing. It is also how Linux began, as a program that switched between printing AAAA and BBBB.
Notation
The first step is to decide what the method's inputs and output are, and write the header with a placeholder body.
Annotate
The header is the hard part and the body is the easy part. Getting the inputs and output right first is what makes the rest mechanical.
Worked example
To test the method we invoke it from main with sample values. The values are not arbitrary — they are chosen so that you already know the right answer.
double dist = distance(1.0, 2.0, 4.0, 6.0);
// horizontal distance 3.0, vertical distance 4.0
// so the answer should be 5.0| quantity | value | why you know it |
|---|---|---|
| x2 - x1 | 3.0 | 4 minus 1 |
| y2 - y1 | 4.0 | 6 minus 2 |
| dsquared | 25.0 | 9 plus 16 |
| distance | 5.0 | the hypotenuse of a 3-4-5 triangle |
Pick inputs whose answer you can work out independently.
Why: A 3-4-5 triangle is the classic choice because the answer is a whole number you already know.
Write down the expected answer before running anything.
Why: When you are testing a method, it is necessary to know the right answer.
Note the intermediate values too.
Why: 3.0, 4.0 and 25.0 — each one is a checkpoint you can verify on the way.
Only then start filling in the body.
Why: Every step now has something to check against.
Verify: Once the method is finished, distance(1.0, 2.0, 4.0, 6.0) must give exactly 5.0.
Why: If it gives 25.0 you forgot the square root; if it gives 7.0 you added the differences instead of their squares. Knowing the intermediate values is what lets you tell those two apart immediately.
Ranking
Building the distance method, from nothing to finished.
Put in order
Why: The header comes first because it decides everything else; the stub is compiled immediately to catch syntax errors while there are only three lines to search. The test case is chosen before the body is written, so every subsequent step has something to check against. Scaffolding goes last, once nothing still depends on it.
Concept
The print statements you add to check intermediate values are useful while you are building and are not part of the finished method. Think Java calls them scaffolding.
public static double distance
(double x1, double y1, double x2, double y2) {
double dx = x2 - x1;
double dy = y2 - y1;
System.out.println("dx is " + dx); // scaffolding
System.out.println("dy is " + dy); // scaffolding
return 0.0; // stub
}| line | permanent or temporary? |
|---|---|
| double dx = x2 - x1; | permanent — a real step |
| println("dx is " + dx); | scaffolding — remove when finished |
| return 0.0; // stub | temporary — replaced by the real answer |
The name is exact: scaffolding is helpful for building the program but is not part of the final product. Leaving it in is not harmless — it clutters the output and confuses anyone who reads the method later.
Trap
The all-at-once approach. Forty lines, then compile, then face whatever comes.
// write all of it
// compile
// 6 errors, in 4 different lines
// fix one, recompile, 5 errors
// ...| what went wrong | where do you look? |
|---|---|
| a syntax error | anywhere in 40 lines |
| a wrong intermediate value | anywhere in 40 lines |
| two mistakes interacting | anywhere, twice |
The cost is not the number of errors — it is that each one could be anywhere. Debugging becomes a search rather than a check.
One change, then compile and run. The error is in what you just added.
// stub -> compile, run (finds header errors)
// add dx, dy -> compile, run, print (should be 3.0 and 4.0)
// add dsquared -> compile, run, print (should be 25.0)
// add sqrt -> compile, run (should be 5.0)| stage | what you check | expected |
|---|---|---|
| stub | it compiles | no output |
| differences | dx and dy | 3.0 and 4.0 |
| squares summed | dsquared | 25.0 |
| square root | the return value | 5.0 |
After each incremental change you recompile and run. If there is an error, you have a very good idea where to look: the lines you just added. As you gain experience you may write more than one line at a time — but if you find yourself spending a lot of time debugging, take smaller steps.
Prediction
It returns 0.0 and computes nothing. It seems like a wasted step.
Predict first
What is the point of compiling the stub before writing any real code?
Correct: To catch syntax errors in the header while there are only three lines to search
Why: The header is where a misspelled type, a missing comma between parameters or an unbalanced parenthesis will be, and compiling now means any such error is in the three lines you just typed. It is the first instance of the whole principle: never let the amount of unverified code get large.
Error analysis
A method part-way through development. Decide what stays and what goes.
Annotate
double dx = x2 - x1; — permanent. It is a real step in the calculation, and it is also a temporary variable that makes the value inspectable, which Think Java recommends keeping.println("dx is " + dx); — scaffolding. It exists to check an intermediate value and should be removed when the method works.double dsquared = ...; — permanent, for the same reason as dx.println("dsquared is " + dsquared); — scaffolding.return 0.0; // stub — neither. It is a placeholder to be REPLACED, not removed: the finished method still needs a return statement, just one that returns the real answer.Removing scaffolding is a real step, not tidying. A method that prints while computing cannot be used inside a larger program without spraying output everywhere.
Constraint
Practise the method itself rather than the example.
Discussion prompt
You need a method that returns the average of three doubles. Write down, in order, the four or five incremental stages you would go through — including what you would check at each one and what test values you would use.
Hint: Start with the header, and pick numbers whose average you know instantly.
Answer:
1. Decide the header: three double parameters, returning a double. 2. Write the stub return 0.0; and compile it. 3. Choose a test case — 2.0, 4.0 and 6.0, whose average is plainly 4.0. 4. Add double sum = a + b + c; and print it; expect 12.0. 5. Replace the stub with return sum / 3.0; and check the result is 4.0. 6. Remove the print.
The two decisions that matter are choosing test values with an obvious answer, and checking sum separately. If the final answer were wrong, knowing whether sum was right immediately halves the search — the bug is either in the addition or in the division, and never in both at once.
Note the 3.0 rather than 3. Sum is a double so the division is floating-point either way, but writing 3.0 says so, and Lesson 2b is a good reason to be in that habit.
Section
Section 4.9, continued
Concept
Here is the completed method. Nothing in it is complicated — the point of the section is that it was never written in this form. It was grown, one checked line at a time.
public static double distance
(double x1, double y1, double x2, double y2) {
double dx = x2 - x1;
double dy = y2 - y1;
double dsquared = dx * dx + dy * dy;
double result = Math.sqrt(dsquared);
return result;
}| line | added at stage | checked against |
|---|---|---|
| the header | 1 | it compiles |
| dx and dy | 2 | 3.0 and 4.0 |
| dsquared | 3 | 25.0 |
| result and return | 4 | 5.0 |
Each line was compiled and run before the next was added. At no point was there more than one line of unverified code in the method.
Picture it
The same method at each stage. What grows is the body; what stays constant is that it compiles and runs at every step.
Figure (svg): Two panels comparing the stub version of distance with the finished version
The left version is worth more than it looks. It proves the header is right, which is where most of the syntax errors in a new method live.
Worked example
Run the finished method by hand on the values chosen earlier, checking each intermediate against what was predicted.
double dist = distance(1.0, 2.0, 4.0, 6.0);| statement | expression | value | predicted? |
|---|---|---|---|
| double dx = x2 - x1; | 4.0 - 1.0 | 3.0 | yes |
| double dy = y2 - y1; | 6.0 - 2.0 | 4.0 | yes |
| double dsquared = dx * dx + dy * dy; | 9.0 + 16.0 | 25.0 | yes |
| double result = Math.sqrt(dsquared); | sqrt(25.0) | 5.0 | yes |
| return result; | — | 5.0 | yes |
Compute the differences.
Why: Note that they are computed as x2 minus x1, not the other way round — though squaring makes the sign irrelevant here.
Square each and add.
Why: Think Java notes you could use Math.pow, but multiplying each term by itself is simpler and more efficient.
Take the square root.
Why: Math.sqrt on the sum of the squares — the definition of distance.
Return the result.
Why: The temporary variable makes the value inspectable in a debugger, which is why it is kept.
Verify: The answer must be exactly 5.0 — the hypotenuse of a 3-4-5 triangle.
Why: Every intermediate matched its prediction, which means that had the answer been wrong, the error would have to be in the one step you had not yet checked. That is the entire payoff of developing incrementally.
Prediction
Suppose the method returned dsquared instead of its square root.
double dx = x2 - x1; // 3.0
double dy = y2 - y1; // 4.0
double dsquared = dx * dx + dy * dy;
return dsquared; // the bug| value | correct | with the bug |
|---|---|---|
| dx | 3.0 | 3.0 |
| dy | 4.0 | 4.0 |
| dsquared | 25.0 | 25.0 |
| returned | 5.0 | 25.0 |
Predict first
Which checked value first differs from what was predicted?
Correct: The returned value — every intermediate is correct
Why: All three intermediates are right, so the bug must be in the only step after them — the square root. This is precisely how incremental development pays off: because each earlier value was checked, the error is localised to the last thing added, without any searching.
Concept
Incremental development is a dial rather than a rule. The right step size depends on how confident you are, and there is a simple feedback signal for adjusting it.
| situation | step size |
|---|---|
| a language feature you have just met | one line at a time |
| a pattern you have written many times | several lines |
| you are spending a lot of time debugging | take smaller steps |
| everything works first time, repeatedly | you can afford larger ones |
Think Java's own phrasing: as you gain more experience programming, you might write and debug more than one line at a time. But if you find yourself spending a lot of time debugging, consider taking smaller steps. The amount of time you spend debugging is the signal telling you to adjust.
Trap
The mistake. Calling the new method with arbitrary numbers to 'see if it works'.
double d = distance(1.7, 2.3, 5.1, 8.8);
System.out.println(d); // 7.457881... is that right?| what you learn if it prints 7.457881 | what you learn if it prints 55.61 |
|---|---|
| nothing — it might be right | nothing — it might be right |
| you cannot tell | you cannot tell |
A test you cannot grade is not a test. It tells you the method did not crash, which you already suspected, and nothing about whether the answer is correct.
Choose values whose answer you already know.
double d = distance(1.0, 2.0, 4.0, 6.0);
System.out.println(d); // must be exactly 5.0| test case | why it is a good one |
|---|---|
| (1,2) to (4,6) | a 3-4-5 triangle — the answer is exactly 5.0 |
| (0,0) to (0,0) | the same point — the answer must be 0.0 |
| (0,0) to (3,0) | horizontal only — the answer is 3.0 |
Think Java states this as a requirement rather than advice: when you are testing a method, it is necessary to know the right answer. The other two cases here are worth adding as well — a zero distance and a purely horizontal one are the edges where a sign or a swapped variable would show up.
Definition probe
A test is useful only if you know what it should produce.
Sort into buckets
Sort each proposed test of distance.
Scale up
Four stages, each one compiled and run before the next.
Step through it
At which stage would a misspelled parameter name have been caught?
The first. That is the whole argument for compiling a stub that does nothing: it verifies the part of the method that everything else depends on, while there is almost nothing to search.
Real world
Incremental development is not just advice for beginners. It has a name in professional practice.
Discussion prompt
The rule is: never let the program be broken for long, and always know that the last change is the cause. Where have you seen that idea outside programming — and what would it look like in a team of twenty people?
Hint: Think about anything built in stages where you can go back a step.
Answer:
In a team it becomes continuous integration: everyone's changes are compiled and tested automatically, many times a day, so a break is always attributable to a change made minutes ago rather than to any of a thousand made this month.
It is the same logic scaled up. The reason to keep the program working is not tidiness — it is that a working baseline turns debugging from a search into a check. Think Java makes the point with Linux, which grew from a program that alternated printing AAAA and BBBB.
The habit transfers to anything built in steps: keep a version that works, change one thing, verify, repeat. It feels slower and is reliably faster.
Comparison
The two kinds, side by side. Fill the blanks.
Comparison matrix
| void method | value-returning method | |
|---|---|---|
| header says | void | the type of the value it returns |
| body contains | statements only | at least one return statement with an expression |
| how you call it | on a line of its own | assign the result, or use it in an expression |
| can be nested in another call? | no — there is no value to pass | yes — this is composition |
| example | printTime(11, 59); | double a = calculateArea(5.0); |
The fourth row is the practical difference. A void method is a dead end; a value-returning one can be built into something larger, which is what makes a program more than a list of actions.
Pattern
Developing any new method follows the same four stages, and the discipline is worth more than any individual piece of Java in this chapter.
// 1. header: what goes in, what comes out
public static double average(double a, double b, double c) {
return 0.0; // stub - compile it now
}
// 2. test case with a KNOWN answer
double x = average(2.0, 4.0, 6.0); // must be 4.0
// 3. one step at a time, checking intermediates
double sum = a + b + c; // print it: expect 12.0
// 4. finish, then remove the scaffolding
return sum / 3.0;| stage | what it proves |
|---|---|
| stub compiles | the header is right |
| test case chosen | you will be able to grade the output |
| intermediate checked | the error, if any, is in the last line added |
| scaffolding removed | the method is usable inside a larger program |
Check
Work it out before you click.
public static int addTen(int n) {
return n + 10;
}
public static void main(String[] args) {
int a = 5;
int b = addTen(a);
System.out.println(a + " " + b);
}| step | a | b |
|---|---|---|
| int a = 5; | 5 | — |
| addTen(a) returns 15 | 5 | — |
| b = that value | 5 | 15 |
Check your understanding
What does this display?
Answer: A
Why: The method receives a copy of a's value and returns 15, which main assigns to b. a itself is never modified, because a method cannot change its caller's variables — only the assignment in main changes anything, and it changes b.
a as well, but the parameter received a copy and the caller's variable was untouched.Check
Work it out before you click.
Check your understanding
Which line correctly computes the sine of 30 degrees?
Answer: A
Why: Trigonometric methods take radians, so the degrees must be converted first — and Math.toRadians does exactly that conversion. The result is 0.49999999999999994, which is 0.5 to within floating-point rounding error.
sin in the current class and reports 'cannot find symbol'.Check
Work it out before you click.
Check your understanding
Why does Think Java recommend compiling the stub before writing any of the method's real code?
Answer: A
Why: The header — the parameter list, the return type, the braces — is where most of a new method's syntax errors live, and compiling immediately means any such error is in the three lines you just typed rather than hidden among forty.
Real world
You could have written toRadians yourself in one line. There are reasons to look for the library version first.
Discussion prompt
Math.toRadians(d) does what d / 180.0 * Math.PI does. Give two reasons to prefer the library call, and one situation where writing it yourself would be better.
Hint: Think about what a reader sees, and what a typo would do.
Answer:
It says what it means. A reader sees the intent rather than having to recognise a formula. And it cannot be mistyped into a plausible wrong formula — writing 180 instead of 180.0 gives zero, silently.
The wider point is that the standard edition of Java comes with several thousand classes, and Think Java's advice is to take a minute to read the documentation for a class before using it. Time spent finding an existing method is usually less than time spent debugging your own.
When to write it yourself: when you are learning, and the point of the exercise is to understand the operation rather than to get the answer. That is exactly why this chapter has you compute radians both ways before telling you the library method exists.
Commit first
Commit to an answer and to your confidence.
Predict first
What is wrong with int n = Math.round(2.7);?
Correct: Math.round returns a long, and assigning a long to an int is a narrowing conversion Java will not do silently
Why: A long holds far larger values than an int, so assigning one to the other could lose information and Java refuses to do it automatically — the same rule that forbids int x = 1.1; in Lesson 2b. The fix is either long n = Math.round(2.7); or an explicit cast, int n = (int) Math.round(2.7);. Note the cast here is harmless: round has already produced a whole number, so nothing is truncated.
Explain it
Two minutes, out loud.
Discussion prompt
A classmate has written a method that computes an answer and prints it, and cannot see why anyone would return a value instead. Give them a concrete example where returning is necessary and printing will not do.
Hint: What if the caller needs to use the answer for something else?
Answer:
Suppose you have a method that computes the distance between two points. If it prints the answer, that is all it can ever do. If it returns the answer, the caller can print it — or compare it with another distance, or add it to a running total, or pass it to Math.round.
And you cannot compose printing. Math.sqrt(printDistance(...)) is meaningless, because printing produces no value. Returning is what lets a method be used as a building block rather than as a finished action.
The general principle worth stating: a method that computes should return, and a method that displays should print — and the two jobs belong in different methods. Mixing them is why the classmate's method cannot be reused.
Exit ticket
One question before you close the deck.
Predict first
What are the two things a value-returning method has that a void method does not?
Correct: A declared return type in place of void, and at least one return statement with an expression
Why: Those two go together: the header declares what type the method promises to produce, and the return statement delivers a value of that type. The compiler checks both ends — a method that declares a type and never returns one gives 'missing return statement', and a return whose expression has the wrong type is rejected too. Parameters and modifiers appear on both kinds of method, and stubs and scaffolding are development techniques rather than parts of a method.
Connect it up
One page, from memory.
Draw it
Draw the stack diagram for double area = calculateArea(5.0); at the moment just before the return, labelling which value crosses the boundary in each direction. Then write out the four stages of building the distance method, with the value you would check at each stage. Finally, write one sentence saying why a method that prints its answer cannot be composed.
Recap
Four sections that complete the chapter: methods you did not write, methods that hand a value back, and the discipline for building them without ever debugging much at once.
| if you remember one thing | it is this |
|---|---|
| about methods | compute and return; display somewhere else |
| about testing | you must know the right answer first |
| about building | never let unverified code get large |
Math.PI, not Math.PI().cannot find symbol, because the compiler searches your own class.Want this taught 1-on-1? Alexander tutors Java — $55/session, free consultation.