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.
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.
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.
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.
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.
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.
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.
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.
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 rooty(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.
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.
How Polymorphism Makes getRootsDescription() Work
Here is the most important Java concept in the entire upgrade, stated plainly:
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:
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?”
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.
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
Pros, Cons, and What a Real Code Review Would Say
- 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
DecimalFormatshared via a static constant eliminates formatting noise throughoutSHOULD_SHOW_BREADCRUMBSis 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
OrderedPairis geometrically consistent with how roots are stored - All PCNICOTGSU elements remain clearly annotated
roots[1] = nullfor the repeated-root case is a trap — callingroots[1].toString()crashes with NullPointerException; better to store the repeated root in both slotsSystem.exit(0)in a setter is aggressive — professional code throws and lets the caller decide; setters especially should never terminate the programMath.abs(discriminant - 0)is slightly odd —Math.abs(discriminant)is clearer (subtracting zero adds noise, not clarity)- Tight coupling:
OrderedPairandComplexOrderedPairnow referenceQuadraticDriverdirectly — in a larger project these classes would define their own formatter - The
-0.0 + 0.0hack incomputeVertex()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
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.