Last updated: 2026-09-16
Case Study: The Numerical Tower
Students are taught that int and double are the defaults — and rarely see how those abstractions leak until a critical bug arrives. This case study builds a numerical tower: a hierarchy of number types, inspired by Scheme and Lisp but built cleanly in modern Java with records and interfaces, in which values promote themselves upward when a computation demands more than their representation can actually deliver. The name and the underlying idea are Scheme's own: the language standard defines a tower of number types (integer, rational, real, complex) in which each level conceptually sits on the one below, with arithmetic between mismatched levels resolved by promoting the less general operand upward1 — precisely the promotion rule this Java case study reimplements with interfaces and records instead of Scheme's built-in numeric types. It ties together interface contracts, polymorphism, immutability and performance trade-offs — the whole OOP track in one worked example. (The complete source, NumericalTower.java, accompanies this page.)
1. The Broken Abstractions of Hardware Primitives
The integer overflow illusion
long x = Long.MAX_VALUE;
System.out.println(x + 1); // -9223372036854775808 — silently negative
Hardware arithmetic does not fail loudly; it wraps. No exception, no warning — it simply pollutes your data space with quiet garbage. The tower's ground floor refuses that bargain: PrimitiveInt wraps a long but performs arithmetic with Math.addExact(), which throws on overflow — and the tower catches that signal and promotes both operands to the next level instead of corrupting the result:
record PrimitiveInt(long value) implements TowerNumber {
@Override
public TowerNumber add(TowerNumber other) {
if (other.getLevel() > this.getLevel()) return other.add(this);
long otherVal = ((PrimitiveInt) other).value();
try {
return new PrimitiveInt(Math.addExact(this.value, otherVal));
} catch (ArithmeticException e) {
return this.promoteTo(2, null).add(other.promoteTo(2, null)); // escalate, don't corrupt
}
}
}
The floating-point catastrophe
System.out.println(0.1 + 0.2 == 0.3); // false
System.out.println(0.1 + 0.2); // 0.30000000000000004
IEEE 754 cannot represent 0.1 or 0.2 exactly in binary, and the tiny errors compound. For graphics it rarely matters; for currency, compliance or anything cumulative it is a correctness bug wearing a performance costume — a direct failure of the verifiability pillar in Trustworthy Software. The tower's answer is a level of exact rationals: BigFraction keeps numerator and denominator as BigIntegers, normalised by GCD in the record's compact constructor, so \(\tfrac{1}{10} + \tfrac{2}{10}\) is exactly \(\tfrac{3}{10}\).
2. Anatomy of the Tower
(exact-checked long)"] --> L2["Level 2: BigFraction
(exact rationals)"] L2 --> L3["Level 3: LazySymbolic
(π, e — evaluated on demand)"] L3 --> L4["Level 4: ArbitraryDecimal
(BigDecimal, chosen precision)"] L4 --> L5["Level 5: ComplexNumber
(real + imaginary)"]
The power of the interface contract
Everything rests on one small contract:
interface TowerNumber {
int getLevel();
TowerNumber add(TowerNumber other);
TowerNumber multiply(TowerNumber other);
TowerNumber promoteTo(int level, MathContext mc);
}
Client code never asks how a number is implemented — only that it can add, multiply, and promote. Any mix of levels can flow through the same arithmetic; this is Liskov substitution doing real work, a far better illustration than "Cat extends Animal" (see SOLID).
Symmetric dispatch via promotion
Binary operations are OOP's awkward corner: add dispatches on one receiver, but addition has two operands of possibly different types. How do you add a PrimitiveInt to a ComplexNumber? The tower's elegant answer is the level check — one line at the top of every operation:
if (other.getLevel() > this.getLevel()) return other.add(this);
A lower type meeting a higher type yields control; the higher type then promotes the lower operand up to its own level and proceeds. Two records and a comparison achieve what double-dispatch visitor machinery usually does with ceremony — and adding a Level 6 touches no existing class.
Lazy evaluation: a number that hasn't happened yet
record LazySymbolic(String symbol,
Function<MathContext, BigDecimal> evaluator) implements TowerNumber {
// π is not 3.14159...; π is a RECIPE that yields 3.14159... to any requested precision
}
LazySymbolic demonstrates that a "number" need not be bits in memory — it can be a closure that delays a costly high-precision computation (the accompanying PiCalculator is such a recipe) until someone actually asks, with a required precision. Symbolic exactness upstream, numeric commitment only at the last moment.
3. The Performance Matrix
Absolute precision is not free; engineering is the art of priced trade-offs:
| Type | Representation | Operation cost | Memory | Best for |
|---|---|---|---|---|
Hardware primitives (long) | 64-bit raw registers, unboxed | \(O(1)\) — single CPU cycle | 8 bytes | Game loops, graphics, array indexing |
Exact fractions (BigFraction) | Two heap BigInteger references | GCD-dominated (logarithmic) | Medium, heap | Analytical engines, geometry, anywhere decimals must not drift |
Arbitrary decimals (BigDecimal) | Unscaled digit array + scale | Varies with MathContext precision | Grows with precision | Financial transactions, compliance, physics at chosen precision |
| The full tower | Dynamic polymorphic dispatch | Depends on promotion path | Highest (boxing + dispatch) | Domains demanding absolute safety; mathematical modelling; notebooks |
4. Engineering a Performant Tower (Capstone Challenges)
- Minimise allocations. Records are immutable, so every
addallocates a fresh object — in a tight loop, that is Garbage Collector pressure (see the JVM section of Advanced Java). Where could operations stay unboxed? - Primitive fast paths. Design the tower to remain inside raw
longarithmetic as long as possible, promoting only on the first exception — measure how much of a realistic workload never leaves Level 1. - Watch Project Valhalla. JVM value classes aim to give objects like these the memory layout of primitives — potentially the tower's safety at close to hardware speed. Reading the JEPs and estimating the impact on your benchmark is a current engineering exercise.
A note on academic integrity and AI co-piloting: if you extend this tower with an AI assistant, remember the learning feedback loop. Do not ask the model to "write a Matrix level". Write the behavioural unit tests for matrix operations first, watch them fail on your own terminal build (CLI First), and use the model to analyse your type-dispatch design against them. You must remain the system architect.
References
Shinn, A., Cowan, J., & Gleckler, A. A. (Eds.) (2013). Revised⁷ Report on the Algorithmic Language Scheme. Section on numbers/the numeric tower. https://small.r7rs.org/ ↩