8. Efficiency and Redundancy

A lot of what we define to be important for efficiency boils down to “don’t do anything unnecessary”. Knowing what’s necessary and what isn’t can be difficult, but following the suggestions of the assignment specifications for certain implementation details will lead you in the right direction. Unfortunately, there’s no hard and fast rule for avoiding every inefficiency. To avoid them, you will have to dedicate some time to reasoning about your code. In other words you may find it useful to perform algorithm analysis as taught the first week of class on the code you write.

As a general rule, reduce or remove complicated expressions and logic where reasonably possible.

8.1 Creating new objects

Avoid making objects that you don’t need. This can be tricky to spot, but in particular you should review your code statements that instantiate new objects or call other methods that do so. Making sure that every object you instantiate is necessary (no other available methods can provide the same or reasonable behavior) and logical will give some sense of security here.

The first example makes a new ArrayList<>(); just to discard it in the next line. When we set words = generateWords() in the second line, the previous value of words is lost (unless it’s saved elsewhere). The second example doesn’t throw away any newly made objects.

Bad
ArrayList<String> words = new ArrayList<>();
words = generateWords();
Good
ArrayList<String> words = generateWords();

Likewise, avoid building up a String inside a loop. Strings are immutable. A program that builds up a String inside of loop using String concatenation, creates a new String with each concatenation. Also, building up a String in a loop can be inefficient. Prefer using a StringBuilder when building up a String via a loop.

Bad
// assume data is an array of Objects
String result = "";
for (Object o : data) {
    result += o + " ";
}
return result;
Good
// assume data is an array of Objects
StringBuilder sb = new StringBuilder();
for (Object o : data) {
    sb.append(o);
    sb.append(" ");
}
return sb.toString();

8.2 Recomputing values

Avoid recomputing complex expressions and method calls. If you have to use the value of a method call or a complicated expression more than once, you should store it in a well-named variable. Using the value stored in this variable will give you instant access instead of recomputing the value by evaluating the method call or expression again. This will also clear up the clutter of your code by replacing generic symbols and expressions with a name to describe some context.

There are some methods that are fast enough that storing the value in a variable for the future doesn’t save us anything. For example, the size() method for any data structure we use in this course will have the same instant access whether we store it or call the method. In such a case, efficiency is not an issue we have to worry about.

8.3 Factoring

If you have lines of repeated or very similar code that need to be executed in different places, group the code of the task into a private helper method. Putting the code into one place instead of several will make making changes easier, as well as make our code more readable.

Code inside if/else structure should represent different cases where the code is inherently different. This means duplicate lines of code or logic in an if/else structure should be factored out to come before or after, so that they are only written to happen once, unconditionally.

Bad
Note that there are repeated lines of logic that actually always happen, instead of conditionally like how our structure is set up. We can factor these out to simplify and clean our code.
if (x % 2 == 0) {
    System.out.println("Hello!");
    System.out.println("I love even numbers too.");
    System.out.println("See you later!");
} else {
    System.out.println("Hello!");
    System.out.println("I don't like even numbers either.");
    System.out.println("See you later!");
}
Good
System.out.println("Hello!");
if (x % 2 == 0) {
    System.out.println("I love even numbers too.");
}  else {
    System.out.println("I don't like even numbers either.");
}
System.out.println("See you later!");

Review any if/else structures you write and make sure there are no duplicate lines of code or logic at the beginning or the end.

8.4 Redundant conditional logic

This section details issues with writing unnecessary if/else logic that can be omitted. We will refer to any control flow like if, else, etc. as conditional logic.

8.4.1 Loops

Good bounds and loop conditions will already deal with certain edge case behavior. Avoid writing additional conditional logic that loop bounds already generalize.

Bad
The if here is redundant. The loop won’t produce any output (or even run) if n were 0 or negative in the first place, so we should remove the check.
if (n > 0) {
    for (int i = 0; i < n; i++) {
        System.out.println("I love loops <3!");
    }
}

8.4.2 Already known values

By using control flow (if/else/return) properly, you can guarantee some implicit facts about values and state without explicitly checking for them. The examples listed below add in redundant conditional logic that should be removed.

Bad
Because someTest was tested for alone in the first if, future branches of the if/else structure know someTest must be false, so the check for !someTest is redundant and can be omitted.
if (someTest) {
    ...
} else if (!someTest ...) {
    ...
}
Bad
At the second if statement, we know that someTest must have been false, so it’s not necessary to check its value again.
if (someTest) {
    throw exception/return
}
if (!someTest) {
    ...
}

8.5 Reuse existing code and methods

Avoid re-implementing the functionality of already existing methods including commonly used methods from the Java standard library, by just calling those existing methods.

In the following example, the two methods are almost identical, except indexOf is more powerful/flexible than contains. So, contains can actually just use a call to indexOf to reuse already written behavior.

public int indexOf(int value) {
    for (int i = 0; i < size; i++) {
        if (elementData[i] == value) {
            return i;
        }
    }
    return -1;
}
Bad
public boolean contains(int value) {
    for (int i = 0; i < size; i++) {
        if (elementData[i] == value) {
            return true;
        }
    }
    return false;
}
Good
public boolean contains(int value) {
    return indexOf(value) >= 0;
}

The classes you will use in this course (String, ArrayList, TreeMap, etc.) will provide many useful methods and you should aim to take advantage of them by using them. Rewriting existing behavior is less flexible, redundant, and by writing more lines of code you raise the risk of encountering more bugs.

This includes repeated code in the methods you write. If you have the exact same block of code, three or more lines long, repeated in a method or multiple methods, factor it out into a separate method. This includes constructors with repeated code.

8.6 Prefer code that can scale as the problem changes or grows.

Avoid re-implementing the functionality of already existing methods including commonly used methods from the Java standard library, by just calling those existing methods.

In the following example, the two methods are almost identical, except indexOf is more powerful/flexible than contains. So, contains can actually just use a call to indexOf to reuse already written behavior.

Example: Assume we want to add up the lengths of four Strings in an array of Strings given an index.

Bad
// data is an array of Strings.
// We know no elements of data are null
// We know index is inbounds and that the next three indices are also in bounds.
int sum = data[index].length() + data[index + 1].length() + data[index + 2].length + data[index + 3].length();
Good
int sum = 0;
final int LAST_INDEX = index + 3; // 3 can be changed to scale the problem.
for (int i = index; i <= LAST_INDEX; i++) {
    sum += data[i].length();
}     

8.7 Stop computing when the answer is known.

When looking for an answer or solution, stop computing when that answer is known.

Example: Determine if an array of ints contains a target value a given number of times or more.

Bad
public static boolean containsAtLeast(int[] data, int tgt, int minNum) {
    // Always looks at every element in the array.
    int count = 0;
    for (int element : data) {
        if (element == tgt) {
            count++;
        }
    }
    return count >= minNum;
}
Good
public static boolean containsAtLeast(int[] data, int tgt, int minNum) {
    // If we find the minimum number required, stop looking. We have our answer.
    int count = 0;
    for (int element : data) {
        if (element == tgt) {
            count++;
            if (count == minNum) {
                return true;
            }
        }
    }
    // Did not find minNum occurrences of tgt.
    return false; 
}

8.8 Implement compareTo and compare simply if possible

When implementing the Comparable interface we must implement the method compareTo. compareTo returns an int. The return value shall be < 0 if the calling object (this) is "less than" the explicit parameter, = 0 if the objects are "equal", and an int > 0 if the calling object is greater than the explicit parameter. While some implementations of compareTo return -1, 0, or 1 this is not required and can lead to unnecessarily verbose code.

Example: We have Person class. Each person has a height in inches. The Person class implements the Comparable interface and we want to order Person objects based on the field that represents the persons height.

public class Person implements Comparable<Person> {

    private int heightInInches;

    // other fields not shown

Bad
public int compareTo(Person other) {
    if (heightInInches < other.heightInInches) {
        return -1;
    } else if (eightInInches == other.heightInInches) {
        return 0;
    } else {
        return 1;
    }
}
Good
public int compareTo(Person other) {
    return heightInInches - other.heightInInches;
}