Readability and Style in Java

Readability and Style

At this point in the series, we’ve covered the majority of core Java syntax. There are a handful of structures we’ll look at in the future, but now seems like the perfect time to touch on readability. In this tutorial, we’ll dive into what it takes to produce code that can be understood by other people. Some of these notes are subjective, so there might be a little controversy. Regardless, let’s get into it!

Table of Contents

Core Concepts in Style and Readability

Coding is a team sport. Why do you think I use the term ‘we’ all the time? We have to get used to working on complex systems together. Which means we have to find ways to communicate our ideas better.

One of the best ways to do that is to improve the overall readability of our code. Readability is a complex issue that is often subjective. Before we get to my personal style guide, we need to lay down a couple best practices.

There are two main practices we can follow to keep ourselves out of trouble: consistency and clarity.

Consistency

Before we even get into the code, there are a couple universal rules that we should always make sure to follow when writing code. The first rule is to keep code consistent. Teams rely on consistency because it eliminates most of the struggle of trying to decipher someone else’s code.

Many times teams will put together a style guide to help make code look and feel consistent throughout the library. Living without a style guide is sort of like reading a paper that was obviously written by more than one person. The lack of flow makes it difficult to follow the text.

The same rules apply for code; a contrast in style can sometimes feel like reading another language.

Clarity

In tandem with consistency, the other major style principle is clarity—be clear in the intent. If a method is supposed to add two numbers together, make sure the method name accurately reflects that.

Likewise, make sure the method only adds two numbers together. In other words, methods should follow the single responsibility principle. While this rule is really a principle for classes, it applies to methods as well.

An Unclear Example

Using our addition example, let’s try implementing the solution in code:

public void addTwoNumbers(int a, int b) {
    result = a + b;
    System.out.println("The result is " + result);
    return result;
}

Notice how our method adds two numbers as expected. However, as a side effect, the method also prints the result to the user. What if the user doesn’t want the method to print? What if we had multiple similar methods like subtractTwoNumbers and divideTwoNumbers? The print statement would need to be copied throughout.

For the sake of clarity, we should probably refactor the method to only add two numbers. In other words, the caller will be responsible for printing out the result if they so desire.

Improving Clarity

Instead, let’s separate the addition and print statement into two methods:

public void addTwoNumbers(int a, int b) {
    result = a + b;
    return result;
}

public void printResults(results) {
    System.out.println("The result is " + results);
}

Now, if we add a couple more methods that simulate operators, we can easily print their results without pasting the same line in each method. Better still, users aren’t forced to print with their methods.

As a result, these addition and printing functionalities are now decoupled, and we could even write a wrapper method to recombine the functionalities later if needed.

While this is a lesson on readability, the selling point of forcing a single responsibility per method is testing. It’s hard to tell from the silly example above, but testing becomes difficult when methods have too many dependencies.

For instance, imagine instead of printing, we were actually pushing the results to a database. This is probably a larger design flaw, but you get the idea. It would be nearly impossible to write portable tests without mocking the database connection. Instead, we just pull out the business logic into its own method, so it’s easy to isolate and test.

Coding Styles

When it comes down to it, good style is subjective. Every team has a different culture which can drive different coding styles. As a consequence, we should take some of the following strategies with a grain of salt. In other words, the following section is my personal take on a coding style guide—feel free to give your input in the comments.

Java Naming Conventions

Among everything discussed in the coding styles section, naming conventions are probably the least debatable. Every language has a standardized set of naming conventions that we should all follow. The following is a nice breakdown of some of those standards for Java:

Constants must be named using all capitals letters with underscores to indicate spaces:

public static final int SIZE_OF_WINDOW = 100;

Classes must be named with the first letter of each word capitalized:

public class FourDimensionalMatrix { ... }

Methods must be named using the camelCase convention:

public void printAllWordsInDictionary() { ... }

Variables must be named using the camelCase convention:

String myName = "Jeremy";

For everything else, refer to some sort of style guide.

Comments

Comments are beneficial because they add an element of natural language to the code. Here we can explicitly declare the intent of a method or chunk of logic.

However, comments can be tricky. Ideally, code is self explanatory. We should strive to give variables, methods, and classes explicit names. When this isn’t possible, comments can play an important role.

For the purposes of this tutorial series, we should strive to use JavaDoc comments whenever possible. JavaDoc comments allow us to clearly document each and every class and method. The beauty of this is that we can compile the results into a web page that can be browsed just like a regular Java API.

Anything beyond JavaDoc should be limited. We should avoid adding too many comments as they can make code just as difficult to navigate. This is especially true when comments aren’t maintained. For instance, imagine we commented our first addTwoNumbers method to say something like the following:

/**
 * Adds two numbers then prints the result.
 *
 * @param a the first number
 * @param b the second number
 * @return the result of adding a and b
 */

If we refactored our code into two methods like before but forgot to edit this comment, users would start to report bugs in our API.

The next person to read over this comment would probably be smart enough to notice that the method no longer prints the result. Hopefully, they would just clean up the comment. However, there’s a chance that they would incorporate the print again. After all, the intent of the method seems to be add two numbers and print the result.

As we can see, comments can sometimes be dangerous. That said, that shouldn’t stop us from using them. If anything, it should just push us to write better code. After all, code is read far more often than it is written.

Braces

At this point, we’re going to step into some dangerous territory as everyone seems to have a different opinion when it comes to braces.

Braces are those curly bracket symbols that we use to denote a code block. Some languages like Python have phased them out completely to force white space sensitivity. That’s because braces are just another form of boilerplate code. As a result, many developers try to avoid them like the plague.

Life Without Braces

Back in the JUnit Testing tutorial, we touched on a section of code that may have been written by one of these developers. In this example, the developer wrote an if statement without braces.

Of course, the problem was that the code appeared totally fine at first glance. In fact, Python developers may have never even noticed the issue. Unfortunately, the problem was that if statements without braces only execute code up until the next semicolon. For example:

if (x < 5)
    System.out.println("How Now");
    System.out.println("Brown Cow");

In the case that x is less than 5, both strings print as expected. Otherwise, "Brown Cow" prints almost mysteriously.

In my opinion, that’s reason enough to be consistent and enclose all code blocks in braces. It doesn’t matter if the code block is a quick one liner. The braces help us to be explicit in the intent of the code.

Luckily, most IDEs will do this for us, so it shouldn’t even be an issue. There are arguments that the braces can waste valuable vertical screen space, but it just doesn’t feel like that holds. In the event that a method is so large it doesn’t fit on the screen, it may be time to do some refactoring.

Brace Placement

After we’ve decided to always use braces, we should probably figure out where to put them. The actual location of our braces is a bit more contentious. In reality, it’s personal preference. However, we should probably pick a style and stick to it if we’re in a team. Otherwise, we’re free to experiment.

The two main styles are as follows:

public void max(int a, int b) {
  if (a > b) {
    return a;
  } else {
    return b;
  }
}
public void max(int a, int b)
{
  if (a > b)
  {
    return a;
  }
  else
  {
    return b;
  }
}

Personally, I prefer the first option as you have probably noticed in these tutorials. I like how compact the code appears, and I believe it is the accepted style for Java in most places—Google and Sun in particular.

However, the second option has quite a bit more merit. First, the bracing is far more consistent. In other words, braces only appear on two columns instead of five. Likewise, it is easier to comment. Comments can be added above line 3 and line 7.

Unfortunately, option one is not nearly as explicit, so for the sake of clarity and consistency it makes sense to follow option two. In fact, I believe option two is the preferred syntax in C#.

Break, Continue, and Return

As we’ve discussed, clarity is key. However, sometimes convention doesn’t always match up with clarity.

For instance, loops terminate when their condition is no longer satisfied. Java provides additional control over loops via the breakcontinue, and return statements. We specifically avoided the first two keywords because they’re very easy to misuse. That said, they have a place when it comes to improving the intent of a loop.

An Early Exit Example

For the sake of argument, a for each loop—a special kind of for loop—doesn’t allow us to specify a terminating condition. Instead, it terminates once it has iterated over its set.

Perhaps we want to write a find function to search for a character in a string. When using a for each loop, we’re forced to iterator over the entire string even if we find the the letter we need. As a potential solution, we can break out of the loop when we find our letter. Alternatively, we could return immediately:

public static boolean findChar(char toFind, char[] charList) {
    for (char test : charList) {
        if (test == toFind) {
            return true;
        }
    }
    return false;
}

In this example, we can pretend that charList is a string. With this special for each loop, we can look at every char in the string and determine if it is the char we want. As soon as we find it, we return true. Otherwise, the loop terminates, and we return false.

An Alternative Example

Some may argue that this is a poorly crafted method because we have two return statements. Personally, I find this much better in terms of readability as it doesn’t contain any extraneous local variables to store booleans. Also, it saves time by returning no later than we have to.

The alternative might look like the following:

public static boolean findChar(char toFind, String myString) {
    boolean found = false;
    char test = 0;
    for (int i = 0; !found && i < myString.length(); i++) {
        test = myString.charAt(i);
        if (test == toFind) {
            found = true;
        }
    }
    return found;
}

In this case, the code complexity has gone up significantly. We now have to maintain two local variables as well as evaluate a compound condition just to match the behavior of the first implementation. Granted, this time I used a string rather than an array. In my opinion, we should probably stick with the first implementation.

Personally, I haven’t found a good use for continue. However, it probably has its place much like break. We should still focus on creating carefully crafted loop conditions in most cases, but we shouldn’t force these conditions in situations where a break or return might make the intent more explicit.

Do Style and Readability Really Matter?

Alright, we’ve poked at a few different areas of style and we’ve learned a couple important stylistic principles. However, should we really care how our code looks? After all, it doesn’t make a difference if the code fails testing.

Ultimately, we should care. A consistent and clear coding style is critical to working in a team. It’s even important when we’re working alone. Sometimes we’ll need to dig through our old libraries, and we’ll realize we have no idea what we wrote. Sticking to a style helps us get back up to speed, so we can start making changes that matter.

That said, don’t take my word for it. Take a look at some of the following articles:

Make sure to take a peek at the comments when you’re finished reading these articles. There’s always a nice little debate surrounding the topic.

Now that we’ve dug into three different areas of style that are often debated, we should feel more confident when writing code. Ultimately, we can make the decision to use one style or another. However, we need to keep those two core principles in mind: consistency and clarity. The more we follow these principles, the easier it will be for others to work with our code.

As always, thanks for stopping by. If you have any questions or comments, feel free to use the comments below. If you’d keep up with the latest articles on The Renegade Coder, don’t forget to subscribe!

Series Navigation← Loop Syntax and Design in JavaJava Basics Series Review →
Advertisements

Leave a Comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.