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.
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.
ArrayList<String> words = new ArrayList<>();
words = generateWords();
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.
// assume data is an array of Objects
String result = "";
for (Object o : data) {
result += o + " ";
}
return result;
// assume data is an array of Objects
StringBuilder sb = new StringBuilder();
for (Object o : data) {
sb.append(o);
sb.append(" ");
}
return sb.toString();
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.
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.
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!");
}
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.
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.
Good bounds and loop conditions will already deal with certain edge case behavior. Avoid writing additional conditional logic that loop bounds already generalize.
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!");
}
}
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.
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 ...) {
...
}
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) {
...
}
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;
}
public boolean contains(int value) {
for (int i = 0; i < size; i++) {
if (elementData[i] == value) {
return true;
}
}
return false;
}
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.
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.
// 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();
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();
}
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.
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;
}
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;
}
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
public int compareTo(Person other) {
if (heightInInches < other.heightInInches) {
return -1;
} else if (eightInInches == other.heightInInches) {
return 0;
} else {
return 1;
}
}
public int compareTo(Person other) {
return heightInInches - other.heightInInches;
}