Example of simplifying and improving code
When I see program code that can be simplified or improved, I seize the opportunity to make those changes. I’m passionate about working with code that is small and easy to understand, and abhor code that is unnecessarily complicated, long, or overly clever.
I practice code simplification all the time – in my personal programming projects, school homework, occasional participation in open-source projects, and coding in the workplace. I come across bad code on a regular basis (especially at most jobs), so in this article I hope to illustrate an example of how I apply my skills to clean up poor code.
Contents
- Original program code
- Local changes
- For-each loop
- Unnecessary parentheses
- Unnecessary if+return
- Recommended parentheses
- Non-Javadoc comment
- Contextual changes
- Eliminating copies
- Merging functions
- Narrowing scope
- Reworking logic
- Inlining variable
- Semantic changes
- Revising
divisorSum()
- Revising the sum
- Revising
- Final program code
- Additional comments
Original program code
This is Rian J Stockbower’s solution to Project Euler problem #21 (used without permission; contacted with no response). File: Problem021-0.java
import java.util.ArrayList; import java.util.Iterator; public class Problem021 { private static int MSum; private static int NSum; private static int sumList(ArrayList<Integer> list) { int sum = 0; for (Iterator<Integer> iter = list.iterator(); iter.hasNext();) { sum += iter.next(); } return sum; } private static ArrayList<Integer> createList(int n) { // Creates a list of integers that evenly divide into n excluding n itself long root = Math.round(Math.sqrt(n)) + 1; ArrayList<Integer> test = new ArrayList<Integer>(); test.add(1); for (int i = 2; i <= root; i++) { if (n % i == 0) { test.add(i); // Add the divisor & its complement test.add(n/i); } } return test; } private static boolean isAmicable(int n) { /*** If n's divisors form an amicable set, return true ***/ // Create a list of n's proper divisors ArrayList<Integer> NList = new ArrayList<Integer>(createList(n)); // Sum n's proper divisors (NSum) NSum = sumList(NList); // Create list of NSum's proper divisors (MList) ArrayList<Integer> MList = new ArrayList<Integer>(createList(NSum)); // Sum m's proper divisors (MSum) MSum = sumList(MList); if ((MSum == n) && (MSum != NSum)) return true; return false; } public static void main(String[] args) { long begin = System.currentTimeMillis(); int sum = 0; for (int i = 0; i < 10000; i++) { if (isAmicable(i)) { sum += NSum; } } System.out.println(sum); long end = System.currentTimeMillis(); System.out.println(end-begin + "ms"); } }
Local changes
These small changes can be made by only considering a few lines of code at a time. They’re easy to understand and easy to verify for correctness.
For-each loop
If we’re using generics, then we’re using Java SE 5+, which means the for-each loop is also supported. We should use the enhanced for statement to eliminate the boring boilerplate code of using an explicit iterator. This makes the import statement for java.util.Iterator
no longer needed. Also, we take advantage of auto-unboxing to use the int
type rather than Integer
. Afterward, since the loop has very few tokens, I prefer to eliminate the braces too.
import java.util.Iterator; ... for (Iterator<Integer> iter = list.iterator(); iter.hasNext();) { sum += iter.next(); }
Revised:
for (int x : list) sum += x;
Unnecessary parentheses
We know that ==
and !=
have a higher precedence than &&
. Using equality and logical operators in the same expression is so common that it pays to learn this precedence by heart. Thus we should remove the unnecessary parentheses here.
if ((MSum == n) && (MSum != NSum))
Revised:
if (MSum == n && MSum != NSum)
Unnecessary if+return
Using an if-statement to return a Boolean true/false value is redundant. We can take the condition of the if-statement and return it directly instead.
if (MSum == n && MSum != NSum) return true; return false;
Revised:
return MSum == n && MSum != NSum;
Recommended parentheses
This expression is correct but brittle – for example, if we add a string in front (e.g. print("Time: " + end-begin + "ms")
), then it’ll fail to compile. So it’s a good idea to add parentheses here.
System.out.println(end-begin + "ms");
Revised:
System.out.println((end-begin) + "ms");
Non-Javadoc comment
Because this comment starts with /**
, it’s syntactically a Javadoc comment. But Javadoc comments are only applicable to classes, interfaces, enums, fields, methods, and constructors – not to local variables. This comment would appear to document the local variable NList
, but this isn’t supported and the content of the comment is unrelated to NList
anyhow. Also, some text editors show Javadoc comments differently from regular comments, so this is misleading. Let’s remove the extra stars in the comment.
/*** If n's divisors form an amicable set, return true ***/ // Create a list of n's proper divisors ArrayList<Integer> NList = new ArrayList<Integer>(createList(n));
Revised:
/* If n's divisors form an amicable set, return true */ // Create a list of n's proper divisors ArrayList<Integer> NList = new ArrayList<Integer>(createList(n));
After local changes
After performing the five changes above, plus deleting {a pair of braces, a few trailing tabs, and blank lines}, we get Problem021-1.java.
Contextual changes
These changes take data flow and global usage into consideration. They do not change the values that are computed by the program.
Eliminating copies
In isAmicable()
, the return value of createList()
is fed into the constructor for a new ArrayList
. This extra copy is unnecessary because the (actual) return type of createList()
is already ArrayList
, and the data is already a unique (non-shared) copy.
ArrayList<Integer> NList = new ArrayList<Integer>(createList(n)); ... ArrayList<Integer> MList = new ArrayList<Integer>(createList(NSum));
Revised:
ArrayList<Integer> NList = createList(n); ... ArrayList<Integer> MList = createList(NSum);
Merging functions
As we see in isAmicable()
, the function sumList()
is always called with the result of createList()
. Also, the result of createList()
is never used in any other way. Since createList()
and sumList()
aren’t public API methods that external code might call, we can merge the functionality of sumList()
into createList()
, then rename the function more appropriately to divisorSum()
.
import java.util.ArrayList; ... private static int sumList(ArrayList<Integer> list) { int sum = 0; for (int x : list) sum += x; return sum; } private static ArrayList<Integer> createList(int n) { // Creates a list of integers that evenly divide into n excluding n itself long root = Math.round(Math.sqrt(n)) + 1; ArrayList<Integer> test = new ArrayList<Integer>(); test.add(1); for (int i = 2; i <= root; i++) { if (n % i == 0) { test.add(i); // Add the divisor & its complement test.add(n/i); } } return test; } private static boolean isAmicable(int n) { /* If n's divisors form an amicable set, return true */ // Create a list of n's proper divisors ArrayList<Integer> NList = createList(n); // Sum n's proper divisors (NSum) NSum = sumList(NList); // Create list of NSum's proper divisors (MList) ArrayList<Integer> MList = createList(NSum); // Sum m's proper divisors (MSum) MSum = sumList(MList); return MSum == n && MSum != NSum; }
Revised:
private static int divisorSum(int n) { // Finds the sum of integers that evenly divide into n excluding n itself long root = Math.round(Math.sqrt(n)) + 1; int sum = 1; for (int i = 2; i <= root; i++) { if (n % i == 0) { sum += i; // Add the divisor & its complement sum += n/i; } } return sum; } private static boolean isAmicable(int n) { /* If n's divisors form an amicable set, return true */ // Sum n's proper divisors (NSum) NSum = divisorSum(n); // Sum m's proper divisors (MSum) MSum = divisorSum(NSum); return MSum == n && MSum != NSum; }
Narrowing scope
The variable MSum
is only used in isAmicable()
, and it’s written before it’s read. It only needs to be visible within isAmicable()
and doesn’t need to carry state from one call to the next. Thus we can narrow its scope (and lifetime) from the class level (static field) to the method level (local variable). This has the side effect of making isAmicable()
safely reentrant for potentially recursive or concurrent calls.
The variable NSum
is written and read in the same way within isAmicable()
, but it’s also read in main()
immediately after calling isAmicable()
. We can narrow its scope in the same way, but in main()
we need to compute the value of NSum
afresh.
private static int MSum; private static int NSum; ... private static boolean isAmicable(int n) { ... NSum = divisorSum(n); ... MSum = divisorSum(NSum); ... } ... public static void main(String[] args) { ... if (isAmicable(i)) sum += NSum;
Revised:
private static boolean isAmicable(int n) { ... int NSum = divisorSum(n); ... int MSum = divisorSum(NSum); ... } ... public static void main(String[] args) { ... if (isAmicable(i)) sum += divisorSum(i);
Reworking logic
In isAmicable()
, we effectively examine a sequence of three numbers (n
, NSum
, MSum
) – the function returns true
iff the first is equal to the third but the second is unequal to the third. We can change the “but” part to “the first is unequal to the second”, to yield some gains later on.
Proof that MSum == n && MSum != NSum
is logically equivalent to MSum == n && n != NSum
: In the case that MSum == n
, by substitution we have MSum != NSum
being logically equivalent to n != NSum
. Otherwise with MSum != n
, both expressions are identically false
.
return MSum == n && MSum != NSum;
Revised:
return MSum == n && n != NSum;
Inlining variable
Now that isAmicable()
only has one usage of MSum
, we can inline its declared value into the single usage. We reverse the two conditions because neither has a (real) side effect, and it’s more efficient to skip the expensive call to divisorSum()
if the simple integer comparison fails first.
Also, we rename NSum
to nSum
because Java’s style convention states that local variables start with a lowercase letter. Finally, we eliminate all the comments – the opening comment is implied by the function name, and the two other comments don’t describe the code well.
private static boolean isAmicable(int n) { /* If n's divisors form an amicable set, return true */ // Sum n's proper divisors (NSum) int NSum = divisorSum(n); // Sum m's proper divisors (MSum) int MSum = divisorSum(NSum); return MSum == n && n != NSum; }
Revised:
private static boolean isAmicable(int n) { int nSum = divisorSum(n); return n != nSum && divisorSum(nSum) == n; }
After contextual changes
After performing the five changes above, we get Problem021-2.java.
Semantic changes
These major changes involve an understanding of the problem domain, and often visibly change the behavior of the program or subfunctions.
Revising divisorSum()
This function is subtly incorrect in two ways. The for-loop should only search up to and including floor(√n
), but in the code it can go up to and including floor(√n
) + 2 due to the rounding and the +1. For example, for n
= 3, root
= 3, but we actually want root
= floor(√3) = 1. The wrong search range means that extra factors are considered, which does make the final sum incorrect in some cases (e.g. n
= 2, 3).
Secondly, if n
is a perfect square and i
= √n
, then i
= n
/i
, so they are not two distinct factors. We need to detect and handle this special case.
Other points: √n
≤ n
. Since n
is int
, we can fit √n
in int
too (no need for long
; the author probably did this because Math.round()
returns long
). By luck, the old incorrect divisorSum()
algorithm doesn’t seem to cause isAmicable()
to be wrong for any number, so it doesn’t affect the program’s final answer.
private static int divisorSum(int n) { // Finds the sum of integers that evenly divide into n excluding n itself long root = Math.round(Math.sqrt(n)) + 1; int sum = 1; for (int i = 2; i <= root; i++) { if (n % i == 0) { sum += i; // Add the divisor & its complement sum += n/i; } } return sum; }
Revised:
private static int divisorSum(int n) { // Finds the sum of integers that evenly divide into n excluding n itself int root = (int)Math.sqrt(n); int sum = 1; for (int i = 2; i <= root; i++) { if (n % i == 0) { sum += i; // Add the divisor & its complement if (i * i != n) sum += n/i; } } return sum; }
Revising the sum
From the problem statement, we know that amicable numbers come in pairs (a, b), where a ≠ b. The way the original program was written, if a is amicable then b is added to the sum. We should be more direct and add a to the sum instead. This eliminates a function call and is truer to the problem statement’s intent. Moreover, if a < 10000 but b ≥ 10000, then it matters which number is added to the sum. (By luck, this is not an issue. But if the problem’s upper bound were 5200, then this would cause a discrepancy.)
if (isAmicable(i)) sum += divisorSum(i);
Revised:
if (isAmicable(i)) sum += i;
Final program code
File: Problem021-3.java
public class Problem021 { private static int divisorSum(int n) { // Finds the sum of integers that evenly divide into n excluding n itself int root = (int)Math.sqrt(n); int sum = 1; for (int i = 2; i <= root; i++) { if (n % i == 0) { sum += i; // Add the divisor & its complement if (i * i != n) sum += n/i; } } return sum; } private static boolean isAmicable(int n) { int nSum = divisorSum(n); return n != nSum && divisorSum(nSum) == n; } public static void main(String[] args) { long begin = System.currentTimeMillis(); int sum = 0; for (int i = 0; i < 10000; i++) { if (isAmicable(i)) sum += i; } System.out.println(sum); long end = System.currentTimeMillis(); System.out.println((end-begin) + "ms"); } }
Additional comments
In this article, we took a piece of code and walked through the process of how I would simplify it, clean it up, and fix errors, one step at a time. The size of the program started at 81 lines of code and went down to 42 lines.
By comparison, here is my own Java solution for this same Project Euler problem #21 (I wrote this Java code a year before this article). As we can see, the structure is extremely similar – 3 major functions and an essentially identical isAmicable()
function – though my code uses a different brace style, a top-down arrangement of functions, and a slower non-square-root divisorSum()
for simplicity. When I write code I naturally simplify as I go along, so everything that is committed to version control is already simplified; almost none of my intermediate work is archived. So it’s normal for my code to uphold a high level of quality all the time.
It was hard to find a suitable piece of code for this article – it needed to be short, have many opportunities for improvement, and be non-confidential. I typically see enough improvement opportunities in code that’s about 300 to 1000 lines long, but just the scrolling would likely bore a reader to death. Most short programs I encounter don’t have enough improvement opportunities. Also, I have many examples of bad code from the workplace but not many from public sources, however workplace code is confidential. In the worst case I might have chosen to fabricate bad code by mashing up various real examples I actually worked with, but it would feel rather unnatural and contrived. So it was very fortunate that I ran into this piece of code, which more than met my requirements.
Here an example from another author, who took a verbose and sloppy piece of code and turned it into something concise and self-evident. He shows the original code and revised code, and justifies why the new logic is functionally equivalent to the old logic. Link: 8th Light: Fecophiles.
What do you think – does code simplicity matter? Is it helpful or is it a waste of time? Maybe the discussion of this topic will inspire you to aim for perfection in your code craft.