Monkeys at Keyboards: The Javanomicon
© Michael James Heron
Topic: Java Programming
Level: 2
Version: delta

I am prepared to die, but there is no cause for which I am prepared to kill.
Mahatma Gandhi

3 - Hallmarks of Good Code

PreviousTable of ContentsNext
Forum


Chapter Objectives

By the end of this chapter, the reader will be able to:

  • Be able to optimise simple programs for maintainability.
  • Be able to optimise simple programs for efficiency.
  • Be able to optimise simple programs for reliability.
  • Define the terms robustness and correctness.


3.1

Introduction

Programming as a discipline offers a degree of granularity in terms of how code is structured internally. It's possible that a piece of code will do everything it's supposed to do, but still be 'bad code'. In this section of the book we're going to look at the hallmarks of good code and what exactly qualifies a program as being well coded.

This is a matter of some subjectivity - certain coding techniques are likely to appeal to developers on the basis of nothing more than personal preference. There is nothing wrong with this, provided that the code checks out on each of the hallmarks:

  • Efficiency
  • Robustness
  • Maintainability
  • Readability

We can often say that one method of coding is better than another method, but it is difficult to precisely quantify this. Provided any given approach checks out on these standards of quality then it doesn't really matter, beyond personal preference, how a piece of functionality is implemented. As you gain experience, you'll also gain an appreciation of what works, why it works, and why it's worth doing.

The unfortunate thing is that often these hallmarks are mutually exclusive. It is possible to write a piece of code that is supremely efficient, but not very readable. It is possible to write code that is extremely maintainable, but not very efficient. True software quality comes from a balance of these principles, except when real world constraints dictate otherwise. We'll discuss this point some more in a later chapter.

In the code we develop for this textbook, there are no over-riding functional requirements that favour the emphasis of one aspect over another, so quality is defined as code that blends each of the hallmarks to a satisfactory degree.

In the real world, it is often the case that the functional requirements of a piece of software over-ride one or more of the hallmarks. When writing a piece of software for a device with limited memory or processing power (like a mobile phone or a PDA), then efficiency will be an over-riding concern over the readability of the code. Likewise writing a framework for an application designed to grow as its deployment environment changes will favour maintainability over efficiency.

3.2

Efficiency

With modern computers, efficiency isn't as much of a fundamental requirement as it once was. With gigahertz processors being the baseline for systems, and hundreds of megabytes of memory being easily available, the old ethos of writing compact, super efficient code has largely died out in favour of the benefits that come from having a heavy framework that allows for powerful and configurable development.

Object orientation as a programming principle is an example of this - every object that is created in an application takes up memory and system resources, and these are often much greater than they functionally need to be. The flexibility that comes from object orientation is considered to be a suitable payoff for this loss of efficiency.

For the purposes of this textbook, we define efficiency as:

The code makes use of system resources in an optimised manner. The scope of variables is consistent with the requirements of the functionality. Loops are terminated at appropriate junctures and there is no unnecessary processing done on data.

Note that this definition does not require your code to be as fast as it could possibly be - there are often multiple ways of attempting a task and some of these are guaranteed to be faster than others. It is not required that you choose the fastest method of execution over any others, although an application should be considered inefficient if it has an unnecessary amount of processing required to meet the requirements when a simpler, more compact method is available.

For a simple example, let's look at a program that utilises a for loop designed to give one number to the power of another:


public class FindPower {
static int num1, num2, i;
static int answer;

public static void main (String args[]) {
num1 = 2;
num2 = 4;
returnPower();
System.out.println ("" + answer);
}


public static void returnPower() {
answer = num1;
for (i = 1; i < num2; i++) {
answer = answer * num1;
}

}

}

This class will work exactly as intended - as in, it meets the requirements of the application. However, it is inefficient. The variables have all been declared with a class wide scope, which means that they exist and take up space in memory as long as the class exists. Even once we've called the returnPower method, num1, num2, and i continue to exist.

Instead of this, we should write the class ensuring that variables have their scope restricted to the bare minimum they need to accomplish their task:


public class FindPower {

public static void main (String args[]) {
int num1, num2;
int answer;
num1 = 2;
num2 = 4;
answer = returnPower (num1, num2);
System.out.println ("" + answer);
}


public static int returnPower (int num1, int num2) {
int temp;
int i;
temp = num1;
for (i = 1; i < num2; i++) {
temp = temp * num1;
}

return temp;
}

}

This second class does exactly the same thing as the previous class, but does it slightly more efficiently - because the scope of variables is correct it means that they don't exist for longer than they are actually needed.

The first class meets the requirements of the application, but it does so inefficiently. It meets the requirement, but is still, to a degree, 'bad code'.

The second class meets the requirements of the application, and it does so as efficiently as it can be.

For a second example, let us consider a loop that prints out every even number between two values given by the user:


public static void printEvenNumbers (int start, int end) {
int i;
for (i = start; i < end; i++) {
if (i 2 == 0) {
System.out.println ("" + i);
}

}

}

This would seem to be an efficient way of meeting the requirements of the application. However, consider what is actually being done in the code:

for every number between the start and end points. Check to see if the number is even. If it is, print it to the screen

Considering what we're actually doing and how restricted the problem really is, we can write a slightly different method:


public static void printEvenNumbers (int start, int end) {
int i;
if (start % 2 == 1) {
start += 1;
}

for (i = start; i < end; i = i + 2) {
System.out.println ("" + i);
}

}

This is actually a more efficient way of doing this. For one thing, we only do the check for an odd number once, at the very start. If it's odd, we then add one to it so that it is even - from this point on, we know every second number will be even. Then in the loop, we move on two places every time around - so we only loop half as many times.

There is a method we can use to determine exactly how efficient one piece of code is in comparison to another - we simply take a note of the time at the start of execution and take it again at the end of the execution. Then we print out the end time minus the start time, and this will give us the execution time. Using the particular methods we are going to use, it will give us the difference in nanoseconds (doesn't that make you feel as if you're writing code for the Starship Enterprise?). We will write a method that we can use to evaluate how long a particular piece of code takes to run.

To write this method, we make use of a method called nanoTime in the System class


public static void timeMethod() {
long now = System.nanoTime();
long then;
int iterations;
long timeElapsed;
// Our method call goes in here.
then = System.nanoTime();
System.out.println ("Method took " + (then – now) / iterations + " nanoseconds.");
}

Most of the time, any given piece of code will be executed so quickly that it won't even register - so we put our code to time within a for loop that repeats the code a suitable number of times. This also has the effect of averaging out the effect of unforeseen system events:

for (int i = 0; i < 10000; i++) { 
// Our method call goes in here.
}

For example, we could write a class that incorporates this method to test our very first version of the printEvenNumbers method:


public class MethodTimer {

public static void timeMethod() {
long then;
int iterations = 10000;
long timeElapsed = 0;
long now = System.nanoTime();
for (int j = 0; j < iterations; j++) {
printEvenNumbers (0, 10000);
then = System.nanoTime();
timeElapsed = timeElapsed + (then - now);
now = then;
}

System.out.println ("Method took " + timeElapsed / iterations + " nanoseconds.");
}


public static void main (String args[]) {
timeMethod();
}


public static void printEvenNumbers (int start, int end) {
int i;
for (i = start; i < end; i++) {
if (i % 2 == 0) {
}

}

}

}

Note that we've taken out the System.out.println call - see how long the method takes to run if you leave it in!

The program itself will do much of the averaging out for us, but we may still wish to run it a number of times to see the level of variation between runs. The longer we leave it running, the more accurate the number we get will be, but after a certain point it won't change radically. Running this testing framework gives us the following output:

Method took 42856 nanoseconds.

Note too that if you run this on two different computers you will get two different answers - the figures we get here are only relative indications of how long code takes to execute.

Java tip

Efficiency numbers relate only to the system on which you are testing. However, the general trend indicated by the numbers (slower or faster) will be true on any (almost) system.


Next, we want to compare this against our second implementation, so we swap out the printEventNumbers method code - everything else remains exactly the same:


public class MethodTimer {

public static void timeMethod() {
long then;
int iterations = 10000;
long timeElapsed = 0;
long now = System.nanoTime();
for (int j = 0; j < iterations; j++) {
printEvenNumbers (0, 10000);
then = System.nanoTime();
timeElapsed = timeElapsed + (then - now);
now = then;
}

System.out.println ("Method took " + timeElapsed / iterations+ " nanoseconds.");
}


public static void main (String args[]) {
timeMethod();
}


public static void printEvenNumbers (int start, int end) {
int i;
if (start % 2 == 1) {
start += 1;
}

for (i = start; i < end; i = i + 2) {
}

}

}

This gives us the following output:

Method took 18240 nanoseconds.

Bear in mind here that the numbers are not a straightforward comparison - there is some overhead in making a method call that is factored into the execution time for each of the methods (mainly because it's too difficult to swap out comparative methods otherwise). We cannot simple assume that method one takes over three times as long to execute as method two - but what we can see is that method two is clearly the most efficienct of the two. We even have metrics to back that up!

Historical note:

Older versions of Java (those prior to Java 1.5) do not have the nanoTime() method, and so much use a much less accurate method in the Date class called getTime().


Java tip

Efficiency testing only makes sense in comparison to some firm benchmark. If you feel your code is inefficient in some area, generate some metrics and then test the effect of efficiency changes to find out if there is any improvement.


This is a very simple method, but we can see that careful construction of the appropriate constructs can have very real gains in efficiency. In this particular example, even when the method is executed 10000 times, the efficincy gain is not something that will particularly affect a normal user... but imagine if this was a method in a 3D game engine - particularly a method called many times per second.

We will return to the topic of optimisation later in the book - it is a complex subject that carries with it a number of caveats that must be considered before it is attempted. For now, it is sufficient that we have an understanding of the kind of impact coding decisions may have on efficiency of performance.

3.3

Robustness and Correctness

Robustness means that your code should deal gracefully with contingencies and not simply keel over and die when someone does something unexpected. Consider a simple division method:


public static int divide (int num1, int num2) {
return num1/num2;
}

This will work as intended, until someone passes in 0 as a second parameter. At this point, the method will throw an error and your application will terminate. You can solve this problem by including a check:


public static int divide (int num1, int num2) {
if (num1 == 0) {
System.out.println ("Cannot divide by zero.");
}

return num1/num2;
}

This demonstrates the basic principle of robustness - where possible, your code should deal with errors in a sensible way and ensure that an error somewhere in the code doesn't bring down the rest of the application.

This is a secondary concern at this point in the book - when we talk about exceptions in chapter 16 we will see a way for dealing with these kind of problems correctly. For your first few forays into Java, this is not a consideration, but once we discuss the idea of exceptions you will be expected to ensure the robustness of your code if you wish to improve the quality of your applications.

The idea of robustness once again introduces the idea of the distinction between good and bad code. More accurately, it introduces a distinction between correct code (which accomplishes the task for which it was written), and robust code (which handles unexpected input in a graceful way). A program can be correct and not robust, and also robust but not correct. The best programs, of course, are both.

Ensuring robustness at this stage is difficult, because we haven't covered the techniques we can use to identify problems in code... it's fairly easy to apply an ad hoc approach to identifying faults in code, but this isn't enough to ensure any kind of confidence in the robustness (or indeed, correctness) of a piece of code. There are formal tools for doing this, and we'll touch upon them in chapter 17 and 18, but for now we're pretty much incapable of finding all of the faults in our projects.

However, we shouldn't be discouraged by this - where we can fix faults, we should... the fact that we can't yet fix them all does not mean that we shouldn't fix any of them. Consider it to be your task as a developer to ascend to ever higher levels of correctness and robustness, until your program achieves the kind of of perfected ascension normally associated with departed Jedi Knights:

Levels of Correctness
Fig 3.1: Levels of Correctness

We will call the table above the 'Javanomicon Correctness Metric'. Most programs that compile will begin somewhat above level one for robustness - it's pretty rare that a program, even in its initial stages, will crash with all input. It's possible, of course... but such programs don't last in that stage for long. However, a program cannot be classed as level two until it can be proven (through formal testing) that it will accept all standard input.

All programs begin at level one for correctness - until there has been some formal testing, there is no way to determine (or at least, prove) the level of correctness of the program.

There is nothing to stop a suitably motivated developer achieving level five in both robustness and correctness - the highest level is not an unachievable ideal, it is something well within the reach of programmers. It just takes the will to get there.

Reminder

Programs should be both robust and correct. A program that is robust is not necessarily correct, and a program that is correct is not necessarily robust. Both of these must be tested.


3.4

Maintainability

Requirements change all the time when developing code - good developers keep this fact in mind when writing applications and make sure their code is ready to be modified should fundamental aspects of the requirements be altered.

At its most basic level, this means ensuring your code is modular - in other words, that your code is broken up into sensible methods (and classes, but we'll get to that later) and that you make use of all the expandable code structures provided for you, such as arrays (we'll talk about these in a later chapter).

If a requirement changes and it means that your code has to be rewritten from scratch, it's an indication that you probably need to make your code design more modular.

For example, consider an application designed to go through a list of ten numbers and print out the square of each:

System.out.println ("" + num1 * num1); 
System.out.println ("" + num2 * num2);
System.out.println ("" + num3 * num3);
System.out.println ("" + num4 * num4);
System.out.println ("" + num5 * num5);

If later on the requirements change so that instead of printing out the square of each number we must print out the cube, we must then change every line of code that does this calculation. We could have made this more modular, and thus more maintainable, by using a method:


public static int giveAnswer (int number) {
return number * number;
}

System.out.println ("" + giveAnswer (num1));
System.out.println ("" + giveAnswer (num2));
System.out.println ("" + giveAnswer (num3));
System.out.println ("" + giveAnswer (num4));
System.out.println ("" + giveAnswer (num5));

Now when our requirements change, we need only change the giveAnswer method:


public static int giveAnswer (int number) {
return number * number * number;
}

And voila, our code now meets the changed requirements.

Much like with the efficiency section, this is a very simple example of the idea - a proof of concept rather than a genuine argument as to why it is necessary to write modular code. The only way to really gain an appreciation for why maintainability is an important issue is to have someone fundamentally change your programming requirements on you at the last minute.

Such changes occur quite a lot when developing a real world application - people change their minds, and as the software they see starts to take shape, they begin to have new ideas and request extra or modified functionality. That's the nature of the beast - but provided you have anticipated this, you will find it much easier to cope with the hectic demands that software developing will place on your delicate foreheads.

3.5

Readability

One of the aspects of good code most overlooked is that of readability - this is a sin that most developers are guilty of, the author being no less sinful than others. The readability of your code has an effect on almost all aspects of development - it eases bug fixing, improves maintainability, and makes it easier for others to expand upon your code or aid in development. Making your code readable is a doubleplusgood thing to get into the habit of.

There are simple rules to follow when writing code - make sure you keep them in mind to improve the readability of your programs:

Use Meaningful Variable Names

The names you give your variables within your code should have some meaning as to what they actually do. For example, if you have an integer value that you are using to store the number of times a button has been pressed, then the name should reflect that:

int numberOfButtonPresses; 

Rather than for example:

int magicBunnies; 

When coding purely for one's own pleasure, it is easy to get into the habit of naming variables in an idiosyncratic way. For example, I myself often use the variable names bing, bong, bang, blue, and frog when naming local variables within a method - this is not helpful when others read the code - or indeed, when I read the code in later months!

For any work you do where others are going to have to read (and possibly assess) your code, you should ensure that your variable names have some relevance to what they are actually doing.

In addition to this, you should adhere to some kind of naming convention when giving your variables names. The naming convention used throughout Java as standard is that the first letter of a variable name is lower case, with upper case letters being used to separate out words. This is only a convention and not enforced by Java itself. It doesn't really matter what convention you use, as long as you are consistent with it:

Bad:

int myBunnies; 
int someone_else's_bunnies;

Better:

int my_bunnies; 
int someone_else's_bunnies;

Comment Your Code

Again, this is something that is widely preached and only occasionally practiced. You should comment your code effectively to improve the readability.

When writing comments, there are some thoughts to bear in mind. The main one is - who is going to be reading the comments? It's not your grandmother, your sister, or some guy you meet on a bus. It's going to be someone who has at least some passing familiarity with code, and as such you can assume that there is no need to comment lines of code that are fundamental parts of the language. For example:

int myBunnies; 

This is not a helpful comment since all it does is restate the line of code in English. That is not to say that there is no use in commenting variables:

int myBunnies; 

The difference lies in what the comments tell us. One simply restates the line of code, the other states the intention and the use.

Mainly though you need only comment those sections of code that have some complex or semi-complex functionality. You should state the intention of the code, not what it actually does - these may not actually be the same. For example:

Bad:

int i; 
/*
This for loop modifies a temporary variable by the counter.
*/

int temp = 10;
for (int i = 1; i < 10; i++) {
temp = temp + i;
}

Better:

/*
This for loop gives the power of temp to ten
*/

int temp = 10;
for (int i = 1; i < 10; i++) {
temp = temp + i;
}

The second comments are better because they indicate the intention of the code. With the first comments, it's not possible to tell simply from the loop that it is incorrect. With the second it's clear that the loop is not working correctly because it uses a + instead of a *.

Format your code consistently

This is a dangerous area when discussing readability of code -arguably nothing affects the readability of code more than the way it is laid out. There are often holy wars of formatting that break out amongst developers as to how a piece of code should be indented.

The truth is that it really doesn't matter, as long as you are consistent. The two main ways of laying out code are as follows:

Method 1:


public void myFunction() {
// Some Code
}

Method 2:


public void myFunction() {
// Some Code
}

Either of these methods are fine. The convention for both is that you indent one level for each level of braces - the only difference is where the opening brace goes:

Method 1:


public void myFunction() {
for (int i = 0; i < 10; i++) {
if (i == 5) {
System.out.println ("" + i);
}

}

}

Method 2:


public void myFunction() {
for (int i = 0; i < 10; i++) {
if (i == 5) {
System.out.println ("" + i);
}

}

}

As you will have noticed from the example code, Method 1 is the way the programs in this book are formatted. You may be more familiar with Method 2 from other programming text books, but it is important than you understand how to read code formatted using both methods. It is just unimportant with which of the methods you choose to format your own code.

Do not use Magic Numbers

Magic numbers are those numbers that appear throughout code, usually used as a mathematical modifier for some variable, but have no explanation given as to what they represent. For example, a bank that has an interest rate of 2.15% that is applied to all accounts annually may have a method such as:


public void addInterest (int accountValue) {
accountValue = (accountValue / 100) * 102.15;
}

This is an example of readability and maintainability - even without saying what the interest rate is it's pretty clear what this method is doing from the name of the method, the name of the variable, and the code itself.

But if you encounter a line of code somewhere in a method that looks like:

tmpVar = tmpVar / 7.2; 

There is no indication as to what 7.2 actually means and what it represents. Likewise we cannot infer its use from the name of the variable it is being applied to. 7.2 is a magic number.

It is very bad form to use magic numbers throughout your code. Instead you should create a meaningful variable name and assign it a particular value:

int interestRate = 102.15; 
int percent = 100;

public void addInterest (int accountValue) {
accountValue = (accountValue / percent) * interestRate;
}

This has the added value of increasing maintainability - if you use this variable throughout your code, you need only change the variable once to update all of your code with the correct values.

Keep It Simple, Stupid

There is a natural assumption when learning a programming language that the lebel of complexity of the code is a reflection on the intelligence of the coder... this is usually not the case. It can be fun and satisfying to write 'clever' code to solve a problem, but it has a big impact on the readability of your programs. There should never be a case when you write a piece of code that only you can understand, unless it's the only way implement piece of functionality.

Keep your code as simple as possible (but to paraphrase Albert Einstein, not one bit simpler). It makes it easier to read, easier for others to maintain, and doesn't make you look like someone with a compulsive need to impress.

Reminder

Readability is often overlooked in program design, but when you are working with other developers or in an environment in which others may be looking at your code, it is vital!


3.6

Elegance

All of these concepts are tied together in a general category called elegance. This is the most subjective of all these categories since the definition of elegance in code is a largely aesthetic exercise.

It is possible for code to meet each of the hallmarks above, but still be inelegant. There are several definitions of elegance, but the one we're going to use for the textbook concerns the flow of logic through a program.

One of the main sources of inelegance in coding is that of convoluted logic flows:

if (blah) { 
if (blue) {
}

else if (bong) {
}

while (bang) {
for (frogs) {
if (bong) {
}

else {
}

}

}

}

This is an example of a convoluted logic flow - thinking through this structure is a relatively difficult exercise for humans. One of the principles of elegance is that if you have to write a logic structure like this then there is probably a better way of doing it.

The idea of elegance isn't something that will be particularly important when learning the concepts in this textbook - it is something to aim for once you have understanding.

Often the elegance of a solution to a problem is tied up in the underlying representation of the data - at the moment we simply don't have the tools in our toolkit to effectively model complex relationships of data.

The elegance of a solution is the exactness and conciseness of its data representation and the ease with which it can be manipulated. For reasons of efficiency, readability and maintainability, it is sometimes necessary to sacrifice elegance for more pragmatic considerations.

The search for elegance in computation is one of the few areas in which programming is more like an art than a science - it's when the act of programming is no longer about solving a problem (as in, we just want an solution) but expressing an approach to the solution that has at its core an appreciation of the nature of both the problem area and the act of programming itself.

Elegance is not something that can be methodically implemented when designing a computer application - it's something that must be built into the application at the start, and it comes from thought, consideration and planning about what is the simplest and most effective way of designing an application.

It is impossible for a solution to be elegant if it does not meet the requirements of all the hallmarks above, but a solution that does meet all these hallmarks is not necessarily elegant. Elegant solutions are what we should be aiming for all the time as programmers - it's only appropriate that they are so hard to achieve.

3.7

Conclusion

We will have cause to return to this topic at the end of the book, since it is a very deep subject that is worth further exploration. For now, we're interested in these concepts only so far as they impact on quality of our intial forays into programming. We haven't really covered enough of Java to be able to really go into any depth about any of these topics. There are tips and strategies for optimising all of these aspects of a program that are just too complex for us to cover at the moment.

This book is not just about learning more tools relating to programming - it's about learning to program better. This is something that comes only with practise - this book can only nudge you in the right direction. It's up to you to make sure that you start walking.

3.8

Reader Exercises

Exercise One

Get together with a friend and exchange copies of programs that you have written for previous modules.

Analyse your friend's code for maintainability. What would happen if the requirements changed sometime in the future. Would it be easy to update the code?

Analyse your friend's code for robustness. Can you break the program using nothing more than the provided interface?

Analyse your friend's code for efficiency. Are there any obvious areas where code is being executed when it doesn't need to be?

Analyse your friend's code for readability. Can you understand what the code is doing, and what it is supposed to be doing? These may not be the same thing...

Exercise two

Analyse your own code. Go through all of the programs you wrote in first year and comment them properly.

Exercise three

Consider the following code snippet. Identify areas of bad practise:

String myName; 
// Declares a String variable called myName.
int my_age;
// Creates a variable to hold an age.
String stars = "";
if (my_age == 20) {
for (int i = 0; i < my_age; i++) {
stars = stars + "*";
}

System.out.println (stars);
}

System.out.println ("Happy twentieth birthday!");

Exercise four

Using only the standard Java syntax we have covered in the past three chapters, how would you improve the robustness of the following piece of code?


public int divideNumber (int num, int divideBy) {
return num / divideBy;
}

Further Reading

The following table details further reading on the topic in this chapter, and also any external resources that you may find useful.

ResourceDescription
Example Programs from this chapterThis is a zip file of all the programs shown in this chapter.
An essay on commentsHow should you write comments? Have a read of this and think about it.
How to write maintainable codeWant to know a little bit more about mixing colours? This is your page right here.

PreviousTable of ContentsNext

© 2004-2006 Michael James Heron