The args parameter explained at last, validating what arrives from outside the program, BigInteger for numbers too large for a long, and the encapsulate-and-generalize process that turns a few working lines into a reusable method. Follows Think Java 2e, Chapter 9 (Immutable Objects), Sections 9.5-9.9, pp. 152-160, cross-referenced against Java SE 21 API — java.math.BigInteger.
Subject: Java · 65 slides · code lesson
Open the interactive version of this deck
Title
Think Java 2e · Chapter 9 · Immutable Objects
Sections 9.5-9.9 · pp. 152-160
Objectives
This lesson follows Think Java 2e, Chapter 9 (Immutable Objects), Sections 9.5-9.9, pp. 152-160. Everything on these slides can be checked against those pages.
1. Read command-line arguments from args, and explain why its elements are Strings.
2. Display a usage message when required arguments are missing.
3. Validate a String parameter against both null and empty, in the right order.
4. Explain why the null check must come first, using short-circuit evaluation.
5. Use BigInteger for values beyond Long.MAX_VALUE, and say why the usual operators do not work on it.
6. Apply encapsulation and generalization to turn working code into a parameterised method.
Warm-up
A parameter you have copied since Chapter 1 without knowing what it was for.
Discussion prompt
Every main method you have written begins public static void main(String[] args). What do you now know about the type String[], and what would its elements be?
Hint: You met the type in Chapter 7 and the element type in Chapter 9.
Answer:
String[] is an array of Strings — so args is a variable holding a reference to an array whose elements are String objects.
That is everything you need. Chapter 1 said this part would be explained once you knew about arrays and Strings, and now you do — so this lesson opens by finally using it.
Concept
Now that you know about strings, arrays and wrapper classes, the args parameter can finally be explained. It is the first of several places this lesson deals with data arriving from outside — and everything that arrives from outside arrives as text.
Figure (svg): A pipeline showing command-line words becoming an array of strings, then being validated, then parsed into numbers
Downey & Mayfield, Think Java, 2nd edition (Green Tea Press / O'Reilly, 2020) — Think Java 2e, Chapter 9 (Immutable Objects), Sections 9.5-9.9, pp. 152-160 — Sections 9.5-9.9, printed pages 152-160.
Section
Section 9.5
Concept
Rather than reading numbers from System.in with a Scanner, we can pass them as command-line arguments. Whatever you type after the class name is passed to main as the elements of args.
import java.util.Arrays;
public class Max {
public static void main(String[] args) {
System.out.println(Arrays.toString(args));
}
}| what you type | args contains | args.length |
|---|---|---|
| java Max | [] | 0 |
| java Max 10 -3 55 0 14 | [10, -3, 55, 0, 14] | 5 |
| java Max hello | [hello] | 1 |
Note that with no arguments, args is an empty array — it has no elements, but it is not null. That distinction is exactly the one from Lesson 9a, and it matters in a moment.
Picture it
It is not clear from the output, but every element of args is a String — even the ones that look like numbers.
Figure (svg): An array of five string elements holding the text forms of 10, -3, 55, 0 and 14
So args is the array {"10", "-3", "55", "0", "14"}. To find the maximum number, we have to convert the arguments to integers — which is what Integer.parseInt from Lesson 9a is for.
Worked example
An enhanced for loop parses each argument and keeps the largest value seen so far. The starting value of max is the interesting decision.
int max = Integer.MIN_VALUE;
for (String arg : args) {
int value = Integer.parseInt(arg);
if (value > max) {
max = value;
}
}
System.out.println("The max is " + max);| arg | value | greater than max? | max after |
|---|---|---|---|
| (start) | — | — | -2147483648 |
| "10" | 10 | yes | 10 |
| "-3" | -3 | no | 10 |
| "55" | 55 | yes | 55 |
| "0" | 0 | no | 55 |
| "14" | 14 | no | 55 |
Initialise max to the smallest possible int.
Why: Integer.MIN_VALUE — that way the first value parsed will replace it, whatever it is.
Parse each argument.
Why: The elements are Strings, so each must be converted before it can be compared numerically.
Keep the larger value.
Why: This is Lesson 7a's reduce pattern with an accumulator, and the accumulator is declared before the loop.
Notice why 0 would be wrong as a starting value.
Why: For the arguments -5 -3 -9 the answer would come out 0, which is not in the list at all.
Verify: Run java Max 10 -3 55 0 14 and expect The max is 55.
Why: Then run java Max -5 -3 -9 and expect -3. If you had started max at 0 you would get 0 — which is the edge case that makes Integer.MIN_VALUE the right choice, and it is exactly the empty-maximum problem raised in Lesson 7a.
Prediction
Count the words after the class name.
// java Max 10 -3 55| typed | goes into args? |
|---|---|
| java | no — the command |
| Max | no — the class name |
| 10, -3, 55 | yes — three elements |
Predict first
What is args.length?
Correct: 3
Why: Only the words after the class name become arguments — java is the command and Max names the class to run. So args holds three String elements, and running the program with no arguments at all gives an empty array of length 0 rather than null.
Concept
If args is empty the loop never runs and the result is MIN_VALUE, which is nonsense. We can prevent that by checking at the beginning.
if (args.length == 0) {
System.err.println("Usage: java Max <numbers>");
return;
}| element | purpose |
|---|---|
| args.length == 0 | no arguments were supplied |
| System.err | this is an error, so it goes to the error stream |
| "Usage: java Max <numbers>" | tells the user how to run the program |
| return | leaves main, which ends the program |
It is customary for programs requiring command-line arguments to display a usage message when the arguments are not valid. Run javac or java with no arguments and you will get a very long one. This is Lesson 5b's guard clause, applied to a different source of input.
Trap
Every argument is a String, and not every string parses.
// java Max 10 hello 55
for (String arg : args) {
int value = Integer.parseInt(arg); // throws on "hello"
...
}| arg | parseInt result |
|---|---|
| "10" | 10 |
| "hello" | NumberFormatException — the program stops |
| "55" | never reached |
The program crashes partway through with a stack trace, which tells the user nothing useful about what they typed wrongly. This is Lesson 9a's parsing hazard, now at a boundary where a user supplies the data directly.
Validate, or handle the failure — the same two options as always.
for (String arg : args) {
int value;
try {
value = Integer.parseInt(arg);
} catch (NumberFormatException e) { // Chapter 15
System.err.println(arg + " is not a number");
return;
}
...
}| approach | available now? |
|---|---|
| check args.length | yes — and the book does this |
| catch NumberFormatException | Chapter 15 |
| check each argument's characters first | possible, but tedious |
The book's Max program checks the count but not the content, which is honest about what you can do with the tools so far. Knowing that the gap exists is the point — and Chapter 15's exception handling is what closes it.
Prediction
Consider a list of negative numbers.
int max = 0; // instead of MIN_VALUE
for (String arg : args) { ... }
// java Max -5 -3 -9| starting max | for -5 -3 -9 | correct? |
|---|---|---|
| 0 | 0 — never replaced | no |
| Integer.MIN_VALUE | -3 | yes |
Predict first
What goes wrong if max starts at 0?
Correct: For all-negative input it returns 0, which is not in the list
Why: No negative value is greater than 0, so max is never replaced and the program reports a number that was never supplied. Starting at Integer.MIN_VALUE guarantees the first parsed value replaces it, whatever it is — which is the standard way to initialise a maximum accumulator.
Definition probe
Everything in this program starts as text.
Sort into buckets
Sort each item by its type.
Real world
A Scanner would also let the user supply numbers.
Discussion prompt
The Max program could have read its numbers with a Scanner. What does taking them on the command line make possible that prompting for them does not?
Hint: Think about running the program a thousand times.
Answer:
It makes the program scriptable. A command-line program can be run by another program, with arguments supplied automatically, a thousand times over — nobody has to sit and type at a prompt.
It also makes the invocation repeatable and recordable: java Max 10 -3 55 is a complete description of a run that you can put in a file, share, or re-run months later.
That is why javac and java themselves take arguments this way, and why so much professional tooling is command-line driven. Prompting is for people; arguments are for people and programs both.
Section
Section 9.6
Concept
As Section 5.9 discussed, you should never assume program input will be in the correct format. Users make mistakes; someone might make intentional mistakes to see what your program does; and programmers make mistakes too. It is good practice to validate arguments passed to methods, including main.
public static boolean isCapitalized(String str) {
return Character.isUpperCase(str.charAt(0));
}| str | what str.charAt(0) does |
|---|---|
| "Ada" | returns 'A' — fine |
| "" | StringIndexOutOfBoundsException — no character at index 0 |
| null | NullPointerException — cannot invoke a method on null |
str.charAt(0) makes two assumptions: that the String object referenced by str exists, and that it has at least one character. Both can fail at run time, and they fail with different exceptions.
Picture it
The two failure modes correspond exactly to the two pictures from Lesson 9a — and each needs its own test.
Figure (svg): Two variables: str1 is null with no arrow, and str2 points to an empty String object
null and empty are different concepts. str1 does not reference an object at all; str2 refers to the empty string, an object that exists. A single test cannot catch both, which is why the guard has two conditions.
Worked example
Prevent both exceptions by validating at the start of the method. If the argument is invalid, return before executing the rest.
public static boolean isCapitalized(String str) {
if (str == null || str.isEmpty()) {
return false;
}
return Character.isUpperCase(str.charAt(0));
}| str | str == null | str.isEmpty() | returns |
|---|---|---|---|
| null | true | not evaluated | false |
| "" | false | true | false |
| "ada" | false | false | false — from charAt |
| "Ada" | false | false | true |
Check for null first.
Why: You cannot invoke any method on null, so this must come before anything else.
Then check for empty.
Why: isEmpty() is safe now, because we know str refers to an object.
Return early if either check fails.
Why: The guard-clause shape from Lesson 5b: handle the bad case and leave, so the rest of the method can assume good input.
Only then do the real work.
Why: str.charAt(0) is now guaranteed safe — there is an object and it has at least one character.
Verify: Test with null, "", "ada" and "Ada" and expect false, false, false, true.
Why: The first two return false without throwing, which is the whole point of the guard. Note that returning false for null is a decision — you could equally throw an exception to say the caller made a mistake, and Chapter 15 gives you that option.
Prediction
The unguarded version of the method.
public static boolean isCapitalized(String str) {
return Character.isUpperCase(str.charAt(0));
}| str | what fails |
|---|---|
| null | invoking charAt on nothing |
| "" | there is no character at index 0 |
Predict first
What does the empty string cause?
Correct: StringIndexOutOfBoundsException
Why: The empty string is a real object, so invoking charAt on it is fine — but there is no character at index 0, so the index is out of bounds. A null reference would give a NullPointerException instead. Two different failures needing two different tests, which is why the guard has two conditions.
Concept
Beginners sometimes check for empty first. Doing so causes a NullPointerException — the very exception the guard was written to prevent.
// wrong:
if (str.isEmpty() || str == null) { ... }
// right:
if (str == null || str.isEmpty()) { ... }| order | when str is null | outcome |
|---|---|---|
| isEmpty() first | isEmpty() is invoked on null | NullPointerException |
| null check first | the || short-circuits | returns false safely |
Checking for null first works because of short-circuit evaluation from Lesson 5b: if str == null is true, the || evaluates to true immediately and str.isEmpty() is never called. This is the guard pattern that lesson introduced — put the protective test on the left — and here it is load-bearing rather than merely tidy.
Trap
Both tests are present and the order defeats them.
public static boolean isCapitalized(String str) {
if (str.isEmpty() || str == null) { // wrong order
return false;
}
return Character.isUpperCase(str.charAt(0));
}| str | first operand evaluated | result |
|---|---|---|
| "Ada" | isEmpty() is false | proceeds correctly |
| "" | isEmpty() is true | returns false correctly |
| null | isEmpty() invoked on null | NullPointerException |
Two of the three cases work, so this passes any test that does not include null. The guard looks complete and fails on precisely the input it was written for.
Null first, always. The || then protects the second test.
public static boolean isCapitalized(String str) {
if (str == null || str.isEmpty()) {
return false;
}
return Character.isUpperCase(str.charAt(0));
}| general rule | why |
|---|---|
| test for null before dereferencing | you cannot call a method on nothing |
| put the cheap or protective test on the left of &&/|| | short-circuit evaluation skips the right one |
| validate at the start of the method | the rest can then assume good input |
The middle row is the generalisation, and it is the same rule that made count != 0 && total / count > 10 safe in Lesson 5b. Whenever one test protects another, the protector goes on the left.
Ranking
Each test assumes the previous ones passed.
Put in order
Why: You cannot ask whether an object is empty until you know there is an object, and you cannot look at its first character until you know it has one. Each test makes the next one safe, which is why the order is forced rather than a matter of taste.
Fill the middle
Return false for a null or empty string.
Fill in the blanks
if (str == null || str.isEmpty()) ___
Why: == is the correct comparison against null — this is the one case where == on an object reference is right, because you genuinely are asking about identity with nothing. And || short-circuits, so when the reference is null the second test never runs.
Counterexample
The book returns false. That is a choice, not a rule.
Discussion prompt
isCapitalized(null) returns false. An alternative would be to throw an exception saying the caller made a mistake. What is the argument for each, and how would you decide?
Hint: Is null a reasonable thing for a caller to pass here?
Answer:
Returning false treats null as a legitimate input meaning no string, and lets callers pass whatever they have without checking first. It is forgiving.
Throwing treats null as a caller bug and reports it immediately, at the point where it can be fixed — rather than quietly returning an answer that may be wrong for a different reason.
The deciding question is whether null is expected. If callers routinely have optional strings, returning false is kind. If null would only ever arrive because of a bug, silently returning false hides it. Both appear throughout the Java library, and being able to say which you chose and why is what matters.
Section
Section 9.7
Concept
It might not be clear why you would need an integer object when you can use an int or a long. One reason is the variety of methods the wrappers provide. Another is when you need very large integers that exceed Long.MAX_VALUE.
import java.math.BigInteger;
long x = 17;
BigInteger big = BigInteger.valueOf(x); // from a number
String s = "12345678901234567890";
BigInteger bigger = new BigInteger(s); // from a string| type | largest value | roughly |
|---|---|---|
| int | Integer.MAX_VALUE | 2 billion |
| long | Long.MAX_VALUE | 9 quintillion |
| BigInteger | no upper bound | limited only by memory and speed |
BigInteger can represent arbitrarily large integers — there is no upper bound except the limitations of memory size and processing speed. It lives in java.math, so it must be imported.
Notation
The distinction between the two forms is easy to blur and the book flags it explicitly.
Annotate
valueOf takes a number. It converts a long or an int, which means the value must already fit in a long — so it cannot be used for anything genuinely huge.new BigInteger(String) takes text. That is how you create a value too big to write as a literal: the 20-digit number in the example cannot be a long, so it has to arrive as a string.The reason for two forms is simply that they take different types. If your value fits in a long, use valueOf; if it does not, it has to come from a string.
Worked example
Since BigIntegers are not primitive types, the usual math operators do not work on them. You use methods instead.
BigInteger a = BigInteger.valueOf(17);
BigInteger b = BigInteger.valueOf(1700000000);
BigInteger c = a.add(b);| you would write | for BigInteger |
|---|---|
| a + b | a.add(b) |
| a * b | a.multiply(b) |
| a - b | a.subtract(b) |
| Math.pow(a, n) | a.pow(n) |
Invoke the method on one operand.
Why: a.add(b) reads as a, add b — a is the object the method runs on.
Pass the other as an argument.
Why: Both operands must be BigIntegers; you cannot add a plain int to one.
Catch the return value.
Why: Like strings, BigInteger objects are immutable — add, multiply and pow all return new BigIntegers rather than modifying an existing one.
Note that a + b would not compile.
Why: The arithmetic operators are defined for primitives, and BigInteger is a class.
Verify: Print c and expect 1700000017.
Why: Then try a.add(b); without assigning the result and confirm nothing changes — the immutability rule from Lesson 9a applies here exactly as it does to strings. Assign the return value or the call accomplished nothing.
Prediction
One takes a number, the other takes text.
long x = 17;
String s = "12345678901234567890";| source | method |
|---|---|
| a long or an int | BigInteger.valueOf(x) |
| a String | new BigInteger(s) |
Predict first
How do you create a BigInteger from the 20-digit string?
Correct: new BigInteger(s)
Why: valueOf takes a long or an int, so it cannot accept a string — and in any case a 20-digit number does not fit in a long, which is why it had to be written as text in the first place. parseInt would produce an int and overflow, and a cast between a String and a number is not a compatible conversion.
Concept
There is no magic here. Internally, a BigInteger is implemented using an array of ints — similar to the way a string is implemented using an array of chars.
Figure (svg): An array of int elements each storing a portion of one very large number
Each int in the array stores a portion of the BigInteger, and the methods traverse this array to perform addition, multiplication and so on. That is why the operations are methods rather than operators, and why they are slower than primitive arithmetic — each one is a loop over an array, which is exactly the kind of code you have been writing since Chapter 7.
Trap
The arithmetic operators are for primitives.
BigInteger a = BigInteger.valueOf(17);
BigInteger b = BigInteger.valueOf(25);
BigInteger c = a + b; // does not compile
if (a > b) { ... } // does not compile either| operator | works on BigInteger? | use instead |
|---|---|---|
| + | no | a.add(b) |
| > | no | a.compareTo(b) > 0 |
| == | compiles, compares references | a.equals(b) |
The third row is the dangerous one: == does compile, because comparing two references is always legal — it just asks the wrong question, exactly as it does for Strings and Integers.
Methods for arithmetic, compareTo for ordering, equals for equality.
BigInteger c = a.add(b);
if (a.compareTo(b) > 0) { ... }
if (a.equals(b)) { ... }| want | write |
|---|---|
| a plus b | a.add(b) |
| is a greater than b? | a.compareTo(b) > 0 |
| are they equal? | a.equals(b) |
compareTo is the same method you met on String in Lesson 6b, returning a negative number, zero or a positive number. The pattern of comparing the result against zero rather than against a specific value is identical.
Definition probe
BigInteger is a class, so the operators do not apply.
Sort into buckets
Sort each expression by whether it works on two BigIntegers.
Prediction
BigInteger objects are immutable.
BigInteger a = BigInteger.valueOf(17);
BigInteger b = BigInteger.valueOf(3);
a.add(b);
System.out.println(a);| statement | a |
|---|---|
| BigInteger.valueOf(17) | 17 |
| a.add(b); | 17 — a new object was made and discarded |
Predict first
What is displayed?
Correct: 17
Why: Like strings, BigInteger objects are immutable — add returns a new BigInteger rather than modifying an existing one. With no assignment the result is discarded and a still holds 17. This is exactly the same trap as s.toUpperCase(); from Lesson 9a, in a different class.
Explain it to yourself
An int has a fixed size. A BigInteger apparently does not.
Discussion prompt
An int is 32 bits and a long is 64, and both have hard limits. How can a BigInteger have no upper bound, and what is the cost?
Hint: What is it made of?
Answer:
Because it is an array of ints, and an array can be as long as memory allows. A bigger number simply needs a longer array — there is no fixed-size box it has to fit into.
The cost is speed and memory. Adding two ints is one machine instruction; adding two BigIntegers is a loop over two arrays, with carrying between elements — the same algorithm you learned for addition on paper.
That is why the primitives still exist. Use int or long when the values fit, and BigInteger when they do not — and for very large floating-point values, java.math.BigDecimal, which interestingly represents its numbers internally using a BigInteger.
Section
Section 9.8
Concept
One challenge of programming, especially for beginners, is figuring out how to divide a program into methods. This section presents a process that lets you divide a program as you go along, called encapsulation and generalization.
Figure (svg): Three boxes showing the steps: write and test a few lines, wrap them in a method, then replace literals with parameters
Notice that the method is written after the code works, not before. That is the opposite of how beginners usually try to design, and it is why this process is easier to follow.
Notation
Start with something small and concrete. This loop displays the multiples of two, all on one line.
Annotate
2 * i, padded with spaces so it is four characters wide — the %4d specifier from Lesson 3a, which is what makes the columns line up.printf keeps it on one line, because unlike println it appends no newline.println() after the loop finishes the line. Remember that in some environments none of the output is displayed until the line is complete.This is the smallest useful thing, working. Everything that follows is a transformation of code that already produces the right output.
Worked example
Wrap the working code in a method, test it again, and only then replace the literal with a parameter.
// 2. encapsulate: wrap the code in a method
public static void printRow() {
for (int i = 1; i <= 6; i++) {
System.out.printf("%4d", 2 * i);
}
System.out.println();
}
// 3. generalize: the literal 2 becomes a parameter
public static void printRow(int n) {
for (int i = 1; i <= 6; i++) {
System.out.printf("%4d", n * i); // generalized n
}
System.out.println();
}| version | what it can print | call |
|---|---|---|
| the loose lines | the multiples of 2, once | — |
| printRow() | the multiples of 2, whenever you like | printRow() |
| printRow(int n) | the multiples of any number | printRow(3) |
Wrap the working code in a method.
Why: Nothing inside changes — this is why it is safe. Test again to confirm.
Find the literal that should vary.
Why: The 2 in 2 * i is what makes this the two times table rather than a general row.
Replace it with a parameter.
Why: printRow(int n) and n * i. This step is called generalization, because it makes the method less specific.
Check the original behaviour survives.
Why: Invoking printRow(2) yields the same output as before, which confirms nothing was broken.
Verify: Call printRow(2) and expect the original output; call printRow(3) and expect 3, 6, 9, 12, 15, 18.
Why: The second call is the payoff: a method that did one thing now does a family of things, and it took one edit to a line that was already known to work.
Ranking
Encapsulation and generalization, in the order the book gives them.
Put in order
Why: Each step transforms something already known to work, so a failure can only have come from the change just made. Generalizing first would mean designing parameters for an algorithm you have not yet verified, which is exactly the all-at-once approach Lesson 4b warns against.
Concept
With printRow generalized, the multiplication table from Chapter 6 becomes a loop over rows — and the nested loop from Lesson 6a is now one loop calling a method.
for (int i = 1; i <= 6; i++) {
printRow(i);
}| i | call | row printed |
|---|---|---|
| 1 | printRow(1) | 1 2 3 4 5 6 |
| 2 | printRow(2) | 2 4 6 8 10 12 |
| 3 | printRow(3) | 3 6 9 12 15 18 |
| ... | ... | ... |
This is the same output as the nested loops in Section 6.4 — but the inner loop is now encapsulated in printRow. That is the real benefit: the outer loop reads as for each row, print it, and the details of printing a row are somewhere else.
Trap
Parameters added to code that has never run. Now there are two things that might be wrong.
// written all at once, never tested:
public static void printRow(int n, int cols, int width) {
for (int i = 1; i <= cols; i++) {
System.out.printf("%" + width + "d", n * i);
}
System.out.println();
}| if the output is wrong | where do you look? |
|---|---|
| the loop bounds | possible |
| the multiplication | possible |
| the format string | possible |
| the parameters | possible |
Nothing has ever been verified, so a wrong result could come from anywhere. This is Lesson 4b's all-at-once mistake, applied to method design instead of to a single method's body.
Write, test, encapsulate, test, generalize, test.
// 1. loose lines, tested -> known good
// 2. wrapped in printRow() -> test again
// 3. n becomes a parameter -> test with 2, then 3
// 4. cols becomes a parameter -> test again| after each step | you know |
|---|---|
| the lines work | the algorithm is right |
| the method works | the wrapping did not break it |
| one parameter works | the generalization is right |
| two parameters work | and so is the second |
Each step starts from something known to work and changes one thing — so a failure is always attributable to the change you just made. That is the same principle as incremental development in Lesson 4b, and it is why the process has three separate steps rather than one.
Prediction
The generalized version, with six columns.
public static void printRow(int n) {
for (int i = 1; i <= 6; i++) {
System.out.printf("%4d", n * i);
}
System.out.println();
}| i | n * i |
|---|---|
| 1 | 4 |
| 2 | 8 |
| 3 | 12 |
| 6 | 24 |
Predict first
What is the row?
Correct: 4 8 12 16 20 24
Why: The loop runs i from 1 to 6 and prints n * i each time, so with n = 4 the row is the first six multiples of four. The %4d pads each to four characters so the columns line up regardless of how many digits each number has.
Definition probe
Two different moves, often confused.
Sort into buckets
Sort each change.
Explain it to yourself
The order is deliberate.
Discussion prompt
The process says wrap the code in a method first, and only then replace literals with parameters. Why not do both at once — and what would you lose?
Hint: What are you testing after each step?
Answer:
Doing both at once means that if the output changes, you cannot tell whether the wrapping broke it or the parameter did. Two changes, one symptom.
Encapsulating first is a change that provably cannot alter behaviour — the same code runs, just from a different place. So the test after step 2 confirms the wrapping, and the test after step 3 confirms the generalization.
This is incremental development applied to design rather than to code. The principle is the same one from Lesson 4b: never let the amount of unverified change get large.
Section
Section 9.9
Concept
The previous result is similar to the nested-loops approach in Section 6.4 — but the inner loop is now encapsulated in printRow. The outer loop can be encapsulated as well, and then generalized in exactly the same way.
// encapsulate
public static void printTable() {
for (int i = 1; i <= 6; i++) {
printRow(i);
}
}
// generalize
public static void printTable(int rows) {
for (int i = 1; i <= rows; i++) { // generalized rows
printRow(i);
}
}| version | displays |
|---|---|
| printTable() | always six rows |
| printTable(7) | seven rows |
| printTable(3) | three rows |
The same two moves applied to a second method. The initial version always displays six rows; replacing the literal 6 with a parameter makes the number of rows a decision for the caller.
Picture it
printTable(7) produces seven rows — but printRow still hard-codes six columns, so the table is not square.
Figure (svg): A grid of seven rows and six columns showing a multiplication table that is taller than it is wide
That is better, but it always displays the same number of columns. The literal 6 in printRow is now the thing that should vary — so generalize again.
Worked example
Generalize printRow a second time, and update the one place that calls it.
public static void printRow(int n, int cols) {
for (int i = 1; i <= cols; i++) { // generalized cols
System.out.printf("%4d", n * i);
}
System.out.println();
}
public static void printTable(int rows) {
for (int i = 1; i <= rows; i++) {
printRow(i, rows); // pass rows as cols
}
}| parameter | means | for printTable(7) |
|---|---|---|
| n | the value whose multiples to display | 1 through 7, one per row |
| cols | the number of columns | 7, passed from rows |
| result | — | a square 7 by 7 table |
Add the parameter and use it in the condition.
Why: i <= cols instead of i <= 6.
Update every call site.
Why: Since we added a parameter to printRow, we also have to change the line in printTable where it is invoked.
Decide what to pass.
Why: printRow(i, rows) — when this line executes it evaluates rows and passes the value, which is 7 in this example.
Note the effect.
Why: Inside printRow that value is assigned to cols, so the number of columns equals the number of rows, giving a square table instead of the 7 by 6 one.
Verify: Call printTable(7) and count: seven rows of seven columns.
Why: Then call printTable(3) and confirm a 3 by 3 table. The method now handles a family of tables, and every step from the original six lines was small enough to check.
Prediction
The argument passed for cols decides it.
public static void printTable(int rows) {
for (int i = 1; i <= rows; i++) {
printRow(i, i);
}
}| i | cols | row length |
|---|---|---|
| 1 | 1 | 1 entry |
| 2 | 2 | 2 entries |
| 3 | 3 | 3 entries |
Predict first
What does printTable(3) produce?
Correct: A triangular table, each row as long as its row number
Why: Passing i for both parameters means the number of columns equals the row number, so row 1 has one entry, row 2 has two, and so on. Since the multiplication table is symmetric, this shows every distinct product exactly once — a capability that came free from generalizing the method.
Concept
When you generalize a method appropriately, you often find it has capabilities you did not plan for. The multiplication table is symmetric — since a times b equals b times a, every entry appears twice.
printRow(i, i); // using i for both n and cols| call in printTable | each row's length | the table |
|---|---|---|
| printRow(i, rows) | always rows | square |
| printRow(i, i) | the same as its row number | triangular |
You could save ink by printing only half the table, and you would have to change only one line. Passing i for both parameters means the length of each row equals its row number, giving a triangular table. Generalization makes code more versatile, more likely to be reused, and sometimes easier to write.
Trap
The method changes and its callers do not.
public static void printRow(int n, int cols) { ... }
public static void printTable(int rows) {
for (int i = 1; i <= rows; i++) {
printRow(i); // still one argument
}
}| the method expects | the call supplies | result |
|---|---|---|
| two arguments | one | compile error |
| — | — | method printRow cannot be applied to given types |
This is the good case: the compiler catches it. Lesson 4a's error message names exactly what was required and what was found, so the fix is immediate.
Update every call site, and decide deliberately what to pass.
printRow(i, rows); // square table
// or
printRow(i, i); // triangular table| what you pass for cols | what you get |
|---|---|
| rows | every row the same length — a square |
| i | each row as long as its number — a triangle |
| 6 | the original behaviour, six columns |
The third row is worth noticing: passing the old literal restores the old behaviour exactly. That is a good check after any generalization — the general version should be able to reproduce the specific one it came from.
Definition probe
Generalize what should vary; leave what should not.
Sort into buckets
Sort each literal in the original printRow.
Comparison
Fill the blanks from the three versions in Chapters 6 and 9.
Comparison matrix
| version | structure | how many things it can print |
|---|---|---|
| Chapter 6 nested loops | two loops in main | one — a 10 by 10 table |
| printRow(n) | a loop in a method, called from a loop | any single row |
| printTable(rows) with printRow(n, cols) | two methods, both parameterised | tables of any size, square or triangular |
Every version produces the same output for the original case. What changes is how much else each one can do — and each step was a small, tested transformation of the one before it.
Edge cases
Generalization has been presented as an unmixed good. Test that.
Discussion prompt
You could add parameters to printRow for the width, the starting value, the separator and the newline. What would that cost, and how would you decide when to stop?
Hint: Think about what the call site looks like.
Answer:
The call becomes printRow(3, 6, 4, 1, " ", true) — six arguments whose meanings are impossible to read without looking at the definition. The method is more capable and much harder to use.
The book's own phrasing is careful: if it is appropriate, replace literal values with variables and parameters. The deciding question is whether a caller would realistically want to vary that value.
Generalize what varies; leave what does not. A parameter nobody ever passes differently is pure cost — it makes every call site longer and every reader's job harder, in exchange for flexibility that is never used.
Comparison
This lesson and Chapter 5 handle three. Fill the blanks — the pattern is the same in all three.
Comparison matrix
| source | arrives as | what can go wrong | the guard |
|---|---|---|---|
| Scanner input | text typed by the user | not a number | hasNextInt before nextInt |
| command-line args | an array of Strings | missing, or not numbers | check args.length, then parse carefully |
| a method parameter | whatever the caller passed | null, or empty | str == null || str.isEmpty() |
Three sources, one principle: validate at the boundary. Inside the program, past the guards, you can write code that assumes its data is good — which is what makes the interior simple.
Pattern
Encapsulation and generalization turn working code into a reusable method, one tested step at a time.
// 1. write a few lines and test them
for (int i = 1; i <= 6; i++) {
System.out.printf("%4d", 2 * i);
}
System.out.println();
// 2. encapsulate - wrap them, unchanged, and test again
public static void printRow() { ...the same lines... }
// 3. generalize - a literal becomes a parameter, and test again
public static void printRow(int n) { ...n * i... }
// 4. repeat: a second literal becomes a second parameter
public static void printRow(int n, int cols) { ...i <= cols... }| step | what changes | what you verify |
|---|---|---|
| write | nothing exists yet | the algorithm is right |
| encapsulate | structure only | the wrapping broke nothing |
| generalize | one literal | the old call still works, and a new one does too |
| repeat | one more literal | the same, again |
||.Check
Work it out before you click.
public static void main(String[] args) {
System.out.println(args.length);
System.out.println(args[0]);
}
// run as: java Prog one two three| element | value |
|---|---|
| args[0] | "one" |
| args[1] | "two" |
| args[2] | "three" |
Check your understanding
What does this display?
Answer: A
Why: Only the words after the class name become arguments, so args has three elements and args[0] is the first of them, "one". Neither the java command nor the class name appears in the array.
Check
Work it out before you click.
public static boolean check(String s) {
if (s.isEmpty() || s == null) {
return false;
}
return true;
}
// called as check(null)| operand | evaluated? | result |
|---|---|---|
| s.isEmpty() | yes — it is on the left | invoked on null |
| s == null | never reached | — |
Check your understanding
What happens when this is called with null?
Answer: A
Why: || evaluates its left operand first, and s.isEmpty() requires an object to invoke the method on. Since s is null, it throws before the null check is ever reached. Reversing the two tests fixes it, because short-circuit evaluation would then skip isEmpty entirely.
Check
Work it out before you click.
BigInteger a = BigInteger.valueOf(5);
BigInteger b = BigInteger.valueOf(3);
BigInteger c = a.add(b);
System.out.println(a + " " + c);| variable | value after |
|---|---|
| a | 5 — unchanged |
| b | 3 — unchanged |
| c | 8 — a new object |
Check your understanding
What is displayed?
Answer: A
Why: BigInteger objects are immutable, so add creates and returns a new object rather than modifying a. The result is assigned to c, which is 8, while a still holds 5 — exactly as s.toUpperCase() leaves a String unchanged.
Real world
Run javac or java with no arguments and you get a very long message. That is not an accident of implementation.
Discussion prompt
Every serious command-line tool prints usage instructions when its arguments are wrong. What does that tell you about who the error message is for — and how does it change what a good message should say?
Hint: Who is reading it, and what do they need to do next?
Answer:
The reader is a user who has just made a mistake and wants to fix it, not a programmer debugging the tool. So the message should say how to run the program correctly, not what went wrong internally.
That is why Usage: java Max <numbers> is the right shape: it shows the correct invocation, with placeholders for what the user must supply. A stack trace would tell them nothing they can act on.
The general principle: an error message should tell the reader what to do next. It applies well beyond command-line tools — and it is why the Logarithm program in Lesson 5b names the offending word rather than just saying 'invalid input'.
Commit first
Commit to an answer and to your confidence.
Predict first
Why must str == null be tested before str.isEmpty() in a guard?
Correct: Because || short-circuits, so a true null check skips the method call that would throw
Why: || evaluates its left operand first and stops if that operand is true, so putting the null check on the left means isEmpty() is never invoked on a null reference. Written the other way round, the method call happens first and throws a NullPointerException on exactly the input the guard exists to catch. This is Lesson 5b's guard pattern — the protective test goes on the left — and here getting it backwards is not a style issue but a crash.
Explain it
Two minutes, out loud.
Discussion prompt
A classmate has written a 40-line main method and cannot see how to split it into methods. Teach them encapsulation and generalization as a process they can apply right now, and explain why you would not just tell them to design the methods first.
Hint: The order of the three steps is the point.
Answer:
Find a few lines that do one identifiable thing and make sure they work. Then wrap exactly those lines in a method — do not change them — and run it again to check nothing broke. Then look for a literal inside that ought to vary, and turn it into a parameter.
The reason not to design first is that you would be designing an interface for code you have not written. This way each step starts from something that works, so if it breaks you know exactly which change did it.
The connection worth drawing for them: this is the same argument as incremental development in Lesson 4b. Never let the amount of unverified change get large — whether the change is code or structure.
Exit ticket
One question before you close the deck.
Predict first
Why are the elements of args Strings, even when you type numbers?
Correct: Because everything arriving from outside the program arrives as text, and must be parsed to become a number
Why: A command line is characters typed by a person, so java Max 10 supplies the two-character text "10" rather than the number 10 — and Integer.parseInt is what converts it. The same is true of Scanner input, file contents and network data: every boundary of a program carries text, which is why parsing and validation cluster at those boundaries and why NumberFormatException is a boundary exception.
Connect it up
One page, from memory.
Draw it
Draw the args array for the command java Max 10 -3 55, showing that its elements are Strings, and mark where parseInt turns each into an int. Beside it, write the two-condition guard for a String parameter and annotate why the null test must be first. Then write the three steps of encapsulation and generalization, and apply them to a loop that prints the first six multiples of 5 — showing all three versions.
Recap
Five sections that finish the chapter: data from outside the program, a class for numbers with no limit, and a process for growing methods out of working code.
| if you remember one thing | it is this |
|---|---|
| about outside data | it arrives as text, and must be validated |
| about guards | the protective test goes on the left |
| about design | write it, wrap it, then parameterise it |
args holds the command-line arguments as an array of Strings — never null, but possibly empty.return to end the program.Integer.MIN_VALUE, not to 0.|| short-circuits and protects the second test.Want this taught 1-on-1? Alexander tutors Java — $55/session, free consultation.