What Changed from Upgrade 1 — and Why
Upgrade 1 was about making the Quadratic class correct: roots stored as
OrderedPair objects, EPSILON for safe double comparison, DecimalFormat
for clean output. Those are quality-of-life improvements to the computation itself.
Upgrade 2 is about making the class self-aware and the driver educational. The class now knows its own memory address. The driver now demonstrates, with real output, the difference between creating a new object and creating an alias. These additions do not change what the math computes — they change what the class teaches.
memoryAddress ivar is the mechanism that makes this lesson visible and
provable in the driver output.
The final Keyword on Constants
In Upgrade 1, the three driver constants were declared as static but not final:
// Upgrade 1 public static boolean SHOULD_SHOW_BREADCRUMBS = false; public static DecimalFormat DF = decimalFormatManager(3); public static double EPSILON = 1E-9;
Upgrade 2 adds final to all three:
// Upgrade 2 public static final boolean SHOULD_SHOW_BREADCRUMBS = false; public static final DecimalFormat DF = decimalFormatManager(3); public static final double EPSILON = 1E-9;
final on a variable means it can only be assigned once. For a static final
field, that assignment happens exactly once — when the class is loaded. After that, any
code that tries to reassign it gets a compile-time error, not a runtime bug.
That is the strongest possible guarantee: the problem is caught before the program even runs.
final. The convention in Java is to name
static final fields in ALL_CAPS_WITH_UNDERSCORES, which is why
EPSILON and SHOULD_SHOW_BREADCRUMBS look different from regular
variables. The ALL_CAPS naming is a signal to every reader: “this value is fixed; do
not try to change it.”
Verdict: This is a clear improvement. final on constants should
always be present. Its absence in Upgrade 1 was an oversight, not a decision. The only nuance:
final on a reference type (like DF, which is a
DecimalFormat object) means the variable cannot be reassigned to a different object,
but the object itself can still be mutated. For SHOULD_SHOW_BREADCRUMBS (primitive
boolean) and EPSILON (primitive double), final is a full immutability
guarantee. For DF, it prevents replacing the formatter with a different one, which
is the protection we actually care about.
extends Object — Making the Implicit Explicit
In Java, every class implicitly extends Object. You never have to write it.
Upgrade 2 writes it anyway:
public class Quadratic extends Object { // explicit parent declaration
...
}
Why? Because the next step — calling super.toString() — requires
students to understand that Quadratic has a parent class. When
extends Object is present in the source file, that parent is visible. Students
can see the inheritance chain without having to know it was there all along.
The super keyword in Java refers to the immediate parent class. When
Quadratic overrides toString() with its own version, the original
Object.toString() is not gone — it is just hidden. Calling
super.toString() inside Quadratic bypasses the override and goes
directly to Object.toString().
What does Object.toString() return? By default, it returns
a string of the form ClassName@hexadecimalHashCode. For example:
// Object.toString() default output: Quadratic@1b6d3586 // The hex number is System.identityHashCode() formatted as hex. // It corresponds to the object's location in JVM memory (in practice).
This is normally considered “useless” output — which is why every serious
class overrides toString() with something meaningful. But in Upgrade 2, it becomes
deliberately useful: it gives every Quadratic object a unique, printable identity
string that the driver can compare.
Verdict: extends Object is pure pedagogy. It adds no
functionality. In a production codebase, you would remove it because it is redundant noise.
In a teaching codebase, it is exactly the right move — it makes a previously invisible
relationship explicit and sets up the super.toString() lesson that follows.
obtainMemoryAddress() and the memoryAddress Ivar
Every Quadratic constructor ends with:
memoryAddress = obtainMemoryAddress();
And obtainMemoryAddress() is:
public String obtainMemoryAddress(){
if(QuadraticDriver.SHOULD_SHOW_BREADCRUMBS)
System.out.println("...obtainMemoryAddress...");
//we can call a method from the parent by using the prefix 'super'
String simulatedAddress = super.toString();
return simulatedAddress;
}//end obtainMemoryAddress
This captures the JVM’s default identity string for this object and stores
it as a private ivar. From that point on, the object knows its own address as a String, and
anyone can retrieve it via getMemoryAddress().
The address string is captured at construction time. For the copy constructor,
this happens after new allocates fresh memory — so the copy gets its
own address, different from the original. For an alias (q7 = q2), no
constructor runs at all. q7 simply holds the same reference as q2,
so they share the same memoryAddress string. Comparing these strings with
.equals() reveals whether you have two independent objects or one object
with two names.
A note on precision: what super.toString() returns is technically the
identity hash code formatted as hex, not a raw memory address. In modern JVMs,
the garbage collector may move objects, making the “address” a simplified
stand-in. For teaching purposes, “memory address” is the right mental model
even if the implementation is slightly different under the hood.
Verdict: Clever and purpose-built. The only concern is that capturing the address as a String ivar is a one-time snapshot — if Java ever moved the object in memory after construction, the stored string would be stale. In practice, Java’s identity hash code is stable for the lifetime of the object, so this works reliably. It is not something you would do in production code, but it is an excellent teaching instrument.
The Aliasing Trap — Why new Is Not Optional
This is the most important new teaching feature in Upgrade 2. It targets one of the most common misconceptions among new Java programmers and makes the consequence of that misconception visible in the program’s own output.
The Misconception
Beginners often believe that Quadratic q7 = q2; creates a second, independent
copy of q2. It does not. Java variables that hold objects are
references — they hold the memory address of an object, not the object
itself. The assignment q7 = q2 copies the address from q2
into q7. Both variables now point to the same object.
Quadratic q7 = q2; does NOT create a new object. No constructor runs.
No memory is allocated. q7 is just another name for the same block of
memory that q2 refers to. Changing the object through q7
changes it through q2 as well — because they are the
same object.
What the Driver Proves
The driver tests both cases and reports the memory addresses:
The driver confirms this in plain English: "q2, the 'original' was changed too!!!".
The three exclamation marks are intentional — this is a surprise when you first
encounter it, and the output drives the lesson home.
Why This Happens: References vs Values
Java uses two kinds of variables:
- Primitive variables (
int,double,boolean…) hold the actual value directly.int x = 5; int y = x;givesyits own copy of the value 5. Changingydoes not affectx. - Reference variables (objects) hold an address — a pointer to
where the object lives in memory.
Quadratic q7 = q2;copies the pointer, not the data. Bothq7andq2now hold the same address.
The copy constructor (new Quadratic(q2)) allocates new memory and copies
the field values from q2 into the new object. The new object is at a different
address. From that point on, they are fully independent.
memoryAddress ivar is the key that unlocks this lesson.
Without it, you can describe aliasing in words, but students have to take your word for it.
With it, the program itself prints the evidence. The addresses match for the alias. They
differ for the copy constructor. Students see the proof in the output of code they can
run themselves.
new. Assignment (=) between two reference variables never
creates a new object — it only makes a second variable point at the same existing
object. This distinction matters enormously when you start writing methods that receive
objects as parameters: the method receives the same reference, not a copy. Changes made
inside the method affect the original object. This is called
“pass by reference” behavior (though Java is technically
“pass by value of the reference”). Aliasing is the same principle applied
to variable assignment rather than method parameters.
axisOfSymmetry — A Computed String Ivar
The axis of symmetry of f(x) = ax² + bx + c is the vertical line
x = −b/(2a) — the same x-coordinate as the vertex. Upgrade 2 stores
this as a formatted string:
private String axisOfSymmetry; // "x = 2.5" or "x = -1" // Computed inside computeVertex(): axisOfSymmetry = "x = " + QuadraticDriver.DF.format(h);
Why a String rather than a double? Because the axis of symmetry
is already a labeled equation — "x = 2.5" is what you write on a math
exam, not just 2.5. Storing it pre-formatted means the summary()
method can print it without any additional work. The DF.format(h) call also
ensures it displays cleanly (e.g., "x = -1" instead of
"x = -0.9999999999").
A secondary benefit: axisOfSymmetry is computed inside computeVertex()
rather than in a separate utility method. This is correct because the axis of symmetry is
literally the x-coordinate of the vertex. They share the same computation. There is no need to
compute h twice.
Verdict: A clean, well-reasoned addition. The only design critique is that
it ties the string’s format to the choice to store it as a String at all.
If you later wanted to change the format (e.g., to "x = 5/2" as a fraction),
you would need to change how the ivar is stored. A double ivar with a formatting
method would be more flexible. But for a teaching context where the display format is fixed,
a pre-formatted string is simpler and perfectly appropriate.
printSummary() — Utility Calling Utility
Upgrade 1 used summary() to build a multi-line string, and the driver
called System.out.println(q4.summary()) to print it. Upgrade 2 adds
printSummary():
// Driver (Upgrade 1):
System.out.println(q4.summary());
// Driver (Upgrade 2) — the commented line shows the old approach:
//System.out.println(q4.summary());
q4.printSummary(); // utility calling a utility
// Inside Quadratic.java:
public void printSummary(){
if(QuadraticDriver.SHOULD_SHOW_BREADCRUMBS)
System.out.println("...printSummary...");
System.out.println(this.summary());
}//end printSummary
The comment “utility calling a utility” is deliberate. It is a
pattern worth naming. printSummary() does not compute anything; it delegates
entirely to summary(). summary() in turn delegates to
toString(), getVertex(), getRootsDescription(),
and the getters. The whole system is a chain of small responsibilities.
The practical benefit for the driver is minor: q4.printSummary() is shorter
to type than System.out.println(q4.summary()), and the intent is clearer
(“print a summary of this object” vs “print whatever the string
that summary() returns happens to look like”). The pedagogical benefit is larger:
it demonstrates that a method does not have to do interesting work itself; it can simply
orchestrate calls to other methods.
Verdict: A minor but instructive addition. The one trade-off is that
printSummary() hard-codes the output destination to
System.out. A more flexible design would have summary() return
the string (as it does) and leave the printing decision to the caller. That is
actually the better design — but printSummary() remains a useful
teaching example of the delegation pattern.
Upgrade 1 vs Upgrade 2 — What Each One Teaches
finalon constants is correct and should have been there from the start- The aliasing demonstration is the most effective teaching feature in either upgrade — students see the proof, not just hear the warning
extends Objectmakes a crucial hidden relationship visible to students at exactly the right momentsuper.toString()is a concrete, working example of calling an overridden parent method — a concept that is often abstract in textbooks- Memory address comparison using
.equals()is technically correct (String comparison, not reference comparison) axisOfSymmetryas a pre-formatted String makessummary()output cleaner and usesDF.format()consistentlyprintSummary()demonstrates delegation; the commented-out old approach side-by-side shows the why clearly
extends Objectis redundant in production code — it is a teaching aid, not a design decision; a comment explaining this distinction would help- The
memoryAddressstring is a snapshot, not a live address — the method is called “obtainMemoryAddress” but what it stores is an identity hash, which is subtly different from a raw pointer - Unused imports in
Quadratic.java(DecimalFormat,NumberFormat,RoundingMode) — these were moved to the driver in Upgrade 1 but the imports weren’t cleaned up; any IDE would flag these as warnings printSummary()couples output toSystem.out— aprintSummary(PrintStream out)overload would be more reusable- The alias demonstration mutates the original
q2, which then affects the remaining test runs in the driver if they useq2(they don’t, but it’s a fragile ordering dependency) axisOfSymmetrystores a formatted string rather than a raw double — less flexible if the display format needs to change
- Store derived values as typed objects, not raw numbers
- Polymorphism: same array slot, different behavior at runtime
- EPSILON for safe double comparison
- Shared
DecimalFormatfor consistent output - Conditional logging with
SHOULD_SHOW_BREADCRUMBS summary()vstoString()separation of detail levels
- Aliasing vs copy constructors — the most critical new lesson
- Reference semantics: variables hold addresses, not objects
finalenforces true constant immutabilityextends Object: every class has a parent, even when invisiblesuper.toString(): calling an overridden parent method- Utility-calling-utility delegation pattern
- Objects can be “self-aware” about their own identity
new.
No exceptions.