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

Quadratic Upgrade
Analysis & Critique

What works. What could be stronger. Why the roots-as-OrderedPair decision is more elegant than it first appears. Written for students who want to understand the design, not just the code.

APCS-Java Teacher & Java Developer Perspective
Overview

What Was Upgraded and Why It Matters

The Concept #8 version of the Quadratic class was already a well-formed example of OOP: it had all the PCNICOTGSU elements, three constructors, setter side effects, and the elegant use of ComplexOrderedPair to represent complex roots inside getRootsDescription(). That was a strong foundation.

This upgrade takes it to the next level. The central idea is that roots are not just numbers — they are points on a coordinate plane, and points belong in OrderedPair objects. Once you accept that framing, a cascade of improvements follows naturally: pre-computed roots, consistent output formatting, safe double comparisons, and a richer summary() method.

The design philosophy shift: In Concept #8, the roots were computed inline inside getRootsDescription() and thrown away after the string was built. In this upgrade, the roots are pre-computed and stored as instance variables in a roots[] array. They exist as real objects, accessible from outside the class, and they update automatically whenever a setter changes a coefficient. That is the difference between a function that produces output and an object that knows its own state.
Design Decision #1

The SHOULD_SHOW_BREADCRUMBS Pattern

Every non-trivial method in this upgrade prints its own name to the console when QuadraticDriver.SHOULD_SHOW_BREADCRUMBS is true. Set it to false and the program runs silently.

public static boolean SHOULD_SHOW_BREADCRUMBS = false;

// In any method:
if(QuadraticDriver.SHOULD_SHOW_BREADCRUMBS)
    System.out.println("...computeRoots...");

This is TNT’s version of a pattern you’ll encounter throughout your programming life: conditional logging. In professional Java, you’d use a logging framework (SLF4J, java.util.logging, Log4J) with configurable log levels. The concept here is identical: detailed trace output when you need it, silence when you don’t.

For students: When you’re debugging a constructor chain (like the one here, where every constructor calls four utility methods), it can be hard to tell which things ran and in what order. The breadcrumb pattern makes the execution trail visible. Try setting SHOULD_SHOW_BREADCRUMBS = true and running the driver. You’ll see exactly what fires and when — that’s more diagnostic information than a typical debugger breakpoint gives you on a single run.

Verdict: This is a solid teaching tool and a genuinely useful professional pattern. The one limitation is that a static boolean on the driver class is a blunt instrument — you either get ALL the breadcrumbs or none. A more flexible design would let individual classes control their own verbosity. But for a classroom context, this is exactly right.

Design Decision #2

The EPSILON Pattern for Double Comparison

This is one of the most important upgrades in the entire project, and it fixes a genuine bug.

In Concept #8, the degenerate quadratic check was:

if (a == 0) throw new IllegalArgumentException("a cannot be zero");

That looks correct. But a is a double, and comparing floating-point numbers for exact equality is unreliable. Floating-point arithmetic sometimes produces values like 0.0000000000000001 or -0.0000000000000001 instead of exactly 0.0, depending on how the number was computed. The test a == 0 would miss those cases entirely.

The upgrade replaces every equality check with a tolerance test:

public static double EPSILON = 1E-9;

// Instead of: if (a == 0)
if (Math.abs(a - 0.0) < QuadraticDriver.EPSILON)

This asks: “Is a within one billionth of zero?” That threshold is small enough to catch mathematically-zero values while large enough to be immune to floating-point noise.

The same pattern appears for the discriminant: Math.abs(discriminant - 0) < QuadraticDriver.EPSILON tests whether the discriminant is effectively zero — the case where the parabola has exactly one (repeated) root. Without this, a discriminant of -0.0000000000003 would incorrectly be treated as negative, producing complex roots when the correct answer is one real root.

Verdict: Essential. This is not a style preference; it’s correctness. Any professional Java developer reviewing this code would flag double == 0 as a bug. The EPSILON approach is the standard fix. One small note: Math.abs(discriminant - 0) could be simplified to Math.abs(discriminant) — subtracting zero changes nothing — but the intent is clearer with the subtraction explicitly written.

Design Decision #3

Shared DecimalFormat for Clean Output

Java’s default double-to-string conversion is not user-friendly. The number 3.0 prints as 3.0, but internal floating-point arithmetic often produces things like 2.9999999999999996 or 6.000000000000001. Without formatting, your Quadratic output looks like this:

// Without DecimalFormat:
Roots: x = R1(2.9999999999999996, 0.0) and x = R2(-3.0000000000000004, 0.0)

// With DF set to 3 decimal places max, 0 min:
Roots: x = R1(3, 0) and x = R2(-3, 0)

The upgrade stores a single DecimalFormat object as a static field on the driver:

public static DecimalFormat DF = decimalFormatManager(3);

Because DF is static and public, all four classes can use it simply by writing QuadraticDriver.DF.format(someDouble). One formatter, consistent output everywhere, no duplication.

Verdict: Excellent professional practice. The one architectural critique is that OrderedPair and ComplexOrderedPair now depend on QuadraticDriver — a support class is depending on the program’s main class. In a larger system you would put DF in a separate utility class (e.g., MathFormat) that any class could import independently. For this four-file teaching context, the current approach is practical and readable.

Design Decision #4 — The Key Insight

Roots as OrderedPair Objects: Why This Is Clever

This is the most important design decision in the entire upgrade. To understand it, you need to think geometrically first — then mathematically — then as an object designer.

The Geometric Insight

When we say “the root of a quadratic equation is x = 3,” we mean something specific: the graph of f(x) crosses the x-axis at the point where x equals 3. That crossing point is a coordinate on the Cartesian plane. The y-value there is zero, because that’s what “root” means: f(x) = 0. So the root x = 3 corresponds to the point (3, 0).

That’s exactly what an OrderedPair is.

Key Insight

A real root is not just a number — it is a point on the x-axis. Storing it as new OrderedPair(r, 0) makes this geometric truth part of the data structure itself. The ordered pair “knows” it’s on the x-axis because its y-coordinate is zero.

Three Cases, Three Constructions

The computeRoots() method handles all three discriminant cases using this framework:

// Case 1: discriminant > 0 — two distinct real roots
// Each root is a point where the parabola crosses the x-axis.
root1 = new OrderedPair(r1, 0);    // e.g., (3, 0) for x = 3
root2 = new OrderedPair(r2, 0);    // e.g., (2, 0) for x = 2

// Case 2: discriminant == 0 — one repeated root
// The parabola just touches the x-axis at the vertex.
// The x-coordinate of the repeated root IS the vertex's x-coordinate.
root1 = new OrderedPair(vertex.getX(), 0);   // same x as vertex, y = 0

// Case 3: discriminant < 0 — complex roots
// No real x-axis crossing. Complex roots use ComplexOrderedPair.
roots[0] = new ComplexOrderedPair(rp,  ip);  // a + bi
roots[1] = new ComplexOrderedPair(rp, -ip);  // a - bi (conjugate)

Notice the elegance of Case 2: the repeated root is literally the vertex’s x-coordinate. That makes mathematical sense — a parabola with one repeated root touches but does not cross the x-axis, and that touch point is the vertex. The code expresses this geometric relationship directly: vertex.getX().

Complex Roots: What Is ComplexOrderedPair Doing Here?

Complex roots are different. When the discriminant is negative, the parabola never crosses the x-axis. The roots are complex numbers of the form a + bi, where a is the real part and b is the imaginary part.

Now look at what ComplexOrderedPair already has from its parent OrderedPair: two instance variables, x and y. We can repurpose them:

  • x (inherited) → stores the real part of the complex root
  • y (inherited) → stores the imaginary part of the complex root

So new ComplexOrderedPair(rp, ip) creates an object where x = real part and y = imaginary part. No new fields needed. The inheritance gives us the storage for free.

Where do rp and ip come from? Using the quadratic formula for a negative discriminant:
• Real part: rp = -b / (2a) — the same as the vertex’s x-coordinate
• Imaginary part: ip = √(-discriminant) / (2a)
So for x² + 2x + 5: rp = −1, ip = √16/2 = 2, giving roots −1 + 2i and −1 − 2i.
Design Decision #5 — The Payoff

How Polymorphism Makes getRootsDescription() Work

Here is the most important Java concept in the entire upgrade, stated plainly:

Polymorphism at Work

The roots[] array is declared as OrderedPair[]. But it can hold ComplexOrderedPair objects too, because ComplexOrderedPair extends OrderedPair — a ComplexOrderedPair IS-A OrderedPair. When getRootsDescription() calls roots[0].toString(), Java automatically uses the actual type of what’s in that slot, not just the declared type of the array. This is called dynamic dispatch or runtime polymorphism.

In concrete terms, here is what happens for two different quadratics:

// x² - 5x + 6 (two real roots) roots[0] = new OrderedPair(3.0, 0) ← label "R1" roots[1] = new OrderedPair(2.0, 0) ← label "R2" roots[0].toString() "R1(3, 0)" ← OrderedPair.toString() roots[1].toString() "R2(2, 0)" ← OrderedPair.toString() Output: "Two real: x = R1(3, 0) and x = R2(2, 0)" ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ // x² + 2x + 5 (complex roots: −1 ± 2i) roots[0] = new ComplexOrderedPair(-1.0, 2.0) ← label "CR1" roots[1] = new ComplexOrderedPair(-1.0, -2.0) ← label "CR2" roots[0].toString() "-1 + 2i" ← ComplexOrderedPair.toString() (OVERRIDE!) roots[1].toString() "-1 - 2i" ← ComplexOrderedPair.toString() (OVERRIDE!) Output: "Complex: -1 + 2i and -1 - 2i"

The same code in getRootsDescription() — literally return "Complex: " + roots[0] + " and " + roots[1]; — produces completely different output depending on what type is actually stored in the array. The @Override annotation on ComplexOrderedPair.toString() is what makes this happen. Java looks at the runtime type of the object and calls the most specific toString() available. The caller never has to check: “Is this real or complex?”

Why this matters for your APCS exam: This pattern — declaring a variable or array as a parent type but storing child-type objects — is the textbook example of polymorphism. The question “What does roots[0].toString() print when roots[0] is a ComplexOrderedPair?” is exactly the kind of question that appears on AP practice exams. The answer is: it prints the overridden version. Always. Because Java always uses the actual type at runtime, not the declared type.
Design Decisions #6, #7, #8

The Y-Intercept, summary(), and Setter Side Effects

The Y-Intercept as OrderedPair

The y-intercept of f(x) = ax² + bx + c is always the point (0, c). The upgrade stores it as new OrderedPair(0, c) — the same geometric framing used for real roots. This creates a satisfying consistency: vertex, roots, and y-intercept are all stored as coordinate objects, not bare numbers. The parabola knows its own key points.

Notice that computeYIntercept() is called in all three constructors and inside setC() (but not setA() or setB(), since changing those coefficients doesn’t affect the y-intercept — the y-intercept only depends on c).

The summary() Method

The upgrade keeps toString() concise (just the equation) and adds a separate summary() method for detailed output. This is good API design: toString() should give you a quick snapshot; a specialized method gives you depth when you want it. The APCS convention is to keep toString() short and human-readable. A multi-line dump does not qualify as “short.”

Setter Side Effects and the Recompute Chain

Every setter in the upgraded Quadratic calls the full chain: computeYIntercept() (when relevant), computeDiscriminant(), computeVertex(), computeRoots(). This is the core discipline of encapsulation: when you change a coefficient, all derived values update automatically. The object is always in a consistent state. A caller who changes b via setB() does not have to remember to also call computeVertex(). The setter handles it.

The driver test that exercises this most clearly:

Quadratic q5 = new Quadratic(1, 0, -4);   // x² - 4: roots at x = ±2
q5.setB(-4);                               // now x² - 4x - 4
q5.setC(9);                               // now x² - 4x + 9: complex roots!
System.out.println(q5.summary());         // roots recalculated automatically
Overall Assessment

Pros, Cons, and What a Real Code Review Would Say

What Works Well
  • Storing roots as OrderedPair[] with polymorphism is genuinely elegant — the caller never needs an if/else for real vs. complex
  • EPSILON for double comparison is correct and professional — this is a real bug fix from Concept #8
  • DecimalFormat shared via a static constant eliminates formatting noise throughout
  • SHOULD_SHOW_BREADCRUMBS is a clean, zero-cost debug toggle
  • Every setter triggers the full recompute chain — the object is always consistent
  • summary() correctly separates quick-view (toString) from detailed-view output
  • The y-intercept as OrderedPair is geometrically consistent with how roots are stored
  • All PCNICOTGSU elements remain clearly annotated
What Could Be Stronger
  • roots[1] = null for the repeated-root case is a trap — calling roots[1].toString() crashes with NullPointerException; better to store the repeated root in both slots
  • System.exit(0) in a setter is aggressive — professional code throws and lets the caller decide; setters especially should never terminate the program
  • Math.abs(discriminant - 0) is slightly odd — Math.abs(discriminant) is clearer (subtracting zero adds noise, not clarity)
  • Tight coupling: OrderedPair and ComplexOrderedPair now reference QuadraticDriver directly — in a larger project these classes would define their own formatter
  • The -0.0 + 0.0 hack in computeVertex() is correct but fragile; a named constant or explicit check is clearer
  • computeRoots() called from the constructor chain means roots are computed four times on construction (all utility methods are called); a flag could prevent redundant work
Bottom line for students: This is APCS-level code that demonstrates real design thinking. The roots-as-OrderedPair idea, the polymorphic toString() dispatch, and the EPSILON fix are all decisions that a professional Java developer would approve of immediately. The cons listed above are the kinds of comments you would see in a real code review — not rejections, but refinements. Learning to see both sides of a design is exactly what this exercise is designed to teach.