Introduction

This is a style guide for CSE 123 and contains a list of guidelines we expect you to follow when writing code. This document covers the entire quarter, so it is not meant to be something that you memorize and understand entirely through one read-through. Rather, it is a reference guide that you will be able to use to look up rules and examples. Using this in combination with the the spec is a great way to be careful with your internal code details.

Note that this guide is not 100% comprehensive. It cannot replace feedback your TAs give or specific instructions from the spec. This guide lists the general rules to follow, but your TAs will be able to give you specific feedback on the code you write and how they fit into issues of style.

Motivation

Here is a summary of why code quality matters:

  • Programming is a collaborative activity, so it is important to know how to write code easily understandable by other humans.
  • Many programmers will distrust and judge you if you routinely churn out unreadable and ugly code.
  • Writing clean code reduces the chances that our code is buggy.
  • Pragmatically, many people in industry and in open source projects will conduct code reviews on your code and will and block changes that do not match their internal standards. The way we grade assessments mimics this industry best practice.

Many people believe that when writing code, the only thing that matters is if your code is working or not. This is false - while it's important to get your code working, it is just as important to make sure your code is understandable to other people.

Programming is a highly collaborative activity - it's fairly common to have to work together with other people, sometimes even hundreds of them at a time! As a result, it's very important to write code that is easy to work with to allow others to integrate seamlessly with your work. Another benefit is that since clean code is easier to understand, you'll work much faster and introduce fewer bugs. While you will not work in groups on assignments in CSE 123 unless otherwise specified, it's still important to practice programming with good style - it's the core difference between whether a program is pleasant or impossible to work with.

In general, focusing on writing clean code forces you to acquire an eye for detail. Programming, and writing correct (externally and internally) programs requires careful attention, so it's better to acquire that habit sooner, rather then later. After all, code is now a part of everyday life - we use code to control cars, control medical equipment, moderate our infrastructure, perform legal analysis, hold private data, and so forth. It's possible you'll go on to help create the software that ends up becoming part of others' lives. If that's the case, we have a responsibility to write as clean and well-designed code as possible to minimize the chances of bugs and accidents that might sneak in due to carelessness. As a result, the sooner you can get into the habit of caring about detail and correctness, the better.

Note that there is not **just one way** to write code. A code quality guide outlines a common set of standards that a community of programmers would like everyone to use. Different communities may use completely different rules than one another, and that is totally fine! Having a common set of standards for code quality allows for those in the community to better edit and read that code. So just because there is something in our code quality guide doesn't make it the "one right way to write" something. But you will still need to follow our guidelines, just like if you were to go work at a tech company, you would have to follow theirs.

Source file structure

Import statements

Use wildcard import statements instead of specific imports. e.g. use statements like import java.util.*; over import java.util.ArrayList;

Note: There are some merits in importing classes individually, but for this course use wildcard import statements in your submissions. This is just one example of different groups might have different rules about what is "good code quality." In our context, we ask that you use wildcard imports since it is helpful for our context (learning and testing your code). Different groups might feel differently about this rule though, but what matters is being consistent with your peers.

Member ordering

  1. Place field declarations at the top of your class.
  2. The order of how you place methods is up to you, but it helps readability to maintain some kind of logical ordering. One common ordering is to include all constructors in the beginning, then all other public methods before private methods. Programmers also commonly choose to put related public/private pair methods together so that another viewer of the code can see the pair at the same time.

Formatting

The following section describes rules about how your code should be organized visually. It should be noted that the rules listed below aren't inarguably the best rules; there are plenty of reasonable alternatives out there. It is important, however, to adhere to the relevant style of the work environment. For other readers, code that follows the style guidelines is easier to understand than code that doesn't. Being able to anticipate conventional formatting of code eliminates time wasted on deciphering the meaning of any unfamiliar structure and formatting.

Indentation

Each time a curly brace is opened, increase the indent level. When the closing curly brace occurs and ends the code block, the indent should return to the previous indent level. The indent level applies to both code and comments throughout the block defined by the curly braces.

Choose 3 or 4 spaces to be your indent level and stay consistent.

Spacing

Java is not a whitespace sensitive language, but maintaining good spacing throughout your program helps your code be more readable for human eyes. Most Java programmers follow the below spacing conventions for their programs.

  • Class headers: public class SomeClass {
  • Method headers: public static void someMethod(int number1, int number2) {
  • Method calls: someMethod(4, 3);
  • Loops:
    • for (int i = 0; i < 4; i++) {
    • while (someTest) {
    • for (String s : list) {
  • Arrays: int[] someArray = new int[4];
  • Variable initializations: int someNumber = 4;
  • Expressions:
    • int x = 4 * (3 + 2) - 1;
    • boolean result = test1 && test2 || (x != 3);

The spacing around methods also matters. Remember to leave a blank line before each method comment for readability.

One statement per line

Each time you finish a statement with a semicolon, you should use a new line to begin the next statement.

Note: The exception to this rule is that the for loop header should have its 3 parts all on one line.

Line length

No line should exceed 100 characters in length.

Terminology Note: When code that might otherwise legally occupy a single line is divided into multiple lines, this activity is called line-wrapping. Use line-wrapping to break up long lines that would otherwise exceed 100 characters, as described below.

When line-wrapping, each line after the first (each continuation line) should be indented at least one level. Common choices for how much to indent include 2 levels of indentation or lining up elements from the previous line of the long line.

// It's good that this line is wrapped to maintain the 100 char line limit,
// but the continued line should be indented at least 1 indentation level.
public static void method1(String param1, String param2, String param3,
String param4, String param5) {
    ...
}
// This method header is properly indented and uses 2 levels of indentation
// for the broken up line.
public static void method1(String param1, String param2, String param3,
        String param4, String param5) {
    ...
}
// This method header is also properly indented, and lines up elements from
// the broken up line.
public static void method1(String param1, String param2, String param3,
                           String param4, String param5) {
    ...
}

Tip: Extracting some method call(s) into a local variable may solve the problem without the need to line-wrap.

Exceptions

Exception code should come at the top of the method. Everything related to checking for and throwing the exception (computing a value to be used in the condition, the if statement itself) counts as exception code. Any code unrelated to the exception conditions / throwing should be placed lower than exception code. This is both an attempt to fail early to avoid unnecessary computation and a readability rule.

If an exception check cannot be checked at the beginning of the method (e.g. requires significant computation / additional statements), check for it when it can be more easily detected as the method continues.

// This example doesn't check/throw for the exception at the very beginning of the method.
public static int countVowels(String name) {
    int total = 0;
    if (name.length() <= 1) {
        throw new IllegalArgumentException();
    }
    ...
}


Do not attach else statements onto exception checks to connect regular behavior code.
Following an exception check, non-exception code should not be placed in an attached else. This is another readability rule (mostly done by convention. Another reason: why indent all of the method’s regular behavior code a whole level if the structure is still clear without indenting?) as well as a logic rule. Exception code is fundamentally different than regular method behavior code, and shouldn’t be connected to regular behavior with one long if/else structure - doing so almost implies the if/else branches are on the same level of importance or that they are related.

// This example attaches an 'else' to an exception check.
// This leads to indenting all the normal behavior code,
// which makes it a little bit harder to read.
public static int countVowels(String name) {
    if (name.length() <= 1) {
        throw new IllegalArgumentException();
    } else {
        int total = 0;
        ...
    }
}
// This one is perfect! No else attached with regular behavior
// and the check for exception is at the top of the method.
public static int countVowels(String name) {
    if (name.length() <= 1) {
        throw new IllegalArgumentException();
    }
    int total = 0;
    ...
}

Note: else ifs that check for more exceptions are fine. Just don't use else to have a branch for the method's normal behavior code. For example the following code structure would be fine.

    if (exceptionCheck) { 
     throw exception1
 } else if (exceptionCheck2) {
     throw exception2
 }
 // non-exception behavior code here

[OPTIONAL] Curly braces

The rules mentioned below document the style that you will likely see your TAs and lecturers use, but is optional if you want to use a reasonable (readable) alternative instead. The most important thing is consistency.

  • No line break before the opening brace.
  • Line break after the opening brace.
  • Line break before the closing brace.
  • Line break after the closing brace, only if that brace terminates a statement or terminates the body of a method, constructor, or class. For example, there is no line break after the brace if it is followed by else.

Example:

public static void method1(int times) {
    for (int i = 0; i < times; i++) {
        if (i % 2 == 0) {
             System.out.print(i);
        } else {
             System.out.print("x");
        }
        System.out.println(" hello");
    }
}

Class design

This section describes the style specifics of designing Java classes. Unlike the majority of work we do in lecture or section, your assessments are largely centered around class design. Some assignments in CSE 123 involves writing your own "blueprint" classes, so you may find it helpful to review this section weekly and use it as a general checklist.

Fields

Initialize fields inside the constructor

Do not initialize fields at declaration - this is both a flexibility and readability rule. It's possible that you want to initialize a field to different values in different constructors. Initializing a field at declaration and again in a constructor is both redundant and distracting.

Fields should always be declared with the private access modifier.

This modifier ensures clients of the class cannot modify the internal data without using an allowed (public) method.

Note: There is one exception to this rule. For node classes (ListNode, AssassinNode, IntTreeNode, QuestionNode, HuffmanNode) that you will work with in lecture and for assessments, it is acceptable (and encouraged) to use public fields so that the client (you again) can directly manipulate the field references to other nodes without using setter methods.

Why? As you'll see when inner classes are introduced, ideally these classes would be private inner classes, as clients should be abstracted away from the nodes themselves. Being an inner class also means that the field access modifiers wouldn't matter. However, we're writing them as separate classes, so public field access is the way to allow the nodes to be manipulated in the natural / conventional way.

Avoid extra fields

The more fields your class has, the more difficult it becomes to maintain and reason about your code. With more fields, each instance of the class will also take up more memory.

Tip: When revising your code, watch out for fields that are only used in one public method, and/or that are recomputed / cleared every call to a specific method. These can likely be simplified to local variables within the scope of that public method / its private helper methods. Also watch out for redundant fields that can be inferred from the properties of other fields.

Note: Designing a class always comes with its own set of tradeoffs, and deciding on what fields to include can be particularly tricky. In some situations, there may be debate about whether or not to make something a field to improve efficiency. Suppose you were writing a class that needed to keep track of a String field. To also store the .length() of the String as a field might seem "more efficient" (not actually more efficient), because then the code of the class won't have to call .length() more times, but this is a case that .length() is fast enough (and accessible by accessing the String itself) that storing this field isn't significantly beneficial and should be omitted. You might also run into cases where it does make sense to include another field (that could be omitted) because it makes code significantly easier to write or it relieves a bunch of recomputation. In any case, you should always review what fields you have and consider any tradeoffs, pros/cons, etc. to your current design and the other possibilities. See the section on efficiency for some more thoughts.

Class constants

Class constants should be declared with public, static, final modifiers.

  1. public allows a client to directly interact with the constant
  2. static means the constant is associated with the class rather than a specific instance of the class. (e.g. each instance of a class should have its own independent fields from other instances, but it's fine if they all share the same class constant value)
  3. final prevents the constant from being assigned a new value. i.e. the initialized value is final.

Create and use class constants when directed by the spec and where appropriate. A good creation/use of a constant constitutes replacing a raw value that is used multiple times in your program with a constant that stores the value, but with more descriptive and readable name. This also provides more flexibility if the raw value ever needs to change.

Note: Class constants have to be initialized at declaration unlike how fields should be initialized.

Tip: Watch out for using raw numbers aka “magic values” in your code. These values might confuse another reader of your code, so you should avoid them and consider other options. There may be a useful method or way to access the value that is more logical, or the spec might have more information.

Methods

Access modifiers

Methods (and constructors) to implement that are listed in the spec should all be declared public, so that clients can call these methods. Any additional methods/constuctors you write in the class should be declared private, so that these methods can only be called internally. This will not only prevent clients from calling these methods in ways that might corrupt your fields or otherwise break your program, but also simplify the functionality a client will have to deal with.

Parameters

Avoid extra parameters. For the same reasons to cut down on fields, simplifying the number of parameters can simplify the code. Watch out for unused parameters or parameters whose values can be inferred by other parameters.

Returns

Avoid unnecessary returns. If you pass in a reference to an object as a parameter, there's no reason to define your method to return that same reference. Wherever called the method already has a reference to the object, as it was able to pass it in the first place, so returning the same value back is redundant. In fact, defining a method to have a redundant return may imply that you made a copy of the object passed in, and may confuse a reader of your code.

// This method unnecessarily defines a return that always returns
// the reference to the original object.
// Because whoever called this method has a reference to the List object,
// we can change this method's return type to void to simplify the method.
public List<String> generateNames(List<String> toFill) {
    // add some elements to toFill
    ...
    return toFill;
}
// an improved reduced version
public void generateNames(List<String> toFill) {
    // add some elements to toFill
    ...
}

Note: For the public methods defined in the spec, the parameters and returns listed are required. You should define your method headers to match with the same parameters and returns. For private methods, however, you are in control of the design–this is where extra parameters and returns (4.3.2 and 4.3.3) come into play.

Unspecified behavior

  • Do not add additional behavior beyond what's described in the spec. This includes additional exceptions - only include exceptions listed in the specification for each method.
  • Do not modify object parameters passed to public methods, unless allowed by the spec (this is also an implication of the previous rule). An unsuspecting client of the method may not have wanted their data dismantled while your method ran for a different purpose.

Constructors

  • When applicable, reduce redundancy by using the this() keyword to call another constructor in the same class. Generally, the constructors that assume more defaults (fewer parameters) call the more general constructors (more parameters).
  • Clients of a class should never have to set fields of an object immediately after construction - there should be a constructor included for this situation. The general behavior of a constructor is to set up the state of an object correctly, and this should encompass setting fields.
  • Clients of a class should never have to pass in dummy values to a constructor to set up the state they want - instead, there should be a constructor included so that only the relevant parameters need to be passed.
  • Only implement constructors that are used somewhere.

Naming

Note: The names of the public methods and the class must appear exactly how they appear in the spec (case matters). If these names are not exactly correct, the provided main program should fail to compile if you try to.

Casing

  • Fields, local variables, and methods should be camelCased, where every word after the first begins with capitalization.
  • Class names and constructors should be UpperCamelCased. This is camel casing but the first word also begins with capitalization.
  • Class constants should be SCREAMING_CASE, where words are all uppercase and separated by underscores.

Note: Private helper methods that aid a constructor are still methods (have explicit return types and aren't used with new) and should be camelCased unlike the constructor itself.

Descriptiveness

Use descriptive names that will help out another reader of your code. Giving some kind of context for what the value(s) is or giving a high-level 1 or 2 word description is usually appropriate.

For the following example, assume we're trying to name a List<String> that will keep track of the names of each chapter in a book. Since we have some context for what we want to use this variable for, we can try and come up with a descriptive name.

Example Discussion
List<String> l = new ArrayList<String>();
l isn't super helpful - it really doesn't say anything about what the values in l or l itself represent.
List<String> listOfStrings = new ArrayList<String>();
Restating the type may seem better, but it doesn't actually help a reader understand the code any more easily. A reader still doesn't have any context or high level understanding what listOfStrings represents.
List<String> values = new ArrayList<String>();
Every variable stores a value, so this one is a little bit too vague to be helpful.
List<String> chapterTitles = new ArrayList<String>();
Sweetness! This one gives some context for what the list will store in it / represent in high-level.

Note: One-letter names may be easy to type, but they aren't too helpful as far as descriptiveness goes. Other readers of your code will likely ponder over their meaning if they need to understand what your code is doing. There are some appropriate uses, however, like for for loop variable names. They're also used to describe values that are temporary or so general that there isn't really any context to describe. But for the most part, try to use more helpful names when you can think of them. Other readers will likely appreciate it.

Use of types

Type parameters

Always include the type parameters when dealing with generic classes like ArrayList, TreeSet, etc.

Bad:   

Stack visited = new Stack();
Good:
Stack<String> visited = new Stack<String>();

Failing to do so will likely lead to casting that clutter up the code and / or unsafe type operations.

It is also possible (and encouraged) to use the "diamond operator" to reduce the number of times you have to write the type parameter.

Stack<String> visited = new Stack<>();

Interface types

Always declare variables of a specific interface type instead of the class type, where applicable.

Bad:   

ArrayList<String> names = new ArrayList<String>();
Good:
List<String> names = new ArrayList<String>();

This includes field declarations and parameter and return types in method headers. By working with more general types rather than the specific class type, our code will be more generalized and more flexible. For example, a method that has a parameter of type List<Integer> will be able to accept anything that implements the List interface (LinkedList<Integer>, ArrayList<Integer>, etc.). This is more flexible than a method that defines a parameter of type LinkedList<Integer> that can only accept a LinkedList<Integer>.

Variable types

Sometimes we have options for how we want to represent the data and either option(s) will be able to produce correct behavior. Instead of using a more complicated representation that can do everything we want and more, we should try use the representation that fits the specific problem the best. The intent and logic of your code will be simpler to another reader. Some examples are listed below.

  • If you're using an int to keep track of whether it's one value or another, you likely want to use a boolean instead.
  • If you're using a String to keep track of a single letter, you probably want to use a char.
  • If you're using Integer because you want to keep track of a sum, you probably can just use int.

Data structure usage

The point of a data structure is to store related values. If your data structure just has one value in it, it probably should just be a variable of a simpler type like the String or int it is. Another example of funky usage is to store unrelated values in the same data structure. For example, one way to store all your variables would be to use a Map<String, Object> where the keys are the names of the original variables and the objects are just the value of the variable. This would would be a bad usage of a map and data structures in general.

Efficiency

Note: Not all issues of efficiency that we care about in 123 can be addressed in this section. There are concrete examples of common efficiency issues listed below, but there are too many possibilities to cover every single one. However, we can still talk about some general guidance for reviewing code for efficiency. 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 way the spec phrases certain implementation details will lead you in the right direction. Being familiar with the kind of code we write in lecture and section will also give you a ballpark for what we expect the assessment code to be. If you find yourself having to look up tons of things online or write some code that seems noticeably more complicated than code we've written in section or lecture, you might want to take a step back and rethink the current approach. Unfortunately, there's no hard and fast rule for finding every inefficiency. To find them, you will have to dedicate some time to reason about your code.

Above all refer to the spec for guidelines on how to write the code in a reasonable manner. The spec is the true guide for what we expect for that specific assessment.

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<String>(); 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.

List<String> words = new ArrayList<String>();
words = generateWords();
List<String> words = generateWords();

Note: Initially, it can be confusing to understand the difference between declaring variables of object types and actually making new objects. This is a discussion that won't really be touched on here, but will happen during the quarter. It should be noted, however, that you can declare variables of object types for use without setting them to new objects. Setting List<String> words = null; or List<String> words; is totally fine (these statements do not make new objects). This may come in to play for some types of factoring.

Tip: Watch out for unnecessary object computation to do with fields.

Complex code

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.

Tradeoffs

For any task, there will be multiple implementations that can achieve the desired behavior. These different solutions may have different benefits and it may not be immediately obvious which one is the best. In the end, writing and designing code comes with many decisions, and you should try your best to choose an appropriate solution that matches what the spec emphasizes and requires. This isn't normally a huge focus in the style you should be focusing on, but it may come up and is worth thinking about.

If you're still worried about choosing "the wrong" implementation for something you did ponder over, make sure that there are true benefits (follows other mantras of style, other things listed in this guide) to your implementation that you can argue. If you can convince yourself, then you can have some confidence in your solution.

Tip: If such a decision is not well defined in the spec, try writing out both implementations and looking at them side-by-side. List out the pros and cons of each and see how closely those things align with what is written in the spec and what we've talked about in lecture/section. It should also be noted that if all the options seem poor, don't eliminate the possibility of other solutions existing.

Logic and redundancy

This section is all about less being more. Generally we want to follow the mantra that more concise code will be eaier to read and reason about. As a general rule, aim to revise your code to have fewer complicated expressions, ifs, elses, loops, etc., where reasonably possible.

Boolean zen

Boolean zen is all about using boolean values efficiently and concisely, and is best described through a few examples.

The following code shows a tempting way to write an if statement based on boolean value.

if (test == true) {
    // do some stuff
}

Note that test itself is a boolean value. When it is true, we are asking whether true == true. true == true will evaluate to true, but remember that the if branch will execute as long as what is in its parentheses evaluates to true. So we can actually use test directly:

if (test) {
    // do some stuff
}

Note: If you want to check for the opposite of test, don't check for test == false, use !test to check that the opposite of test evaluates to true.

Here's an example that uses what we learned in the previous section about simplifying boolean expressions.

if (test) {
    return true;
} else {
    return false;
}

There is actually a much more concise way to do this. If we want to return true when test is true and false when test is false, then we actually just want to return whatever test is.

return test;

In general, make your use of booleans to simplify if/elses and boolean expressions. This is something to look out for whenever you have conditions or boolean values.

Tip: If you ever have to write true or false literally in your code, double check that there's no way to simplify your logic. Using true or false literally in your code isn't inherently redundant, but it's something to double check.

Inappropriate if/else

if (someTest) {
    System.out.println("hello!");
}
if (!someTest) {
    // Because this code always wants to do one or the other, (and
    // doesn't involve a return or exception) we want to express this
    // code more simply as an if/else
    System.out.println("I'm redundant");
}
if (max < result) {
    return max;
}
if (max == 0) {
    // Note that the behavior inside this if block is exactly the same behavior as in
    // the other if block.
    // Instead of rewriting the same code twice, we can combine the two if conditions with ||
    // and just write the behavior once.
    return max;
}
if (max < 0) {
    // It doesn't matter if you think of conditions/cases or their negated versions, but after
    // revising your code don't include empty condition blocks with no line of code inside.
    // Instead, just flip the condition to have if (max >= 0), and no else.
} else {
    ...
}

Factoring

If you have lines of code that appear in multiple places in your program, you should consider trying to cut down on redundancy with some kind of factoring.

Private helper methods

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.

If / else factoring

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.

// 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!");
}
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!");

Tip: 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.

Loop zen

Loop bounds

When writing loops, choose loop bounds or loop conditions that help generalize code the best. For example, the code before this for loop is unnecessary.

System.out.print("*");
for (int i = 0; i < 4; i++) {
    System.out.print("*");
}

Instead, why not just run the loop one extra time? This way we avoid writing duplicate behavior.

for (int i = 0; i < 5; i++) {
    System.out.print("*");
}

Only repeated tasks

If you have something that only happens once (maybe at the end of a bunch of repeated tasks), then don't put code for it inside of your loop.

for (int i = 0; i < 4; i++) {
    if (i != 3) {
        System.out.println("working hard!");
    } else {
        System.out.println("hardly working!");
    }
}
for (int i = 0; i < 3; i++) {
    System.out.println("working hard!");
}
System.out.println("hardly working!");

Similarly, a loop that always runs one time is also a bad use of a loop. When reviewing the logic of your code, it might be helpful to check if a loop can be reduced to an if or just deleting the loop entirely.

Redundant conditions

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.

Loops

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

// 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");
    }
}

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.

if (someTest) {
    ...
} else if (!someTest ...) {
    // 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) {
    throw exception/return
}

// At this point, we know if someTest were true, we would have thrown
// an exception or returned from the method.  So for all code
// from here on, someTest must be false and it is redundant to check again.

if (!someTest) {
    ...
}

Reuse existing code

Avoid re-implementing the functionality of already existing methods, 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[] numbers, int value) {
    for (int i = 0; i < numbers.length; i++) {
        if (numbers[i] == value) {
            return i;
        }
    }
    return -1;
}

public boolean contains(int[] numbers, int value) {
    for (int i = 0; i < numbers.length; i++) {
        if (numbers[i] == value) {
            return true;
        }
    }
    return false;
}
public boolean contains(int[] numbers, int value) {
    return indexOf(numbers, 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.

Tip: Refer to section handouts and the spec for reminders about helpful methods.

Miscellaneous

The following sections detail information about style that doesn't quite fit into any of the previous categories. These aren't a huge focus in CSE 123, but are always good to consider.

Scope

It is generally good practice to keep variables in the most local scope possible. Remember that the scope of a variable is the closest pair of curly braces that surround it. In the following example, the variable nextScore has been declared in the scope of the entire method, but note that declaring it in such a wide scope is actually not necessary.

public static void printTenScores(Scanner console) {
    int nextScore;
    for (int i = 1; i <= 10; i++) {
        System.out.print("next score? ");
        nextScore = console.nextInt();
        System.out.println("score " + i + " = " + nextScore);
    }
}

Notice that nextScore is only ever accessed inside of the for loop, so we can actually localize its scope to just the loop itself rather than the whole method.

public static void printTenScores(Scanner console) {
    for (int i = 1; i <= 10; i++) {
        System.out.print("next score? ");
        int nextScore = console.nextInt();
        System.out.println("score " + i + " = " + nextScore);
    }
}

By localizing scope, our code becomes simpler to reason about, because we've minimized the number of different variables floating around at any given location in our code. Note that eliminating unnecessary fields is often just an example of localizing variables. Also note that "re-declaring" the variable nextScore inside the loop is just fine efficiency-wise because of how the compiler will optimize our code. In this course, you shouldn't ever have to worry about micro-optimizations like the number of variable declarations.

Convenience methods

This is a section about reimplementing methods that has to do with smaller-scale redundancies than Reimplementing Methods (Section 8.6). Try to take advantage of already-implemeted methods (that aren't from Java 8 or later) if they exist for a particular task.

For example, instead of checking for

list.size() == 0
or
stringA.length() == 0
use the more readable method that was made for this purpose:
.isEmpty()

Another common offender is checking

stringA.toLowerCase().equals(stringB.toLowerCase())
Intead, use
stringA.equalsIgnoreCase(stringB)

Forbidden Features

In general, once a class has been discussed, it is available for use by students. (For example, once the String class is introduced, you may assume that you can call any of the available String methods in future programming assignments without asking permission). Unless otherwise specified, language features should not be used before they have been introduced in class (e.g. you should not use if statements in any assignment until you have seen them in class). Students should realize, however, that saying that you are not forbidden from using a certain construct is not the same thing as saying that it is a good idea to use a certain construct. We generally don't give advice to students about which constructs to use; you should use your best judgment to decide. (Though if a particular assignment states that you are required to use a construct, you should do so.)

In addition, there are some constructs that you are NEVER allowed to use, as described below. Java has grown to be a complex language with many features, and we don't have time to teach all of these features in CSE 123. We have a general rule that students should not use "advanced" material that we have not covered in class. In addition, we have identified several Java features that we do not want students to use at all. It is not bad style to use these features, but we want to have a level playing field for all students. Since we don't teach these features in the class, we do not allow them to be used to avoid giving an advantage to students who have learned about the features on their own.

The following features should NEVER be used in any graded CSE 123 work (unless otherwise specified by the assignment or problem):

  • break or continue
  • return from a void method
  • the System.exit method
  • the switch statement
  • try/catch
  • the var keyword
  • Java 8 functional features (e.g., lambdas, streams, method references)
  • Java 11 features (e.g., local variable type inference, String methods like repeat(n))
  • the toCharArray, join, and matches methods of String
  • the StringBuilder, StringBuffer, StringJoiner, StringTokenizer classes
  • the methods Arrays.fill, Arrays.copyOf, Arrays.copyOfRange, and Arrays.sort
  • the toArray and clone methods of ArrayList
  • the methods Collections.copy and Collections.sort
  • System.console
  • LinkedHashMap

(Don't worry if you are not familiar with one of the features listed above. You can simply think of these as Java features we have chosen not to teach in this class so that we can focus on other important concepts instead.)

Acknowledgements

Thanks to Michelle and Michael, who provided several code examples and explanations that are used in this guide.

Additional thanks to proofreaders who help find typos and suggest rewordings. Finally, thanks to the students reading this guide. Surely after reading this guide, you'll contribute to writing stylish code that exists in the world.

Note: If you find any typos or otherwise have suggetions or clarifications, feel free to email the head TA to suggest a fix (and optionally have your name listed in the acknowledgements).