This lab will introduce you to the different types of errors you may encounter, the basics of debugging in Java, utilizing the IntelliJ debugger tool, and working with GitHub.
Goal: Become familiar with resolving merge conflicts in GitHub, distinguishing types of errors, using printlines to debug, and using the IntelliJ debugger tool.
Your TAs will be adding you to a shared repository with your partner(s) for this portion of the lab. You should get an email with a link to the repository, but if not, go to GitHub and search for lab5merging
. This is the same process that we will use for partnered projects!
Once you have the URL of this GitHub repository, open the IntelliJ terminal. Move into the src/
folder, then use the command git clone <URL>
Once you’ve cloned your personal repository from GitHub, you’ll need to rename the folder from lab5merging-<yourGitHubLogin>-<yourPartnersGitHubLogin>
to just lab5merging
. To do this, right click on the folder, then go to Refactor, Rename. You will have issues running your code until you make the change.
This part of the lab is partnered but the code you write will not be identical to your partner’s.
For this part of the lab, you and your lab partner will be working on different pieces of the project at the same time – just like you will for your partner project later in the course! Decide now who will be Partner A and who will be Partner B.
We’ve provided you with a possible working solution for lab 1. Try running the program either using the terminal or the green play button in App.java
. Note that you will not be able to run your code with IntelliJ’s play button if you have errors in other packages (labs or assignments).
You may notice that the CupcakeShop
does not display any welcome message at the beginning. This is because we don’t call the method to do that - displayShopUpdate
. This method takes in a string - the message to be displayed.
Follow these instructions carefully - it’s important that you each follow your individual instructions in order.
TODO: Both partners should call this.displayShopUpdate(<some text>);
at the end of the constructor of CupcakeShop
– each of you should pass in a different display message
TODO: Partner A should add, commit, and push their code to GitHub
TODO: After Partner A has successfully pushed their code, Partner B should attempt to add, commit, and push their code to GitHub
Uh-oh! You’re unable to push your work to GitHub! This is because your partner pushed their changes to the shared repository that you’re yet to have locally. You should get an error message like this:
So how do we resolve this?
git pull
pulls any changes present in your shared repository on GitHub and merges them into the code you have locally.
TODO: To get the changes your partner made, Partner B should type git pull
in the lab5merging
directory.
When Partner B tries to run git pull
, you may see a message like this:
Github is alerting you that if you pull, a merge conflict might occur so it is giving different options of how to proceed. In this case, we do want the merge conflict to occur, Partner B should run git config pull.rebase false
and then git pull
again. Now you should see the merge conflict in your code!
Wait, another error?
You just ran into a merge conflict! Because you and your partner both tried to edit the same line of code and push your change to GitHub, it's not sure which version it should choose.
Now, let's work on resolving this conflict.
When working with a partner and using GitHub, you may run into merge conflicts. Merge conflicts generally arise when two people have changed the same lines in a file, or if one person deleted a file while another was modifying it. It’s important to know how to resolve these conflicts so you can continue to code and work. Your code will not compile and run if you have merge conflicts. You can easily see the files that have merge conflicts since IntelliJ will highlight them in a different color.
To resolve merge conflicts, we must decide which of our code or their code we want to keep. Sometimes you will only want yours, sometimes you will only want theirs, and sometimes you will want a combination of both. Communication is key when solving merge conflicts!
TODO: Partner B should open CupcakeShop.java
and look over the file.
You’ll notice some strange characters on the file:
Think of these as conflict markers. The code after <<<<<<< HEAD
are changes that you made (HEAD
represents the current commit you’re working on). The =======
line acts as a divider. The code below this line and the >>>>>>> [commit hash]
line are changes being brought in (that your partner made!), in this case from a different git commit 48ceadf56648d2736a68f88bde2d16390c4e6f87
. This long number is the commit hash. Think of it as a unique ID for each commit made on the repository.
Back to the CupcakeShop
, you should have these conflict markers in the constructor because both you and your partner made different edits to the call to displayShopUpdate
.
In order to resolve this conflict and have your code successfully compile, you should remove all of the lines in the conflict marker EXCEPT the lines that do what both you and your partner want.
For example, let's say we had a merge conflict like this:
How would we go about resolving this? The first thing that we should do is communicate with our partner about which parts of the code we want to keep. Once we decide that, we can remove everything else.
If both of you decide that you wanted the first string, you would adjust this portion of code to look like this:
return "Baker made " + this.numCupcakesMade + " cupcakes”;
Remember to remove all of the lines including "<<"
, "==="
, and ">>"
TODO: Fix the merge conflict in CupcakeShop.java
by keeping only Partner B’s code (this is the HEAD).
TODO: Partner B should add, commit, and push their code to GitHub! Once that’s done, Partner A should run the command git pull
to make sure their repository is up to date!
Once this is done, both partners should have the same code that displays Partner B’s shop update. If this isn’t the case or you’re having trouble with this, feel free to call a TA over for help!
If you take a look through the program you’ll notice the instance variables don’t follow CS15 Style Guidelines on naming conventions. Let’s fix that!
TODO: Both partners should separately change the names of instance variables in CupcakeShop.java
and Baker.java
to make the code easier to read – choose whatever name you see fit, it doesn’t have to be the same as your partner’s.
SPECIAL INTELLIJ TIP #1:
You can change the name of any variable in IntelliJ by right-clicking on the variable name, then choose Refactor > Rename
.
SPECIALER INTELLIJ TIP #2: You can change the name of any variable in IntelliJ by renaming the first instance of it, then pressing option+enter
(or alt+enter
on Windows), then selecting “rename usages of…”
and hitting enter again.
TODO: Change the shop’s menu so that order1
is different for both partners.
Here is the menu that Partner A should create:
ORDER 1: Chocolate Frosted Cupcake with a Cherry
Here is the menu Partner B should create:
Order 1: Vanilla Frosted Cupcake with Sprinkles
Feel free to work together to implement this code! But please make sure that each partner has their specified menu.
TODO: Once both of your menus are done, Partner B should add, commit, and push their code to GitHub.
TODO: After Partner B has successfully pushed their code, Partner A should attempt to add, commit, and push their code to GitHub. Then Partner A should git pull
(Note: Partner A may now need to run git config pull.rebase false
)
Notice you get another merge conflict, and this time it’s in both the CupcakeShop.java
and Baker.java
files.
Fix any merge conflicts and get your code to compile! You can choose either Parner A or B's order.
Note: You need to click on the file (or close and re-open it) before the merge conflicts will appear.
Call a TA over and show that both your and your partner’s code compiles and has the correct menu!
As you’ve now learned, while merge conflicts look scary, they’re not actually too hard to fix! It would make your life a lot easier if you could avoid them completely, though. That may not always be possible, but here are some guidelines for reducing the number of merge conflicts you encounter!
Run “git pull” before you start modifying any code during your partner projects.This will get your local code up-to-date with the repository on GitHub before you start coding, thus avoiding the chance that you modify something locally that your partner has also changed – the basic cause of a merge conflict.
Add, commit, and push your code to GitHub as soon as you finish working on your project for a period of time. Make sure that your local code is always pushed to GitHub as soon as you’ve made any changes – even if it’s just two lines. This is so that your partner can pull those changes when they start working, and the code on both of your computers will match each other.
Commit any changes you have before pulling. Git won’t let you pull changes from the remote if you have changes in your code that you haven’t committed.
Communicate! Communication with your partner is key to reducing the number of merge conflicts you encounter.
This portion will be completed individually
Click here to get the stencil from GitHub - refer to the CS15 GitHub Guide for help with GitHub and GitHub Classroom. Once you have the URL of your personal GitHub repository, open the IntelliJ terminal. Move into the src/
folder, then use the command git clone <URL>
Once you’ve cloned your personal repository from GitHub, you’ll need to rename the folder from lab5debugging-<yourGitHubLogin>
to just lab5debugging
. To do this, right click on the folder, then go to Refactor, Rename. You will have issues running your code until you make the change.
Compiler errors happen when you violate the rules of the programming language and the compiler can’t determine what you are trying to do. You can think of this as the way a spell check works, flagging any errors in your english. Examples of this would be: missing semicolon, a typo on a variable name, or calling a method that doesn't exist. Make sure you’re familiar with Java syntax to help you avoid these errors!
If you have a compiler error, you will get something like this:
Common sources of compiler errors:
Note: For this part of the lab, instead of using the green arrow to run your code, we suggest you first compile your code using javac *.java
and then run it with java lab5debugging.surfboard.App
. This way you can better distinguish between compiler and runtime errors since they often create similar-looking errors in the terminal. For this part of the lab, you’ll be working only in the surfboard
directory. Here’s a summary of the steps.
lab5debugging
directory with cd lab5debugging
surfboard
directory with cd surfboard
javac *.java
cd ..
java lab5debugging.surfboard.App
TODO: From within the surfboard
directory, try compiling the surfboard
package.
Uh oh! We’ve got our first terminal-produced error. Since this error appears when we try to compile our code, before we even get to run it, we know it’s a compiler error.
As we just learned, the information printed in the terminal can tell us a lot about the compiler error. It tells us:
Based on the information given, we can see that incorrect syntax was the cause of the error.
TODO: Use the information in the terminal to track down the source of the compiler error. Then, debug it! Try to compile the code again. This compiler error should no longer appear.
Runtime errors indicate an error in the logic of the code. These bugs occur while the program is running and can either cause your program to crash (ex: NullPointerException) or not function as expected. As time goes on, most of your time will be spent on runtime bugs, some of which can be extremely tricky.
Note: The class or method that ran into the error is not necessarily the one that caused it. This happens most often if the method accepts a parameter. For example, if you passed a null value as the actual parameter into a method, an error would result because a null value should never have been passed. You can, however, trace down from this class or method to figure out where your error was caused.
There are many different Runtime Errors. For now, we’re only going to introduce you to two, which are probably the ones that you will encounter most frequently at this point.
Cause
NullPointerException
.Tips for Finding the Bug:
You should check that every variable was properly initialized before being used, especially ones being used in the line pointed to by the stack trace.
A useful tip is to remember to check the order that you call methods in, to make sure that you are initializing variables in the correct order. Use printlines to determine where the error is occuring
Once you find where the error is being thrown, use printlines to check the value of every variable at that point. If multiple variables are being used in a single line (i.e. car.move(grid.getLocation());
), make sure to check the value of all of the variables.
Cause:
Tips for Finding the Bug
Note: Runtime errors may occur at any point while the program is running. For instance, if you have a game, you may only get a runtime error if you press a certain combination of keys. This is why testing is so important, in order for you to identify the edge cases which may make the Runtime errors arise.
Let’s see what happens when we actually try to run our code.
TODO: Move back to the sources folder with cd ..
Run your compiled code using java lab5debugging.surfboard.App
You’ll notice that nothing pops up. Looking at your terminal you should see a terminal error. This error is called a stack trace, which lists all of the methods that were executed in the order they were executed when the error occurred.
Don’t be intimidated by the error’s length! You won’t need much of the information in it to debug your code. When debugging runtime errors like this one, focus on lines in the error that reference classes you’ve written.
Let’s focus our attention on the latter half of the stack trace since it includes the most references to classes in our lab5debugging
folder.
The stack trace provides directions to the source of the runtime error. Each of the blue underlined lines refer to the class and the line that was run at the time of the error. If you click the blue underlined lines, they will bring you to that line and class.
The stack trace provides directions to the source of the runtime error. Each of the blue underlined lines refer to the class and the line that was run at the time of the error. If you click the blue underlined lines, they will bring you to that line and class.
TODO: Take a moment to click through the blue underlined references to just classes in lab5: try to track how each class interacts with each other. We recommend starting from the bottom and moving upwards in the stack trace.
Now that we know how to look for the error, how do we know what to look for?
TODO: Look through the top line of this section of the terminal-produced error, try to identify the type of exception that caused the error.
Hopefully, you’ll see that this bug was caused by a NullPointerException
. Exceptions, like this NullPointerException
, are “thrown” by Java as the result of runtime errors. Remember, a NullPointerException
is thrown when a program tries to use null when it isn’t expected (e.g.: if you call a method on an instance before it’s been initialized).
Let’s use the IntelliJ debugger to debug this!
Breakpoints let you stop your code at designated lines when debugging, giving you a chance to see the state of your code and how it runs at specific points. Breakpoints can be added by clicking in the column where the green arrow to run your code appears (in between the line number and where the code starts). A red circle should appear next to the line you would like your code to stop at when it runs. When debugging, the code will stop at this line before running it.
To remove a breakpoint, simply click on the red dot again.
TODO: Set a breakpoint on the line giving us a NullPointerException
.
Now let’s run our code. When debugging with the IntelliJ Debugger, we need to run IntelliJ’s debug mode rather than running it normally. Navigate to the App
class and right click the green play button on the left side of the code.
TODO: Press Debug 'App.main()'
in order to run the debugger. After you do this once, you can use the green bug icon next to the green play button in the top bar of IntelliJ to run in debug mode.
After running the debugger, the Debugger Console should pop up at the bottom of your screen.
TODO: Take a look at the right side of the console to see the values of our instance variables, board
, design
, and stripe
.
Notice that one of them is null. That’s what is causing the NullPointerException
on line 26 of our code!
Now that you have successfully identified which variable is causing our NullPointerException
, let’s initialize this variable to get rid of the error.
TODO: Initialize the variable in the Surfboard
class (using the constants provided in the Constants
class).
Great! We should’ve fixed our NullPointerException
now and our code should run without throwing exceptions.
TODO: Run your code again. You should now see the following (call over a TA if you do not):
Uh oh! The surfboard is showing up in all black – you can’t even tell what it is anymore :( This is known as a logical error. So let’s talk about them so that we can fix this bug!
Call a TA over to check that your code compiles and you’re getting the black surfboard
Logical errors occur when your code contains problems within its logic and structure. Code with logical errors may compile and run, but result in incorrect or unexpected behavior (e.g. Pong never stops even when a winner is found). Logical errors are the most difficult to debug since they require a deep look into your code to find the source of the problem. Logical errors typically do not produce terminal errors, so you need to practice debugging logical errors without collaborating with other students in CS15.
In the example above, we should print “a is greater than b” if a > b and not the other way around. The code compiles and runs, but it does not run as expected, which hints that it is a logical error.
Incremental coding can help you catch logical errors as you write your code. Without incremental coding you run the risk of burying the source of the logical error, making it more difficult to find later. Other tactics include using printlines to check the values of variables, using printlines to check if methods are actually being called, and using the IntelliJ Debugger.
Another great technique is "rubber duck debugging"! Imagine you have a rubber duck in front of you, and you're talking through your code with it. As silly as this sounds, talking through your code aloud can often help you spot the bug!!
Time to fix this pesky bug that makes our surfboard entirely black. Using printlines can be really useful in helping us debug errors like this.
Sometimes when you run into errors (compiler or logical) it may be useful to find out what your program is actually doing when you run it.
How could we accomplish this? One way is by printing out the values of the variables that we care about. For example, let’s say we wanted to check which contestants are competing in a challenge. We could use printlines to check like this:
System.out.println(this.challenge.getContestants() + “ are competing”);
This way, when we run our program we can access the value of the variable and find out if it is what we expect it to be! If the variable value is changed somewhere in the program, it might be useful to put a few printlines in your program to find out which change is affecting your outcome.
Another time printlines are helpful is when we think a piece of code is being executed, but it’s not doing what we want. We can place a printline right before whatever that code is to see whether it’s actually not being reached (nothing is printed out in the terminal), or it is being reached but isn’t doing what we want (the prinline prints to the terminal, meaning that the code is being reached but executed improperly).
Let’s use this second strategy to figure out why our surfboard is not pretty and designed like we want it to be.
TODO: Take a look through your Surfboard class to get familiar with the methods. Is there one that might help us make our surfboard prettier?
You probably found the makePretty
method, which seems to color all three pieces of our board. Something’s clearly wrong, though, since the board is showing up in all black. Is there a problem with our use of the setFill
method, or is this code never being executed? Let’s use printlines to figure it out!
TODO: Put a printline as the first line of this method body, make it print out something informative like “in the makePretty
method!” and run your program again.
sout
followed by a tab, and IntelliJ will autofill System.out.println
for you!TODO: Take a look at the terminal – is anything being printed out?
Nothing is printed! That is an indicator to us that the makePretty
method is never being called.
TODO: Call the makePretty
method within setUpSurfboard
.
TODO: Compile and run your code. You should now see the following:
Now that we’ve finally fixed the aesthetics of our program, it seems like it’s all done! It can never hurt to test it out a little bit more though.
TODO: Press the “Move Left!” and “Move Right!” buttons to test their functionality.
Oh no! Our move left button moves the surfboard to the right and the move right button moves left! But why is this happening? We don’t even have any errors popping up anywhere?!
This is another example of a logical error. We just saw how to use printlines to debug logical errors, now let’s learn more about the IntelliJ debugger to solve this one.
TODO: Navigate to the SurfboardMover
class and look at the code related to movement. We see that we have a moveSurfboard
method. This might be a good place to start.
TODO: Place a breakpoint on the first line of the method body of the moveSurfboard
method (line 41).
TODO: Run your program in debug mode by clicking on the bug icon in the top right of the screen. Once it appears, press the "Move Right!" button.
What happened? Notice how the surfboard disappeared. This is because the program has stopped at the breakpoint, allowing us to see the state of our variables.
We will now navigate through the program line by line and try to pinpoint the problem in our logic.
You just used the debugger to see the values of variables and solve a NullPointerException
. Now, let’s turn our attention to the row of blue and red arrows at the very top of the debugger console.
The leftmost arrow allows you to “Step Over” lines of code. When you press it, it will move through your code line-by-line, running each line of code as it goes.
TODO: Press the “Step Over” arrow. Notice how the distance
variable appears in the debugger console once we move past the line where we declare it!
TODO: Press the “Step Over” arrow again 3 more times. We can now see that we went through the if statement and found that the distance
variable is multiplied by negative 1.
Is this something that we want? Remember that as you move towards the right side of the screen, the x coordinate increases.
TODO: Change the moveSurfboard
method to fix the multiplication condition of the distance variable.
Now that we have identified our error and are done using the debugger, let’s close out of it.
TODO: Use the red stop button at the top right of the screen to stop the debugger from continuing to run your program.
Finally let’s test out if things work!
TODO: Run the program again and test out the Move Left and Move Right buttons.
TA Checkpoint 3: Call a TA over to show them your final surfboard program!
We’ve walked through a couple examples of how you can use the debugger, but now it’s time to flex your debugger skills and solve one more problem!
The Avatar gang is looking for treasure and need YOUR help!!! Zuko stumbled upon three keys and a treasure chest that can only be busted by using the debugger. Help the Avatar gang collect their booty!
Within the Treasure Hunt program, there are secret codes hidden that will unlock the locks and open the treasure chest. Your job is to use the debugger to step through and discover these codes to find the treasure! In this part of the lab, you’ll be working within the treasureHunt
directory and focusing on the TreasureHunt
class.
In the hunt()
method you’ll find 4 calls to methods from the TreasureKeeper
class: checkLock1Password()
, checkLock2Password()
, checkLock3Password()
, and checkChestPassword()
. The passwords for all of these methods are currently set to 0, and if you run the code you’ll notice that all 3 locks are locked and the treasure chest is closed.
Each time you find the code for a lock or the chest, you should enter the password as an argument to the corresponding check password method. If you’ve inputted the correct password, the lock should appear unlocked or the chest should be open when you run the code. Above each check lock password method is a method to debug that will lead you to the code. You’ll use the debugger with these methods to discover the codes!
We’ll walk through the first lock together to get you started.
This button is useful when you are moving through a method that makes calls to other relevant methods that you may want to step through. If you use were to use the step over method, the debugger would move completely over the method call and automatically execute everything in that method.
By using the “Step Into” button, we can move the debugger INTO the method that we are calling and can now move line by line throughout that method.
TODO: Place a breakpoint on the line that calls uncoverLock1Code()
(line 32). Run the debugger and Step Into
the uncoverLock1Code()
method.
You should now be inside the uncoverLock1Code()
method. You’ll be using the debugger to follow the value of the lock1Code
variable. Once you’ve reached the end of the method, the value of lock1Code
will tell you the password to enter to unlock the first lock.
TODO: Use the Step Over
tool to track the value of lock1Code
.
After two steps, you should see that the value of lock1Code
is 635. You can see this either on the code or in the debugging window:
Continue to step over until you reach the end of the uncoverLock1Code()
method and have found the final value for lock1Code
.
Remember: if the debugger is highlighting a certain line, then that line hasn't been executed yet. To get to the end of the method make sure you step through to the bottom curly bracket.
TODO: Return to the hunt()
method and enter the correct password into the checkLock1Password()
method.
If you run the code now, you should see lock 1 unlocked, like this:
Great job! You’ve used the debugger to step through the hints and found the password for lock 1. You’ll continue to use the debugger to unlock the remaining locks and open the treasure chest.
TODO: Place a breakpoint on the line that calls uncoverLock2Code()
(line 35). Run the debugger and use what you’ve learned about the debugger to find the password for lock 2!
HINT
Lock 2 needs both an int
and a boolean
to be unlocked. The boolean
is based on the value of this.lock2Condition
in uncoverLock2Code()
. You won't need to change the value of the instance variable this.lock2Condition
, just the argument passed into checkLock2Password()
.
TODO: Once you’ve found the password and the boolean for lock 2, input the correct values to checkLock2Password()
.
If you’ve found the correct values, the lock 2 should be unlocked when you run the code now.
Nice work! You’ve unlocked both locks 1 and 2. Let’s take a look at lock 3 now.
TODO: Use what you learned with locks 1 and 2 to unlock lock 3!
You’ll notice that there are several helper methods for lock 3. To discover the code, you’ll have to pay close attention to the debugger and where it tells you to find the code. You should make use of both the Step Over and Step Into tools. Be careful, not all of the methods will lead you to the code!
TODO: Once you’ve found the code for lock 3, enter it into the checkLock3Password()
method.
When you run the code now, you should see all 3 locks unlocked.
Hooray! You’ve unlocked all 3 of the locks. Onto the treasure chest!
TODO: Place a breakpoint on the checkChestPassword()
method call (line 41). Use the debugger window and the dropdown menus within it to explore the instance variables in the TreasureChest
class. You can find all of the info that you need in the debugger window - you do not need to step into or step over anything. Discover the code and open the treasure chest!
HINT
The TreasureChest
class is a component of the TreasureKeeper
, and this structure is reflected in the dropdown menus in of the debugger window!
TODO: Once you’ve found the code for the treasure chest, enter it into the checkChestPassword()
method and run your code.
If you’ve found the correct code, you should see the treasure chest open.
Great job! You opened the treasure chest and found the treasure 🥳🥳
TA Checkpoint 4: Call a TA over to show them your finished hunt!
Show a TA that you’ve finished the ATA Feedback Survey and Mid-Semester Survery to get checked off for lab!
This assignment was updated this year to improve your CS15 learning experience! If you have any feedback for us, please fill out our student opinion form here.
Here are some more tools that will be very useful when debugging!
This button will run your program until it hits the next breakpoint when you’re ready to move on!
Click on this button to add a “Condition” for the breakpoints that you have already placed! Play around with the options that will appear on the panel on the right side of the window
The variables panel will appear within your debugger panel. This panel is useful for keeping track of the values of variables. Using the “+” icon, you can add additional code snippets to keep track of the values (ex: in the image above, I can track if the isLeft boolean value is false).
For example, let’s say you break at a point where you have assigned bending arts to TAs and want to check what art each TA has. However, you only store the TAs and not their bending arts.
Luckily, we can add a breakpoint and type this.sarah.getBendingArt()
into the evaluator in order to get the value.