About this guide
This guide is an (informal) style guide for CSE 142 and 143, and contains a list of rules we expect you to follow on your homework assignment. To learn more about a particular rule, click the "expand" button.
Please note that this guide is not necessarily comprehensive. We've done our best to include all the different style rules we can think of, but there's always the case we might have missed one.
You can also filter and search for specific rules based on content, chapter, or assignment using the filters to the right.
Why do we care about style?
Programming is a very collaborative activity, so it is very important for humans to be able to read and use your code. It is very difficult to collaborate with with and trust a programmer who writes code that has bad style.
"Programs must be written for people to read, and only incidentally for machines to execute"
– Hal Abelson
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 should not collaborate on homework assignments in CSE 142 and 143, it's still important to practice programming with good style – it's the core difference between whether or not a program is pleasant or impossible to work with.
For example, take the following code, which is written using bad style:
public void f(int n ){ for (int i=1;i<=n;i++){for(int j=i;j>0;--j)System.out.print("*") ;System.out.print("\n" ) ; } }
Can you tell what this code does? Not easily. Contrast that to the following code, which does the exact same thing, but is written using good style:
public void printStaircase(int numSteps) { for (int i = 1; i <= numSteps; i++) { for (int j = 0; j <= i; j++) { System.out.print("*"); } System.out.println(); } }
This is much more readable. Even if we don't understand a single line of code, we can infer from the very first line that this chunk of code will likely print a staircase, and roughly how the code works from the variable names and indentation.
This is obviously an extreme example, but it does demonstrate in a nutshell what style is and why it's important to follow.
As a result, we place a very high emphasis on style and internal correctness in this class. In fact, internal correctness points will typically take up half of your homework points, and it's typically much harder to get full internal correctness then it is to get full external correctness.
The fundamental principles of style
Good style is not about memorizing a list of rules – every rule can be derived from the same core set of principles. If you understand those principles well, you should be able to independently predict and derive nearly every rule in this guide.
When students have difficulty predicting what is good style or not, it's usually because they approach style as a list of somewhat arbitrary rules to memorize. However, this is not actually how style works. Ultimately, our style rules (and any organization's rules) are created for one purpose only: to help make your code cleaner and more understandable. If a rule exists, it's because without it, your code will be messier and harder to understand.
More specifically, you'll find that all of our rules regarding style ultimately stems from four fundamental principles regarding style: we want to write code that is readable, efficient, concise, and modular. Everything derives from these four rules.
Our ultimate goal is to write code which both works correctly and is easily read and maintained by other human beings. We call this "external correctness" and "internal correctness" respectively.
How you achieve external correctness is obvious: if your code compiles and does exactly what we tell you to do, you've achieved external correctness.
Achieving internal correctness can be more difficult, but can be achieved by constantly keeping these core principles in mind:
-
Readable:
You write code that is easily read and understood. You want to write code that is easily understandable, modified, and extended by other human beings.
-
Concise and lacks redundancy:
You write code that contains no duplicate or copy-pasted code. That way, if you need to change something, you only need to change a single piece of your code, not every single copied-and-pasted chunk.
-
Well-organized and modular:
You write code that is easily combined and reusable. Changing a single portion of your code does not cascade out and force you to change the entire codebase. Your code is also modular and composes well, like legos or building blocks.
-
Efficient:
You write code that does not do extra work and does not consume unnecessary memory. The faster your code runs and the less resources it consumes, the better.
As a note, we find that most students will initially have a poor understanding of what does and does not constitute "efficient code". Until we formally introduce the concept of efficiency and Big-O analysis near the start of CSE 143/the second half of CSE 143x, you don't really need to worry about efficiency on your homework assignments. If you follow the other style guidelines, your code is most likely already efficient.
We will elaborate on why exactly these four sections are important throughout this style guide, but for now, keep them in the back of your head. When you're not sure whether or not something is good style or not, just ask yourself: "how does doing X impact readability, concision, modularity, and/or efficiency?"
How this guide is organized
The content in this guide is arranged primarily by category – you can filter by category, assignment, and book chapter using the drop-down menu to the right.
We recommend you filter the guidelines by either book chapter or assignment, especially for CSE 142 students or students in the first half of CSE 143x. Many of the style guidelines are meant more for CSE 143 students, and will not be immediately relevant or understandable.
More specifically, the content in this guide is ordered according to the following categories:
-
Introduction
Explains in general what style is, why we care, etc.
-
Formatting and conventions
Focuses on rules related to formatting individual segments of code. This is the most detail-oriented and microscopic section of the style guide. If we were to compare writing code to writing an essay, this section would be about rules related to punctuation, spacing, and so forth.
-
Control flow
Focuses on how to organize code within a single method and how to correctly use fundamental Java constructs such as if statements or loops. To reuse our metaphor, this would be the sentence or paragraph-level oriented segment of our guide.
-
Division of responsibility
Focuses on the entire structure of your program as a whole, and how each of your methods and classes interrelate together. Also discusses how the code you write fits into the large ecosystem of programs, and how classes should ideally compose and build up together to form well-formed code. This would be the essay and library-level oriented segment in our metaphor.
-
Commenting
This segment is an extension of the previous section, and discusses how to apply those ideas to write clean comments. Our metaphor is running a bit ragged now, so I'll drop it here.
-
Efficiency
This segment focuses on what it means to write efficient code, what efficiency is and isn't, and common mistakes and misconceptions. Most of what we learn about efficiency will be covered in lecture during CSE 143, so this section will be fairly minimal.
-
Data structures
This segment focuses on miscellaneous style rules and guidelines regarding the different data structures and objects we teach you about. Most of what we teach you here will be covered either in lecture, section, or in the homework spec, so again this section will be fairly minimal.
If you'd prefer to learn about concepts in the order they're introduced in CSE 142 and 143, instead of by thematic topic, we recommend you use the filter selector to the side to look at style guidelines per specific assignment.
Each style guideline will compound on each other – the guidelines introduced in assignment 1 will continue to stay relevant through the rest of the quarter + in both CSE 142 and 143.
This section of the guide contains some basic rules we expect you to follow throughout the quarter. These are not really "style guidelines", but are still important to follow.
Academic Integrity
Programming HW must be completed individually; all code you submit must be your own work. Please consult the syllabus for more details.
Advanced material
Do not use material that you haven't been taught. When in doubt, ask.
In this course, you are not permitted to use "advanced material" – material we have not taught you yet. Advanced material isn't necessarily poor style, but it can sometimes make your assignments "too easy", preventing you from learning what we were trying to teach.
If you are uncertain about what counts as advanced material, or would like to use such material in an assignment, please ask. If you make a persuasive enough case, we may make an amendment to our rules (though this is unlikely).
What constitutes "advanced material" will obviously vary from assignment from assignment. However, our lectures tend to follow the textbook very closely. If you know what chapter we're on, then you can safely assume that you may use any material the textbook references in that chapter or in earlier chapters.
If you have prior programming experience, and have just started CSE 142 or CSE 143x, you should be aware that on your first assignment, the only language features you are allowed to use are:
println
statements- Methods that accept no parameters
If you have skipped CSE 142 and have jumped straight to CSE 143, you should be aware that the only material you are initially allowed to use is material covered during CSE 142 and directly taught in lecture or section. It may be good idea to review old 142 websites to get a sense of what is and isn't allowed, but in general, you are expected to know and are permitted to use:
- Control flow structures (
if
,else-if
,else
,for
,while
, but NOTforeach
) - Methods, returns, and parameters
- Variables and basic types (
int
,double
,boolean
,char
,String
) - Arrays (but NOT ArrayList!)
- Basic objects (some inheritance)
This is not a comprehensive list of topics, but should be enough to get you started on your first homework assignment and get a basic feel for what is and isn't expected.
An important aspect of good style is having code that is formatted cleanly. Cleanly formatted code will greatly help enhance readability.
As previously mentioned, one of the core guiding principles is readability – writing code that is as easy to read and understand as possible.
An important part of maintaining readability is having clean and consistent formatting.
It's similar to writing an essay. It doesn't matter how well-written your words are if you're forgetting your periods, adding random spaces everywhere, not using paragraphs, and so forth. We have similar rules regarding code, which is the focus of this section of the style guide.
As an example, take the following paragraph:
!c"all" M"e" ish!M,Ael: some ?yeAR"s? ?aGo-NevEr min!D how lon!G p,recis"el!y?-havi"ng" lit,T?LE o?r No monEy in my purs,E, anD notHi?Ng pArticulaR to? inTeReST ,mE on ,sh,or,e, i? tHOUght i! ,would sail abOut" a l!ittLE! aNd, SeE th?e waTEry paRt oF? !thE Wor?Ld: it ,iS a way, i ha"Ve of D!Riv!Ing ofF tHe sp?le"en and" rEG"U!lating !The ci"rCulat?iOn: whenEve?r I finD MY!SelF, grow!ing "Gr,im !aboUt !the? !mout?h; wheneveR i"T I,S ,a damp, driZzly" noVemBe,r I"n m,Y soUl; whenever I FiNd, Mys"elf I!nVOlunTARi"ly !P"aus,ing Be!fOre coffI!N Ware?hO?us,Es, and br!inGINg up "the" rear? oF everY! fuNeral I "Meet;? and espe?cIally whEnever m"y hypos geT su"ch an? u?ppeR hANd o"f Me", that it? r"eqU"IreS A stROnG mO?rAL pR"incIple tO, prEveN,t, Me! fRO"m d!eli?bera"teLy ?stePpiNg? Into the stREet, and met"HoDiCally kn?o?ck!INg peo"p"le\'S hat,s off"-,Th,EN, I, Ac!coUNt it !h,igh Time tO! g!eT to s?ea as soo?N as, "i c?an: t,his is my ,suBS?t!itUtE for ,PiStOl and !bal,l: wi"th a P,hIlo!soPhicAL flour"isH! CATo ThrOws" ?hi"m,Se?l!f uPon his sWord; I qui"et,l"Y TakE T"o The s"hip: ,there is nOtHIng SUr"PrIsINg in thi,S: iF "THey but !KnE?W iT, AlmoSt! all !MeN In! their ,dEgree, somE? TiME? or OTher?,! cheRIsh VerY Near"ly! The Same feelIngs towarDs t,he" OcEan w?ITh Me:
This is the first paragraph from Moby Dick, except formatted very oddly and inconsistently. Because we aren't abiding by the rules of English grammar and formatting, and because we're used to text being formatted a certain way, this paragraph is quite difficult to read.
Similarly, many programmers will expect code to be formatted in a certain way and will have a lot of difficulty reading sloppy code. As a result, it's very important to format your code cleanly, just as you would write English cleanly.
One important thing to keep in mind is that just like English, formatting rules are inherently a little subjective. In particular, if you have any prior experience with programming, or know how to program in a language other then Java, you might find that our formatting rules contradict the ones that you've been taught.
This is actually ok. The exact rules themselves aren't too important – rather, being consistent is. The way you format your code will convey certain things to people reading it, so if you break a rule and suddenly adopt a new convention, you'll make it harder for people to understand. The rules we've outlined are rules which most professional Java programmers tend to follow, so you should make a good attempt to learn them so you understand what professional Java looks like.
Different programming language and organization will naturally adopt slightly different rules and conventions, which is perfectly fine.
If you have another formatting style which you prefer, you should talk to the instructor for this quarter if that style is ok or not. Some instructors will accept only the style outlined in this guide, and other instructors are more flexible and will accept variations, as long as you're consistent.
Indentation and nesting
When you see a matching set of opening and closing curly brackets, indent all lines of code in between by one additional level. Be consistent in how big each indent is.
Indentation is a useful tool for helping a reader visually see the "structure" of your code. Without indentation, it's easy to lose track of how your curly brackets are nested. This makes it harder to see the overall "flow" of your code.
Like many of our other style guidelines, the compiler will not care about indentation. However, human readers do, and typically care very strongly.
In any case, here are some guidelines that you should follow:
-
Indent each line of code between two matching curly brackets by one extra level.
-
Be consistent in how much you indent each time.
If you decide to indent each new level by four spaces, you shouldn't suddenly switch to indenting by three spaces half-way through your code.
Do note that we don't care if you prefer tabs or spaces (though we do strongly suggest using spaces), or how many spaces you use per indent, as long as you're consistent.
-
Place each opening curly bracket on the same line as the preceding control flow statement, and place each closing curly bracket on a separate line.
The only possible exception to this rule is with
else
andelse if (...)
statements. Some programmers will prefer to place theelse
statement on the same line as the previous closing curly bracket for brevity. If you prefer this style, feel free to use it.
A good rule of thumb to tell if your indentation is ok is to imagine that all of your curly braces suddenly turned invisible. Can we still determine how your code is structured? If so, then your code is most likely correctly indented (or close to being so).
If you prefer another convention for indenting your code, please check with the current instructor for this quarter or ask on the message board to see if he or she is ok with that convention.
Now, let's look at an example of bad indentation. Try and see how many errors you can find:
public class MyProgram { public static void main(String[] args) { System.out.println("main-1"); System.out.println("main-2"); myMethod(); } public static void myMethod() { System.out.println("myMethod-1"); for (int i = 0; i < 10; i++) { System.out.println("myMethod-for-1"); System.out.println("myMethod-for-2"); } System.out.println("myMethod-2"); }}
This is an example of bad indentation. If we try applying the "invisible curly brackets" test to it, we can immediately see that it's very difficult to deduce the overall structure of this program from just the indentation alone:
public class MyProgram public static void main(String[] args) System.out.println("main-1"); System.out.println("main-2"); myMethod(); public static void myMethod() System.out.println("myMethod-1"); for (int i = 0; i < 10; i++) System.out.println("myMethod-for-1"); System.out.println("myMethod-for-2"); System.out.println("myMethod-2");
Here is a somewhat better, but still bad version of the above:
public class MyProgram { public static void main(String[] args) { System.out.println("main-1"); System.out.println("main-2"); myMethod(); } public static void myMethod() { System.out.println("myMethod-1"); for (int i = 0; i < 10; i++) { System.out.println("myMethod-for-1"); System.out.println("myMethod-for-2"); } System.out.println("myMethod-2"); } }
While this is better, the indentation is still not ideal. Things are not lining up perfectly, and we sometimes indent by 3 spaces, or by 5.
Here is a corrected version of the above program:
public class MyProgram { public static void main(String[] args) { System.out.println("main-1"); System.out.println("main-2"); myMethod(); } public static void myMethod() { System.out.println("myMethod-1"); for (int i = 0; i < 10; i++) { System.out.println("myMethod-for-1"); System.out.println("myMethod-for-2"); } System.out.println("myMethod-2"); } }
This is a good example of indentation. If you look at each matching set of curly brackets, each line inside is indented by four spaces, helping make the structure and flow of the code more obvious.
Even if you have no idea what a for
loop is, you can still deduce that lines 11
and 12 "belong" or are nested inside line 10.
Here is another example of good code:
public class Grade { private double gpa; public Point(double gpa) { this.gpa = gpa; } public String getLetterGrade() { if (this.gpa > 3.5) { return "A"; } else if (this.gpa > 2.5) { return "B"; } else if (this.gpa > 1.8) { return "C"; } else if (this.gpa > 0.7) { return "D"; } else { return "F"; } } }
Unlike the previous example, we've indented 3 spaces per indent in this example. This is ok, as long as we're consistent. Each else if and else statement is also placed on the same line as the preceding curly bracket for brevity.
(And again, even if you don't recognize any of the code you see here, it's still easy to determine how the code is nested)
Here is another version of the previous code snippit that uses the alternate else
block formatting style. This would also be acceptable.
public class Grade { private double gpa; public Point(double gpa) { this.gpa = gpa; } public String getLetterGrade() { if (this.gpa > 3.5) { return "A"; } else if (this.gpa > 2.5) { return "B"; } else if (this.gpa > 1.8) { return "C"; } else if (this.gpa > 0.7) { return "D"; } else { return "F"; } } }
Indentation and comments
Comments should be indented to the same level as the thing they are commenting.
Here is an example of bad indentation and commenting:
// A program that prints the number '8' using stars public class Digit8 { public static void main(String[] args) { printLine(); printSides(); printLine(); printSides(); printLine(); } // Prints a horizontal line of 4 stars public static void printLine() { System.out.println("****"); } // Prints two vertical lines that are 2 spaces tall, and are // separated by 2 spaces horizontally public static double printSides() { System.out.println("* *"); System.out.println("* *"); } }
Notice that each of the method comments are too far to the left -- they should all be indented by one level so they line up with the method:
// A program that prints the number '8' using stars public class Digit8 { public static void main(String[] args) { printLine(); printSides(); printLine(); printSides(); printLine(); } // Prints a horizontal line of 4 stars public static void printLine() { System.out.println("****"); } // Prints two vertical lines that are 2 spaces tall, and are // separated by 2 spaces horizontally public static double printSides() { System.out.println("* *"); System.out.println("* *"); } }
Here is a somewhat more complex example, which uses some material from chapters 2-6 in the textbook. Note that each inline comment is placed immediately before the thing that it is commenting, and follows all of our regular indentation rules.
// A program to help me find the area of a triangle. import java.util.*; public class FindTriangleArea { // The main entry point of the program. Prompts the user to // enter the three sides of the triangle, and attempts to // convert it to a triangle if possible. Prints either the // area or an error message to the console. public static void main(String[] args) { Scanner userInput = new Scanner(System.in); double sideA = prompt(userInput, "side A"); double sideB = prompt(userInput, "side B"); double sideC = prompt(userInput, "side C"); if (isTriangle(sideA, sideB, sideC)) { System.out.println(computeArea(sideA, sideB, sideC)); } else { System.out.println("Not a triangle"); } } // Prompts the user for a double using the provided message public static double prompt(Scanner input, String message) { System.out.print(message + " "); return input.nextDouble(); } // Determines if the provided side lengths form a valid triangle. // Returns false if any side length is non-positive, or if the side lengths // fail the triangle inequality theorem. public static boolean isTriangle(double sideA, double sideB, double sideC) { // all sides must be positive if (sideA <= 0 || sideB <= 0 || sideC <= 0) { return false; } double smallest = Math.min(sideA, Math.min(sideB, sideC)); double largest = Math.max(sideA, Math.max(sideB, sideC)); double middle = sideA + sideB + sideC - smallest - largest; // The triangle inequality theorem states that any side of a triangle // is shorter then the sum of the other two sides. if (smallest + middle <= largest) { return false; } } // Finds the area of a triangle given the side lengths. // Assumes the side lengths form a valid triangle (see isTriangle for criteria) public static double computeArea(double sideA, double sideB, double sideC) { // Currently using Heron's formula double s = 0.5 * (sideA + sideB + sideC); double area = Math.sqrt(s * (s - sideA) * (s - sideB) * (s - sideC)); return area; } }
Notice how each comment lies directly before the thing that it's commenting, and is indented to the same level.
Curly bracket and indent style
There are many different conventions regarding how exactly to go about formatting and placing your curly brackets. We strongly suggest you stick to the standard Java conventions, but you are free to pick whichever convention you want, as long you consistently follow it (and the convention you pick isn't too bizarre).
Expand for details on our recommended style.
We recommend you format your curly brackets following Java conventions:
- Do not put a new line before an opening curly bracket.
- Always start a new line after an opening curly bracket.
- A closing curly bracket should always belong on a separate line, except
for
else
statements. - Put one blank line between each method.
Here is an example of some code that follows all these conventions:
public class MyProgram { public static void main(String[] args) { System.out.println("foo"); methodA(); methodB(3); } public static void methodA() { for (int i = 0; i < 10; i++) { for (int j = 0; j < 10; j++) { System.out.print("*"); } System.out.println(); } } public static void methodB(int param) { if (param > 0) { System.out.println("Positive"); } else if (param < 0) { System.out.println("Negative"); } else { System.out.println("Zero"); } } }
Do note that we allow you to deviate from these rules to some degree, as long as the convention
you pick is consistent and not too bizarre. For example, if you prefer compressing your
if/else
blocks more tightly, it would be fine to rewrite methodB
like so:
public static void methodB(int param) { if (param > 0) { System.out.println("Positive"); } else if (param < 0) { System.out.println("Negative"); } else { System.out.println("Zero"); } }
Here is an example of some code which deviates from these rules even further, but would still be acceptable:
public class MyProgram { public static void main(String[] args) { System.out.println("foo"); methodA(); methodB(3); } public static void methodA() { for (int i = 0; i < 10; i++) { for (int j = 0; j < 10; j++) { System.out.print("*"); } System.out.println(); } } public static void methodB(int param) { if (param > 0) { System.out.println("Positive"); } else if (param < 0) { System.out.println("Negative"); } else { System.out.println("Zero"); } } }
While we don't recommend you follow this style (it require more scrolling to read), you are free to do so as long as you consistently follow it.
You should never mix together multiple different styles – it looks ugly:
public class MyProgram { public static void main(String[] args) { System.out.println("foo"); methodA(); methodB(3); } public static void methodA() { for (int i = 0; i < 10; i++) { for (int j = 0; j < 10; j++) { System.out.print("*"); } System.out.println(); } } public static void methodB(int param) { if (param > 0) { System.out.println("Positive"); } else if (param < 0) { System.out.println("Negative"); } else { System.out.println("Zero"); } } }
Long lines
Do not have lines of code that over 80 characters wide, including indents and comments. Expand for strategies on handling long lines which you cannot shorten.
The reason why we have these rules is that if your code is too wide, your readers will have to horizontally scroll in order to read your code, which hinders readability.
This is an potential problem, even on large desktop monitors. It's common practice for developers to put multiple windows side-by-side (for example, a web browser open to the left, and their editor to the right). Many tools programmers use are built around the assumption that the code it inspects is wrapped to a sensible level.
Here's an example of some code that violates this rule:
public class LongAndAnnoying { public static void main(String[] args) { rageInducing("hello world", "blah blah blah", "asdfasdfasdf", 1337); // We're testing this method by adding in some random characters. } public static void veryAnnoying(String seedString, String longParamName, String anotherLongName, int finalLongName) { // ... } }
The 3rd line and the 5th line would be considered too long – counting the four spaces for indentation at the start, and the comment, we've crossed the 80 character threshold.
We can fix the first long line by moving the comment to the previous line:
public class LongAndAnnoying { public static void main(String[] args) { // We're testing this method by adding in some random characters. rageInducing("hello world", "blah blah blah", "asdfasdfasdf", 1337); } public static void veryAnnoying(String seedString, String longParamName, String anotherLongName, int finalLongName) { System.out.println("foo"); // ... } }
However, this still leaves us with the long method header. We could try and shorten this by shortening the parameter names, and sometimes that works, but it is a bit of a compromise – we can often do better. In particular, what we can do is wrap our line, like so:
public class LongAndAnnoying { public static void main(String[] args) { // We're testing this method by adding in some random characters. rageInducing("hello world", "blah blah blah", "asdfasdfasdf", 1337); } public static void veryAnnoying( String seedString, String longParamName, String anotherLongName, int finalLongName) { System.out.println("foo"); // ... } }
Note: we don't have any set convention on how you should wrap your lines. The only thing we require is that any wrapped lines such be indented at minimum twice from the the regular indentation level to indicate that they're out of their regular flow.
For example, all of the below would have been acceptable:
public class WrappedLines { // ...snip... public static void version1(String seedString, String longParamName, String anotherLongName, int finalLongName) { System.out.println("version 1"); // ... } public static void version2(String seedString, String longParamName, String anotherLongName, int finalLongName) { System.out.println("version 2"); // ... } public static void version3( String seedString, String longParamName, String anotherLongName, int finalLongName) { System.out.println("version 3"); // ... } public static void version4(String seedString, String longParamName, String anotherLongName, int finalLongName) { System.out.println("version 4"); // ... } }
Note that the above is not an exhaustive list – there are many, many different valid ways of indenting and wrapping long lines beyond just those three.
However, the following would not be acceptable:
public static void confusing(String seedString, String longParamName, String anotherLongName, int finalLongName) { System.out.println("confusing"); // ... }
The wrapped line is indented only once from the normal flow, so doesn't "stand out" enough.
Blank lines
Leave a single blank line between each significant chunk of code. At minimum, leave one blank line between each method. Do not write multiple consecutive blank lines. Do not leave a blank line after an opening curly brace, or before a closing curly brace.
You should leave at minimum one blank line between methods for readability:
// My class header comment goes here public class Example { // Method header comment public static void main(String[] args) { // ... } // method header comment public static void method1() { // ... } // method header comment public static void method2() { // ... } }
Omitting blank lines will leave your class feeling cramped and suffocated:
// My class header comment goes here public class Example { // Method header comment public static void main(String[] args) { // ... } // method header comment public static void method1() { // ... } // method header comment public static void method2() { // ... } }
If you want, you can also leave the occasional blank line inside your method to enhance readability, although you should not leave a blank line after every line:
// My class header comment goes here public class Example { // Method header comment public static void main(String[] args) { String name = getName(); int[] data = retrieveData(name); data = cleanData(data); plotData(name, data); displayStatistics(data); } // ...snip... }
Do not leave a blank line after an opening curly brace, or before a closing curly brace. It's a waste of space. The indentation and curly brackets are already enough to help us distinguish where a method starts and ends. Think of curly braces like hugs – air hugs where you don't actually touch the person are kinda lame:
// My class header comment goes here public class Example { // Method header comment public static void main(String[] args) { String name = getName(); int[] data = retrieveData(name); data = cleanData(data); plotData(name, data); displayStatistics(data); } // ...snip... }
The same principle applies for control flow structures like if statements or for loops – don't do the following:
public class Example{ // ...snip... public static void foo(int a, int b) { if (a < b) { System.out.println("Foo"); } else { System.out.println("Baz"); } } }
Instead do:
public class Example{ // ...snip... public static void foo(int a, int b) { if (a < b) { System.out.println("Foo"); } else { System.out.println("Baz"); } } }
One statement per line
Place each statement on its own line.
By placing each statement on a separate line, you make it easier for readers to understand the gist of your code by simply scanning down the left-hand side of your code.
You may feel the compulsion to avoid this rule to, especially when you have short if statements. However, you should avoid doing so. For example, the following is not ok:
if (something) { System.out.println("Ew, a one-liner"); } if (somethingElse) System.out.println("Even worse");
There may be a few rare cases where it's ok to do the above if it ends up improving readability, but that is something that needs to be decided on a case-to-case basis.
If you feel that combining statements would boost readability, you should talk to a TA first to double-check.
The corrected version looks like this:
if (something) { System.out.println("Hooray for readability!"); } if (somethingElse) { System.out.println("I got all my curly braces"); }
Spacing
Place spaces to enhance readability. Be sure to place spaces such that your code is neither too cramped nor too expansive. Expand for a list of specific spacing rules with examples.
Determining how exactly to space out your code can sometimes be difficult. The guiding principle is to try and space your code to maximize readability, but it can sometimes be difficult to tell exactly what is more readable, especially if the difference is just one or two characters.
For example, which of these two method definitions is consider better style?
public static void main(String[] args) { // or public static void main( String[] args ) {
Different communities and organizations will have differing opinions as to which version is better. In this particular case, most Java developers will assert that the first version is better, but many PHP developers would disagree and call the second version better.
That said, for the sake of consistency, we do expect you to follow the general Java conventions regarding spacing. While some programming languages and and organizations have different rules, we do expect you to demonstrate that you understand the Java standard.
This means that you MUST leave a space in the following situations:
-
Leave a single blank space before every opening curly bracket.
For example, do
if (something) {
instead ofif (something){
. This emphasizes that a control flow condition and its body are two separate units. -
Leave one space between a control flow statement (such as
if
orfor
) and its opening paren.For example, do
if (something) {
, notif(something) {
. This emphasizes that a control flow statement and its condition are two separate units. -
Leave one space before and after each operator.
For example, do
double bar = 3 + b * (4 - a);
, notdouble bar=3_+b*(4-a);
. Give your numbers room to breathe.If following this rule causes your expressions to become very long, you should strongly consider breaking your mathematical expression into smaller ones using variables. (This will also let you "name" subexpressions, enhancing readability.)
You should NOT leave a space in the following situations:
-
Do not include a space between a method name and its opening paren.
For example, do
public static void test() {
, notpublic static void test () {
. This emphasizes that the method name and its arguments are a single unit, unlike control flow statements. -
There should be no space after an opening paren or before a closing paren.
For example, do
double foo = myMethod(a, b, c);
, notdouble foo = myMethod( a , b , c );
. This rule exists mostly due to convention – Java programmers will argue those extra spaces adds unnecessary noise.
Printing and newlines
If you want to print a newline, always do System.out.println()
instead of
System.out.print("\n")
or System.out.println("")
. With a few rare
exceptions, avoid using the \n
or \r
newline escape characters.
The reason why we never print a newline by doing System.out.println("");
is for
one simple reason – it's redundant. Doing simply System.out.println();
will
do the exact same thing, so why bother adding an extra string when we don't need to?
The reason why we don't permit newlines by doing something like the following is more subtle:
System.out.print("line 1 \n line 2 \n")
Part of the reason why this is forbidden is because it negatively impacts readability. Ideally, we want each line of output to correspond to a single println statement so we can easily scan the method to see how many lines we're printing out, and using newlines interferes with that. We can no longer easily tell how many lines a method could potentially print out.
The other more subtle reason is that the newline character \n
is not
platform-independent – what exactly \n
does will vary from
operating system to operating system and from program to program.
More specifically, if you want a newline in Linux-based operating systems, you need to
use \n
(the newline character). If you want a newline on Macs, you need to use
\r
(the carriage return character). And finally, if you want a newline on Windows,
you need to use \r\n
(carriage return, followed by newline). And of course, to try
and handle this, different IDEs and editors will (inconsistently) attempt to interpret
these different kinds of newlines, adding to the confusion.
Why? Historical happen-stance.
Println has none of these issues, and will do the correct thing on all operating
systems. To perform an equivalent task, your program would have to print
System.getProperty("line.separator")
instead of \n
.
There are only two exceptions to the above rule. The first exception is when you're
attempting to dynamically form a string containing newlines. In that case, we have no
choice but to use the \n
character (and for the sake of simplicity, we'll ignore
the fact that \r
exists.
The other exception is when using printf
. We frequently want to use
printf
to print out a single line, like println
, but printf
will
not append a newline to the end of the string for us. As a consequence, we're ok with you
using \n
when using printf
.
We will always tell you when using \n
is ok, so unless instructed otherwise,
avoid using it entirely.
Descriptive variable names
When naming variables, fields or methods, be descriptive and succinct. With a few rare exceptions, you should never use create a variable or method with a single-letter name or an overly long name. Expand for more specific guidelines.
How you chose to name your variables and methods can be very powerful tool for enhancing the overall readability of your code. Picking good variable names can turn even the most complex and confusing code understandable to rank beginners.
If we distil variable naming down to the very basics, we'll arrive at the following set of rules:
-
Give all variables and methods names related to what it's supposed to represent or do, and not just its type.
Try and pick names that add context to your code. For example, instead of naming a method
checkInt
, call itcheckIfPrime
instead. -
Avoid single-letter names, and too-long names.
For example, instead of calling a variable
x
ortheNewStringThatIAmAppendingDataTo
, call itresult
instead. -
Avoid variables with similar names yet generic names.
For example, you should avoid giving variables names like
temp1
,temp2
,temp3
, etc. Try and be purposeful and specific about naming. What does each variable represent? What exactly does it contain? -
Methods should always use verb or verb clauses as names, and variables should always use nouns or noun clauses as names.
This naming convention reflects the fundamental difference between methods and variables. Methods do, and variables are.
-
While you should generally avoid single-letter variable names, there are a few exceptions to this rule:
- Loop counters in for loops, such as
i
,j
, andk
. - When working with scientific formulae, such as
c = Math.sqrt(a * a + b * b)
. You will almost never encounter this case in CSE 142 and 143. - When the variable has no inherent meaning beyond its type, such as with the Graphics object, the Random object, and with many exam questions. We will explicitly inform you when you run into this case.
- Loop counters in for loops, such as
What happens if we don't follow these rules? As an example, take the following code, which violates essentially every rule listed above:
public List<String> get(Map<String, Integer> map, String strA) { int num = map.get(strA); int c = 0; List<String> r = new ArrayList<String>(); for (String n : map) { if (!n.equals(strA)) { int num2 = map.get(n); if (num == num2) { r.add(n); } } } return r; }
Can you tell what this code is doing? No! If you're in CSE 142, you probably wouldn't be able to understand most of it, given that this code sample uses several 143 concepts. If you're in CSE 143, you might be able to understand what it's doing if you sat down and traced through it, but you wouldn't be able to intuitively understand what this method was intended to do in the first place.
Certainly, we could clarify the intent of the code by including inline comments, but we can do better.
For example, what if I took that above method and renamed all of my methods and variables to the following?
public static List<String> getMyRoommates( Map<String, Integer> nameToRoomNumber, String myName) { int myRoomNumber = nameToRoomNumber.get(myName); List<String> myRoommates = new List<String>(); for (String name : nameToRoomNumber) { if (!name.equals(myName)) { int theirRoomNumber = nameToRoomNumber.get(name); if (myRoomNumber == theirRoomNumber) { myRoommates.add(name); } } } return myRoommates; }
Suddenly, it's much more understandable what exactly this method is doing.
If you're in CSE 142, you still won't understand what things like List
s
or Map
s are, but at least you now understand that this method will in some
way get all your roommates and have some sense of what each parameter is.
The variable and method names have added a valuable layer of context that allows you to understand the meaning and intent of this code.
Now, while in 90% percent of the cases you should try and write code like this, it's worth talking about the three main exceptions to this rule:
- Loop counters in for loops, such as
i
,j
, andk
. - When the variable has no inherent meaning beyond its type, such as with the Graphics object, the Random object, and with many exam questions. We will explicitly inform you when you run into this case.
- When working with scientific or mathematical formula where single-letter variable names are the norm.
The first exception is with loop counter variable names, such as i
,
j
, and k
. It's ok and encouraged to like this:
public static void printSquare(int size) { for (int i = 0; i < size; i++) { for (int j = 0; j < size; j++) { System.out.print("*"); } System.out.println(); } }
This is partially because loop counters don't really need context, and also because
using the variables i
, j
, and k
as loop counters
is a convention every single programmer will understand, both inside and outside UW.
Of course, you're also free to pick different variable names if you want. For example, if you want to be super explicit, you could rewrite the above code sample as:
public static void printSquare(int size) { for (int lineCount = 0; lineCount < size; lineCount++) { for (int charCount = 0; charCount < size; charCount++) { System.out.print("*"); } System.out.println(); } }
And in fact, if you're still trying to get the hang of for loops, it may be a good idea to write code in this second, more verbose style. That way, it'll be easier for you to keep track of which variable represents what.
The second exception is when you're writing code where there is literally no context
for you to work with. For example, take the Graphics
and Random
objects. There's really no inherent "meaning" behind those objects in the same way the
variables and methods had "meaning" in the getRoommates
example. You could
declare your variable ast Graphics graphics = new Graphics()
if you really
wanted, but it is a bit tiring, so we're ok with single-letter variable names here.
Another example of this is with many exam questions. For example, in CSE 143 a common
midterm question is to ask you to take some stacks and queues and manipulate them.
However, there's no inherent meaning behind those stacks and queues. We're only
interested in the data structure itself. As a result, we'd be ok with you naming those
variables s
and q
.
Because it's often difficult to determine when this second exception applies, we will explicitly tell you when it's ok to use single-letter variable names. If we don't mention anything, you should always assume that full variable names are required.
The final reason is with when working with scientific and mathematical formula, where single-letter variable names are so standard that longer names are unnecessary. You will almost never run into this case while taking CSE 142 and 143.
The only case I can think of is using x
and y
for representing
X and Y coordinates while working with graphics-related code. Note that quantities like
"width" and "height" should not be abbreviated to w
and h
, since
not everybody will recognize that abbreviation.
Java naming conventions
Variables, methods, and fields should be in lowerCamelCase
, classes should
be in UpperCamelCase
, and constants should be in
SCREAMING_CASE
.
-
Variables, methods, and fields should in "camelCase" – each word starts with an uppercase, except the first word.
Examples of good variable, method, and field names include
record
,totalGuesses
,findBiggestFamily
,name
, andnumberOfGuesses
. -
Classes should be in "CamelCase" – each word starts with an uppercase and is placed together with no underscores.
Examples include
String
,Scanner
,Critter
, andLetterInventory
. -
Constants should be in "SCREAMING_CASE" -- each word is completely capitalized and separated by an underscore.
Examples include
DEFAULT_CAPACITY
,INITIAL_NAME
, andHEIGHT
.
In Java, we have several different kinds of "things". We have variables, methods, constants, fields, and classes. In order to avoid getting confused between them, we want to stick by some sort of naming convention so that other programmers can more easily understand what something is supposed to be. However, we also don't want to memorize five different naming conventions, so we simplify and combine some of the cases.
The first thing we want to distinguish between is variables and constants. On the surface, they seem the same (they both represent some value), but semantically, they're completely different – you can tell just from the name. Variables are meant to always be changing, and constants are meant to never change. Historically, constants were denoted using SCREAMING_CASE (to make them really stand out), so Java programmers generally tend to use this convention as well.
Next, we want to distinguish between classes and variables + methods. The reason for this is because if we don't, we could potentially end up writing code like this:
graphics graphics = (new graphics()).graphics();
This doesn't actually compile, and is a bit of a contrived example, but if does show that if we don't have naming conventions, we could potentially end up using a class, variable, and method, all with the same name. Yikes!
graphics graphics = (new graphics()).graphics(); // ^ ^ ^ ^ // type variable creating object method
However, if we make all classes start with uppercase letters, we can salvage this to:
Graphics graphics = (new Graphics()).graphics();
...which, while not perfect, is at least unambiguous.
Then, the final question remaining is why don't we distinguish between methods, fields, and variables? This is because Java lets us use all three of those things in unambiguous ways anyways, so there's no point in adding our own conventions on top of that:
public class ConfusingButUnambiguous { private int foo; public ConfusingButUnambiguous(int foo) { System.out.println(foo); // variable System.out.println(this.foo); // field System.out.println(foo()); // method System.out.println(this.foo()); // another way to call the method } public int foo() { return 3; } }
Here is some more information on using the this
keyword. Note that if you are in CSE 142, this information will not be relevant until
your final homework assignment.
Always include curly brackets
Always include curly brackets, even for one-line if statements or for loops.
Java has a "feature" which allows you to omit curly brackets when writing if statements, for loops, or while loops containing only a single statement. You should never use this feature – always include curly brackets.
More specifically, Java will let you do something like this:
if (something) System.out.println("Look, no curly brackets!");
However, you should ALWAYS include curly brackets and do the following instead:
if (something) { System.out.println("Curly brackets!"); }
The reason for this is because omitting curly brackets increases your chances of writing buggy code. For example, take the following code:
if (getStatus(message) == MESSAGE_VALID_STATUS) turnOnLights();
What this snippit of code is doing is fairly obvious – if the status of the message is valid, it'll turn on lights (perhaps we're coding a robot, who knows).
Now, let's say that several months down the line, either you or your teammate needs to revisit this segment of code so that our robot both turns on the lights and starts driving forward. So then, you modify your code to look like this:
if (getStatus(message) == MESSAGE_VALID_STATUS) turnOnLights(); driveForward();
Is this correct? No!
It may not be obvious because of the indentation, but because we omitted curly brackets, the above snippit of code is actually equal to:
if (getStatus(message) = MESSAGE_VALID_STATUS) { turnOnLights(); } driveForward();
If you don't include curly braces, you will always have to vigilant to make sure you don't accidentally write a similar bug. If you always include braces, you can forget this category of bug exists altogether.
And while this may seem like a silly and basic mistake, this exact same bug actually occurred to Apple in 2014. In their SSL verification code, which is responsible for making sure things like your passwords and banking information stays secure, Apple had the following lines of code:
hashOut.data = hashes + SSL_MD5_DIGEST_LEN; hashOut.length = SSL_SHA1_DIGEST_LEN; if ((err = SSLFreeBuffer(&hashCtx)) != 0) goto fail; if ((err = ReadyHash(&SSLHashSHA1, &hashCtx)) != 0) goto fail; if ((err = SSLHashSHA1.update(&hashCtx, &clientRandom)) != 0) goto fail; if ((err = SSLHashSHA1.update(&hashCtx, &serverRandom)) != 0) goto fail; if ((err = SSLHashSHA1.update(&hashCtx, &signedParams)) != 0) goto fail; goto fail; /* BUG! Code will always goto the failure case */ if ((err = SSLHashSHA1.final(&hashCtx, &hashOut)) != 0) goto fail; err = sslRawVerify(...);
This is in C, so the details are a little different then Java, but essentially it's the same bug. Somebody accidentally copied a line of code on line 13, which falls outside of the if statement and causes the code to skip a few checks and jump immediately to the error handling section of the code, which then returns a success message.
So, that means that under certain conditions, attackers could exploit this to circumvent Apple's security system by skipping those additional checks, completely breaking security. More specifically, it allowed an attacker to capture or modify data protected by SSL/TLS, which includes data like your banking account, social security info, credit card information, etc.
This was a hugely catastrophic bug, caused Apple huge amount of embarrassment, and could have easily been avoided if their engineers had just always used curly braces.
For a more detailed writeup about this bug, see What you need to know about Apple's SSL bug.
Besides formatting, another important aspect of writing code with good style is making sure that the actual flow of logic within your method is clean and sensible.
Our guidelines regarding control flow have less to do with how your code is visually formatted, but more on how it's internally structured and designed. Unlike our formatting rules, which are sometimes specific to Java, our rules regarding control flow will typically be applicable to most mainstream programming languages.
Fundamentally, the reasons why we want a clean flow of logic inside our method harks back to the set of fundamental principles of style. If your code flows cleanly, it is much more understandable, making it more readable. The more easily you can understand code, the more easily you can refactor it to make your code more efficient and concise. And finally, if your flow is clean, it's easier to see where distinct tasks start and end, helping making your code more modular and reusable.
Variable type
Choose the type which is most appropriate for the data you are trying to represent. Use
an int
when you only need whole numbers, doubles
when you need
decimal precision, booleans
for when you need a binary true or false value,
Strings
for arbitrary text, and so forth.
Use int
when you only need whole numbers. Use double
when you
need decimal precision. For example:
double numCreditsTaken = 15; // should be a number int scorePercentage = 86; // we probably want decimals
A corrected version would look like so:
int numCreditsTaken = 15; double scorePercentage = 86.2;
If you want an int
to be a double
so that you don't have to
worry about issues with integer division, you should use either multiply your number by 1.0
or use casting, rather then making a separate variable.
public static void foo(int a, int b) { double tempA = a; double tempB = b; return tempA / tempB; }
The corrected version:
public static void foo(int a, int b) { return 1.0 * a / b; // OR // return (double) a / b; }
Don't try and shoehorn variables into something they aren't. For example, don't try and
use a String
or an int
to represent true/false values – that's what
boolean
s are for.
Constants and magic numbers
Use constants whenever you want to define a single value that won't change during the
execution of your program. In particular, whenever you have an arbitrary number or value in
your code (a "magic value"), you should use a constant instead. Declare all constants to be
public static final
.
All constants should take the following form:
public static final int CONSTANT_NAME = 3;
Notice that the constant is always declared public, static, and final, and that the constant name is always comprised of all uppercase letters with underscores separating words.
Whenever you have any sort of arbitrary number or value hard-coded in your code, you should almost always replace them with a constant. We call those hard-coded numbers "magic values" because their intended purpose is mysterious, like magic. Constants allow us to take those mysterious values and give them a name to help make the intent of our code more clear.
You should make sure to always remember the static
and final
keywords, and remember to capitalize the name of the constant. None of the following would
be acceptable:
public static int SOME_CONSTANT = 3; public final int OTHER_CONSTANT = 4; public static final int anotherConstant = 5;
If you are working with an object (for example, when working on the Critters assignment for CSE 142 or when working on any of the homework assignments for CSE 143), you are permitted to also make your constants private if you think it makes sense to do so. However, we encourage you to just keep all your constants public. Since they can't be modified, there's no harm in letting the client view them, and it usually makes sense to allow the client to see what our "default" behavior is likely to be.
Variable phobia
Make liberal use of variables, especially to eliminate redundant and repeated expressions.
When you have identical expressions that are repeated throughout your method, you should strongly consider using variables to help eliminate that redundancy and to help make your code more concise.
For example, take the following:
public static void drawPixelX( Graphics g, int startX, int startY, int size, int pixelSize) { for (int i = 0; i < size; i++) { g.drawRect( startX + i * pixelSize, startY + i * pixelSize, pixelSize, pixelSize); g.drawRect( startX + (size + 1) * pixelSize - i * pixelSize, startY + i * pixelSize, pixelSize, pixelSize); } }
While this method works correctly (it draws a pixelated "X"), it also has several repeated expressions that could be refactored out.
For example, the expression startY + i * pixelSize
is repeated twice,
the expression i * pixelSize
is repeated four times...
We can also refactor out the expression (size + 1) * pixelSize
. Although
that expression isn't redundant, it does make that particular line longer.
Whenever you see these sorts of repeated expressions, you should refactor them out. For example:
public static void drawPixelX( Graphics g, int startX, int startY, int size, int pixelSize) { int maxWidth = (size + 1) * pixelSize; for (int i = 0; i < size; i++) { int step = i * pixelSize; int currentY = startY + step; g.drawRect(startX + step, currentY, pixelSize, pixelSize); g.drawRect(startX + maxWidth - step, currentY pixelSize, pixelSize); } }
Now, our code is more concise. It's also more readable – by putting complex expressions into variables and giving them distinct names, it's easier for somebody to understand what each equation means.
On looping
Java lets you "loop" through values in several different ways. Though many of these looping constructs are, to a certain extent, mechnically interchangable, they each have a different meaning and use. Which looping construct you use will imply multiple things about your code. Use the one that best fits your overarching intent.
For loops
Use a for
loops when you know exactly how many times you need to loop
before entering the loop. Do not use loops when you end up always looping only once.
Another way of thinking about for loops is to consider them as finite or definite loops – you know precisely how many loops you need.
For example, say you wanted to print out a square of side length n
out to the
console. This is a perfect example of when to use a for loop:
public static void printSquare(int n) { for (int i = 0; i < n; i++) { for (int j = 0; j < n; j++) { System.out.print('*'); } System.out.println(); } }
We know before starting the loop exactly how many times we need to repeat the operation:
exactly n
times.
Contrast this to the behavior of while loops, which is covered in the following subsection.
While loops
Use a while
loops when you do NOT know exactly how many times you need
to loop, and can only find out in the middle of looping – when you need a potentially
infinite or indefinite loop.
An example of when you would use a while loop is if you want to perform some operation on every line in a file. Because we have no way of knowing how many lines are in a file and therefore how many times we need to loop before opening it, you should use a while loop:
public static void printLines(Scanner input) { int lineNumber = 0; while (input.hasNextLine()) { lineNumber += 1; String line = input.nextLine(); System.out.println("Line " + lineNumber + ": " + line); } }
Do note that we could theoretically abuse for loops and accomplish the exact same thing as the above using for loops in Java:
public static void printLines(Scanner input) { for (int lineNumber = 1; input.hasNextLine(); lineNumber++) { String line = input.nextLine(); System.out.println("Line " + lineNumber + ": " + line); } }
Although this is shorter, it's not good style because it uses for loops for something it was not semantically meant to do, which confuses the underlying intent of your code. The only reason you could do this is because Java decided to design their for loop syntax in such a way that it gives you a little too much flexibility. For loops do not necessarily work in the same way in other programming languages, so it's good to not get into the habit of relying on this particular quirk.
Do-while loops
We do not expect you to know about or use do-while
loops in CSE 14x –
consider them optional material. You may use do-while
loops once we have taught
you about regular while
loops, if you wish.
Due to time constraints, we typically do not have time to teach you what a
do-while
loop is, or how it works. If you're curious to learn more, chapter 5.1 in
your textbook should contain more information.
Because a do-while
loop shares many similarities with a regular while
loop, we do permit you to use them once we have formally introduced what a while
loop is.
Single-iteration loops
You should never write a loop (for
loop or while
loop) that
will either never run, or will always runs exactly once. If a loop will never run, that
loop should be deleted entirely. If a loop always runs once, you don't really need a loop
to begin with.
You should never write a loop that look this:
for (int i = 0; i < 0; i++) { System.out.println("foo"); }
Why? The loop will never actually run, so the code can just be completely deleted.
Similarly, never write a loop that looks like this:
for (int i = 0; i < 1; i++) { System.out.println("foo"); }
This loop will only ever run once, so we can get rid of the loop and rewrite the code to look like this:
System.out.println("foo");
Sometimes, it's not necessarily entirely obvious when a loop never runs or runs only once. For example, take the following:
int n = // ... for (int i = 3; i < (n + 1) / n; i++) { System.out.println("foo"); }
In this case, it turns out that the expression (n + 1) / n
can only be
one of three values: 0 when n < 0
, 2 when n == 1
, and
1 when n > 1
. (The code throws a DivideByZeroException
if
n == 0
.)
Since we always start i
at 3, that means that the condition will always
be false. That means that the loop will never run, and we can delete this loop
completely.
These types of edge cases, where it's not obvious how many times a loop will run, are rare, but is still worth watching out for.
Special-casing specific iterations
Likewise, you should avoid structuring the contents of a loop so that a segment
of it runs only during a specific iteration. For example, avoid adding in an
if
statement into a loop that will always run only during the first iteration,
or only during the second and third.
In general, you should always try and write loops where their content is "generalized".
That is, avoid adding in if statements
that will run only during specific
iterations of the loop – it's usually an indication that your code could have
been structured more elegantly.
For example, the following code snippit is bad style:
for (int i = 0; i < 10; i++) { System.out.print("-"); if (i == 4) { System.out.print("*"); } }
What this code snippit does is print out a star surrounded by five dashes to either
side: -----*-----
.
The reason why this code snippit is bad style is because the way the loop is structured doesn't really make the "symmetry" of our output very clear. It's also bad because we ended up hard-coding a specific case into our loop. The entire point of a loop is to repeat a certain task a given number of times, and it's inelegant to write a loop that repeats a certain task a set number of times, except in one specific, special case.
A better way of writing the above code snippit would be like so:
for (int i = 0; i < 5; i++) { System.out.print("-"); } System.out.print("*"); for (int i = 0; i < 5; i++) { System.out.print("-"); }
When we look at this improved version, we can immediately see that there's some redundancy. Once we factor that out, we get our final, best version:
drawLine(); System.out.print("*"); drawLine(); // ...snip... public static void drawLine() { for (int i = 0; i < 5; i++) { System.out.print("-"); } }
As another example, avoid writing a loop that does something special on the first or last iteration. In those cases, you're usually able to "shorten" the loop and pull the first or last case outside of the loop, making the intent of your code more clear.
Of course, that isn't to say that it's bad to have if
statements
and such inside of your loops. It's perfectly fine and encouraged to combine if
statements and loops. For example, the following would be perfectly acceptable and ok:
public static void printPrimeNumbers(int start, int end) { for (int i = start; i < end; i++) { if (isPrime(i)) { System.out.print(i + " "); } } } public static boolean isPrime(int number) { // ... }
Because this if
statement could potentially trigger multiple times while
our loop runs + isn't targeted towards a single iteration, this loop would be considered
good style.
Break and continue
Do NOT use break
or continue
statements.
Note: If you don't know what break
and
continue
are, that's fine. We deliberately do not teach you about them, but
some students find out anyways.
While there's nothing inherently bad with break
or continue
,
newer programmers often have a habit of misusing them as a shortcut or crutch to avoid
writing clean code. As a result, we strongly discourage the use of break
and
continue
statements on assignments.
We're also careful to structure our assignments and exam problems so that you never
need to use them (and so that the cleanest solution doesn't use them at all).
So, if you find yourself wishing that you could use break
or
continue
, it's typically a sign that there's a better way to do whatever you
want to do.
If you think you've found a situation where you genuinely think using break
or continue
is the right solution, it would be a very good idea to go talk to
Adam or your TA to get confirmation.
Branching semantics
Java allows you to combine if/else statements and branch in several different ways. Select the method that best suits what kind of branch you want to execute. Expand for more details.
More precisely, you should:
- Use
if / if
if each branch is independent: any, all, or none of the if statements could execute - Use
if / else if
if either zero or exactly one of the statement bodies will execute. - Use
if / else
if each branch is mutually exclusive: exactly one of the statement bodies will execute.
As an example, let's say we want to write a method which takes in a gpa and returns the corresponding letter grade. We have multiple different valid ways we could implement this method. We could use independent if statements:
// version A: multiple independent if statements public static String getLetterGrade(double gpa) { if (gpa > 3.5) { return "A"; } if (gpa > 2.5) { return "B"; } if (gpa > 1.8) { return "C"; } if (gpa > 0.7) { return "D"; } return "F"; }
Or we could use else if statements with no attached else:
// version B: if / else-if, no else public static String getLetterGrade(double gpa) { if (gpa > 3.5) { return "A"; } else if (gpa > 2.5) { return "B"; } else if (gpa > 1.8) { return "C"; } else if (gpa > 0.7) { return "D"; } return "F"; }
Or we could use if/else:
// version C: if / else public static String getLetterGrade(double gpa) { if (gpa > 3.5) { return "A"; } else if (gpa > 2.5) { return "B"; } else if (gpa > 1.8) { return "C"; } else if (gpa > 0.7) { return "D"; } else { return "F"; } }
Which of these three versions are preferred?
Well, simply from a mechanical standpoint, it doesn't make a huge difference: Java will end up doing the exact same thing in all three cases. So, the deciding factor really comes down to the underlying intent of our code. What do we intend to mean? Do we intend to mean that these checks are independent? That the first couple are exclusive but the last statement isn't? That every statement is mutually exclusive?
We probably mean the latter: you can only have one letter grade, after all. Therefore, version C would be the best in this case.
Let's consider another example. Let's say we have a method where want to calculate the resistance of two resistors that are placed in parallel, but want to return early if either of the resistors has a resistivity of zero ohms. Well, again, we have a dilemma:
// version A public double resistivity(double r1, double r2) { if (r1 == 0) { return 0; } if (r2 == 0) { return 0; } double inverse = 1 / r1 + 1 / r2; return 1 / inverse; } // version B public double resistivity(double r1, double r2) { if (r1 == 0) { return 0; } else if (r2 == 0) { return 0; } double inverse = 1 / r1 + 1 / r2; return 1 / inverse; } // version C public double resistivity(double r1, double r2) { if (r1 == 0) { return 0; } else if (r2 == 0) { return 0; } else { double inverse = 1 / r1 + 1 / r2; return 1 / inverse; } }
If we ignore the fact that we probably should be combining those two initial if statements into a single one, which version is best?
Well, in this case, those two initial checks are really independent from each other. Either of those two variables could be equal to zero at any given time, so version A would probably be best. Unlike the previous example where each branch was "equal", in this version the first two branches are merely "fail-early" branches that look for some specific criteria and terminate early if it looks like the method is about to fail. (In this case, we would have ended up dividing by zero if it weren't for those two checks).
Factor out common code
If you have common code at the beginning or end of a conditional, you should pull that out so you don't end up duplicating the code and end up with redundancy.
When writing if statements, be careful to make sure that you don't have redundancy between multiple branches.
For example, this would be bad style due to the redundancy:
if (x % 2 == 0) { int half = x / 2; System.out.println("half of x is " + half); System.out.println("x was even"); } else { int half = x / 2; System.out.println("half of x is " + half); System.out.println("x was odd"); }
Notice that the first two lines of both branches are doing the exact same things: lines 2-3 are the same as 6-7. While we could fix this by pulling those lines into a helper method, a better alternative would be to pull them out of the if statement entirely. A fixed version would look like this:
int half = x / 2; System.out.println("half of x is " + half); if (x % 2 == 0) { System.out.println("x was even"); } else { System.out.println("x was odd"); }
Typically, this sort of redundancy will take place at during the first few lines of your branches, or at the last few lines. For example, lines 3 and 6 are redundant in the following example:
if (x > 10) { x = x % 10; System.out.println("Result: " + x); } else { x = x * 2; System.out.println("Result: " + x); }
We would want to fix that to look like this:
if (x > 10) { x = x % 10; } else { x = x * 2; } System.out.println("Result: " + x);
Unnecessary tests
Avoid unnecessary tests. If you already know that something is true, then you shouldn't bother to have a test for it. Such tests just make your code longer and harder to understand.
As an example, let's take our GPA to grade method from earlier. The following would be a bad way of implementing it:
// version C: if / else public static String getLetterGrade(double gpa) { if (gpa > 3.5) { return "A"; } else if (gpa <= 3.5 && gpa > 2.5) { return "B"; } else if (gpa <= 2.5 && gpa > 1.8) { return "C"; } else if (gpa <= 1.8 && gpa > 0.7) { return "D"; } else { return "F"; } }
Remember, the moment Java finds a true conditional, it will execute that branch and will
not continue to check the other branches. More specifically, what Java will do is literally
run the checks on lines 3, 5, 7, and 9 in precisely that order. If it turns out that the
check on line 5 is the first one to evaluate to true, Java will immediately run that branch
and ignore checks on lines 7 and 9 (and the last else
statement).
We can, and should, exploit this sequential property of if statements to write a cleaner version of our code. The cleaner version will look like this:
public static String getLetterGrade(double gpa) { if (gpa > 3.5) { return "A"; } else if (gpa > 2.5) { return "B"; } else if (gpa > 1.8) { return "C"; } else if (gpa > 0.7) { return "D"; } else { return "F"; } }
So, as an example, we would not need to check if the GPA is greater then 3.5 on line 5. If that were true, the first branch would have already triggered, making that specific test in the second branch unnecessary.
Empty blocks
You should never have empty blocks in your code – never have at set of curly braces that contain nothing inside.
Let's say that we want to write a method that prints something out if some condition is NOT true. A naive way of writing this might be like this:
if (myVar > 0) {
} else {
System.out.println("Error: value is negative.");
}
While this works, it's very messy. That empty block on lines 1-3 is very icky, and should be removed entirely. (It's sort of like an unnecessary appendage, just sort of dangling there).
We can do this by negating the condition, and swapping the two blocks. The
negation of myVar > 0
is !(myVar > 0)
, or more
concisely, myVar <= 0
.
After performing that inversion, our code now looks like this:
if (myVar <= 0) {
System.out.println("Error: value is negative.");
} else {
}
This is better, but still bad since we still have an empty block. Thankfully, the fix in
this case is pretty simple – remove the else
block entirely.
if (myVar <= 0) {
System.out.println("Error: value is negative.");
}
Switch-case
We do not expect you to know about or use switch-case in any of your assignments and exams. We don't explicitly forbid you from using it, but we strongly encourage you to use if/else statements instead.
Do not compare a value to true
You should never need to compare a boolean against another boolean – just directly use the boolean itself. This is because comparing a boolean against a boolean will produce yet another boolean, which is redundant and needless.
We call doing this sort of comparison bad boolean zen.
Consider the following code:
if (number < 0 == true) { System.out.println("The number is negative."); }
While at first glance, this code snippit seems reasonable, it actually contains some
unncessary code. Specifically, notice that the == true
part is unnecessary.
If we remove it, the if statement will do the exact same thing:
if (number < 0) { System.out.println("The number is negative."); }
Therefore, since we can remove it, we should. After all, if we're comparing some value
to true
, that means that the value must already be a boolean to begin with.
In that case, just use the value directly. Don't beat around the bush.
A good rule of thumb is that you should never type the characters
== true
in Java. Whenever you see that code snippit, it can almost always
be deleted with zero impact on the code.
Use the negation "!" operator instead of comparing to false
As an extension of the previous rule, you should never do value == false
or value != true
– this would also count as bad boolean zen. Instead, do
!value
instead.
The reason why you should always prefer using the negation !
operator is
because it's more concise and reads better.
It also makes it unambigously clear that we're flipping the boolean sign of the value.
Be careful when returning or setting a boolean
When writing a method that returns a boolean be very careful to make sure that your
code has good boolean zen. If you need an if statement to decide if something should be
true
or false
, you can often simplify away the entire if
statement.
We see this often with boolean methods:
public boolean isNegative(int number) { if (number < 0) { return true; } else { return false; } }
Since the condition must already be either true
or false
,
we can simplify the code to the following:
public boolean isNegative(int number) { return number < 0; }
(Note that this method is also a trivial method and probably shouldn't exist in the first place).
Similarly, you should be careful when setting a boolean value. Don't do this:
boolean isNegative; if (number < 0) { isNegative = true; } else { isNegative = false; }
Instead, do this:
boolean isNegative = number < 0;
Foreach loops
Use a foreach
loop when you want to iterate over some sort of collection such
as an array or a list, and work on the collection one element at a time. Prefer using
foreach
loops over for
loops when possible.
Note: Foreach loops are CSE 143-only material.
One way of thinking about foreach loops is that they're a more specific kind of for loops geared specifically towards iterating over collections or streams. As it turns out, this is a really common sort of thing to do – there are even some programming languages which have eliminated for loops entirely in favor of foreach loops – namely, Python.
A good example of when to use a foreach loop is when you want to iterate over a list:
public static int sum(List<Integer> numbers) { int total = 0; for (int number : numbers) { total += number; } return total; }
Contrast this to using regular for loops:
public static int sum(List<Integer> numbers) { int total = 0; for (int i = 0; i < numbers.size(); i++) { int number = numbers.get(i); total += number; } return total; }
In comparison to the first version, this is much more verbose and clunky – foreach loops are just more elegant in general.
The primary exception to this rule is when you need both the loop counter and want to iterate over a collection. In this case, it'd probably be better to just default to using a regular for loop since we need the counter anyways:
public static void inspect(List<Integer> numbers) { for (int i = 0; i < numbers.size(); i++) { int number = numbers.get(i); System.out.println("arr[" + i + "] => " + number); } }
There are some programming languages which allow you to neatly handle this case and still use a foreach loop, but alas Java is not one of them so we must compromise.
The other exception is if you want to modify a collection while iterating over it, which is covered in the following section.
Iterators
Use iterators when you want to use a foreach
loop, but also want to
simultaneously modify the collection you are iterating over. Prefer using foreach
loops whenever possible.
Note: Iterators are CSE 143-only material.
Iterators should primarily be used for the special case when we need to iterate over a collection while modifying it by either adding or removing elements. While you might think that a foreach loop would be a good choice here, Java will actually throw a ConcurrentModificationException
if you try.
So, we are forced to use iterators out of necessity. For example:
public static void removeBadCandies(List<String> candies) { Iterator<String> iter = candies.iterator(); while (iter.hasNext()) { String candy = iter.next(); if (candy.startsWith("rotten")) { iter.remove(); } } }
Sometimes, you will find that it's nicer to use a regular for loop in this particular case. If so, feel free to do so.
Also note that foreach loops are actually internally using iterators, except in a nicer syntax – you don't lose anything by switching to foreach loops if it's possible to do so.
Recursion
Use recursion when none of the above appear to be a good fit when looping, and when simple iteration will not suffice. For example, recursion is often a good fit when dealing with branching data structures such as trees.
Note: Recursion is a CSE 143-only topic.
Recursion is a complex topic which we don't have room to discuss here – it's mostly included for completeness since it is possible to solve any problem involving iteration using recursion. In general, you should not be using recursion as a replacement for any of the above constructs, unless explicitly told otherwise or unless you feel doing so would result in cleaner or more efficient code.
Higher-order functions
Do not use higher-order functions (streams, lambdas, map, etc).
Java includes a few more constructs which can be used for looping and iteration as of Java 8 – namely, lambdas, streams, and higher-order functions. While those constructs are definitely useful and interesting, they will not be covered in CSE 142 and 143 and will constitute advanced material.
Checked vs unchecked exceptions
If you are writing a method that could potentially throw an exception, you should only include that exception as a part of the method header if your code will not compile if you omit it.
In Jaava, there are two different kinds of exceptions – checked exceptions, which you need to specify in the method header, and unchecked exceptions, which you don't.
Checked exceptions are meant to be reserved for errors that is beyond the immediate
control of the implementor classes. For example, let's say that a method is trying to
open a file. If the file itself is missing, and you expected it to be there, then there's
typically not much you can do about it, and the only thing you can do is throw a
FileNotFound
exception. It's important to inform the client of this so they can
handle this sort of error correctly. For example, they could try providing another file,
ask the user to fix the error, provide some default value, etc...
As a consequence, Java wants all checked exceptions to be a part of the public contract and interface, and wants us to declare are method headers as so:
public static void inspectFiles(String pathToFile) throws FileNotFoundException { // ... }
In contrast, unchecked exceptions like IllegalArgumentException
could happen
with pretty much any method, so there's no point in any method. An
IllegalArgumentException
could be thrown from pretty much any method that takes
in arguments and typically indicates that there's a bug somewhere, meaning that the
correct thing for the client to do is to just fix their code to avoid throwing the
exception in the first place. If a file is missing, it's not necessarily the program's
fault, but if the arguments are bad, then it's probably the programmer's fault.
Or so the theory goes – in practice, the difference between what constitutes a checked exception and an unchecked exception can be somewhat arbitrary, and many programming languages have actually removed the concept of having checked exceptions completely. One major problem with checked exceptions is that once you introduce one in a method, any other method that calls that first method will also need to add that checked exception to their method header, which will potentially require you to make a cascading series of changes to your code (which is frankly tedious) or suppress it using try/catch (which we don't teach in 142 or 143, and so you shouldn't be doing).
You may have noticed this problem yourself – the moment you instantiate an
instance of the File
object, you suddenly have to add throws
FileNotFoundException
to that method, any methods that call that method, etc. all
the way up until you hit your main
method. In theory, this is a good thing
because you're being explicit about what could go wrong, but it can often become a major
pain. If you change how your method is implemented so that it decides to open a file,
you're also forcing your client to make changes.
In fact, there are some people who dislike checked exceptions so much that they actually install a 3rd party library which sort of patches or extends the compiler to get rid of them altogether, which is pretty crazy – see Project Lombok's Sneaky Throws. (Of course, it goes without saying that you should never try and do something like this in CSE 142 or 143, and it's highly debatable whether or not it's a good idea to use something like this in general, but I'd figure I'd mention this for the sake of completeness).
Now pragmatically, what does this mean about style? Well, when writing code, you should just assume and hope that all exceptions are unchecked, and write your method headers normally like this:
public static void foo(int whatever) { // ... }
And only add the exception to the method header if Java complains about it and refuses to compile. Why do extra work if we don't need to?
Exception checks and if statements
Exception checks should always be placed into individual if statements with no
attached else
statements.
Because each exception check that your method performs is completely independent from each other, you should use separate if-statements like so:
public void myMethod(int a, int b) { if (a == b) { throw new IllegalArgumentExceptions("Params should not be equal"); } if (this.myField.length() > b) { throw new IllegalStateException("Internal data has grown too large"); } System.out.println("Rest of method goes here"); }
In contrast, the following version is incorrect – by using else-if, you're implying the exception checks are related and mutually exclusive, which is inaccurate – both of those conditions could actually be false:
public void myMethod(int a, int b) { if (a == b) { throw new IllegalArgumentExceptions("Params should not be equal"); } else if (this.myField.length() > b) { throw new IllegalStateException("Internal data has grown too large"); } System.out.println("Rest of method goes here"); }
Likewise, placing the body of the method into an else
branch would also be
incorrect for the same reason: it implies that each of the three options are "equal" in
rank, and are mutually exclusive:
public void myMethod(int a, int b) { if (a == b) { throw new IllegalArgumentExceptions("Params should not be equal"); } else if (this.myField.length() > b) { throw new IllegalStateException("Internal data has grown too large"); } else { System.out.println("Rest of method goes here"); } }
Fail early
You should perform your exception checks as soon as possible – move them to as close to the top of a given method as possible. Think of your exception checks as gatekeepers that prevent corrupt data from entering your method. You want gatekeepers to be stationed close to the gate, not in the middle of your castle.
You should place your exception checks as early as possible. For example:
public void add(String item) { item = item.lower(); if (item.isEmpty()) { throw new IllegalArgumentException("'item' should not be an empty string"); } // etc }
The exception check does not rely on the string being lowercase, so it's safe to swap the two.
A corrected version would look like this:
public void add(String item) { if (item.isEmpty()) { throw new IllegalArgumentException("'item' should not be an empty string"); } item = item.lower(); // etc }
Sometimes, you'll find that your exception check cannot go literally at the top of your method. That's fine, as long as you make an attempt. For example:
public void sumOfGrades() { int sum = 0; for (int i = 0; i < size; i++) { if (this.data[i] < 0) { throw new IllegalStateException("Cannot have a negative grade"); } } return sum; }
Custom error messages
Although this is not strictly speaking required, you should always include a custom error message for every exception you throw.
For example, take the following snippit, which is missing a custom error message:
if (age < 21) { throw new IllegalArgumentException(); }
While we do permit code like this, we discourage it because when you try and run this, and when this exception is triggered, all the client will see is something like this:
Exception in thread "main" java.lang.IllegalArgumentException at EventPlanner$GroceryStore.buyAlcohol(EventPlanner.java:140) at EventPlanner.purchaseSupplies(EventPlanner.java:30) at EventPlanner.main(EventPlanner.java:8)
...which is not particularly helpful. All the client can tell is that the class threw an IllegalArgumentException of some kind, which is not particularly helpful when debugging since it forces them to either have to read your comments or dig through your source code, which is extra work for them.
Instead, it's better to be descriptive and provide a detailed error message such as this:
if (age < 21) { throw new IllegalArgumentException( "Cannot purchase alcohol if age < 21; currently age = " + age); }
This way, when this exception is thrown, you get a stack trace which looks like this:
Exception in thread "main" java.lang.IllegalArgumentException: Cannot purchase alcohol if age < 21; currently age = 19 at EventPlanner$GroceryStore.buyAlcohol(EventPlanner.java:140) at EventPlanner.purchaseSupplies(EventPlanner.java:30) at EventPlanner.main(EventPlanner.java:8)
This is much more helpful – we can immediately identify what the problem is and how to fix it just from the stack trace alone.
As the problems and the programs you write grow increasingly complicated, you'll soon find that trying to cram all of your logic and code into a single method will result in very nasty and difficult-to-work with code.
To prevent that from happening, it's valuable to understand how to go about decomposing and breaking apart a problem into smaller pieces and components, how to use things like methods to help arrange, structure, and organize your code on a broad scale, and how to divide responsibility between the different pieces of your program.
Programs almost never exist alone in isolation.
The code we write is built up on top of countless smaller classes and programs, and the code we write in turn will be used by larger programs to help form solutions to even bigger problems and tasks. This core idea of taking small pieces and combining them to form larger solutions is a fundamental part of programming, and in fact, with science and engineering in general.
On the micro scale, we work with a very small and basic set of commands that the computer can understand – if statements, loops, variables, and so forth. And even those commands are a product of multiple smaller commands. A computer, after all, must fundamentally work with electricity and physics, with ones and zeros, not if statements and loops.
On the other end of the spectrum, on the macro scale, you'll find that as you move on to higher level courses, or to academia, or into industry, you'll often have to work with many other programmers and create programs made up of hundreds of thousands of separate classes and methods working in concert. This can seem a little intimidating, and it is. How do programmers manage to deal with and write programs of such staggering complexity? How do you get anything done without your code devolving into a chaotic mess and collapsing under its own weight?
The solution is to learn how to write code which is modular and composable, to learn how to decompose problems into their component parts, and learn how to divide responsibility between those different components. We want to be able to write code in such a way that distinct portions are kept isolated from one another, and changing one segment of the code won't result in a cascade of changes throughout your codebase.
Learning how to effectively do this is something that can only come with practice and experience, and so is a major focus of this course. Unlike the previous sections in this style guide, which gave you fairly straightforward rules to follow, this segment of the style guide is going to naturally be much more conceptual and as a result somewhat ambiguous.
At most, we can only really give you some rules of thumb and heuristics that will help you design your program. It's up to you to take the rules we've outlined here, reconcile them with the realities of the problem you're trying to solve and the four fundamental principles of style, and figure out just what it is you need to do.
What is redundancy?
You should aim to eliminate as much logical redundancy from your code as possible. If you need to repeat a task, don't copy-and-paste the code. Instead, take the common lines and refactor them into a helper method.
A large part of this class and of your grade will be oriented around being able to detect and eliminate redundancy.
When writing code, we want to reuse much of the old code we've written as possible. We shouldn't have to constantly re-invent the wheel and rewrite the same code over and over, nor should we take old code and copy-and-paste it everywhere we need it. After all, having to both copy and maintain copy-pasted code is tedious. If we were to copy-and-paste, and later discover a bug in our code, we'd suddenly have to go back everywhere we copied-and-pasted and fix every instance of the bug which is boring and error-prone.
We call repeated code redundant code, and a large part of this class is oriented around being able to detect and eliminate redundancy.
As an example, consider the following code:
// Produces the following output: // 171717171717 // 444444444 // 00000000 public static void main(String[] args) { for (int i = 1; i <= 6; i++) { System.out.print(17); } System.out.println(); for (int i = 1; i <= 9; i++) { System.out.print(4); } System.out.println(); for (int i = 1; i <= 8; i++) { System.out.print(0); } System.out.println(); }
If we look at the expected output, we can see that there's a definite pattern and a fair bit of redundancy: we print a value a certain number of times. We consider this sort of repetition redundant and not very concise. The code in the above sample also happens to look very similar to each other, though that is not always the case – code does not need to look similar to be considered redundant. Consider:
// Produces the same output as above public static void main(String[] args) { System.out.println(171717171717); for (int i = 1; i <= 9; i++) { System.out.print(4); } System.out.println(); int i = 0; while (i < 8) { System.out.print(0); } System.out.println(); }
While this code snippit doesn't really have "repeated code," it should be clear that both blocks are redundant.
We can improve on both versions by making use of methods to eliminate redundancy like so:
public static void main(String[] args) { printNumber(4, 9); printNumber(17, 6); printNumber(0, 8); } public static void printNumber(int number, int count) { for (int i = 1; i <= count; i++) { System.out.print(number); } System.out.println(); }
This code is pretty simple, but illustrates our goal when dealing with redundancy. In many programs, we have tasks that we need to perform more than once – if there is such a task, it's important to make sure we don't write the same code for it twice.
This is usually more straightforward in 142, but as code gets more complex, we occasionally have two code blocks that are similar enough to seem redundant, but different enough to make us pause before creating a generalized function to handle both cases.
In these cases, it's helpful to think about your methods not in terms of their similar
code, but in terms of their purpose – if the code blocks are performing the same
task, such as printNumbers
above, it's probably a good idea to reduce the code.
However, if the code blocks are performing fundamentally different tasks, you should
pause before creating a generalized method. A method should stand alone and have a single
purpose.
Detecting redundancy
Sometimes, it can be hard to tell when something is worth refactoring or not. If you
have 6-8 identical lines of code, it's pretty obvious that should be refactored, but what
about 1-2 lines of code? What about expressions like 'a' + 1
? Where does the
boundary lie?
When considering this sort of question, you should ask yourself how many "operations" you'll end up saving if you refactor something into a method. If you end up with a net gain after refactoring something, it was probably the correct decision.
For example, let's say that I have the following method:
public static void printTree() { System.out.println(" ** "); System.out.println(" **** "); System.out.println("******"); System.out.println(" ** "); System.out.println(" **** "); System.out.println("******"); }
The redundancy in this method is pretty obvious: we're repeat the exact same set of printlns twice. Now, let's say we refactor that:
public static void printTree() { printTriangle(); printTriangle(); } public static void printTriangle() { System.out.println(" ** "); System.out.println(" **** "); System.out.println("******"); }
If we take a look at our original method, we were in essence performing exactly six logical operations: we had six printlns.
Now, if we look at our newly refactored printTree
method, it looks as if we've
simplified it down to just two operations: two method calls.
Of course, in the end, we still need to run six println statements, but within the
context of printTree
we've turned six "operations" into just two, which is a net
improvement. As a result, we know that this was a good refactoring to have
performed.
What if instead we tried refactoring printTree
to look like this?
public static void printTree() { myPrintln(" ** "); myPrintln(" **** "); myPrintln("******"); myPrintln(" ** "); myPrintln(" **** "); myPrintln("******"); } public static void myPrintln(String line) { System.out.println(line); }
In this case, we didn't end up reducing the logical redundancy at all. Our
printTree
method started off with six operations, and after our refactoring, it
still performs six operations. The method didn't end up getting any simpler, and we ended
up gaining a method in the process, which is a slight net loss.
We may have less redundancy if we look at the amount of literal characters that make up our code, but that sort of redundancy is incidental to what the code is actually logically doing.
Let's take another example: the Pythagorean Theorem. It looks something like this:
double sideA = 3; double sideB = 4; double hyp = Math.sqrt(sideA * sideA + sideB * sideB);
Ignoring the variable assignment, this would be about four "operations"
– we call Math.sqrt
once, we multiply two times, and we add once.
Now, let's replace this with a method call:
double sideA = 3; double sideB = 4; double hyp = pythagorean(sideA, sideB);
This is a net gain for us – we've turned four operations into just a single one again. And if we're repeating the same calculation all over our code, we've just simplified every single one of them down into a single operation.
We can even be pseudo-mathematical if we want – let's say we want to compute the hypotenuse 10 times throughout our code. That means before refactoring, we had about 40 operations scattered throughout our code in order to compute the answer. After refactoring it into a method, we call the method 10 times and have the 4 operations inside the method – we're now performing about 14 operations in total. We've saved 26 operations in total, which is fantastic.
Of course, counting the number of "operations" is a bit of a fuzzy and subjective metric. It's also sort of pointless in the sense that the computer will run 40 operations regardless or not if we refactor. Reducing the number of operations is mostly something we do for the sake of readability and concision, and so you should use the number of operation simply as a rule of thumb to help you refactor. Eventually, you'll get to the point where you do not have to explicitly compute some arbitrary number and will instinctively feel when it's appropriate to refactor.
Now, let's take this from a different perspective. Let's say that we have the following code:
int a = 1; int b = 2; int c = a + b;
Setting aside the variable assignment, we have only a single operation – the addition. We could refactor this into a method, and do something like this:
int a = 1; int b = 2; int c = add(a, b);
...but that was pretty pointless – we're still doing only one operation, so never really reduced the complexity at all. We might be doing addition all over the place, but replacing every instance of addition with our `add` method doesn't simplify the code the same way it did with the Pythagorean Theorem algorithm. There's no net gain, and using `add` methods would most likely obfuscate and confuse the code.
Now, on rare occasion, it might still be good to make a method to wrap around a single operation, especially if that operation is really confusing -- see the section on trivial methods for more discussion on this topic.
However, this is really something you need to evaluate on a case-by-case basis. Would sticking your code into a method reduce the number of "operations", reducing the redundancy? And if not, is there some other technique you could be using instead? And finally, does the advantages in your case of sticking a single operation in a method cancel out the badness of having a trivial method with only one operation?
It really depends, and being able to determine when it's appropriate and when it's not appropriate to pull a chunk of code into a separate method is an important part of what we try and teach in CSE 142 and 143.
Use methods to help provide structure and organization
Besides using methods to help reduce redundancy, you should also use methods to help structure and organize your code into distinct sections to enhance readability.
A good rule of thumb is to try and write methods that contain a single primary responsibility. Try and avoid trying to do too many things at once: use helper methods to divide and delegate subproblems.
More specifically, we are not saying that each method has to be made up of literally a single operation or component – that would be silly. Rather, what we're saying is that your method should be responsible for doing only a single thing, and should delegate everything else to other methods and classes.
For example, take the following method which prints out a staircase, square, and rectangle in succession:
public static void drawArt() { for (int i = 1; i <= 5; i++) { for (int j = 0; j < i; j++) { System.out.print("*"); } System.out.println(); } for (int i = 1; i <= 5; i++) { for (int j = 1; j <= 5; j++) { System.out.print("#"); } System.out.println(); } for (int i = 1; i <= 5; i++) { for (int j = 1; j <= 20; j++) { System.out.print("@"); } System.out.println(); } }
The code is really doing three separate things here: drawing a staircase, drawing a square, and drawing a rectangle. Forcing the method to explicitly handle all three of these things is giving it too much to do. When you're trying to draw art, you shouldn't have to worry how exactly a square is drawn. A better version would look something like this:
public static void drawArt() { drawStaircase(5); drawSquare(5); drawRectangle(5, 20); } // etc
Now, our drawArt
is directly responsible for one and only one thing: drawing
something. Our helper methods have now been placed in charge of drawing a staircase,
drawing a square, and drawing a rectangle respectively.
Note that after completing this refactoring, we haven't decreased the amount of redundancy at all. This is ok – we did end up improving the overall readability, so we've experienced a net gain on that front.
In general, you should feel free to use methods to either reduce redundance or to help organize your code, or both.
In some cases, it can be more subtle and difficult to tell when it's a good idea to split apart an existing method. For example, take the following method, which prompts the user if they want to change their email address:
public static String tryGettingNewEmail(Scanner scan, String oldEmail) { System.out.println("Old email: " + oldEmail); String answer = ""; while (!answer.startsWith("Y") || !answer.startsWith("N")) { System.out.print("Change email? (Y/N): "); answer = scan.nextLine().toUpperCase(); } if (answer.startsWith("Y")) { System.out.print("New email: "); return scan.nextLine(); } else { return oldEmail; } }
An added complication to this method is that it will only let the user continue if they type in a string starting with either "Y" or "N". If they type in something like "maybe", this code snippit will treat that as an error and ask them again.
Should this method be refactored? At first glance, it seems like the method is already responsible for one single thing, and doesn't contain any obvious redundancy.
As it turns out, we can, if we treat the "select Y or N" part of the method as a distinct subtask:
public static String tryGettingNewEmail(Scanner scan, String oldEmail) { System.out.println("Old email: " + oldEmail); boolean userWantsChange = userSelectedYes(scan, "Change email?"); if (userWantsChange) { System.out.print("New email: "); return scan.nextLine(); } else { return oldEmail; } } public static boolean userSelectedYes(Scanner scan, String prompt) { String answer = ""; while (!answer.startsWith("Y") || !answer.startsWith("N")) { System.out.print(prompt + " (Y/N): "); answer = scan.nextLine().toUpperCase(); } return answer.startswith("Y"); }
Once we make this change, it's much easier to tell what each method is individually
doing. Our tryGettingNewEmail
email is also much more readable since we were
able to take the subproblem of validating user input and split it off into a separate
helper and use readable method and variable names.
Main methods should be a summary
Your main method should be a summary of your entire program – like a table of contents. This is an extension of the previous rule.
This rule is really a logical extension of the previous rule, but it bears repeating.
You should resist the urge to try and cram too much code into your main method, since it'll then become too crowded and too difficult to read.
For example, you don't want your main method to look like this:
public static void main(String[] args) { for (int i = 1; i <= 5; i++) { for (int j = 0; j < i; j++) { System.out.print("*"); } System.out.println(); } for (int i = 1; i <= 5; i++) { for (int j = 1; j <= 5; j++) { System.out.print("#"); } System.out.println(); } for (int i = 1; i <= 5; i++) { for (int j = 1; j <= 20; j++) { System.out.print("@"); } System.out.println(); } }
This is the same example as before. A good main method would look like this:
public static void main(String[] args) { drawStaircase(5); drawSquare(5); drawRectangle(5, 20); } // etc
Reading a main method should feel akin to reading a table of contents, and less like reading the entire book.
Be careful not to err to the other extreme. Don't write code that looks like this:
public static void main(String[] args) { doSomethingMinor(); doEverythingElse(); } public static void doSomethingMinor() { // ...etc } public static void doEverything() { // ...etc } // etc
While your main method is technically now summarizing your program, it's a very poor summary. To continue using our metaphor of the book, this is like seeing a table of contents with exactly two entries: "Introduction" and "Rest of Book". While you could technically write a book with such a table of contents, it'd be pretty useless.
Trivial methods
You should never write a method which does only a single operation or is composed of only a single component. Instead, you should just perform that operation directly and skip the middle-man.
This is an example of what a trivial method looks like:
public static void myPrintln(String line) { System.out.println(line); }
This method does only a single thing – print a message. While on the surface,
this method appears to simplify things (now, we don't have to keep typing
System.out
when we want to print things), this would NOT be a good method to
write.
This is because if our method does only one thing, then it's sort of useless, since we could have just done that "one thing" up in the caller. Remember, we want to keep code logically concise and reduce redundancy, but don't really care about keeping code literally concise and doing things solely for the sake of lowering the character count.
This method also does not enhance readability in any appreciable way: the method name does not make it easier to understand what the underlying code is doing.
So, since this method does not enhance readability or reduce redundancy in any meaningful way, we call this a trivial method.
However, do note that not all one-line methods are trivial methods – this is a common misconception many students have. For example, take the following:
public static double pythagoreanTheorem(double sideA, double sideB) { return Math.sqrt(sideA * sideA + sideB * sideB); }
Even though this method is only one line long, it's doing 4 separate operations (two multiplications, one addition, and one square root) so is definitely worthy of being refactored into a helper method.
However, it can sometimes on rare occasion be acceptable to write a method to wrap around an operation which does only a single thing if we gain some other tangible benefit as a result. For example, take the following segment of code:
public int foo() { // ... if (symbol == '~') { // ... } // ... }
Why are we comparing the symbol
variable to a tilde? What is the significance of the tilde? We could leave an inline comment explaining, or wrap the comparison in a helper method instead:
public int foo() { // ... if (isStart(symbol)) { // ... } // ... } public boolean isStart(char symbol) { return symbol == '~'; }
This is a trivial method, but we've redeemed it somewhat by giving it a more useful name which makes understanding the intent of the caller more clear. Contrast this to the initial println
example – we gain zero clarity by wrapping System.out.println
in a println
method.
Also note that both versions of the above code would be acceptable. We don't need to wrap symbol == '~'
in a method, after all. You also shouldn't go overboard on doing this sort of thing – new semantic name notwithstanding, this is still a trivial method.
Minimize your parameters
Your method should accept only the bare minimum parameters that it needs in order to successfully complete its task.
In particular, you should:
- Remove any parameters you've never used
- Avoid passing in information that could be derived from other parameters
- Avoid passing in information that could be derived from existing constants
- Avoid passing in information that is already directly contained within fields (when working with objects)
The reasoning behind minimizing your parameters is it helps to minimize the overall complexity of your method. The more parameters you give a method, the more potential interactions (and therefore potential bugs) there will be between each kind of input you could possibly give the method.
You should adopt a mindset where you carefully consider each and every parameter you need to determine whether or not it's necessary, and not fall into the bad habit of adding parameters essentially at whim.
Note that we deliberately do not enforce any sort of limit on how many parameters a method is allowed. Some methods require none, others require one or two, and even more will require seven or more. Again, our emphasis is on whether or not each parameter is necessary or not, not on how many of them there are.
Early returns
Stylistically, we're indifferent as to whether or not you have only a single return in your method or if you have multiple. The only time we require you to return early from a method is if doing so would benefit efficiency (for example, returning a value from the middle of a loop).
For example, it would be good style to return early in a scenario such as this:
public static String foobar(List<String> myCollection) { for (String s : myCollection) { if (checkSomething(s)) { return s; } // do something else with s } return defaultValue; }
It would be inefficient to store the result in a variable and wait to go through the entire list if we didn't need to.
Method chaining
Do not chain your methods together. If you have a sequence of tasks that are meant to be linear, you should keep them linear. If you need data to flow from one method to another to yet another, you should return that data instead of directly passing it some method.
Note that what method chaining is and isn't can be subtle. You'll probably need to read the expanded explanation for this one.
So, full disclosure. I personally hate the name "method chaining". I don't know if it's because of the name we gave it, or if it's because of how we teach it, but I've seen way too many students who seem to have a misunderstanding of what method chaining is, and actually end up writing code worse then they would have otherwise.
In fairness, this might be because it's rather difficult to explain what method chaining actually is without resorting to loads of diagrams and pictures. Let me start with what method chaining is NOT. The term "method chaining", as we use it in this class, does not refer to code that looks like this:
String bar = foo.method1().method2().method3().method4();
Code which looks like this is perfectly fine. It might be a good idea to split it up into several lines and use variables to help make it more readable, depending on what exactly you're doing, but there's nothing inherently wrong with this sort of code.
Method chaining also does not mean "helper methods calling helper methods". To the contrary, it's very much good style to have helper methods calling other helpers, especially if it helps reduce redundancy or helps make your code more readable.
So, what actually is method chaining? Well, I think it's something which is best
explained visually. Let's say that I have a method named myMethod
, and it does
tasks A, B, and C. It doesn't matter what those tasks are – perhaps they're printlns,
perhaps they're if statements, perhaps it's a return statement. Well, we can
represent that method like so:

We have a single method, and it does four tasks in succession. In code, it would look something like this:
public void myMethod(){ taskA; taskB; taskC; taskD; }
This is good, regular, code. We want to do four things, and we do them one after the other. So far, everything's good.
Now, let's say that for whatever reason, you decide to write your code like this:
public void myMethod(){ taskA; methodB(); } private void methodB() { taskB; methodC(); } private void methodC() { taskC; methodD(); } private void methodD() { taskD; }
Well, if we represent it as a flowchart, we basically get this:

Notice how that we've taken what's essentially a linear task, and arbitrarily made one method call another method call another method instead of taking the first approach.
THIS is what method chaining is – it's not private methods calling other private methods. Rather, it's taking what should be an inherently linear task and stringing it along, making the linearity non-obvious. What your code does is starting to become misaligned compared to the "form" or "shape" of your code.
It also makes your code far less modular. Previously, all four tasks were kept independent from one another, and could be rearranged, moved, and deleted at whim. Now, if you want to perform task A, you're forced to also perform tasks B, C, and D whether you want to or not.
90% of the time, the reason why students end up doing method chaining is because they want to pipe data from one function to another, but are afraid of using return statements. For example, they'll do this:
public void myMethod(){ taskA; methodB(something); } private void methodB(String param) { taskB; methodC(something); } private void methodC(String param) { taskC; methodD(something); } private void methodD(String param) { taskD; }
...when they really ought to be using returns, and do something like this:
public void myMethod(){ taskA; something = methodB(something); something = methodC(something); methodD(something); } private void methodB(String param) { taskB; return something; } private void methodC(String param) { taskC; return something; } private void methodD(String param) { taskD; }
...and linearize it. So really, if you look closely at it, method chaining often stems from a misunderstanding or reluctance to use parameters and returns. However, both students AND TAs seem to instead look at the most obvious issue (methods calling other methods) and think that's the main issue when really it's just a symptom of an underlying problem.
As a result, many students end up picking up the wrong instinct and become too paranoid about their code – they grow to incorrectly assume that method chaining is when methods call other methods or something, and that going too deep is bad. Again, there's nothing wrong with methods calling other methods. Take the following flowchart, for example:

This has method calling method after method – it's as deeply nested, if not more, as the previous flowchart. However, this would NOT constitute method chaining. Because the tasks that we're trying to do are inherently non-linear, it makes sense to have multiple helper methods and nesting. What the code is intending to do aligns well with the shape of the code.
So basically, method chaining is whenever you take an inherently non-linear task, and decide to string it along a series of methods (most likely due to misunderstanding how to use parameters and returns) and force a previously independent series of operations to become tangled and dependent – the net result is that you end up with a program with a poor "shape". It feels unbalanced and unhealthy.
If we think about it visually, healthy code is either flat (like a stick resting peacefully on the ground) or like a tree with lots of leaves and branches.

On the other hand, method chaining is like a dead tree, withered and decaying, about to topple over:

So basically, if your code looks like a forest fire hit it, you'll get deducted points for method chaining.
Minimize your fields
You should keep the number of fields in your class to a bare minimum needed to successfully complete your assignment without compromising on the other principles of style. If it is possible to perform a task via passing data around via parameters and returns instead of using a field, you should do so.
The reasons why we want you to minimize the number of fields you need to use are identical to those outlined in the section on Minimizing scope – the more fields you have, the more potential interactions there are between methods, and the higher the odds are of your program developing bugs.
Having multiple fields unnecessary fields also starts to violate the principle of modularity. If your method is dependent on 5-6 different fields to be in some particular state in order to function effectively, we really cannot say that the method is all that modular. Parameters and returns are the key mechanisms we should be using to pass data back and forth.
Fields should be reserved to store data which must be persisted between multiple independent calls to that method or other methods within the class, or when not using a field would result in drop in efficiency.
Make all fields private
You should always make all your fields private. This prevents the client from looking at and tinkering with the internal details of your code. The client should only ever interact with your object through a carefully-defined set of public methods to prevent subtle bugs from occurring.
There are a few exceptions to this rule, but we will explicitly tell you when that is the case.
While making fields public may seem convenient at first, it frequently results in the
client and implementor classes becoming tangled together. For example, say I wanted to
create a Point
class to represent an x-y pair of coordinates. In that case, I
could naively do something like this:
Point class:
public class Point { public double x; public double y; public Point(double x, double y) { this.x = x; this.y = y; } public double getMagnitude() { return Math.sqrt(x * x + y * y); } public double getAngle() { return Math.atan2(this.y, this.x); } }
Client usage:
Point p = new Point(3.2, -6.6); double newX = p.x * 2; p.y = foo(p, bar);
And this might work, for a little while. But then, let's say that as I kept working on my program, I slowly started to realize that it would have been better for me to store the data directly as a magnitude and value – doing so would dramatically simply the math and angle calculations I'm doing.
But now I'm stuck. If change my fields to magnitude
and angle
,
all of the client code I wrote before would no longer work since they were all directly
accessing those fields. If I keep the x and y fields, and add the magnitude and angle
fields on top of that, then my data would fall out of sync since some clients would modify
some the data, but not all of them.
However, if I had forced the clients to manipulate my class exclusively through public methods, then this problem would have been a much easier problem to solve:
public class Point { private double magnitude; private double angle; public Point(int x, int y) { this.setXY(x, y); } public getMagnitude() { return this.magnitude; } public setMagnitude(double magnitude) { this.magnitude = magnitude; } public getAngle() { return this.angle; } public setAngle(double angle) { this.angle = angle; } public getX() { return Math.cos(angle) * magnitude; } public setX(int x) { this.setXY(x, this.getY()); } public getY() { return Math.sin(angle) * magnitude; } public setY(int y) { this.setXY(this.getX(), y); } public setXY(int x, int y) { this.magnitude = Math.sqrt(x * x + y * y); this.angle = Math.atan2(y, x); } }
Now, no matter what fields we use, we can always adjust our methods so that they
continue work and the client can still use our Point
class uninterrupted. If
the client wants to do mostly x/y manipulations, they can still do so.
Unfortunately, in the pursuit of good style, we've ended up making our code fairly bloated, and a common question many students will have is if there's a way of accomplishing the same thing without having to tediously write multiple getter and setter methods.
There is such a way in other programming languages using something called properties, but alas, Java currently doesn't have properties, so we're stuck with writing out getters and setters for the foreseeable future.
Because writing getters and setters can often be tedious, we will on occasion explicitly
grant you permission to make classes with public fields when the vast majority of the
manipulations you do to that class is on those fields. Usually, we allow public fields on
classes such as LinkedListNode
or TreeNode
which are primarily
implementation detail anyway, are likely to be private inner classes to some
LinkedList
or Tree
class, and so do not need a stringent interface.
Field initialization
Always initialize your fields inside your constructor, not outside.
You should never directly initialize your field, like so:
public class Foo { private int myField = 3; private Random rand = new Random(); public Foo() { // ... } // ... }
Rather, you should initialize everything inside of your constructor, like so:
public class Foo { private int myField; private Random rand; public Foo() { this.myField = 3; this.rand = new Random(); } // ... }
There are several reasons for this. One reason is that it's not always possible to directly declare and initialize certain fields, especially when our initialization code requires some additional logic.
The other reason is for consistency. In many cases, we need to initialize fields based on parameters that we pass into our constructor. In those cases, the only place where we can initialize our fields is inside the constructor itself, and having some fields be initialized in the constructor vs directly looks inconsistent:
public class Foo { private Scanner input; private int size; private Random rand = new Random(); public Foo(Scanner input, int size) { this.input = input; this.size = size; } // ... }
Again, an important aspect of style is consistency. If some of our fields need to be initialized in the constructor, then it's best to make all of them be initialized there:
public class Foo { private Scanner input; private int size; private Random rand; public Foo(Scanner input, int size) { this.input = input; this.size = size; this.rand = new Random(); } // ... }
However, do note that it is ok to directly declare constants like we always have been doing so far:
public class Foo { public static final int MY_CONSTANT_VALUE = 42; private Scanner input; private int size; private Random rand; public Foo(Scanner input, int size) { this.input = input; this.size = size; this.rand = new Random(); } // ... }
The reason for this is because there's no other way to do this: it's impossible to set a constant inside a constructor, or any other method (if you could, they wouldn't be constants).
Extra public functionality
You should not include extra public methods beyond what the spec instructs you to include. Any helper methods you make must be kept private.
The reasoning here is that by making helper methods public, you're exposing implementation detail that the client shouldn't be really need to know about.
If you want to intentionally add public functionality to extend the behavior of your class, you should not do that. When grading we have to be very strict to ensure that we're equally fair to all students, and even if your extension is a good idea, we'll probably have to dock you points because you've ended up ignoring portions of the spec.
this
You should always use the this
keyword whenever it's possible to do so.
Doing so will make the difference between local variables/static methods vs fields/instance
methods very clear.
For example, we would consider the following to be bad style:
public class Foo { private int myField; public class Foo(int data) { myField = transform(data); } private int transform(int data) { // ... } }
The corrected version would look like this:
public class Foo { private int myField; public class Foo(int data) { this.myField = this.transform(data); } private int transform(int data) { // ... } }
Both the field and the method belong to the instance, so we'll use the this
keyword to explicitly indicate so.
The reason we are making you do this is because doing so will force you to explicitly
think about what exactly an object is and how it works whereas omitting the this
keyword will make it easier to forget.
Duplicate constructors
When writing classes with multiple constructors that share many similarities, you should always try and structure your constructors such that they all delegate to one single, general constructor instead of repeating your initialization logic.
For example, say we had an object which we use to represent a spaceship, like so:
public class Spaceship { private List<String> crew; private double fuelLeft; private String name; public Spaceship() { // ??? } }
There are multiple valid ways we could initialize the initial state of this spaceship. We might want to start by simply give the spaceship a name, leaving the crew and fuel remaining empty to start with. Alternatively, we might want to be super-detailed and give the spaceship the amount of fuel it has and its crew members to start with. Or perhaps we want to make use of some futuristic cloning technology and copy another spaceship and its crew outright. Each of these are valid (though possibly ethically dubious) ways of setting up the initial state of a spaceship, which means we should have a constructor for each:
public class Spaceship { private List<String> crew; private double fuelLeft; private String name; public Spaceship(String name) { this.crew = new ArrayList<String>(); this.fuelLeft = 0; this.name = name; } public Spaceship(Spaceship other) { // clone the crew this.crew = new ArrayList<String>(other.crew); // violate conservation of energy because science this.fuelLeft = other.fuelLeft; // copyright violation? this.name = other.name; } public Spaceship(List<String> crew, int fuelLeft, String name) { if (fuelLeft < 0) { throw new IllegalArgumentException("Cannot provide negative fuel"); } this.crew = crew; this.fuelLeft = fuelLEft; this.name = name; } }
However, the problem with doing something like this is that we have a lot of redundancy and duplicate code. We can fix this by using the this
keyword like so:
public class Spaceship { private List<String> crew; private double fuelLeft; private String name; public Spaceship(String name) { this(new ArrayList<String<(), 0, name); } public Spaceship(Spaceship other) { // clone the crew, violate conservation of energy, violate copyright this(new ArrayList<String>(other.crew), other.fuelLeft, other.name); } public Spaceship(List<String> crew, int fuelLeft, String name) { if (fuelLeft < 0) { throw new IllegalArgumentException("Cannot provide negative fuel"); } this.crew = crew; this.fuelLeft = fuelLEft; this.name = name; } }
Now, we've isolated all of our initialization and setup logic into a single constructor, almost completely reducing all redundancy in the process.
Method independence
When writing objects, you should take care to ensure that no single method relies on any other method being called first, or in any particular order.
The only thing you may assume is that one of your constructors will be the very first thing that is run. Apart from that, anything goes.
A common mistake students make is to assume that the objects they're writing will always be called and executed in some specific order. However, that is not the case.
While the test classes we give you will call your methods in a specific order, you should design your classes so that they don't rely on that and could work with any arbitrary client.
So, if you have a class with three methods named methodA
,
methodB
, and methodC
, you should be prepared for the client
to do something crazy like call methodA
1000 times in a row, or alternate
between calling methodB
and methodC
three hundred times, pause
for a minute, repeat, etc...
No matter what the client tries, your code must do something reasonable in response.
Class header comments
You must always include class header comments for every class you write. Your class header comment must include metadata (your name, date, assignment, TA, etc), and a description of the class as a whole. Assume that whoever is reading the description is unfamiliar with the assignment.
A class header comment serves one main purpose: to summarize what your program does, and what the purpose of the program is to the reader. It helps a reader who is unfamiliar with your code understand just what your class is doing, on a high level, and gives them enough context to understand what is happening when they go on the read the rest of your code.
When writing a class header comment, you should assume that whoever is reading it is a competent coder, but is unfamiliar with the assignment/has not taken the intro courses at UW. This simulates how you should be writing your comments in the industry.
You should also include some metadata in your class header comment to help your TA keep track of the different submissions while they're grading.
More specifically, you should always include exactly the following things:
- Your first and last name
- The assignment name and number
- The date that you turned the assignment in
- Your section id
- Your TA's name
- A summary of the entire program (at minimum, 2-3 lines long)
Here's an example of what a good class header comment might look like:
// Arthur Dent // Assignment #10: TeaRobot // Wednesday, March 14, 2015 // CSE 143, Section AB // TA: Zaphod Beeblebrox // // This program will control a robot that given a set of constraints // and taste preferences, will create and serve the user the perfect // cup of tea.
This is a good class header comment because it contains all of the required metadata, and includes a good summary that strikes a good balance between describing what the program does without being too wordy.
In contrast, here's what a bad class header looks like. It includes barely any metadata, and does not contain a good summary of the program:
// Arthur -- TeaRobot
Here is another example of a bad class header comment. While it does contain the metadata we're looking for, its class header description is too specific and detailed. Your description should be a broad summary of your program, and not a running description of what each and every method is doing.
// Arthur Dent // Assignment #10: TeaRobot // Wednesday, March 14, 2015 // CSE 143, Section AB // TA: Zaphod Beeblebrox // // This program allows you to control the a tea-making robot using the 'makeTea' method, // which accepts a set of properties for the perfect tea, lets you serve a user using the // 'serveTea' method, which accepts a String specifying the name of the person to serve // tea to, and a 'cleanTable' method which will clean the specified table, wash all the // dishes, and such.
Do not plagarize
Your comments should be written in your own words. You should not directly copy-and-paste any text from the spec.
We want to see if you understand how to write good comments, not how good you are at copying-and-pasting. It's ok to be inspired by the spec, borrow specific phrases from it, and use it to remind you on things to comment on, but you should never copy it wholesale.
Note: this section of the guide covers our expectations for commenting methods in CSE 142 and the first half of CSE 143x. Our expectations are stricter in CSE 143.
Know your audience
When commenting your methods, be sure to keep in mind who exactly your comments are addressed to. You should assume that your reader is competent, does not know anything about the assignment, and does not care about how your code works, only what it does.
Like any form of writing, it's very important to know who exactly your audience is before starting. That way, you know what sort of tone you should be taking, what kind of information is relevant, and what sorts of things to focus on.
Because we want you to get practice in how to write high-quality comments (the kind that other professional programmers would expect), we want you to assume that whoever will be reading your comments fits the following profile:
-
Assume your readers are competent programmers.
That means you should not spend the time explaining how things like a for loop or a println works. Assume that your reader is already familiar with Java to a reasonable extent.
-
Assume your readers know nothing about the assignment.
You should assume that your reader knows nothing about your program, what it is, what it's supposed to do, and why it exists (perhaps they didn't take classes at UW). They just need to understand how to interact with your code.
-
Assume your readers don't care about how exactly your code works, only how to use it.
We will revisit and elaborate on this assumption in CSE 143, but for now, you should assume that whoever is reading your method header comments is doing so because they need to interact with your code in some way. That means you should spend a lot of time elaborating on what your parameters are (and what values are acceptable), and what effect calling that method has/what values you'll return.
However, they don't care how precisely that method works, whether you're using a
print
orprintln
or anything to that effect.IMPORTANT: This bullet point only applies to method header comments, NOT to inline comments (comments inside a method). If somebody is reading your inline comments, that means that they are interested in how exactly your method works. In that case, this assumption wouldn't apply.
This neatly summarizes the profile of a typical professional developer. (It's very common for professionals to collaborate + reuse other people's code. It's also very common for professionals to be on a deadline and not have time to actually dive deep into whatever they're using).
Here are some common mistakes that students will make when they forget to keep their target audience in mind:
-
Assume the readers has read the spec/can read the spec.
So, you should not say things like "for a full description, see page X-Y in the spec".
-
Address their comments to the TA.
Remember, the TA is not your intended audience, and you should not be catering your comments to them. If you really do feel the need to address your TA directly, leave it as an inline comment (and explicitly indicate that this is not a normal comment).
Comment every method
You must comment every single method in your class. Your comment should describe what that method does (but not how it does it).
We expect you to comment every single method written in your class, even if they seem very simple.
Your comments must describe what the method does, but not how it does it. You should assume that whoever is reading your comments is very interested in learning how to use your method, but does not care how it works or what it internally looks like. Again, this simulates what the typical programmer will be looking for in comments in industry.
That said, your comments also should not be too short.
For example, take the following method:
public static void printMultiplicationTable() { for (int i = 1; i <= 10; i++) { for (int j = 1; j <= 10; j++) { System.out.print(i * j + "\t"); } System.out.println(); } }
This would be a bad example of a method header comment.
// Prints a multiplication table public static void printMultiplicationTable() { ... }
This comment is too brief. It hardly answers the most basic question of "what does the method do?"
// Uses two nested for loops and print statements to print a multiplication // table. Both loops start at 1 and go to 10. System.out.println() is used // every ten numbers to move the table to the next line. public static void printMultiplicationTable() { ... }
This version of the comment is too elaborate. It mentions that the method prints a multiplication table that goes to 10, but it also includes unnecessary details about the method's implementation. Knowing that the method uses nested for loops doesn't change how a client will use the method.
// Prints a tab-separated 10 by 10 multiplication table. public static void printMultiplicationTable() { ... }
This comment is much better. It concisely yet thoroughly describes the method's behavior.
Exception: Commenting your main method is optional
The only method you are not required to comment is your
public static void main(String[] args)
method (though you can, if you want to).
The reason why commenting your main method is optional is because there is an extremely high chance that your comment is going to be near-identical to your class header comment, since both comments essentially describe the entire program.
However, if you do try and do so, feel free to do so. It's sort of pointless, but will not detract from your program.
What to comment on
When writing a method header comment, focus on describing your parameters, any return values, and any interactions your program will have with the outside world (printlns, reading or writing to files, getting input from the user, etc).
Besides describing what your method does, you should also spend some time describing any parameters your method accepts, and what values it returns (if any).
Provide enough information that somebody calling your method understands what values they're allowed to plug in, and what they can expect as output.
For example, this is an example of a method which does not adequently comment on parameters and output:
// Moves a robot public boolean moveRobot(double distance, double angle) { // ... }
While it does describe what the method does, it doesn't fully describe what its parameters are, or what it returns. Here is a better example:
// Moves a robot forward by the given distance at the specified angle. // Returns 'true' if the robot was able to move the specified distance // without running into any kind of obstacle. // // The specified distance must be in meters, and cannot be negative. // // The angle must be in degrees, and in the range -90 to 90 (inclusive). // If the angle is positive, the robot curves right. If the angle is negative, // the robot curves left. If the angle is positive, the robot curves right. // If the angle is equal to 0, the robot moves forward. public boolean moveRobot(double distance, double angle) { // ... }
This is a much better method header comment – it explicitly mentions each parameter, their expected ranges, their units, and all other information somebody might need to use it. It also mentions what the method returns, and any other relevant information.
Formatting your comments
You may format your comments however you wish as long as it's readable and the information we're looking for is there. Expand for suggested formatting styles.
We do not require your comments to be formatted following any particular convention. Here are some suggested commenting formats:
// Your name here // TA name // January 8, 2033 // Assignment 1 // // This commenting style uses a series of single-line comments // for everything. public class Example { public static void main(String[] args) { // ... } // Notice that there is one blank line between one method // and the next, but no blank line between the comment and // the thing that it is commenting. public static int foo(int a, int b) { // ... } // You also don't have to write everything in a single paragraph. // Feel free to add // // line breaks and // // - bulleted // - lists public static int bar(int c, int d) { // ... } }
You can also use multi-line comments:
/* Your name here * TA name * January 8, 2033 * Assignment 1 * * Here, I'm using multi-line comments. Even though I don't need to, * I'm prefixing the start of each line with a star because it * looks prettier. */ public class Example { public static void main(String[] args) { // ... } /* Same thing here -- multi-line comments. */ public static int foo(int a, int b) { // ... } /* And again, another comment. All of the guidelines from * up above still apply. */ public static int bar(int c, int d) { // ... } }
If you know what Javadocs commenting style is, feel free to use that also:
/** * Your name here * TA name * January 8, 2033 * Assignment 1 * * Do note that you still need to include the metadata up above, even * though it's not a part of the Javadocs standard. * * <p>If you want to use a subset of Javadocs or come up with your own * standard, feel free to, as long as it's readable and consistent. */ public class Example { public static void main(String[] args) { // ... } /** * All of the same guidelines apply. * * @param a does something * @param b does something else * @return blah */ public static int foo(int a, int b) { // ... } /** * Just don't switch between styles in a single assignment. * It looks messy. * * @param c description * @param d more description * @return even more description */ public static int bar(int c, int d) { // ... } }
Method header vs inline comments
There are three different kinds of comments you can write: class header comments, method header comments, and inline comments (which are comments that go inside your method).
It's important to note that what we expect from inline comments are somewhat different from what we expect from class/method header comments.
Inline comments
Use inline comments to describe complex pieces of code – to explain your implementation. You may also use inline comment to help delineate distinct "sections" of your method. Do not overcomment and leave excessive amounts of inline comments.
Unlike class header comments, which summarizes your entire program, and method header comments, which explains how to use your method + what users can expect from it, inline comments are meant to explain how your method works.
You should assume that whoever is reading your inline comments is not only interested in how to use your method, but also in understanding how it works (possibly so they can modify it later). Like before, you should assume whoever is reading your inline comments is a competent programmer.
They will already know a fair amount about Java, but will need help understanding why you made certain decisions, or understanding what a particularly complex piece of logic does.
Your reader will NOT need help understanding how your code works on a line-by-line basis. In particular, do not comment like this:
// Returns how many of a given character is in the provided string, // ignoring case. public int countCharacters(String str, char letter) { // converts the letter to lowercase char lower = Character.toLowerCase(letter); // converts the letter to uppercase char upper = Character.toUpperCase(letter); // start counting from 0 int count = 0; // start looping from i = 0 up to the end of the string, incrementing // once each time for (int i = 0; i < str.length(); i++) { // get the i-th character char ch = str.charAt(i); // compare the current char against the upper and lowercase // strings to see they match, ignoring case if (ch == lower || ch == upper) { // increment the counter count++; } } // return the count return count; }
This is terrible commenting because the comments add zero value and annoy the reader. All the comments do is simply parrot and repeat what the code does. An experienced programmer does not need a comment to explain how a for loop or a return statement works.
A better-commented version would look like this:
// Returns how many of a given character is in the provided string, // ignoring case. public int countCharacters(String str, char letter) { // Stores lowercase/uppercase versions of the char to // avoid recomputing it in the loop char lower = Character.toLowerCase(letter); char upper = Character.toUpperCase(letter); // Uses cumulative sum to find matches int count = 0; for (int i = 0; i < str.length(); i++) { char ch = str.charAt(i); if (ch == lower || ch == upper) { count++; } } return count; }
Now, the comments are much less intrusive, and adds some additional value. We now understand a little better why we implemented the code in the way we did, and clearly delineates the code into two separate sections.
Here is another example of bad inline commenting, which uses a CSE 143 example:
// Randomly shuffles a list of numbers in place public static void shuffle(List<Integer> numbers) { // Creates a new random object Random random = new Random(); // Iterates through the list of numbers for (int currIndex = 0; currIndex < numbers.size(); currIndex++) { // Picks a random number between 0 to (numbers.size() - currIndex) // offset by the currIndex int randIndex = random.nextInt(numbers.size() - currIndex) + currIndex; // Grabs two cards from the currIndex and randIndex String cardA = numbers.get(currIndex); String cardB = numbers.get(randIndex); // Sets the currIndex and randIndex numbers.set(currIndex, cardB); numbers.set(randIndex, cardA); } }
The inline comments here add zero value – all they do is parrot what the code is doing. A better version would look like this:
// Randomly shuffles a list of numbers in place public static void shuffle(List<Integer> numbers) { // Implements the Fisher-Yates shuffle algorithm // http://en.wikipedia.org/wiki/Fisher%E2%80%93Yates_shuffle Random random = new Random(); for (int currIndex = 0; currIndex < numbers.size(); currIndex++) { // Selects a random element beyond this one (selecting literally // any element is proven to result in an in an imperfect, not // fully random shuffle). int randIndex = random.nextInt(numbers.size() - currIndex) + currIndex; // Swaps the randomly selected card with the current one String itemA = numbers.get(currIndex); String itemB = numbers.get(randIndex); numbers.set(currIndex, itemB); numbers.set(randIndex, itemA); } }
This is much better, since the inline comments now links to the specific algorithm we're using in case the reader wants to learn more, and we're explaining peculiarities, such as why the code to select a random number is so convoluted.
Note that we also don't comment on things like "we're creating a Random object" or "we're looping over the list" – your reader already knows Java, and does not need help to understand these things. However, we do briefly note that the four lines of code at the end are being used to swap two elements. This is the sort of thing which may not be immediately obvious, so is worth briefly mentioning.
However, an even better way to "comment" this method would be to break up the code into smaller methods, like so:
// Randomly shuffles a list of numbers in place public static void shuffle(List<Integer> numbers) { // Implements the Fisher-Yates shuffle algorithm // http://en.wikipedia.org/wiki/Fisher%E2%80%93Yates_shuffle Random random = new Random(); for (int currIndex = 0; currIndex < numbers.size(); currIndex++) { // Selects a random element beyond this one (selecting literally // any element is proven to result in an in an imperfect, not // fully random shuffle). int randIndex = randomInt(random, currIndex, numbers.size()); swap(numbers, currIndex, randIndex); } } // Gets a random int between min (inclusive) and max (exclusive), using // the given random object. public static int randomInt(Random random, int min, int max) { return random.nextInt(max - min) + min; } // Swaps the items at the two indices in the given list public static void swap(List<Integer> numbers, int indexA, int indexB) { String itemA = numbers.get(indexA); String itemB = numbers.get(indexB); numbers.set(indexA, itemB); numbers.set(indexB, itemA); }
Now, our shuffle
method is much more clear, since we've moved each distinct subtask into a separate helper with a clear and readable method name. We still keep some of our inline comments, but we're able to convert some of the others into method header comments.
Formatting inline comments
There are no hard-and-fast rules for formatting inline comments. However, you should generally place inline comments on the line directly before the thing you are commenting.
There is no "correct" way to format an inline comment – anything reasonable will be acceptable.
However, you should most likely avoid writing inline comments that go on the same line as the thing you are commenting. For example, you will typically want to avoid writing code that looks like this:
// Returns how many of a given character is in the provided string, // ignoring case. public int countCharacters(String str, char letter) { char lower = Character.toLowerCase(letter); // Stores lowercase/uppercase versions char upper = Character.toUpperCase(letter); // of the char to avoid recomputing // it in the loop int count = 0; for (int i = 0; i < str.length(); i++) { // Uses cumulative sum to find matches char ch = str.charAt(i); if (ch == lower || ch == upper) { count++; } } return count; }
There are several reasons for this. The main reason is that this will most likely end up violating the 80-characters-per-line limit that we have. You'll find it hard to fit in both your code and your comments on a single line.
The other reason is that you want to be able to freely edit and tweak your inline comments. When you need to share space with existing content, you'll often end up becoming distracted as you constantly format and reformat your text.
Note: This section is for CSE 143. We do not expect you to follow these guidelines in CSE 142 (though feel free to do so if you like).
Expectations in CSE 143
Our expectations for CSE 143/the second half of 143x are higher then they were in CSE 142. In addition to what we previously expected from you, we now expect you to always include information about your preconditions and postconditions in your method header comments, and to never include any implementation detail.
It's important to note that the rules for commenting that we teach you do not contradict what you were taught in CSE 142 in any way. Rather, we're being more rigorous about what exactly is expected.
Preconditions and Postconditions
When writing a method header comment, focus on describing the preconditions and postconditions for that method.
As mentioned earlier, the method header comment is NOT to describe how that particular method works, but rather to describe how to use it.
More specifically, what you are required to comment on is preconditions and postconditions for using that method.
Preconditions are any conditions that the caller, or client of the method must fulfill in order to successfully use your method. So, if an int parameter must be greater then five, or if the caller must call some other method first before using yours, you would qualify all those as preconditions.
Postconditions are any conditions that you, the writer of the method, will guarantee to be true after the method has finished running. So, if your method always prints something, or always returns some double between 0 and 1, you would qualify those all as postconditions.
Together, preconditions and postconditions fulfill a sort of "contract" or "promise" between you (the implementor) and whoever is using your method or class (the client). The client agrees to fulfill the specified preconditions before calling your method, and you promise to satisfy the listed postconditions in exchange.
(And when that doesn't take place, well, that's where bugs come from)
Here is an example of what good comment discussing pre and post conditions looks like:
// Counts the number of occurrences of the provided name // in the given list of names. // // Pre: // - The list of names must not be null, otherwise an IllegalArgumentException // will be thrown. // - The target name must not be null, otherwise an IllegalArgumentException // will be thrown. // // Post: // - Returns the number of times targetName appears in names as a a positive int. public static int numberOfOccurrences(List<String> names, String targetName) { int count = 0; for (String name : names) { if (name.equals(targetName)) } count += 1; } } return count; }
However, you are not required to explicitly mark preconditions and postconditions so long as you mention that information in some way. For example, the following would also be acceptable:
// Counts and returns the number of occurrences of the provided name // in the given list of names as a positive int. Throws an IllegalArgumentException // if either the list of names or the target name is null. public static int numberOfOccurrences(List<String> names; String targetName) { // ... }
You are free to present the information in any format you wish, as long as the content we're looking for is there.
Implementation detail
Your method header comments should never include implementation detail – information about how exactly the method or class works.
One useful way of determining if something would count as implementation detail is to ask yourself if your code would survive a total internal rewrite. If it doesn't, odds are it contains implementation detail.
When commenting, you should try and write comments that are robust and focus on describing the contract or promise between the caller and the implementor: preconditions and postconditions. They should not mention how exactly that contract is fulfilled (that way, the implementor is free to change their mind about how exactly to go about writing a method without ever bothering the client).
A useful rule of thumb is to ask yourself if your comments would surive a total internal rewrite. More specifically, imagine that somebody took the homework spec, and produced a program that has full external correctness, but decided to make the class internally completely crazy. They used advanced material, crazy data structures, a completely different programming language, horrible style, etc...
Because both the twisted version and the normal version of the assignment both have full external correctness, and behave completely identically from the perspective of the client, you should be able to take your comments, copy-and-paste them into the twisted program, and still have the comments perfectly and accurately describe the behavior of the twisted program. Their internal behavior might be completely different, but they're identical externally, so your comments should apply to both versions.
As an example an example of how this process might work, let's say that we have a method
which we're adding to our ArrayIntList
class where we find the sum of all the
numbers. Well, this might be one valid way of writing the method:
public int sum() { int output = 0; for (int i = 0; i < this.size; i++) { output += this.elementData[i]; } return output; }
Now, let's say that we added the following (bad) method header comment:
// Uses a for loop to iterate through this.elementData up to the `size`-th // element and adds together and returns each number. public int sum() { int output = 0; for (int i = 0; i < this.size; i++) { output += this.elementData[i]; } return output; }
This is an example of a very bad comment – it exposes far too
much implementation detail. We don't care about exactly how the method works, all we care
about is what it's meant to do. It also wouldn't survive a complete internal
rewrite. For example, what if we were to change the name of our fields to data
and length
?
// Uses a for loop to iterate through this.elementData up to the `size`-th // element and adds together and returns each number. public int sum() { int output = 0; for (int i = 0; i < this.length; i++) { output += this.data[i]; } return output; }
Well, our comment is now suddenly outdated – we're no longer using those specific fields! We can edit our comment to remove all reference to fields to something like so:
// Uses a for loop to iterate through the internal array and adds up each // element together and returns the final sum.
Well, we're still not done. For example, let's say that we decided to rewrite the code to use a while loop:
// Uses a for loop to iterate through the internal array and adds up each // element together and returns the final sum. public int sum() { int output = 0; int i = 0; while (i < this.length) { output += this.data[i]; i += 1; } return output; }
Again, our comment is now suddenly outdated. We're no longer using a for loop! Well, we can try and patch this up by editing our comment to remove all references to a for loop:
// Uses a loop to iterate through the internal array and adds up each // element together and returns the final sum.
But even this isn't great – what if instead, for whatever reason, we decided to
use an ArrayList
to store our data instead of an array? (And if you're
wondering why you'd use an ArrayList
to represent an
ArrayIntList
, well, who knows? We did say our hypothetical
implementor was crazy, after all).
import java.util.*; // ...snip... // Uses a loop to iterate through the internal array and adds up each // element together and returns the final sum. public int sum() { int output = 0; for (int num : this.data) { output += num; } return output; }
Again, our comment did not survive an internal rewrite. In particular, we aren't even using an array. Again, we rewrite:
// Uses a loop to iterate through the list and adds up each // element together and returns the final sum.
Still not good enough. What if we decided to completely abandon CSE 14x and use something that's hugely and blatantly advanced material?
import java.util.*; import java.util.stream.*; // ...snip... // Uses a loop to iterate through the list and adds up each // element together and returns the final sum. public int sum() { return this.data.stream().limit(this.size).sum(); }
Well, crap – what even is that? We've blown way past 142 and 143 material, and it doesn't look like we're even using a loop in our method – our comments still did not survive the internal rewrite.
Well, let's try again. We might not be using a loop, but we're still iterating in some way, right?
// Iterates through the list and adds up each // element together and returns the final sum.
Still not good enough. What if instead of iterating over it, we stored the sum as a
separate field, and added and subtracted from it whenever we added or removed a number
from our ArrayIntList
?
// Iterates through the list and adds up each // element together and returns the final sum. public int sum() { return this.totalSum; }
Failure again. Our comment still hasn't survived an internal rewrite. Well, fine, we can rewrite to the following:
// Returns the sum of all numbers in the ArrayIntList. public int sum() { return this.totalSum; }
And finally, for the very first time, we've hit upon a correct comment with absolutely no implementation detail. No matter how we chose to write our method, this comment will still stay perfectly accurate and describe every aspect of our method in perfect detail:
// Returns the sum of all numbers in the ArrayIntList. public int sum() { int output = 0; for (int i = 0; i < this.size; i++) { output += this.elementData[i]; } return output; } // Returns the sum of all numbers in the ArrayIntList. public int sum() { int output = 0; for (int i = 0; i < this.length; i++) { output += this.data[i]; } return output; } // Returns the sum of all numbers in the ArrayIntList. public int sum() { int output = 0; int i = 0; while (i < this.length) { output += this.data[i]; i += 1; } return output; } // Returns the sum of all numbers in the ArrayIntList. public int sum() { int output = 0; for (int num : this.data) { output += num; } return output; } // Returns the sum of all numbers in the ArrayIntList. public int sum() { return this.data.stream().limit(this.size).sum(); } // Returns the sum of all numbers in the ArrayIntList. public int sum() { return this.totalSum; }
Notice how our comment works perfectly no matter how our method is implemented. Our comment can survive a complete internal rewrite and therefore contains no implementation detail.
You will still have to spend some time making sure you've covered all the preconditions and postconditions, but at least you know that there's no material that you should be actively deleting.
Assumptions about the client
Your comments should never make any assumptions about the behavior of the client.
This principle is the inverse of implementation detail: your comments should not expose internal detail, and should not speculate on external detail. That is because your class should be flexible enough to be reused by a theoretically infinite number of clients, and it's silly to bind your class to a specific client.
Again, comments document the boundary between client and implementation. You should never mention either of the two, and restrict yourself to only talking about that boundary and interface between the two.
For example, the following would be an example of a bad comment – it assumes that the Scanner object is reading from a file:
// Reads in a sequence of lines from a file, and stores them in a list public List<String> readFile(Scanner input) { List<String> output = new LinkedList<String>(); while (input.hasNextLine()) { output.add(input.nextLine()); } return output; }
This would be another example of a bad comment, since it mentions a specific client by name:
// Represents a chunk of arbitrary text as a sequence of characters for a guessing game // program public class String { // ..snip.. }
You should assume that a potentially infinite number of clients may choose to use your particular object for any arbitrary reason. Consequently, you should not "hard-code" in references to specific clients into your comments.
Retain only useful information
We do not enforce any sort of length requirement or restriction of comments. You are free to make your comments as long or short as you'd like, so long as don't omit any important information and all the information included is actually useful.
This will mean that your method header comments may sometimes be only single sentence long, while others will be several paragraphs.
For example, here is an example of poor commenting:
// This method takes in three parameters, the first of which is a // variable of type int named 'times', the second of which is a variable // of type String named 'a', and the third of which is _also_ a variable of // type String named 'b'. This method will return a string where a and // b are added together and repeated the number of times the int // parameter tells us to. // The int param must not be a negative number. public void alternate(int times, String a, String b) { // Adds together the strings outside the loop instead of // inside to make the code more efficient String combined = a + b; // Start of cumulative sum! String output = ""; // Start repeating and looping for (int i = 0; i < times; i++) { output += combined; } // Returns the output return output; }
While the comment technically doesn't really contain implementation detail, and while all of the comments are technically accurate, it also isn't very useful. A lot of the information was just repeating the method signature, which is a bit redundant since the client can see that information anyway. The inline comments are also not very useful – they often comment on things that are obvious or just repeat what the code is saying.
Here's a more concise version:
// Returns a string where 'a' and 'b' are alternating the given // number of 'times'. Example: // // alternate(3, "--", "*") // "--*--*--*" // // pre: 'times' must not be negative. public void alternate(int times, String a, String b) { // combines strings outside loop for efficiency String combined = a + b; String output = ""; for (int i = 0; i < times; i++) { output += combined; } return output; }
This is much more concise. It conveys the same amount of information, but requires less words to do so.
Note: this section is for CSE 143 only.
Previously, we stated that there are three kinds of methods: class header comments, method header comments, and inline comments.
We are going to extend this model further by taking a look at the differences between public method header comments, and private method header comments. We expect slightly different things from the different types of method headers.
Public methods are mean to be written for a client who only cares how to use your class. Class header comments are like public method comments, except more general. Private method header comments are meant to be read by people who are adding new public methods to your class. Inline comments are meant to be read by people who need to directly modify your class. You should write all of your comments accordingly.
Another way of thinking about these four comments is to think of them as sitting on a sliding scale of how much implementation detail is permitted. Public method and class header comments should contain zero implementation detail, inline comments are usually all about implementation detail, and private method header comments will sit somewhere in between.
However, you should not make the mistake of assuming that we are more lenient regarding private method comments or inline comments. We expect the same degree of quality in rigor in all three types of comments – it's just that the topic of each will be different.
Since we have already discussed what we expect from class header comments and inline comments in the previous portions of this guide/in CSE 142, this section of the guide will focus on the difference between public and private method header comments.
Public comments
The client of a public method is somebody who might want to use your code. As a result, you should avoid including info about how exactly your class and methods work and instead focus on commenting external behavior.
Public comments are comments that are meant to be addressed to the client. The only thing the client cares about is how to use your class, and what they can expect from it. In particular, the client does NOT care about how exactly your class works, or any of its internal details.
Rather, the purpose of a comment is to describe the boundary or interface between two bodies of code. This means that when commenting methods, you should take care to focus on describing preconditions, which are things which must be true if the client wants to successfully use your method, and postconditions, which are things your method guarantees it will do assuming all the preconditions are met.
Here are some examples of things that would constitute as preconditions and postconditions:
Preconditions:
-
Parameters:
What is the expected range of your parameters? Do they need to be in any particular format? What constitutes a valid and invalid parameter?
-
Exceptions:
What happens if the user passes in an invalid parameter? If you throw an exception, what kind of exception do you throw? Under what specific conditions will an exception be thrown?
-
State:
Does the object need to be in any particular state in order for the method call to complete successfully? If so, what is that state? What does the client need to do in order to put the object in that state?
Postconditions:
-
Return:
What will the method return once the method successfully finishes? What is the expected range and format of whatever you return? Is the return value in any particular format? What constitutes a valid and invalid return value?
-
Behavior:
Besides returning some value, is there something else the method will do, like printing something or modifying some state? Be sure to comment only on things the client will actually notice and care about.
You should also take care to explicitly avoid ever discussing implementation detail, which is information about how specifically your method works.
That way, if you avoid elaborating on the internal details of the method, you're free to arbitrarily change how your class works in the future without changing the comments and without disturbing the client. All the client cares about is how they would go about using your class. For example, a client of ArrayIntList doesn't care exactly how ArrayIntList works – all they care about is how to use the class to store ints.
Here some examples of things which would constitute as implementation detail:
-
Fields
The client should be completely ignorant as to what fields you're using, what you're using them for, the names of your fields, etc. If you mention your fields, you're describing internal details for them, which the client won't care about.
-
Specific data structures or algorithms
Again, you want to retain as much freedom as possible to change how your class internally works without bothering the client. You might think that it's best to use an array on Monday, change your mind and use a Queue on Tuesday, and change your mind yet again and use a TreeMap on Wednesday. You want to retain as much flexibility as possible without overcommitting.
-
Information about how specifically your method works
Same reasoning as above.
Private comments
The client of a private method is somebody who wants to add a new public method to your class, and will potentially use your private method to do so. As a result, it's ok to talk about some internal details such as your fields, but still not ok to talk about how your method works.
Note that the expectations for private method header comments are nearly identical to those for public method header comments, except that our target audience has shifted. For public method header comments, the clients are people who are interested in using your class in some way. For private method header comments, the clients are people who are interested in adding a new public or private method to your class, and will be using (though not modifying) your private method in some way.
As a consequence, you should still take care to comment on all preconditions and postconditions, and describe all parameters, potential exceptions, expected behavior, and so forth.
However, because the client is somebody who is extending the functionality of your class, the client will naturally be able to view the specific fields and therefore can deduce quite a bit about what algorithms and data-structures your class uses.
Because of that, there's really no point in trying to word your private method header comments in such a way to try and hide information about fields, data-structures, and algorithms from your client. Rather, it's encouraged to talk about those sorts of things since it's potentially useful for your client to understand how exactly your method manipulates and works with your fields.
However, do keep in mind that your comments should NOT talk about how specifically your method works. That still counts as implementation detail, since there's no need for the client to know how exactly your private method works.
Redundancy in comments
While having redundancy in code is very bad and should be avoided whenever possible, redundancy in comments is sometimes unavoidable and is not as big of a deal.
If your method header comments for two methods are very similar, it's acceptable to have some duplicated information. If those two comments are for public methods, it's acceptable to have one comment refer to the other. If you have a public method and a private method which are very similar (for example, for public-private recursion problems), you should adjust your comments for the private method to focus more on the differences between the two methods. More examples follow below.
When you have two public methods which are very similar, you are allowed to let them explicitly mention each other. For example, this would be an example of acceptable commenting:
// Picks a single flower of the given `color` and adds it // to the flower basket. // // Equivalent to calling `pickFlowers(color, 1)` (including all // preconditions, postconditions, and exceptions) public void pickFlower(Color color) { pickFlowers(color, 1); } // Picks `n` flowers of the given `color` and adds it // to the flower basket. // // Preconditions: // - `color` must not be null, otherwise an IllegalArgumentException // will be thrown. // - `n` must be 1 or greater, otherwise an IllegalArgumentException // will be thrown. // - The internal flowerbasket must not be full, otherwise an // IllegalStateException will be thrown. // // Postconditions: // - The flowers are added to the flower basket, assuming it is not // full. public void pickFlowers(Color color, int n) { // ...snip... }
Even though pickFlower
and pickFlowers
have very similar
preconditions and postconditions, the simpler method pickFlower
can simply
state that it's equivalent to calling another method in a certain way and thus avoid
having to repeat a lot of the comments.
However, if you have a public method and a private method which are very similar (for example, when writing a pair of public-private methods for recursion problems), you cannot have the public method refer to the private one. After all, the client of the class is unable to view any of your private method header comments so would not be able to view what you are referring to.
Instead, you should adjust your private method header comment so it focuses more on implementation and the differences between it an the public method.
As an example, let's say we have a method which takes a number, and prints out all the different ways we can start from 0 and add up the numbers 1 or 2 to reach that number.
In that case, this would be an acceptable way of commenting the public and private methods with minimal redundancy:
// Prints out all the ways you can add up the numbers 1 and 2 // in order to reach the provided number `n`. For example, calling // `printPossibleSums(4)` would produce the following output: // // 1 1 1 1 // 1 1 2 // 1 2 1 // 2 1 1 // 2 2 // // Preconditions: // - `n` must be non-negative // // Postconditions: // - prints out all possible combinations separated by space // to the console, with one combination per line. public void printPossibleSums(int n) { printPossibleSumsHelper(n, ""); } // Part of the public-private pair for printPossibleSums. // Recursively explores all the different ways you can // add the numbers 1 or 2 to reach `n`. // // Preconditions: // - `path` must contain a sequence of numbers explored so // far, separated by space. The entire string must end // with a single space. // // Postcondition: // - When `n == 0`, prints out the path to the console. private void printPossibleSums(int n, String path) { if (n == 0) { System.out.println(path); } else if (n > 0) { printPossibleSums(n - 1, path + "1 "); printPossibleSums(n - 2, path + "2 "); } }
Notice how our public pair went into great detail about the high-level details of the method was meant to do, whereas the private pair when into roughly equal detail, but focused more on implementation and the new parameter we just added, instead of focusing on what it did identically to the public pair.
Comment your exceptions
Whenever you have a method that could potentially throw an exception
(IllegalArgumentException
, FileNotFoundException
, etc), you
should mention that your code could throw this exception, and under what conditions.
If the client wants to use your code, it's very important that they understand the limitations of your method. In particular, if it's possible for your method to fail to work under some conditions, you should be very detailed about what those conditions are and what'll happen if they're violated.
For example, take the following code:
// This method returns 'true' if the array contains the value, and // false otherwise. public static boolean contains(int[] numbers, int value) { if (numbers == null) { throw new IllegalArgumentException("array cannot be null"); } for (int i = 0; i < numbers.length; i++) { if (numbers[i] == value) { return true; } } return false; }
This code will fail if numbers
happens to be null. However, our comments
are ignoring this fact completely – the client doesn't know that this is a problem,
will probably attempt passing in null at some point assuming everything will work, and will
be very surprised when it doesn't.
Instead, you should explicitly indicate that this is a problem, like so:
// This method returns 'true' if the array contains the value, and // false otherwise. It will throw an IllegalArgumentException if the // array of numbers is null. public static boolean contains(int[] numbers, int value) { if (numbers == null) { throw new IllegalArgumentException("array cannot be null"); } for (int i = 0; i < numbers.length; i++) { if (numbers[i] == value) { return true; } } return false; }
Note that we not only mention the specific kind of exception, but under what specific conditions it'll fail.
Now, what if the spec did not tell us to include an exception in our method? What if the spec told us to write the following instead? How should we comment it?
// This method returns 'true' if the array contains the value, and // false otherwise. public static boolean contains(int[] numbers, int value) { for (int i = 0; i < numbers.length; i++) { if (numbers[i] == value) { return true; } } return false; }
If we try passing in null now, the code still won't work, so it's important that we comment this. However, although our code happens to throw a NullPointerException in this case, that's more a reflection of the fact that our exceptions are incomplete, not a fundamental part of the spec and contract.
So, in this case, it's ok to not explicitly mention the exception, since our code never explicitly handles an exception:
// This method returns 'true' if the array contains the value, and // false otherwise. The array of numbers must not be null. public static boolean contains(int[] numbers, int value) { for (int i = 0; i < numbers.length; i++) { if (numbers[i] == value) { return true; } } return false; }
Unlike the previous sections, which focused primarily on topics such as readability, concision, and modularity, this section focuses almost exclusively on efficiency, and guidelines for writing efficient code.
Out of all our style guidelines and principles, the principle of efficiency is perhaps the most immediately obvious. We want our programs to run as quickly as possible so that we can either get more work done faster or to ensure a more pleasant experience for the user.
Anybody who has experienced lag when playing a video game, has waited for data to buffer when watching a video, or has waited for hours for a computation to finish can attest to how important efficiency is.
However, as important as efficiency is, it's important to not neglect all of the other principles of style such as readability, concision, and modularity. A common mistake many beginners will make is to emphasize efficiency above all else to the detriment of the other principles, or inadvertently end up micro-optimizing, trying to optimize things which have no real significance in the grand scheme of things.
As a result, this section will first start by reviewing what exactly efficiency is and isn't, and what sorts of things are important to prioritize before moving on to specific guidelines.
Write algorithmically efficient code
In this class, when we're considering efficiency, we're primarily interested in the behavior of your algorithm in the long run, and are not particularly concerned with micro-optimizations.
More specifically, when trying to write code to solve a problem, you should focus on making the time-complexity of your algorithm as low as possible
The reason for this is a little complicated, and is best explained through an example. For example, take the following code. Which method is do you think is faster and more efficient?
public static void method1() { System.out.println("Hello world"); } public static void method2() { System.out.print("Hello "); System.out.println("world"); }
Trick question! Nobody cares!
Exactly which method runs more efficiently will depend on a mindbogglingly wide array of factors, potentially including factors such as...
- Which Java compiler you're using
- How aggressively Java's compiler decides to optimize your code, which will vary based on where you're calling your method, what the caller is doing, the overall size and complexity of your entire program, etc...
- What kind of hardware your program is running on
- What other programs and processes are running on your computer at the same time (and competing for resources and attention from the CPU)
- Which operating system you're using
- How Java is configured to actually print to the console (does it buffer all output? etc)
- Whether or not you're actually printing to the console (you could instruct your computer to pipe to a file)
- The refresh rate of your actual monitor
It's a fool's game to try and keep track of all these different factors. The moment you figure out how to fully optimize your program and eke out one or two milliseconds worth of improvement, it'll completely fall apart when you try and run your program on a separate computer (and will probably fall apart if you try re-running your program after a few hours)
Instead, we're concerned about performance on a bigger and more abstract scale. Rather then considering how long in seconds it takes for a given operation to complete, we instead measure how any "operations" it takes as we feed our code increasingly bigger and bigger input.
For example, consider these two methods. Which will run more efficiently?
public static int sumArray1(int[] numbers) { int total = 0; for (int i = 0; i < numbers.length; i++) { total += numbers[i]; } return total; } public static int sumArray2(int[] numbers) { int total = 0; for (int i = 0; i < numbers.length; i++) { for (int j = 0; j < numbers.length; j++) { // found the index! if (j == i) { total += numbers[j]; } } } return total; }
Well, let's consider the first method, and count roughly the number of "operations"
that method has to do. Let's imagine we pass in an array of 10 elements. Well, we start
off by initializing the total
and i
variables. We'll say that
takes two operations.
Then, we check i < numbers.length
, get numbers[i]
, add it
to total
, and increment i
exactly 10 times – once per
each element in our array. So, if we do four operations per iteration, and we needed two
operations to do setup, we've performed a total of 42 operations. Then, we return the
total, which gives us a final operation count of 43.
Now, let's try the same thing, but with an array of a thousand elements. The variable
declarations and returns still takes up 3 operations, and we now perform
1000 * 4
operations within the loop – we've performed 4003 operations.
We can form a generalized formula for this – if we say that n
is the
size of our array, our first algorithm will perform roughly 4n + 3
operations
every time you call it.
That's all fine and good. Now, let's try the same thing with our second method, again starting with an input of 10 elements.
We start by initializing total
and i
, which is two operations.
Then inside of our loop, we perform yet another loop.
This inner loop declares a variable j
, then performs the following five
operations: check j
against the length of the array, do the if statement,
access the array, add it to the total
, then increment j
. So, if
our array has 10 elements, our inner loop does 1 + 10 * 5
operations.
Then, our outer loop does three things: check i
against the index, run the
inner loop, and increment i
, and it will do these three things 10 times. So,
our outer loop does 10 * (1 + 10 * 5)
operations.
Then, taking into account the variable declarations and returns, we end up performing
3 + 10 * (1 + 10 * 5) = 513
operations in total. If we scale up and pass in an
array of a 1000 elements, we end up doing 3 + 1000 * (1 + 1000 * 5) = 5001003
operations. And finally, we can convert this into a generalizable equation:
3 + n * (1 + n * 5) = 5n^2 + n + 3
.
Clearly, our first method beats the second one out of the water. If it takes our first method roughly 4000 operations to work through a single array, and the second method roughly 5000000 to do the same thing, there's clearly something terribly wrong with the second method. We've blown way past whether or not something like a print statement or a println is faster. We've exposed a fundamental flaw in the underlying algorithm.
It doesn't matter if we take our two algorithms and implement them in a language other then Java, it doesn't matter if we run our algorithm on a separate computer, it doesn't even matter if we decided to run these algorithms by hand, in person. We'll see the exact same behavior no matter
More formally, we can say that the first algorithm has a linear time complexity whereas the second algorithm has a quadratic time complexity.
When writing your own code, you should keep this idea of "time complexity" in the back of your mind, and try and write code that minimizes the total number of "operations" you need to perform as you increase the size of the input.
An algorithm that takes the same amount of "steps" to run no matter how large the input is (a constant time complexity) is much more preferable to an algorithm that has a linear time complexity.
You should also always keep in mind that what constitutes an "operation" is sort of
fuzzy – we deliberately handwaved what did and did not count as an operation in
our examples up above. Depended on what we count as an "operation", our equation for the
first method could have just as well been 3n + 2
, or 4n + 5
, or
just n
. We care more about how the algorithm "grows" rather then the precise
equation.
Minimize the data structures you use
Besides trying to minimize the time complexity of your code, you should also minimize the number of data structures you are actively using at any given time.
When writing code, you should take care to minimize the number of data structures that are "active" at any given time, especially if those data structures are going to be containing a large number of elements. Each data structure will take up some amount of memory in your computer, and you want to get into the habit of writing programs that try not to hog memory.
For example, let's say we wanted to write a method that counts how many times a symbol appears in a string. Here might be one way of doing this:
public static int charCount(String str, Char target) { int count = 0; for (char c : str.toCharArray()) { if (c == target) { count += 1; } } return count; }
While on the surface this looks good, what happens if the client decides to pass in a
string that's 10,000 characters long? Then, the str.toCharArray()
method is
going to end up having to copy over all 10,000 characters to a new array before you can
iterate over the string.
The net result is that our code is now very inefficient, both in terms of memory usage and time complexity. You're essentially looping twice over the same data, and storing two identical copies of the data. This might be fine if the input string is relatively small, but if the client is providing the string, you have no guarantee of how big or small it might be.
A better solution would look like this:
public static int charCount(String str, Char target) { int count = 0; for (int i = 0; i < str.size(); i++) { char c = str.charAt(i); if (c == target) { count += 1; } } return count; }
Although this is a bit clunky, it doesn't require an extra loop or an extra copy, and so would be considered much more efficient then the first version.
Of course, that isn't to say you should avoid using data structures. For example, take the following:
// Version 1 for (int i = 0; i < 10000; i++) { List<String> myList = new ArrayList<String>(); // Do something complicated with the list System.out.println(myList); } // Version 2 List<String> myList = new ArrayList<String>(); for (int i = 0; i < 10000; i++) { myList.clear(); // Do something complicated with the list System.out.println(myList); }
Although the first version will repeatedly create an ArrayList
10,000
times, the ArrayList
will be garbage-collected at the end of the loop (the
memory it was using up will be reclaimed), which means that both versions will use about
the same amount of memory. (And as it turns out, both versions have the same time
complexity – object creation is a constant-time operation).
Consequently, both versions would be considered good style, and are largely interchangable. (In fact, the first version would probably be preferable, since that way you don't have to remember to clear the list with each iteration).
This section of the style guide isn't particularly well-integrated. It mostly contains a variety of miscellaneous style things related to specific data structures and objects that you'll be using during your assignment.
Most, if not all of the style issues we mention in this section will already be explicitly mentioned in the spec, but for completeness, they're also all listed here.
Arrays should be homogeneous
You should use arrays to store a related sequence of data which has all the same type. Do not try and use arrays has a hack to store multiple pieces of unrelated data. You should use loops to process arrays whenever possible.
When you first learn about arrays, it's very tempting to try and use them in such a way to return multiple unrelated pieces of information from a method at once:
public static void main(String[] args) { Scanner input = new Scanner(System.in); String[] name = getData(input); String firstName = name[0]; String lastName = name[1]; //... } public static String[] getData(Scanner input) { System.out.print("First name: "); String firstName = input.next(); System.out.print("Last name: "); String lastName = input.next(); String[] result = {firstName, lastName}; return result; }
However, this is an abuse of arrays, and is a bit of a hack. It's better to use the proper mechanism for returning multiple values, which, in Java, is typically an object.
Unfortunately, in CSE 142 and 143, we don't really permit you to construct and return arbitrary objects from your methods. As a result, you're effectively restricted to returning one and only one conceptual "thing" from any given method at a time.
This may sometimes require you to rewrite or readjust your program. For example, we'd most likely have to rewrite the above program to look something like this:
public static void main(String[] args) { Scanner input = new Scanner(System.in); String firstName = prompt(input, "First name: "); String lastName = prompt(input, "Last name: "); //... } public static String prompt(Scanner input, String message) { System.out.print(message); return input.next(); }
Instead, you should use arrays for their intended purpose – to store a sequence of related data. If you use arrays properly, you'll as a consequence will almost always operate over them using a loop of some sort.
Token and line-based processing
Do not try and mix together token and line-based processing with the same scanner. Doing so will almost always result in hard-to-debug bugs.
This style rule is less of a style rule, and more of a "if you do this, you'll have a bug rule". Most style rules exist because they help prevent bugs in one way or another, but this particular rule is unusually direct.
If you mix together token-based processing (next()
, nextInt()
,
etc) and line-based processing (nextLine()
), your code will not work:
Scanner scan = new Scanner(System.in); Integer number = scan.nextInt(); // ... some code here String line = scan.nextLine(); // bug!
It's not important to understand why exactly this happens, but to summarize, this potential bug has to do with how Scanners internally work. When you call a token method, the scanner will internally advance to the end of the current word, number, what-have-you. When you call a line method, the scanner will internally advance to the next newline.
When you call a token and then a line method, the scanner will internally advance to the end of the word and then move exactly one character over to the following newline, instead of asking the user to plug in more input.
To avoid this potential bug, you should always immediately decide when you create a scanner whether or not you intend to use that as a token scanner, or a line scanner, and stick with that decision throughout your program.
It is also ok to create a new scanner if you need to grab tokens from a specific line. For example, the following is ok:
Scanner lineScan = new Scanner(System.in); while (lineScan.hasNextLine()) { String currentLine = lineScan.nextLine(); Scanner tokenScan = new Scanner(currentLine); while (tokenScan.hasNext()) { String token = tokenScan.next(); // Do something with token } }
Interfaces
When declaring or initializing an object, you should always make the declared type an interface, not a concrete object.
When declaring an object, you should never do something like this:
ArrayList<String> lst = new ArrayList<String>();
Instead, whenever possible, you should change the declared type to use an interface, like so:
List<String> lst = new ArrayList<String>();
The reason for this is because in programming, we often like to make the distinction between an actual object, and an interface.
For example, let's say that we want to program different kinds of cars – perhaps we might have a Toyota, perhaps we'll have a Ford, etc... Each of these cars have different internals and parts, so the actual code to control them is going to be all slightly different from each other. For the sake of simplicity, let's just say we have two different classes – a "Toyota" class, and a "Ford" class.
Now, let's say that we want to create a method that'll be responsible for turning this car on, and driving it in a line for about 5 seconds, then turning it off in order to test the car.
Our tester program might look something like this:
public class TestCar { public static void main(String[] args) { Toyota t = new Toyota(); Ford f = new Ford(); testToyota(t); testFord(f); } public static void testToyota(Toyota t) { t.turnOn(); t.driveForward(5); t.turnOff(); } public static void testFord(Ford f) { f.turnOn(); f.driveForward(5); f.turnOff(); } }
Ok, cool, this works.
However, just looking at it, it seems sort of redundant, and a little silly. If all we want to do is just move the car forward for 5 seconds, why does it matter what kind of car it is? Forget about the internals and the parts – the steps to turn a car on and drive it forward are going to be pretty much the same, no matter what brand or model the car actually is. If the steps are the same, why do we need to make this distinction between the Ford and the Toyota?
Well, we can solve this problem by using interfaces. If we modify our Toyota or Ford cars to either inherit from a "Car" base class or extend the "Car" interface, then we can modify our testing program to look like this:
public class TestCar2 { public static void main(String[] args) { Car t = new Toyota(); Car f = new Ford(); testCar(t); testCar(f); } public static void testCar(Car c) { c.turnOn(); c.driveForward(5); c.turnOff(); } }
This is much cleaner. By doing Car t = new Toyota()
, we're telling Java
"yes, I'm making a new Toyota, but I want you to treat it as a Car". Then, our
testCar
method can now accept any kind of car, regardless of the specific
brand or type, and be flexible enough to handle all those different kinds of cars.
This is similar to the Critters assignment in CSE 142 – you had the "Critter" base class or interface, and made subclasses of it – the Ant, Husky, Lion, etc classes.
This same principle applies to Java's classes. When you do
ArrayList<String> words = new ArrayList<String>();
...you're telling Java to create a new instance of an ArrayList, and treat it as an ArrayList. In contrast, if you do this:
List<String> words = new ArrayList<String>();
...you're telling Java to create a new instance of an ArrayList, but treat it as a general "List" object. This is much more flexible -- if you change your mind about using an ArrayList and want to use a different kind of list instead, then you only need to modify a single line:
List<String> words = new LinkedList<String>();
To put in other words, this line is essentially saying "yes, I have to pick a certain kind of list (in this case, a LinkedList), but I'd like my code to work with pretty much any type of List in existence without any fuss". Again, we want our code to be as flexible and modular as possible.
What are comments?
Comments are specially marked lines of text in your code that can be used to describe what's happening in your program. Java ignores comments when it runs your program, so they're not necessary for actually making code work properly.
There's no exact formula to writing comments. The important details of a method or program are different depending on what the program or method does. The simplest way to know if your comments are sufficient is to ask yourself this: "If I didn't understand the code in this method, would I still be able to use it properly by reading only the comment and the method header?"
Why comments are useful
It may seem like comments are a bit unnecessary, since you're the one writing your code. The purpose of comments isn't necessarily for the author's benefit (though they can be very useful as programs get more and more complex), but rather for the benefit of others who read your code. You might have a useful method that someone wants to use for another program, for example. Another programmer shouldn't have to reinvent the wheel – they should use your method instead of writing their own. If your method is properly commented,all they should have to read to know how your method works is the comment and the first line of the method (the header).
Expectations for CSE 142 vs 143
In general, our expectations for commenting are much higher in CSE 143 compared to CSE 142.
In CSE 142, you'll get full points for commenting as long as you:
However, we have much higher expectations for commenting in CSE 143. In addition to the above, you are expected to: