Quadratic Upgrade 2  •  Analysis & Critique  •  09/23/2026
TNT | JavaQuadraticSaga — Teacher’s Analysis

Quadratic Upgrade 2
Analysis & Critique

What changed from Upgrade 1 and why. The centerpiece: a live aliasing trap that proves why new is not optional when you want an independent object. Plus final, extends Object, super.toString(), and two new ivars.

APCS-Java Teacher & Java Developer Perspective
Overview

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.

The design philosophy shift: Upgrade 1 taught: “store derived values as objects.” Upgrade 2 teaches: “objects live at specific addresses; variables hold those addresses; assigning one variable to another copies the address, not the object.” The memoryAddress ivar is the mechanism that makes this lesson visible and provable in the driver output.
Design Decision #1

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.

For students: When something is logically a constant — a value that should never change — declare it 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.

Design Decision #2

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.

Design Decision #3

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

Key Insight

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.

Design Decision #4 — The Key Lesson

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.

The Trap

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:

// ─── Copy constructor: creates a NEW, independent object ─── Quadratic q6 = new Quadratic(q2); ← 'new' allocates separate memory q2.getMemoryAddress() → "Quadratic@1b6d3586" q6.getMemoryAddress() → "Quadratic@4554617c" ← DIFFERENT address Output: "q2 and q6 are different objects" // Mutating q6 does NOT affect q2: q6.setC(100) → q2 is unchanged ✓ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ // ─── Alias: q7 IS q2 (same address, same object) ────────── Quadratic q7 = q2; ← NO 'new' — copies the reference only q2.getMemoryAddress() → "Quadratic@1b6d3586" q7.getMemoryAddress() → "Quadratic@1b6d3586" ← SAME address! Output: "q2 and q7 are the same object!" // Mutating q7 corrupts q2: q7.setC(666); // q2 = "y = f(x) = 1.0x² + -5.0x + 666.0" ← q2 is now corrupted!

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; gives y its own copy of the value 5. Changing y does not affect x.
  • Reference variables (objects) hold an address — a pointer to where the object lives in memory. Quadratic q7 = q2; copies the pointer, not the data. Both q7 and q2 now 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.

The 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.
For students: Any time you want a separate, independent object, you must use 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.
Design Decision #5

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.

Design Decision #6

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.

Overall Assessment

Upgrade 1 vs Upgrade 2 — What Each One Teaches

These two upgrades are complementary, not competing. Upgrade 1 is about correctness and professional practice. Upgrade 2 is about self-awareness and the most common mistake new Java programmers make. Both belong in the toolkit of any APCS teacher.
What Works Well
  • final on 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 Object makes a crucial hidden relationship visible to students at exactly the right moment
  • super.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)
  • axisOfSymmetry as a pre-formatted String makes summary() output cleaner and uses DF.format() consistently
  • printSummary() demonstrates delegation; the commented-out old approach side-by-side shows the why clearly
What Could Be Stronger
  • extends Object is redundant in production code — it is a teaching aid, not a design decision; a comment explaining this distinction would help
  • The memoryAddress string 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 to System.out — a printSummary(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 use q2 (they don’t, but it’s a fragile ordering dependency)
  • axisOfSymmetry stores a formatted string rather than a raw double — less flexible if the display format needs to change
Upgrade 1 Teaches
  • Store derived values as typed objects, not raw numbers
  • Polymorphism: same array slot, different behavior at runtime
  • EPSILON for safe double comparison
  • Shared DecimalFormat for consistent output
  • Conditional logging with SHOULD_SHOW_BREADCRUMBS
  • summary() vs toString() separation of detail levels
Upgrade 2 Teaches
  • Aliasing vs copy constructors — the most critical new lesson
  • Reference semantics: variables hold addresses, not objects
  • final enforces true constant immutability
  • extends Object: every class has a parent, even when invisible
  • super.toString(): calling an overridden parent method
  • Utility-calling-utility delegation pattern
  • Objects can be “self-aware” about their own identity
Bottom line for students: Upgrade 1 makes the math better. Upgrade 2 makes the memory model visible. Both are APCS-level code. The aliasing trap in Upgrade 2 is the single most important lesson you can take from this series: when you need a separate object, you need new. No exceptions.