Making an object display itself with toString, deciding what it means for two objects to be equal, the difference between a static method and an instance method, and an add method that is presented wrong on purpose so that carrying can be added to it. Follows Think Java 2e, Chapter 11 (Designing Classes), Sections 11.5-11.8, pp. 190-196, cross-referenced against Java SE 21 API — java.lang.Object (toString, equals).
Subject: Java · 65 slides · code lesson
Open the interactive version of this deck
Title
Think Java 2e · Chapter 11 · Designing Classes
Sections 11.5-11.8 · pp. 190-196
Objectives
This lesson follows Think Java 2e, Chapter 11 (Designing Classes), Sections 11.5-11.8, pp. 190-196. Everything on these slides can be checked against those pages.
1. Write a toString method, and say why it is not static.
2. Explain what println does with an object, and why the default output is an address.
3. Distinguish identity from equivalence, and write an equals method.
4. Say why the seconds are compared with a tolerance rather than with ==.
5. Convert a static method into an instance method, and name the three changes.
6. Recognise that an object-producing method can be plausible and still wrong, and add the carrying it needs.
Warm-up
Two things you have watched happen without being able to cause them.
Discussion prompt
Printing an array gave [I@bf3f7e0 but printing a Point gave java.awt.Point[x=3,y=4]. What must Point have that an array does not? And from Lesson 9a, what does == ask about two objects?
Hint: One is a method; one is a question about identity.
Answer:
Point provides a toString method that returns a readable representation, and println calls it automatically. == asks whether two references point at the same object, not whether their contents match.
This lesson writes both for a class of your own — which turns the Time objects from Lesson 11a from opaque into usable.
Concept
Lesson 11a built a Time class with data and constructors. It is still unusable: you cannot see inside one, compare two, or combine them. This lesson adds the three methods that fix that — and every one of them is a method the library classes already have.
Figure (svg): A UML class diagram for Time showing private data and the public methods toString, equals and add
Downey & Mayfield, Think Java, 2nd edition (Green Tea Press / O'Reilly, 2020) — Think Java 2e, Chapter 11 (Designing Classes), Sections 11.5-11.8, pp. 190-196 — Sections 11.5-11.8, printed pages 190-196.
Section
Section 11.5
Concept
To display Time objects we can write a method that prints the hour, minute and second. Using printTime from Section 4.4 as a starting point, this works — and printf makes it concise.
public static void printTime(Time t) {
System.out.printf("%02d:%02d:%04.1f\n",
t.hour, t.minute, t.second);
}| specifier | means | for 11, 59, 59.9 |
|---|---|---|
| %02d | total width 2, leading zeros if necessary | 11 |
| %02d | the same, for the minute | 59 |
| %04.1f | total width 4, one digit after the point, leading zeros | 59.9 |
| the whole line | — | 11:59:59.9 |
As a reminder from Lesson 3a: use %d with integers and %f with floating-point numbers. The 02 option pads to two digits, which is what makes 9 o'clock display as 09 rather than 9.
Picture it
There is nothing wrong with printTime. It is simply not the shape object-oriented code takes.
Figure (svg): Two panels comparing a static method taking a Time against a method invoked on the Time itself
There is nothing wrong with a method like printTime, but it is not consistent with object-oriented style. A more idiomatic solution is a special method called toString — and it is also strictly more useful, for the reason on the right.
Worked example
Three specifiers with width options, which is the first time this course has needed them. Each part controls one column of the output.
System.out.printf("%02d:%02d:%04.1f\n",
t.hour, t.minute, t.second);| value | specifier | output | why |
|---|---|---|---|
| 9 | %02d | 09 | padded to two digits with a leading zero |
| 59 | %02d | 59 | already two digits |
| 5.5 | %04.1f | 05.5 | width 4 including the point, one decimal |
| 59.9 | %04.1f | 59.9 | already four characters |
Read the letter first.
Why: d for an integer, f for floating-point — Lesson 3a's rule, and the one that throws at run time if you get it wrong.
Read the number before the point as the total width.
Why: 02 means two characters wide, and the leading 0 says pad with zeros rather than spaces.
Read the number after the point as the decimals.
Why: .1 in %04.1f means one digit after the decimal point.
Count the width carefully.
Why: In %04.1f the 4 is the total width including the decimal point, so 5.5 becomes 05.5 — three characters plus a leading zero.
Verify: Print a Time of 9, 5, 5.5 and expect 09:05:05.5.
Why: That is the payoff of the padding: a column of times lines up regardless of the digits, which is exactly why the format specifiers exist rather than plain concatenation.
Prediction
Width and padding.
System.out.printf("%02d", 7);| option | effect |
|---|---|
| 2 | total width of two characters |
| 0 | pad with zeros rather than spaces |
Predict first
What is displayed?
Correct: 07
Why: The 2 sets a total width of two characters and the leading 0 says to pad with zeros rather than spaces, so a single digit becomes 07. Without the zero — %2d — you would get a space and a 7, which does not read as a time.
Concept
Before writing toString, it is worth seeing what happens without one. Every object already has a toString — it just is not useful.
public static void main(String[] args) {
Time time = new Time(11, 59, 59.9);
System.out.println(time);
}
// Time@80cc7c0| part of the output | meaning |
|---|---|
| Time | the type of the object |
| @ | separator |
| 80cc7c0 | its address in memory, in hexadecimal |
Every object has a method called toString, and when you display an object with print or println, Java invokes it. By default it displays the type and the address — which is the same output shape as printing an array in Lesson 7a or System.out in Lesson 3a. This address can be useful for debugging if you want to keep track of individual objects.
Trap
printTime can only print. Anything else you want to do with the text is impossible.
printTime(t); // prints it
String s = printTime(t); // no - it returns void
if (printTime(t).equals(other)) { } // no
label = "Departs at " + printTime(t); // no| you want to | possible with printTime? |
|---|---|
| display it | yes |
| store the text | no |
| put it inside a longer message | no |
| compare two times' text | no |
This is Lesson 4b's rule appearing again: a method that displays its answer can only ever display it. Returning the text leaves the decision to the caller.
Return the string; let the caller print it.
String s = time.toString();
System.out.println(time); // calls it for you
label = "Departs at " + time; // and so does concatenation| use | works with toString? |
|---|---|
| println(time) | yes — invoked automatically |
| a String variable | yes |
| concatenation | yes — + calls toString too |
| comparison | yes |
The third row is worth noticing: concatenating an object with a string calls its toString as well, which is why "Time: " + time works once you have written one. That is Chapter 2's rule about + converting the other operand, finally explained.
Prediction
The default behaviour.
Time time = new Time(11, 59, 59.9);
System.out.println(time);| has a toString? | output |
|---|---|
| no | Time@80cc7c0 |
| yes | whatever it returns |
Predict first
What appears?
Correct: Something like Time@80cc7c0 — the type and address
Why: Every object has a toString, and the default one displays the type and the object's address in hexadecimal. That is the same output shape as printing an array in Lesson 7a — and it is why Arrays.toString exists, and why Point's readable output meant Point had written its own.
Definition probe
The letter must match the type.
Sort into buckets
Sort each value by the specifier it needs.
Explain it to yourself
One takes the Time as a parameter; the other does not.
Discussion prompt
printTime(Time t) is static and takes the object as a parameter. toString() is not static and takes nothing. Where does the object come from in the second case?
Hint: Lesson 10b named it.
Answer:
It arrives as this — the object the method was invoked on. time.toString() passes time implicitly, without it appearing in the parameter list.
That is the whole difference between a static method and an instance method: a static one belongs to the class and must be handed an object; an instance one is invoked on an object and receives it as this.
Section 11.8 makes this explicit by writing the same add method both ways — which is the clearest way to see that the two forms do the same work with the object arriving by different routes.
Section
Section 11.6
Concept
You can override the default behaviour by providing your own toString method. It returns a String rather than printing one, and Java invokes it whenever an object is displayed.
public String toString() {
return String.format("%02d:%02d:%04.1f\n",
this.hour, this.minute, this.second);
}| difference from printTime | why |
|---|---|
no static keyword | it is an instance method, invoked on an object |
this instead of a parameter | the object arrives implicitly |
| String.format instead of printf | it returns the text rather than displaying it |
| returns String | so the caller decides what to do with it |
instance method — A method invoked on an instance of a class, which receives that object as this. Sometimes called non-static.
The definition does not have the keyword static, because it is not a static method. It is an instance method, so called because when you invoke it, you invoke it on an instance of the class. Instance methods are sometimes called non-static, and you may see that term in an error message.
Notation
The body is nearly the same. Each change turns a method that prints into a method that produces.
Annotate
this replaces the parameter. Inside the method, this refers to the current instance — the object the method was invoked on.String.format replaces printf. It takes the same arguments and returns a formatted String rather than displaying it, which is Lesson 6b's distinction.static disappears. A static method belongs to the class; this one belongs to each object and needs this to know which.toString, with no parameters — that is how Java knows to call it when displaying the object.Everything else — the format string, the specifiers, the order of the values — is unchanged. The method was already right; only its shape needed to move.
Worked example
You can call toString directly, or let Java call it for you. Both give the same string.
Time time = new Time(11, 59, 59.9);
String s = time.toString(); // directly
System.out.println(time); // indirectly
System.out.println("at " + time); // and through concatenation| written | who calls toString | result |
|---|---|---|
| time.toString() | you | the string "11:59:59.9" |
| println(time) | println | displays 11:59:59.9 |
| "at " + time | the + operator | the string "at 11:59:59.9" |
Call it directly when you want the text.
Why: String s = time.toString(); — the value of s is the string 11:59:59.9.
Let println call it when you want to display.
Why: You can also invoke toString indirectly by invoking print or println.
Note that concatenation calls it too.
Why: This is why "Time: " + time produces readable output once toString exists.
Note what this refers to in each case.
Why: Either way, when you use this inside toString it refers to the same object as time.
Verify: Print the object three ways and confirm all three show 11:59:59.9.
Why: Then notice the newline: the book's toString ends its format string with \n, so println adds a second line break. Whether a toString should include a newline is a real design question — most do not, precisely so the caller can decide.
Prediction
toString is defined and returns 11:59:59.9
Time time = new Time(11, 59, 59.9);
System.out.println("Now: " + time);| step | what happens |
|---|---|
| the + operator | one operand is a String, so it concatenates |
| converting time to text | calls its toString |
| result | "Now: 11:59:59.9" |
Predict first
What appears?
Correct: Now: 11:59:59.9
Why: Concatenating an object with a String converts the object to text by calling its toString — the same mechanism println uses. This is Chapter 2's rule that + converts the other operand, and toString is what it converts with.
Concept
Every readable output from an object in this book has been a toString. Naming it explains all of them at once.
| printing | output | because |
|---|---|---|
| an int | 5 | primitives are not objects |
| a String | its characters | String has a toString |
| a Point | java.awt.Point[x=3,y=4] | Point has one |
| an array | [I@bf3f7e0 | arrays do not |
| a Time with no toString | Time@80cc7c0 | the default one |
| a Time with toString | 11:59:59.9 | yours |
The fourth row is why Arrays.toString(a) exists — it supplies the readable form that arrays lack. And the last two rows are the same class before and after you write six lines: toString is the cheapest improvement you can make to a class you have written.
Trap
A toString that prints instead of returning. It compiles only if you also fix the return type.
public void toString() { // wrong return type
System.out.printf("%02d:%02d", this.hour, this.minute);
}| problem | consequence |
|---|---|
| returns void | println has nothing to display |
| prints directly | the text cannot be stored or concatenated |
| does not match the expected signature | Java will not call it when displaying the object |
The name is right and the signature is not, so Java's default toString is used instead — and your method is never called. println(time) still shows the address, which is a genuinely puzzling symptom.
Return a String; print nothing.
public String toString() {
return String.format("%02d:%02d:%04.1f\n",
this.hour, this.minute, this.second);
}| required | value |
|---|---|
| name | toString |
| parameters | none |
| return type | String |
| static? | no |
All four must match for Java to use your method. A toString that prints is a printTime with the wrong name — and it gives up everything the return value buys.
Definition probe
Does the method need a particular object?
Sort into buckets
Sort each method.
this — so it can read that object's instance variables without being handed them.Fill the middle
Return the formatted time rather than printing it.
Fill in the blanks
public String toString() format}("%02d:%02d", this.hour, this.minute);
}
Why: The return type must be String for Java to use the method when displaying the object, and String.format produces the text rather than printing it — the difference from printf that Lesson 6b introduced. Note there is no static: toString is an instance method and gets its object as this.
Real world
Java gives one to every object, even though the default is barely useful.
Discussion prompt
Every object has a toString whether its author wrote one or not. Why would the language guarantee that rather than leaving it optional?
Hint: What has to happen when you concatenate an arbitrary object with a String?
Answer:
Because println and the + operator have to work on any object. If a class could lack a toString, those operations would have to fail for some objects — so the language supplies a default that always exists.
The default shows the type and address, which is not useful for reading but genuinely useful for debugging: it lets you tell two objects apart and see when two variables refer to the same one.
So overriding it is a choice, not an obligation — and it is the difference between [Time@1a2b, Time@3c4d] and [09:00:00.0, 17:30:00.0] when you print a list of them. That is why it is worth writing early.
Section
Section 11.7
Concept
We have seen two ways to check whether values are equal. With objects you can use either, but they are not the same.
Time time1 = new Time(9, 30, 0.0);
Time time2 = time1;
Time time3 = new Time(9, 30, 0.0);
time1 == time2 // true - the same object
time1 == time3 // false - different objects
time1.equals(time3) // should be true - equivalent values| == | equals | |
|---|---|---|
| asks | are these references identical? | are these objects equivalent? |
| means | do they refer to the same object? | do they have the same values? |
| definition | always the same | different for different classes |
| can a class change it? | no | yes — by providing its own equals |
The definition of identity is always the same, so == always does the same thing. But the definition of equivalence is different for different objects, so objects can define their own equals methods.
Picture it
The assignment operator copies references, so two of these variables refer to one object and the third refers to another.
Figure (svg): Three Time variables where time1 and time2 point to one object and time3 points to a second object with identical values
time1 == time2 is true because they are identical — the same object. time1 == time3 is false because they are two different objects. And yet time1 and time3 represent the same time of day, so we should consider them equivalent — which is what equals is for.
Worked example
By default, the equals method does the same thing as ==. For Time objects that is probably not what we want, so we provide our own.
public boolean equals(Time that) {
final double DELTA = 0.001;
return this.hour == that.hour
&& this.minute == that.minute
&& Math.abs(this.second - that.second) < DELTA;
}| comparison | operator | why |
|---|---|---|
| this.hour == that.hour | == | ints — exact comparison is right |
| this.minute == that.minute | == | ints |
| Math.abs(this.second - that.second) < DELTA | a tolerance | doubles — rounding error |
| joined with | && | all three must hold |
Make it an instance method returning boolean.
Why: equals is an instance method, so it does not have the keyword static.
Use this for one object and that for the other.
Why: that is not a keyword — the parameter could have any name, but using that makes the code nicely readable.
Compare the integers with ==.
Why: Because hour and minute are integers, comparing them exactly is correct.
Compare the doubles with a tolerance.
Why: Because of rounding errors it is not good to compare floating-point numbers with == — so check whether the difference is smaller than a threshold.
Verify: Call time1.equals(time3) and expect true, since their instance variables are equal.
Why: The third line is the one worth dwelling on: it is Lesson 2b's rounding-error warning, appearing as a design decision inside a method rather than as a caution in prose. Never compare two doubles with == — and DELTA is how you avoid it.
Prediction
Two objects, three variables.
Time time1 = new Time(9, 30, 0.0);
Time time2 = time1;
Time time3 = new Time(9, 30, 0.0);| comparison | identical? | equivalent? |
|---|---|---|
| time1 and time2 | yes | yes |
| time1 and time3 | no | yes |
Predict first
What is time1 == time3?
Correct: false — they are different objects
Why: new was called twice, so two objects exist and the two references differ — == asks about identity and answers no. time1.equals(time3) is what asks about equivalence, and with the equals method written it answers yes.
Concept
Comparing floating-point numbers exactly fails for values that ought to be equal, which Lesson 2b demonstrated with 0.1 added ten times.
final double DELTA = 0.001;
Math.abs(this.second - that.second) < DELTA| approach | for 59.9 and 59.90000000000001 |
|---|---|
| this.second == that.second | false — the bits differ |
| Math.abs(difference) < 0.001 | true — the difference is negligible |
Math.abs gives the magnitude of the difference regardless of which is larger, and DELTA is declared final because it is a fixed threshold — Lesson 3a's named constant, used exactly as intended. The value 0.001 is a judgement about how precise a second needs to be, and a different application would choose differently.
Trap
Without your own equals, it behaves exactly like ==.
// no equals method in Time
Time t1 = new Time(9, 30, 0.0);
Time t3 = new Time(9, 30, 0.0);
System.out.println(t1.equals(t3)); // false| what you expect | what the default does |
|---|---|
| compares the values | compares the references |
| true for equivalent times | false for different objects |
| a meaningful answer | the same answer as == |
This is worse than a compile error, because equals looks like it is comparing contents. Calling it without having written it gives a plausible-looking false.
Define what equivalence means for your class.
public boolean equals(Time that) {
final double DELTA = 0.001;
return this.hour == that.hour
&& this.minute == that.minute
&& Math.abs(this.second - that.second) < DELTA;
}| class | two objects are equal when |
|---|---|
| String | they contain the same characters |
| Integer | they wrap the same value |
| Time | hour, minute and second all match |
| a class with no equals | they are the same object |
Many objects have a similar notion of equivalence — two objects are equal if their instance variables are equal. But other definitions are possible, and only the class's author can decide: two Cards might be equal if their ranks match regardless of suit, for instance.
Definition probe
Identity is always the same question; equivalence is defined per class.
Sort into buckets
Sort each comparison by what it asks.
Prediction
Comparing doubles exactly.
// two seconds values that should be equal:
double a = 0.1 * 3;
double b = 0.3;
System.out.println(a == b);| value | stored as |
|---|---|
| 0.1 * 3 | 0.30000000000000004 |
| 0.3 | 0.3 (approximately) |
Predict first
Why does equals compare seconds with a tolerance rather than ==?
Correct: Because rounding error means values that should be equal often are not, bit for bit
Why: Lesson 2b showed that most floating-point numbers are only approximate, so two values that are mathematically equal can differ in their final bits. Testing Math.abs(difference) < DELTA asks whether they are close enough, which is the only reliable way to compare doubles. == does compile for doubles — it just gives the wrong answer often enough to be unusable.
Counterexample
Equivalence is defined by the class, so it can be anything sensible.
Discussion prompt
Time's equals requires all three instance variables to match. Can you think of a class where two objects should count as equal even though some of their data differs?
Hint: Think about a Card, or a person's record.
Answer:
Plenty. Two Card objects might be equal if the ranks match, ignoring suit, for a game that does not care about suits. Two customer records might be equal if their ID numbers match, whatever else differs. Two Time objects might be equal if they fall in the same minute.
That is exactly why the definition of equivalence is different for different objects and why classes define their own equals. == cannot be redefined because identity has only one meaning.
The obligation this creates: if you write a class whose objects will be compared, you have to decide what equal means and write it down. Leaving the default means equal only if it is literally the same object, which is rarely what anyone wants.
Section
Section 11.8
Concept
Suppose a film starts at 18:50 and runs for 2 hours 16 minutes. To add two Times there are two ways to write the method — as a static method taking both objects, or as an instance method invoked on one.
// static
public static Time add(Time t1, Time t2) { ... }
Time endTime = Time.add(startTime, runningTime);
// instance
public Time add(Time t2) { ... }
Time endTime = startTime.add(runningTime);| difference | static | instance |
|---|---|---|
| the keyword | has static | does not |
| parameters | two: t1 and t2 | one explicit: t2, plus the implicit this |
| invoked with | the class: Time.add(...) | an object: startTime.add(...) |
That is all there is to it. Static methods and instance methods do the same thing, and you can convert from one to the other with just a few changes.
Notation
Three edits turn one into the other, and they are always the same three.
Annotate
static keyword. That is what makes the method belong to each object rather than to the class.this. t1.hour becomes this.hour, throughout the body.Time.add(a, b) becomes a.add(b) — the first argument moves in front of the dot.Reading a.add(b) as Time.add(a, b) with the first argument moved is the clearest way to understand what this is: it is the argument that does not appear in the parameter list.
Worked example
To demonstrate the difference, the book writes both. Trace what each one does with the same two objects.
Time startTime = new Time(18, 50, 0.0);
Time runningTime = new Time(2, 16, 0.0);
// static
Time endTime = Time.add(startTime, runningTime);
// instance
Time endTime = startTime.add(runningTime);| static version | instance version | |
|---|---|---|
| startTime arrives as | the parameter t1 | this |
| runningTime arrives as | the parameter t2 | the parameter t2 |
| what is computed | 18+2, 50+16, 0.0+0.0 | the same |
| what is returned | a new Time | a new Time |
Create a new object to hold the result.
Why: Time sum = new Time(); — using the default constructor, which zeroes everything.
Add the corresponding instance variables.
Why: hour to hour, minute to minute, second to second.
Return the new object.
Why: Neither version modifies its inputs, which makes both safe to call — Lesson 10a's distinction.
Note that both give 20:66.
Why: Which is not a valid time. However, there is a problem with both of these methods: they are not correct.
Verify: Run either version and expect 20:66 — 18 + 2 hours and 50 + 16 minutes.
Why: Sixty-six minutes is not a time. Both methods are plausible, symmetrical and wrong, which is exactly the kind of bug that survives a reading. The next idea fixes it.
Prediction
The first argument moves in front of the dot.
public static Time add(Time t1, Time t2) { ... }
public Time add(Time t2) { ... }| method | call |
|---|---|
| static | Time.add(startTime, runningTime) |
| instance | startTime.add(runningTime) |
Predict first
How do you invoke the instance version?
Correct: startTime.add(runningTime)
Why: An instance method is invoked on an object, which arrives inside the method as this — so the object that was the static version's first parameter moves in front of the dot. The static version is invoked on the class instead, which is why it reads Time.add(...).
Concept
The two are interchangeable, so the choice is about which reads better and which class the behaviour belongs to.
| prefer | when |
|---|---|
| an instance method | the behaviour belongs to one object — startTime.add(runningTime) |
| a static method | there is no natural owner — Math.max(a, b) |
| a static method | it operates on the class rather than an instance — Integer.parseInt |
| an instance method | you want the object-oriented style Lesson 10a described |
Math.max is static because neither argument owns the operation. startTime.add(runningTime) is an instance method because the start time is naturally the thing being added to. Both are legitimate, and every static method you have written since Chapter 4 has been static because it belonged to no object.
Trap
There is no object, so there is nothing for this to refer to.
public static Time add(Time t1, Time t2) {
Time sum = new Time();
sum.hour = this.hour + t2.hour; // error
...
}| method kind | is there a this? | why |
|---|---|---|
| instance method | yes | invoked ON an object |
| static method | no | invoked on the class |
The compiler reports something like non-static variable this cannot be referenced from a static context. That message names the distinction exactly, and it is one you will meet often — usually from trying to use an instance variable inside main, which is static.
A static method gets everything through parameters.
public static Time add(Time t1, Time t2) {
Time sum = new Time();
sum.hour = t1.hour + t2.hour;
sum.minute = t1.minute + t2.minute;
sum.second = t1.second + t2.second;
return sum;
}| in a static method | in an instance method |
|---|---|
| every object arrives as a parameter | one arrives as this |
| no this | this is available |
| invoked on the class | invoked on an object |
This also explains why main is static and why you cannot use instance variables directly inside it: main is invoked before any object exists. Everything main works with must be created or passed in.
Definition probe
Does the method operate on a particular object?
Sort into buckets
Sort each method you have met.
this and supplies the data the method works with.Fill the middle
Remove the first parameter and use this.
Fill in the blanks
public Time add(Time t2) this}.hour + t2.hour;
sum.minute = this.minute + t2.minute;
return sum;
}
Why: The object the method is invoked on replaces the static version's first parameter and is named this; the second object still arrives explicitly as t2. Note that static has also been removed — an instance method must not be static, because a static method has no this.
Explain it to yourself
You have written it since Chapter 1 without asking.
Discussion prompt
main is declared static. Given what static means, why must it be — and what does that explain about errors beginners hit inside main?
Hint: When main starts, how many objects exist?
Answer:
When the program starts, no objects exist yet. Java has to invoke main without having an object to invoke it on, which means main must belong to the class rather than to an instance.
That is why you cannot use instance variables directly inside main, and why trying gives non-static variable cannot be referenced from a static context. Everything main works with must be created inside it or passed in through args.
It also explains why every method you wrote before Chapter 11 was static: they were all called from main, which has no object to offer them. Instance methods only became possible once you had a class with objects to invoke them on.
Section
Section 11.8, continued
Concept
Both versions of add are plausible and both are not correct. Adding 18:50 and 2:16 gives a result of 20:66 — sixty-six minutes, which no clock shows.
// 18:50:00.0 + 02:16:00.0
// hour: 18 + 2 = 20
// minute: 50 + 16 = 66 <- not a valid minute
// second: 0.0 + 0.0 = 0.0| field | valid range | the result | valid? |
|---|---|---|---|
| hour | 0 to 23 | 20 | yes |
| minute | 0 to 59 | 66 | no |
| second | 0.0 to 59.99... | 0.0 | yes |
If second exceeds 59 we have to carry into the minutes column, and if minute exceeds 59 we have to carry into hour. That is the same carrying you learned for addition on paper, and it is what the method is missing.
Picture it
Three checks, and the order matters: seconds can push minutes over sixty, which can then push hours over twenty-four.
Figure (svg): A pipeline showing carrying from seconds into minutes, then minutes into hours, then hours wrapping past 24
The order is not a preference. Carrying the seconds may push the minutes to 60, so the minute check has to come after it — which is exactly why you add from the right on paper.
Worked example
Three if statements, each handling one column. Here is the better version of the instance method.
public Time add(Time t2) {
Time sum = new Time();
sum.hour = this.hour + t2.hour;
sum.minute = this.minute + t2.minute;
sum.second = this.second + t2.second;
if (sum.second >= 60.0) {
sum.second -= 60.0;
sum.minute += 1;
}
if (sum.minute >= 60) {
sum.minute -= 60;
sum.hour += 1;
}
if (sum.hour >= 24) {
sum.hour -= 24;
}
return sum;
}| step | hour | minute | second |
|---|---|---|---|
| after adding | 20 | 66 | 0.0 |
| second >= 60.0? | 20 | 66 | no change |
| minute >= 60? | 21 | 6 | 0.0 |
| hour >= 24? | no change | 6 | 0.0 |
| result | 21 | 6 | 0.0 — 21:06 |
Add the corresponding fields first.
Why: Producing a possibly invalid Time, which is fine because it is a local variable nobody else can see.
Carry the seconds.
Why: If seconds reach 60, subtract 60 and add one to the minutes.
Then carry the minutes.
Why: If minutes reach 60, subtract 60 and add one to the hours. This must come after the seconds check.
Then wrap the hours.
Why: If hour exceeds 23 we subtract 24 hours — but there is no days attribute to carry into, so the day is simply lost.
Verify: Add 18:50 and 2:16 and expect 21:06 — a film starting at ten to seven and running 2 hours 16 minutes ends at six minutes past nine.
Why: Then check the last note: adding 20:00 and 6:00 gives 02:00, and the fact that it is the next day is nowhere recorded. That is a limitation of the class, not a bug in the method — and noticing the difference is a real skill.
Prediction
No carrying at all.
public Time add(Time t2) {
Time sum = new Time();
sum.hour = this.hour + t2.hour;
sum.minute = this.minute + t2.minute;
sum.second = this.second + t2.second;
return sum;
}| field | 18:50 + 2:16 |
|---|---|
| hour | 20 |
| minute | 66 |
Predict first
What does it return?
Correct: 20:66 — which is not a valid time
Why: The method adds the corresponding fields and does nothing else, so 50 + 16 gives 66 minutes and no carry into the hours. Nothing throws — the Time object holds 66 quite happily, because nothing in the class enforces a valid range. It is a logic error, and the output looks almost right.
Concept
The original method is symmetrical, readable and obviously about adding. Everything about its appearance is reassuring.
| what made it look right | what would have caught it |
|---|---|
| three parallel lines, one per field | trying an example with a carry |
| no obviously missing case | asking what the valid range of each field is |
| it compiles and runs | checking the output against a real clock |
| it works for 1:00 + 2:00 | choosing a test case that exercises the boundary |
The bottom row is the lesson. A test that does not cross a boundary cannot find a carrying bug — and Lesson 4b's rule about knowing the right answer before you test is what makes you choose 18:50 + 2:16 rather than 1:00 + 2:00.
Trap
Checking the minutes before the seconds misses a carry that the seconds create.
if (sum.minute >= 60) { // checked first
sum.minute -= 60;
sum.hour += 1;
}
if (sum.second >= 60.0) { // too late
sum.second -= 60.0;
sum.minute += 1; // minute can now be 60 again
}| adding 0:59:30 and 0:00:45 | minute | second |
|---|---|---|
| after adding | 59 | 75.0 |
| minute >= 60? no | 59 | 75.0 |
| second >= 60? yes | 60 | 15.0 |
| result | 60 — invalid | 15.0 |
The minute check already ran, so nothing catches the 60. The result is 0:60:15, which is exactly the kind of invalid value the carrying was meant to prevent.
Smallest unit first, so each carry can feed the next.
if (sum.second >= 60.0) { // seconds first
sum.second -= 60.0;
sum.minute += 1;
}
if (sum.minute >= 60) { // then minutes
sum.minute -= 60;
sum.hour += 1;
}
if (sum.hour >= 24) { // then hours
sum.hour -= 24;
}| adding 0:59:30 and 0:00:45 | minute | second |
|---|---|---|
| after adding | 59 | 75.0 |
| second >= 60? yes | 60 | 15.0 |
| minute >= 60? yes | 0 and hour + 1 | 15.0 |
| result | 1:00:15 | valid |
This is exactly why written addition works right to left: a carry propagates in one direction, so the columns must be handled in that direction. The same reasoning applies to any mixed-radix arithmetic — feet and inches, pounds and pence.
Ranking
A carry can trigger the next one.
Put in order
Why: Each carry can push the next column over its limit, so the checks must run from the smallest unit upward. Checking the minutes before the seconds would miss a minute that only reaches 60 because of the seconds carry — the same reason written addition proceeds from right to left.
Prediction
The final check has no column to carry into.
if (sum.hour >= 24) {
sum.hour -= 24;
}
// adding 20:00 and 6:00| step | hour |
|---|---|
| after adding | 26 |
| hour >= 24? yes | 2 |
| what is lost | the fact that it is the next day |
Predict first
What does adding 20:00 and 6:00 give, and what is lost?
Correct: 02:00, and the information that a day has passed
Why: Subtracting 24 wraps the hour back into range, giving a valid time of 02:00 — but there is no days attribute to carry into, so the day is simply discarded. That is a deliberate limitation of the Time class rather than a bug in add, and recognising the difference between the two is a real design skill.
Edge cases
Nothing in the class enforces valid ranges.
Discussion prompt
The constructor accepts any values, and add can produce 20:66. What other nonsensical Time objects can be created — and where would you put a check to prevent them?
Hint: Lesson 11a suggested a place.
Answer:
new Time(99, -5, 1000.0) is perfectly legal — the constructor stores whatever it is given. So is any value a setter is handed.
The place to check is the constructor and the setters, which are the only ways data enters the object. That is exactly what Lesson 11a's validating setter did, and it is Lesson 5b's validate at the boundary applied to a class: the class's boundary is its public methods.
Making the private data private is what makes this possible. If hour were public, no check could ever be enforced — anyone could write t.hour = 99 and there would be nowhere to intervene. That is the strongest practical argument for information hiding, and it arrives here rather than in Chapter 11a because you now have a concrete invalid value to point at.
Comparison
Fill the blanks. All three are instance methods, and each replaces something a library class already has.
Comparison matrix
| method | returns | what it decides |
|---|---|---|
| toString | a String | how this object should be displayed |
| equals | a boolean | what it means for two of these objects to be equal |
| add | a new Time | how two of these objects combine |
Notice that all three return rather than print or modify. That is not an accident: a method that returns can be composed, stored and tested, which is the argument from Lesson 4b applied to a class you wrote.
Pattern
Converting between a static method and an instance method is three mechanical edits, and knowing them makes the meaning of this concrete.
// static: everything arrives as a parameter
public static Time add(Time t1, Time t2) {
... t1.hour ...
}
Time end = Time.add(start, running);
// instance: the first parameter becomes this
public Time add(Time t2) { // 1. drop static, 2. drop t1
... this.hour ... // 3. replace t1 with this
}
Time end = start.add(running); // and the argument moves left| question | the answer |
|---|---|
| how does the object get into an instance method? | as this, without appearing in the parameters |
| why can main not use instance variables? | it is static, so there is no this |
| what does println do with an object? | calls its toString |
| what does == ask about two objects? | whether they are the same object |
+.== is identity and always means the same thing; equals is equivalence and is defined per class.== — use a tolerance.Check
Work it out before you click.
public String toString() {
return String.format("%02d:%02d", this.hour, this.minute);
}
Time t = new Time(9, 5, 0.0);
System.out.println(t);| value | specifier | output |
|---|---|---|
| 9 | %02d | 09 |
| 5 | %02d | 05 |
Check your understanding
What is displayed?
Answer: A
Why: println invokes the object's toString automatically and displays what it returns, and %02d pads each value to two digits with a leading zero. The padding is what makes a column of times line up rather than reading as 9:5.
Check
Work it out before you click.
Time a = new Time(9, 30, 0.0);
Time b = a;
Time c = new Time(9, 30, 0.0);| comparison | same object? | same values? |
|---|---|---|
| a and b | yes | yes |
| a and c | no | yes |
Check your understanding
Which comparisons are true, assuming Time has the equals method from this lesson?
Answer: A
Why: b = a copies the reference, so a and b are the same object and both comparisons succeed. c was created separately, so a == c is false — but the values match, so a.equals(c) is true. That is precisely the distinction between identity and equivalence.
a == c is false: two separate new calls produce two objects however identical their contents.a == b is true as well, since b was assigned a's reference.Check
Work it out before you click.
// 0:59:30.0 + 0:00:45.0| step | minute | second |
|---|---|---|
| after adding | 59 | 75.0 |
| carry seconds | 60 | 15.0 |
| carry minutes | 0, hour + 1 | 15.0 |
Check your understanding
With the corrected add, what is the result?
Answer: A
Why: The seconds carry first, turning 75 into 15 and pushing the minutes to 60; the minute check then carries again, giving 0 minutes and one more hour. Handling the seconds before the minutes is what makes the second carry possible — reversing the order would leave 60 minutes standing.
Real world
Think Java deliberately presents an add method that is wrong, lets it look right, and then repairs it. That choice is worth noticing.
Discussion prompt
The first version of add is symmetrical, readable and obviously about adding — and produces 20:66. What kind of testing would have caught it, and what kind would not?
Hint: Which inputs cross a boundary?
Answer:
Adding 1:00 and 2:00 gives 3:00 and passes. Only an input that crosses a boundary — where a column exceeds its maximum — exercises the missing code at all.
So the testing that catches it is testing chosen deliberately at the edges, which is Lesson 4b's rule about knowing the right answer: you pick 18:50 + 2:16 because you can see it should be 21:06 and that a naive version would not give that.
The transferable habit: for any method that combines values, ask what the valid range of each output field is, and test an input that pushes one past it. Times, dates, money, angles, array indexes — the same question finds the same class of bug in all of them.
Commit first
Commit to an answer and to your confidence.
Predict first
Why does Time's equals compare the seconds with a tolerance rather than with ==?
Correct: Because second is a double, and rounding error makes exact comparison of doubles unreliable
Why: Lesson 2b showed that most floating-point numbers are stored only approximately, so two values that ought to be equal can differ in their last bits — which makes == give false for times that are genuinely the same. Checking Math.abs(difference) < DELTA asks whether they are close enough instead. Note the contrast within the same method: hour and minute are ints, so they are compared with ==, and that is correct. The rule is about doubles specifically, not about equals.
Explain it
Two minutes, drawing as you go.
Discussion prompt
A classmate wrote an equals method for their own class and is confused that == gives false for two objects with identical data. Draw the memory diagram and explain the difference between the two comparisons — then say why Java could not simply make == compare contents.
Hint: The second half is the interesting part.
Answer:
Draw two variables with arrows to two separate objects holding the same values. == asks whether the arrows point at the same box — they do not, so it is false. equals looks inside both boxes and compares what it finds.
Java cannot make == compare contents because equivalence means something different for every class. Two Cards might be equal if the ranks match; two customer records if the IDs match. Only the class's author knows, so only the class can define it — while identity has exactly one meaning and never needs defining.
If the drawing has one object in it, start again. The whole explanation depends on there being two.
Exit ticket
One question before you close the deck.
Predict first
What are the three changes that turn a static method into an instance method?
Correct: Remove static, remove the first parameter, and replace its uses with this
Why: Those three edits convert Time.add(t1, t2) into t1.add(t2): the object that was the first parameter now arrives implicitly as this, and dropping static is what allows a this to exist at all. At the call site the first argument moves in front of the dot. Static methods and instance methods do the same thing — the difference is only how the object gets in, which is also why main being static means it has no this to offer.
Connect it up
One page, from memory.
Draw it
Draw the memory diagram for Time a = new Time(9,30,0); Time b = a; Time c = new Time(9,30,0); and write beside it the value of a == b, a == c and a.equals(c). Then write the static and instance versions of add side by side and circle the three differences. Finally, trace adding 0:59:30 and 0:00:45 through the three carrying checks in order, and say what would go wrong if the minute check came first.
Recap
Four sections that turn the Time class from a container into something you can display, compare and combine.
| if you remember one thing | it is this |
|---|---|
| about printing | toString returns; println calls it |
| about comparing | == is identity, equals is equivalence |
| about combining | test an input that crosses a boundary |
+ both call toString automatically, which is why printing a Point was readable.this and String.format rather than a parameter and printf.== tests identity — the same object; equals tests equivalence — the same values.==.this — three edits convert between them.Want this taught 1-on-1? Alexander tutors Java — $55/session, free consultation.