Command-Line Arguments, BigInteger, and Incremental Design

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

What this lesson covers

The lesson, slide by slide

1. Command-Line Arguments, BigInteger, and Incremental Design

Title

Think Java 2e · Chapter 9 · Immutable Objects

Sections 9.5-9.9 · pp. 152-160

2. What you will be able to do

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.

3. Retrieve before you read

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.

4. Data from outside the program

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.

5. Command-line arguments

Section

Section 9.5

6. args holds what you typed after the class name

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 typeargs containsargs.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.

7. The elements are Strings

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.

8. Finding the maximum

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);
argvaluegreater than max?max after
(start)——-2147483648
"10"10yes10
"-3"-3no10
"55"55yes55
"0"0no55
"14"14no55

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.

9. What is args.length?

Prediction

Count the words after the class name.

// java Max 10 -3 55
typedgoes into args?
javano — the command
Maxno — the class name
10, -3, 55yes — three elements

Predict first

What is args.length?

  • 3
  • 5
  • 4
  • 0

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.

10. The usage message

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;
}
elementpurpose
args.length == 0no arguments were supplied
System.errthis is an error, so it goes to the error stream
"Usage: java Max <numbers>"tells the user how to run the program
returnleaves 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.

11. Assuming the arguments are numbers

Trap

The 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"
    ...
}
argparseInt 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.

The fix

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;
    }
    ...
}
approachavailable now?
check args.lengthyes — and the book does this
catch NumberFormatExceptionChapter 15
check each argument's characters firstpossible, 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.

12. Why start max at Integer.MIN_VALUE?

Prediction

Consider a list of negative numbers.

int max = 0;                     // instead of MIN_VALUE
for (String arg : args) { ... }
// java Max -5 -3 -9
starting maxfor -5 -3 -9correct?
00 — never replacedno
Integer.MIN_VALUE-3yes

Predict first

What goes wrong if max starts at 0?

  • For all-negative input it returns 0, which is not in the list
  • Nothing — 0 works for every input
  • It throws an exception
  • It returns the first argument

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.

13. Where does each value come from?

Definition probe

Everything in this program starts as text.

Sort into buckets

Sort each item by its type.

a String or array of them
args; args[0]
an int
Integer.parseInt(args[0]); args.length; Integer.MIN_VALUE
str
The command line delivers text: args is an array of Strings and each element is a String object, even when its characters look like a number.
num
An int — either the array's length, a class constant, or the result of parsing a String into a number.

14. Why offer command-line arguments at all?

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.

15. Argument validation

Section

Section 9.6

16. Never assume input is in the correct format

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));
}
strwhat str.charAt(0) does
"Ada"returns 'A' — fine
""StringIndexOutOfBoundsException — no character at index 0
nullNullPointerException — 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.

17. null and empty, again

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.

18. Validating before using

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));
}
strstr == nullstr.isEmpty()returns
nulltruenot evaluatedfalse
""falsetruefalse
"ada"falsefalsefalse — from charAt
"Ada"falsefalsetrue

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.

19. Which exception does each input cause?

Prediction

The unguarded version of the method.

public static boolean isCapitalized(String str) {
    return Character.isUpperCase(str.charAt(0));
}
strwhat fails
nullinvoking charAt on nothing
""there is no character at index 0

Predict first

What does the empty string cause?

  • StringIndexOutOfBoundsException
  • NullPointerException
  • NumberFormatException
  • Nothing — it returns false

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.

20. The order is not optional

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()) { ... }
orderwhen str is nulloutcome
isEmpty() firstisEmpty() is invoked on nullNullPointerException
null check firstthe || short-circuitsreturns 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.

21. Checking for empty before null

Trap

The 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));
}
strfirst operand evaluatedresult
"Ada"isEmpty() is falseproceeds correctly
""isEmpty() is truereturns false correctly
nullisEmpty() invoked on nullNullPointerException

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.

The fix

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 rulewhy
test for null before dereferencingyou 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 methodthe 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.

22. Order the checks in a guard clause

Ranking

Each test assumes the previous ones passed.

Put in order

  1. is the reference null?
  2. is the object empty?
  3. is the first character uppercase?

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.

23. Write the guard

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.

24. Should an invalid argument return false, or throw?

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.

25. BigInteger arithmetic

Section

Section 9.7

26. Integers with no upper bound

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
typelargest valueroughly
intInteger.MAX_VALUE2 billion
longLong.MAX_VALUE9 quintillion
BigIntegerno upper boundlimited 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.

27. Two ways to create one, and why they differ

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.
  • This is the same pattern as Lesson 9a's parsing. Text is the universal way to carry a number that no primitive type can hold.
  • Take a minute to read the documentation, which you can find with a web search for Java BigInteger — the class has far more methods than this section uses.

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.

28. Arithmetic with methods instead of operators

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 writefor BigInteger
a + ba.add(b)
a * ba.multiply(b)
a - ba.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.

29. Which creation method for which input?

Prediction

One takes a number, the other takes text.

long x = 17;
String s = "12345678901234567890";
sourcemethod
a long or an intBigInteger.valueOf(x)
a Stringnew BigInteger(s)

Predict first

How do you create a BigInteger from the 20-digit string?

  • new BigInteger(s)
  • BigInteger.valueOf(s)
  • Integer.parseInt(s)
  • (BigInteger) s

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.

30. What a BigInteger is made of

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.

31. Using operators on a BigInteger

Trap

The 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
operatorworks on BigInteger?use instead
+noa.add(b)
>noa.compareTo(b) > 0
==compiles, compares referencesa.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.

The fix

Methods for arithmetic, compareTo for ordering, equals for equality.

BigInteger c = a.add(b);
if (a.compareTo(b) > 0) { ... }
if (a.equals(b)) { ... }
wantwrite
a plus ba.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.

32. Operator or method?

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.

works
a.add(b); a.equals(b); a.compareTo(b) > 0
does not compile
a + b; a > b
ok
A method call on the object. BigInteger provides methods for everything the operators do for primitives.
no
The arithmetic and relational operators are defined for primitive types, and BigInteger is a class — so these are compile errors rather than silent mistakes.

33. Does add modify a?

Prediction

BigInteger objects are immutable.

BigInteger a = BigInteger.valueOf(17);
BigInteger b = BigInteger.valueOf(3);
a.add(b);
System.out.println(a);
statementa
BigInteger.valueOf(17)17
a.add(b);17 — a new object was made and discarded

Predict first

What is displayed?

  • 17
  • 20
  • 3
  • a compile error

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.

34. Why can BigInteger have no upper bound?

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.

35. Encapsulation and generalization

Section

Section 9.8

36. A design process, in three steps

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.

37. Step one: a few lines that work

Notation

Start with something small and concrete. This loop displays the multiples of two, all on one line.

Annotate

  • Each pass displays 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.
  • The println() after the loop finishes the line. Remember that in some environments none of the output is displayed until the line is complete.
  • Nothing here is a method yet. It is a few lines in main that do one concrete thing, and — crucially — they have been run and checked before anything else happens.

This is the smallest useful thing, working. Everything that follows is a transformation of code that already produces the right output.

38. Steps two and three: encapsulate, then generalize

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();
}
versionwhat it can printcall
the loose linesthe multiples of 2, once—
printRow()the multiples of 2, whenever you likeprintRow()
printRow(int n)the multiples of any numberprintRow(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.

39. Order the three steps

Ranking

Encapsulation and generalization, in the order the book gives them.

Put in order

  1. write a few lines of code and test them
  2. wrap the working lines in a new method and test again
  3. replace literal values with parameters

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.

40. Building the table from the row

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);
}
icallrow printed
1printRow(1)1 2 3 4 5 6
2printRow(2)2 4 6 8 10 12
3printRow(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.

41. Generalizing before it works

Trap

The 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 wrongwhere do you look?
the loop boundspossible
the multiplicationpossible
the format stringpossible
the parameterspossible

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.

The fix

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 stepyou know
the lines workthe algorithm is right
the method worksthe wrapping did not break it
one parameter worksthe generalization is right
two parameters workand 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.

42. What does printRow(4) display?

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();
}
in * i
14
28
312
624

Predict first

What is the row?

  • 4 8 12 16 20 24
  • 1 2 3 4 5 6
  • 4 4 4 4 4 4
  • 4 8 12 16

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.

43. Encapsulation or generalization?

Definition probe

Two different moves, often confused.

Sort into buckets

Sort each change.

encapsulation
wrapping a working loop in a method; giving a block of code a name
generalization
replacing the literal 2 with a parameter n; replacing the literal 6 with a parameter cols
enc
Wrapping code in a method and giving it a name. The behaviour is unchanged — only the structure moves.
gen
Replacing a specific value with a parameter, which makes the method less specific and able to handle a family of cases.

44. Why encapsulate before generalizing?

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.

45. More generalization

Section

Section 9.9

46. Encapsulate the outer loop too

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);
    }
}
versiondisplays
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.

47. Seven rows, six columns

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.

48. Adding a second parameter

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
    }
}
parametermeansfor printTable(7)
nthe value whose multiples to display1 through 7, one per row
colsthe number of columns7, 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.

49. What shape is this table?

Prediction

The argument passed for cols decides it.

public static void printTable(int rows) {
    for (int i = 1; i <= rows; i++) {
        printRow(i, i);
    }
}
icolsrow length
111 entry
222 entries
333 entries

Predict first

What does printTable(3) produce?

  • A triangular table, each row as long as its row number
  • A square 3 by 3 table
  • Three rows of six columns
  • A compile error

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.

50. Capabilities you did not plan

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 printTableeach row's lengththe table
printRow(i, rows)always rowssquare
printRow(i, i)the same as its row numbertriangular

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.

51. Adding a parameter and forgetting the call sites

Trap

The 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 expectsthe call suppliesresult
two argumentsonecompile 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.

The fix

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 colswhat you get
rowsevery row the same length — a square
ieach row as long as its number — a triangle
6the 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.

52. Which literal should become a parameter?

Definition probe

Generalize what should vary; leave what should not.

Sort into buckets

Sort each literal in the original printRow.

worth generalizing
the 2 in n * i (the multiplier); the 6 in i <= 6 (the column count)
leave it
the 1 in int i = 1 (where multiples start); the 4 in "%4d" (the column width)
yes
It encodes a decision a caller might reasonably want to make differently — which multiples, and how many of them.
no
It is structural rather than a choice: multiples conventionally start at one, and the column width is a formatting detail nobody is likely to want to vary. Generalizing everything makes a method harder to call, not more useful.

53. The same table, three ways

Comparison

Fill the blanks from the three versions in Chapters 6 and 9.

Comparison matrix

versionstructurehow many things it can print
Chapter 6 nested loopstwo loops in mainone — a 10 by 10 table
printRow(n)a loop in a method, called from a loopany single row
printTable(rows) with printRow(n, cols)two methods, both parameterisedtables 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.

54. Can a method be too general?

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.

55. Three sources of outside data

Comparison

This lesson and Chapter 5 handle three. Fill the blanks — the pattern is the same in all three.

Comparison matrix

sourcearrives aswhat can go wrongthe guard
Scanner inputtext typed by the usernot a numberhasNextInt before nextInt
command-line argsan array of Stringsmissing, or not numberscheck args.length, then parse carefully
a method parameterwhatever the caller passednull, or emptystr == 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.

56. The pattern to carry away

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... }
stepwhat changeswhat you verify
writenothing exists yetthe algorithm is right
encapsulatestructure onlythe wrapping broke nothing
generalizeone literalthe old call still works, and a new one does too
repeatone more literalthe same, again

57. Check: command-line arguments

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
elementvalue
args[0]"one"
args[1]"two"
args[2]"three"

Check your understanding

What does this display?

  • A. 3 then one (correct)
  • B. 4 then Prog
  • C. 3 then Prog
  • D. 5 then java

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.

Why B tempts people
This counts the class name as an argument, but it names the class to run rather than being passed to it.
Why C tempts people
The length is right but args[0] is the first ARGUMENT, not the class name.
Why D tempts people
This counts every word on the command line, including the command itself.

58. Check: guard order

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)
operandevaluated?result
s.isEmpty()yes — it is on the leftinvoked on null
s == nullnever reached—

Check your understanding

What happens when this is called with null?

  • A. NullPointerException — isEmpty is invoked on null (correct)
  • B. It returns false
  • C. It returns true
  • D. A compile error

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.

Why B tempts people
That is what the correct order produces; this version throws before it can return anything.
Why C tempts people
The guard is intended to return false for null, and in any case the method never gets that far.
Why D tempts people
Both operands are valid boolean expressions, so it compiles — this is a run-time failure.

59. Check: BigInteger immutability

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);
variablevalue after
a5 — unchanged
b3 — unchanged
c8 — a new object

Check your understanding

What is displayed?

  • A. 5 8 (correct)
  • B. 8 8
  • C. 5 5
  • D. 8 5

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.

Why B tempts people
This assumes add modified a in place, which no method on an immutable object can do.
Why C tempts people
The return value was assigned to c, so c holds the sum rather than a copy of a.
Why D tempts people
This has the two variables the wrong way round — a is printed first and was never changed.

60. Why usage messages exist

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'.

61. How sure are you?

Commit first

Commit to an answer and to your confidence.

Predict first

Why must str == null be tested before str.isEmpty() in a guard?

  • Because || short-circuits, so a true null check skips the method call that would throw
  • Because null checks are faster
  • Because isEmpty returns null for a null string
  • Because Java evaluates the right operand first

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.

62. Explain it to someone else

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.

63. Exit ticket

Exit ticket

One question before you close the deck.

Predict first

Why are the elements of args Strings, even when you type numbers?

  • Because everything arriving from outside the program arrives as text, and must be parsed to become a number
  • Because Java cannot pass numbers to main
  • Because args is declared as String[] by mistake
  • Because command lines only accept letters

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.

64. Draw the whole lesson

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.

65. Recap

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 thingit is this
about outside datait arrives as text, and must be validated
about guardsthe protective test goes on the left
about designwrite it, wrap it, then parameterise it

Sources

  1. 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
  2. Java SE 21 API — java.math.BigInteger
  3. Think Java 2e — free online edition and source code

Want this taught 1-on-1? Alexander tutors Java — $55/session, free consultation.

Book on Wyzant · Text (657) 465-8108