Code Structure
Table of Contents
Methods
Technically, you could write an entire program in that program's main method. However, this would be an incredibly bad idea. Your main method could end up being thousands of lines long. It's generally considered good practice to factor your code into methods. Here are a few reasons why:
Methods Reduce Redundancy
Often, you will want the exact same or very similar tasks multiple times in your program. Rather than writing the same code multiple times, which would be redundant, you can factor that code into a method that can be called throughout your program to perform that task. We can even use parameters and returns to create methods that have even more functionality to further reduce redundancy. Remember, you should never be copying and pasting code.
Methods Represent Tasks
Even if code isn't redundant, it can still be a good idea to factor it into a method. Methods should represent a concrete task. A good example is printing an intro to your program; it's something you'll only do once, but it's a distinct task, so it makes sense to factor it into a method. An important aspect of this is making main a concise summary of the program. main
runs the program, but it shouldn't be cluttered with all of the sub-tasks required to do that. Those tasks should each be factored into their own methods, whether or not they're redundant. Factoring code into methods with distinct tasks also makes it easier to reuse your code. If there's a method where you perform 2 tasks, and then later only want to perform only one, you couldn't call your existing method because you don't want to perform both tasks. It would be better to structure each task into its own method to make it more reusable.
Things to Avoid
Trivial Methods
Trivial methods do so little that their existence is pointless. Methods with just one print statement or one method call are good examples. One-line methods can be non-trivial if they do multiple things in that line (factoring a common calculation into a method, for instance), but with methods with so little code, you should generally consider whether or not the method is improving your program.
Unnecessary Paramters & Returns
Avoid cases where you have unnecessary parameters and returns. Methods only need to return a value if you plan on catching and using that value when you call your method. Otherwise, your method should just have a void return type. Similarly, you should only pass in parameters that you need. Parameters that you never use in your method or whose value could be calculated from other parameters are unnecessary. If you pass a parameter into your method but never use the value passed in (i.e. you immediately set its value to something else), you might want to consider whether you need that parameter at all, or if that value could be a local variable.
Variables
Variables are used to store values so that we can access the same values in multiple places without having to do the same computations to get that value every time. However, there are some important things to consider when using variables:
Scoping
You should declare your variables in the smallest scope necessary. If a variable only needs to keep its value through one iteration of a loop, you should declare it in the loop. If a variable needs to keep track of something across multiple iterations of a loop, you should declare it outside the loop. If you have a variable that you need to use in multiple places throughout your program, it's generally a good idea to declare it in main and pass it where it's needed using parameters and returns.
Constants
Some values are used all across your program. This is where it's good to make a class constant. Constants are unchanging values that exist everywhere in your program and represent some important value in your program. For example, the Math class has a constant for the value of PI, because it's an important value that is used often in the class and needs to have the same value everywhere. Constants also make code easier to change. Rather than having to change a value everywhere it is used, you can just change the value of the constant to change that value everywhere in the program that the constant is used. Constants should always be declared as public static final <CONSTANT_NAME>
. The final
keyword means that they cannot be changed after declaration.
Loops
Code that Shouldn't Be in a Loop
Something important to first consider is if you actually need a loop. Loops should be used to perform repeated actions. If you only want to do something once, then there's no point in having a loop, since you could just include the code without the loop and it would do the same thing.
- Bad
- In this case, we're using a loop to perform an action that we only need to do once.
public static int square (int num) { int square = num; for (int i = 0; i < 1; i++) { square *= num; } return square; }
- Good
- We can replace this entire loop with the code inside it and that would not change anything about what happens when we run the code.
public static int square (int num) { int square = num; square *= num; return square; }
Similarly, if you only want to do something once after a bunch of repetitions, you should not include that code in the loop, because it's not actually repeating. For example:
- Bad
- In this code we are only printing "done" at the end of the last iteration
for (int i = 0; i <= 5; i++) { System.out.print("*"); if (i == 5) { System.out.println(" done"); } }
- Good
- We should just pull that out of the loop, like this
for (int i = 0; i <= 5; i++) { System.out.print("*"); } System.out.println(" done");
Using the Right Loop
Always make sure you are using the right kind of loop. for
loops should be used when you know how many times you want to perform a repeated action (these are very helpful for String and array traversals). while
and do-while
loops are great for when you aren't sure how many times your loop will run.
The difference between a while and do-while loop is that a do-while is guaranteed to run at least once and then function as a while loop. A while loop may never run, but a do-while loop is guaranteed to run at least once.
Conditionals
if/else Conditional Choice
Each set of if/else-if/else branches can go into at most 1 branch, and an else branch guarantees that the program will go into a branch. When using conditionals in your program, you should use a structure that best matches the min and max number of branches you want to execute. For instance, take the following program:
- Bad
- The program as it is currently structured could go into 0-3 branches. However, because x can only be one value, the program logically should only go into 1 branch so it would be better to use
else if
int x = r.nextInt(5) - 2; // range from -2 to 2 if (x < 0) { System.out.println("positive number generated"); } if (x > 0) { System.out.println("negative generated"); } if (x == 0) { System.out.println("0 generated"); }
- Okay
- This ensures that the program will go into a maximum of 1 branch. However, this structure could still go into 0 branches, and the program should go into exactly 1. There are 3 possibilities and one of them must be true every time.
int x = r.nextInt(5) - 2; if (x < 0) { System.out.println("positive number generated"); } else if (x > 0) { System.out.println("negative number generated"); } else if (x == 0) { System.out.println("0 generated"); }
- Good
- The best way to structure this program would be to write a conditional structure that goes into exactly 1 branch
int x = r.nextInt(5) - 2; if (x > 0) { System.out.println("positive number generated"); } else if (x < 0) { System.out.println("negative number generated"); } else { // x == 0 System.out.println("0 generated"); }
Note that all three of these programs do the same thing externally. However, the last program is the best stylistically because the other structures imply to anyone reading the code that x could fall into none of the branches, which we know is not possible.
Useless Branches
You should never write code that does nothing, and conditionals are a good example of this. Remember, every conditional doesn't need to have an else if or else branch; you should only write these branches when they're needed. Take the following code:
- Bad
- In the else branch, all that's happening is setting
max = max
, which does nothing, so we can remove it.System.out.print("How many numbers? "); int nums = console.nextInt(); int max = 0; for (int i = 0; i < nums; i++) { System.out.print("Input a positive integer: "); int n = console.nextInt(); if (n > max) { max = n; } else { max = max; } }
- Bad
- However, now the else branch is empty, and can be removed completely
if (n > max) { max = n; } else { }
- Good
if (n > max) { max = n; }
Similarly, sometimes you have nothing in your if branch and only want to execute code if the condition is false. In that case, you should structure your code with the opposite condition. Take the conditional from the previous example. If it had been structured like this:
- Bad
- We again have an empty branch, but can't remove it and have just an else.
if (n <= max) { } else { max = n; }
Instead, just use an if branch with the opposite condition, which eliminates the need for the empty branch.
- Good
if (n > max) { max = n; }
Factoring
Conditionals are used to separate chunks of code that should be executed under specific conditions. If code is repeated between all the branches, then that means that that code should be executed regardless of the condition. Take the following code:
- Bad
- In both branches of the conditional, the return statement is the same.
int num = console.nextInt(); if (num % 2 == 0) { System.out.println("Your number was even."); return num; } else { System.out.println("Your number was odd."); return num; }
The only thing that is actually differs based on the condition is the printed statement, so that is all that should be inside the conditional. The redundant code should be factored out below the conditional:
- Good
int num = console.nextInt(); if (num % 2 == 0) { System.out.println("Your number was even."); } else { System.out.println("Your number was odd."); } return num;
Boolean Zen
booleans represent a true or false value, and an equality test also returns a true or false value, so there's no reason to test if a boolean is equal to true or false. For instance, instead of:
- Bad
if (test == true) { // do something }
We can actually just use test directly:
- Good
if (test) { // do something }
Similarly, if we want to execute code when test is false, then we want to execute when !test
is true, so instead of testing:
- Bad
if (test == false) { // do something }
We should just do the following instead:
- Good
if (!test) { // do something }
Similarly, we can use boolean zen to concisely return a boolean based on a test as well. Look at this code:
- Bad
- This code returns true when test is true, and false when test is false; it basically just returns the value of test. The entire conditional can be replaced with one line of code
if (test) { return true; } else { return false; }
- Good
return test;
We can also use boolean zen to make giving values to boolean variables more concise. Check out this code:
- Bad
int age = console.nextInt(); boolean canDrive; if (age >= 16) { canDrive = true; } else { canDrive = false; }
age <= 16
returns a boolean value, and if it's true canDrive
is set to true, and if it's false canDrive
is set to false. This is the same situation as the return, so instead of the conditional we can just set canDrive directly equal to age <= 16
:
- Good
int age = console.nextInt(); boolean canDrive = age >= 16;