toString, equals, and Methods That Combine Objects

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

What this lesson covers

The lesson, slide by slide

1. toString, equals, and Methods That Combine Objects

Title

Think Java 2e · Chapter 11 · Designing Classes

Sections 11.5-11.8 · pp. 190-196

2. What you will be able to do

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.

3. Retrieve before you read

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.

4. Making a class behave like a library class

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.

5. Displaying objects

Section

Section 11.5

6. A method that prints the attributes

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);
}
specifiermeansfor 11, 59, 59.9
%02dtotal width 2, leading zeros if necessary11
%02dthe same, for the minute59
%04.1ftotal width 4, one digit after the point, leading zeros59.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.

7. Why this method is not idiomatic

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.

8. Reading the format string

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);
valuespecifieroutputwhy
9%02d09padded to two digits with a leading zero
59%02d59already two digits
5.5%04.1f05.5width 4 including the point, one decimal
59.9%04.1f59.9already 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.

9. What does %02d do with 7?

Prediction

Width and padding.

System.out.printf("%02d", 7);
optioneffect
2total width of two characters
0pad with zeros rather than spaces

Predict first

What is displayed?

  • 07
  • 7
  • 7
  • 70

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.

10. What println does with an object

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 outputmeaning
Timethe type of the object
@separator
80cc7c0its 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.

11. A print method that cannot be reused

Trap

The 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 topossible with printTime?
display ityes
store the textno
put it inside a longer messageno
compare two times' textno

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.

The fix

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
useworks with toString?
println(time)yes — invoked automatically
a String variableyes
concatenationyes — + calls toString too
comparisonyes

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.

12. What does println show for an object with no toString?

Prediction

The default behaviour.

Time time = new Time(11, 59, 59.9);
System.out.println(time);
has a toString?output
noTime@80cc7c0
yeswhatever it returns

Predict first

What appears?

  • Something like Time@80cc7c0 — the type and address
  • 11:59:59.9
  • a compile error
  • nothing

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.

13. Which specifier for which value?

Definition probe

The letter must match the type.

Sort into buckets

Sort each value by the specifier it needs.

%d
the hour, an int; the minute, an int
%f
the second, a double
%s
a name, a String
d
An integer value. Handing %d a double throws IllegalFormatConversionException at run time, since the compiler never checks a format string.
f
A floating-point value, and %04.1f additionally controls the width and the number of decimals.
s
Text. %s is the most forgiving specifier — almost anything can be displayed as a string.

14. Why is printTime static and toString not?

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.

15. The toString method

Section

Section 11.6

16. Override the default representation

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 printTimewhy
no static keywordit is an instance method, invoked on an object
this instead of a parameterthe object arrives implicitly
String.format instead of printfit returns the text rather than displaying it
returns Stringso 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.

17. The three changes from printTime

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.
  • The return type becomes String. That is what makes the text available to the caller instead of going straight to the screen.
  • static disappears. A static method belongs to the class; this one belongs to each object and needs this to know which.
  • The name must be exactly 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.

18. Two ways to invoke it

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
writtenwho calls toStringresult
time.toString()youthe string "11:59:59.9"
println(time)printlndisplays 11:59:59.9
"at " + timethe + operatorthe 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.

19. What does this display?

Prediction

toString is defined and returns 11:59:59.9

Time time = new Time(11, 59, 59.9);
System.out.println("Now: " + time);
stepwhat happens
the + operatorone operand is a String, so it concatenates
converting time to textcalls its toString
result"Now: 11:59:59.9"

Predict first

What appears?

  • Now: 11:59:59.9
  • Now: Time@80cc7c0
  • a compile error
  • Now: followed by nothing

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.

20. Where you have seen this already

Concept

Every readable output from an object in this book has been a toString. Naming it explains all of them at once.

printingoutputbecause
an int5primitives are not objects
a Stringits charactersString has a toString
a Pointjava.awt.Point[x=3,y=4]Point has one
an array[I@bf3f7e0arrays do not
a Time with no toStringTime@80cc7c0the default one
a Time with toString11:59:59.9yours

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.

21. Writing print inside toString

Trap

The 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);
}
problemconsequence
returns voidprintln has nothing to display
prints directlythe text cannot be stored or concatenated
does not match the expected signatureJava 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.

The fix

Return a String; print nothing.

public String toString() {
    return String.format("%02d:%02d:%04.1f\n",
                         this.hour, this.minute, this.second);
}
requiredvalue
nametoString
parametersnone
return typeString
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.

22. Static or instance method?

Definition probe

Does the method need a particular object?

Sort into buckets

Sort each method.

instance method
public String toString(); public int getHour()
static method
public static void printTime(Time t); public static int max(int a, int b)
inst
No static keyword. It is invoked on an object, which arrives as this — so it can read that object's instance variables without being handed them.
stat
Marked static, so it belongs to the class rather than to any object. Everything it works on must arrive as a parameter.

23. Write toString

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.

24. Why does every class get a toString?

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.

25. The equals method

Section

Section 11.7

26. Identity against equivalence

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
asksare these references identical?are these objects equivalent?
meansdo they refer to the same object?do they have the same values?
definitionalways the samedifferent for different classes
can a class change it?noyes — 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.

27. Three variables, two objects

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.

28. Writing equals for Time

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;
}
comparisonoperatorwhy
this.hour == that.hour==ints — exact comparison is right
this.minute == that.minute==ints
Math.abs(this.second - that.second) < DELTAa tolerancedoubles — 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.

29. What are the three comparisons?

Prediction

Two objects, three variables.

Time time1 = new Time(9, 30, 0.0);
Time time2 = time1;
Time time3 = new Time(9, 30, 0.0);
comparisonidentical?equivalent?
time1 and time2yesyes
time1 and time3noyes

Predict first

What is time1 == time3?

  • false — they are different objects
  • true — they hold the same values
  • true — == compares contents for objects
  • a compile error

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.

30. Why a tolerance, and why 0.001

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
approachfor 59.9 and 59.90000000000001
this.second == that.secondfalse — the bits differ
Math.abs(difference) < 0.001true — 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.

31. Relying on the default equals

Trap

The 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 expectwhat the default does
compares the valuescompares the references
true for equivalent timesfalse for different objects
a meaningful answerthe 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.

The fix

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;
}
classtwo objects are equal when
Stringthey contain the same characters
Integerthey wrap the same value
Timehour, minute and second all match
a class with no equalsthey 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.

32. == or equals?

Definition probe

Identity is always the same question; equivalence is defined per class.

Sort into buckets

Sort each comparison by what it asks.

==
two ints; whether two Times are the same object
equals
whether two Times represent the same moment; whether two Strings contain the same characters
eq
Either they are primitives, where == compares values directly, or you genuinely want to know whether two references point at the same object.
meth
You are asking about equivalence — whether the contents match — which is defined by the class's own equals method.

33. Why the tolerance?

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);
valuestored as
0.1 * 30.30000000000000004
0.30.3 (approximately)

Predict first

Why does equals compare seconds with a tolerance rather than ==?

  • Because rounding error means values that should be equal often are not, bit for bit
  • Because == does not work on doubles at all
  • Because seconds are more important than minutes
  • Because Math.abs is faster 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.

34. Could two Times be equal without matching?

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.

35. Static methods and instance methods

Section

Section 11.8

36. The same work, two shapes

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);
differencestaticinstance
the keywordhas staticdoes not
parameterstwo: t1 and t2one explicit: t2, plus the implicit this
invoked withthe 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.

37. Converting between the two forms

Notation

Three edits turn one into the other, and they are always the same three.

Annotate

  • Remove the static keyword. That is what makes the method belong to each object rather than to the class.
  • Remove the first parameter. It no longer arrives explicitly — the object the method is invoked on takes its place.
  • Replace every use of it with this. t1.hour becomes this.hour, throughout the body.
  • And at the call site, Time.add(a, b) becomes a.add(b) — the first argument moves in front of the dot.
  • Nothing else changes. The arithmetic, the local variable, the return statement are all identical.

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.

38. Both versions, side by side

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 versioninstance version
startTime arrives asthe parameter t1this
runningTime arrives asthe parameter t2the parameter t2
what is computed18+2, 50+16, 0.0+0.0the same
what is returneda new Timea 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.

39. How is each version invoked?

Prediction

The first argument moves in front of the dot.

public static Time add(Time t1, Time t2) { ... }
public Time add(Time t2) { ... }
methodcall
staticTime.add(startTime, runningTime)
instancestartTime.add(runningTime)

Predict first

How do you invoke the instance version?

  • startTime.add(runningTime)
  • Time.add(startTime, runningTime)
  • add(startTime, runningTime)
  • new Time().add(startTime, runningTime)

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(...).

40. Which form to prefer

Concept

The two are interchangeable, so the choice is about which reads better and which class the behaviour belongs to.

preferwhen
an instance methodthe behaviour belongs to one object — startTime.add(runningTime)
a static methodthere is no natural owner — Math.max(a, b)
a static methodit operates on the class rather than an instance — Integer.parseInt
an instance methodyou 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.

41. Using this in a static method

Trap

The 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 kindis there a this?why
instance methodyesinvoked ON an object
static methodnoinvoked 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.

The fix

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 methodin an instance method
every object arrives as a parameterone arrives as this
no thisthis is available
invoked on the classinvoked 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.

42. Static or instance?

Definition probe

Does the method operate on a particular object?

Sort into buckets

Sort each method you have met.

static — invoked on a class
Math.sqrt(x); Integer.parseInt(s)
instance — invoked on an object
time.toString(); startTime.add(runningTime); s.length()
stat
Invoked with the class name, because no particular object owns the operation. Everything it needs arrives as a parameter.
inst
Invoked on an object, which arrives as this and supplies the data the method works with.

43. Convert to an instance method

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.

44. Why is main static?

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.

45. Carrying, and why the first version was wrong

Section

Section 11.8, continued

46. 20:66 is not a time

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
fieldvalid rangethe resultvalid?
hour0 to 2320yes
minute0 to 5966no
second0.0 to 59.99...0.0yes

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.

47. Carrying, in order

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.

48. The corrected add

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;
}
stephourminutesecond
after adding20660.0
second >= 60.0?2066no change
minute >= 60?2160.0
hour >= 24?no change60.0
result2160.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.

49. What does the uncorrected add produce?

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;
}
field18:50 + 2:16
hour20
minute66

Predict first

What does it return?

  • 20:66 — which is not a valid time
  • 21:06
  • a run-time error
  • 20:06

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.

50. Why the bug survived a reading

Concept

The original method is symmetrical, readable and obviously about adding. Everything about its appearance is reassuring.

what made it look rightwhat would have caught it
three parallel lines, one per fieldtrying an example with a carry
no obviously missing caseasking what the valid range of each field is
it compiles and runschecking the output against a real clock
it works for 1:00 + 2:00choosing 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.

51. Carrying in the wrong order

Trap

The 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:45minutesecond
after adding5975.0
minute >= 60? no5975.0
second >= 60? yes6015.0
result60 — invalid15.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.

The fix

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:45minutesecond
after adding5975.0
second >= 60? yes6015.0
minute >= 60? yes0 and hour + 115.0
result1:00:15valid

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.

52. Order the carrying checks

Ranking

A carry can trigger the next one.

Put in order

  1. add the three fields
  2. if second >= 60, carry into minute
  3. if minute >= 60, carry into hour
  4. if hour >= 24, subtract 24

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.

53. What happens past midnight?

Prediction

The final check has no column to carry into.

if (sum.hour >= 24) {
    sum.hour -= 24;
}
// adding 20:00 and 6:00
stephour
after adding26
hour >= 24? yes2
what is lostthe fact that it is the next day

Predict first

What does adding 20:00 and 6:00 give, and what is lost?

  • 02:00, and the information that a day has passed
  • 26:00, which is invalid
  • an exception, because hours cannot exceed 23
  • 00:00

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.

54. What other invalid Times can exist?

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.

55. The three methods this lesson added

Comparison

Fill the blanks. All three are instance methods, and each replaces something a library class already has.

Comparison matrix

methodreturnswhat it decides
toStringa Stringhow this object should be displayed
equalsa booleanwhat it means for two of these objects to be equal
adda new Timehow 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.

56. The pattern to carry away

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
questionthe 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

57. Check: toString

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);
valuespecifieroutput
9%02d09
5%02d05

Check your understanding

What is displayed?

  • A. 09:05 (correct)
  • B. 9:5
  • C. Time@80cc7c0
  • D. nothing — toString does not print

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.

Why B tempts people
This ignores the 02 width option, which pads single digits with a leading zero.
Why C tempts people
That is the default toString's output, used only when a class does not provide its own.
Why D tempts people
toString indeed does not print, but println prints what it returns — which is the whole mechanism.

58. Check: identity against equivalence

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);
comparisonsame object?same values?
a and byesyes
a and cnoyes

Check your understanding

Which comparisons are true, assuming Time has the equals method from this lesson?

  • A. a == b, a.equals(b) and a.equals(c) — but not a == c (correct)
  • B. all four
  • C. only a == b
  • D. only the two equals calls

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.

Why B tempts people
a == c is false: two separate new calls produce two objects however identical their contents.
Why C tempts people
This ignores the equals method entirely, which is exactly what the class defined it for.
Why D tempts people
a == b is true as well, since b was assigned a's reference.

59. Check: carrying

Check

Work it out before you click.

// 0:59:30.0  +  0:00:45.0
stepminutesecond
after adding5975.0
carry seconds6015.0
carry minutes0, hour + 115.0

Check your understanding

With the corrected add, what is the result?

  • A. 1:00:15.0 (correct)
  • B. 0:59:75.0
  • C. 0:60:15.0
  • D. 1:59:15.0

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.

Why B tempts people
That is the result with no carrying at all, which is the uncorrected version.
Why C tempts people
This carries the seconds but stops there, which is what happens if the minute check runs first.
Why D tempts people
This carries into the hour without reducing the minutes, which no single check does.

60. Correct-looking code

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.

61. How sure are you?

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 ==?

  • Because second is a double, and rounding error makes exact comparison of doubles unreliable
  • Because == does not compile for doubles
  • Because seconds are less important than hours
  • Because equals must always use Math.abs

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.

62. Explain it to someone else

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.

63. Exit ticket

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?

  • Remove static, remove the first parameter, and replace its uses with this
  • Add a return type, remove the parameters, and rename it
  • Make it private, add this as a parameter, and remove the class name
  • Remove the return type, add static to the class, and rename the parameters

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.

64. Draw the whole lesson

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.

65. Recap

Recap

Four sections that turn the Time class from a container into something you can display, compare and combine.

if you remember one thingit is this
about printingtoString returns; println calls it
about comparing== is identity, equals is equivalence
about combiningtest an input that crosses a boundary

Sources

  1. 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
  2. Java SE 21 API — java.lang.Object (toString, equals)
  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