Objects are better than primitives, often but not always
At one extreme of programming paradigms there is an over-reliance on objects for every damn little thing, even when it would be much easier to just pass a primitive. Also wrapping primitives when there’s no good reason to do so (though sometimes that’s the result of carelessness rather than extremism).
And at the other extreme we see an over-avoidance of objects, dumping all the program logic into a single essentially static main class, defining no other objects and using only the bare minimum of standard library objects needed to get the job done.
The former is part of what gives Java a reputation for being bloated. You need one object to do one little task and next thing you know you’re importing ten different packages.
And the latter, sometimes termed “primitive obsession,” can give a Java program an almost procedural flavor. It could even look like an example for an early chapter in a programming course, before objects are introduced.
Of course you and I know better, and generally opt for the middle road. But I am not afraid to admit that once in a while I need a nudge away from either extreme.
Here is a concrete example from an actual program I’m working on: an Algebraic Integer Calculator (source and tests available on GitHub). There is some simple though unfamiliar math here, but if you have a solid grasp of basic high school algebra, you should be able to follow along without any problem.
This project grew out of my program to draw diagrams of prime numbers in imaginary quadratic integer rings. Diagrams like this one:
This is drawn by the
RingWindowDisplay class, which does use a single
ImaginaryQuadraticRing object to hold some basic information about the particular number domain, and one or two
ImaginaryQuadraticInteger to loop through the numbers in a given view.
But a lot of the calculations are done with integer primitives, a few with floating point primitives. Most of these primitives are local variables, so I guess that’s fine.
The capability to add and multiply arbitrary imaginary quadratic integers is not a capability that
RingWindowDisplay needs at this point, but
ImaginaryQuadraticInteger does provide functions for this purpose.
Barring overflows, you can add and multiply any two
ImaginaryQuadraticInteger objects (subtraction is very easily conceptualized as addition in reverse).
There are more restrictions on dividing one
ImaginaryQuadraticInteger object by another. For one thing, the divisor must not be 0. And for another, the dividend should be divisible by the divisor.
If it’s not,
ImaginaryQuadraticInteger.divides() will throw
NotDivisibleException. Throwing an exception for something that happens all the time in math might seem like overkill. But this will still turn out to be an example of “primitive obsession.”
I have debated with myself whether the
NotDivisibleException is appropriate or not, and I have to come to the conclusion that it is because this exception can arise in different situations where there may be different ways to recover from non-divisibility.
In some cases, you might just want an algebraic number rounded “down” to an algebraic integer. In other cases, you might want to know all the algebraic integers that immediately surround an algebraic number in the given domain.
Okay, so how do you construct a
NotDivisibleException to throw? In the older program, that exception had to deal with rounding numbers like 7/3 + (4√−5)/3.
Those would be passed as a bunch of primitives. Something like 7, 4, 3, −5 in this example. In that order, if I recall correctly.
NotDivisibleException would reconstruct the algebraic number 7/3 + (4√−5)/3 and figure out how to round it to an algebraic integer.
In the Algebraic Integer Calculator project,
NotDivisibleException will also need to be able to deal with numbers like 1/10 + (3∛2)/10 + 29(∛4)/10 and 1/7 + ζ₈/7 + i/7 + (ζ₈)³/7.
I assume the denominators will always be the same, I could be wrong about that. But I am far more certain that there should be as many numerators as the pertinent algebraic degree indicates, no more, no less.
Zeroes may be used as placeholders if needed, as in for example, to express 1/7 + (ζ₈)³/7 as 1/7 + 0/7 + 0/7 + (ζ₈)³/7. The exact order should probably go according to the power basis.
Whatever order is chosen, it should always be the same, so that
NotDivisibleException can correctly locate the algebraic number and round it to the correct algebraic integer.
So then this new version of
NotDivisibleException should be passed two arrays of integer primitives, one for the numerators and one for the denominators, right?
And that’s what I did, even though I was aware of all the problems that opened up. What if someone tries to construct a
NotDivisibleException with an array of three numerators and five denominators?
Or what if the two arrays do match in length and they’re of the right length, but the array of denominators includes one or more 0s? In either eventuality, the
NotDivisibleException constructor would throw an
That seems like a terrible, rotten thing to do, to force you to deal with another exception when you’re trying to throw an exception. Though it’s no excuse for programmers who write overly vague exception catches.
It wasn’t until last week that I finally realized I had a better answer practically under my nose. About a couple of months ago, I wrote a
Fraction class in Java inspired by some examples in Scala for the Impatient by Cay Horstmann.
So… what if instead of taking two arrays of integer primitives that might differ in length, the
NotDivisibleException constructor takes a single array of
That way the
NotDivisibleException constructor only needs to check that the array of fractions has the right length. There is no need to worry that there might be more numerators than denominators, nor vice-versa.
And there is no need to worry that any of the denominators could be 0. The
Fraction constructor should make sure that doesn’t happen. If that passes the relevant test in
FractionTest, we don’t need to worry about it in
The first step to implement this change was to bring
FractionTest into the Algebraic Integer Calculator project. Next, I rewrote
NotDivisibleException to use
And then I gave NetBeans one or two minutes to do a background scan of the project so that it would flag what other files needed to be edited to account for the change.
Obviously the first of those would be
NotDivisibleExceptionTest… uh, oh, I may have missed an opportunity for test-driven development here. On the bright side, the change simplifies
Fraction in the
fractions package, so the affected files need to
import fractions.Fraction. So now there’s a little more overhead to take care of before you can throw a
NotDivisibleException, but this is a rather small price to pay for not having to worry about misplacing numbers in an array of integer primitives.
In this particular instance, using an array of objects has turned out to be much better than using two arrays of primitives.
Of course here it is still possible to go to the other extreme. To construct a new
Fraction, we could pass it two
Long objects instead of two
Or worse, we could rewrite the Fraction constructor so that it takes one
Pair<Long, Long>, and rewrite the
NotDivisibleException constructor so that it takes an
And worse still, we could implement our own
ArrayList (although perhaps
javafx.util.Pair might not quite be the 2-tuple I thought it was, so implementing our own
Pair might not be such a bad idea after all).
That could be considered over-engineering, since there doesn’t seem to be anything in the program requirements to indicate that we need to put in so much effort to avoid primitives.
A more immediate concern for me is that in making the improvement described here I could cause another part of the program that was previously working correctly to now malfunction.
Since I have tests for all the source files that were affected by the change, it suffices to run the tests to pinpoint if there’s anything that needs to be fixed.
The only tests that failed were tests that were failing before the change. I shouldn’t let failing tests pile up like that, but that’s a discussion for another time.
If there had been failing tests as a consequence of the change, they would have been very easy to fix. Certainly much easier to fix than problems in a program filled with global primitives whose purpose is not always clear.
Sometimes an object isn’t the best solution to a particular problem. But at least when it comes to validating data passed from one part of the program to another, it’s usually better to pass a well-designed object than a bunch of “loose” primitives.