Lab 1 - About the SU Computer Science and Engineering Department

READ ME 1 THE LABORATORY APPROACH HOW TO READ THIS TECHNICAL MATERIAL SETTING UP ECLIPSE 1 2 3 LAB 1 -­ READING CODE 6 BACKGROUND TYPES OF LANGUAGES AND THE JAVA VIRTUAL MACHINE INTEGRATED DEVELOPMENT ENVIRONMENTS SETTING THINGS UP SYNTAX ERRORS KEYWORDS WRITING SOME CODE RUNNING THE CODE THE ECLIPSE DEBUGGER USING THE DEBUGGER TO PAUSE THE PROGRAM THE DEBUG PERSPECTIVE VARIABLES ASSIGNMENT STATEMENTS WATCHING AN ASSIGNMENT WHILE LOOPS THE LOOP IN OUR CODE SIMPLE OUTPUT WATCHING CONDITIONALS 6 6 8 8 9 9 9 10 11 11 12 14 15 16 16 18 18 19 LAB 2 – READING ARRAYS 22 GENERATING RANDOM NUMBERS LAB BACKGROUND ARRAYS FOR LOOPS SETTING THINGS UP TO PLAY WITH ARRAYS WHAT DOES IT DO? CONSTANTS HOW DOES OUR PROGRAM DO THAT? MATHEMATICAL OPERATORS THE OUTPUT MORE TRIALS? WHERE ELSE CAN WE USE THIS? WHAT IF THE RANGE GOES NEGATIVE? CONCLUSION 22 23 23 25 26 28 28 29 31 32 32 32 33 35 LAB 3 – PRINTING GRAPHS AND METHODS 37 METHODS OUR FIRST METHODS FIRST A SIMPLE GRAPH COMPUTING THE HORIZONTAL POSITION 37 37 41 43 ROUNDING ERRORS IT WORKS FOR OTHER FUNCTIONS, TOO METHODS CAN HAVE MULTIPLE PARAMETERS GRAPHING THE AREA BETWEEN TO FUNCTIONS 45 46 46 47 LAB 4 – CLASSES AND OBJECTS 53 INTRODUCTION STRUCTURE OF A CLASS SETTING UP THE CARD CLASS CREATING AND USING OBJECTS CONSTRUCTORS VARIABLE INITIALIZERS EXPLORING OBJECTS WATCHING THE CONSTRUCTOR WORK THE TOSTRING() METHOD FIXING OUR OUTPUT NAMING CONVENTIONS OUTPUTTING THE SUITS ONE MORE TIME! THE DECK CLASS NESTED FOR LOOPS WATCHING THE CREATION OF THE DECK NOW FOR THE OUTPUT LET’S SHUFFLE THINGS UP THE SHUFFLE CODE CONCLUSION 53 54 54 55 57 58 59 61 62 62 63 64 65 66 68 69 71 71 72 73 LAB 5 – DETAILS ABOUT VARIABLES 76 INTRODUCTION VISIBILITY MODIFIERS SETTING UP OUR CLASS GETTERS AND SETTERS ADDING GETTERS TO OUR CODE SIZE LIMITATIONS PLAYING WITH SIZE WATCHING THE OVERFLOW LET’S TRY LONG VARIABLES IF-­THEN-­ELSE STATEMENTS MAKE THE SEQUENCE WRAP BIGINTEGERS LEARNING ABOUT BIGINTEGER JAVADOC COMMENTS SETTING UP ECLIPSE TO GENERATE JAVADOC COMMENTS PRIMITIVE VS. REFERENCE TYPES USING BIGINTEGERS 76 76 77 78 79 81 83 83 84 84 85 87 87 88 89 89 91 LAB 6 – TEST DRIVEN DEVELOPMENT AND MORE ABOUT VARIABLES 96 TEST DRIVEN DEVELOPMENT 96 DESIGNING OUR CLASSES SETTING UP OUR TESTS MAKE THE TEST COMPILE MAKE THE TEST PASS CLEAN UP THAT MESS! TDD IN MORE DETAIL CHECKING OUT A BOOK (PHASE 1) CHECKING OUT A BOOK (PHASE 2) SCALE IN TDD CHECKING A BOOK BACK INTO THE LIBRARY VARIABLE SCOPE AND LIFETIME INSTANCE VARIABLES LOCAL VARIABLES METHOD PARAMETERS FOR LOOP VARIABLES IMPLICATIONS OF SCOPE THE KEYWORD THIS @BEFORE IN JUNIT REFACTORING THE SET UP OF OUR TESTS NUMBER OF TIMES A BOOK HAS BEEN CHECKED OUT (PHASE 1) NUMBER OF TIMES A BOOK HAS BEEN CHECKED OUT (PHASE 2) NUMBER OF TIMES A BOOK HAS BEEN CHECKED OUT (PHASE 3) TEST DRIVEN DEVELOPMENT REVIEW 97 97 98 100 100 101 101 102 103 103 104 105 105 106 106 107 108 109 109 110 111 111 112 LAB 7 -­ TEST DRIVEN DEVELOPMENT AND CONDITIONALS 114 REMINDER: TDD SET UP OUR FIRST TEST MAKE THE TEST PASS CLEAN UP BORDER CASES IF-­THEN-­ELSE STATEMENTS WRITING OUR FIRST CONDITIONAL NESTED CONDITIONALS THE DANGLING ELSE PROBLEM MORE TAX LEVELS SIMPLE DEPENDENT DEDUCTION VARYING DEDUCTIONS PROBLEM DECOMPOSITION MAKE TWO LEVEL DEDUCTIONS PASS FIRST SPOUSE TAX SECOND SPOUSE TAX NOW THINGS ARE GETTING TRICKY THE TRICKIEST TAX OF THEM ALL 114 114 115 115 116 116 116 117 118 119 120 120 121 122 122 122 123 124 124 LAB 8 – LOOPS AND STRINGS 127 INTRODUCTION SET UP COUNT-­CONTROLLED LOOPS 127 127 131 THINGS WE CAN DO WITH STRINGS COUNTING BLANKS MORE DETAILS ABOUT UNICODE COUNTING LOWER CASE LETTERS BORDER CASES FOR LOWER CASE LETTERS SENTINEL-­CONTROLLED LOOPS THE POSITION OF THE FIRST BLANK STRINGS WITH NO BLANKS? MORE COMPLEX CONDITIONS EARLY EXIT CONDITIONS TWO THINGS TO STOP THE LOOP WHERE’S A SPECIFIC BLANK? OFF THE END OF THE STRING AGAIN STRING CONTAINMENT MORE THINGS YOU CAN DO WITH STRINGS STRINGS CONTAIN STRINGS PARTIAL MATCHING WITH REGULAR EXPRESSIONS 131 132 133 133 134 134 134 135 136 138 139 140 140 141 141 142 143 LAB 9 – WRITING METHODS TO SORT AN ARRAY 146 BACKGROUND SETTING THINGS UP BUBBLE SORT BUILDING BUBBLESORT ESTIMATING RUN-­TIME SELECTION SORT BUILDING SELECTION SORT (PHASE 1) MORE TESTS BUILDING SELECTION SORT (PHASE 2) RUN-­TIME ANALYSIS OF SELECTION SORT INSERTION SORT BUILDING INSERTION SORT RUN-­TIME ANALYSIS OF INSERTION SORT BEST CASE ANALYSIS 146 146 147 149 151 152 152 154 155 156 157 159 160 162 LAB 10 – ITUNES SONGS 164 INTRODUCTION GETTING STARTED SONG TOSTRING() SONG TOSTRING() AGAIN ARRAYS OF OBJECTS CREATING ALBUMS TEST ADDING ONE SONG TEST FILLING THE ALBUM TESTING DURATION (PHASE 1) TESTING DURATION (PHASE 2) TESTING DURATION (PHASE 3) TESTING TOSTRING() CONCLUSION 164 165 165 166 166 167 169 171 171 172 172 173 173 LAB 11 – ARRAY PRACTICE WITH ZIP CODE ENCODING 175 A REVIEW OF STRINGS ZIP CODE ENCODINGS SET THINGS UP START WITH THE CONSTRUCTOR CONSTRUCTOR BORDER CASES PART 1 TOSTRING() PHASE 2 MORE TOSTRING() TESTING TESTS FOR COMPUTING CHECK DIGITS THE MOD OPERATOR COMPUTING THE CHECK DIGIT BORDER CASES FOR CHECK DIGITS? BAR CODES -­‐ THE TEST REFACTORING HOW WE STORE THE ZIP CODE (BACKGROUND) REFACTORING HOW WE STORE THE ZIP CODE (PHASE 1) REFACTORING HOW WE STORE THE ZIP CODE (PHASE 2) REFACTORING HOW WE STORE THE ZIP CODE (PHASE 3) INITIALIZING ARRAYS AT DECLARATION BACK TO BAR CODES BAR CODE GENERATION (PHASE 1) BAR CODE GENERATION (PHASE 2) BAR CODE GENERATION (COMPLETION) LET'S BE THOROUGH 175 175 176 177 178 178 178 179 179 179 180 180 181 182 182 183 183 184 184 185 185 185 LAB 12 – METHOD OVERLOADING AND THROWING EXCEPTIONS WITH ZIP CODE DECODING 188 INTRODUCTION METHOD OVERLOADING BUILDING THE NEW CONSTRUCTOR DECODING THE BAR CODE (PHASE I) DECODING THE BAR CODE (PHASE II) DECODING THE BAR CODE (PHASE III) WHAT COULD GO WRONG? THROWING EXCEPTIONS TESTING FOR EXCEPTIONS TESTING FOR BAR CODES THAT ARE TOO SHORT MORE BARCODE EXCEPTIONS BONUS – MORE EXCEPTIONS 188 188 189 189 190 190 190 191 192 192 193 194 LAB 13 – BUILDING AN APPLICATION AND CATCHING EXCEPTIONS 195 INTRODUCTION PREPARING FOR USER INPUT SCANNER GETTING USER INPUT HANDLING CONVERSIONS IN BOTH DIRECTIONS WHAT IF THE BAR CODE IS INVALID? CATCHING EXCEPTIONS HANDLING BAD BAR CODE INPUT 195 195 196 196 196 197 198 200 LET THE USER ENTER CODES MORE THAN ONCE 201 LAB 14 – HUMANS AND ALIENS 203 INTRODUCTION HIT THOSE ALIENS! RANDOMNESS DON’T FORGET THE BORDER CASES ALIENS CAN RECOVER 203 203 203 205 205 STRANGE SITUATIONS IN RECOVERY 205 PLANNING FOR HUMANS INHERITANCE CREATING OUR HIERARCHY START MAKING HUMANS WHAT ABOUT TAKING HITS? TEST SUITES WEAPONS LET’S PICK UP CAREFULLY USING WEAPONS CAN’T SHOOT WITHOUT A WEAPON OVERRIDING METHODS HUMANS ARE SMARTER THAN ALIENS PICKING UP MORE WEAPONS ANOTHER BORDER CASE SWITCHING WEAPONS 206 206 208 213 213 214 214 215 215 216 216 217 218 219 219 GLOSSARY 221 INDEX 226 Read Me
The Laboratory Approach
This book uses a unique approach to teaching Java programming. Using a sequence of laboratory
exercises, the text guides you through three distinct phases designed to move you from a novice
to a basic level Java programmer. The goal is for you to spend more time “doing” than
“reading.”
To get some insight into the approach we have taken with this book, imagine the skills required to
be a chef. Chefs need to know about many individual ingredients like egg whites, lemons, and
cream of tartar. Chefs also gain experience from following existing recipes. However, knowledge
of ingredients and the ability to follow existing recipes only qualifies them as cooks. In order to
be a chef, they have to be able to create entirely new recipes from scratch. That requires
additional knowledge about how the ingredients interact with each other and a sense of the
particular culinary experience they want to create. It also requires experimentation to develop
one’s own insight into what makes a recipe “good.”
The cook/chef comparison captures the goal of this book. If we only wanted to teach you to be a
coder, then teaching you the details of the language would suffice. A textbook for a coder would
include a reference library to all possible language constructs (the ingredients) and rote examples
showing how the various constructs solve specific problems (recipes). However, as computer
scientists and software engineers, you are much more than coders. You are, in practice, chefs.
You need to be able to use the language constructs to solve problems that have never been solved
before (create new recipes).
In addition, computer scientists and software engineers need to acquire the skill of learning new
features of a language as the language changes. So, memorization of all of the details is not the
goal. Instead, you will learn about the language in much the same way that engineers learn about
new features on the job. Instead of listing every Java detail in this text, we will often refer you to
the standard Java documentation; so you will learn how to find and incorporate new ideas into
your work. So remember, your goal is not to be able to follow someone else’s work; your goal is
to be able to create completely new solutions.
The first phase of this book is focused on reading Java programs. During this phase, you’ll learn
various Java language constructs and how they can be used to evoke particular behaviors. You
will not develop code on your own, yet, and you don’t need to know how the author wrote the
code. The only goal is for you to understand how the language causes the behavior that you see.
You will observe these behaviors with an application called Eclipse that is used by software
engineers to develop code in industry. Eclipse has a feature called a debugger that will let you
watch the code execute one statement at a time displaying exactly what each statement does.
The second phase of this book will teach you the basics of writing code. You’ll develop small
pieces of code for very specific problems. These problems are designed to help you learn to write
code with each of the major Java constructs. In addition, when your code isn’t behaving the way
you expected, you’ll learn to use the debugger to help you find and fix the problems yourself.
Finally, the third phase of the text is focused on writing solutions to more complex problems with
increasingly less support from the text. You’ll use the knowledge you gained from the first two
phases to apply the Java constructs to solve these much more interesting problems. This section
will continue to help you develop your debugging skills and will also cover a variety of strategies
for how to develop an entire application.
1
In addition to the programming activities, we will explore some of the types of questions
computer scientists and software engineer wrestle with in real world situations.
How to Read This Technical Material
Each chapter of this book is based on a lab activity with the necessary instructional content
interspersed. You will notice that the text will change between three different fonts representing
the three elements of this text: content, descriptions of the lab activities, and code. Color is used
extensively to distinguishing the parts of code. We use the same color scheme found in Eclipse,
so code you write in Eclipse will appear the same as it does in this text.
The font you have been reading is used for content.
This is the font used when lab instructions are given. The font will change again
when code is presented as follows:
int x;
while (x < 2)
{
System.out.println(x);
x = x + 1;
}
System.out.println("Finished");
We have attempted to make the material in this text as readable as possible; however, there are
some specific strategies you can use to get the most out of the time you spend reading. Reading
technical material isn’t a passive process. To truly understand the subject you must actively
process the text.
In Literature classes, you were taught to look for symbolism and foreshadowing. In order to do
that, you couldn’t just read the words. As you read, you had to think about what the author
intended with each sentence. You were trying to get into the mind of the author. You should
take the same approach when you are reading technical material.
As authors, we have carefully thought about the overall structure of this text and about each
sentence and example it includes. In order to truly understand what you are reading, it is helpful
to ask yourself questions like, “Why would the author put this topic here?”, “Why did the author
pick that example?”, “What is the important point being made here?”, and “Why is it important
that the concept is true?” Asking those types of questions will help you see beyond the facts on
the page. You will comprehend their relevance. That analysis will help you truly learn the
material in this text.
This text includes example code with descriptions. The descriptions of each piece of code are
essential to understanding the code. Reading them completely will give you insight that will
decrease the time it takes you grasp how the code works. However, reading the book’s
descriptions isn’t enough. When you are reading a section of code, pretend you are the computer,
and execute the code by hand to see exactly how it works. This may take a little more time, but it
is critical to your understanding of the material. If you can’t mimic the examples, you will not be
able to write your own code.
The content portions of the chapters assume that you are doing the laboratory activities as you
read them, especially once you are writing your own code. If a section of the lab stumps you, it is
often helpful to re-read the previous content section. If that doesn’t help, reading the next section
2
might help, but you shouldn’t go too far before you get the lab activity to work. That will assure
that you have learned the material in that section.
Setting Up Eclipse
One thing that software engineers agree on is that good tools are critical to efficiently developing
software applications. Therefore, some of the labs in this text specifically involve the Eclipse
Integrated Development Environment (IDE). This is an open source piece of software that you
can download and install from http://www.eclipse.org . The installation instruction should be
found there.
After you have installed Eclipse and brought it up, it should show you a window offering
tutorials. Feel free to play with the tutorials. When you finish with those, Eclipse will take you
to the workbench. At this time, we would like you to change Eclipse’s default formatting. The
examples in this book format code slightly differently from the basic conventions of Java. We
made this choice because we have found that novice programmers often read code more easily in
the non-default format.
Do the following: select Project->Properties from the pull-down menus at the top of the screen.
That will bring up another window that lets you change many aspects of Eclipse’s configuration.
On the left side of that window, click on the triangle next to “Java Code Style.” Under those
options, click on “Formatter.” Your window should look like this:
Since we want to change the setting for all of your projects, click on “Configure Workspace
Settings” near the top right corner of the screen. Then you should see this:
3
Eclipse won’t let you change the built in settings, but you can create your own profile by clicking
the “New” button. Give your profile a name, and click OK. That should bring up this window:
Here you can change many aspects of the way Eclipse will format your code, but we only need to
change one thing to match the examples in this text. Click on the “Braces” tab and change all of
the pull-downs to the value “Next line” like this:
4
Click OK until you are back at the desktop and you’re ready to go!
5
Lab 1 - Reading Code
Background
One definition of computer science is the study of algorithms. Informally, an algorithm is a
sequence of steps that accomplish a particular task. We use algorithms every day. For example,
the algorithm for brushing your teeth could be:
1. Put some tooth paste on the tooth brush
2. Use the brush in an up-and-down motion to clean the front and back of every tooth
3. Spit some of the tooth paste into the sink
4. Use the brush to clean the chewing surfaces of your teeth
5. Spit the remaining tooth paste into the sink
6. Rinse off the tooth brush and put it away
That could be considered to be an algorithm because it is sequential (you do the steps in order)
and it gets the job done.
When we build an algorithm, we have to be aware of how the algorithm will be executed because
it will only work if the executer (person or machine) understands the algorithm. This requires
two things. First, the algorithm has to be specified in a language that the executer understands. If
you tell an American how to brush his teeth in German, you are unlikely to get the result you’re
hoping for. Second the algorithm has to be specified at an appropriate level of detail. Consider
this algorithm for washing your hair (which is on many shampoo bottles):
1. Lather
2. Rinse
3. Repeat
These steps actually require that the executer knows something about the goal of the algorithm.
The first instruction doesn’t specify what to lather, how much shampoo to use, how long to lather,
or even describe what a lather is so you know when you have done it. The last instruction is even
more vague: are we supposed to repeat all three steps forever, the first two steps, or just the rinse?
It isn’t very specific.
Computer programming is the process of specifying an algorithm in sufficient detail so that the
computer will understand exactly what to do. That means we have to write it in a language the
computer understands (in our case that will be Java) and at a level of detail appropriate to the
computer. You will quickly find that computers are terribly naïve. They won’t do anything you
don’t tell them to do, and they will do exactly what you tell them to do even if it isn’t really what
you meant!
Types of Languages and the Java Virtual Machine
Computer scientists are in the business of finding ways to improve computing complex tasks or
simplifying tedious tasks. As it turns out, once a solution is well thought out, the very act of
writing code that the computer can read is a tedious job in itself. To simplify the process of
writing code, computer scientists have developed several pieces of software that act as tools for
software developers.
6
When you look at the details, the processors in our computers don’t understand Java at all. In
fact, they only understand a very primitive language made up only of ones and zeros called
machine code. When computers were first built, programmers had no choice but to program in
ones and zeros. That was very tedious and error prone. The first improvement we made was to
create assembly languages that gave alphabetic names to various parts of the instructions the
machine understood. Then, for example, the programmers could use the command ‘ADD’ instead
of the machine code ‘01011’. They built pieces of software called assemblers that translated the
alphabetic names into the ones and zeros the machine needed.
Although assembly language is a great improvement over machine code, the programmer still
needs to understand the low level operations of the target machine (the machine that will
execute the code). This is because there was a one-to-one relationship between assembly
language instructions and machine instructions for each particular machine. If you moved an
application from one machine to another whose instruction set was different, you would be
required to completely redevelop the application for the second machine.
For this reason, programming in assembly language requires a strong understanding of the
underlying machine and is error prone because of the level of detail required when we specify an
algorithm. As a result, the next tools developed were high-level languages, like C/C++ and Java.
These languages are different from assembly languages because one statement in a high-level
language may be translated into many machine instructions. They are designed to make
programming less tedious and error prone. However, in order for these languages to work, we
had to build programs, called compilers, which translate the high-level language programs into
the machine language.
7
While the language output by the compiler is the same as the one output by the assembler, we
often call the code produced by a compiler object code.
High-level languages were a great improvement over assembly languages and significantly raised
the scope of the problems software engineers could address. However, they have one Achilles
heel: the object code runs on exactly one type of processor and may even depend on the operating
system. This is why, when you purchase software (you buy object code that the computer can
read, not source code that we can read), you have to buy the version that is built for your
machine. While software engineers architect systems to try to minimize the amount of high-level
source code that depends on the processor or the operating system, significant resources are spent
on the process of changing the source code so that the compiler of a new machine can translate it
successfully. This is known as porting code.
Java was developed in the age of the Internet and was designed to hide the details of the target
machine from the compiler. In order to do this, they specified a software “machine” called the
Java Virtual Machine (JVM). The operations supported by this machine are standard, but there
is a different JVM for each target machine. The Sun engineers who develop java produce most of
these virtual machines (except the one for Macs that is developed by Apple). The Java compiler
translates java source code into Java byte code, the language that the JVM understands.
There is one significant difference that results from this strategy: instead of the compiler building
object code that runs directly on the machine, the object code produced by the Java compiler is
interpreted by the JVM. This means that, as the JVM is running the object code, it is actually
translating it to the machine language understood by the target machine. When Java was
originally developed, this made execution of java programs slower than for other high level
languages. However, over the years, the Java developers have optimized the JVMs to eliminate
this problem.
Integrated Development Environments
Whenever computers scientists can, they build tools that make the job of building systems easier.
In recent years, these tools have been packaged together into software applications called
Integrated Development Environments (IDE). These applications group the actions of writing
code, running code, and diagnosing problems in the code all in one application.
Setting Things Up
In order to experiment with some code, we’re going to use a powerful tool called
Eclipse. Software engineers use Eclipse to build systems and we’ll learn many of its
capabilities. This lab uses its “debugging” tools to show how Java code works.
8
To start, bring up Eclipse and create a new Java project using File -> New ->
Project. Open the ‘Java’ wizard folder, select ‘Java Project’ and click ‘Next’. You
can name the project anything you like. Leave all default values and click ‘Finish’.
Select your new project in the Package Explorer on the left side of the screen.
Create a new class by clicking the "new" button (
) near the top left of the
screen. In the ‘Java’ wizard folder, select ‘Class’ and click ‘Next’. Name the class
"Potatoes" and click "Finish".
Eclipse will start you out with code that looks something like this:
public class Potatoes
{
}
Your Eclipse settings may have caused it to build slightly more interesting code. In
particular, you might see sections of code that start with ‘/**’ and end with ‘*/’.
These are called comments and are completely ignored by the computer. Their only
purpose is to help humans read and understand the code.
Syntax Errors
All languages of some overarching grammatical structure, called its syntax. For example, in
English, there are very specific rules that govern the use of commas. Computer languages also
have specific syntax rules that all programs must obey. As you type, Eclipse is checking your
program against those rules. When your code breaks these rules, the errors are called syntax
errors and Eclipse marks them with red squiggles. Sometimes, Eclipse will jump ahead and give
you a red squiggle before you finish typing a statement – those you can just ignore.
Also, sometimes it’s difficult to tell when the Eclipse re-checks your code when you try to fix the
squiggles. If you think you’ve fixed the problem and you want to force the compiler to run, just
save the file (the little disk icon at the top of the screen).
Keywords
In addition to the structural rules defining its syntax, Java gives special meanings to some words
that are called keywords. Eclipse will color the keywords in maroon, so you can see that our
program is using two keywords so far: public and class. In general, keywords are special in that
they are specific instructions to the compiler. Sometimes they are called reserved words because
you cannot use them in other ways in your program; the word “public” means something special
and you can only use it in specific situations as defined by Java.
Writing Some Code
Now we’re ready to put some real code into our class. Make your class look like
this:
9
/**
* This program prints out the words to a rhyme used by children
* to select someone.
*/
public class Potatoes
{
/**
* The sequence of instructions that should execute when we
* run the program
*
* @param args just ignore these for now!
*/
public static void main(String[] args)
{
int potatoNumber;
potatoNumber = 1;
while (potatoNumber < 8)
{
System.out.print(potatoNumber);
System.out.print(" potato");
System.out.println("");
potatoNumber = potatoNumber + 1;
}
System.out.println("More");
System.out.println("You're out!");
}
}
If you make any errors in this, Eclipse will underline them with red squiggles (like
spelling errors in Word). Remember that the computer is truly stupid; capitals and
all punctuation must be exactly as they are shown here.
Also, notice that Eclipse is trying to be helpful by doing the indenting for you.
While the whitespace (tabs and blanks) between words doesn’t matter to the
computer, it does make the code easier for people to read. As we play with this
code, you’ll see the motivation for the indentation.
Don’t go on until your code looks like the example and there are no red squiggles.
Running the Code
Before we analyze this too far, let’s just let the machine run the code to see what
happens. Right click on the name of the class in the package explorer and pick Run
As -> Java Application. (Click ‘OK’ if a “Save and Launch’ window appears.)
10
It may take a second or two for the JVM to initialize itself and run the code, but
when it does, Eclipse will show you the results in the Console tab near the bottom
of the screen. It should look like this:
1 potato
2 potato
3 potato
4 potato
5 potato
6 potato
7 potato
More
You’re out!
Though the output isn’t exactly correct, our program is “chanting” a rhyme from
childhood! Don’t go on until you see this output.
The Eclipse Debugger
One of the really powerful capabilities of Eclipse is its debugger. A debugger is a tool that stops
the execution of the code and let’s us watch it work one statement at a time. This is an excellent
way to figure out what the code is doing.
One of the common ways to use the debugger is to set breakpoints. A breakpoint marks a line in
the code telling the debugger to pause the execution of the program at that line. So, when we run
the program using the debugger, it will run normally until it hits a breakpoint. At the breakpoint,
the debugger will stop executing the program and Eclipse will change perspectives to let us take
full advantage of the debugger’s capabilities. While the debugger has the program paused, we’ll
be able to look at what the program has been doing, and, once the program is paused, we’ll be
able to make the debugger pause after every statement. That way, we’ll be able to see what each
statement does.
Using the Debugger to Pause the Program
In order to watch how this code runs, we want to set a breakpoint right where the
program starts running. Now, look at your code and try to guess where it starts.
(You’d be surprised how easy it is – just read the code from the top and ignore the
stuff that doesn’t make any sense).
We’ve already talked about the comments (the text between /* and */). Since that
is for human consumption only, that can’t be where the program starts.
There are two statements that start with public; at this point, they probably don’t
mean anything to you. For now, just consider them to be instructions to the
compiler letting it know what our code should be named.
The next statement we could consider is “int potatoNumber;”. We’ll come back
to that one in a minute. For now, just know that it’s more instructions to the
compiler.
11
Finally, we see the statement “potatoNumber=1; ”. That should look like algebra
to you. Just like in algebra, we have a variable, and its value is 1. That statement
actually does something, so that’s where we want to put the breakpoint.
To put a breakpoint on a line, you just have to double click on the blue edge of the
frame at that line. When you’ve done that, you should see a blue dot on that point:
Now we are ready to run the debugger. Right click on the name of the class in the
package explorer and select Debug As -> Java Application. If it asks you to save
the file, let it save everything. When it runs, it should ask you for permission to
change to the debug perspective. Let it make that change and the layout of the
frames within Eclipse will change. That means the debugger has paused the
execution of the program and we’re ready to look around.
The Debug Perspective
In Eclipse, the debug perspective lets us control the debugger and shows us what is
happening in the program. In the middle of the window, you see your program with
a greenish line showing where the debugger has paused it. The frame at the
bottom of the window can show you a number of types of output from your program.
If you click on the “Console” tab, you’ll see the same output you saw in the Java
perspective. Our program has not output anything yet, so that will be blank for
now.
The frames at the top of the screen are the heart of the debugger. In the left,
you should see this:
12
That shows us that one program is running (the one we told it to run!) and it is
stopped in “main” at line 18 (if you put a different number of blank lines into your
code, the line number may be different). Look back at the code and find the word
main. It’s in one of the “public . . .” statements. Essentially, it is naming the section
of code that is running and that section is contained within the curly brackets (“{“
and “}”) that follow that declaration.
This part of the debug perspective also lets us control the debugger. At the top of
this frame there are buttons to control how the program runs. In the first section,
you see DVD-like controls that let you run, pause, and stop the program. If we
wanted to just let it finish, we could use those. However, we want to watch it run
one step at a time; the next section of buttons tell the debugger to run one
statement. For now, we just want to use the second of these buttons:
which
we will call “step over.” Essentially, it runs the one statement that the debugger is
paused on and stops on the next statement (it is stepping “over” the current
statement). Click that button once and you should see two things change: the line
number in the debug frame should change, and the greenish line in the frame
showing your code should move to the “while” statement.
There’s one more important part of the debug perspective: The variables view in
the upper right hand frame:
13
This view shows us the values of all of the variables in use at the time the debugger
stops the program. For now, ignore the “args” variable. The second line shows our
variable “potatoNumber” and that it has a value of 1.
Variables
Variables let the computer store information that changes as the program runs (i.e. their values
vary). Variables are made up of three things:
1. a name – we can name a variable with any continuous sequence of characters that follows
these rules:
•
it isn’t a keyword
•
it starts with a letter, a dollar sign ($), or an underscore (_)
•
it does not contain any white space
2. a value – we say a variable “holds” a value
3. a memory location – a place the compiler puts the value to be remembered
It’s important to remember that the compiler cares about capitalization. For example, a variable
named “isMine” is different from a variable named “ismine”. There is a convention that, when a
variable name contains multiple words, the words are not separated by anything (some languages
prefer that the words be separated by a character like underscore) and all but the first word are
capitalized. This is called camel capitalization. (ex. “thisVarUsesCamelCaps”)
In order for the compiler to set up the memory required for a variable, we have to “declare” the
variable to the compiler. This declaration tells the compiler the name of the variable and the type
of information the variable will hold. Then the compiler knows how much memory to allocate
for that variable. For example, this statement:
int count;
declares a variable whose name is count and whose type is “int”. This means that the variable
will hold an integer. Memory diagrams have a particular structure and can help us envision
how the computer is storing information. For this declaration, the memory diagram would be
this:
14
That diagram shows that the variable named count is holding a zero (and the lack of a decimal
point implies that the value is an integer). We will use memory diagrams to demonstrate how
various pieces of code manipulate the values in the computer’s memory.
There are quite a few other types of variables that the Java compiler understands. For now, you
need only worry about these:
•
int – holds positive and negative integers
•
double – holds positive and negative real numbers
•
boolean – holds the values true and false
Assignment Statements
We can change the value that a variable holds with an assignment statement. These statements
have a very specific structure:
<variable name> = <expression>;
For example, if count is a variable that is declared to be an integer,
count = 32;
will store the value 32 in the variable named count resulting in this memory diagram:
Notice that, while this statement uses an equals sign, it isn’t really declaring that count and 32 are
equal. It’s better to read it as “count gets the value 32.” In other words, the left hand side of the
equals must always be the name of a single variable where we want to store the information and
the right hand side evaluates to the value we want to store.
The expressions we can use in these statements can include a variety of types of operations:
mine = 32;
count = mine + 2;
In the first statement, the 32 is stored into the variable named mine. In the second statement, in
order to evaluate the right hand side of the statement, the computer will get the value stored at
mine (which is 32) and then add 2 to that value. The result (34) will be stored into the variable
named count. Again, an assignment statement does these things (in this order):
1. evaluate the right hand side and
2. store the result in the variable on the left hand side.
If you incorrectly think of assignment statements as “equals,” they will may seem very confusing.
For example, look at this code:
15
int count;
count = 2;
count = count + 4;
The first statement declares a variable named count that holds an integer. The second statement
stores a 2 into that variable. The third statement is where things get interesting. If you read it as
“equals” it looks like it is patently wrong. How can count be equal to count+4? That would
mean 2 equals 6. However, reading it as “count gets the value of count+4” gives a much better
perspective on what the computer will do. First, it must evaluate the right hand side of the
statement by getting the value stored in count (2) and adding 4 to it resulting in the value 6.
Second, it stores that result (6) into the variable named on the left hand side of the statement
(count). In the end, count will have the value 6.
Watching An Assignment
Notice that the statement that you stepped over (potatoNumber = 1;) was an
assignment statement and that’s how the variable potatoNumber got the value 1.
Also, if you look a little further up in the code, one of the statements we ignored
was
int potatoNumber;
That declared our variable potatoNumber and told the compiler that it would hold
an integer.
While Loops
We’ve seen that the JVM executes our instructions sequentially (in order), but sometimes we
want to do something more than once. One way we can accomplish that is by using a while
loop. As an example, look at this code:
int count;
while (count < 2)
{
System.out.println(count);
count = count + 1;
}
System.out.println("Finished");
When JVM executes a while statement, it will evaluate the condition and, if it is true, will execute
all of the statements inside the curly brackets. Then it will go back to the condition and, if it is
still true, it will again execute all of the statements in the loop. It will continue to do this until the
condition is false and then the JVM will continue executing the code after the loop.
In our example, this is what the JVM would do:
16
Java Statement
int count;
while (count < 2)
System.out.println(count);
count = count + 1;
while (count < 2)
System.out.println(count);
count = count + 1;
while (count < 2)
System.out.println("Finished");
JVM’s action
Value of count after
statement is complete
Create space for the
variable count;
0
Evaluate (count< 2).
Since that is true,
decide to execute the
instructions inside the
loop.
0
Print the value of count
to the console. (The
output will be “0”)
0
Evaluate “count+1”
which will be 1 and
store it in the variable
count.
1
Evaluate (count< 2).
Since that is true,
decide to execute the
instructions inside the
loop.
1
Print the value of count
to the console. (The
output will be “1”)
1
Evaluate “count+1”
which will be 2 and
store it in the variable
count.
2
Evaluate (count<2).
Since that is false, stop
executing the loop and
execute the first
statement following the
loop.
2
Output “Finished” to
the console
2
17
The Loop in Our Code
Now, look at the statement the debugger is paused on:
while (potatoNumber < 8)
That looks like it’s going to let something happen while the value of the variable
potatoNumber is less than the value 8. Right now, potatoNumber has the value 1, so
somewhere that value must change or the while statement will loop forever. Look
further down in the code to find the code that changes that value.
In order to figure out what that while statement is really doing, use the step over
button as many times as you like to watch the code run. After each step, look at
where the program is stopped and what the value of the variable potatoNumber is.
If you want to start the program over, first, stop it by clicking on the stop button
and then you can restart it by right clicking in the view that shows your code
and selecting “Debug As -> Java Application” again. It will remember the breakpoint
that we have set at the beginning of the program.
Watch the code run until you can answer these questions:
1.
2.
3.
4.
How does the value of potatoNumber change?
What is the while statement doing?
What is the purpose of the curly brackets?
What is the purpose of the way we indented the code?
Simple Output
If you look carefully, there are two statements that are causing things to be output
to the console: System.out.print and System.out.println.
In these statements, the expression inside the parentheses is evaluated and put
out to the console. Watch these statements execute until you think you understand
the difference between print and println. For each of the following, make a
hypothesis about what would happen. Then make a change to the code and run it to
test your hypothesis:
1. What is the difference between print and println?
2. What would the output look like if we made them all be “print”? if we made
them all be “println”?
3. Why does the potatoNumber in the first statement not have quotes, while
all of the rest of the things being output are in quotes?
4. In the statement that is outputting “ potato”, how would the output change
if the blank inside of the first quote was missing?
5. What is the effect of System.out.println(“”);
18
Make it Right!
The correct output for this program is:
1 potato
2 potato
3 potato
4
5 potato
6 potato
7 potato
more
You’re out!
We’d like it to not print out “potato” if potatoNumber is 4. Let’s add an if
statement to make the output correct. To do that, make your code look like this:
int potatoNumber;
potatoNumber = 1;
while (potatoNumber < 8)
{
System.out.print(potatoNumber);
if (potatoNumber != 4)
{
System.out.print(" potato");
}
System.out.println("");
potatoNumber = potatoNumber + 1;
}
System.out.println("More");
System.out.println("You're out!");
Watching Conditionals
Run your code and step over your code to watch how it skips parts of the code. In
Java, if statements allow the program to change its behavior depending on the
information in the system.
Watch your program run until you can answer these questions:
1. How do you know which code will be executed if the conditional is true?
2. What is the purpose of the indentation there?
Note: when you use curly brackets in Eclipse you may see that the ending bracket is
automatically included. It is wise to check for proper pairing of brackets making
sure that only the statements you want executed are within the pair.
Also, if you change the program and you attempt to run the code again, Eclipse may
warn you that your class contains obsolete methods. This means that you have left
the debugger stopped at a breakpoint. Click on “Terminate” to clean that up.
19
Vocabulary
Assembler
Assembly Language
Assignment Statement
Breakpoint
Comments
Compiler
Conditional Statement
Debugger
High Level Language
Integrated Development
Environment
Interpreted
Java Byte Code
Java Virtual Machine
Keyword
Loops
Machine Language
Memory Diagram
Object Code
Output
Reserved Word
Source Code
Syntax
Target Machine
Variable
While Loop
Lab Questions
1. Explain what this code does:
int i;
2. What does a while loop do?
3. How did you change the code so that the “potato” didn’t get output with 4? Are there
other ways you could have modified the code to get the same result?
4. Explain what this code accomplishes:
x = x + 1;
5. Draw a memory diagram showing how this program changes the contents of the variables
over time.
6. In the code given in the lab, which brackets may be removed between the while statement
and the final print statement without changing the behavior of the program?
Chapter Questions
1.
2.
3.
4.
5.
6.
7.
8.
What is the difference between a compiler and an assembler?
What is the difference between an interpreted and a compiled language?
How does the Java Virtual Machine change the way object code is executed?
What is the purpose of a breakpoint? How do you think it might be useful when you start
writing code?
How do you set a breakpoint?
What is the difference between “Run As . . .” and “Debug As . . .”?
What is the debugger and how do you use it to watch what your code is doing?
Draw the memory diagram for each of the following and specify what the value of the
variable x will be at the completion:
a. int z;
z = 30000;
int x;
x = 32;
if (z<30000)
x = 0;
20
b. int y
int x
while
{
x
}
= 1;
= 2;
(y<3)
= x + x;
c. int x;
x = 0;
int i;
i = 0;
while (i<5)
{
i = i+1;
x = x+i;
}
9. What is the output of the following code:
a. int i;
i = 0;
while (i<6)
{
System.out.print(i+ “ ”);
i=i+1;
}
10. For each of the following, draw the memory diagram and describe the output.
a. int x
int y;
x = 54;
y = x + 4;
System.out.print( “y is “ + y);
x = 15;
System.out.print( “y is “ + y);
b. int x;
int y;
x = 3;
y = 2;
while (x < 6)
{
x = x + 1;
y = y + 6;
}
System.out.println( x + “ “ + y);
21
Lab 2 – Reading Arrays
Generating Random Numbers
Often, we want to generate a random number in a particular range. For example, if we were
building a game that included dice, we might want to simulate the rolling of a single die. We
could do that by generating a random integer from 1 to 6.
Java only gives us one way to generate random numbers; we can call Math.random() which
will return a randomly generated real number (a double) whose value is between 0 and 1. That
value could be zero, but it cannot be one. In other words, it generates a real number that has a
zero to the left of the decimal point. While that may initially seem quite limited, we can actually
use it to generate random numbers in any range.
In our dice rolling example, we want to generate a random integer from 1 to 6. We can do that
using the sequence of steps in this diagram:
Math.random() gives us a number between 0 and 1 (not quite getting to 1). Think about
what happens when we multiply that number by six. The values near zero stay near zero, zero
stays zero, but the numbers close to one will result in a value close to six. By multiplying by six,
we have essentially increased the range of our results. Now, our random number will be between
zero and six (not quite getting to six).
Then, if we add one to our result, that will shift the range of possible results one position to the
right on our number line. Now the range is from one to almost seven. Notice that we could get
the value 1, but we won’t get the value seven.
Finally, we need our result to be an integer, so we need to convert our double to an int. We can
do this by putting(int)in front of our expression. This is called typecasting the value to an int.
We’ll study this technique more in a later lab.
22
Here’s the expression for simulating our dice roll:
int roll = (int)(Math.random() * 6) + 1);
In general, these are the steps to generate a random number between two values (max and min):
1. Call Math.random()
2. Multiply that result by (max – min + 1) to make the spread of values the correct size
3. Add min to shift the possible values into the correct range
So, in general, our expression to generate a random integer between max and min is:
(int) Math.random() * (max – min + 1) + min
Notice that, in this case, we wanted each number to be equally likely to be selected. In other
words, we wanted the random number to be uniformly distributed across the values. The original
random number that we got by calling Math.random() was uniformly distributed between 0 and 1,
so our resulting values from our expression will also be equally likely. If you have a situation
where you do not want the values to be distributed uniformly, your task will be much harder!
Lab Background
In this lab, we are going to experiment with the generation of random numbers to
test how uniformly distributed they really are. In order to do that, we are going to
generate thousands of random numbers and count how many times each value is
generated. If the random number generator is distributed uniformly, then every
value should be generated about the same number of times.
Arrays
Up to now, we have used variables that hold exactly one piece of information. For example,
when we declare a variable with this statement:
double x;
we are telling the compiler that the variable x will hold one real number.
Sometimes, however, we’d like something more flexible. For example, say you want to hold the
ages of four people. We could declare four integer variables:
int age1;
int age2;
int age3;
int age4;
age1 = 32;
age2 = 35;
age3 = 15;
age4 = 52;
Given that set up, if we wanted to compute the average age, we could do this: (Note: We need to
define a place to store the result as a double, since dividing integers may gives us a decimal
remainder.)
double averageAge;
averageAge = (age1 + age2 + age3 + age4)/4;
However, this strategy has a number of weaknesses. Imagine that we want to average the ages of
100 people. How much code would that require just to define the variables? Also, each time we
23
add another age, we would have to change the formula for averageAge. If we were working with
the ages of 100 people, that formula would become very long.
Clearly, storing each age in a separate variable doesn’t handle either of these situations very well.
Instead, we use a different type of variable called an array. An array can hold a number of
values as long as all of those values are of the same type. If we store our four ages in an array, it
would look like this:
The four ages are stored in the four positions of the array and the positions are numbered from 0
to 3. The offset associated with a position is an index into the array associated with that position.
For example, the position holding the 15 has an index of 2. (Note: Computer scientists count
from 0, so the third person’s age is stores in index 2.) [Think of this as a book about ages, with
four pages each holding one age and the four pages are numbered starting at zero.]
Here is the code that builds that array:
int[] ages;
ages = new int[4];
ages[0] = 32;
ages[1] = 35;
ages[2] = 15;
ages[3] = 52;
The first line of the code declares ages as an array (that’s what the square brackets mean) that
holds integers. At this point, the compiler doesn’t know how much memory to allocate because
we haven’t told it how many ints will be stored in the array. So, the memory diagram would look
like this:
The variable ages is declared so that it will be able to hold the pointer to the array, but, for now, it
doesn’t point to anything (that’s what “null” means). [Think of this as telling a librarian that you
plan to write a book about ages, and they should reserve a reference card for your book.]
The second line is an assignment statement. (Remember, the equal sign means “is assigned the
value.”) The right hand side of the equals sign is a new statement. We use it to tell the
computer to allocate some memory. In this case, it is creating the array to hold four integers.
When the new statement completes, it returns a reference to the memory that was allocated and
the assignment statement stores that reference into the ages variable. [Think of this as placing
your book with 4 blank pages on the shelves, and now the librarian can fill in the reference card
to direct people to where your book is located. You first have to find the book before you can read
or edit one of its pages. Similarly, you must address your array before you can read or write to
one of its indexed memory locations.]
At this point, the memory diagram would look like this:
24
Once the array has been allocated, each position in the array behaves like a variable and we
access it using the index associated with that position. For example, ages[2] is the third position
in the array. (Remember that we always number the positions starting with zero.) The last four
lines of the code above put values into each of the positions in the array. We now have the final
memory diagram:
Storing things in arrays gives us the ability to use loops to “walk through” the array and do
something to each value in the array. For example, after a year passes, we would like to update
everyone’s age. The following loop adds one to each position of the ages array: (Note: We are
using another type of comment here. When you type two slashes, the compiler will ignore
everything in the rest of that line. We will explain the code in more detail below.)
int i=0;
while (i < ages.length)
{
ages[i] = ages[i] + 1;
i = i + 1;
}
// set i to starting value
// increase the ith age by 1
// increment i
When a variable is declared as an array, we can use <variable name>.length to find the
number of positions in the array. In this loop, i will start with the value 0 and each time through
the loop, it will get larger by one (by executing the statement i = i + 1). The loop will stop
when i has the value 4 (the length of the array). At each pass through the loop, ages[i] will be the
value in the position associated with index i and it’s value will be increased by 1.
For Loops
If you look at the while loop in the previous section, there are really three statements that affect
how the loop works. The first statement sets up a variable to start the loop. The second statement
is the condition within the while statement itself that tells the loop to proceed or be skipped
over. The final statement is the incrementing of i that updates the condition for the next
evaluation of the loop.
For situations where you can identify those three pieces of loop control logic, a simpler syntax
can be used that will result in the same behavior as demonstrated by the while loop:
for (int i=0; i < ages.length; i=i+1)
{
ages[i] = ages[i] + 1;
}
This is called a for loop. It puts all of the loop control logic in one line of code. There is a
direct translation between these two ways of specifying a loop. The while loop structure
25
<init>
while (<continue condition>)
{
<stuff we want to do>
<prepare for next pass>
)
translates to this for loop structure
for (<init>; <continue condition>; <prepare for next pass>)
{
<stuff we want to do>
}
While any while loop can be translated into a for loop, we generally prefer for loops in
situations where we are counting at each pass through the loop and while loops are preferred for
more general situations.
When we have an array, “walking through the array” requires a variable that counts through the
indices of the array, so there is a standard for loop structure for this situation:
for (int i=0; i< array.length; i++)
{
<do stuff>
}
It is common to use the shorter i++ which is equivalent to i=i+1. The effect of both of those
statements is that the value stored in the variable gets bigger by one. This is called incrementing
the variable.
Setting Things Up To Play With Arrays
In Eclipse, create a new Java project (File -> New -> Project). Select your new
project in the Package Explorer on the left side of the screen. Create a new class
by clicking the "new" button () near the top left of the screen. Name the class
"RandomNumberChecker" and click "Finish".
Eclipse will start you out with this code (your code may contain more comments):
public class RandomNumberChecker
{
}
But you need a lot more code than that! Make your class look like this (the code is
longer than one page):
26
/**
* An experiment in how uniformly distributed are the results our
* random number generation algorithm.
*
* @author Merlin
*/
public class RandomNumberChecker
{
static final int MAX_VALUE = 6;
static final int MIN_VALUE = 1;
static final int NUMBER_OF_VALUES =
(MAX_VALUE - MIN_VALUE + 1);
static final int NUMBER_OF_TRIALS = 1000;
/**
* @param args
*/
public static void main(String[] args)
{
int[] count;
count = new int[NUMBER_OF_VALUES];
for (int i=0;i<NUMBER_OF_TRIALS;i++)
{
int position;
int random;
double randomReal;
randomReal = Math.random() * NUMBER_OF_VALUES
+ MIN_VALUE;
random = (int)(randomReal);
position = random - MIN_VALUE;
count[position] = count[position]+1;
}
System.out.println("Counts");
for (int i=0;i<count.length;i++)
{
System.out.println((i + MIN_VALUE) + ": " +
count[i]);
}
int expectedCount;
expectedCount = NUMBER_OF_TRIALS/NUMBER_OF_VALUES;
System.out.println("The expected count is "+expectedCount);
System.out.println("Distance From Average");
for (int i=0;i<count.length;i++)
{
int distanceFromExpected;
distanceFromExpected = count[i] –
NUMBER_OF_TRIALS/NUMBER_OF_VALUES;
System.out.println((i + MIN_VALUE) + ": " +
Math.abs(distanceFromExpected));
}
}
}
27
As in the previous lab, if you make any errors in this, Eclipse will underline them
with red squiggles (like spelling errors in Word). Remember that the computer is
truly stupid; capitals and all punctuation must be exactly as they are shown here.
You can add or remove white space except that you cannot eliminate all of the
space between words or add spaces within words.
What Does It Do?
To start, let's just run it. Right click on the name of the class in the package
explorer and pick Run As -> Java Application. It should run and produce this output
in the console window at the bottom of the screen: (Remember that we are
generating numbers randomly, so the counts you see won’t exactly match these.
However, they should be close.)
Counts
1: 161
2: 153
3: 168
4: 169
5: 166
6: 183
The expected count is 166
Distance From Average
1: 5
2: -13
3: 2
4: 3
5: 0
6: 17
Don't go on until you see similar output.
Constants
Variables typically hold values that vary as the program runs (hence the name variable).
However, sometimes we want to use a value that never changes. For example, in the example of
computing averages of ages, let’s say we need to add one year to everyone’s age. We could just
write the code making it aware of the fact that there are exactly four things in the array like this:
for (int i = 0; i< 4; i++)
ages[i] = ages[i] + 1;
When we use a value the way we used that 4, we call it hard-coding the value into the code. The
problem with hard-coding is that, if we want to change the value later, we may need to change it
in many places and problems can result if we don’t find them all. As a better alternative, a
constant is a variable whose value cannot be changed (yes, that sounds contradictory!). We
declare a constant in the same way that we declare a variable, but with the modifier “final”
which means the value cannot be change. For this reason, we must include the value in the
declaration. For example, we could declare the constant for our array of ages like this:
28
final int NUMBER_OF_AGES = 4;
Notice that we capitalize all characters of a constant. With that declaration, we can use our
constant throughout our code:
ages = new int[NUMBER_OF_AGES];
for (int i = 0; i< NUMBER_OF_AGES; i++)
// do something to each position of the array of ages
In this way, if we later decide that we need to change the number of ages we are working with,
we can just change the value in the declaration of the constant. In addition, by marking the
variable as “final” (making it a constant), we ensure that no one will accidentally change its value
when the system is running.
How Does Our Program Do That?
As a first step in looking at this program, look at the declarations of constants.
Four constants define the range of random numbers we are going to generate and
the number of random numbers we’re going to generate. In particular, we are going
to generate the integers from 1 to 6 inclusive. This would be equivalent to
simulating the roll of a die. For now, just ignore the modifier “static”; we’ll explain
that in a later lab. It’s also important to pay special attention to this declaration:
static final int NUMBER_OF_VALUES =(MAX_VALUE - MIN_VALUE + 1);
Notice that this constant is being assigned a value that results from a computation.
Since NUMBER_OF_VALUES is a constant, we can only do this if the computation
depends only on other constants (and not other variables). Furthermore, the
constants used in the computation must already be defined.
Now, let's use the debugger to watch how this RandomNumberChecker program
really works. Put a breakpoint at the first line of the main method by double
clicking on the blue vertical bar to the right of that line like you did in the previous
lab.
Since we have just run our program, we can run it in the debugger by clicking on the
debug button (
) in the toolbar near the top of the screen. When you click that,
it will probably ask if you want to switch into the debug perspective - say ok!
The debugger has stopped the execution of the program. The editor will put the
cursor on the line that will be executed next. Click the “step over” button just
once. You have executed "count = new int[NUMBER_OF_VALUES];" Look at variable
count in the variable pane:
29
The "int[6]" means that count is an array that holds six integers, and the triangle
on the left can be used to expand it to show all of those values. Click on the triangle
and you should see this (you may have to scroll to see all of the values):
Inside the first for loop, you see three variable declarations and these statements:
randomReal = Math.random() * NUMBER_OF_VALUES + MIN_VALUE;
random = (int)(randomReal);
position = random - MIN_VALUE;
count[position] = count[position]+1;
The first line generates a real random number between 0 and NUMBER_OF_VALUES
(it could be zero, but it can’t quite be NUMBER_OF_VALUES) and then adds
MIN_VALUE to shift the values to the right range. The second line truncates that
result to convert the real number to an integer. Together, those statements match
the explanation of how to generate random numbers at the beginning of this lab and
you should be able to explain exactly how it works.
The third line calculates a position in the array. To understand what this is doing,
we need to look carefully at how the code is using the count array. Remember that
the array has indices 0 to 5, but we are generating numbers from 1 to 6. In order
for the count of each possible value to have a position in our array, the code will
subtract 1 (the minimum value we are generating) from the number generated to
compute the index associated with that number. In other words, we’ll store the
count of occurrences of 1 at index 0, the count of occurrences of 2 at index 1, the
count of occurrences of 3 at index 2, and so on.
So, each time we go through that loop, we will generate one random number and
count its occurrence.
30
Use step over to watch how that loop is working. Each time you execute the loop
you should see variables changing in the variable pane. When you reach the first
line inside of the loop, the value for i (the loop count) will be displayed. When the
second line is executed, the position value is displayed in the variables tab. As you
return to the for loop statement, the count array has been incremented by 1 at
the position (index) you saw before. (Note that the line your debugger has stopped
on doesn’t happen until you step to the next statement.)
Mathematical operators
Many of the mathematical operators we use when we program are the same as those you learned
in elementary school. For example, ‘+’ and ‘-‘ mean ‘add’ and ‘subtract.’ However, not
everything is so straightforward in programming. Multiplication is denoted with the ‘*’ operator
because keyboards don’t have a “dot” operator and just putting the variables next to each other
would make it difficult for the compiler to translate the code, and we have a number of operators
that aren’t common in mathematics. Here is a table of the common mathematical operators in
Java:
Operator
Number of
Operands
Example
Expression
Meaning
+
2
3+4
Addition
-
2
3–4
Subtraction
*
2
3*4
Multiplication
/
2
3/4
Division
++
1
i++
Increment (add one)
--
1
i--
Decrement (subtract one)
%
2
4%3
Modulus (remainder after
division)
-
1
-3
Negation
+
1
+3
Positive
We can use these operators to build complex mathematical calculations. In addition, we can use
parentheses to group sub-expressions just like we do in algebra. We’ll learn more about building
expressions in a later lab.
In algebra, remember that multiplication and division operations are completed before addition
and subtraction. For example, 3+2*4 has the value 11. The same is true in Java. Every computer
language contains precedence rules that specify the order in which operations will be computed.
31
Here are those rules for Java:
Precedence
Group
Operators
Highest
Subexpression
()
Second Highest
Unary Operators
Unary (one operand) + and -
Third Highest
Multiplicative Operators
*, /, %
Lowest
Additive Operators
+, -
The Output
All that’s left to explore in this code is the two loops that produce the output.
They are both our standard “walk though the array” type of loop. Watch them
execute and justify why the System.out.println statements result in the output
you see. Hint: Math.abs(x) evaluates to the absolute value of x, Math.abs(-3)
would evaluate to 3 and Math.abs(3) would also evaluate to 3.
More Trials?
One of the benefits of using constants instead of hard-coding values is that it is
easy to change their values and see how that affects the results. Play with the
constant NUMBER_OF_TRIALS to change the number of trials. If you make it small,
do the results support the idea that the random number generator is equally likely
to generate each numbers? As the number of trials grows, you will probably
become more confident in your conclusions about the random number generator.
Where Else Can We Use This?
We use random numbers in lots of computer applications. In particular, games use
them so that the behavior of the game isn’t the same each time we run it. For
example, in a game, when we hit an enemy with a weapon, it doesn’t always do the
same amount of damage. Suppose we had a weapon that could cause between 20 and
50 points of damage. Let’s see if Java will generate those values uniformly.
We have constants that define the range of values we want to generate:
MIN_VALUE and MAX_VALUE. Modify those values to be 20 and 50 and run the
program. You should see that it generates that range of values uniformly.
At this point, the constant NUMBER_OF_VALUES has become important. You
should be able to explain why the calculation of NUMBER_OF_VALUES includes
“+1”.
32
What if the Range Goes Negative?
In the game Final Fantasy 10, if you use black magic on one of the “undead,” it has
the potential to heal them instead of hurting them. That would mean that the
amount of damage may be negative. Suppose we had a magic spell which, when used
on the “undead” caused a range of damage from -5 to 5. Let’s see if our code works
for that range. Change the constants to match that range and run the program.
You should see output that looks like this:
Counts
-5: 0
-4: 914
-3: 955
-2: 884
-1: 934
0: 1876
1: 873
2: 907
3: 894
4: 891
5: 872
The expected count is 909
Distance From Average
-5: -909
-4: 5
-3: 46
-2: -25
-1: 25
0: 967
1: -36
2: -2
3: -15
4: -18
5: -37
That doesn’t look like the values are being evenly distributed. We never see the
smallest value and we see about twice as many zeros as we should. Something must
be very wrong! Let’s use the debugger to see what is happening.
There are two possibilities for the source of this problem: the way we convert
random numbers to the positions in the count array and they way we generate the
random numbers has a problem when one of the endpoints is negative.
First, let’s think about how the counts for these numbers will be stored in the count
array. We are generating the numbers from -5 to 5. That means that
NUMBER_OF_VALUES is 11 and our array will have 11 positions. The position of
each number is found by:
position = random - MIN_VALUE;
In this experiment, MIN_VALUE is -5, so the position will be the random number
plus five. This means that count will look like this:
33
That seems to work and doesn’t explain why we are getting twice as many zeros as
we should be getting.
Since we’ve explained away our first concern, let’s investigate our second concern:
that the bug is in the generation of random numbers. First, let’s think about how it
ought to work. Look back at the description at the beginning of this lab and draw
the diagram for how it should work when the min and max values are -5 and 5.
Now let’s use the debugger to see if it is working the way we expect it to. Put a
breakpoint on the line that calculates position so that we can look at the values for
randomReal and random. Run the program in the debugger. When it stops, look at
those values and compare them to the diagram you drew in the last paragraph. Do
they map correctly?
One run of the program showed these values:
That means that, in the last step of generating the number, -0.7 is being converted
to 0. In the diagram, when we typecast to int, it always mapped the real number to
the next lower integer (because the typecast is just throwing away the decimal
part of the number). However, when that real number is negative, dropping the
decimal part of the number gives us the int greater than our number! In other
words, since the typecasting is just dropping the decimal part, all of the numbers
from -0.9999999 to 0.9999999 will become zero. That is why we have twice as
many occurrences of zero than anything else.
In order to fix this problem, we are going to have to change the typecast to
something that will always give us the integer below our real value. The “floor”
function is exactly that:
Math.floor(x)
will evaluate to the largest integer that is less than or equal to x. Here are some
examples to demonstrate:
34
Math.floor(1.5) is 1
Math.floor(1) is 1
Math.floor(-1.5) is -2
Math.floor(-1) is -1
For some (unexplained) reason, Math.floor() returns a double (even though it will
have zeroes in the decimal portion), so we still have to typecast the result to an
int. Change your code to look like this:
random = (int) Math.floor(randomReal);
Stop the code that is running in the debugger by clicking the red square. Then run
your program without the debugger to verify that the values are now uniformly
generated.
It seems we have fixed a problem, but we should be careful to make sure we haven’t
broken anything. Change the constants back to generate values for rolling a
standard die and verify that the output is still correct.
Conclusion
The underlying motivation for this lab was to practice reading code that contained
arrays. However, as a side effect, we’ve done an interesting experiment on random
number generation in Java. When you look at the results, do you think each of the
values are roughly equally likely? In other words, is the random number generation
uniform across the values?
Vocabulary
array
increment
precedence
constant
index
typecasting
hard-coding
new statement
Lab Questions
1. Explain how we used the count array. How did it relate to the random numbers that were
generated?
2. What did this code do?
for (int i=0;i<count.length;i++)
{
int distanceFromExpected;
distanceFromExpected = count[i] –
NUMBER_OF_TRIALS/NUMBER_OF_VALUES;
System.out.println((i + MIN_VALUE) + ": " +
distanceFromExpected);
}
3. What did this code do?
35
int random;
random = (int)(Math.random() * NUMBER_OF_VALUES +
MIN_VALUE);
4. What other values of the constants did you play with? Explain how changing each of the
constants affected the behavior of the program.
Content Questions
1. Draw the memory diagram for each of the following pieces of code:
a. int x;
x = 3;
int y;
y = x;
int z;
z= = y+x;
b. int x;
x = 3+4*5; // specify the exact value of x
c. int[] x;
x = new int[6];
x[2] = 3;
2. What are the two steps of creating an array?
36
Lab 3 – Printing Graphs and Methods
Methods
As our programs grow larger, they can be easier to understand if we divide them into smaller
pieces. We can name sections of code and, then use them other places. We call these named
sections of code methods. (Soon, we will see many other ways methods help us architect quality
systems).
For example, here is a method that outputs the message “Foo.”1
void foo()
{
System.out.print(“F”);
System.out.println(“oo”);
}
A method can contain any number of statements and those statements get executed when another
piece of the code refers to the name of the method. For example, this code:
int y;
y = 3;
foo();
System.out.println(“Finished”);
declares a variable y, gives it a value of 3, and then invokes the method named foo. Notice the
parentheses after the name foo; they tell the compiler that foo is a method (as opposed to a
variable). When we use the name of the method in our code, we say we are calling the method.
When the JVM executes the line that contains the methods name, it jumps to that method,
executes the code within that method and then jumps back to the line that called the method. In
this case, the execution looks like this:
int y;
y = 3;
foo();
System.out.print(“F”);
System.out.println(“oo”);
System.out.println(“Finished”);
When the JVM sees the call on foo, it jumps to that method and the two statements in that method
are executed. Then it jumps back to the line that called it (which is now completed) and then
continues executing from that point.
Our First Methods
In this lab, we are going to build a program that “draws” a bar graph to the screen.
When we are finished, it will draw this graph of the function sin(x) from -π to π.
1
To use this code in the types of classes we’ve been looking at, the declarations of the methods in this
section would need to be preceded by the word “static.” As we learn more, that will no longer be necessary.
We only need to use that when the methods are being called from our main() method.
37
Simple Bar Graphs Without Graphics
-1
0
1
------------------------------------------------------------------*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
To start, create a project and in that project create a class named GraphPrinter.
Make that class look like this:
38
/**
* Print out a graph using characters from the keyboard
* @author Merlin
*
*/
public class GraphPrinter
{
private static final int MAX_SCALE = 65;
/**
* Print the heading and scale of our graph
*/
public static void printHeading()
{
System.out.println(" Simple Bar Graphs Without Graphics");
System.out.print("-1");
for (int i=0;i<MAX_SCALE/2;i++)
{
System.out.print(" ");
}
System.out.print("0");
for (int i=0;i<MAX_SCALE/2;i++)
{
System.out.print(" ");
}
System.out.print("1");
System.out.println();
for (int i=0;i<MAX_SCALE+2;i++)
{
System.out.print("-");
}
System.out.println();
}
/**
* Build the graph in which we are interested
*/
public static void main(String[] args)
{
printHeading();
}
}
When we run that program, it will print out the heading for our graph. Put a
breakpoint on the only line in the main() method. That line is calling our method
named printHeading(). Run the program in the debugger and when it stops there,
click the “Step Into” button that looks like this:
. That will make the
debugger stop at the first line inside that method so that we can watch it run. You
should see that the JVM is now executing the statements inside the printHeading()
method.
Notice the distinction between “Step Into” and “Step Over.” “Step Into” will stop
inside the method to let us see what it is doing. “Step Over” will cause the machine
39
to execute the whole method and stop on the line after the method call. In
previous labs, we have always used “Step Over” because, without us being aware of
it, we’ve been calling methods all along; every time we called System.out.println(),
we were calling a method provided by Java.
Use “Step Over” to watch printHeading() print out the heading of our graph until
you are back in the main() method.
Passing Information Into Methods
The foo() method is very simple, but methods can be much more useful than that. For example,
look at this method:
void foofoo(int x)
{
System.out.print(“Foo ”);
System.out.println(x);
}
The “int x” in the declaration of the method foofoo is a special kind of variable declaration. It
declares a variable named x whose type is int, but x is special because its value is set when the
foofoo method gets called. The variables that are declared as part of the method declaration are
called the parameters of the method.
When a method with parameters is called, the values that we want those parameters to have are
put into the parentheses in the calling statement. For example,
foofoo(3);
calls the foofoo method and, as that call is executed, the 3 in the calling statement is the value
given to the x in the declaration of the method. In some ways, you can think of passing
parameters like assignment statements. In this case, the compiler essentially executes x=3 at the
beginning of the foofoo method and the output will be “Foo 3”.
We can pass any expression whose value matches the type of the parameter in the call to the
method. All of these are valid calls on foofoo:
int p;
p = 3;
foofoo(p); // will pass a 3 into x
foofoo(p+4); // will pass a 7 into x
However,
double z;
z = 3.2;
foofoo(z);
will result in a syntax error because z’s type is not int and passing a double into an int would
cause the loss of all of the fractional part of z’s value. This example demonstrates that Java is a
strongly typed language. This means that the compiler checks to make sure that the types of
variables in assignment statements and parameter lists match the types of values being given to
them. This is a safety feature because it ensures that you do not accidentally lose information.
40
First a Simple Graph
Printing each row of our graph requires a similar strategy: print blanks to move to
the right in the row and then print an asterisk. The only difference in each row is
how many blanks we print before the asterisk. Since this is something we want to
do repeatedly and it isn’t trivial, let’s make a method that prints a single row. Add
this method to your class:
/**
* Print one row of our graph
* @param height the number of blanks we should print before the
* asterisk
*/
public static void printBar(int height)
{
// print one blank to match the - sign on the -1 in
// the scale
System.out.print(" ");
for (int i=0;i<=height; i = i + 1)
{
System.out.print(" ");
}
System.out.println("*");
}
That method one integer parameter: height. That parameter specifies how far over
the asterisk should be printed. Let’s write some simple code that uses that method
so we can see how it works. Add this loop to your main() method:
for (int h = 0;h < MAX_SCALE; h = h + 5)
{
printBar(h);
}
That loop will give the variable h these values: 0, 5, 10, 15, . . ., 60. Trace it on
paper to see why it steps by 5s and why the last value it reaches is 60.
Let’s watch how the method works. Put a breakpoint at the beginning of that for
loop. Click “step into” once and you should see the variable h get the value of zero.
Click “step into” again and it will stop at the beginning of the printBar() method.
Look at the value in the parameter; the zero from h has been copied into the
parameter height. Use “step over” to watch how the method works. When it
finishes, it will jump back to the line that called the method. Use “step over” to
watch the for loop change the value of h to 5 and use “step into” to see printBar()
work again. This time, the value of h is 5, so that is the value passed into the
parameter height. You should see that value. Use “step over” to watch the method
work again. Continue to use “step into” and “step over” until you understand how the
variable height is getting its value from the method call.
When you understand, let’s get rid of the break point. In the frame at the upper
right, click on the Breakpoints tab. You can delete a single breakpoint by selecting
41
it and clicking on the single X icon. The double X icon will delete all of the
breakpoints. Now, you can click the “resume” button
which will tell the
debugger to run the program until either it hits another break point or to the end
of the program. Your output should look like this:
Simple Bar Graphs Without Graphics
-1
0
1
------------------------------------------------------------------*
*
*
*
*
*
*
*
*
*
*
*
*
Now we would like to write some more code, but to do that, we want Eclipse to be in
the “Java” perspective. You can switch back and forth between the Java and debug
perspectives with these icons at the top right of the Eclipse window (if the option
you want isn’t shown, try clicking on the double arrows):
Instead of outputting the line of asterisks we currently have, our goal was to print
out the sine function. The challenge in that is that the range of the sine function is
-1 to 1, but the number of blanks we can print before the asterisk is zero to 65.
We’re going to need to make a conversion between those ranges.
Methods That Compute Things
Up to this point, all of our methods have performed some action (output some information), but
methods can also make computations and return values.
private int positive(int x)
{
if (x >0)
{
return x;
}
return -1*x;
}
In this case, the method positive() is used to compute the absolute value of the integer passed into
it. In its declaration, notice the type “int” before the name of the method. That is called the
return type of the method and tells you the type of variable that the method computes. In all of
our previous method examples, the keyword “void” has been in that place. That meant that those
methods were designed to perform an action, but not to compute a value.
42
When a method computes a value, a “return” statement makes the method stop execution and the
value given in that statement is the result of the method. For example, study this code:
int p;
p = positive(-3);
The second statement is an assignment statement. That means that the computer will evaluate the
right hand side of the equals and store the result in the variable on the left hand side of the equals.
When the computer evaluates a method call, the resulting value is the value in the return
statement executed inside the method. In this case, when positive() is called, x will be given the
value -3. Since x is less than 0, the condition (x > 0) will be false, so the body of the if statement
will be skipped and the “return -1*x” statement will be executed. Since x has a value of -3,
position(-3) will evaluate to 3 and that value will be stored into the variable p.
Computing the Horizontal Position
In order to print the sin function, we need to build a method that will take a
number from -1 to 1 and convert it to a position in one line of our output. To start,
add the definition of these constants below the definition of MAX_SCALE:
private static final double MIN_POINT = -1.0;
private static final double MAX_POINT = 1.0;
That defines the range of values of the function we are going to graph and
MAX_SCALE defines how many positions each row of output contains. We can use
those to make the necessary conversion using this method:
/**
* Compute the number of blanks that is in proportion to x
* @param x the value we want to convert
* @return the number of blanks
*/
private static int calculateHeight(double x)
{
double shifted;
double rangeSize;
double numBlanks;
shifted = x - MIN_POINT;
rangeSize = MAX_POINT - MIN_POINT;
numBlanks = shifted/rangeSize * MAX_SCALE;
return (int) numBlanks;
}
Let’s analyze the definition of that method:
private static int calculateHeight(double x)
The first two words are modifiers that the compiler needs. We’ll learn more about
them later. The next word, int, means that the method is going to calculate a
value that is an integer and return that value to the code that called the method.
The methods name is calculateHeight and it has one parameter, named x, which
holds a real number.
43
Inside the curly bracket of the method are five statements that are executed
when the method is called:
double shifted;
double rangeSize;
int numBlanks;
These statements declare three variables, shifted and rangeSize, each of which
can hold a real number and numBlanks which holds an integer.
shifted = x - MIN_POINT;
After this statement, shifted essentially shifts x so that it will be a positive
number. We have to do this, because we can’t print out a negative number of
zeroes. So, since x has a range of -1 to 1, shifted has a range of 0 to 2.
rangeSize = MAX_POINT - MIN_POINT;
The variable rangeSize is on the same scale as shifted, but holds the maximum
value shifted could have. So, shifted/rangeSize will be a fraction that is
proportional to how far shifted is into rangeSize. At this point, an example might
help. Supposed that x was -0.5. Since our range is from -1 to 1, we’d like x to be
plotted about one quarter of the way across our line of 65 spaces. The value of
shifted will be 0.5 and rangeSize will be 2. So shifted/rangeSize will be 0.25
(there’s our “one quarter”).
numBlanks = shifted/rangeSize * MAX_SCALE;
This statement multiplies MAX_SCALE (the number of blanks we can print) by
shifted/rangeSize to determine the number of blanks we should print. In our
example where x was 0.5, numBlanks will get the value 48.75.
return (int) numBlanks;
The return statement causes the method to stop running and execution returns to
the code that calls the method. In addition, the value that is specified in the
return statement is the value that the method call will be given. For example, look
at this code:
System.out.println(calculateHeight(0.5));
This statement will print out the result returned when we call calculateHeight()
passing 0.5 into the parameter x. In the method, the return statement will take
the 48.75 in numBlanks, typecast it to an integer and return 48. So,
calculateHeight(0.5) evaluates to 48 and that is the value that will be printed out.
Now that we understand how calculateHeight() works, let’s use it to output the sine
function. Change your main() method to look like this:
44
public static void main(String[] args)
{
printHeading();
for (double h = -Math.PI; h <= Math.PI; h = h + 0.15)
{
double s;
s = Math.sin(h);
printBar(calculateHeight(s));
}
}
When you run this, you should get a graph similar to the one at the beginning of this
program. Put a break point at the beginning of that method and use step into and
step over until you can answer these questions:
1. What values will h take on as the program executes?
2. What do you think is the value of Math.PI (hint: this is a constant that Java
gives us)?
3. What does Math.sin() return? Does it use degrees or radians?
4. How does the value from Math.sin() get into calculateHeight() and how does
printBar() know the right number of spaces to output?
Rounding Errors
If you look closely at your graph, it doesn’t quite match the pretty one at the
beginning of this lab. In particular, look at the first valley. Yours looks like this:
*
*
*
*
*
*
But the one at the beginning of the lab looks like this:
*
*
*
*
*
*
This is happening because we are getting some round-off error in calculateHeight().
Remember our example where the return statement was trying to return the 48.75
in numBlanks and, because it was forced to return an int, it returned 48? It really
would have been better if it had returned 49. Change your return statement to
look like this:
45
return (int)Math.round(numBlanks);
Math.round() is a method provided by Java that rounds a real value to the nearest
integer. So, when numBlanks is 48.75, Math.round(numBlanks) will be 49.00 (for
some strange reason, Math.round() returns a double even though there will be
nothing after the decimal point). When we typecast that to an integer, the method
will return 49.
Re-run your program to make sure the peaks and valleys have nice, gentle curves
like the picture at the beginning of the lab.
It Works for Other Functions, too
Our graph is pretty, but there’s an important subtlety to the way we have built this
code: it will graph any function whose range is -1 to 1. Let’s make it graph cosine
just to check. Change the call an printBar() in your main() method to call Math.cos():
printBar(calculateHeight(Math.cos(h)));
Run your program and the graph should now be of cosine(x) instead of sine(x).
Notice that we didn’t have to change how we calculated anything except the
function we wanted to graph.
It’s a little hard to see that the graph has changed – let’s make it graph BOTH of
the functions!
Methods Can Have Multiple Parameters
Methods are not limited to single parameters and, when they have multiple parameters, the order
of their declaration defines which value is given to each parameter. In this example,
void foofoofoo(int x, int y)
{
System.out.println(“x: “ + x);
System.out.println(“y: “ + y);
}
the method has two parameters: x and y. When we call the method, we must pass values into
each of them:
foofoofoo(3,4);
and the values will be given in order. So, x will be assigned the value 3 and y will be assigned
the value 4 and the output will be:
x: 3
y: 4
46
Graphing the Area Between to Functions
Now we want to change our program so that it “shades” the graph between two
functions. For example, if the functions are sine(x) and cosine(x), the graph should
look like this:
Simple Bar Graphs Without Graphics
-1
0
1
------------------------------------------------------------------*********************************
*****************************
***********************
****************
*********
**
******
*************
********************
*************************
*******************************
************************************
****************************************
********************************************
**********************************************
***********************************************
**********************************************
**********************************************
********************************************
******************************************
**************************************
*********************************
****************************
**********************
****************
*********
**
*******
**************
********************
**************************
*******************************
************************************
****************************************
********************************************
*********************************************
***********************************************
***********************************************
**********************************************
********************************************
******************************************
**************************************
Building that output means that we have to change the way each line is output.
That means we’ll need a different method than printBar(); let’s call it
printBetween2Bar(). It’s going to need two parameters: the distance (in blanks)
47
each of two functions is from the left edge of our graph. In other words, we want
to be able to call it like this:
printBetween2Bar(calculateHeight(sin), calculateHeight(cos));
So, its definition will look like this:
private static void printBetween2Bar(int h1, int h2)
The big challenge here is that we don’t know which one of our functions will be
smaller than the other. Therefore, the general strategy for this method is:
-
figure out which parameter, h1 or h2, is smaller
-
print blanks to the smaller parameter value
-
print asterisks to the larger parameter value
Here is the definition of that method:
static void printBetween2Bar(int h1, int h2)
{
int min = Math.min(h1, h2);
int max = Math.max(h1, h2);
int i = 0;
while(i<=min)
{
System.out.print(" ");
i= i+1;
}
System.out.print("*");
i = i + 1;
while (i <= max)
{
System.out.print("*");
i = i + 1;
}
System.out.print("*");
System.out.println();
}
Add it to your program and look through it carefully to see how it implements our
strategy.
To see that method in use, change your main() method to look like this:
48
public static void main(String[] args)
{
printHeading();
for (double h = -Math.PI; h <= Math.PI; h = h + 0.15)
{
double sin;
sin = Math.sin(h);
double cos;
cos = Math.cos(h);
printBetween2Bar(calculateHeight(sin),
calculateHeight(cos));
}
}
Run your program and you should see the output we were hoping for. Run it in the
debugger and use “Step Into” and “Step Over” until you understand how our new
method works.
Formal Structure of Methods
Now that we’ve seen the definition and use of a number of methods, let’s look at the general
structure of a method. The declaration of a method always takes this form:
[modifiers ] <return type> <method name> ( <parameter list> )
{
<statements>
}
For now, we’ll ignore the modifiers (Eclipse will put in those we need and we will learn more
about them later). Here are the rest of the parts of this declaration:
<return type>: The type of variable that will be returned by the method. This is also the type to
which calls to the method will evaluate.
<method name>: This is the name of the method. The rule for naming methods is the same as
the rule for naming variables; it follows the convention that words after the first are capitalized.
<parameter list>: This is a sequence of variable definitions separated by commas. Each definition
must have a type and a variable name
<statements>: This is the code that is contained within the method.
49
Vocabulary
Math.round()
return type
strongly typed
method
parameter
void
return
Lab Questions
1.
2.
3.
4.
5.
6.
What are the for loops in printHeading() doing?
What is the difference between Step Into and Step Over?
Explain what the for loop in printBar() is doing.
What is the purpose of the return statement?
What was the source of our rounding error and how did we fix it?
Why did we still need the (int) after we added the call to Math.round() to
calculateHeight()?
7. When a method has multiple parameters, how do we decide which parameter variable gets
which value we pass into the method?
Content Questions
1. For each of the following method declarations, specify its return type, and its method name,
and list the parameters and their types.
a. private void silly(int x)
b. public int another(double x)
c. public double third(int x, double y)
d. private int fourth()
2. For the following code, fill in the missing parts of the comments and then answer these
questions:
/**
* <describe the overall purpose of this code
* @author Merlin
*/
public class Box
{
/**
* <fill in the description of this method>
* @param length <fill in the description of this parameter
*/
public static void drawFullLine(int length)
{
for (int i=0;i<length; i++)
{
System.out.print("*");
}
System.out.println();
}
/**
* <fill in the description of this method>
50
* @param length <fill in the description of this parameter
*/
public static void drawEdges(int length)
{
System.out.print("*");
for (int i=0; i< length-2; i++)
{
System.out.print(" ");
}
System.out.println("*");
}
/**
* <fill in the description of this method>
* @param size <fill in the description of this parameter
*/
public static void drawBox(int size)
{
drawFullLine(size);
for (int i=0; i< size-2; i++)
{
drawEdges(size);
}
drawFullLine(size);
}
/**
* <fill in the description of this method>
*/
public static void main(String[] args)
{
drawBox(8);
}
}
a. What shape would be drawn by the following code?
for (int i=0;i<4;i++)
{
drawFullLine(i);
}
b. Can you use this code to draw two boxes arranged like this? If so, how? If not, why
not?
****
* *
* *
****
****
* *
* *
****
c. Can you use this code to draw two boxes arranged like this? If so, how? If not, why
not?
51
********
* ** *
* ** *
********
52
Lab 4 – Classes and Objects
Introduction
When we are only developing small programs, how we structure them doesn’t really matter. We
can put all of the code in one file (just like we’ve been doing) and still find everything easily.
However, as the amount of code our system requires increases, we must be careful about how we
structure it. If we put too much code in one file, we will soon have great difficulty finding parts
of the code, understanding how the code works, and, as a result, our ability to improve the system
degrades. To address this, we divide the system into smaller, more manageable, pieces. One
aspect of the design of the system consists of how we arrange those pieces and how they interact
with each other. Building systems with strong, well-thought out designs is the key to successfully
building larger systems.
Different languages support different strategies for how to design the code. Java is an objectoriented language. This means that the code is structured around objects. When we design the
system, we think about the things the system needs to know about. For example, in this lab we
are going to build a deck of cards for a solitaire game. This system will need to know about cards
and decks of cards, so those become our objects. The King of Clubs is an object; the Two of
Diamonds is an object; and the deck itself is an object. In order to build this system, we need a
way to tell the system what “cards” and “decks” are.
Essentially, when we are programming in an object-oriented language, most of what we do is to
define our own types of objects and how they interact. Java comes with some types like integers
(ints) and real numbers (doubles and floats), and so on. In addition, Java gives us the ability to
define our own types that we call classes. Each class is a template for a type of object (which can
equivalently be called instances) of that type. The question is: what does the system need to
know for each new type. To answer that, let’s look at ints. Each variable whose type is int stores
exactly one integer value and the system knows how to execute some specific operations on ints
(add, subtract, etc.). So, when we think about designing a class, there are two things we think
about:
•
•
the things objects of that type store and
the operations object of that type can perform.
In our solitaire game, each card needs to know (store) its face value and its suit. We’ll worry
about what operations cards need to support later. Each deck needs to store a set of cards and
could support operations like “shuffle.”
So, one way to think about a class is that it is a template for objects of that type. In broad terms, a
class contains two things: instance variables which hold the information each object must store,
and methods that define the operations each object can do.
We started this discussion by talking about the need to design well-structured code. Classes help
us structure our code because each class is in a separate file and that file must be named by the
name of the class with a “.java” extension. Therefore, in our example, we will need to develop
Card.java and Deck.java. (Note: Eclipse hides the actual file names from you. When you create
the class Card, it will automatically put it in the file Card.java.)
53
In summary, we actually use the term “class” to mean three different things:
•
•
•
A type that we are declaring: like our type “Card.”
The set of objects of a particular type: the class Card can mean the set of card objects in
our game (the Ace of Hearts, King of Clubs, etc.)
The java code defining the class: the code in the Card.java file. This code declares the
type and can be thought of as a template for building objects of that type.
Structure of a Class
In Java, every (public) class is stored in a separate file that must be named with the name of that
class with a .java extension. In addition, the format of that file is specified by Java. It must
include the statement “public class <class name> { . . . }” where the definition of the variables
and methods of that class are between the curly brackets. Remember that two things define a
class: the things objects of that type store, and the operations objects of that type perform.
Instance variables specify the things the objects can store, and methods specify the operations
those objects support.
Setting up the Card Class
Create a new project in Eclipse and, within it, create a class named Card. Make it
look like this:
54
One thing probably needs to be explained more carefully here: we are using
integers to represent the face value and the suit of each card. The face value will
be an integer between 0 and 12: 0 = Ace, 1 = Two, 2 = Three, . . ., 11 = Queen, and
12 = King. You’ll see a little later why we numbered them from zero even though
that makes things look a little weird here. The suit is also represented by an
integer: 0 = Spades, 1 = Hearts, 2 = Diamonds, and 3 = Clubs.
Creating and Using Objects
Once we have created a class, we have essentially defined a new type in Java, so we can declare
variables of that type. For example, suppose we have written this Account class:
55
/**
* a VERY simple class that manages the balance of a savings
* account
* @author Merlin
*
*/
public class Account
{
private double balance;
/**
* Deposit a certain amount into the account
* @param depositAmount the amount to deposit
*/
public void deposit(double depositAmount)
{
balance = balance + depositAmount;
}
/**
* Get the current balance of the account
* @return the current balance
*/
public double getBalance()
{
return balance;
}
}
Then, we can declare a variable of this type:
Account savings;
(We do this the same way we define a int variable: int x; )
When the compiler sees this code, it knows we want to store an instance of Account in a variable
named “savings”, but it doesn’t know how much space that will require. So, at this point, the
compiler just allocates enough space to hold a reference to another memory location. This
situation is very similar to the declaration of arrays we saw in lab 2; when you declare the array,
the compiler doesn’t know how many things you’ll put into it, so it just allocated the space for the
pointer. After that declaration, the memory diagram looks like this:
The “null” means that our pointer isn’t pointing to anything yet. We tell the computer to create an
instance (object) of a class using the “new” statement (which we already used to create the space
for an array):
savings = new Account();
This creates the space that will hold the instance variables for this object and make our variable
point to it:
56
Now that the object has been created, we can call its methods using the dot operator:
savings.deposit(3200.14);
(Notice that we don’t put commas in numbers even when they are larger than 999) This will call
the method named “deposit” passing 3200.14 into its parameter. Looking back that that code, we
can see that deposit will add the amount in the parameter to the instance variable “balance”, so,
after this method call, our object will look like this:
Constructors
A constructor is a method that is used to create and initialize an object of the class type. The
declaration of a constructor looks like this:
public <class name> (<params>)
{
. . .
}
The declaration of a constructor differs from other methods in two ways: it does not have a return
type, and its name is the name of the class.
In the Account class, we did not write a constructor, because we didn’t need to do anything
special to initialize new objects (instance variables that are doubles get the default value zero
which is a reasonable initial value for our account balance). In this case, Java will give our class
a default constructor. This constructor doesn’t have any parameters and doesn’t initialize
anything, but it does allocate the memory required by the object. We invoke the default
constructor by passing no parameters to the new statement:
For our lab, creating instances of Cards requires some initialization: we have to tell the card its
face value and suit. Since we have a Card class as a template to define the type Card, we can
declare a variable to be of type Card:
Card myCard;
Since Card is not a primitive type (one of the types the compiler knows about), the compiler just
allocates enough space for a pointer:
In our Card class, we declared a constructor that takes two ints: the face value and the suit of the
card being created. Look back at the code to find the specification of the constructor. For our
card variable, myCard, we can use the contructor to assign the initial value of the card to be the
57
King of Hearts (by passing to the contructor the two values associated with cards: 12 as the face
value and 1 as the suit):
myCard = new Card(12,1);
This would result in this memory diagram:
The new statement does two things: it allocates the memory that holds the card and it executes
the constructor with two int parameters. The constructor stores the information passed into the
parameters into the instance variables of the new object. In the diagram, the square with the
rounded corners represents one instance of the class Card. The variables associated with that
instance are shown within that square. In the case of a Card, it stores its face value and its suit.
To summarize, making a variable that holds an instance requires two steps: declaring the variable
and then using the new statement to allocate the space for the instance. The new statement calls
the appropriate constructor to allocate the memory and, possibly, initialize the instance.
Variable Initializers
Up to this point, we have kept the declaration of a variable separate from the statement that gives
us a value. For example, we have used:
int x;
x = 32;
If we know the initial value at the time we are making the declaration, these two statements can
be combined:
int x = 32;
This type of statement is called an initializer and we can do this with almost any type of variable.
You can also do this with the declaration of objects. This single statement:
Account savings = new Account();
declares the variable savings to have the type Account, calls the constructor to create a new
instance and makes savings refer to that instance.
We can also do this with arrays in two different ways. First, the statement:
int[] x = new int[4];
declares x to be an integer array and allocates space so it can hold four integers. At the end of this
statement, the memory diagram for x is:
58
Second, we can use an initializer to give each position in the array a value. With this statement:
int[] y = {32, 64, -1, 103, 15};
we create the variable y as an int array that contains the five given values (the number of values in
the list determines the size of the array):
Exploring Objects
Our cards don’t do much yet, but let’s write a small program that uses them to see
how they really work. Create another class called CardRunner and make it look like
this:
/**
* Just a main method to let us play with Decks and Cards
* @author Merlin
*
*/
public class CardRunner
{
/**
* @param args
*/
public static void main(String[] args)
{
Card c1 = new Card(0,0);
System.out.println(c1);
Card c2 = new Card(12,3);
System.out.println(c2);
}
}
It’s time that we explained the “public static void main (. . .)” declaration. This
statement declares a method whose name is main. When we tell the JVM to run a
class, it looks for a method whose declaration exactly matches that of main, and
executes the code in that method. Only classes that contain a main specified with
exactly that declaration can be run by the JVM. These classes are called runnable.
59
Look at this code and make some guesses about where variables of the type Card
are declared and where cards are created. How many lines of output do you expect
this program will produce? What would you like that output to look like?
Put a breakpoint at the first line in the program that will take a breakpoint (inside
the main method). Run the program in the debugger and look at the variables tab
when it stops.
The line “Card c1 = new Card(0, 0);” declares a variable c1 whose type is Card, calls
the constructor of Card to create a new instance of the Ace of Spades (make sure
you can explain that!), and makes c1 refer to that new instance. This is an initializer
for the variable c1.
Click “Step Over” and look at the variables. We have created an instance of card
and stored it in the variable c1. If you click on the triangle next to c1, you will see
that the card has two variables, faceValue and suit, matching the instance variables
of our class.
Click “Step Over” until you get passed the line that declares and initializes c2. At
this point, if you expand c2, your variables frame should look like this:
In this frame, you can see that c1 and c2 are both of type Card and each have their
own copies of the instance variables of the Card class. In fact, at this point, those
variables hold different values.
Another way that you can see that c1 and c2 are pointing to different instances of
Card is by the id numbers they are given. (Yours may not match the 18 and 20
shown in this diagram, but the id values for c1 and c2 will be different.) The JVM
gives every object in our system a unique id number that we can use to see if two
variables are pointing at the same or different objects.
60
Watching the Constructor Work
In order to watch how our constructor works, terminate the current debugging
session. Remove the breakpoint you have and put a new breakpoint at the line that
declares c2. Start the debugger and it will stop at your breakpoint. At this point,
the variables frame should show the variable named c1, but not c2.
This time, we are going to use “Step Into” to watch the constructor work. Click
“Step Into” and the debugger should stop at the declaration of our constructor in
the Card class. Remember, you know this is a constructor because it has no return
type and its name matches the name of the class. At this point, the variables
frame should look like this:
When we are executing a method within a class, the variable this represents the
object that is performing the operation. So, in this case, this is the object that
the constructor is initializing. If you click on the triangle next to this, you will see
that it contains two variables (our instance variables faceValue and suit) and both
are currently zero. Java initializes instance variables to default values. (Integers
are set to zero.)
The variables v and s are the parameters to the constructor and contain the values
our new statement passed into them. The code that invoked the construct was:
new Card(12,3)
The values 12 and 3 are assigned to the variables in the parameter list of the
method in the order in which they are passed. Therefore, v contains 12 and s
contains three.
Click “Step Over” and you’ll be on the first line of the constructor. This is an
assignment statement. What do you think it will do? Use “Step Over” to let it
execute. You’ll see that the value of the variable v has been copied into the
variable faceValue. If you “Step Over” the next line, you see that it copies the
value of s into the instance variable suit. Now our instance is initialized, so the
constructor exits.
Now, terminate that debugging session and run the program without the debugger.
The output should look something like this (though the part after the “@” may be
different):
61
Card@c2ff5
Card@a46701
That probably isn’t what you thought the output should look like. Let’s learn about
what’s happening and how we can fix it.
The toString() Method
In Java, whenever we want to print out the value of an instance, the JVM calls the toString()
method in the instance’s class. A string is a sequence of characters (letters, punctuation,
numbers, etc.) kind of like an array of characters. In addition to the primitive types (int, double,
etc.), Java also contains a large number of pre-defined classes. One of those classes is String. It
contains what each object of type String need to store (essentially an array of characters), and the
methods that define the operations that are supported by object of type String.
The purpose of the toString() method is to create a String that describes the instance, usually for
the purpose of outputting the instance. The toString() method has the following structure:
public String toString()
{
. . .
}
Its return type is String and it takes no parameters. If we do not define a toString() method, Java
gives us a default one that returns a String that contains the name of the class followed by an @
sign and the address of the current instance (in hexadecimal). Generally, it is considered good
practice to define a toString() method that reports the values in the most important instance
variables.
Fixing Our Output
Now we know that the strange output we were seeing is a result of the fact that we
haven’t declared a toString() method in our class. The line
System.out.println(c2);
will try to convert c2 to a String (since that’s what println knows how to print out)
and will use toString() to make that conversion. Since we didn’t declare a toString()
method, Java gave us the default one. That doesn’t make for very pleasing output,
so let’s build a toString() to make it better.
To start, let’s just print out the instance variables by adding this declaration of
toString() to our Card class:
/**
* @see java.lang.Object#toString()
*/
public String toString()
{
return "faceValue = " + faceValue + " and suit = " +
suit;
}
That just looks confusing! Can we be adding Strings? Of course, not. When a plus
sign has an operand of type String, the operation represented by the plus sign is
62
not addition, but is concatenation. In other words, the plus signs between strings
means “make a new string that has these two strings right next to each other.”
Also, the parts of the statement that are inside of quotes are strings, but the
parts that aren’t in quotes are expressions. Expressions will be evaluated (in this
case, they will just get the value of the variable) and a string representing that
value will be the operand of the concatenation. (So the number 12 becomes the
character string “12” suitable for printing.)
To put it another way, this is the sequence of things the computer does to execute
that return statement:
•
•
•
•
start with the String “FaceValue = “
evaluate (get the value of) the variable faceValue and concatenate that onto
our String
concatenate “ and suit = “ onto the end of our String
evaluate suit and concatenate the result onto the end of our String.
Run the program to see what the output has become. You should see this:
faceValue = 0 and suit = 0
faceValue = 12 and suit = 3
Remember, each time we enter toString(), it will look at the instance variables of
the instance that is executing the operation. That is how it knows which values to
put into the String.
Naming Conventions
We have been looking at a lot of code and you may have noticed that we are consistent in the
ways that we name things. These are not requirements, but Java developers generally use some
standard conventions for the names of the things in our code:
Rule
Example
Classes
Every word capitalized with
nothing between the words
MyFancyClass
Variables
Camel capitalization with
nothing between the words
myFancyVariable
Methods
Camel capitalization with
nothing between the words
myFancyMethod
Constants
Fully capitalized with
underscores between the
words
MY_FANCY_CONSTANT
Camel capitalization means that we capitalize all of the words except the first (the humps are in
the middle – not at the front).
63
While these are only conventions and not enforced by Java, coding standards require that we
always abide by them.
Outputting the Suits
Now that we have built the toString() method, our output is better. We can now
see what the objects are holding, but we can still do better. We stored the suits
as integers, but those values don’t mean anything to our users. Who wants to have
to remember that 0 means Spades? Let’s change our toString() method to make it
output the name of the suit instead of the number we’ve stored. To do this, we’re
going to build an array of strings that holds the names of the suits. In each
position, we will store the name of the suit associated with the index of that
position. Before we add the code, let’s look at the memory diagram for the array
we want to build:
In this diagram, SUIT_DESCRIPTION is an array that contains four strings.
Remember, String is a class that Java gives us, so the positions in the array are
only big enough to hold a reference to the object. Each String object holds a
string containing the name of the suit associated with the integer matching its
position in the array. Earlier, we said that “the suit is also represented by an
integer: 0 = Spades, 1 = Hearts, 2 = Diamonds, and 3 = Clubs.”
With this definition, think about what value this expression would have:
SUIT_DESCRIPTION[suit]
In order to evaluate that, the computer will find the value of the variable suit. Let’s
assume for a minute that suit contains a two, meaning that our card’s suit is
Diamonds. So, since suit has a value of two, that expression is equivalent to
SUIT_DESCRIPTION[2]
which will evaluate to whatever is in the position associated with index 2 (the third
position in the array since we number from zero). That will be the string
“Diamonds” and that is exactly what we are hoping for! We could use that in our
toString() method to make the output make much more sense.
64
Remember earlier when we said we’d explain why we made the values for suit (and
faceValue) start at zero? Well, now you know. They start at zero because that is
where the indexes to arrays start. Setting them up that way lets us use the stored
values as indexes when we are converting from int to String.
In order to build this array, add this code right after the initial curly brackets in
your Card class:
final String[] SUIT_DESCRIPTION =
{ "Spades", "Hearts", "Diamonds", "Clubs" };
This uses an initializer to create our array and makes it a constant (notice the
“final” modifier) because we don’t want the values to change. You might have
guessed that it was going to be a constant by the name we chose to give it
(remember those naming conventions).
Now, change your toString() method to look like this:
/**
* @see java.lang.Object#toString()
*/
public String toString()
{
return "faceValue = " + faceValue + " and suit = " +
SUIT_DESCRIPTION[suit];
}
Before you run the code, make a guess about what the output will look like. Run the
code and, if the output didn’t match your guess, re-examine the code to explain why
it behaves as it does and not as you initially expected.
One More Time!
Our output is continuing to improve, but our users want something better. They do
not care at all about how we’ve stored the information. They want the output to be
something like “King of Spades” or “Two of Diamonds.”
In order to build that output, we’re going to need another array to help us translate
the value in our faceValue variable into the appropriate String in much the same
way as what we did for suits. Add this declaration immediately after the
declaration of SUIT_DESCRIPTION:
final String[] FACE_VALUE_DESCRIPTION =
{ "Ace", "2", "3", "4", "5", "6", "7", "8", "9", "10",
"Jack", "Queen", "King" };
As before, this declares a constant array of Strings in which each position contains
the string that should be output for cards with that position’s index as their
faceValue. For example, when faceValue has the value 11,
65
FACE_VALUE_DESCRIPTION[faceValue]
will evaluate to “Queen”.
Now we can make our toString() method output something that our user will
understand:
/**
* @see java.lang.Object#toString()
*/
public String toString()
{
return FACE_VALUE_DESCRIPTION[faceValue] + " of " +
SUIT_DESCRIPTION[suit];
}
Again, make a guess about what you think our output will be, and then run the
program to see if you are correct. If not, re-examine the code and explain why the
output differs from what you expected.
The Deck Class
Looking at an example that has only one class doesn’t really give you the flavor of
how classes and objects work, so let’s add another class to our system: A Deck
class. For this class, imagine the standard deck of cards (52 cards, 4 suits). That
is what our object will be. Once we build the class that describes the deck object,
we could make as many as we like. So, to build that class, we need to think about:
•
•
what does a deck need to store and
what does a deck need to do.
A new deck of cards consists of 52 cards in a particular order, so that’s what a
deck needs to store: all the cards in order. We can store that as an array of 52
cards that will remain in order. Let’s build that part of the class and then think
about what a deck needs to do.
To start, create a new class named Deck and make it look like this:
66
/**
* A standard deck of 52 cards in four suits.
* @author Merlin
*/
public class Deck
{
private static final int NUMBER_OF_SUITS = 4;
private static final int NUMBER_OF_CARDS = 52;
Card [] cards;
/**
* Create the deck by filling it with the appropriate cards
*/
public Deck()
{
cards = new Card[NUMBER_OF_CARDS];
int position = 0;
for (int suit = 0; suit < NUMBER_OF_SUITS; suit++)
{
for (int faceValue = 0;
faceValue < NUMBER_OF_CARDS/NUMBER_OF_SUITS;
faceValue++)
{
cards[position] = new Card(faceValue, suit);
position++;
}
}
}
/**
* @see java.lang.Object#toString()
*/
public String toString()
{
String s = new String();
for (int i=0; i<cards.length; i++)
{
s = s + cards[i] + "\n";
}
return s;
}
}
67
Before you read any further, go through that code a line at a time and try to figure
out what it is doing. Where do these things happen in this code:
•
•
•
•
the allocation of space for the array of cards
creation of the 52 cards the deck will hold
definition of the number of cards in the deck
construction of a string that describes the deck
In order to play with this class, we need our main method to create a deck to play
with, so add this code to the end of your main() method in the CardRunner class:
System.out.println();
Deck d = new Deck();
System.out.println(d);
All that will do is print an empty line after the output from our two cards, create a
new deck, and then print out a description of that deck.
Nested For Loops
It is possible to put one loop inside of another – these are called nested loops. As an example,
consider this code:
for (int var1=0; var1<3; var1++)
{
for (int var2=4; var2<=6; var2++)
{
System.out.println("outer " + var1 + " and inner " + var2);
}
}
When you see code that isn’t familiar to you, the best approach is to pretend you are the computer
and execute the code one line at a time. In this case, the first line sets up a for loop and
initializes var1 to have the value 0. It then compares that var1 to the value three. The condition
will evaluate to true, so it will execute the code inside the loop (with var1 having a value of
zero).
The code inside the loop is another loop. It will start by initializing var2 to the value four and,
since that is less than or equal to six, the computer will enter that loop, too. Inside that loop, it
will print out:
outer 0 and inner 4
The inner loop then goes back to the top and increments var2, so it will have a value of five.
Since that is less than or equal to six, it, again, enters the loop. The output this time will be:
outer 0 and inner 5
Again, the inner loop goes back to the top, increments var2, compares it with six and enters the
loop, resulting in this output:
outer 0 and inner 6
This time, when the inner loop goes to the top, var2 becomes 7. Since that is greater than six,
the inner loop is finished executing. That means that the outer loop has finished executing its
68
body, so it goes back to the top and increments var1 to be 1. Since that is less than three, it
enters the loop. The inner loop then starts over by giving var2 the value of 4 and entering its
body. The output will be:
outer 1 and inner 4
Essentially, for each pass through the outer loop, the inner loop runs from beginning to end. The
entire output for these loops would be:
outer
outer
outer
outer
outer
outer
outer
outer
outer
0
0
0
1
1
1
2
2
2
and
and
and
and
and
and
and
and
and
inner
inner
inner
inner
inner
inner
inner
inner
inner
4
5
6
4
5
6
4
5
6
The outer loop variable takes on the values from zero to two and, for each of those values, the
inner loop variable progresses from four to six. In general, when we nest loops, the inner loop
runs in its entirety for each pass through the outer loop.
Watching the Creation of the Deck
The code in our constructor (remember, you can find that by looking for a method
that has the same name as the class and no return type) is more complex than any
constructor we’ve seen up to this point. Let’s look at it in more detail before we
watch it run.
The first line of the constructor is:
cards = new Card[NUMBER_OF_CARDS];
That statement is allocating the space for our array of cards. The declaration of
NUMBER_OF_CARDS is near the top of the class where it is declared as a constant
(because of the “final” modifier) integer with a value of 52. Therefore, our array
will have 52 spaces. The memory diagram now looks like this:
As a reminder, pay attention to these details. First, our array starts at index zero
and has 52 elements, so the last position has an index of 51. Second, our array is
supposed to hold cards, but when the compiler build the code for this class, it didn’t
know how much space a card requires. Therefore, it has only allocated space for a
reference – we have to use the new statement to call the card constructor to
actually create the cards.
69
Let’s see how this looks in Eclipse. First, let’s clear out the old breakpoints.
Remember that you can do that in the Breakpoints tab in the debug perspective.
Now, set a new breakpoint at the beginning of the constructor in Deck. Run your
program in the debugger and click “Step Over” to get past the new statement. If
you click on the variables tag (in the debug perspective) and click on this and cards,
you should see this:
Remember, this is the object that is performing the current operation, so it refers
to the deck we are in the midst of creating. It has one instance variable, cards,
that is an array of 52 cards. At this point, all of the positions of the array are null
because we have not yet created any cards.
Now that we have space to store them, we need to create each of the cards.
Remember, when we create a card, we have to tell it its suit and face value. In our
deck, we want thirteen cards in each of four suits. That is what those two for
loops are doing for us. Before we analyze them further, let’s simplify the code
(only on paper) by figuring out the values of the constants:
•
•
NUMBER_OF_SUITS = 4
NUMBER_OF_CARDS/NUMBER_OF_SUITS = 13
So, it’s using four suits and 13 cards in each suit. The outermost for loop contains
only one statement (the entire inner for loop). Therefore, it will execute that
statement with the variable suit taking on each of the four values (0, 1, 2, and 3).
The inner for loop is going to give faceValue each of the values from 0 to 12. At
each call on the constructor of Card (using the “new” statement), there will be a
different pair of values for suit and faceValue. Write out the sequence of pairs
you think it will generate.
Use “Step Over” to watch the code execute and pay attention to how each
statement affects the values of the variables. You should be able to see cards
70
being put into the cards array and, for each, you should look at the face value and
suit it was given. Keep watching until you can answer these questions:
•
•
What is the actual sequence of pairs that are being generated? If that is
different from what you expected, explain what is working differently from
what you had predicted.
What is the purpose of the variable named position?
Now for the Output
In your main method, you put a line that would output the deck. As we’ve seen, that
means that it will call the toString() method in the Deck class. Look at the code for
that method and make a guess about what the output will look like. (hint: “\n” is
treated as a single character that makes the output go to the beginning of the next
line).
Stop the debugger and watch the program run without the debugger. Was the
output what you expected?
For thoroughness, let’s allow the toString() method work. Put a breakpoint at the
beginning of that method and remove the one it the constructor. Run the program
in the debugger and use “Step Over” to watch the method build a string that has a
description of every card in the array. Here are some points to notice:
•
the for loop is the standard “walk through the array” type of for loop you
saw in the previous array
•
at each step in the for loop, we are concatenating (remember: “+” means
concatenate when one of the operands is a string) descriptions of each card
onto the end of the String stored in s.
•
when we concatenate a card onto a string, that will call the toString()
method in Card to get the description of that card.
Just for good measure, run you program without the debugger and look at the
output. You should see a well-ordered deck.
Let’s Shuffle Things Up
All we’ve done so far is create things and print them out. To see that we can give
objects many types of methods (to let them have many kinds of operations), let’s
add a shuffle() method to our Deck class. The purpose of this method is to
randomize the order of the cards. It turns out that shuffling isn’t a trivial
problem. We are going to implement a Fisher-Yates Shuffle (which is sometimes
called a Knuth Shuffle). Go to wikipedia and search for “shuffle.” On that page,
you’ll see a section on shuffling algorithms. Read it to see where we are heading.
The details matter if you really want it to work correctly. However, don’t get
71
bogged down in them – they’ll make much more sense when you’ve had more
computer science experience.
The basic strategy of this algorithm is: for each card, pick a position between that
card’s position and the end of the deck (inclusive) and swap the card with the one in
that position. We know how to do the “for each card” part; that’s our standard
“walk through the array” type of loop. However, randomly picking a position is a new
challenge for which we’ll be using the random number generation strategies of the
last lab.
The Shuffle Code
Here is the code for implementing our shuffle() method. Add it to your Deck, class.
/**
* randomly shuffle the cards using the Fisher-Yates shuffle
*/
public void shuffle()
{
for (int i=0;i<NUMBER_OF_CARDS;i++)
{
int swapPosition =
(int)(Math.random()*(NUMBER_OF_CARDS-i) + i);
Card temp = cards[i];
cards[i] = cards[swapPosition];
cards[swapPosition] = temp;
}
}
In order to see this work, add this line to you main method right after you create
the deck and before you print it out:
d.shuffle();
Run the program and you should see that the deck has been randomized.
Look at the code for the shuffle method. Find the line that generates the random
position with which we want to swap the current card. You should see that it is
generating a random integer between i and 51. You should be able to explain how
that code matches the general form for generating random numbers given in the
previous lab.
The next three lines of code are swapping the values at indices i and swapPosition.
Swapping values between two variables is like swapping the contents of two cups.
If I have a cup of coffee and a cup of tee and I want to switch which cup holds
which liquid, I need a third cup. I pour the tea into the third cup, then pour the
coffee into the cup that used to hold the tea, and the pour the tea into the cup
that used to hold the coffee. That’s exactly what those three assignment
statements are doing. We put the value from index i into a temporary variable, put
the value from index swapPosition into position i and then put the value in the
temporary into position swapPosition.
72
Put a breakpoint at the beginning of the shuffle() method and use “Step Over” to
watch it run until you understand how it works.
Conclusion
This has been a pretty long lab that has required you to build two classes. The goal was to show
you how classes and objects work, but you’ve learned a lot more than that along the way.
Remember, we are still learning how to read code. You should understand how all of this works,
but we don’t expect that you should be able to generate code like this on your own.
In all of the labs up to this point, you have learned:
•
•
•
how to use the debugger to figure out what a piece of code is doing
how to look at code and make educated guesses about how it might behave
how to use a wide variety of Java constructs
Vocabulary
camel capitalization
Fisher-Yates Shuffle
object
class
initializer
object-oriented
constructor
instance
runnable
default constructor
instance variable
String
design
naming convention
toString()
Lab Questions
1. Draw the memory diagram for each of the following pieces of code:
a. Card c = new Card(12,3);
b. Deck d = new Deck();
2. Explain how this code works:
/**
* @see java.lang.Object#toString()
*/
public String toString()
{
return "faceValue = " + faceValue + " and suit = " +
SUIT_DESCRIPTION[suit];
}
3. What was the original order in which the Deck was created?
4. Explain the Fisher-Yates shuffle and how our code implemented it.
73
Content Questions
1. What is the output of the following code:
2.
3.
4.
5.
6.
7.
8.
9.
a. for (int i = 0; i < 3; i++)
{
for (int j = 2; j > 0; j = j - 1)
{
System.out.println(i + “, “ + j);
}
}
Fill in the blanks:
a. For a class, “what it knows” is stored in __________________.
b. For a class, “what it does” is coded in ____________________.
What are the three ways we use the word “class”?
What is the purpose of a constructor?
What are the parameters to the default constructor?
What are the naming conventions for each of the following:
a. variables
b. methods
c. classes
How do you know that a method is a constructor?
What is the format of the string generated by the default toString method?
Mark the following things in this class:
a. Circle each instance variable
b. Underline the name of the class
c. Put a box around any constructors
d. Put a heart over each getter (make sure it goes to the top and bottom of the getter)
e. Put a triangle over each setter (make sure it goes to the top and bottom of the
setter)
f. Put a star at the beginning and ending of each javadoc comment.
74
/**
* A set of shares of stock that we purchase together
* @author Merlin
*/
public class Lot
{
private double shares;
private double price;
/**
* Lots have shares and prices
* @param s
* @param p
*/
public Lot(double s, double p)
{
shares = s;
price = p;
}
/**
* Change the number of shares in this lot
* @param shares
*/
public void setShares(double s)
{
shares = s;
}
/**
* change the price we paid for this lot
* @param price
*/
public void setPrice(double p)
{
price = p;
}
/**
* Returns the price we paid for this lot
*/
public void getPrice()
{
return price;
}
/**
* @return the current value of this holding
*/
public double getValue()
{
// calculate the value of these shares
return Math.round((shares * price)*100)/100.0;
}
}
75
Lab 5 – Details About Variables
Introduction
In this lab, we are exploring some details about how variables really work in Java. However, we
need a problem around which we can build code to demonstrate these concepts, so we have to
take a short aside . . .
In this lab, we are going to play with a mathematical sequence called the Fibonacci Numbers.
The formal definition of this sequence is:
⎧
0
n=0
⎪
F(n) = ⎨
1
n =1
⎪ F(n −1) + F(n − 2) n > 1
⎩
Basically, that means that the sequence starts with 0 and 1 and then every number after that is the
sum of the two numbers that precede it. The resulting sequence is:
€
0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89,
. . .
In this lab, we will be a class that will generate that sequence one value at a time. While the
sequence is important, remember that the goal of this lab is to deepen our understanding of some
important details in how Java really works. As you work through this lab, pay attention to each
and every line of code and make sure that you don’t go on until you understand how each line
works and how they work together to get the job done.
Visibility Modifiers
We’ve talked about one motivation for designing systems using classes and objects: designing the
code so that we can continue to find various pieces of functionality. There are actually a number
of other benefits of this approach. One such benefit is that we can control which parts of the
system have access to methods and instance variables. The strategy of limiting the visibility of
our variables and methods is called information hiding and it makes our systems more robust
and easier to debug. For example, suppose we have a variable and it is incorrectly being
modified. If we have limited the portions of the system that have access to that variable, we have
limited the amount of the system in which we have to search for the bug.
In Java, we can control the portions of the system that can see a variable or a method by putting a
modifier in the declaration of the variable. For example, if we declare an instance variable with
this statement:
private int myVariable;
only the class that is declaring the variable can access (read or modify) that variable. Java
supports four types of visibility, but, at this point, only two are relevant (the other two are related
to constructs we have not yet studied):
•
public: visible to the entire system
•
private: visible only within the class making the declaration
For now, if you do not include a modifier, it is equivalent to a public declaration (when we learn
about inheritance, we will re-visit this topic to see the other possibilities).
76
In general, instance variables should be declared to be private. This way, all of the code that can
affect those variables is in one place and defects involving those variables will be limited to a
small portion of the system.
Setting up our Class
For this lab, we are going to play with classes that generate the Fibonacci sequence.
Create a new project and a class named Fibonacci within that project. Here’s the
code that we’ll start with:
/**
* A class that gives the Fibonacci sequence
*
* @author Merlin
*/
public class Fibonacci
{
private int oneBack = 1;
private int twoBack = 0;
/**
* @return the next value in the sequence
*/
public int findNext()
{
int result = oneBack + twoBack;
twoBack = oneBack;
oneBack = result;
return result;
}
}
In order to see how this works, we need a class with a main method to run our code.
Create a class named Runner and make it look like this:
77
/**
* A simple main to let us play with Fibonacci numbers
* @author Merlin
*/
public class Runner
{
/**
* @param args
*/
public static void main(String[] args)
{
Fibonacci f = new Fibonacci();
int next = f.findNext();
while (next < 100)
{
System.out.println(next);
next = f.findNext();
}
}
}
Set a breakpoint at the beginning of that code and use “Step Into” and “Step Over”
to watch it run. Before you continue, make sure that you understand how the two
instance variables, oneBack and twoBack, are keeping track of where we are in the
Fibonacci sequence so that the method findNext() returns the next value in the
sequence each time it is called.
Look at the declarations of our two instance variables and notice that we have
followed the principle of information hiding by making their values private. This
ensures that only code in the Fibonacci class can modify their values.
Getters and Setters
The principle of information hiding motivates us to mark all instance variables as private in
order to limit access so that only the class defining the variable can see or modify that variable’s
value. That is a very strong limitation; often code in other classes needs to be able to see the
value of an instance variable and may even need to modify that value. In order to allow that type
of access while obeying the principle of information hiding, a class can contain public methods
that allow other classes to access private variables.
Consider this addition of our Person class:
78
/**
* A Person that only has an age
* @author Merlin
*/
public class Person
{
private int age;
/**
* Create a person with a given age
* @param age the age of the person
*/
public Person(int age)
{
this.age = age;
}
/**
* @return the age of this person
*/
public int getAge()
{
return age;
}
/**
* Change the name of this person
* @param age the person's new age
*/
public void setAge(int a)
{
age = a;
}
}
This class obeys information hiding because its instance variable, age, is private. However, it
provides two methods, getAge() and setAge(), that other classes can use to see the value in that
variable and to change the value of that variable respectively. In some ways, providing this kind
of access seems to undermine the control that information hiding values. However, it is still true
that only code in this class can actually change the value of a variable. While the change may be
initiated by another class, the actual code that changes the value, age = a, is in this class.
Therefore, it is still true that, if a defect causes that value to be changed incorrectly, we can put
breakpoints only in this class to debug what is going wrong.
Methods like getAge() and setAge() are called getters and setters, respectively. In general,
coding conventions require that we name the methods “get” or “set” with the variable name using
the standard camel capitalization. Getters and setters together are sometimes called accessors.
Adding Getters to Our Code
For clarity in the experiments that follow, it would be valuable for our main()
method to be able to display the values of Fibonacci’s instance variables oneBack
and twoBack. Since we have followed the principle of information hiding and made
those variables private, we will need getters to allow the Runner class to see the
79
values they hold. Eclipse can help us write that code. With the Fibonacci class the
visible class in the editor, right click and select Source -> Generate Getters and
Setters. You will see this window:
Click on the triangles next to oneBack and twoBack so that you can select the
getters you want to create. We don’t really want to create setters because
allowing someone to modify one of these variables would disrupt the sequence that
the class is trying to produce, so don’t select those options. This window also gives
you options for specifying where the new methods should be placed in the file and
options for selecting modifiers for their declarations. For now, we can ignore those
options. When you click “OK,” the window will close and your source code for
Fibonacci will contain the two new getter methods.
For an example of how we would use those getters, add the following line to the end
of our main() method:
System.out.println("next = " + next + ", oneBack " +
f.getOneBack() + ", and twoBack = " +
f.getTwoBack());
Now, run the program and you should see this output:
80
1
2
3
5
8
13
21
34
55
89
next = 144, oneBack 144, and twoBack = 89
At this point, some people get output that starts like this:
1
next
2
next
3
next
5
next
8
= 2, oneBack 2, and twoBack = 1
= 3, oneBack 3, and twoBack = 2
= 5, oneBack 5, and twoBack = 3
= 8, oneBack 8, and twoBack = 5
They have put the println statement in the wrong place. Figure out where
they put it to make it output so many times.
Before you go on, make sure you can explain why the variables next and oneBack
have the same values.
Size Limitations
We have been using the types that Java gives us without paying close attention to their limitations
when, in fact, each type has a fixed size. On some standardized tests, there are boxes for each
digit of an answer. If the test only has four boxes, you know that the answer cannot be bigger
than 9,999. If our calculations give us an answer larger than that, we won’t be able to write it in
the space on the answer sheet.
Here is a list of all of the primitive types provide by Java and the values they can hold:
81
Type
Description of Values
Range of Values
byte
integer
-128 to 127
short
integer
-32768 to 32767
int
integer
-2,147,438,648 to
2,147,438,647
long
integer
-9,223,372,036,854,775,808 to
9,223,372,036,854,775,807
float
real numbers
double
real numbers with higher
precision
boolean
conditions
true or false
char
characters
Unicode characters
We’ve been using int variable every time we wanted an integer. This is very common even if
we expect that the values won’t be larger than a short can handle. Many of the standard Java
functions return int values, so storing them in short or byte variable requires an explicit
conversion. Therefore, in general, unless you have a large array of values, the space saved by
selecting the more specific data types isn’t worth the effort the conversions require.
While it isn’t exactly accurate, you can think of the integral types as having a boolean that holds
whether the number is positive or negative. That boolean is called the sign bit. If we try to store
a number that is too big for the space allocated, the operation overflows and the sign bit gets
changed unexpectedly. This means that, if we try to store too large a number, the value we
retrieve from the variable will be dramatically different from what we expect. For this reason, we
have to be careful not to cause our variables to overflow.
When we work with real numbers, we have two options: float and double both hold real
values. To understand how these types work, remember scientific notation where every number
is written in this type of form: 0.333 x 104 which represents 3330. In this notation, a real number
is stored in parts: the mantissa that holds the fractional part and the exponent that specifies how
many places to move the decimal point. As with the integral types, the difference between float
and double is the amount of memory that each allocates: double variables are given more
memory, so they can hold larger mantissas. This means they can hold more precise values.
However, like the integral types, many Java functions return double values, so unless we are
allocating a large number of spaces (like a large array), we generally use double to reduce type
conversions.
Technically, the char data type holds integers, but each value represents a character (letter,
digit, punctuation and characters from other languages). Unicode is the mapping from each
integer to a specific character.
82
Playing With Size
When numbers overflow the space allocated for them, some very surprising things
can happen. In your main() method, change the condition of the while loop to be
(next > 0)
It seems like that should make our program run forever. After all, the Fibonacci
sequence we are generating is an increasing sequence, so how could the number we
generate ever be less than zero?
Run the program and you should see output that ends with this pattern (scroll up to
see that the sequence is correct up to this point):
102334155
165580141
267914296
433494437
701408733
1134903170
1836311903
next = -1323752223, oneBack -1323752223, and twoBack = 1836311903
Notice that, if we don’t do anything special, Java doesn’t put the commas in these
larger numbers. That’s true when we write code, too: numbers can only contain
digits and, possibly a sign.
Let’s check that the earlier portions of the sequence are working:
102334155
+ 165580141
--------267914296
That part of the sequence looks right. The last step should have done this addition:
1134903170
+ 1836311903
---------2971215073
Instead, the result it gave was -1323752223. To understand what has happened
here, remember that the largest number an int can hold is 2,147,483,647. When
our addition tried to store a number larger than that limit, an overflow occurred
and that changed the sign bit. This made the larger number appear to be negative.
Once the overflow happens, the value in result is invalid.
Watching the Overflow
At this point, it might feel like adding two large numbers causes the overflow, but
we can actually cause the overflow by just adding one. Let’s add some code that
83
demonstrates exactly where the overflow occurs in int variables. Add this code at
the bottom of your main() method:
System.out.println("");
System.out.println("Adding one . . .");
int i;
for (i = 2147483645; i > 0; i++)
{
System.out.print(" " + i);
}
System.out.println(" " + i);
The loop in this code starts with i having a value that is close to the limits of what
an int can hold. At each pass through the loop, it adds one to i and, surprisingly,
the loop will end when i becomes negative. Run your program and the output should
end with this:
Adding one . . .
2147483645 2147483646 2147483647 -2147483648
In this output, you can see that, when we add one to the largest number an int can
hold, an overflow occurs and the result turns negative.
Let’s Try
Long Variables
Maybe we can fix our problem by storing our values in long variables instead of
int variables. To try this, change all of these things to have the type long instead
of int:
•
•
•
the variable next in our main() method (be very careful NOT to change the
type of the variable i or your for loop will run for a very long time).
the instance variables in Fibonacci (oneBack and twoBack)
all three of the methods in Fibonacci (findNext(), getOneBack(), and
getTwoBack() ).
Now, run the Runner again. Changing to store our sequence in long variables lets us
generate bigger numbers, but they still overflow eventually. Your output should end
like this:
259695496911122585
420196140727489673
679891637638612258
1100087778366101931
1779979416004714189
2880067194370816120
4660046610375530309
7540113804746346429
next = -6246583658587674878, oneBack -6246583658587674878, and twoBack
= 7540113804746346429
If-then-else statements
In Lab 1, we learned about conditional statements that look like this:
84
if (<condition>)
{
// then block
}
When that is executed, the computer evaluates the condition and only executes the code in the
then block if the condition is true. Sometimes, we want to choose between two different
behaviors: one when the condition is true and another when the condition is false. In these cases,
we can add an else block to the condition:
if (<condition>)
{
// then block
} else
{
// then block
}
When these are executed, the condition is evaluated. If it is true, the code in the then block
executes and, if it is false, the code in the else block executes.
Make the Sequence Wrap
Instead of letting the sequence go negative (which feels VERY wrong), let’s change
our Fibonacci class so that, when we our numbers get too long to hold, the sequence
starts over. In other words, we’d like the sequence to look like this:
0
1
1
2
3
5
.
.
.
2880067194370816120
4660046610375530309
7540113804746346429
0
1
1
2
3
5
.
.
.
This means that we need two ways of computing the new values of next, oneBack,
and twoBack: the normal way of generating the sequence and a different way to
make the sequence wrap around. Make your findNext() method in Fibonacci look
like this:
85
public long findNext()
{
long result = oneBack + twoBack;
if (result < oneBack)
{
oneBack = 0;
twoBack = 1;
result = 0;
} else
{
twoBack = oneBack;
oneBack = result;
}
return result;
}
This code is detecting that an overflow has occurred because the value we
generated as the next one in the sequence is less than the previously generated
value. Since the sequence is always increasing, this clearly shows that something
has overflowed. If the overflow has occurred, it sets up our variables so that next
starts at zero and subsequent calls to findNext() will generate the subsequent
values in the sequence.
To see this work, let’s look at our main() method. This loop is the loop that is
generating the entire sequence:
Fibonacci f = new Fibonacci();
long next = f.findNext();
while (next > 0)
{
System.out.println(next);
next = f.findNext();
System.out.println("next = " + next + ", oneBack " +
f.getOneBack() + ", and twoBack = " +
f.getTwoBack());
}
Since we have made the sequence start over at zero, this loop will stop executing
right at that point. Add this code right after that to verify that the sequence
restarts properly:
int j;
for(j = 0; j< 5; j++)
{
next = f.findNext();
System.out.println("next = " + next + ", oneBack " +
f.getOneBack() + ", and twoBack = " +
f.getTwoBack());
}
When you run your program, your output should end like this:
86
next = 4660046610375530309, oneBack 4660046610375530309, and twoBack =
2880067194370816120
4660046610375530309
next = 7540113804746346429, oneBack 7540113804746346429, and twoBack =
4660046610375530309
7540113804746346429
next = 0, oneBack 0, and twoBack = 1
next = 1, oneBack 1, and twoBack = 0
next = 1, oneBack 1, and twoBack = 1
next = 2, oneBack 2, and twoBack = 1
next = 3, oneBack 3, and twoBack = 2
next = 5, oneBack 5, and twoBack = 3
Adding one . . .
2147483645 2147483646 2147483647 -2147483648
You should be able to show how that demonstrates that the sequence has started
over and be able to explain how the values we gave next, oneBack, and twoBack
caused the sequence to start over correctly.
BigIntegers
In some situations, we need to store integers that are much larger than the values that even long
variables can hold. As an example, the prime numbers that are used in encryption algorithms can
have millions of digits. In these cases, we clearly have to do something beyond the primitive
types Java gives us. However, Java still helps! One of the standard classes that comes with Java
is BigInteger. The integers that can be stored by an instance of BigInteger can be arbritrarily
large. Essentially, their values are unbounded.
Learning About BigInteger
Java provides us with hundreds of classes that can make programming much easier.
Instead of having to build (and debug) a class of our own, we can often make use of
one of these existing classes. That set of classes and the methods they provide
are called the Java Application Programming Interface or Java API for short. The
challenge is in finding the class and figuring out how to use it. One of the keys to
that challenge is looking at the standard documentation of the Java API.
Go to http://java.sun.com and find the API documentation for the standard edition
of Java. You may have to roam around a bit, but it’s generally under “resources” or
“APIs.” Just remember that you’re looking for the documentation, not the code.
When you find it, you will see frames that look like this:
87
The top left frame is the standard Java packages. In Java, a package is a set of
related classes. We partition large systems into packages in order to keep related
things together as another strategy for helping us find existing code. The bottom
left frame is a list of all of the classes that Java provides. Scroll down there to
find the BigInteger class and click on it. The main frame will then contain the
documentation of that class. It will tell you about the class in general and then will
give you descriptions of every public constructor, constant, and method that the
class provides. Read through it to see the types of things you can do with
BigIntegers.
Javadoc Comments
The type of documentation that you saw in the Java API documentation is quite standard and is
actually generated from the code itself! When we write code, we include a special kind of
comment at every declaration. These are called javadoc comments and have this form:
/**
* A summary sentence. A more complete description which can
* as long as you like and can be formatted using html.
*
* <block tags with details>
*/
When we place comment of this format before each declaration of a public class, method,
instance variable, or constant, we can use a tool called javadoc to generate a website of the same
structure as the Java API website. The details in that website will be extracted from the javadoc
comments in the source code.
There are a variety of block tags, but the most commonly used tags are:
•
•
Useful in all comments
• @author <name>
For methods
• @return <description of return value>
• @param <param name> <description of parameter>
• @see <class name>
88
Setting up Eclipse to Generate Javadoc Comments
Eclipse can be configured to report warnings or errors for syntax errors in javadoc
comments. To do this, select the project in the Project Explorer and click Project>Properties. Under Java Compiler, select Javadoc. Click on Configure Workspace
Settings so that your changes will affect all of your projects. Then select
“Warning” for all three types of problems listed and make that apply to all members
as visible as Public.
When you OK those changes, the compiler will re-run. If you have any problems in
your javadocs, you will see yellow squiggly lines denoting the warnings from the
compiler. For example, the getters in Fibonacci should have a warning because they
do not have javadoc comments. Eclipse can help again. If you right click on the
yellow circle with the exclamation point at the left side of the line with the
warning, one of the options will be “Quick Fix.” Clicking that will give a list of the
possible fixes Eclipse can make for you. In this case, it only offers “Add Javadoc
Comment” which is what we want, so click on that. Eclipse will generate the
framework of the comment with some block tags as appropriate. You then only have
to fill in the English!
Fix all of the warnings in your project before you go on.
At any time, you can generate the javadoc documentation for your code. Just click
Project->Generate Javadoc. It will ask you where you want it to store the
documentation. When it finishes, you’ll see an “index.html” file in that folder which
will be the root of all of your documentation.
Primitive vs. Reference Types
The types in the previous table are primitive types meaning that the Java compiler knows how
much memory they require. When you declare a variable to have a primitive type, the compiler
immediately allocates the space it needs. All other types, including arrays are reference types.
Since the compiler doesn’t know how much space they will require, their declaration only
allocates enough space for a pointer. Subsequent use of the new statement is required to allocate
the space for the array or instance.
In addition to the use of the new statement, there is one other important difference in how
primitive and reference types behave. Consider this code:
int x1;
int x2;
x1 = 3;
x2 = x1;
The second assignment statement will copy the value of x1 into the variable x2. This is the
resulting memory diagram:
89
Now, suppose these were the next lines in our program:
x1 = 4;
System.out.println("x2 = " + x2);
The first statement would modify the contents of x1 only:
So the println would output “x2 = 3”.
Let’s see how this sequence of operations behaves differently when we have reference variables.
Suppose FancyClass is a class that holds one instance variable, value, that is an integer. That
instance variable is initialized by the constructor and has the appropriate getter and setter. Then,
code with similar functionality would look like this:
FancyClass x1;
FancyClass x2;
x1 = new FancyClass(3);
x2 = x1;
x1.setValue(4);
System.out.println("x2 = " + x2.getValue());
After the call on the constructor, the memory diagram would look like this:
As before, the assignment statement (x2 = x1;) will copy the contents of x1 into x2. This
time, however, that means that it will copy the pointer from x1 to x2, resulting in this memory
diagram:
Notice that both x1 and x2 are pointing to the same instance. When we call the setter on x1, it
changes the value in that instance:
90
This means that, when we print out the value returned from calling the getter of x2, we get 4 – not
3.
The important thing to notice is that primitive and reference variables differ in how they behave
when they are the only thing on the right hand side of an assignment statement. In both cases, the
“value” of the variable is copied into the variable on the left hand side. However, for reference
types, that “value” is a pointer, so both variables end up pointing at the same instance. That
means that calling methods on one of the variables will change the values seen by the other
variable. This is a significant difference between reference and primitive variables to which you
must pay careful attention.
Using BigIntegers
While changing from int to long was relative easily, changing to BigInteger is going
to be a little more complex. The change from a primitive type to a reference type
complicates the code a bit. Therefore, we are going to create a new class named
BigFibonacci that produces the same sequences of numbers, but stores everything
in BigIntegers. Create that class and make it look like this:
91
import java.math.BigInteger;
/**
* A class that supports arbitrarily large Fibonacci numbers.
* @author Merlin
*/
public class BigFibonacci
{
private BigInteger oneBack = new BigInteger("1");
private BigInteger twoBack = new BigInteger("0");
/**
* @return the next value in the sequence
*/
public BigInteger findNext()
{
BigInteger result = oneBack.add(twoBack);
twoBack = oneBack;
oneBack = result;
return result;
}
/**
* @return the most recent value we generated
*/
public BigInteger getOneBack()
{
return oneBack;
}
/**
* @return the previous value we generated
*/
public BigInteger getTwoBack()
{
return twoBack;
}
}
Compare this class to the Fibonacci class and you should see these differences:
•
•
creating oneBack and twoBack required a call on a constructor (since the are
objects) and the String parameter to that constructor specifies the initial
value
in order to add BigIntegers we have to call a method because the operator
‘+’ is only defined for the primitive types.
The code in our main() method that references these Fibonacci numbers will have to
be a little bit different from our previous code as well. Add the following code at
the end of your main() method (the number of nines in the comparison doesn’t really
matter):
92
BigFibonacci bigF = new BigFibonacci();
BigInteger bigNext = bigF.findNext();
while (bigNext.compareTo(new BigInteger("9999999999999999")) < 1)
{
System.out.println(bigNext);
bigNext = bigF.findNext();
}
System.out.println(bigNext);
First, look at how this loop differs from the loop we used when we were using long
variables. Just like with ‘+’, the ‘<’ operator does not accept BigIntegers as
operands. Therefore, the loop has to use the compareTo() method in the
BigInteger class. This method compares the values of the BigInteger on which it is
called (bigNext in this case) and the BigInteger passed into its parameter. It
returns 0 if they are equal, 1 if the parameter is smaller and -1 otherwise.
In order to make this code compile, you need to tell the compiler where to find the
BigInteger class. To do that, add this line at the beginning of your Runner class:
import java.math.BigInteger;
Run your code again. This time, the sequence never goes negative. Play with the
number in the comparison (which now is a sequence of nines) until you believe that
the sequence can go as high as you like. While BigIntegers can be negative, don’t try
making that number less than or equal to zero as that will make the loop run forever
(the sequence will never generate something that small since it is always increasing)
Vocabulary
accessor
Fibonacci Numbers
local variable
setter
BigInteger
float
long
sign bit
block tag
getter
mantissa
short
boolean
information hiding
overflow
Unicode
byte
int
package
char
Java API
primitive type
double
javadoc comment
reference type
Lab Questions
1.
2.
3.
4.
5.
6.
Show one place in the code that exhibits the property of information hiding.
What do the variables oneBack and twoBack do?
What is the difference between long and int?
Why is BigInteger a class instead of a primitive?
Why did we have to do addition differently when we started using BigIntegers?
What is overflow? Why does it look like it goes negative?
93
Content Questions
1. What is the value of the variable x at the completion of each of the following pieces of
code:
a. int z = 34;
double x = z/3.0;
b. int z = 34;
double x = z/3;
2. What information can be stored in a variable whose type is each of the following
a. int
b. double
c. boolean
d. char
3. Draw the memory diagram for each of the following pieces of code:
a. String x = new String(“Fred” );
String y = new String(“Henry” );
y = x;
4. Draw the memory diagram and show the output for the following pieces of code.
Assume that there is a class named Stupid that has an instance variable, silly, and its
getters and setters.
a. int x;
x = 32;
int y;
y = x;
y = 14;
System.out.println(x);
b. int[] x;
x = new int[4];
x[2] = 32;
for (int i = 0; i < x.length; i++)
{
System.out.println(x[i]);
}
c. Stupid[] x;
x = new Stupid[3];
for (int i = 0; i < x.length; i++)
{
Stupid temp;
temp = new Stupid();
temp.setSilly(i);
x[i] = temp;
}
System.out.println(x[0]);
94
d. Stupid x;
x = new Stupid();
x.setSilly(32);
Stupid y;
y = x;
y.setSilly(14);
System.out.println(x.getSilly());
5. Why are primitive and reference types treated differently?
95
Lab 6 – Test Driven Development
and More About Variables
Test Driven Development
We are now ready to start thinking about how code is developed. In previous labs, we have been
focusing on reading code that was written by someone else to demonstrate many of the common
Java constructs. The code has been presented in a functionally complete and cleaned up form, but
it’s rarely in that form until we are ready to share our code. The process of writing code is an
iterative process with many false starts and wrong turns along the way. In general, during each
step, we add a little bit of functionality making sure we don’t break anything we built in previous
steps.
As we are developing code, we need to test the parts we have written. Up to this point, we have
done that by creating a runnable class that uses instances of the other classes we are building.
That may work for the small programs we have used, because with each change we made, we
were able to look at the output to see if it was correct. As our systems grow and we have more
classes and each class has more methods to test, the amount of output that the runnable class
produces becomes quite large. It may become impractical to run the program and input, by hand,
all the data necessary to be sure the new section of code executed correctly. For example, let’s
say your new code allows you to change the age of the people in your database. You can run the
code and easily check if one specific person’s age changed, but did it work for a 2 month old
baby with zero as his year, or a woman who is 99 turning 100? (Note the extra digit needed to be
stored.) Can you add to or subtract from a person’s age? What happens if you subtract more
years than the person’s age? In situations like these, we’d like a set of tests that checks all of the
functionality we’ve built and tells us exactly which parts are broken. To do that, we will use a
tool called JUnit.
To use JUnit, for each class we are going to build, we create another class that holds a set of tests
that verify the functionality we are building. For example, in a previous lab, we developed two
classes: Card and Deck. If we had used JUnit to test them, we would have also built two classes
containing their tests: TestCard and TestDeck. In each of those test classes, methods encode
each of the tests we want JUnit to run. While there are many subtleties to using JUnit, most of
the tests we will develop have a common structure that we’ll learn about in this lab.
JUnit gives us the ability to structure our approach to writing code. Since we know we shouldn’t
write all of the code at once, we want a process that helps us manage each change we make to the
system. The strategy we will use is called Test Driven Development (TDD). This strategy is
summarized by this mantra: RED, GREEN, REFACTOR:
•
•
•
RED – write a test and watch it fail
GREEN – make the test pass
REFACTOR – clean up the code as necessary
For this lab, most of the code will be given to you, so we won’t really need the REFACTOR step.
However, as our systems get larger, we’ll see that stopping to examine our code and cleaning
things up at each step will be very important. As we use this strategy, we’ll look at its effects on
our development process and code to evaluate its effectiveness.
96
Designing Our Classes
For this lab, we’re going to build a library system that keeps track of books and the
people who have checked them out. As always, the classes in our system will be the
“things” the system needs to know about: books and patrons.
Remember, when we think about designing a class, there are two issues we address:
•
•
what it stores and
what it does.
“What it stores” means what data is unique to each instance. “What it does” is the
set of methods that encode the operations that will give the instances the required
behaviors. For our Book class, these are:
•
•
What
o
o
o
What
o
o
o
it stores:
the title of the book,
whether or not the book is currently checked out, and
the number of times the book has been checked out.
it does:
gets checked out
gets checked in
answers these questions
 are you checked out?
 how many times have you been checked out?
Setting Up Our Tests
In Eclipse, create a new project to hold the files for this lab. The first class we’re
going to create is the class that will hold the unit tests for our Book class:
TestBook. To do this, right click on the package in the Package Explorer and select
New->JUnit Test Case. At the top of the popup, select “New JUnit 4 Test” and give
it the name TestBook. Click Finish and a popup will appear. It offers to add JUnit 4
library to the build path. Click OK because that is telling the compiler where to find
the extra classes it needs to run JUnit. With that, Eclipse will build the
framework of your class.
Make your test class look like this:
97
import static org.junit.Assert.*;
import org.junit.Test;
/**
* Tests the functionality provided by the Book class
*
*/
public class TestBook
{
/**
* When a book is created, it should know its title.
*/
@Test
public void testInitialization()
{
Book book1;
book1 = new Book("Catch 22");
assertEquals("Catch 22", book1.getTitle());
}
}
At this point, this test won’t compile because we haven’t built the Book class, but
there are some important things to notice:
First, the import statements at the top of the file are telling the compiler where to
find the definition of specific JUnit classes that we need.
Second, we have exactly one test and it is defined by the method named
testInitialization. We know that method is a test because “@Test” precedes its
declaration. That is called an annotation and gives the compiler and the JVM more
information about that method.
JUnit verifies the results of our test with calls on the assertEquals() method. This
method has two parameters. The first parameter is the expected value; the result
that is the “correct” result. The second parameter is the actual value; the result
our code is giving us. If these two values are equal, the test will continue to run
and, if the test method completes, the test passes. If the two values differ, the
test will immediately stop and fail.
So, here is a summary of exactly what each step of the test is doing:
•
•
•
declare a variable whose type is Book (this will hold one instance of the Book
class)
create a book with the title “Catch 22” make book1 point to it.
check to make sure that the book says that its title is “Catch 22”
Make the Test Compile
We are still working on the RED part of our mantra because we still haven’t seen
the test fail. To compile our test, we’re going to need the beginnings of the Book
class. Use Eclipse to create the Book class. Start by figuring out what it needs to
98
know. In order to make the test pass, a book object would have to remember its
title, so it needs an instance variable that is a String and basic definitions of the
methods the test calls. Make your Book class look like this:
/**
* A class that represents a book in our library
* @author Merlin
*/
public class Book
{
/**
* Create the book and give it a title
* @param t
*/
public Book(String t)
{
}
/**
* @return the title of this book
*/
public String getTitle()
{
return null;
}
}
Clearly, this code isn’t going to work! At this point, the goal is to write as little
code as possible to make the test compile. Now you can run the test by right
clicking on the test class’s name in the package explorer and selecting Run As>JUnit Test. You should see a new tab open where the package explorer is and it
should look like this:
It shows that you ran TestBook, the red line signals you that it failed (we have now
completed the RED part of our mantra), and it displays the number of failures as 1.
If you click on the triangle next to TestBook, it will show you the status of each
test that was run.
99
Make the Test Pass
When we are doing TDD, we want to be in the RED state as little as possible. So,
we write as little code as possible to make the test pass.
If you look at the lower left panel of Eclipse, JUnit will tell you what error has
occurred. In this case, the error message is:
java.lang.AssertionError: expected:<Catch 22> but was:<null>
and the line below that tells you exactly which line of which test failed. Double
click on that message, and Eclipse will put the cursor at the line of the test that
failed. In this case, Eclipse puts you on the assertEquals() call in our test. The
error message is saying that the expected value is “Catch 22”, but our getTitle()
method is returning null. If you look at our code, the test is right, getTitle() is
always returning null – not the title of our book.
With the goal of getting to GREEN as soon as possible, change your getTitle()
method to look like this:
/**
* @return the title of this book
*/
public String getTitle()
{
return "Catch 22";
}
Since the test wants us to return “Catch 22”, that’s what we’ll return! Re-run your
tests and they should be GREEN. (You can re-run the most recently run test by
clicking the green circle with the “play” triangle in the JUnit frame.)
Clean Up That Mess!
If we look at our code, it works well only if the book we are interested in has the
title “Catch 22.” Obviously, that isn’t going to work for all of the books in our
library. Now that the test passes, we need to clean up our code (REFACTOR) and
make it work for other books.
One way to clean up is to look for duplication. In this case, we have the string
“Catch 22” in both our TestBook and our Book classes. In order to eliminate that
duplication, think about our Book class. One of the things we would like each book
to “know” is the book’s title. That means that we need an instance variable to hold
the title. Add the declaration of an instance variable whose name is title and whose
type is String to your Book class. Make sure you pay attention to the principle of
Information Hiding by marking that variable as private.
When we initialize a book, we need it to remember its title, so our constructor will
have to save the title that is passed as its parameter in that instance variable. To
do that, add this assignment statement to your constructor:
100
title = t;
Now, to finish getting rid of the duplication, make getTitle() return the title we’ve
stored in the instance variable. Change it to look like this:
/**
* @return the title of this book
*/
public String getTitle()
{
return title;
}
Re-run your test to verify that we haven’t broken anything while we were cleaning
up.
TDD in More Detail
Now that you’ve been through one cycle of our RED, GREEN, REFACTOR mantra, there are a
few more details you should see.
The RED part of our process involves writing test and writing just enough code to make it
compile. That lets us run the test and see it fail. People often ask, “Why do we bother to watch
the test fail?” We do this because, it helps catch mistakes we can make in writing the test. For
example, sometimes we accidentally write a test that doesn’t really “test” anything. Either we
forgot the assertEquals() calls or coded them so that they would pass in any situation. As our
system grows, we can also run into situations where we think we need to add functionality, so we
write the test and then, to our surprise, we find that the system already does what we need. While
that is rare in projects you develop alone, in projects where many people are developing the code,
this can happen quite easily.
Once the test is RED, we write the simplest code that will make the tests GREEN. In the process,
we’ll figure out which parts of the system need to change. We’ll also verify that we didn’t break
other tests as we build the code for this test.
Writing the simplest code that works isn’t a recipe for building well-designed code. Therefore, at
each step along the way, we clean up our code. As we said above, one way to find the parts of
code that require attention is to look for duplication. However, you can REFACTOR anything
that doesn’t look good. In this step, we also make sure that all of our javadoc comments are
complete.
Checking Out a Book (Phase 1)
Now that we can create books, let’s work on checking one out. This will require that
we build the portions of our Book class that are related to being checked out. To
start, let’s modify our test to verify that the newly created Book is not checked
out. Add this line to your test:
assertFalse(book1.isCheckedOut());
(Fix the comment for the test, too!) In this check, we are using assertFalse()
instead of assertEquals(). The test will pass if the value in the parameter is false
and will fail otherwise.
101
For it to compile, but fail, you must add the isCheckedOut() method to your Book
class:
public boolean isCheckedOut()
{
return true;
}
Re-run your tests and they should be RED.
The simplest way to get to GREEN is to change isCheckedOut() to return false.
Now, clearly, always returning false from isCheckedOut() isn’t going to work
forever, but, it works for now. As we write more tests, we’ll have to do better!
Checking Out a Book (Phase 2)
The next step we should build is to check out a book. Here is the test:
/**
* When a book is checked out, it should know that!
*/
@Test
public void testCheckOut()
{
Book book1;
book1 = new Book("Catcher In The Rye");
book1.checkOut();
assertTrue(book1.isCheckedOut());
}
This is what each statement of this test accomplishes:
•
•
•
•
Declare book1 to have the type Book
Create the instance of Book for book1
Check out book1
Verify that book1 knows it is checked out
In order to get to RED, we need to declare the method checkout() in Book:
public void checkOut()
{
}
The method checkOut() will be called whenever a book is going to be checked out
and will perform all of the operations necessary to check a book out. It doesn’t
return anything (that’s why its return type is void), so we don’t need to put
anything in it. Remember, we’re just trying to make it compile so we can watch the
test fail.
Re-run your JUnit tests. You should see that one test passes and one test fails. If
you don’t see a list of all of the tests in the JUnit frame, click on the triangle next
to the name of your test class to expand it. Now we are RED.
102
This time, getting to GREEN is going to be a little bit harder. Since one of our
tests (testInitialization) requires isCheckedOut() to return false and our new test
requires it to return true, we’re actually going to have to make the Book remember
whether it is checked out. Since that’s something a Book will have to know, create
an instance variable named checkedOut that is a boolean (it only has to store true
or false). Here are the things the class will have to do with that variable:
•
•
•
The constructor should initialize checkedOut to have a value of false
because books start out in the library (not checked out).
The method checkout() must set checkedOut to true so that the book
remembers that is has been checked out.
The method isCheckedOut() must return checkedOut instead of true. At
this point, isCheckedOut() has become a getter for the instance variable
checkedOut. Often, when a getter is accessing a boolean, we start the name
with “is” instead of with “get.”
When you’ve made those modifications, your test will be GREEN.
For REFACTOR, make sure that your comments reflect the current functionality.
Other than that, there isn’t much to clean up.
Scale in TDD
For each RED, GREEN, REFACTOR cycle, we pick one piece of functionality we want to build.
We try to pick the smallest piece that helps us move forward. Notice that we took two cycles just
to be able to check a book out and we can’t check it back in, yet. One good way to pick the
functionality for a cycle is to make only one test be RED. In our case, we started by looking at
our existing test and seeing how it would be affected by checking out a book. Only after we had
turned our existing tests to GREEN did we think about writing a new test for the new method we
wanted.
Notice is that we are trying to run our tests as often as possible. We write very little code before
we re-run the tests. If we run the tests frequently, even while we are refactoring, the number of
changes we’ve made will be relatively small. If the tests fail we know that there will only be a
few places to look for the error. To do this, not only do we try to put as little change as possible
in each RED, GREEN, REFACTOR cycle, but we also try to REFACTOR in small pieces,
running the tests after each limited fix.
When you are refactoring, you should not only be looking for duplication. You should also be
making sure that your javadoc comments are complete, you have followed the naming
conventions, and your code is well-formatted.
Checking a Book Back into the Library
Now that we can check a book out, we can think about checking it back in. Since we
already have an instance variable that knows whether a book is checked out, we
don’t need any new instance variables. In other words, we aren’t going to need to
103
change anything that already exists, including our existing tests. We’re just need
to add a new method: checkIn(), and write a new test for it as follows:
/**
* When a book is checked back in, it should not look like it is
* checked out
*/
@Test
public void testCheckIn()
{
Book book1;
book1 = new Book("Catcher In The Rye");
book1.checkOut();
book1.checkIn();
assertFalse(book1.isCheckedOut());
}
In order to get to RED, create an empty checkIn() method in your Book class and
watch the tests fail.
Making this test pass is pretty easy. Just make the checkIn() method change the
value of checkedOut to false. Now things should be GREEN.
For our REFACTOR step, look for duplication in our tests. In each test, we have to
declare and create an instance of Book. It’d be good to get rid of that duplication,
but that’s going to require a little more information . . .
Variable Scope and Lifetime
The principle of information hiding applies to all variables, not just instance variables. Every
variable has a scope reflecting the portion of the system that can access that variable. We can
think about the concept of scope by thinking about student organizations. For example, many
computer science departments have local chapters of the national computer science honor society,
Upsilon Pi Epsilon (UPE). Each of the local chapters has someone they call “President.”
Depending on which campus we are on, the term “president of UPE” refers to a different person.
If I am at Shippensburg University, when I say, “the president of UPE,” I’m referring to the local
president at Shippensburg, but if I am at Drexel, that title refers to someone else. In this case,
which “president” we mean depends on where in the country we are. In computer science, the
scope each of president is defined by the boundaries of that president’s institution.
Just like the UPE chapters define regions in the country, each variable we declare is associated
regions of the code. That region is called the variable’s scope. For the parts of the system
outside the variable’s scope, the variable essentially doesn’t exist. In Java, the scope of a
variable depends on the way that variable is declared.
A variable’s scope is defined at compile time, but there is also a dynamic aspect to each variable.
As the system runs, variables are created and destroyed, as the code requires. Similar to scope,
this run-time behavior, which is called the lifetime of the variable, depends on the way the
variable is declared.
There are four ways a variable can be declared: instance variables, local variables, method
parameters, and for loop variables. Each of these has its own scope and lifetime behaviors.
104
Instance Variables
Remember that instance variables are the things an object “needs to know,” and they are declared
inside the class but outside of all of the methods. Each object of a class has its own copy of each
of the class’ instance variables.
The scope of an instance variable depends on the visibility modifier used in its declaration:

If there is no modifier, then the entire system can access the variable (this will not be true
as we learn more about Java, but it is true for the systems we are building now).

If the modifier is public, the entire system can access the variable

If the modifier is private, only the declaring class can access the variable.
The lifetime of an instance variable is the same as the lifetime of the object that contains it. In
other words, every time we execute a new statement to create an object, all of its instance
variables are created. Later, when the object is no longer in use and no variables point to it, that
object and its instance variables will be destroyed.
Local Variables
The variables that we define inside methods are called local variables. The scope of a local
variable generally begins at the point at which it is declared and ends at the end of the innermost
block containing that declaration. To clarify this point, look at each of the following pieces of
code that are highlighted to show the scope of the variable x (none of these methods do anything
valuable – just look at the structure of the code and what is (and is not) included in the scope of
x).
public void sillyMethod1()
{
int x;
x = 32;
}
public void sillyMethod2()
{
int y = 3;
while (y > 0)
{
int x = 42;
y = y-1;
}
y = 42;
}
105
public void sillyMethod3()
{
int y = 3;
while (y > 0)
{
y = y-1;
int x = 42;
x = x-1;
y = y – 1;
}
y = y-1;
}
public void sillyMethod4()
{
int y = 3;
while (y > 0)
{
y = y-1;
int x = 42;
x = x-1;
for (int i=0;i<3;i++)
{
System.out.println(i);
}
y = y – 1;
}
y = y-1;
}
The lifetime of local variables parallels their scope, but still reflects the running of the system. A
local variable is created when its declaration (or first assignment statement) is executed and it is
destroyed with the execution of the program leaves its scope.
Method Parameters
Method parameters are variables declared in the statement that declares the method (outside the
curly brackets holding the method body), but their scope is the method body. Here is an example
showing the scope of the variable x.
public void sillyMethod4(int x)
{
// this is the method body
x = 32;
// x’s scope is everything between these curly brackets
}
Variables that are declared as method parameters are created when the method is called and are
destroyed when the method returns.
For Loop Variables
For loop statements have top ability to contain the declaration of a variable in the initialization
portion of the statement. The scope of these variables matches the block contained by the for
loop:
106
public void sillyMethod5()
{
int y = 32;
for (int x=0;x<3;x++)
{
y = y + x;
System.out.println("Y is " + y);
}
System.out.println("y ended at " + y);
}
The lifetime of a for loop variable is when for loop is executing; it is created as the for loop
starts running and is destroyed when the for loop finishes running.
Implications of Scope
The scope of a variable is one key to understanding how a piece of code works. For example, the
following code declares two variables named x and two variables named y:
public int subtract(int x, int y)
{
return x-y;
}
public void sillyMethod7()
{
int x = 5;
int y = 3;
System.out.println( subtract(x, y));
System.out.println( subtract(y, x));
System.out.println( subtract(7, 2));
}
Running this code results in this output:
2
-2
5
In sillyMethod7(), x will have the value 5 and the scope of that variable is contained within that
method. Similarly, y will have the value 3 and its scope is from its declaration to the end of that
method. Since their scopes are limited, the method subtract() cannot see those variables.
However, subtract() also declares variables named x and y, but their scope is limited to within the
subtract() method. When the subtract() method is called, the expressions in the calling code are
evaluated and those values are given to subtract()’s variables x and y respectively. So, the values
of x and y in subtract() differ each time the method is called:
code in sillyMethod7()
value of x in subtract()
value of y in subtract()
subtract(x, y)
5
3
subtract(y, x)
3
5
subtract(7, 2)
7
2
107
A good understanding of scope is also critical to writing robust code. To obey the principle of
information hiding, we must declare our variables to have the smallest scope possible. This
minimizes the lines of code that can affect each variable’s value which has two important side
effects. First, it minimizes the portion of the system in which we have to look for defects and,
second, it reduces the risk that code written by someone else will accidentally affect our
variables.
The Keyword this
In our Upsilon Pi Epsilon analogy, sometimes the “scope” of a presidency overlaps. Two local
chapters never have overlapping regions, so the “scope” of the local presidencies never overlap.
However, there is a national UPE organization and it has the office of president. Imagine that you
are in Shippensburg, but you want to refer to the national president of UPE. In this case, the term
“president” has some ambiguity: it could mean the local president or the national president. In
general, we resolve this ambiguity by treating the term “president” as meaning the local president.
If you want to refer to the national president, you have to specifically say, “the national
president.” We can see a similar ambiguity in the scope of variables.
In most cases, Java prevents us from declaring two variables with the same name if their scope
overlaps. However, there is one exception to this rule: local variables can have the same name as
instance variables in the same class. For example, consider this class representing a person:
/**
* A Person that only has an age
* @author Merlin
*/
public class Person
{
private int age; // instance variable
/**
* Create a person with a given age
* @param age the age of the person
*/
public Person(int age)
{
this.age = age;
}
}
This class has one instance variable, age. In the constructor of this class, there is one parameter,
that is also named age. While this creates some ambiguity, Java will let you declare that
parameter to have the same name as the instance variable. For the code that is within that local
variable’s scope, we have to be very careful when we refer to variables with that name. This is
exactly analogous to the local vs. national presidents in UPE; if we want to refer to the local
variable, we just use the variable name, but we have to be more specific if we want to refer to the
instance variable.
Java includes the keyword this to let us be specific about things related to the current instance.
When we precede a variable with this and a period we are specifying that we mean the instance
variable in the object that is running the code. When another variable has the same name as an
instance variable, this allows us to specify which we mean: when we use this we mean the
instance variable and, when we don’t use this, we mean the local variable. Eclipse helps us
108
keep things straight by color coding the variables; all instance variables will be blue while local
variables will be black.
The assignment statement in the constructor of Person in the example above is going to evaluate
the local variable (the one defined in the parameter list) and assign that value to the instance
variable of the object being created. This shows that when we use just the variable name, we are
referring to the local variable and the keyword this allows us to reference the instance variable.
Java will allow you to use this every time you use an instance variable and some people believe
that you should always use it because it makes explicit the fact that the variable isn’t local.
However, the more common use, as in this text, is to only use this when we need to distinguish
an instance variable from a local variable with the same name.
@Before in JUnit
A number of tests often require the same type of set up. In fact, tests often consist of these steps
•
•
•
Create and initialize an instance of the class we are testing
Do something to that instance
Verify that the instance has responded correctly
Often, that first step has a common portion that all of the tests require. In that case, we can create
one method whose return type is void that includes that common portion and label it with a
@Before annotation. This tells JUnit that we want that method to be run at the beginning of
every test we built. This is a good way to reduce the duplication of code in our tests
Refactoring the Set Up Of Our Tests
Creating a set up method will eliminate the duplication in our tests, but first we
need to see how variable scope will affect this change. In our code now, we have
three different variables named book1; there is one in each of our test methods.
These are local variables because they are declared within methods and, since they
are in different methods, their scopes do not overlap.
We want to create a single method that creates an instance of Book that all of our
tests will use. The obvious place to start is to use the set up code from our other
tests. The method is:
@Before
public void setUp()
{
Book book1;
book1 = new Book("Catcher In The Rye");
}
The problem with that code is the scope of the variable book1. Since it is declared
inside that method, it’s scope ends at the end of that method. That means that the
code in the other methods will not be able to see it. We need to increase the scope
of the variable book1 by moving it outside of the method (essentially making it an
instance variable for our test class). Then all of our tests will have access to it.
Make these changes to your test class:
109
•
•
•
Add this import to help the compiler find the @Before annotation:
import org.junit.Before;
move the declaration of book1 out of the setUp method to make it an
instance variable,
delete the duplicated code in each test method including:
o the declaration of the local book1 variables,
o the code that creates the instance of book1.
Remember, Eclipse color-codes variables to tell you whether they are instance
variables or local variables. Instance variables will be blue while local ones will be
black. Verify that all of the book1 variables in your code are blue.
Re-run your tests and they should . . . fail? But we were just cleaning things up!
That’s why we run the tests regularly – sometimes we accidentally break things.
Look at the error message that JUnit gives you:
org.junit.ComparisonFailure: expected:<Catch[ 22]> but was:<Catch[er In
The Rye]>
Click on the second line of the JUnit Failure Trace frame and you’ll see that the
assertion that failed is in testInitialization. The error message is not only showing
you the expected String and the String the test actually saw, but is also marking
the parts of the Strings that differ. Essentially, we made the setup method create
an instance with a different title than this test expected. We can fix that by
changing the assertEquals to expect to see “Catcher In The Rye”. That fix should
get you back to GREEN.
That last problem underscores one more place where we have duplication: we have
hard-coded the title string in multiple places. When we changed one, we created a
problem because we didn’t change the other. Make a constant that holds the String
“Catcher in the Rye” and use it to eliminate the hard-coded values. As always, rerun the tests to make sure things are still GREEN.
Number Of Times A Book Has Been Checked Out (Phase 1)
One of the things we wanted books to be able to do was to tell us how many times
they have been checked out. In other words, we want a method that returns an int
that is a count of times the book as been checked out. As always, we need to start
by examining how this functionality would change the tests we have, so let’s start
with testInitiliazation(). When a book is created, the number of times it has been
checked out should be zero, so add this assertion to your test:
assertEquals(0, book1.getNumberOfCheckOuts());
To get to RED, we’ll need to add the method to the Book class and make sure it
doesn’t return 0.
110
public int getNumberOfCheckOuts()
{
return 1;
}
Run your tests and watch testInitialization() fail and you’ll be at RED.
Getting to GREEN is simple: just make getNumberOfCheckOuts() return zero.
At this point, there’s nothing to REFACTOR except to make sure that your
comments are up-to-date and your code is pretty.
Number Of Times A Book Has Been Checked Out (Phase 2)
We still need to look at our other test to see how it is affected by this new
functionality. Since checking out a book should increase the count, testCheckOut()
should verify that the count has become one, so add this assertion to your test:
assertEquals(1, book1.getNumberOfCheckOuts());
That should compile, so run your tests to watch it be RED.
Making this one pass is going to be a bit harder. We’re now at the point where we
need to have an instance variable that remembers the value of the count and is
incremented when we check a book out. Add that code and your tests should be
GREEN.
Number Of Times A Book Has Been Checked Out (Phase 3)
The first time we check a book out, the number of checkouts should be one, but if
we do it again, that count should become two. When we have functionality that can
happen more than once with different results, it’s good to write a test for at least
the second time to make sure that we did things correctly. In this case, if we had
just made the checkOut() method set the count to one (instead of incrementing it),
all of our tests would pass, but the functionality wouldn’t be correct. So, let’s add
one more test:
/**
* Make sure we can check a book out twice
*/
@Test
public void testCheckOutTwice()
{
book1.checkOut();
assertTrue(book1.isCheckedOut());
book1.checkOut();
assertEquals(2, book1.getNumberOfCheckOuts());
}
When you run the tests, they may be GREEN already. If so, change checkOut() to
just set the count to one instead of incrementing it. That will make the test RED
111
and verify that we really are testing an important part of our implementation. Once
you see RED, put everything back and watch it be GREEN again. There isn’t any
duplication in our code, but use the REFACTOR step to verify that all of your
comments are correct, you have followed our naming conventions, and you have
followed our formatting standards.
Test Driven Development Review
Even though this lab gave you much of the code you need, you have experienced the flow of Test
Driven Development. At each step, we made a test that required a small amount of new
functionality, watched it fail, wrote the code to make it pass, and then made sure that the code
was clean and well-documented. This incremental approach to development has many benefits.
At this point, you should see that it helps you divide the problem into smaller, more manageable
pieces, so that you don’t have to think about the whole system at one time. We’ll notice other
benefits as we get more practice with this technique.
When you are coding, always remember our mantra: RED, GREEN, REFACTOR.
Vocabulary
@Before
assertFalse()
Test-Driven Development
@Test
local variable
this
annotation
refactor
assertEquals()
scope
Lab Questions
1. When we design a class, we think about “what is stores” and “what it does.” How are
each of these things manifest in our code?
2. Explain what assertEquals() does. In particular,
a.
what are its parameters?
b. how does it affect the test when they are equal?
c. how does it affect the test when they are not equal?
d. can you have more than one assertEquals() in a test?
3. What does @Before do?
4.
Explain why this test isn’t a very good one:
@Test
public void testCheckOutTwice()
{
book1.checkOut();
assertTrue(book1.isCheckedOut());
book1.checkOut();
assertEquals(2, book1.getNumberOfCheckOuts());
112
}
Content Questions
1. Write code that initializes three variables that hold the scores a student got on three
exams and then outputs the students average grade.
2. Write a method that takes an integer parameter x and returns true if x is negative and
false otherwise.
3. What is the difference between an instance variable and a local variable in terms of:
a. the lifetime of the variable (when it is created and when it is destroyed)
b. the scope of the variable (which portions of the code can see it)
4. What is the purpose of the keyword this?
5. Explain our test-driven development mantra.
6. Explain why we write the tests before we write the code.
7. What are the benefits of incremental development?
8. The following test could be part of the tests for Java’s GregorianCalendar code. What is
it doing?
public void testSilly()
{
GregorianCalendar cal = new GregorianCalendar(2005,9,7);
assertEquals(2005, cal.get(Calendar.YEAR);
assertEquals(7, cal.get(Calendar.DAY_OF_MONTH);
}
9. The following test could be part of the tests for Java’s Math code. What does Math.floor
do?
public void testSilly()
{
assertEquals(1,Math.floor(1.2));
assertEquals(1,Math.floor(1.9));
assertEquals(-2, Math.floor(-1.2));
assertEquals(-2, Math.floor(-1.9));
}
10. The following test passes. What does the “add” method do?
public void testX()
{
GregorianCalendar cal = new GregorianCalendar(2007,9,7);
cal.add(Calendar.YEAR, 2);
assertEquals(2009,cal.get(Calendar.YEAR));
cal.add(Calendar.MONTH, 3);
assertEquals(2010,cal.get(Calendar.YEAR));
assertEquals(0,cal.get(Calendar.MONTH));
}
113
Lab 7 - Test Driven Development and Conditionals
Reminder: TDD
We explained in the last lab that when we write code, we will use a technique called Test Driven
Development (TDD). Essentially, we’re using the development of automated unit tests to help us
design the code that needs to be written. However, the development of the tests does not
completely precede the development of the code. We cycle between test and code development
in the following pattern:
-
RED: write a single test and watch it fail
GREEN: write the smallest amount of code that will make all of the tests pass
REFACTOR: refactor the code (both the production code and the test code)
This pattern is very important because it gives TDD much of its power.
During the RED phase, the engineer is structuring the code in his mind. This ensures that we
aren’t coding randomly and slows the coding process down to make sure that the engineer thinks
about the code before he writes it. During the time spent writing tests, it’s interesting to note that
the engineer is focused on “how will the code be used” instead of “what does the code need to
do.” This often means that the resulting classes have less complex interfaces.
During the GREEN phase, the engineer only writes the code necessary to make the test pass. This
means that 100% of our code should be executed by our tests when we are finished. “Green”
doesn’t just mean that the new test passes; it means that all of the tests pass. In this phase, we are
not only ensuring that we built what we needed, but also that we haven’t broken anything we had
built before.
The REFACTOR phase is critical to this process. Since the code is being developed
incrementally, it is critical that we continue to clean it up to improve our design. In TDD,
refactoring is not an overhead process; it is integral to the development process and becomes
second nature to the engineer. The engineer should be looking for every opportunity to refactor
the code and, since these red/green/refactor increments are very small, the refactoring activities
are usually insignificant. TDD results in almost 100% test coverage, so we can refactor with
confidence because the tests ensure that we have not broken any behavior the system was
expected to exhibit.
Set Up
You have seen conditionals (if statements) in a number of the previous labs. In this
lab, you will practice writing conditionals. You will create a class called
TaxCalculator that will contain a sequence of methods with increasingly complex
conditional statements. Each method will compute income taxes under various types
of legislation. You will be given a series of unit tests for the required functionality,
but you must follow the TDD discipline. Remember: RED, GREEN, REFACTOR!
One note about refactoring: in this lab, most of the refactoring you will do is fixing
variable names, indentation, and commenting. That isn’t really refactoring; it’s just
cleaning up. We’ll see real refactoring in later labs.
To start, create a project and a JUnit test class named TestTaxCalculator As
always, remember to select JUnit 4.0 and to have it add Junit to the build path.
114
Our First Test
Here is the first test:
/**
* In the simplest income taxes, everyone pays 5% of their income
*/
@Test
public void testSimpleTax()
{
TaxCalculator tc = new TaxCalculator();
assertEquals(0.05 * 10000, tc.simpleTax(10000.00), 0.001);
assertEquals(0.05 * 50000, tc.simpleTax(50000.00), 0.001);
}
In this test, the assertEquals() calls have three parameters each of which are
double values. This is because comparing real values can be tricky. Because of the
way they are stored, they sometimes have small rounding errors. Those errors are
usually very small, but they can make two doubles not be exactly equal. Therefore,
when you are comparing doubles, assertEquals() requires a third parameter, epsilon,
which specifies how much the expected and actual values can differ by while still
being considered to be equal. For example, in this lab, the double values are holding
amounts of money. Therefore, if they are equal within 1/10 of a penny, no other
rounding errors can introduce errors in our calculations. That is why the third
parameter of the assertEquals() is 0.001.
Put that test into the TestTaxCalculator class. To make it compile, you’re going to
have to do the following:
1. Put these includes at the top of the class so that the compiler can find the
parts of JUnit it needs:
import static org.junit.Assert.*;
import org.junit.Test;
2. Create the TaxCalculator class.
3. Create the simpleTax method in the TaxCalculator class. You can tell from
the assertEquals() calls that it has a single parameter (which is a double) and
returns a double.
4. To make the simpleTax() method compile, you’re going to have to make it
return something. The simplest temporary solution is to make it return 0.
We’ll give it a correct calculation later.
Once it compiles, run it to watch it fail (RED). You should see this error message:
java.lang.AssertionError:expected:<500.0> but was:<0.0>
Make the Test Pass
Look at the comment for the test and the assertEquals() calls. The method
simpleTax() in the tax calculator has one parameter that is the income and it
115
returns a double that is the tax owed. Write the code to make the test pass.
(GREEN)
Clean Up
Look at your code and clean up anything that is poorly done. Make sure that your
variables have descriptive names that follow our naming conventions, and that your
javadoc comments (and any other comments that you think add clarity to your code)
are complete. (Refactor)
Border Cases
When we develop unit tests, our goal is to ensure that the code works correctly in every case.
However, it is a rare occasion when we can code for every case. For example, in our tax
computations, “every case” would mean we’d have to have an assertEquals statement for every
possible income. Clearly that isn’t reasonable!
Since we can’t test every case, we try to test the cases that are most likely to have errors. Often
we can catch problems by focusing on the values where the behavior of the code changes. For
example, say we have an integer variable, num, and we want to output “Big” if num is greater
than 10 and “Small” otherwise. In this case, the behavior changes between the values of 10 and
11. In other words, we get one output (one behavior) when num is 10 and another output (another
behavior) when num is 11. Therefore, we would want to test for both num = 10 and num = 11 to
ensure that the code handles the change in behavior correctly. These cases are called border
cases and testing for them is often a good testing strategy.
When you are designing test cases, it’s important to think carefully about the conditions that
define a border. If, in our previous example, the variable num had been a double, one border
value would be 10.0, but what would the other one be? It should be as close to 10.0 as possible
while still causing the second behavior. In many cases, that “close as possible” depends on the
values the double would hold. For example, if num was holding the values read from a scale and
it was accurate only to 0.001 pounds, then 10.001 would be the appropriate second border case.
In the tests of this lab, you’ll see a variety of types of border tests. In each case, pay attention to
how the two sides of the border are determined.
If-then-else Statements
In Lab 1, you saw a simple conditional statement:
if (potatoNumber != 4)
{
System.out.print(" potato");
}
When this statement is executed, the condition (potatoNumber != 4) is evaluated. If it is
true, then the statements inside the curly brackets are executed. If it is false, those statements are
skipped. This is called an “if statement” and is used to make the behavior conditional (dependent
on the values in our variables).
Sometimes, we want to make the choice between two behaviors. In these cases, we use an “ifthen-else” statement like this:
116
if (score > 69)
{
System.out.println("You passed");
}
else
{
System.out.println("You failed");
}
When this is executed, the condition (score > 69) is evaluated. If it is true, the code in the first
curly brackets (called the then block) is executed and the code in the second block (called the
else block) is skipped. If the condition is false, the then block is skipped and the else block is
executed. In every case, exactly one of the two blocks of code will be executed.
Writing Our First Conditional
@Test
public void testTwoLevelTax()
{
TaxCalculator tc = new TaxCalculator();
assertEquals(0, tc.twoLevelTax(29999.99), 0.001);
assertEquals(30000 * 0.05, tc.twoLevelTax(30000), 0.001);
assertEquals(0, tc.twoLevelTax(10000), 0.001);
assertEquals(34567.89 * 0.05, tc.twoLevelTax(34567.89),
0.001);
}
Compiling this only requires that you add the twoLevelTax() method to your
TaxCalculator class. It has one parameter, a double containing the income, and it
returns a double. Like before, have it return 0 to get it to compile. Once it
compiles, run it to watch it fail (RED). You should see this error message:
java.lang.AssertionError:expected:<1500.0> but was:<0.0>
Examine the test to determine what it is expecting to do. Remember, the last
parameter to assertEquals() when it is comparing doubles is the epsilon value. In
this case, the test requires that the answer be correct within 1/10 of a penny.
That seems sufficient since we are dealing with money.
Here is the comment that should be attached to that test to explain what it’s
testing:
/**
* In two level taxes, people who make less than $30,000 pay no
* tax and everyone else pays 5%. Test the border cases where
* the behavior changes and a few other values.
*/
Write the code to make the test pass. (GREEN)
Clean up your code as necessary (REFACTOR).
117
Nested Conditionals
We can put any statements inside the then and else blocks of our conditionals. When we put if
statements inside if statements, they are called nested conditionals. Look at this example:
if (female == true)
{
if (heightInInches > 65)
{
System.out.println("You
} else
{
System.out.println("You
}
} else
{
if (heightInInches > 70)
{
System.out.println("You
} else
{
System.out.println("You
}
}
are tall");
are not tall");
are tall");
are not tall");
This code looks at two variables: a boolean named female and an integer named heightInInches.
If female is true, then the cutoff between “tall” and “not tall” is 65 inches. For males, the cutoff
is 70 inches.
When you are looking at code that contains nested conditionals, the easiest way to understand
what it does is to execute it just like the computer executes it – one statement at a time. As you
read it, you’ll see the values in the conditions. Those are the border cases. Understanding how
the values around the borders behave will help you figure out what the entire section of code is
trying to accomplish.
Let’s look at one more example:
if (grade > 90)
{
System.out.println("Excellent");
} else
{
if (grade > 80)
{
System.out.println("Average");
} else
{
System.out.println("Poor");
}
}
Let’s assume that grade is an integer. The first condition (grade > 90) tells us that there is a
border between the values 90 and 91 because 90 will make the condition false, while 91 will
make the condition true. Looking at the then block tells us what happens for all values greater
than 90 – “Excellent” is the output.
When grade has a value less than or equal to 90, the else block of the first condition will be
executed. Inside that block, we see another if statement with a condition of (grade > 80).
118
This gives us another border condition. Remember that, at this point in the code, we already
know that grade has to be less than or equal to 90 (that’s how we got into the else block of the
first condition). So, when grade is greater than 80 and less than or equal to 90, “Average” will be
the output and when grade is less than or equal to 80, “Poor” will be the output. In summary, this
is how this code will behave:



If grade is greater than 90, “Excellent” will be the output.
If grade is greater than 80 and less than or equal to 90, “Average” will be output.
If grade is less then or equal to 80, “Poor” will be output.
The Dangling Else Problem
It’s important to remember that the compiler does not pay attention to how we indent our code.
Sometimes, this leads to situations where the computer behaves in a different way than we
expect. As an example, consider these nested conditionals:
if (grade > 90)
System.out.println("Excellent");
if (grade > 80)
System.out.println("Average");
else
System.out.println("Poor");
Since this code has no curly brackets, the compiler will think that only one statement is inside the
conditional. That means that only the System.out.println(“Excellent”); is inside
the conditional. Therefore, the correct indentation of this code would be:
if (grade > 90)
System.out.println("Excellent");
if (grade > 80)
System.out.println("Average");
else
System.out.println("Poor");
In other words, these conditionals aren’t nested at all.
Sometimes, we can confuse the situation even further. Consider this code:
if (grade > 80)
System.out.println("Excellent");
if (grade <= 90)
System.out.println("Average");
else
System.out.println("Poor");
If the indentation affected the behavior, it would seem that the else is part of the first if
statement. If that were true, this code would have the same behavior as our original nested if
example. The situation here is often called the dangling else problem because we need to know
with which if statement the else will be paired. The rule is that each else is paired with the
closest unpaired if statement. So, in this case, the else will be paired with the second if statement.
Therefore, the correct indentation of this code is:
119
if (grade > 90)
System.out.println("Excellent");
if (grade <= 90)
System.out.println("Average");
else
System.out.println("Poor");
Again, that does not give the behavior that the indentation implied. So, it is very important that
we do not assume that indentation implies behavior. However, there are two strategies for how to
prevent these problems. First, if you always put curly brackets around the blocks of code
contained by a conditional, none of these problems is likely to occur. Second, when you click
Source->Format in Eclipse, it formats the code and will indent it to match the way the compiler
understands the code. Re-reading your code after that will help you see how the compiler’s
understanding of the code differs from yours and that will help you figure out how to change the
code to get the behavior you want.
More Tax Levels
The next test is for a graduated tax that has three levels.
/**
* In three level taxes, people who make less than $30,000 pay no
* tax and, people making at least $30,000 but not $50,000 pay
* 5%, and everyone else pays 7%.
*/
@Test
public void testThreeLevelTax()
{
TaxCalculator ct = new TaxCalculator();
assertEquals(0, ct.threeLevelTax(29999.99), 0.001);
assertEquals(30000 * 0.05, ct.threeLevelTax(30000), 0.001);
assertEquals(49999.99 * 0.05, ct.threeLevelTax(49999.99),
0.001);
assertEquals(50000 * 0.07, ct.threeLevelTax(50000), 0.001);
}
Add this test to your test suite. To compile it, you need to add the method
threeLevelTax() to your TaxCalculator. You’ll need to figure out its parameters and
return type from the calls made by the tests. When it compiles, watch it fail.
(RED) Then write the code to make it pass. (GREEN). Clean up as appropriate.
(REFACTOR).
Simple Dependent Deduction
The next tax requires giving the taxpayer a deduction for each dependent they
care for. This means that, instead of being taxed on their entire income, we
subtract a certain amount for each dependent from their income, resulting in an
adjusted income that is taxed.
Here’s the test:
120
/**
* This tax gives you a break for every child you have. Your
* adjusted income is $1000 less than your actual income for
* every child you have. Then your taxes are computed with the
* same three ranges as the three range tax.
*/
@Test
public void testSimpleDependents()
{
TaxCalculator ct = new TaxCalculator();
assertEquals(0, ct.simpleDependentTax(32999.99, 3), 0.001);
assertEquals(30000 * 0.05,
ct.simpleDependentTax(34000.00, 4), 0.001);
assertEquals(49999.99 * 0.05,
ct.simpleDependentTax(51999.99, 2), 0.001);
assertEquals(50000 * 0.07,
ct.simpleDependentTax(53000, 3), 0.001);
}
After it compiles, watch it fail. (RED) Then write the code to make it pass.
(GREEN). Clean up as appropriate. (REFACTOR).
Varying Deductions
Here’s the next test.
/**
* In this tax, the deduction for your dependents depends on the
* number of children you have. The first three children result
* in deductions of $1000 each, but if you have four or more
* children, each results in a deduction of $1100. Your tax is
* computed from your adjusted income as follows:
* 0 - 19,999.99: 5%
* 20,000 - and up: 15%
*/
@Test
public void testTwoLevelDeductions()
{
TaxCalculator tc = new TaxCalculator();
assertEquals(19999.99*0.05,
tc.twoLevelDeductions(22999.99,3), 0.001);
assertEquals(19999.99*0.05,
tc.twoLevelDeductions(19999.99+4400,4), 0.001);
assertEquals(20000 * 0.15,
tc.twoLevelDeductions(23000, 3), 0.001);
assertEquals(20000 * 0.15,
tc.twoLevelDeductions(24400, 4), 0.001);
}
Use the comment and the asserts to figure out what the twoLevelDeductions()
method needs to look like and make the test compile. Watch it fail (RED).
Look carefully at that test: why does it need four assertions? We have the income
border and the deductions border (between three and four). Therefore, we want
to get an adjusted income of $19,999.99 with three and four deductions. The same
issue for $20,000 leads us to test four things.
121
Problem Decomposition
Often, when we start to think about the solution to a problem, things seem overwhelming. When
we try to think of the whole problem, it seems that so many things have to be done that we can’t
imagine coding the entire solution. In these cases, separating the problem into smaller more
manageable problems is a good strategy. This is called problem decomposition. A term that
means thinking about parts of the problem, finding solutions to each part, and then putting those
solutions together to get a solution for the entire problem.
You may have used problem decomposition many times without giving it a formal name. When
you approach cleaning a very messy and disorganized room, at first it can seem overwhelming.
Then you come up with a sequence of steps to use:
•
•
•
•
•
Sort the things on the floor and put them in the drawers where they belong,
Re-stack the books on the shelf,
Dust the shelves,
Make the bed,
etc.
As another example, in algebra you used problem decomposition when you were solving word
problems. First, you have to figure out what the unknown is, then you have to figure out which
variables you have values for, then you have to figure out how to combine them to find the value
of the unknown.
In computer science, you can use the same type of approach. When a method seems
overwhelming, write comments that contain the steps your solution will require (without
worrying about how to code them). Then, try to write the code for each step. If a step seems too
difficult, divide it into smaller steps. Problem decomposition is a good strategy for dealing with
problems that seem to complex to solve.
Make Two Level Deductions Pass
Now we need to make the test pass, but things are getting a little complex. Here’s
a clue: decompose it into two steps. First, compute the adjusted income (and store
it in a variable whose type is double). Second, compute the tax on that adjusted
income. (GREEN).
Clean up as necessary (REFACTOR).
First Spouse Tax
Under certain circumstances, married couples can file a joint return. In these
cases, we’ll let them enter their incomes separately, but they will be taxed as one.
This test is for the first of our spouse tax situations:
122
/**
* This tax is based the income of two spouses. They pay 5% of
* the total of their incomes.
*/
@Test
public void testSpouseSimple()
{
TaxCalculator tc = new TaxCalculator();
assertEquals(40000.02 * 0.05,
tc.spouseSimple(20000.01,20000.01), 0.001);
assertEquals(30000 * 0.05,
tc.spouseSimple(20000, 10000), 0.001);
}
Make the test compile and watch it fail. (RED)
Write the code to make it pass. (GREEN)
Clean up as appropriate. (REFACTOR)
Second Spouse Tax
In this case, the way a married couple is taxed depends on the difference between
their incomes:
/**
* This tax is for married couples, and the tax depends on how
* far apart their salaries are. If the salaries are within
* $10,000 of each other, the tax is 5% of the total. If they
* are more than $10,000 apart, the tax is 5% of the larger
* income plus 3% of the smaller one.
*/
@Test
public void testSpouseDistance()
{
TaxCalculator tc = new TaxCalculator();
// The border case here is when the difference between the //
salaries is 10,000 and the value changes at 10,000.01.
// Since
the order of the parameters shouldn't matter, do
// both cases with both parameter orders
assertEquals(40000*0.05,
tc.spouseDistance(25000,15000), 0.001);
assertEquals(40000*0.05,
tc.spouseDistance(15000,25000), 0.001);
assertEquals(30000.01*0.05 + 20000*0.03,
tc.spouseDistance(20000.00, 30000.01), 0.001);
assertEquals(30000.01*0.05 + 20000*0.03,
tc.spouseDistance(30000.01, 20000.00), 0.001);
}
As usual, make it compile and watch it fail. (RED)
Remember to break the problem down into manageable parts and write the code to
make it pass. (GREEN)
Cleanup as appropriate. (REFACTOR)
123
Now Things are Getting Tricky
In this test, we are going back to a single income (no differences for spouses), but
there is a new wrinkle: an alternate minimum tax. This means that, if someone has
too many deductions, they are not allowed to take them, but instead have to pay a
minimum required tax.
/**
* This tax includes an alternate minimum tax for people in the
* top tax bracket. Incomes are adjusted by $1000 for each
* dependent and the tax levels are:
* 0 - 29,999.99: 0%
* 30,000 - 79.999.99: 5%
* 80,000 and up: 10%
* However, if you are in the top tax bracket and your deductions
* are more than 10% of your income, you pay taxes on your entire
* income.
*/
@Test
public void testAlternateMinimumTax()
{
TaxCalculator ct = new TaxCalculator();
assertEquals(0, ct.alternateMinimum(32999.99, 3), 0.001);
assertEquals(30000 * 0.05,
ct.alternateMinimum(34000.00, 4), 0.001);
assertEquals(79999.99 * 0.05,
ct.alternateMinimum(81999.99, 2), 0.001);
assertEquals(80000 * 0.10,
ct.alternateMinimum(83000, 3), 0.001);
// in the top bracket, we also need to test the border of
// the alternate minimum tax
assertEquals((100000 - 10000) *0.10,
ct.alternateMinimum(100000, 10), 0.001);
assertEquals(99999.99 * 0.10,
ct.alternateMinimum(99999.99, 10), 0.001);
}
Compile the test and watch it fail. (RED)
When you write the code, take a clue from the extra border case we need in the
top bracket. That probably means there will be an extra conditional in that
bracket. For decomposing the problem, try thinking about each tax bracket
separately. You can get much of the test to pass before worrying about the
alternate minimum tax. (Remember that JUnit tells you on which line the test
failed, so can make the assertions pass one-by-one if you like – that’d give you a
sense of positive progress!) (GREEN)
Cleanup as appropriate. (REFACTOR)
The Trickiest Tax of Them All
Here’s one last test. Follow the RED, GREEN, REFACTOR mantra on your own and
use problem decomposition to make it manageable:
124
/**
* This is a single income tax with three tax levels:
* 0 - 19,999.99: 0%
* 20,000 - 39.999.99: 5%
* 40,000 and up: 10%
* Everyone gets a $1000 deduction per dependent
* However, it also includes these special instructions:
*
- for the middle tax group, if their mortgage interest is
*
more than 2% of their adjusted income, they can deduct it
*
from that adjusted income
*
- for the top tax bracket, if their total deductions are
*
more than 10% of their income, they pay taxes on their
*
entire income
*
* The parameters to specialTaxLaws are
*
- income
*
- number of dependents
*
- mortgage interest paid
*/
@Test
public void testSpecialTaxLaws()
{
TaxCalculator ct = new TaxCalculator();
// test the border between the first two brackets
assertEquals(0,
ct.specialTaxLaws(22999.99, 3, 0.0), 0.001);
assertEquals(20000 * 0.05,
ct.specialTaxLaws(24000.00, 4, 0.0), 0.001);
// test the border between the top two brackets
assertEquals(39999.99 * 0.05,
ct.specialTaxLaws(41999.99, 2, 0.0), 0.001);
assertEquals(40000 * 0.10,
ct.specialTaxLaws(43000, 3, 0.0), 0.001);
// in the middle bracket, we also need to test the border
// for mortgage interest
assertEquals(30000*0.05,
ct.specialTaxLaws(30000, 0, 30000*0.02), 0.001);
assertEquals((30000 - (30000*0.02 + 0.01)) * 0.05,
ct.specialTaxLaws(30000, 0, 30000*0.02+0.01), 0.001);
// also, do one with dependents to make sure it's comparing
// with the adjusted income
assertEquals((30000 - (30000*0.02 + 0.01)) * 0.05,
ct.specialTaxLaws(31000, 1, 30000*0.02+0.01), 0.001);
// in the top bracket, we also need to test the border of
// the alternate minimum tax
assertEquals((100000 - 10000) *0.10,
ct.specialTaxLaws(100000, 10, 0.0), 0.001);
assertEquals(99999.99 * 0.10,
ct.specialTaxLaws(99999.99, 10, 0.0), 0.001);
}
125
Vocabulary
@param
dangling else problem
nested conditionals
@return
else block
problem decomposition
border case
javadoc
then block
Lab Questions
1.
2.
3.
4.
5.
6.
What is a border case?
How does understanding what the border cases are help you write the code.
Explain problem decomposition.
How do the tests help you with problem decomposition?
Describe the situations where you would choose to use an if-then-else statement.
How does problem decomposition help you write nested conditionals?
Content Questions
1. Explain what assertEquals() does. In particular
a. what are its parameters?
b. how does it affect the test when the condition is true?
c. how does it affect the test when the condition is false?
d. can you have more than one assertEquals() in a test?
2. Write a conditional statement that outputs the smaller value of the two values stored
in integer variables, x and y.
3. Write code that adds 7 to a variable if it is even and subtracts 7 from it if it is odd.
4. Assuming that you have two integer variables, x and y, write code that outputs “yes”
if x is more than three times y.
5. Assuming that you have two integer variables, x and y, write code that outputs “yes”
if x is more than three times y and “no” otherwise.
6. Assuming that you have three integer variables, x, y and z, write code that outputs the
largest value they hold.
7. Write a method that takes an integer parameter x and returns a double containing the
value that is one third of x.
126
Lab 8 – Loops and Strings
Introduction
In most of our labs, you have seen some type of loops. Java gives us a four ways of coding loops,
but we’ll only talk about three in this lab (we’ll get to the other one later). You’ve seen while
loops and for loops. Java also provides do..while loops which are valuable if you need to be
sure that the statements inside the loop happen at least one time. The general form for the
do…while loop is:
do
{
// statements
} while (<condition>);
Essentially, it is a while loop with the condition at the end of the loop instead of the beginning
of the loop. As an example, suppose we wanted to count how many times we generated a random
number until it generated value bigger than 0.9. In this case, we need to generate at least one
random number before we can check its value. That order of events is a clue that a do…while
loop would be appropriate:
double y;
int x=0;
do
{
y = Math.random();
x = x+1;
} while (y <0.9);
There’s one interesting scope observation we need to make here: if we declared y inside the loop
where we assign it a value, it’s scope would only extend between the curly brackets, so y
wouldn’t be visible at the condition at the end of the loop. That’s why we declare it before the
loop.
As we experiment with these coding loops, you’ll see that they are equivalent; any solution that
you can code with one of them, you can code with the other ones. However, there are situations
where one construct will result in a more concise or more readable solution.
Set Up
In this lab, we will practice writing loops that perform various calculations on
Strings. We are going to build a class that “knows” a String. The class “does” a set
of calculations on that String.
As usual, set up a new project in Eclipse. Following TDD, let’s start by building a
test focused on making sure that instances of our class initialize themselves
properly. Create a new JUnit test (remembering to click “Add JUnit to the build
path” at the bottom of the popup dialog) named TestSentenceCounter and make it
look like this:
127
import static org.junit.Assert.*;
import org.junit.Before;
import org.junit.Test;
/**
* Tests that will make us practice writing loops
* @author Merlin
*/
public class TestSentenceCounter
{
private static final String SENTENCE1 =
"This is my sentence.";
private static final String SENTENCE2 =
"These words make another sentence that is longer";
private SentenceCounter sc1;
private SentenceCounter sc2;
/**
* Create two instances we can play with
*/
@Before
public void setup()
{
sc1 = new SentenceCounter(SENTENCE1);
sc2 = new SentenceCounter(SENTENCE2);
}
/**
* Make sure the instance variable is correct
*/
@Test
public void testConstructor()
{
assertEquals(SENTENCE1, sc1.getSentence());
assertEquals(SENTENCE2, sc2.getSentence());
}
}
Notice that we’ve already moved the creation of two instances of the class we are
testing into an @Before method. This is because we’re going to build several tests.
It’s likely they’ll need to be setup this way, so we can predict the duplication we will
have and just build the setup method right away. At this point, our test class has
two variables, sc1 and sc2, that each point to an instance of SentenceCounter
(which we are about to build). Each of those instances contains a different
sentence because we passed a different sentence into the constructor when we
created them. This sounds much more complex than it really is. Here is a memory
diagram of what we have built:
128
When JUnit runs our tests, it will create an instance of our TestSentenceCounter
class. That class has four instance variables: two String constants (SENTENCE1
and SENTENCE2) and two SentenceCounter instances (sc1 and sc2). We are going
to build SentenceCounter and the constructor of the class has a single parameter
that is the sentence that instance will manipulate. Each of the instances our tests
create contains one of the constants we have defined. Perhaps another look at the
code is in order:
129
Our test checks that we can get that sentence back with a “getter” method.
Create the SentenceCounter class and write just enough code to make the tests
compile. Run your test to see it go RED.
Since our test is checking two instances with different sentences, the only way the
test will pass is to build the full solution. You’ll need to
•
•
•
declare an instance variable that holds a String,
make the constructor store its parameter into that instance variable
make the getter return the value of that instance variable.
Fix the code until your test is GREEN.
At this point, the REFACTOR step only requires that we check that we are
following these precepts of good coding style:
•
•
•
•
our variable names are meaningful,
we have followed appropriate naming conventions,
our indentation is consistent and matches our conventions, and
we have complete javadoc comments for all public methods and classes.
When you are refactoring, remember that the code in our tests is as important as
the code in our classes. Always keep yours tests as clean as you keep the rest of
your code.
130
Count-Controlled Loops
While we use loops for many situations, some general loop structures are often used. One of
these is a count-controlled loop. In count-controlled loops, the loop is controlled by a variable
whose value counts with each pass through the loop. We’ve seen a number of counting loops in
the previous labs. For example, remember this loop from the first lab:
potatoNumber = 1;
while (potatoNumber < 8)
{
System.out.print(potatoNumber);
System.out.print(" potato");
System.out.println("");
potatoNumber = potatoNumber + 1;
}
That loop is count-controlled because the value of the variable potatoNumber controls when the
loop ends and potatoNumber increments (counts) with each pass. Before the loop starts to run,
we know that we will make seven passes trough the loop
In the first lab, we coded that loop with a while statement because that syntax is more intuitive
than the for loop syntax. However, count-controlled loops are usually more concisely written
with the for statement as follows:
for (int i=1;i<8;i++)
{
System.out.print(potatoNumber);
System.out.print(" potato");
System.out.println("");
}
In general, for loops are preferred for count-controlled loops because the loop control and count
is in one place, but remember that for loops and while loops can be translated into each other.
There are many situations that use count-controlled loops, but one of the more common type is
the “walk through the array” loop. In these, the variable controlling the loop is an index into the
array and the condition depends on the length of the array:
for (int i=0; i<array.length; i++)
{
// do something with array[i]
}
Things We Can Do With Strings
In some ways, Strings are like arrays: they hold a set of elements that are all characters. Like
arrays, we number the positions of the String from zero, but we don’t use square brackets.
Instead we use the charAt() method in the String class. Look at the String class in the Java API
documentation and find the charAt() method. It takes one parameter, an integer index into the
String, and returns the character in that position of the String. For example, look at this code:
131
String x = "The quick brown fox";
System.out.println("charAt(4) = " + x.charAt(4));
System.out.println("charAt(0) = " + x.charAt(0));
The output from this code is:
charAt(4) = q
charAt(0) = T
It’s important to note that the return type of charAt() is char which means it is returning a single
character. In Java, character constants are enclosed in single quotes. As an example, given our
String x, this condition would evaluate to true:
(x.charAt(4) == ‘q’)
So, we use single quotes around single characters and double quotes around strings (when there
can be more than one character).
Like arrays, Strings can tells us how many characters they hold, but there is one significant
difference. To get the length of the array named myArray, we use “myArray.length”. The
compiler knows the syntax for arrays, but not for Strings. Therefore, the String class has a
length() method that returns the number of characters in the string. When we want the length of a
string named myString, we use “myString.length()”. The parentheses denote that we are calling a
method.
As an example, here is a count-controlled loop that walks through a string named myString:
for (int i=0; i<myString.length(); i++)
{
// do something with the ith position of x
// using myString.charAt(i);
}
Counting Blanks
The first thing our SentenceCounter class will do is to count the number of blank
characters our String contains. We’re going to write a method that returns an
integer containing the number of blanks in the String. Following TDD as usual, let’s
use the test to figure out exactly what we are going to build.
@Test
public void testCountBlanks()
{
assertEquals(3, sc1.countBlanks());
assertEquals(7, sc2.countBlanks());
}
Write just enough code to make this test compile and watch the test go RED.
To make this test pass, we need a counting loop that will “walk through” the String.
At each position, a running count (held in another local variable) will be incremented
if the character in that position is a blank. That running count is the value this
method will return. Write the code to get to GREEN and then REFACTOR.
132
More Details About Unicode
As we learned before, the computer stores everything as a number and Unicode is the mapping of
numbers we store to the characters they represent. Unicode has been carefully designed to let us
program some translations easily. In particular, UTF-8 (8 bit Unicode) came from the ASCII
(American Standard Code for Information Interchange) code, which preceded the development of
the full Unicode standard. Look up ASCII in wikipedia to see the full encoding.
There are some important things to notice about the ASCII code. It sequentially numbers the
lower case letters, the upper case letters, and the digits. That makes it relatively easy to test to see
if a character is in the range if one of those sequences. For example, this condition:
((ch >=
‘A’) && (ch <= ‘Z’))
will be true only if ch contains a capital letter.
Also, the distance between each upper case letter and its corresponding lower case letter is
constant. That allows us to use a simple calculation to convert between cases. For example, if
chUpper holds an upper case letter, then we can find the corresponding lower case letter and
assign it to chLower:
char chLower = (char) (chUpper + ('a' - 'A'));
Look carefully at how that expression works. First, it is subtracting two characters (which will
subtract their ASCII values) and adding the result to chUpper. The result of that addition is an
int, so we have to typecast it back to a char to store it into chLower. Those type conversions
aren’t really doing much at all; char variables hold 16 bit Unicode values and int variables
hold 32 bit integers, but, since our result is never longer than 16 bits, the conversion doesn’t
change the value. Since Java is strongly-typed, it requires that we make an explicit conversion
with the typecasts. Essentially, the typecast is the way we tell the compiler that we know there’s
a chance we could lose information, but, in this case, it’ll be OK.
Counting Lower Case Letters
The next piece of functionality we’re going to build is a method that counts the
number of lower case letters in our String. Here’s the test:
@Test
public void testCountLower()
{
assertEquals(15, sc1.countLowerCase());
assertEquals(40, sc2.countLowerCase());
}
Make this compile and watch the test go RED. As with counting blanks,
countLowerCase() will need to walk though the String. However, this time it must
check to see if the character in the current position is a lower case letter.
Write the code to make this test go GREEN and then REFACTOR as appropriate.
133
Border Cases for Lower Case Letters
Sentinel-Controlled Loops
Another general loop structure is used when the loop will run until it reaches a particular
condition. These loops are called sentinel-controlled loops. In general, we code sentinelcontrolled loops with while loops and they have this general form:
while (<continue condition>)
{
// loop body that may change the value of the
// continue condition
}
For example, this loop will keep running until Math.random() returns a number larger than 0.8:
double i = 0;
while (i < 0.8)
{
i = Math.random();
}
This differs from a count-controlled loop because we do not know how many times the loop will
run. However, it’s important to notice that sentinel-controlled loops may include variables that
count. For example, here is a loop that looks for the first negative number in an integer array:
int i = 0;
while ((i < myArray.length) && myArray[i] >= 0))
{
i++;
}
When that loop completes, one of two things will be true. If myArray contained a negative
number, the variable i will contain the position of the first negative number in the array.
Otherwise, the variable i will contain the length of the array. Even though this loop contains a
variable that counts, it is a sentinel-controlled loop because the condition that terminates it is not
that the counter reaches a particular value; the condition that terminates it depends on the data in
the array.
The Position of the First Blank
We’re going to add another method to our SentenceCounter class with the goal of
returning the position of the first blank in our sentence. This will test this new
method:
134
@Test
public void testFirstBlankPosition()
{
assertEquals(4, sc1.firstBlankPosition());
assertEquals(5, sc2.firstBlankPosition());
}
Look at the definitions of the Strings in the objects sc1 and sc2 to make sure you
understand where the 4 and the 5 come from. Then get to RED by making the test
fail.
In order to get to GREEN, you are going to need a sentinel loop. The loop will still
walk through each position of the array, but, instead of going to the end of the
String, the sentinel value should stop the loop and return the value of the position
where getChar() finds the first blank.
WARNING: Sometimes, when we code a loop, we make a mistake in the code that
modifies the loop variable (or we forget to modify it all together!). For example,
look at this loop:
int x = 0;
while (x < 3)
{
System.out.println("Still working”);
x--;
}
If you execute that code, you’ll see that the condition x<3 will never be false since
x is getting smaller. This is called an infinite loop because it will never stop running.
If you accidentally write an infinite loop, JUnit will appear to stop running in the
middle of the tests; the green bar will stop part of the way to finished (and the fan
on your computer might turn on because it is working very hard). You will have to
kill your tests in order to stop the loop. You can do this by going to the debug
perspective (using the buttons at the top right of the screen). Then, click on the
running task to select it and the red square in the debugger controls to terminate
that process. If you don’t do this, the performance of your computer will degrade
rapidly.
As always, REFACTOR before you go on.
Strings With No Blanks?
Up to this point, in writing firstBlankPosition() we haven’t worried about the
situations that are out of the ordinary, but that could occur. In particular, this
method doesn’t handle the situation where there are no blanks. Not checking for
this possibility would be a classic error. We want to be sure that the method still
behaves nicely and returns some reasonable value. In situations like this, we often
return a value that signifies that something strange has happened. So, let’s make
firstBlankPosition() return -1 if no blank is found. That clearly isn’t a count value
135
that would normally be returned, so it’s a reasonable indication that something
strange has happened. (In a later lab, we’ll learn better ways to deal with this, but,
for now, this strategy will work).
Let’s write a separate test for this behavior, because we’re going to need to use a
different String than the two previous tests have been using:
@Test
public void testFirstBlankPositionNoBlanks()
{
SentenceCounter sc =
new SentenceCounter("NoBlanksInThisSentence");
assertEquals(-1, sc.firstBlankPosition());
}
This should compile, so just run it to get to RED.
In order to fix the error, look at the message JUnit gives us. Click on the name of
the test and you should see this in the frame below the JUnit frame:
java.lang.StringIndexOutOfBoundsException: String index out of range: 22 at
java.lang.String.charAt(String.java:558)
at
SentenceCounter.firstBlankPosition(SentenceCounter.java:115)
at
TestSentenceCounter.testFirstBlankPositionNoBlanks(TestSentenceCounter.java:51)
If you read the messages from bottom to top, these lines give you the sequence of
methods that were running when the test encountered a problem: our test (named
testFirstBlankPositionNoBlanks() ) had called firstBlankPosition() which had called
charAt(). You can double click on any of those lines to have the editor show you
exactly where it is in the code (except that you probably don’t have the source
code for the String class installed). We were within the loop of firstBlankPosition()
when something went wrong as we tried to get a character out of our String.
The top line of this output tries to tell you exactly what went wrong. “Index out of
bounds exception” means that we tried to index something (in this case a String)
with an invalid position. It even tells you that the invalid index was 22 (our string
only contains 22 characters, so its positions are numbered from 0 to 21).
Essentially, this means that our loop kept going beyond the end of our String. As it
is currently written, our loop stops when it encounters a blank. Since our String
doesn’t contain any blanks, the loop just keeps going until it runs out of String to
search.
More Complex Conditions
Until now, most of our loops have depended on a single comparison as the while conditions.
However, often conditions are more complex than that. As we have seen, we can combine
conditions with three boolean operators:
•
•
•
&& is logical “and”
|| is logical “or”
! is logical “not”
136
For example, we can test if the value of a variable is between 1 and 10, inclusive, with this
condition:
((x >= 1) && (x <= 10))
The && operator means that the combined condition is only true if both of the comparisons are
true. That can only happen if x is between 1 and 10, inclusive.
The || operator takes two boolean operands (just like &&) and evaluates to true if either of the
operands is true. For example, if we wanted to check if x is not between 1 and 10, we could use
this condition:
((x < 1) || (x > 10))
If you imagine a number line, there are two segments of the line that are not between 1 and 10.
This condition will be true if x is in either one of those two segments.
For both of these logical operators, it is important to note that java will evaluate the left operand
first. If the value of that operand guarantees the value of the operator, the right operand will not
be evaluated. For example, consider this loop:
int i=0;
while ((i < x.length) && (x[i] < 42))
{
i = i+1;
}
Assume that all of the values in the array x are greater than 42. Then this loop will run to the end
of the array. When i is past the last index of the array, the first operand of the && will be false.
That means that the && can’t evaluate to true, so evaluation of the && stops and it returns false.
Notice that indexing the array is prevented when it would cause a problem. That will make the
loop stop cleanly. However, consider what would happen if the order of the two operands was
switched:
int i=0;
while ((x[i] < 32) && (i < x.length))
{
i = i+1;
}
In this case, when i goes beyond the end of the array, the left operand will attempt to index the
array anyway and the code will crash. In this case, the order of the operands clearly matters.
In some cases, the control of a loop can become quite complex. There are times when you want a
loop to continue until a particular event occurs, but that event occurs in the middle of the loop
body (as opposed to at the beginning or end of the loop). For example, suppose we want to know
if an array has three sequential positions with the same value. If the array is very long, we’d
really like to stop looking as soon as we have found three sequential values that are equal:
137
public boolean sequenceOfThree(int[] x)
{
boolean keepLooking = true;
int i = 0;
while ((keepLooking && (i < x.length-3)))
{
// assume this set of three will be equal
keepLooking = false;
for (int j = 0; j < 2; j++)
{
if (x[i + j] != x[i + j + 1])
{
// they aren't equal,
// so we have to keep looking
keepLooking = true;
}
}
i++;
}
return !keepLooking;
}
In this code, the purpose of the boolean variable keepLooking is to tell the loop whether we
have found a group of three equal values. As long as a group has not been found, we want to
continue looking at the next position of the array by keeping keepLooking set to true. Once a
group of equal values has been found, we want the loop to stop by setting keepLooking to be
false. Spend some time looking at this code until you see how the two loops interact.
Early Exit Conditions
Sometimes when we are coding a loop that has complex exit conditions, it is easier to return a
value from the middle of the loop. For example, here’s a method that searches an array for a
particular value and stops the loop when it sees that value:
public boolean contains(int[] x, int value)
{
boolean found = false;
int i = 0;
while ((i < x.length) && !found)
{
if (x[i] == value)
{
found = true;
}
i++;
}
return found;
}
The variable named found is a boolean that is keeping track of whether we have seen the given
value in the array. The loop continues while there is more of the array to be searched and we
haven’t found the value we are searching for. Once the loop completes (either by getting to the
end of the array or by finding the matching value), we return the value of found since it reflects
whether the value we were looking for is in the array.
That code clearly works, but some people prefer to code it this way:
138
public boolean containsExit(int[] x, int value)
{
for (int i = 0; i<x.length; i++)
{
if (x[i] == value)
return true;
}
return false;
}
This implementation takes advantage of the fact that return statements do not have to go at the
end of a method; you can use them at any point where you know the method has finished. In this
case, as soon as we have found the value we are searching for, we know we can return true. That
return statement will cause the method to stop executing and return control to the place of the
code that called it. If we finish the loop, then we know that the return statement inside the loop
didn’t occur, so we’ve gone all the way through the array without finding our value. Therefore,
we return false.
There are pros and cons to each of these choices. The second implementation is more concise,
but returns in the middle of loops or conditionals can make code harder to read. In the first
implementation, the conditions that control the loop are clear in the while condition. However,
that isn’t true in the second implementation; the loop can stops before the end of the array even
though the for loop looks like our standard “walking through the array” type of loop. If the
code was more complex, using returns in the middle of things can make it very difficult to
understand how the loop really works.
Using return statements isn’t the only way to get a loop to stop in the middle of the loop body. If
you want to stop the loop and jump to the line at the end of the loop, you can use a break
statement. However, these generally degrade the readability of your code, so use break with
great hesitation.
Two Things to Stop the Loop
In order to make our test pass, we are going to have to change the condition on the
loop in firstBlankPosition(). We want the loop to stop if we get beyond the end of
the String or we see a blank. In other words, we want the loop to continue “while”
we are not off the end of the String and the current position doesn’t hold a blank.
Fix your condition and re-run the test. It still won’t pass, but we can get rid of the
index out of bounds exception. Don’t go on until this tests gives an “assertionError”
that it was expecting -1 but was 22. That means that you’ve fixed the fact that
the loop was crashing. That’s progress even if we are still RED.
In order to make the test pass, we’re going to have to figure out how to return -1.
We used to just return the position that made the loop stop, but now what we
return depends on the value of that position. If we ran off the end of the String,
we want to return -1; otherwise, we want to return the position as we did before.
You can compare the position with the length of the string to decide which return
value is appropriate. Fix the code to make the test GREEN (without breaking any
of the other tests) and then REFACTOR.
139
Where’s a Specific Blank?
The next piece of functionality we’re going to build is a method that, given an
integer parameter, blankNum, returns the index of that blank in our String. For
example, if our String is “This string has blanks” and blankNum is 2, the method will
return 11. If blankNum is 1, then this method will return the same value as
firstBlankPosition. Here is the test for this new method:
@Test
public void testBlankPosition()
{
assertEquals(10, sc1.blankPosition(3));
assertEquals(24, sc2.blankPosition(4));
}
Before you can write the code, it’s important that you really understand what
blankPosition() is supposed to do. Be sure that you can explain why the tests are
looking for the values in the assertEquals() calls. Write enough code to make this
compile and watch it go RED.
Clearly, the blankPosition() method is going to require a loop to go through the
String. As you prepare to write this loop, think about these things:
•
•
•
You need one variable to keep track of where you are in the String.
You need another variable to keep track of how many blanks you have seen.
How should you stop the loop?
Once you get to GREEN, don’t forget to REFACTOR.
Off the End of the String Again
This blankPosition() method is walking through a String, so, again, we need to think
about what would happen if it walked off the end of the String. Let’s write a test
so that we can see what is happening now:
@Test
public void testBlankPositionTooFar()
{
assertEquals(-1, sc1.blankPosition(4));
}
In this test, sc1 doesn’t contain four blanks, so we need our method to signal that it
didn’t really find what we were looking for. As before, we’ll make it return -1 as
that signal. Run the test and, if you didn’t think about this as you wrote the code,
it will be RED. (If it isn’t red, you didn’t write the simplest code that would make
our previous test pass!). The changes you need to make are very similar to the ones
you made in firstBlank(). Get to GREEN and REFACTOR as appropriate.
140
String Containment
We’ve been looking for blanks in our String, but now we’re ready to do more general
matching algorithms. To start, let’s build a method that tells us whether the String
contains a given character. Here’s the test:
@Test
public void testContains()
{
assertTrue(sc1.contains('T'));
assertTrue(sc1.contains('y'));
assertFalse(sc1.contains('z'));
assertFalse(sc1.contains('Y'));
}
Write enough code to make it compile and watch it go RED.
For this method, we need a loop that walks through the String. As soon as we see
the character we’re looking for (inside the loop), we can return true. If we finish
the loop, the character we’re looking for isn’t there, so we need to return false.
Write the code to get to GREEN and then REFACTOR.
More Things You Can Do With Strings
When you use a class that comes with Java (or with any of the many other packages you can
include in Java), it’s a good idea to read over the methods that class provides. Go to the Java API
and find the String class. There are a large number of methods built into the String class. You
should read them all, but this section is a summary of a few you might want to use in this lab.
First, you’ll be disappointed to see that String gives you contains() which you just built. If you
had known that, look how simple your contains() method would have been:
public boolean contains2(char c)
{
return sentence.contains(""+c);
}
Look at the API documentation for the contains() method. It tells you that the parameter is a
String, so we had to convert our character to a String. We did that by concatenating it to an
empty String. The practice you had writing loops was worth it, but, in the future, be sure to look
at the API to see if there is something that might help.
Three other methods in String are commonly used. First, the equals() method takes one
parameter and returns true if that parameter contains the same sequence of characters as the
object the method is being called on. For example, look at this code:
String x = "This is one";
String y = "This is One";
System.out.println(x.equals("This is one"));
System.out.println(x.equals(y));
Since y has a capital ‘O’ in “One”, this is the output from that code:
141
true
false
Sometimes, we want to compare Strings, but we don’t care whether the letters are upper or lower
case. In that case, we can use the equalsIgnoreCase() method:
String x = "This is one";
String y = "This is One";
System.out.println(x.equalsIgnoreCase("This is one"));
System.out.println(x.equalsIgnoreCase(y));
resulting in this output:
true
true
Remember, String has the charAt() method to get an individual character out of the string. It also
has a substring() method that allows us to retrieve a portion of the string. There are two versions
of this method. The first takes only one integer parameter: the beginning index of the substring.
It will return the portion of the String from that index to the end. For example,
String x = "This is one";
String z = x.substring(8);
will make z contain the String “one”.
The second version of the substring method takes two integer parameters. The API
documentation calls them the beginning index and the ending index. The beginning index means
the same things as it meant in the first version: the index that will be the beginning of the
substring. The ending index is the index of the position AFTER the last position in the substring.
As an example:
String x = "This is one";
String z = x.substring(2,6);
z contain the String “is i”. It starts at index two and continues to, but does not include, index six.
Strings Contain Strings
Let’s write a method that uses some of those String methods (and has yet another
loop!). Here’s the test:
@Test
public void testCountOccurrences()
{
assertEquals(1, sc1.countOccurrences("ence"));
assertEquals(2, sc1.countOccurrences("en"));
assertEquals(1, sc2.countOccurrences("that"));
assertEquals(0, sc2.countOccurrences("This"));
}
Make this test compile and watch it go RED.
At this point, you should be able to look at this test, and the name of the method,
to figure out what we expect countOccurrences() is to do. Essentially, you need a
loop that looks at each position of the array to see if it starts a substring that
matches the parameter we were given. The easiest way to get to GREEN is to write
that loop and, inside the loop, use the substring() method to pull out the part of the
142
string you want to check and then use the equals() method to see if it matches the
parameter value.
As always, REFACTOR before you move on.
Partial Matching With Regular Expressions
Often, instead of searching for an exact string, we want to see if the string
contains partial patterns. We specify those partial patterns with regular
expressions. Look up regular expressions on wikipedia to see where we are headed.
For this lab, we’re going to build a method that deals with one regular expression
wild card: ‘.’ that can match any character. For example, if the string we’re search
is “Printing a string” and the regular expression we are given is “.ing” it would match
in two places: the “ting” of printing and the “ring” of string.
For each assertEquals in this test, make sure you understand why the test is
expecting those values:
@Test
public void testMatchesWildCard()
{
assertEquals(1, sc1.countWildCardMatches(".e..e..e"));
assertEquals(2, sc2.countWildCardMatches("o..e"));
}
Make it fail to get to RED.
In order to write this method, you’re going to have to combine two loops. Think
about how this functionality is similar to countOccurrences(). Essentially, instead
of using the equals() method, you’re going to need a loop that checks only the
characters that aren’t ‘.’ in the regular expression.
Get to GREEN and REFACTOR and you’re finished!
Vocabulary
ASCII
infinite loop
Unicode
count-controlled loop
sentinel-controlled loop
UTF-8
Lab Questions
1. What do each of these String method do?
a.
charAt(int i)
b.
length()
c. equals(String otherString)
2. What is ASCII and how does it help you decide if a character is upper case?
143
3. What is the difference between a count controlled loop and a sentinel-controlled loop?
Content Questions
1. How does the code differ if you’re getting the length of an array as opposed to the length of a
string?
2. Make a list of all of the String methods we used in this lab. Include a short description of
what each method does.
3. What is the output of the following code:
a. for (int i=0;i<6;i=i+6)
System.out.print(i+ “ ”);
b. String x = “abc123”;
System.out.println(x.charAt(2));
c. for (int i=32;i<36;i=i+1)
{
System.out.print(i+ “ ”);
}
d. String x = “abc123”;
for (int i=0;i<x.length; i++)
{
if ((x.charAt(i)>= ‘a’) && (x.charAt(i) <= ‘z’))
{
System.out.println(“yep”);
}
else
{
System.out.println(“nope”);
}
}
144
e.
for (int i=4;i<7;i++)
{
for (int j=10; j>=8; j=j-1)
{
System.out.println(i + “, “ + j);
}
}
4. Write code that computes the sum of the values stored in an integer array named myArray
(you can assume that the array exists and already has values in it).
5. Write code that computes the average of the values stored in an integer array named myArray
(you can assume that the array exists and already has values in it)
6. Write a loop that outputs the even numbers from 2 to x. (You can assume that x is larger than
2).
7. Write code that counts the number of times a digit (i.e. one of the characters ‘0’,’1’,…,’9’)
occurs in a String named silly. (again, you can assume that the string exists and contains a
non-empty string.
8. Write the while loop that is equivalent to the following for loop:
double sum = 0;
for(int i=0;i<array.length; i++)
{
sum = sum + array[i];
}
9. Write a method that has one parameter, an integer array named x and returns a double that is
the average of the values in that array.
145
Lab 9 – Writing Methods to Sort an Array
Background
As we develop software, often there is more than one way to solve the problem. Software
engineers should compare many options, weigh their pros and cons, and select the best strategy
for a given situation. In this lab, we are going to examine comparisons that can be used to weigh
several solutions to a single problem.
One of the challenges in systems that manage large amounts of data is giving them the ability to
retrieve a specific piece of information quickly. For example, consider a telephone directory
implemented as a large array containing objects that represent people. For each person we store
an object that includes their name, address, and phone number. If the objects are not in any
particular order, then finding a particular person’s information might require looking through the
entire array (if the person is the last one in the array).
The amount of time that it takes our application to respond to a user request is called the response
time, and as we store more information and the size of the array grows, our response time will
increase. At some point, that response time will become intolerable and we’ll need to find a
better strategy.
Often, when we run into a problem (like unacceptable response times), one strategy for finding a
solution is to emulate how we solve a similar problem in real life. When you look up a phone
number in the phone book, you don’t have to look at every name from the beginning of the book.
Since you know the entries are in alphabetical order, you can estimate where to start your search
and can quickly narrow down your search by reading a few names. We could implement a
similar search strategy for our phone book application if the objects were already sorted by name.
In the sorting problem, you are given a randomly ordered array and must rearrange the data into
ascending order. This one of the common challenges in computer science and there are many
standard algorithms for solving the problem. We will study a few of those algorithms in this lab.
Setting Things Up
We’re going to build one class that contains a number of different sorting
algorithms. Start by setting up a project and a test class with this as your first
test:
146
/**
* Test Bubble Sort on a small, randomly ordered array
*/
@Test
public void testBubbleSmall()
{
int[] x = { 5, 3, 8, 1 };
MySorter s = new MySorter();
s.bubbleSort(x);
for (int i = 0; i < x.length - 1; i++)
{
assertTrue("Out of order at " + i, x[i] < x[i + 1]);
}
}
This test creates an array of randomly sorted integers, using an instance of
MySorter (which we’re going to build) to sort the array and then verifies that each
element in the array is smaller than the one to its right. The loop is a “walk through
the array” type of loop, except that it stops one element before the end of the
array. This is because each pass through the loop is comparing one element of the
array to the one in the next position to verify that they are in the proper order.
We have used a new feature of JUnit: each type of assert gives you the option of
including an error message String as the first parameter. If the assert fails, that
message will be displayed in the JUnit frame. Convince yourself that this test is
sufficient for verifying that the array ends up in sorted order.
Build just enough of MySorter to make this compile and watch it go RED.
Bubble Sort
This simplest sorting algorithm to code is called Bubble Sort and is based on a strategy of
comparing neighbors (items that are adjacent in the array) and swapping their positions if they are
not in increasing order. As a start, consider this loop:
for (int inx = 0; inx < x.length - 1; inx++)
{
if (x[inx] > x[inx + 1])
{
int t = x[inx];
x[inx] = x[inx + 1];
x[inx + 1] = t;
}
}
Let’s run that loop on this array:
147
The loop will start with inx equal to zero and the comparison will be between the items in
positions zero and 1. Since three is less than eight, it will not enter the conditional and the loop
will move on to assign inx the value of one. This time, the comparison will be between eight and
five. Since they are out of order, we will enter the condition. Those three assignment statements
swap the values in positions inx and inx +1 resulting in this array:
The loop then assigns inx the value two and compares the eight to the one. Again, they are out of
order, so they get swapped:
The loop then assigns inx the value of three and, since that is equal to the length of the array
minus one, the loop stops.
The following table summarizes what happens in each pass through the loop:
inx
Compares values
Makes Swap?
Resulting order
0
3, 8
no
3, 8, 5, 1
1
8, 5
yes
3, 5, 8, 1
2
8, 1
yes
3, 5, 1, 8
The net effect of this loop is that the largest value has been moved to the last position of the array.
That is an important part of getting the array into sorted order.
Let’s run the loop a second time on this array (leaving the order we had at the end of the first time
we ran the loop). Here’s a table that summarizes what happens:
148
inx
Compares values
Makes Swap?
Resulting order
0
3, 5
no
3, 5, 1, 8
1
5, 1
yes
3, 1, 5, 8
2
5, 8
no
3, 1, 5, 8
At the end of the second time though the loop, the second largest value has been moved into the
correct position of the array. From this, prove to yourself that, if we ran through the loop once for
each position in the array, the entire array would be sorted. This strategy is called Bubble Sort,
because each time we run the loop, one element “bubbles” up to its correct position in the array.
Building BubbleSort
To get our current test to pass, we need to build a bubbleSort() method. It will
contain the loop we just studied, but we need that loop to happen once for each
position in the array, so we will nest it inside another “walking through the array”
loop:
public void bubbleSort(int[] x)
{
for (int i = 0; i < x.length - 1; i++)
{
for (int j = 0; j < x.length - 1; j++)
{
if (x[j] > x[j + 1])
{
int t = x[j];
x[j] = x[j + 1];
x[j + 1] = t;
}
}
}
}
That should make your test be GREEN. However, that is really ugly code! If you
didn’t know what that code was trying to do, it wouldn’t be easy to figure it out.
Let’s REFACTOR to clean things up a bit. First, the three assignment statements in
the conditional are swapping the values in two positions of the array. Let’s move
them into a method with that name to make our code more readable:
149
/**
* Sort the given array
* @param array the array to be sorted
*/
public void bubbleSort(int[] array)
{
for (int i = 0; i < array.length - 1; i++)
{
for (int j = 0; j < array.length - 1; j++)
{
if (array[j] > array[j + 1])
{
swap(array, j, j+1);
}
}
}
}
/**
* swap the values in two positions of an array
* @param x the array
* @parami the first position to be swapped
* @paramj the second position to be swapped
*/
private void swap(int[] x, int i, int j)
{
int t = x[i];
x[i] = x[j];
x[j] = t;
}
Now our bubbleSort() method is much more readable. Make sure that your tests
are still GREEN.
It’s important to notice what we just did: we created a method whose primary
purpose was to make our code more readable. It wasn’t one of the things we
originally thought of as something our class “does” and it isn’t something we expect
anyone else to call (that’s why we labeled it as private). Sometimes, we create a
method solely for the purpose of making our code more readable. In these cases,
they don’t require that we write any tests because our Bubble Sort test will
essentially test it for us (if swap() didn’t work, bubbleSort() wouldn’t work either).
This code also reminds us of a two topics that we have covered before. The first
topic for review is variable scope. Notice that both our bubbleSort() and our
swap() methods declare integer variables named i and j. Even though they have
the same names, they are local variables defined in different blocks, so they are
completely different variables with their own values.
The second topic for review is primitive versus reference variables. Notice we are
passing an array from bubbleSort() into swap() and expecting to see the changes
that are made to it when swap() returns. This works because arrays are reference
150
types. When we pass a reference variable into a method, we are really passing the
pointer into the method, so, in this case, array in bubbleSort() and x in swap() will
be pointing to the same array:
However, when we pass primitive types into arrays, the value of the variable gets
copied into the parameter variable. Changes to the parameter value will not affect
the variable passed into the parameter. This is exactly the same behavior we saw
when we compared primitive and reference variables in assignment statements (you
might want to review that section from Lab 4), so it is often useful to think of
passing parameters as if we are making an assignment from the variable being
passed to the parameter variable.
Estimating Run-Time
We are interested in comparing algorithms to see how much time is required to sort the array as
the data gets larger (holds more values). If the array is small, no sorting algorithm will take very
long, but as the array gets larger, some algorithms will run faster than others.
One way to compare algorithms is to build them and then run them on large data sets and
compare the amount of time each takes to sort the data. This is called benchmarking and is
useful when you have implementations of each of the algorithms you want to compare. However,
one weakness of benchmarking is that it depends on the machine on which you run the
algorithms.
We often want to make a machine-independent comparison of algorithms. For that, we use runtime analysis in which we estimate the run-time as a function of the size of the input. This type
of analysis does not require an implementation of the algorithm.
In run-time analysis, we figure out which part of the algorithm executes most often and then
count (roughly) how many times it executes as a function of the size of the input. In most sorting
algorithm, the operation that is most performed is often the comparison of two elements in the
array. Let’s think about how many times that comparison happens in Bubble Sort if there are n
elements in the array. First, just examine about the inner loop (ignoring the outer loop
completely). It is a “walking through the array” loop that stops one position before the end of the
array, so we will enter it n-1 times. Since the comparison happens each time we enter the loop,
that means that the inner loop alone uses n-1 comparisons. Using the same logic, we realize that
the outer loop runs the inner loop n-1 times, so the algorithm uses a total of (n-1)*( n-1)
comparisons. This is our initial estimate of run-time.
As n grows large, subtracting one from n doesn’t have much effect, so we generally ignore the -1
and say the run-time of Bubble Sort is (n)*( n), or O(n 2). That is read as “Big-Oh” of n 2 and
means “the run-time is no worse than n squared.”
151
Selection Sort
Another strategy for sorting an array is Selection Sort, and can be summarized by;
•
•
•
Find the biggest element and swap it with the last position in the array
Find the second biggest element and swap it with the second to last position in the array
Keep doing that until the array is sorted
To see how this will work, let’s start by following that strategy to sort this array:
The largest element of the array is the eight, so the first swap we would do is to swap the eight
with the three (since its in the last position of the array).
The second largest element of the array is the five, so the second swap would swap it with itself.
Notice that you are smart enough to know that no swap is necessary, but the algorithm we
specified would make that swap anyway.
The third largest element of the array is the three and we would swap it with the two:
At this point, we can see that the array is sorted. However, the algorithm has to make one more
pass because it only knows that the values from three to eight are sorted. It will find that two is
the fourth largest element and swap it with itself. Once all of the elements except the smallest
element have been swapped into their positions, the algorithm is finished.
Building Selection Sort (phase 1)
Selection Sort isn’t really difficult to understand, but writing the code isn’t trivial.
When you are trying to implement an algorithm and it seems too complex to code,
one strategy to is build methods that contain parts of the algorithm and then put
them together once you’re sure each works. This is another example of the
strategy called problem decomposition that we covered in Lab 6.
If we look at the description of Selection Sort, one of the things that makes it a
challenge to implement is that the steps aren’t exactly the same: finding the
second largest value is different from finding the largest value. Let’s re-structure
152
the wording of the description of Selection Sort to make the steps more similar.
That will help us figure out what pieces we need to build.
In each step of Selection Sort, the largest unsorted value gets put into its correct
position, so we can think of the array as being in two sections: the lower section is
unsorted and the upper section is sorted. With this understanding, we could reword the description of Selection Sort this way:
•
•
•
At the beginning, the unsorted section of the array is the entire array
Do
o In the unsorted section of the array, find the largest value and swap
it with the element in the last position of the unsorted section.
o Reduce the unsorted section of the array by 1 (the sorted portion
increases by 1)
Continue this while the unsorted section is longer than one element
Notice that our description has changed, but it describes the SAME algorithm –
the way the data is moved is exactly the same for both descriptions. Sometimes,
when things seem too complex to be built, that complexity may be a result of how
we describing our algorithm. It is often useful to look for alternative ways of
describing the same algorithm. In this case, we made things that seemed different
into the same thing. The concept of the unsorted and sorted sections of the array
gave us that simplification.
Even with our new description, the algorithm is still pretty complex to build.
However, the algorithm contains two smaller operations that will help us get
started: swap (which we already have) and “find the largest value in the unsorted
section of the array.” Let’s build a test for that operation:
/**
* Test maxPosition for an element in the middle of the
* unsorted portion
*/
@Test
public void testMaxPositionMiddle()
{
int[] x = {1, 5, 3, 7, 8, 9};
MySorter s = new MySorter();
assertEquals(1, s.maxPosition(x, 2));
}
Look carefully at these things in this test:
•
The array is partially sorted: the first three values are in the unsorted
portion of the array and the last three are in the sorted portion
153
•
We are building a new method that returns the position of the largest
element in the unsorted portion of the array. The second parameter to that
method specifies the largest index that is contained in the unsorted portion
of the array. In other words, the second parameter tells us where the
unsorted portion of the array ends.
Write enough code to make this compile and get to RED.
Implementing maxPosition() is easiest if you use this strategy: walk through the
unsorted portion of the array keeping track of two things: the biggest value you’ve
seen so far and the position in which you saw that value. When you get to the end
of the unsorted portion of the array, return the position in which you saw the
biggest value.
Get to GREEN and REFACTOR as necessary
More Tests
In order to be sure that maxPosition() works, we need to test some border cases.
These tests should compile right away and may go GREEN right away. Add them to
your test suite one at a time and get each to GREEN and REFACTOR.
/**
* Test maxPosition for an element in the middle of the
* unsorted portion
*/
@Test
public void testMaxPositionFirst()
{
int[] x = {5, 1, 3, 2, 8, 9};
MySorter s = new MySorter();
assertEquals(0, s.maxPosition(x, 3));
}
/**
* Test maxPosition for an element in the middle of the
* unsorted portion
*/
@Test
public void testMaxPositionLast()
{
int[] x = {1, 5, 3, 7, 8, 9};
MySorter s = new MySorter();
assertEquals(3, s.maxPosition(x, 3));
}
154
/**
* maxPosition must work correctly even when there is only one
* element in the portion of the array it is looking at
*/
@Test
public void testMaxPositionOneElement()
{
int[] x = {1, 5, 3, 7, 8, 9};
MySorter s = new MySorter();
assertEquals(0, s.maxPosition(x, 0));
}
/**
* Make sure that we can find the biggest thing even if it's
* negative
*/
@Test
public void testMaxPositionNegative()
{
int[] x = {-7, -6, -8,-3, -1};
MySorter s = new MySorter();
assertEquals(1, s.maxPosition(x, 2));
}
For the last test, you might want to think about this: when you start, the value in
the zeroth position is the biggest value you’ve seen so far.
Building Selection Sort (phase 2)
Now that we have built a couple of smaller parts of our algorithm, it’s a lot easier to
build the algorithm we’ve been intending to build. Notice that we now have three
reasons to build a method:
•
•
•
Because it is part of what our class “does,”
Because it helps the readability of our code, and
Because building a complex algorithm is easier if we do it in pieces.
So, now that we have maxPosition() and swap(), let’s start piecing them together.
Here’s a test for Selection Sort:
155
/**
* Test Selection Sort on a small, randomly ordered array
*/
@Test
public void testSelectionSmall()
{
int[] x = { 5, 3, 8, 1 };
MySorter s = new MySorter();
s.selectionSort(x);
for (int i = 0; i < x.length - 1; i++)
{
assertTrue("Out of order at " + i, x[i] < x[i + 1]);
}
}
This is the same test as the first test we ran on Bubble Sort; it’s just calling
selectionSort() instead of bubbleSort(). Build enough code to make this compile and
go RED.
Look at the outline of the code that we gave in the section titled “Building Selection
Sort (phase 1).” You need a variable that keeps track of the last position of the
unsorted portion of the array. It will start at one less than the array length and
get smaller by one each time you go through the loop. Use that, maxPosition(), and
swap() to build Selection Sort. That will get you to GREEN and then you can
REFACTOR.
Run-Time Analysis of Selection Sort
Run-time analysis for Selection Sort is very much like the analysis we did for Bubble Sort. The
operation that happens most often is the comparison of the elements in the array, but this time that
happens in maxPosition(). Each time we call maxPosition(), it compares the biggest value it has
seen to every position in the unsorted portion of the array. Depending on how you coded it, there
is a chance that it doesn’t compare anything to the first element of the array. If that’s true, the
number of comparisons is one less than the number of positions in the unsorted portion of the
array, but that one will be small relative to the size of a large array, so we can just ignore it. So, if
we have n elements in our array, the first time we call maxPosition(), it will make (roughly) n
comparisons (the entire array is unsorted). The second time we call maxPosition(), it will make
n-1 comparisons, and this will continue until it makes only one comparison. Therefore, the
number of comparisons is:
Imagine the value of this when n is very large. The n2 will be much larger than any other part of
that function and, if we increase a large n by 1, that n2 will dominate how much the run-time
changes. Using our Big-Oh notation, we can say that the run-time of Selection Sort is O(n2). Just
like Bubble Sort, the run-time of Selection Sort is no worse than n2.
156
It is important to understand what this means. Theoretically, as the size of the data increases, the
performance of Bubble Sort and Selection Sort get worse at about the same rate. Depending on
specific details of your machine and your data, one might have a slightly better benchmark time,
but both will degrade in a similar fashion.
So, at this point, the only reason to pick one of these algorithms over the other is if one is easier
to develop or understand.
Insertion Sort
Insertion Sort is another sorting algorithm that uses the concept that part of the array is sorted and
part is not sorted. For Insertion Sort, the left (lower) part of the array is the sorted part. The basic
strategy behind Insertion Sort is:
•
•
•
When we start, the first element is the sorted part of the array and the rest is the unsorted
part.
Do
o Insert the first element from the unsorted portion of the array into its correct
position in the sorted part of the array by swapping it with its left neighbor until
its left neighbor is smaller than it is or it is at the beginning of the array.
o This increases the sorted portion of the array by 1 (the unsorted portion is
reduced by 1)
Continue this while there are still elements in the unsorted portion of the array
Let’s do this sort on this array:
When the algorithm starts, the first position (the eight) is the sorted portion of the array, and the
rest is unsorted. The first element that will be moved into position is the three. Since it is smaller
than eight, their positions will be swapped (in these diagrams, the red element is the one the
algorithm is trying to position and the dotted line shows the distinction between the sorted and
unsorted portions of the array):
Since the three has been moved as far as it can, it is in position and the sorted part of the array has
grown by one element.
157
The algorithm will now put the nine into position. It compares the nine with the element to its
left and, since it is larger, it knows the nine is in the correct position to extend the sorted part of
the array. Notice that, this time, it didn’t have to swap anything before extending the sorted part
of the array.
Now the algorithm will move the one into position. It starts by comparing it with its left hand
neighbor. Since nine is larger than one, it swaps their positions.
At this point, the sorted portion of the array looks unsorted, but that will be corrected once we
finish putting the one into position. Again, the algorithm will compare the one with its left
neighbor and since eight is larger than one, it will swap them.
The three is still larger than the one, so we will swap it one more time:
The one has been moved into its correct position, since there are no more values to the left. The
sorted portion of the array has grown by one element.
158
Finally, Insertion Sort has to move the six into position. Since the nine is larger than the six, they
will be swapped.
Since the eight is also larger than the six, they too will be swapped.
Now, the six will be compared with the three and, since the three is smaller, we know the six is in
the correct position and the sorted portion of the array has grown by one element.
The unsorted part of the array is empty, so the array is sorted.
Building Insertion Sort
The test for Insertion Sort will look just like the tests for Bubble and Selection
Sort:
/**
* Test insertion sort on a small, randomly ordered array
*/
@Test
public void testInsertionSmall()
{
int[] x = { 5, 3, 8, 1 };
MySorter s = new MySorter();
s.insertionSort(x);
for (int i = 0; i < x.length - 1; i++)
{
assertTrue("Out of order at " + i, x[i] < x[i + 1]);
}
}
Make this compile and get to RED.
159
To build Insertion Sort, use a counting loop to remember where the end of the
sorted portion of the array is (since that goes up by one with each element we
position). Inside that loop, use a while loop to move the correct value into position.
Remember, you want the loop to keep moving things while both of these conditions
are true:
1. The value we are position hasn’t gotten to the beginning of the array and
2. The value to the left of the one we are positioning is larger than the value
we are positioning.
Get to GREEN and REFACTOR.
Run-Time Analysis of Insertion Sort
The run-time analysis for Insertion Sort is a little different than the others we have seen in this lab
because it uses a sentinel-controlled loop for the inner loop. In both of the previous algorithms,
all of the loops were count-controlled loops, so we knew exactly how many times we went
through them. This time, the inner loop runs until the value is correctly positioned. In our
example, the one moved all the way from its initial position to the beginning of the array, while
the six only moved to the left two positions. This complicates our analysis slightly.
Let’s assume that, on average, each element moves half way toward the beginning of the array.
This will be true if the data is truly randomly order and the array is big enough. The number of
comparisons the algorithm makes when moving one value is, roughly, the number of positions it
has to move (it may make one extra comparison if the element doesn’t move all of the way to the
beginning, but we can ignore this one because n will be much larger than 1). This table
summarizes how we’ll calculate the number of comparisons being made:
160
Original position
of value being
placed
Maximum
number of
positions it can
be moved
Average number
of positions it
will be moved
2
1
½
3
2
1
4
3
1½
n-1
½(n-1)
…
n
The arithmetic is similar to what we did for Selection Sort:
Just like in Selection Sort, this means that, in the average case, the run-time of Selection Sort is
O(n2). Initially, this really doesn’t seem to make much sense. Both Selection Sort and Insertion
contain two nested loops, but, in Insertion Sort, the inner loop runs, on average, through half as
many passes as the inner loop of the Selection Sort. Why should they have the same theoretical
run-time? Remember: we wanted this analysis to be independent of the machine running the
algorithm because machines are constantly changing. For example, if Selection Sort takes about
twice as many comparisons, we can make its actual run-time match Insertion Sort just by making
a machine that performs twice as quickly. While that sounds like a great leap, processor speeds
have historically doubled every two years, so that isn’t really that big a difference.
It is important to remember two things about theoretical run times. First, they are only making
comparisons when the size of the input is very large. We dropped constants and lower order
terms in the counts because, as the size of the input grows, only the highest order terms really
affect the end result. If your input isn’t very large, then you should pick the algorithm that is
easiest to code and understand because working hard to implement a complex, but efficient,
algorithm isn’t going to change your run-time noticeably. Second, this type of analysis is only a
theoretical comparison. Actual run-time is affected by many characteristics of the language we
use to build the algorithm and the machine on which it will run. Theoretical run-time is a good
comparison, but your actual run-times may vary!
161
Best Case Analysis
It really seems like Insertion Sort should be able to give us a run-time improvement over
Selection and Bubble Sort. After all, the fact that the inner loop is a sentinel-controlled loop
means that, under some circumstances, it won’t go all the way through the array. That seems like
it should be better than the count-controlled loops in the other algorithms.
Let’s think about what situation would cause that sentinel-controlled loop to stop as soon as
possible. The loop stopped on two conditions: if we hit the bottom of the array, or if the element
to the left of the one we were moving was smaller than the one we’re moving. Think about what
happens if we try to sort an array that is already in sorted order; then the inner loop will only
make one comparison and stop (because the element to the left is smaller than the one we are
moving). Therefore, the run-time will be only O(n) comparisons – roughly one for each position
in the array. This is an example of best case analysis where we look at the run-time under
conditions that make the algorithm run as quickly as possible. So, while in the average case, when
the data is in random order, the run-time of Insertion Sort is O(n2), if the data is already in order,
the run-time goes down to O(n). Now, realistically, no one runs a sorting algorithm on data that
is already sorted, but, you were working with data that was close to being sorted (maybe it had
been sorted recently, but the values of several entries had been changed), then Insertion Sort
would run significantly faster.
While best case analysis makes a difference for Insertion Sort, there is no “best” case for either
Bubble or Selection Sort. For both of those algorithms, those count-controlled loops are going to
run the same number of times regardless of how the data is ordered. Now we have a run-time
difference that could change which algorithm we’d like to use: if our data is close to sorted, we
would pick Insertion Sort because its best case run-time is better than the other algorithms’.
Vocabulary
Average case analysis
Bubble Sort
Selection Sort
Benchmarking
Insertion Sort
Sorting problem
Best case analysis
Response time
Big-Oh
Run-time analysis
Lab Questions
1. Explain why each of the for loops in Bubble Sort stop one index before the end of the
array. Hint: the reasons aren’t the same.
2. In the second description of Selection Sort (with the Do-While loop), why was it OK that
we stopped when the unsorted section still had one element in it?
3. Explain the four border cases we built for testing maxPosition().
4. Give three reasons why you might create a method
Content Questions
1. Describe how each of the sorting algorithms work.
2. Run each of the sorting algorithms on this data. Show exactly how the data gets
rearranged into sorted order.
a. 3, 8, 5, 1
162
b. 5, 8, 10, 12, 1, 2
c. 1, 4, 7, 12, 5
3. We have assumed that the data elements in our arrays are unique (no number is in the
array twice). Would any of our algorithms fail to sort the data if the elements were not
unique? Justify your answer.
4. Given an original array that contains two elements with the same value, a sort is called
“stable” if the matching elements end up in the same order within the sorted array.
Which of the algorithms we have studied is stable? Justify your answer.
5. What is the difference between average case run-time analysis and best-case run-time
analysis?
163
Lab 10 – iTunes Songs
Introduction
At this point, we have examined most of the constructs that are basic to Java. We have also
studied control structures in detail. With that background, we are now ready to begin to build
systems of classes that work together to accomplish a job. As we progress through building these
systems, we will see not only more details about the way Java works; we will also see the process
computer scientists use to think about a problem and build an object oriented solution.
For this lab and the next, we’re going to build a pair of classes that store some of the information
required by music players like iTunes. In particular, we will build a Song class that will hold
information about a single song and an Album class that will hold a number of songs.
Before we can start building objects, we need some idea of where we are headed. Often,
computer scientists use diagrams to visualize the components in a system and there are a variety
of these types of diagrams. In object oriented designs, Unified Modeling Language (UML) is a
set of standard types of diagrams that can be used to visualize different aspects of our systems.
The most commonly use UML model is a class diagram that is used to show the structural
relationships between the classes of our system. As an example, here is a class diagram for the
systems we are about to build:
Here are some facts about class diagrams that may help you understand the details:
•
•
•
Each of the large boxes is a class and those boxes are divided into three sections:
o The top section contains the name of the class
o The middle section contains the instance variables in the class
o The bottom section contains the methods in the classs
For each instance variable and method:
o It is preceded by plus if the component is public and minus if it is private
o The rest of each line has the name of the variable or method, a colon, and its type
(return type for the method)
o Constructors are methods that have the same name as the class
The line between classes shows that there is a relationship between these classes
o The diamond means “contains” and is on the side of the line of the class that
contains the other class
 In this case, it means that Album “contains” Songs
o The quantifiers on the line show the multiplicity of the relationship
 In this case, one album and contain n (any number of) songs.
164
Getting Started
We’re going to start by building the Song class, so create a project in Eclipse and
create a class named TestSong to hold the tests for the Song class. Look at the
class diagram; the Song class has one constructor that has two parameters: the
name of the song and its duration in seconds. Here is a test that verifies that the
constructor works:
/**
* The constructor of song only needs to store the title and the
* duration in instance variables.
* Let's make sure it works by getting those values from the
* instance we created.
*/
@Test
public void testCreation()
{
Song s1 = new Song("Wreck of the Day", 246);
Song s2 = new Song("Weathered",340);
assertEquals(246,s1.getDuration());
assertEquals(340,s2.getDuration());
assertEquals("Wreck of the Day", s1.getTitle());
assertEquals("Weathered",s2.getTitle());
}
RED, GREEN, REFACTOR
Song toString()
For that first test, we had to build almost the entire Song class. Look back at the
class diagram and you’ll see that all we have left to build is the toString() method.
We want the method to build a human-friendly description of the song, so it should
build a String with the title and the duration in “mm:ss” form. Here’s a test to
specify the details:
/**
* Test that our songs can describe themselves accurately
*/
@Test
public void testtoString()
{
Song s2 = new Song("Weathered",340);
assertEquals("Weathered: 5:40",s2.toString());
}
This should compile without any changes. Watch it go RED.
Your toString() method must build the String from your instance variables.
Remember that the mod operator (%) returns the remainder after division.
Get to GREEN and REFACTOR.
165
Song toString() again
We’ve missed one small detail in our toString() method: if the number of seconds
we put into our string is less than ten, it won’t have a leading zero. For example, if
the duration is 121 seconds, the time in our String will be “2:1” instead of the more
recognizable “2:01”. To fix that, we start with a test that demonstrates the
problem:
/**
* If the number of seconds is less than ten, make sure we
* get the leading zero
*/
@Test
public void testtoString2()
{
Song s1 = new Song("Wreck of the Day", 246);
assertEquals("Wreck of the Day: 4:06",s1.toString());
}
As before, this should compile and go RED without any work. In order to get to
GREEN, you’ll need a conditional that only adds the leading zero when you need it.
As always, REFACTOR.
Arrays of Objects
We have used arrays that held primitive values a number of times, but arrays can also hold
objects. As an example, in lab three, our Deck class held an array of Card objects. Let’s look
more carefully at how such arrays are created. Remember that it takes two steps to create all
arrays: declare the array and allocate the space it requires. For example, here is the code for those
two steps for our array of Card objects:
Card [] cards;
cards = new Card[NUMBER_OF_CARDS];
This memory diagram reflects the state of our cards:
Notice that we have allocated the 52 positions in the array, but, since the compiler doesn’t know
how much space a Card requires, only enough space for a pointer has been allocated for each
space in the array. In order to store a Card in that array, we have to create the instance of card
and then store it in the array. For example, this code will put the Ace of Spaces in the first
position of the array:
cards[0] = new Card(0, 0);
166
That statement changes our memory diagram to look like this:
So, when we have an array of objects, creating the array requires the same two initial steps:
declaring the array and allocating its space. However, it also requires that we create the objects
that are stored in each position of the array.
Look back at the code for lab three and find these three things for the cards array:
•
•
•
Its declaration,
The allocation of its space, and
Where each of the 52 instances of Card are created and stored in the array.
Creating Albums
Since our Song class is complete, we’re ready to start building the Album class.
Before we start, let’s make sure we have a clear understanding of how this class is
supposed to work. To start, look back at the class diagram and find the
specification of the constructor. In the Album box, this is what you are looking
for:
+Album(String title, int size)
We can tell that this is referring to the constructor because it is a method whose
name matches the name of the class and it has no return type. The ‘+’ tells us that
the constructor is public and the rest of the line tells us that the constructor
takes two parameters: a String that is the album’s title and an integer that
specifies the number of Songs the album can hold.
The class diagram also shows that Album has a method named addSong():
+addSong(Song s): void
167
This method has one parameter that is a Song and puts that song into the album.
The rest of the methods are getters and the toString() method which will have
their normal behaviors:
•
•
•
•
•
getDuration() will return the album’s playing time in seconds
getNumberOfSongs() will return the number of songs in the album
getSize() will return the number of songs the album can hold
getSong(i) will return the ith song in the album
getTitle() will return the title of the album
toString() will return a String that describes the album and the songs it
contains.
Now we are ready to start writing a test. Create a class named TestAlbum to hold
the tests for our Album class. As usual, we’re going to start by creating the
constructor and enough getters to be able to verify that the constructor initializes
the objects correctly.
First, declare the method that will hold the test:
@Test
public void testConstructor()
{
}
When we create an album, we only need to check that the title and that the album
contains no songs. Here is a line-by-line description of the test:
1. Create an instance of an album whose title is "Heavier Things" which can
contain up to 3 songs
2. check the title of the album
3. check that the album can hold 3 songs
4. check that the album currently contains no songs
5. Create another instance of an album whose title is "A Night at the Opera"
which can contain up to 5 songs
6. check the title of the album
7. check that the album can hold 5 songs
8. check that the album currently contains no songs
Once the test is written, build enough of the Album class to make it compile. Run
the test and watch it fail. You’ve made it to RED.
Look at the error message the test gave you. It should be something like this:
168
junit.framework.ComparisonFailure: expected:<Heavier Things> but
was:<null>
This is because we aren't storing the title or returning in the getter (getTitle). Fix
those two problems and re-run the test. We’ve made some progress; now the error
message is:
java.lang.AssertionError: expected:<3> but was:<0>
Click on the line below the error, it will show you that the assertEquals() comparing
the value of getSize() is the assert that is failing. Again, this is because we haven’t
stored the size, that we were given as a parameter, to the constructor. Create an
instance variable named size to hold it and make getSize() return that value. Re-run
the test.
SURPRISE – it’s GREEN! We know that we haven't really built everything the
constructor needs (we haven't declared or allocated the array that will hold the
songs) and we didn't write the correct code for getNumberOfSongs() (it always
returns zero which won't always work!). We have tested the parts of the
constructor that are externally visible. We'll add the rest of the code when a test
needs it.
There’s one other important thing to notice at this point: we have declared an
instance variable that is not shown in our class diagram. We need it for now, but
when we create the array of songs, we’ll be able to use songs.length and
eliminate that variable. You’ll see that in a REFACTOR step later in this lab.
Make sure that you look at your code and REFACTOR as appropriate before you
move on.
Test Adding One Song
The next simplest thing we can do is to add one song to an album and make sure
that everything is OK. Let's write a test named testOneSong for that. Here's the
description of the test:
1. Create an instance of an album whose title is "Heavier Things" which can
contain up to 3 songs.
2. Create an instance of a song whose title is "Clarity" and whose duration is
4:32.
3. Add the song to the album.
4. Check that the album contains one song.
5. Check that the zeroth song in the album matches the instance we created in
step 2.
Write just enough code to make this compile and run the test to get to RED.
169
We’re going to let the error messages tell us what code to write next. For each
error we see, we’ll write the simplest code that will fix that problem without
breaking anything that previously worked.
The first error we see is:
java.lang.AssertionError: expected:<1> but was:<0>
and it occurs on the first assertEquals() call in this test. The problem is that we
aren’t keeping track of the number of songs in the album. To fix this, declare an
instance variable that will hold the number of songs in the album (look at the class
diagram to see what it’s name should be). It should start at zero and be
incremented in addSong(), because that is where the number of songs in the album
changes.
Re-run the tests and you’ll see this error:
java.lang.AssertionError: expected:<Clarity: 4:32> but was:<null>
Look at that! The error message from JUnit is using the toString() method in Song
to describe the song it was expecting to see. However, we’re returning null from
getSong() and that’s why the test is still failing. We’re finally at a place where it
makes sense to create our array of songs. Getting past this error will require these
pieces of code
•
•
•
We need to declare the array of songs and allocate its space in the
constructor. Remember that the size of the array is equal to the number of
songs we want the album to hold and that number is passed into the size
parameter.
We need addSong() to put the song it is passed into the zeroth position of
the array (later we'll have to make this smarter, but the zeroth position is
good enough for now).
The getSong() method should return the ith position of the songs array.
That should get you to GREEN. Don’t forget to REFACTOR. As a reminder, you
should think about these things in the REFACTOR step:
•
•
•
•
•
Our code has no duplication.
Our variable names are meaningful.
We have followed appropriate naming conventions.
Our indentation is consistent and matches our conventions.
We have complete javadoc comments for all public methods and classes.
At this point, there is duplication that we can clean up. The instance variable
named size is holding a value that is the same as songs.length. Eliminate that
variable and replace its use with songs.length.
170
Test Filling the Album
Adding one song didn't really fill out all of the code related to adding songs, so
let's build another test named testFilledAlbum that puts the maximum number of
songs allowed into an album. This is a good border case: whenever we are using an
array, one border case is when the album is completely full. Here's the description
of the test:
1. Create an instance of an album whose title is "Heavier Things" which can
contain up to 3 songs.
2. Create an instance of a song whose title is "Clarity" and whose duration is
4:32.
3. Add the song to the album.
4. Create an instance of a song whose title is "Something's Missing" and whose
duration is 5:05.
5. Add the song to the album.
6. Create an instance of a song whose title is "New Deep" and whose duration is
4:09.
7. Add the song to the album.
8. Check that the album contains three songs.
9. Check that getSong(0) returns the instance we created in step 2.
10. Check that getSong(1) returns the instance we created in Step 4.
11. Check that getSong(2) returns the instance we created in Step 6.
That test should compile without any changes, so just run it to get to RED.
The test fails at step 9. Look at the error message:
java.lang.AssertionError: expected:<Clarity: 4:32> but was:<New Deep:
4:09>
Step 9 is retrieving the song that is in the zeroth position of the album and, instead
of getting “Clarity” which was the first song we added, it is getting “New Deep”
which is the last song we added. Put a breakpoint at step 3 of this test and use
Step Into to figure out what part of the code we haven’t written yet. You’re hoping
to see the various songs fill the songs array, but that isn’t what you’ll see.
Fix the code to get to GREEN and REFACTOR as necessary.
Testing Duration (phase 1)
Now that we can put songs into the album, let’s write a test for getDuration(). The
duration of an album should be the sum of the durations of the songs in contains.
The simplest test for this would create an album, put one song into it, and make
sure that the duration of the album is correct. Write the test named
171
testDurationSimple() with that functionality and just enough code to get it to
compile. Watch it fail to get to RED.
Making this test pass is pretty simple: just make getDuration() return the duration
of the of the song in the zeroth position of the array. It's not the final code, but it
will make the test pass which demonstrates some progress. Get to GREEN and
REFACTOR if you need to.
Testing Duration (phase 2)
Now we can buckle down and make a real test for getDuration(). In this test (name
it testDurationFull), build a filled album just like you did in testFilledAlbum(). Then
check that getDuration() returns the correct value. Write the test and watch it fail
to get to RED.
Now we need to write the real code in getDuration() to make the test pass. It
needs to sum up the durations of all of the songs in the album. This will require a
“walking through the array” type of counting loop. At each song, the loop will add
the duration of that song to a variable that is holding a running total. After the
loop is finished, the method will return that running total. GREEN and REFACTOR.
Testing Duration (phase 3)
We've tested that completely full albums return correct durations, but what
happens if the album isn't completely filled? The easiest way to know is to write a
test and see what happens. Write a test named testDurationPartiallyFull:
1. Create an instance of an album whose title is "Heavier Things" which can
contain up to 3 songs.
2. Create an instance of a song whose title is "Clarity" and whose duration is
4:32.
3. Add the song to the album.
4. Create an instance of a song whose title is "Something's Missing" and whose
duration is 5:05.
5. Add the song to the album.
6. Check that the duration of the album is 9:37 (577 seconds)
When you run this, if it fails, you’ll see an error message that says something about
a NullPointerException. This means that you have tried to call a method on an
instance that hasn't been allocated. In other words, we have a reference variable
that doesn't point to an instance (it is a NULL pointer). This is because our array of
songs has three positions, but we've only put two songs into it.
To fix this, we need to make getDuration() only walk through the part of the array
that we have filled. You'll need to change the while condition of the for loop to pay
172
attention to the number of songs we've put into the album instead of the length of
the array. As always, GREEN and REFACTOR.
There’s an important thing to notice here: getDuration() isn’t a particularly
complex method, but we used three RED, GREEN, REFACTOR iterations to build it:
1. The simplest case of one song
2. The border case of the full album
3. An in-between case to make sure that non-border cases work.
Again, we want each of the RED, GREEN, and REFACTOR iterations to be VERY
small so that it’s relatively easy to get to GREEN (RED makes us nervous!).
Testing toString()
The last thing we need to build is toString(). As an example of the String we want
the method to build, here is the output of toString() for the full album we created
in testFullAlbum():
Heavier Things:
Clarity: 4:32
Something's Missing: 5:05
New Deep: 4:09
We’ve learned previously that one String can go to a new line if it contains the ‘\n’
control character. Similarly, ‘\t’ is a tab character. Therefore, the String we want
to build is:
Heavier Things:\n\tClarity: 4:32\n\tSomething's Missing: 5:05\n\tNew Deep: 4:09
You can build that String by putting together the title of the album and the
toString() results from each song in the album. However, before you go too far,
remember that we want to build this in small RED, GREEN, and REFACTOR
iterations. In fact, we can use the same three cases we used in creating
getDuration(). Write a test that tests toString() in each of those cases and, for
each, follow RED, GREEN, and REFACTOR to build a fully functional toString()
method.
Conclusion
The purpose of this lab was to experiment with arrays of objects. The Album class
certainly did that. However, this lab has also forced you to practice writing tests
and has given you some control over the mechanics of how you moved through the
RED, GREEN, and REFACTOR iterations. Your ability to become an independent
developer depends not only on your knowledge of Java constructs, but also on your
ability to think about strategies for solving the problem. Using TDD with discipline
173
(i.e. paying attention to RED, GREEN, and REFACTOR) will help you divide the
problem into manageable pieces and ensure that you are always making progress.
Vocabulary
class diagram
UML
Unified Modeling Language
Lab Questions
1. Explain your toString() method in Song. In particular, what was the purpose of the
conditional.
2. What are the steps to creating an array of objects?
3. Why did we need an integer parameter in the constructor of Album?
4. Explain your toString() method in Album.
5. Explain how you knew where to put a song in addSong().
Content Questions
1. We have studied the class String that comes with Java, but someone else had to write it.
If they were using TDD, they would have had to develop tests for the charAt() method.
Write a test for charAt() that tests the border cases.
2. Write the shell of the class represented by the following class diagram. You do not need
to write the bodies of the methods. Write as much of the code as the diagram specifies
and no more.
174
Lab 11 – Array Practice With Zip Code Encoding
A Review of Strings
You should now be proficient at using Strings, but, as usual, there are a few more details you
should know.
First, here is the memory diagram of a given string:
There are a few important observations to make from this diagram:
•
Strings are not primitives; String is a reference type. In other words, every string is really
an instance of the Java’s String class. This is because the compiler has to create the
memory space for a string without knowing how long the string will be.
•
The String holds an array of characters (the primitive type char) that, like all arrays,
indexes the positions starting at zero.
As you have seen, the String class gives us a number of methods to access the individual
characters it contains, and, many applications use those capabilities to deconstruct one large
string into its constituent parts. For example, if we had a String containing the date in
MM/DD/YYYY format, we might want to isolate the substrings for the month, day, and year and
treat them separately.
Zip Code Encodings
The postal service speeds the sorting of mail by printing the zip codes as bar codes that are easily
readable by various sorting machines. In this lab, we'll experiment with encoding and decoding
zip codes. First, we need to understand how 5-digit zip codes are translated into bar codes. As an
example, here is the encoding for the zip code 95014:
The encoding is made up of two sizes of bars: full height and half height. It always begins and
ends with a full height bar to help the scanning equipment line up the image. These bits are called
frame bits.
175
Each digit is encoded with five bits. The first four digits of the encoding are using positional
math (like decimal math) except that the positions have different values. Instead of each position
being multiplied by 1, 10, 100, 1000, etc., 1, 2, 4, and 7 multiplies the value in each position,
respectively. For example, when the first four bits are 1010, the number encoded is 1*7 + 0*4 +
1*2 + 0*1 = 9. This positional encoding is used for every number except zero. Instead of setting
the first four digits of zero to 0000, the encoding is 1100. This translates to 1*7 + 1*4 + 0*2 +
0*1 = 11! We'll come back to that in a minute.
The fifth bit in the encoding of a digit is called a parity bit and is used for error checking. The
value of that bit is selected so that the encoding of every digit has exactly two full-height bars and
three half-height bars. This allows the scanner to detect a variety of scanning errors. For example,
it will be able to detect the error of reading a full height bar as a half height bar. This detection is
valid regardless of whether the misread bar is one of the first four bits or the parity bit, since only
one full height bar would have been scanned for the group of five bars. Now you should be able
to explain why the encoding of 0 is unique.
The following table shows the encodings of every digit between 0 and 9 (where 1 is full height
and 0 is half height):
Digit
Multiplier
Parity
7
4
2
1
1
0
0
0
1
1
2
0
0
1
0
1
3
0
0
1
1
0
4
0
1
0
0
1
5
0
1
0
1
0
6
0
1
1
0
0
7
1
0
0
0
1
8
1
0
0
1
0
9
1
0
1
0
0
0
1
1
0
0
0
The encoding of the full zip code has one more form of error checking: a check digit. The check
digit is chosen so that adding up all of the digits including the check digit results in a sum that is a
multiple of 10. For example, the digits in 17257 add up to 1+7+2+5+7 = 22, so the check digit
would be 8 and the total sum would be 30 which is divisible by 10.
Set Things Up
In this lab, we are going to build a class, ZipCode that builds the bar code for a
given zip code. As a plan for where we are going, here is a class diagram of the
class we are working on building:
176
There is one instance variable that will hold the zip code we are working on and
these are the methods we are going to create are:
-
a constructor that takes an integer parameter and stores it in the instance
variable,
-
toString() that builds a string containing the zip code,
-
getCheckDigit() that computes the value of the check digit for this zip code,
and
-
getBarCode() that builds a String containing the bar code for this zip code.
As always, we’ll build this a little at a time. Create a project in Eclipse and a JUnit
test class called ZipCodeTest.
Start With the Constructor
You're going to create a ZipCode class that is given an integer representing a five
digit zip code at construction. For now, we just want to check to make sure that it
can identify itself correctly in its toString() method. First, we write the test. In
the test class, create a test method called testInitializationSimple(). This test
should do the following:
1. Create an instance of zip code for our zip code (17257).
2. Check that the toString() of that zip code returns the string "17257";
Build the test, write enough code to make it compile, and watch it fail. (RED)
The simplest way to make this test pass is to make toString() return the String
“17257”. Do that to make your test pass. (GREEN)
Now, we have some duplication (the 17257 is in our test and the toString() of our
zip code class). We are going to need to REFACTOR to get rid of it. To do that,
follow these instructions:
-
make the constructor store the zip code in its argument into the instance
variable in the class diagram and
-
return that variable (converted to a String by concatenating it to the empty
String) in the toString() method.
177
Make sure your tests still pass. (GREEN)
Constructor Border Cases Part 1
In our constructor, we are passing the zip code as an integer even though it has a
fixed length. In this case, the border cases involve the zip code beginning or ending
with zeroes because these are the cases in which we are most likely to lose
information, so we’d like to write tests for each of those possibilities. First, let’s
address the issue of trailing zeros. Build a new test that does the following:
1. Create an instance of zip code passing 12340 to the constructor.
2. Check that the toString() of that zip code returns the string "12340";
This test is making sure that the trailing zero in the zip code is handled properly.
Luckily, it will go GREEN without any work. That doesn’t mean the test isn’t useful,
it just means it didn’t require any special code. If you want to be very thorough,
make your test verify zip codes 12300, 12000, and 10000 to make sure that all
trailing zeros work.
Now, let’s deal with leading zeros. Consider the zip code 01234. If we think of that
as an integer, we wouldn’t normally write the leading zero. In Java, if we use 01234
as an int, the leading zero tells the compiler to treat the number as base 8 instead
of base 10. There’s a long explanation for this, but, for this exercise, it’s enough to
know that we shouldn’t put leading zeroes on ints.
To test for zip codes with leading zeros, build a test that does the following:
1. Create an instance of zip code passing 1234 to the constructor.
2. Check that the toString() of that zip code returns the string "01234";
Build the test and watch it fail. (RED) Clearly, leading zeroes are going to require
some special handling.
toString() Phase 2
Making the leading zeros test pass is going to take a little more work, but you've
done this before. Remember the issue we had when we output seconds in the
duration of a song. All you have to do is tag on the leading zero to the output in the
appropriate case. Don't go on until the test passes. (GREEN)
More toString() Testing
Zip Codes can be any value between 1 and 99999, so they may have more than one
leading zero. I doubt we've handled all of those cases, but let's improve our tests
to be sure.
178
For each of the following values, create a test, watch to see it fail (RED), then
write the code to make it pass again. (GREEN)
123,
12, and
1.
At this point, there’s a good chance your toString() method is getting messy. Take
a minute to REFACTOR and make sure your tests still run.
Tests for Computing Check Digits
The first step in encoding is to compute the check digit. As always, write the test
first. Create a new test named testCheckDigit() that creates a number of different
instances of zip code and checks that the getCheckDigit() method returns the
appropriate integer. Watch the test fail. (RED)
The Mod operator
The “modulus” operator (usually shortened to “mod” and expressed as a “%” in Java) performs
integer division, but, instead of returning the quotient, it returns the remainder. For example, 7 %
3 returns 1 (the remainder left after dividing 7 by 3).
Often, the mod operator is used when we cycle through a series of numbers. For example, look at
the following code:
int count = 0;
for (int i=0; i<10; i++)
{
System.out.print(count + “ “);
count = (count + 1) % 4;
}
Run that code with a pencil, what is the output?
.
.
.
The interesting statement is the one that is incrementing count. When count is 0, count + 1 is 1
and the remainder when you divide by 4 is still 1, so count appears to increment. When count
reaches 3, count + 1 is 4 and the remainder when you divide 4 by 4 is 0, so that is what is stored
back into count. In other words, count will take on this sequence of values: 0, 1, 2, 3, 0, 1, 2, 3,
0, 1 (and then the for loop ends).
Computing the Check Digit
Computing the check digit isn’t hard, but it takes a sequence of calculations. First,
we need to compute the sum of the digits in the zip code. We’ve built loops that
179
compute running sums before, so the question is, “How do we tear the digits apart?”
To figure that out, think about what happens if evaluate x%10 for any integer x. If
it helps, try it with a few different values of x. Once you see the magic that gives
you, think about what happens to x if you execute:
x = x/10;
Together, modulus by ten and division by ten can make a beautiful sentinelcontrolled loop that sums the digits of the zip code.
Once you have the sum, you need to pick a number that, when added to that sum will
give an overall total that is divisible by 10. That phrase, “divisible by 10,” should be
a clue that, once again, the mod operator can help you. It might be valuable to think
about what the check digits are when the sum of the digits is 2, 12, and 22.
Write the code to get to GREEN and REFACTOR if necessary.
Border Cases for Check Digits?
As always, we need to think about the border cases. Where does the behavior of
this computation change? It changes at the point where the sum of the digits is a
multiple of ten. When the sum is a multiple of ten, the check digit will be zero, but
when the sum is one more than a multiple of ten, the check digit will be nine. For
example, the zip code 01234 needs a check digit of 0, but the zip code 11234 needs
a check digit of 9. A change of one in the sum that causes a change of nine in the
check digit – that’s certainly a cause for concern.
Write a test that contains the border cases of check digits. If it passes right
away, great, now we’re sure it works. If not, fix the code and make sure that all of
the tests run. Get to GREEN and don’t forget to REFACTOR.
Bar Codes - the Test
Now we're really ready to generate the bar codes (or at least it feels that way!).
Let's start by writing a simple test:
1. Create an instance of the zip code 17257.
2. Check to make sure that getBarCode() returns the correct bar code
Wait a minute! How should we display the bar code? It should be a string, but we
need to represent full height and half height bars. Let's use these characters: |
and ` (which looks upside down, but it's sufficient for now). Our bar code should
look like this:
|```|||```|``|`|`|`|`|```||``|`|
(You should be able to generate that from the zip code!)
So, the assertion in the test will look like this:
180
assertEquals("|```|||```|``|`|`|`|`|```||``|`|",
zip.getBarCode());
Make the test compile. Run it and watch it fail. (RED)
Refactoring How We Store the Zip Code (background)
Encoding our zip code is going to require us to look at its digits individually. We
already did that once to generate the check digit. We could copy that code and
modify it in getBarCode, but it's never good to duplicate code (that duplicates bugs,
too). It's time to think about how we're storing the zip code. This is an excellent
example of the need to refactor. We often start with one solution and then realize
that there is a better way to build the system. In these cases, we refactor the
code in small steps to incrementally change from the current design to the
improved design. Since we have test cases, we can be sure that we don't break
anything along the way.
The easiest initial strategy was to store the zip code as an int and that has carried
us this far. However, since we often want to look at the individual digits, maybe it'd
be better to store the digits in an array of ints. Here is a class diagram of where
we are going:
We’re making a pretty fundamental change to our class and it’s a change that will
affect much of our code. The best strategy is to make this change in a series of
small steps, which are detailed in the following sections of this lab. The basic idea
is to create the new way we will store the data (the integer array) and then, one
method at a time, convert the code to using the new array. At each step, we’ll run
the tests to be sure we haven’t broken anything, and, if the tests are broken, the
portion of the code that we have modified since the last time the tests pass will be
relatively small so it shouldn’t be difficult to locate the problem. We’ll delete the
original integer instance variable only after we have converted all of the code to
use the array.
While we do this, the one test of getBarCode() will be RED, but we should keep the
rest of the tests GREEN. When we are refactoring, it’s most comfortable if the
tests are green at all times. For now, let’s mark the getBarCode test with
“@Ignore” (next to the @Test). You’ll have to import org.junit.Ignore to get
it to compile. Then, when you run the tests, JUnit will show GREEN, and the one
181
test that isn’t complete will be shown as ignored. That way, we won’t forget to
come back to it after the refactoring.
Refactoring How We Store the Zip Code (phase 1)
For the start of this refactoring, declare the array of ints and fill it with the digits
of the zip code in the constructor. (Note - we are not yet removing the int in which
you originally stored the zip code). You’ll want to think about which order you want
to store the digits in the array. You have two choices. For example, here are two
ways we could store the zip code 17257:
The first of those diagrams may seem most logical to you because you see the zip
code in the order you expect. However, it’s often easier to store it in the order of
the second diagram because the digits that are multiplied by higher powers of ten
are in the higher numbered position of the array. This means that we can calculate
the value by multiplying the value in each position by ten to the power of the
position minus one. In this lab, you can pick which order you prefer. And you can
always change it by refactoring later!
To test that you stored the digits into the array correctly, change toString() to
build the string from the array of integers. Don't go on until JUnit is GREEN.
Refactoring How We Store the Zip Code (phase 2)
Our code now has duplication because we have the zip code stored in two places:
the integer and the integer array. We need to get rid of our original integer. The
only other place where we use that original int is in getCheckDigit(). Modify that
method to use the new int array (which should make it simpler). Don't go on until
JUnit is GREEN.
182
Refactoring How We Store the Zip Code (phase 3)
At this point, the original int instance variable is not being used anywhere but the
constructor. Remove it from your code and re-run your tests to be sure that
everything is OK.
It's important that we pay attention to what we just did. We've made a significant
change to our system - its storage mechanisms are completely different. However,
we made the change in stages (leaving the old storage around and replacing its use
one method at a time). At every stage, we made sure that our tests pass. This way,
if we broke anything, we only have to look at a small amount of modified code to
find our mistakes.
Initializing Arrays at Declaration
When we worked on Decks and Cards, we studied array initializers. We’re going to use them
again, so let’s do a quick review.
If we know the initial values to be stored in an array, we can put those values in at the declaration
of the array. For example, if we wanted an array that contained the first five prime numbers, we
could declare and fill the array like this:
int[] firstPrimes = {1, 2, 3, 5, 7};
That would create an array with this memory diagram.
We can use this technique for initializing any array regardless of what type of values it holds. For
example,
boolean[] available = {false, true, true, false };
creates this array:
Initializing arrays at declaration isn’t limited to primitive types. For example, if we have a
Student class whose constructor takes an int representing the student’s id number, this code:
Student[]
new
new
new
myStudents = {
Student(32),
Student(46),
Student(2));
creates this array:
183
Back to bar codes
Now that our zip code is stored in an int array, generating the bar code will be
much easier. Remove the @Ignore tag and our test will be RED. We are going to
build the string in pieces, so the test won’t go GREEN for a couple of steps.
However, at each step we’ll be able to look at the error message from JUnit to
verify that we are getting closer.
To start, we will need some way of generating the substrings for each digit. A
simple way to do that is to store the mapping between digit and encoded string. We
can define an array of strings that hold the encodings of each digit. For example,
the string in position zero of this array will be ||``` since that is the encoding of
0.
Similarly to our declaration of the suits of the Cards, we can declare our array of
strings for each encoded digit like this:
String[] code =
{"||```",
"```||",
"``|`|",
"``||`",
"`|``|",
"`|`|`",
"`||``",
"|```|",
"|``|`",
"|`|``"};
You should be able to verify each string in this array.
Bar code generation (phase 1)
Now, we're finally ready to start generating the bar code. In getBarCode(), you're
going to build a string holding the bar code, but it has several pieces that we'll put
together one step at a time.
Remember, the bar code contains:
1. a framing full-height bit
2. encodings of each of the digits in the zip code
3. encoding of the check digit
4. a framing full-height bit
184
Start by building a string that has the initial framing bar and make the method
return that. (I know it's hard to match up these encoded strings.) Make sure the
test fails with an appropriate error message. (RED)
Bar code generation (phase 2)
Now we need to walk through the array holding the digits of the zip code and
concatenate each digit's encoding onto the string we're building. Here's a clue:
since the array holding the digits is named "zipDigits", the encoding of the first
digit will be
code[zipDigits[0]]
Play with that code until you understand how it works. Here’s how to analyze it:
zipDigits[0] evaluates to an integer that is the first digit of the zip code. For
example, if our zip code is 12345 and we stored the digits in the order we see
them, zipDigits[0] will evaluate to 1. Therefore, code[zipDigits[0]] will be code[1].
That will be the second string in our code array which is ’’’||. That is the string
that will accurately represent the first number of our zip code. Once you see how
that code works, combining it with our usual walking-through-the-array loops should
be straightforward. You should be able to add the strings for the digits in the zip
code to your initial framing bar. Just be careful about the direction you want to
walk through the array.
When you run your test, it still isn't going to pass, but you should be able to verify
that the error message shows your progress. If that's too complex (because the
strings are repetitive), put a break point on the return statement and verify the
contents of the string in the debugger.
Bar code generation (completion)
We're in the home stretch! You just need to concatenate the encoding of the check
digit (you can call getCheckDigit() to get it's value) and tack on the last framing bit.
Watch all of your tests pass! (GREEN)
You’ve written quite a bit of code to get this test to pass. Go back and clean things
up. Remember to check comments, variable names, and indentation. (REFACTOR)
Let's be Thorough
Just to be thorough, beef up your tests for bar code generation to include zip
codes with leading and trailing zeroes and to use every digit from 0 to 9 in at least
one zip code. If you code the tests correctly (and don't have any typos in the codes
array declaration), they should still pass when you're finished.
185
Vocabulary
@Ignore
frame bit
parity bit
Lab Questions
1.
2.
3.
4.
What is the bar code for the zip code 01437?
How did we ensure that the parity of each digit was correct?
Explain your implementation of getCheckDigit().
What does this code evaluate to?
code[getCheckDigit()]
5. Define these terms:
a. frame bit
b. parity bit
c. check digit
6. Explain the way we refactored the code to change how we stored the zip code.
Content Questions
1. Explain why 1010 is the encoding for 9.
2. There were two ways that we could have encoded 7: 1000 or 0111. Which did we pick
and why?
3.
4. Draw the memory diagram for the following code:
double[] x = new double[5];
x[3] = 3.14;
5. Draw the memory diagram of the array built by the following code:
String[] x = new String[6];
for (int i=0;i<x.length;i++)
{
x[i] = “String”+i;
}
6. Write a method that takes a string in the format “mm/dd/yyyy” and returns the day as an
integer.
7. If x contains the String “NXzW^&@3f” what would each of the following evaluate to?
a. x.charAt(2);
b. x.substring(3,5);
c. x.length();
d. x.contains(“W&”);
e. x.contains(“&@3”);
f. x.substring(3);
g. (x == “NXzW^&@3f”)
186
8. What is the output from the following code?
String x = “NXzS^&@3f”;
int count = 0;
for (int i=0;i<x.length; i++)
{
if ((x.charAt(i)>= ‘M’) && (x.charAt(i) <= ‘T’))
{
count++;
}
}
System.out.println(count);
187
Lab 12 – Method Overloading and Throwing Exceptions
With Zip Code Decoding
Introduction
We will continue to work with zip codes, but in the opposite direction: given a
String that represents the bar code, we’re going to compute the zip code. When we
design the classes of a system, we want to have one class for each “thing” in the
systems. Since we already have a class that handles zip codes, we’d like to put all
of the functionality for zip codes in that class. If you look at the class we built in
the last lab, we create an instance of ZipCode by passing the zip code into the
constructor. The constructor stores the information into an array of integers and
everything else uses that array.
In this lab, we want to be able to do all of the same things to our zip code, but we
want to start from the bar code instead of from the zip code. In other words, we’d
like to create an instance of ZipCode by passing the bar code into the constructor
and have the constructor decode that bar code to create the same array our
existing constructor creates. If we could do that, all of the rest of the methods in
the class would work. For example, we’d like this test to pass:
@Test
public void testDecode()
{
ZipCode z = new
ZipCode("|```|||```|``|`|`|`|`|```||``|`|");
assertEquals("17257", z.toString());
}
In order for that test to compile, we’re going to have to build a second constructor
using some very specific rules.
Method Overloading
Sometimes, there are multiple ways to perform the same logical operation. For example, the
String class has two ways you can ask for a substring:
•
•
substring(intbeginIndex) – return the substring starting at index beginIndex and
continuing to the end of the String
substring(intbeginIndex, intendIndex) – return the substring starting at index beginIndex
and stopping up to but not including endIndex
Both of these methods are finding a substring, so it is reasonable that they should have the same
name. However, they behave in slightly different ways and are different methods. Using the
same name for two methods is called method overloading (or overloading the method) and
must be done carefully so that the compiler can tell which of the methods you want to call.
In Java, a method signature is defined by: the name of the method, the number of parameters,
the types of those parameters, and the order of the parameters. Every method must have a unique
signature. If two methods share the same name they must have different parameter lists in order
188
to allow the compiler to infer which method is being referred to by a particular call. In the
substring example, the compiler knows which method you want to call by the number of
parameters you put in the method call. For example, x.substring(3) will call the first
method and x.substring(3,6) will call the second method.
In summary, two methods can have the same name if their parameter lists are of different length,
or the parameters have different types, or the order of the parameters differ. Notice that the
names of the parameters and the return type of the methods do not matter in this restriction.
Building the New Constructor
Remember, by definition, all constructors have the same name as its class. In this
case, we want two versions of the zip code constructor (the one we already built
that takes an integer parameter and a new one that takes a bar code parameter), so
we need to overload the constructor. Since the parameters of the two
constructors have different types (int and String), their method signatures will be
different.
For the first test of our new constructor, let’s make a test that requires the
correct translation of the first digit of the zip code:
@Test
public void testDecodeSingleDigit()
{
ZipCode z = new
ZipCode("|```||||```||```||```||```|`|``|");
assertEquals("10000", z.toString());
}
In order to make that test compile, you are going to have to create a second
constructor in your ZipCode class:
public ZipCode(String barCode)
{
}
Run your tests and you should be RED.
Decoding the Bar Code (Phase I)
The new constructor has to initialize the same instance variables that the old
constructor initialized. That way, the rest of the methods in the class will work
regardless of which constructor we use to create the instance. In the long term,
this means the new constructor is going to have to break the barcode into five
pieces and covert each piece into an int so that it can store them into the existing
array of integers.
This constructor is pretty complicated, but we have already used problem
decomposition. We’ve written a test that only requires that we convert one digit of
the zip code. The rest of the digits will be zero. Java initializes our array to
contain zeros, so we only have to convert the first digit to make this test pass.
189
Converting the one digit of interest requires:
•
•
•
find the substring of the bar code associated with that digit
search through the codes array to find the matching string (the position will
be the number we want to store)
store the position of the codes array into the instance variable at the
correct index.
Write the code and get to GREEN. REFACTOR as necessary.
Decoding the Bar Code (Phase II)
Now that we have the first digit decoding properly, we’re ready to write a test that
requires the rest of the digits. Let’s use the test from our initial example:
@Test
public void testDecode()
{
ZipCode z = new
ZipCode("|```|||```|``|`|`|`|`|```||``|`|");
assertEquals("17257", z.toString());
}
Run the test and it should be RED.
To make this test pass, you have to loop around the code in your constructor so
that it decodes all five digits. Since we know exactly how many times the loop
should execute, you can use a counting loop. Then use your loop variable to calculate
the ends of the substring you need and as the index into the array where you will
store the integer. Get to GREEN. When you REFACTOR, remember that it is bad
form to hard code values like the “5” that probably controls your loop. You should
declare a constant to hold the number of digits in a zip code. That way, if you have
to upgrade to 9 digit zip codes, you only have to change a constant. As always, make
sure all of your tests still run.
Decoding the Bar Code (Phase III)
At this point, you probably have completed the constructor, but there are some
cases we haven’t tested. Write tests for each of these to be sure your constructor
is complete:
•
•
Leading zeros are handled correctly (you already tested trailing zeros) and
That every digit translates correctly.
What could go wrong?
We’ve been coding this lab as if every bar code that is given to the constructor is
valid. However, it’s possible that a number of things could be wrong:
•
The bar code could be too long,
190
•
•
•
•
The bar code could be too short,
The framing bits could be short instead of tall,
Any single digit could have invalid parity (and therefore, won’t be in our
codes array), or
The check digit could be incorrect so that the sum of the digits is not a
multiple of ten.
Since we have ignored these possibilities, most of them would cause our code to
crash. Our system would be more robust if we checked for these problems and
reported them to the code that calls our constructor.
Throwing Exceptions
In Java, an exception is a condition that is unexpected and requires special handling. You have
already seen two exceptions: ArrayIndexOutOfBoundsException and
StringIndexOutOfBoundsException. Those exceptions occurred when your code tried to use an
index that was too large for the structure it was indexing. Since the code didn’t know what to do,
it caused an exception. In the next lab, we’ll study ways to handle exceptions; for now, we will
learn how to detect and throw exceptions.
When the code detects that an exception has occurred, it throws the exception. This means that
the current method stops running (essentially returns without a value) and the exception is
“thrown” to the code that called the method. For example, if we are developed a method that uses
a positive only parameter, but passed a negative value into that parameter, that would be an
exception. We would want to throw an exception. Our code would look like this:
public void sillyMethod(int myInt) throws Exception
{
if (myInt <= 0)
{
throw new Exception("myInt can’t be negative");
}
// the rest of our method starting here won't
// execute if the exception is thrown
}
Since throwing the exception causes the method to return, we can be sure that myInt is positive
when the rest of the method starts executing.
Look carefully at the throw statement that is combined with the new statement. It is creating an
instance of type Exception and that is what is being thrown. The parameter to the constructor of
Exception is a String that is a message explaining what caused the exception.
Exception is the most generic type of exception you can throw. Java also comes with a number of
other exception types. If you look in the Java API for the class Exception it will list a number of
“subclasses” which are more specific types of exceptions you can throw. For example, our
sillyMethod() might have been more specific if it had thrown IllegalArgumentException (which is
a more specific version of RunTimeException) in place of the generic Exception.
When you throw an exception, there is one more thing you have to do. The compiler needs to
know that there is a possibility that an exception will be thrown. It will use this information to
make sure that the code that called your method will either handle the exception or pass it on to
191
the code that called it. Therefore, if the code in your method causes an exception and doesn’t
handle it, you must add a throws clause to your method declaration as shown in the previous
example.
Testing For Exceptions
It is just as important that we test that exceptions are thrown correctly as it is that we test the
normal functionality of our code. JUnit gives us a simple way to denote that a test should expect
an exception to be thrown: after the @Test annotation, we put
“(expected=<expectedException>.class)” With this parameter on the annotation,
the test will only pass if it sees the specified exception.
For example, suppose the sillyMethod() in the previous section was part of a SillyClass. If we
modified it to throw an IllegalArgumentException, this test would verify that passing a zero as the
parameter caused an exception:
@Test (expected = IllegalArgumentException.class)
public void testException()
{
SillyClass s = newSillyClass();
s.sillyMethod(0);
}
The only way for that test to pass is if something throws an IllegalArgumentException.
Testing for Bar Codes that are Too Short
Before we can throw exceptions, we first have to create our own exception class.
The Java API includes some exceptions, but it’d be more specific if we define our
own. Create a new class called ZipCodeException and make it look like this:
/**
* Exceptions caused by invalid zip codes
* @author Merlin
*
*/
public class ZipCodeException extends Exception
{
private String message;
public ZipCodeException(String string)
{
this.message = string;
}
public String toString()
{
return message;
}
}
In Lab 13, we’ll learn what that “extends” keyword really means, but for now it’s
enough to know that it means that objects of type ZipCodeException can do
192
everything that objects of type Exception can do. Essentially, we’ve made a new
type that matches Exception except that it has a different name. This way, our
tests will be able to verify that the correct exception is being thrown by our code.
We’ve allowed that class to store a message describing what occurred just like the
other exceptions that come with Java.
The first exception we’re going to work on is when the bar code has too few
characters to be a legal bar code. The border case for this would be when the bar
code is just one character too short, so let’s use that as our test case. Here is the
JUnit test:
@Test(expected = ZipCodeException.class)
public void testTooShort()
{
ZipCode z = new
ZipCode("|```|||``|``|`|`|`|`|```||``|`|");
}
Notice that the bar code is the same as the one for 17257 except that we deleted
one short bar in the middle. Run the test and it should be RED. In fact, you’ll
probably get an ArrayIndexOutOfBoundsException.
The only way for this test to pass is for the constructor to throw the
ZipCodeException. You’ll need to add code at the beginning of the new constructor
that checks the length of the barcode passed into it. If that length is too long,
throw an instance of ZipCodeException and put a reasonable description of the
problem in the new statement. When you add the throw statement, the compiler
will complain because it isn’t expecting that constructor to throw any exceptions.
In order to fix that, you’ll have to add a throws clause to the constructor definition
like this:
public ZipCode(String barCode) throws ZipCodeException
That will make your ZipCode class compile but it will make a number of your old
tests not compile. Now that the compiler knows that the constructor might throw
an exception, every method that calls that constructor must either handle the
exception (which we don’t yet know how to do) or throw it to the called it. In other
words, since we aren’t going to handle the exception in the tests, they will
automatically throw it onto the code that called them. Therefore we have to add a
throws clause to the signature of each of those test methods. That should make
everything compile and let you get back to RED.
Get to GREEN and REFACTOR to clean things up.
More Barcode Exceptions
Previously, we listed possible errors in a bar code. For each, write a test, watch it
fail (RED), then make it pass (GREEN) and REFACTOR. By the end, your
193
constructor may look pretty complex because it’ll have to check for a variety of
situations. Just REFACTOR at each step to keep things in control and notice that,
once again, the tests are helping us with problem decomposition. Writing all of that
constructor at once would be overwhelming, but the tests help us build it a piece at
a time while making sure we haven’t broken anything else along the way.
Bonus – More Exceptions
We’ve made sure that the constructor that uses the bar code as its parameter is
thoroughly error checking that parameter. However, we didn’t do any error
detection on the parameter in the other constructor. Think about which integer
values are valid and which are not. Build tests for the invalid parameters (at the
border cases) and write the code to throw appropriate exceptions.
Vocabulary
exception
overload
method signature
throw
throws
Lab Questions
1. Explain how you found the part of the bar code that corresponded to a particular digit in
the zip code.
2. Explain how you converted the parts of the bar code to the integers you were storing.
3. Why did we create our own exception class?
Content Questions
1.
2.
3.
4.
5.
What is method overloading and what are the rules that determine how you do it?
How do we mark a test that is testing to be sure a particular exception gets thrown?
What is the syntax to throw an exception?
How do you have to change the method declaration if you throw an exception?
What is the difference between the keywords throw and throws?
194
Lab 13 – Building an Application and Catching
Exceptions
Introduction
We’ve built all of the algorithms necessary to translate zip codes into bar codes
and bar codes into zip codes. In this lab, we’re going to build an application that a
user can use those algorithms to make those translations.
As a first step, we’re going to make an application that outputs the bar code for our
local zip code. To start, create a new project in Eclipse and copy the ZipCode,
ZipCodeException, and ZipCodeTest classes from your previous project. If your
test doesn’t compile, it’s probably because the compiler can’t find Junit. Right-click
on the red spot next to one of the errors and select “Quick Fix.” One of the
options there will be “Add JUnit 4 to the build path.” Selecting that option should
clear up of the compilation errors.
With our current knowledge, we can’t use JUnit to make automated tests that take
user input, so our tests for this lab are going to be sequences of user input and
output. RED will mean that the user input did not result in the correct output and
GREEN will mean that everything worked as planned.
Our first test is very simple; the application should just output this string:
The bar code for 17257 is: |```|||```|``|`|`|`|`|```||``|`|
Now, create a new class that will be your application. We’re going to need this class
to be runnable (If you don’t remember what that means, go back and look it up!) so
make it contain one empty method like this:
public static void main(String[] args)
{
}
You can now run your application by right clicking on this class and selecting Run As
-> Java Application. You see no output, so we are RED.
We could get to GREEN quickly by just making it output the required String, but
that would require correctly typing in all of those bars. It’s easier to use our
ZipCode class to compute the bar code. In your main() method, create in instance
of ZipCode and use it to build the required output String. Don’t go on until your
test is GREEN. REFACTOR as necessary.
Preparing for User Input
Our next step is to allow the user to enter the zip code, so let’s change our test to
this (the regular font is output and bold is input entered by the user):
195
Enter a zip code:
17257
The bar code for 17257 is: |```|||```|``|`|`|`|`|```||``|`|
The first line of the output is called a prompt, because it tells the user what data
must be entered. After the user enters a zip code, the program will output the
appropriate bar code.
Make your program output the prompt, but, even then, this test is RED because it
doesn’t let the user input the zip code.
Scanner
Java contains a class named Scanner that you can use to get input from a keyboard. Creating an
instance of Scanner is pretty simple:
Scanner input = new Scanner(System.in);
The parameter to the constructor tells the scanner to get its input from the keyboard. There are
other parameters you can use to make the scanner read from other places, but, for now, we just
need to read from the keyboard.
To learn more about Scanner, open the Java API documentation. Scanner is in the java.util
package, so click on java.util in the upper left frame. Then, scroll in the lower left frame to find
Scanner and click on it. When you’re looking for a class, but don’t know what package it is in,
just click on “All Classes” in the upper left frame and then look for the class in the lower left
frame.
Scanner will read the input and divide it into tokens. A token is the input between two
delimiters. The default Scanner uses any whitespace (blank, tab, newline, etc.) as the delimiter,
so the input is separated into tokens by white space.
Scanner has two types of methods that are useful in this lab: those that can be used to tell the type
of the next token and those that retrieve the next token. Read through the list of methods to find
the ones you’ll need.
Getting User Input
When your program is running, enter input by clicking on the console tab at the
bottom of the screen and then typing your input followed by the return key. Now
that you know how to read and enter input, fix your code to make the test GREEN.
Make sure that it works with different zip codes. REFACTOR as necessary.
Handling Conversions in Both Directions
Now that we can display the bar code for a given zip code, let’s work on displaying
the zip code for a given bar code. As usual, let’s start by specifying what we need
to build by specifying new tests. Here are a couple of sequences that describe the
system we want.
196
We want to prompt the user of your program to find out if they want to enter a zip
code and see its bar code, or the other way around? First, respond to the prompt
to test the first case:
Which type of conversion do you want?
1. Zip Code to Bar Code
2. Bar Code to Zip Code
1
Enter a zip code:
17257
The bar code for 17257 is: |```|||```|``|`|`|`|`|```||``|`|
Getting this test to GREEN shouldn’t be difficult, but remember to REFACTOR.
Second, they may want to enter a bar code and see what zip code it encodes (the
bar code is in bold because it is input):
Which type of conversion do you want?
1. Zip Code to Bar Code
2. Bar Code to Zip Code
2
Enter a bar code:
|```|||```|``|`|`|`|`|```||``|`|
The zip code is: 17257
In order to build this code, you’re going to have to use the other ZipCode
constructor – the one that takes a String as its parameter. Getting it to compile
will be a little tricky, since the constructor throws an exception. The compiler will
give a syntax error because we are calling a method that throws an exception
without handling that exception. For now, until we’re ready to handle the exception,
we can choose to just throw it to the JVM. In order to do this, we have to mark
the main() method with the “throws” clause like this:
public static void main(String[] args) throws Exception
To get to GREEN, make sure that you make both types of conversions work for a
couple of different zip/bar codes. REFACTOR as appropriate.
What if the Bar Code is Invalid?
In the last lab, we made the constructor of ZipCode that had the String parameter
throw exceptions if the bar code was not well formed. Run your program to see
what happens if you just enter “||” as the bar code. You should see something like
this:
197
Select what you will input:
1. Zip Code
2. Bar Code
2
Enter the bar code:
||
Exception in thread "main" Exception: Bar code is too short
at ZipCode.<init>(ZipCode.java:12)
at ConsoleUI.main(ConsoleUI.java:25)
That’s pretty nasty output from our program – we really ought to handle that
situation more cleanly. The problem is that our constructor is throwing the
exception instead of handling it.
Catching Exceptions
In order to learn more about handling exceptions, let’s look at a standard Java method that throws
exceptions: Integer.parseInt(String). This method is part of the Integer and is used to convert a
String to the integer value it represents. For example,
int x = Integer.parseInt("32");
will store the value 32 into the integer variable x. However, if the string does not contain an
integer, Integer.parseInt will throw a specific type of Exception called
NumberFormatException.
For example, this code:
int x = Integer.parseInt("32.3");
will cause this output:
Exception in thread "main" java.lang.NumberFormatException: For input
string: "32.3"
at
java.lang.NumberFormatException.forInputString(NumberFormatException.ja
va:48)
at java.lang.Integer.parseInt(Integer.java:456)
at java.lang.Integer.parseInt(Integer.java:497)
at ConsoleUI.main(ConsoleUI.java:8)
This message is showing the exact exception that was thrown and the message that was part of
the exception {remember: we put a message in the call to the constructor when we throw an
exception}. The rest of the lines are showing you the exact sequence of method calls that caused
the problem; our main() method called Integer.parseInt() which called another Integer.parseInt()
{remember method overloading?} which threw the exception.
When you call a method that throws an exception, you have two options: you can either throw it
to the code that called your method, or you can “catch” it and handle it. If you want to throw it,
you just mark your method as throwing that type of exception. Catching it is a little more
complex.
Catching an exception is done using a try/catch block. Look at this example:
198
try
{
int x = Integer.parseInt("32.3");
System.out.println("Won't get here");
}
catch(NumberFormatException exception)
{
System.out.println("Bad Int");
}
The code might throw the input exception inside a block marked with “try”. If the code throws
the exception, then the JVM will immediately jump to the catch block and execute the code from
there. The output from this code is:
Bad Int
Notice that the println inside the try block did not get executed because the exception was
thrown causing the JVM to jump to the catch block.
Look carefully at the catch block; the items in the parentheses looks like a parameter list. In
fact, the keyword catch must be immediately followed by the declaration of a variable whose
type is some type of Exception (in this case, it is variable named exception whose type is
NumberFormatException). That declaration behaves much like a parameter to the catch block.
The exception that was thrown by the code in the try block will be passed into the variable
declared in the catch block. That variable can give us information about what has occurred.
For example, we can use its getMessage() method to get the original error message given to its
constructor before it was thrown.
try
{
int x = Integer.parseInt("32.3");
System.out.println("Won't get here");
}
catch(NumberFormatException exception)
{
System.out.println(exception);
}
will output:
For input string: "32.3"
That isn’t the best error message, but that’s what the people who created the message wanted it to
be. If you look back at the output we saw before we caught the exception, you’ll see that the error
message makes more sense when the exception is thrown all the way out to the JVM.
Sometimes, a block of code may throw more than one type of exception. Each type of exception
must have its own catch block like this:
199
try
{
int value = Integer.parseInt(str);
char ch = str2.charAt(value);
System.out.println("No exception!" + ch);
}
catch (NumberFormatException exception)
{
System.out.println("We saw a number format exception:");
System.out.println(exception);
}
catch (StringIndexOutOfBoundsException exception)
{
System.out.println("We saw a String exception:");
System.out.println(exception);
}
First, walk through this code assuming that str is a String that holds “4” and str2 is a String that
holds “abcde”. You should be able to demonstrate that the output would be:
No exception!
e
However, if str contained “4x” the Integer.parseInt() would throw a NumberFormatException
causing this output:
We saw a number format exception:
For input string: "4x"
The second line of that input is the error message that was stored in the exception we caught.
Finally, if str contained “6” the charAt() call would index off the end of the String which would
make it throw a StringIndexOutOfBoundsException resulting in this output:
We saw a String index out of bounds exception:
String index out of range: 6
In summary, try/catch blocks are used to handle exceptions. The code that has the
potential to throw the exception belongs in the try block and each type of exception that can be
thrown gets its own catch block that contains the code that handles that exception.
Handling Bad Bar Code Input
Now that you know about try/catch blocks, use them to catch the exception that
gets thrown by the ZipCode constructor that decodes bar codes. In the previous
lab, you were supposed to put a different message in the exception for each error
condition (if you didn’t do that, go back and fix it!). Use the error in the exception
to explain the problem to the user. Make sure this works for all of the things that
could go wrong when we decode a bar code. For each, think of what the output
should be (that’s the test that’s RED and you need to make it GREEN then
REFACTOR).
200
Let the User Enter Codes More Than Once
At this point, the user can either decode or encode zip codes and we handle invalid
input of bar codes pretty well. There’s only one thing left to do. Our user would
like to be able to have the option to perform another operation. Here’s an
execution sequence to serve as our next test:
Which type of conversion do you want?
1. Zip Code to Bar Code
2. Bar Code to Zip Code
1
Enter a zip code:
17257
The bar code for 17257 is: |```|||```|``|`|`|`|`|```||``|`|
Would you like to go again? (y/n)
y
Which type of conversion do you want?
1. Zip Code to Bar Code
2. Bar Code to Zip Code
2
Enter a bar code:
|```|||```|``|`|`|`|`|```||``|`|
The zip codeis: 17257
Would you like to go again? (y/n)
n
The program doesn’t stop until the user enters “n” in response to the question about
whether he wants to go again. Hint: making this GREEN is a great example of a
time when a do…while loop is appropriate. As always, don’t forget to REFACTOR.
Vocabulary
prompt
Scanner
runnable
token
try/catch
Lab Questions
Content Questions
3.
4.
5.
6.
7.
8.
How can you tell if a class is runnable?
How do you make a class runnable?
Why does the constructor of Scanner take the parameter System.in?
What is a try/catch block?
How do you catch two different kinds of exceptions:
Write the code that declares a Scanner and uses it to get an integer input from the user.
201
9. Write a loop that gets integers from the user until the user enters a negative number and
then outputs the average of the inputs.
10. Write a runnable class called OutputOnly that, when run, will output the current date in
the format mm/dd/yyy. You can use the GregorianCalendar class from the Java API.
202
Lab 14 – Humans and Aliens
Introduction
In this lab, we’re going to build the engine of a game in which humans fight an
invading force of aliens. As always, we’re going to build things incrementally, so for
now, let’s start by creating an Alien class. For now, aliens are given life points at
their creation (later, we’ll shoot them to reduce their life points!). To get you
started, here’s the test you’ll need to put in a TestAlien class:
/**
*Aliens just have life points at creation
*/
@Test
public void testCreation()
{
Alien jarjar = new Alien(50);
assertEquals(50, jarjar.getLifePoints());
}
As always RED, GREEN, REFACTOR.
Hit those aliens!
We are now prepared to attack! Create a method called takeHit() that uses an
integer parameter to subtract that value from the aliens hit points (it returns
nothing). Your assertEquals should verify that the value returned by
getLifePoints() has been updated correctly. Make sure you write the test first and
then RED, GREEN, REFACTOR.
Randomness
Let's make this a little more challenging. The weapon isn't going to perfectly hit
every time, so there should be some randomness in how much damage the weapon
does. Let's say that the damage done by a weapon is a random number that is
uniformly distributed between 0 and the strength of the weapon. In that case, the
test will have to make sure that the resulting life points are within the expected
range. A simple check for this would be to replace the assertEquals() in our test
with this:
assertTrue((45 <= jarjar.getLifePoints())&&
(50 >= jarjar.getLifePoints()));
However, that test will not fail, so we'll never see RED. That means the test isn't
strong enough. In fact, we haven't verified that "uniformly distributed" stuff. We
need to test to perform many hits, record how much damage each one does and
then check to make sure that the results are uniformly distributed. Here's the
code for this new test:
203
/**
* The alien should lose life points matching the
* strength of a hit when it is whacked. The number
* of life points subtracting should be uniformly
* distributed between 0 and the strength of the weapon.
*/
@Test
public void hitsHurtRandomly()
{
int originalHealth = 50;
int weaponStrength = 5;
int[] damage = new int[weaponStrength+1];
int numberOfHits = 10000;
for (int i=0;i < numberOfHits;i++)
{
Alien jarjar = new Alien(originalHealth);
jarjar.takeHit(weaponStrength);
damage[originalHealth –jarjar.getLifePoints()]++;
}
int hitsPerDamage = numberOfHits/(weaponStrength+1);
int epsilon = (int)(hitsPerDamage*0.10);
for (inti=0;i<damage.length;i++)
{
assertTrue("not enough examples of damage = "
+i,(hitsPerDamage-epsilon) < damage[i]);
assertTrue("too many examples of damage = "
+i,(hitsPerDamage+epsilon) > damage[i]);
}
}
This test has used a feature of JUnit that we’ve never used before: every assert
method has the ability to take a String error message as its first parameter. If
the assert fails, that message will be displayed. In this case, we are expecting
damage[i] to be within epsilon (10%) of being evenly distributed. Since that means
it will be between two values, an error message about whether it is too low or two
high could be useful.
Before you watch this test fail, make sure you understand what it is doing. You
might want to review the lab we did on rolling dice because the random effect we’re
playing with here is similar to what we did in that lab.
The test should already be RED. In order to make it pass, you’re going to have to
change takeHit() to subtract a random number between zero and the strength of
the hit. (Note: the test hitsHurtCorrectly() will no longer pass once you make this
change. We’re changing the behavior of the algorithm and you can throw away
hitHurtsCorrectly() because it is obsolete). Make the test GREEN and REFACTOR.
204
Don’t Forget the Border Cases
We need to be sure that an alien’s life points never goes negative. Design a test
that hits the alien enough times to ensure that the life points could go below zero,
but verifies that they don’t actually go negative. That test should certainly fail.
RED, GREEN, and REFACTOR.
Aliens Can Recover
The aliens that are invading have one feature that makes them difficult to
eliminate: they regain life points over time. To implement this feature, you need to
create a method called recover() which lets the alien recover 10% of the life points
it has lost. The test for this method should





Create an alien
Hit the alien which will cause it some damage, but we don’t know exactly how
much
Use getLifePoints() to determine how much damage was caused
Tell the alien to recover
Verify that the correct number of points have been regained.
The definition of “recover” is “regain 10% of the life points that are currently lost.”
This means that the alien is going to have to remember the number of life points it
originally had in order to calculate how much it should recover.
RED, GREEN, and REFACTOR.
Strange Situations in Recovery
When we create a new behavior like recovery, it’s important to think about how
that behavior may be used and make sure it works in all situations. So, first, are
there border cases you should be worried about? If so, code and test them now.
RED, GREEN, and REFACTOR.
However, sometimes, simple border cases aren’t enough. Sometimes we have to
think about how are method should behave when it is a part of a sequence of
operations. We need to take a step back and think about how this method fits
intothe bigger picture of the application we are building. For example, think about
what happens for this sequence of operations:
205




Hit the alien
Recover
Hit the alien
Recover
Each recovery uses only the original life points and the current life points to
calculate how many points are recovered. Write a test to verify that you coded the
behavior properly.
Planning for Humans
Humans are like aliens because they have life points and they lose life points in
battles in the same way that aliens do. However, humans can’t recover life points.
Since humans are different from aliens, we’re going to need a Human class. It
would be nice if we could design the system so that we don’t have to duplicate the
code for life points.
Inheritance
Up to this point, most of the classes we have developed are self-contained – all of the
functionality of the class resides within that class (with the exception of our exception in Labs 11
& 12). However, often, when we are designing systems, there will be multiple classes that share
some functionality. We do not want to copy-and-paste the code to put it in both classes. That
would create duplication and we never want to duplicate code (that duplicates defects and the cost
of maintenance, too). Object-oriented languages include a concept called inheritance to address
the problems. With inheritance, classes can be organized into hierarchical structures in which
functionality can be “inherited” from one class into many other classes.
As an example of inheritance, consider a system that tracks students and faculty at a university.
The things in the systems are students and faculty, so those would be classes in our system. We
would need to store some of the same information for students and faculty. Both have names,
addresses, current courses, etc. The operations they would need to support also overlap since
they both would need to be able to do actions like schedule courses for future semesters.
However, students and faculty each have unique things they need to store and unique operations.
For example, the Faculty class might need to store salary information and the Student class would
need to be able to calculate the student’s GPA.
Remember that the term “class” can mean a set of objects of the same type. In our example, both
“faculty” and “students” are sets in the true mathematical definition of the term. However,
members of those sets are also members of the larger set of “people” as shown in this Venn
diagram:
206
In other words, “faculty” and “student” are both subsets of “people.” “Faculty” share
characteristics with “people” and so do “students.” Inheritance lets us mark classes as
superclasses and subclasses to encode that subset relationship. Once that relationship is coded,
the functionality (instance variables and methods) in the super class is “inherited” into the
subclass.
The fact that superclasses and subclasses can be described with Venn diagrams underscores
another way we can look at this relationship. The subclass relationship is sometimes called an
“is-a” relationship because a member of the subclass “is-a” member of the superclass, too.
In our example, we can use inheritance to address the fact that our Faculty and Student classes
share functionality by defining a Person class that is a superclass that both Faculty and Student
will inherit from. We show this in our class diagrams with arrows that point from the subclass to
the superclass:
In this diagram, Person is the superclass and contains the instance variables and methods that
both Student and Faculty require. Those variables and methods are inherited into the subclasses
Student and Faculty. For example, an instance of Student will have four instance variables:
name, address, currentClasses, and completedClasses. That instance will also have
implementations of three methods: scheduleCourse(), changeAddress(), and calculateGPA().
207
While the concept of sets is one way of looking at classes, sometimes other perspectives are more
insightful. The concept of inheritance comes from the familial relationships that cause
inheritance in people. The superclass is sometimes called the “parent” and the subclass the
“child” and functionality is inherited from parent to child. As the depth of inheritance increases,
we can talk about ancestors that descendents inherit from. In our example, Person is the parent
class, Student and Faculty are the child classes, and functionality is inherited from parent to child.
Another perspective has to do with the specificity of a class. As we move down the inheritance
hierarchy (from superclasses to subclasses), the concepts that classes represent become more
specific. Subclasses are specializations of their superclasses and superclasses are
generalizations of their subclasses. In our example, Student and Faculty are both more specific
than Person, so they are specializations of Person. Similarly, Person is a generalization that
includes both students and faculty.
In Java, we mark a class as a subclass by adding an extends clause to the declaration of the class.
So, for example, the declaration of the Student class in our example would be:
public class Student extends Person
Now, we can explain something about exceptions that we ignored in Lab 11. When we declared
our exception class, it looked like this:
public class ZipCodeException extends Exception
That means that our class, ZipCodeException, was a subclass of Exception which is provided by
the Java API. So, our class inherited everything Exception contained and then we added a small
bit of functionality to our class. In fact, the throw statement requires that the object you throw to
be an instance of either Exception or a descendent of Exception.
Creating our Hierarchy
In our humans and aliens case, since humans and aliens share some characteristics,
they are both subsets of a larger class that we’ll call “life forms.” At this point, we
don’t know much about humans except that they can’t recover like Aliens. We’ll find
out more about humans soon, but, for now, we just know that they need life points
and that being hit has the same effect on humans and aliens. Therefore, we will
create a superclass called LifeForm that will contain the functionality that humans
and aliens share. For now, this is our class diagram:
208
Notice that both humans and aliens will inherit the life points and the methods
related to that functionality from LifeForm. The ability to recover is unique to
aliens, so we’ll leave that where it is. We’re going to refactor our current code and
tests to match this structure before we add any more functionality.
Since we always start with our tests, we need to start by thinking about how this
change in our code should change our tests. Since Human and Alien are both
inheriting functionality from LifeForm, we need to make sure that the tests for
Human and Alien also test that functionality. The easiest way to achieve that is to
make the architecture of the tests match the architecture of the code. So, we’ll
have a TestLifeForm class that contains the tests for the functionality we put into
LifeForm.
Refactoring things to match this new structure requires some care, so follow these
instructions carefully. First, create a TestLifeForm class to hold the shared tests.
Since Alien is a subclass of LifeForm, TestAlien should be a subclass of
TestLifeForm. To do that, make your declaration of TestAlien look like this:
public class TestAlien extends TestLifeForm
There is that extends keyword that we first saw when we created our
ZipCodeException class in Lab 11! Now we can understand what we were doing! Our
ZipCodeException class was a subclass of Exception. We had to do that because
the throw statement requires an instance of Exception. By making our class a
subclass of Exception, we made our instances also be instances of Exception.
Now that Eclipse knows the relationship between TestAlien and TestLifeForm, it
will help with our refactoring. The first thing we want to do is to move
testHitHurtsRandomly() up into TestLifeForm. To do this, click on the name of the
209
method and then right click and select Refactor -> Pull Up. That will cause this
dialog to be displayed:
Since we only want to move hitHurtsRandomly(), just click Finish. That will move
the method from TestAlien into TestLifeForm. Run your tests, they should still be
GREEN.
Even though our tests are green, hitHurtsRandomly() isn’t ready to test humans; in
the middle of the test, it is creating instances of Alien. We need to put the
creation of that object into the subclasses, TestAlien and TestHuman (when we
create it), so that when it gets inherited into each of those classes, it will test the
appropriate type of life form. To do that, we want to change this line in
hitHurtsRandomly():
Alien jarjar = new Alien(50);
to
LifeForm jarjar = createLifeForm(50);
The idea behind this is that TestAlien and TestHuman can each have their own
implementation of createLifeForm() and create objects of the type appropriate to
each set of tests. So, when hitHurtsCorrectly() is inherited into TestAlien, the
createLifeForm() implementation in that class will be called and jarjar will be an
instance of Alien. Similarly, when hitHurtsCorrectly() is inherited into TestHuman,
jarjar will be an instance of Human.
210
This strategy is an example of a technique called polymorphism in which a variable
(jarjar) is declared to be of one type (LifeForm) but actually refers to a variable of
a subclass of that type.
Now that we’ve made that change, getting things to compile is going to take a little
more work. TestLifeForm needs to know the method signature for
createLifeForm(), but we want the actual implementation to be in the subclasses.
To achieve this, we declare the method with the abstract modifier in
TestLifeForm:
/**
* Create a life form appropriate to the subclass we are
* testing
* @param points the number of life points the life form
* should be given
* @return the new object
*/
public abstract LifeForm createLifeForm(int points);
This declaration lets the compiler know the details of the parameters and return
type of this method and requires that the subclasses of TestLifeForm must have a
method matching this signature that is not abstract.
The creation of that abstract method means that we can no longer create instance
of TestLifeForm. This should be expected, since we didn’t expect to run the tests
inTestLifeForm directly; we only want to inherit them into TestAlien and
TestHuman. However, the compiler needs to be sure that we won’t create instances
of TestLifeForm, so we must mark its declaration as abstract, too:
public abstract class TestLifeForm
Notice that we used the keyword abstract in two different, but related, ways:
•
•
When it modifies a method, that method has no implementation in the
current class and must be implemented in all of its subclasses.
When it modifies a class, it marks that class as not being instantiable (we
cannot create instances of that class).
Since we made createLifeForm() abstract, a syntax error in TestAlien states that
it must contain an implementation of that method. You can fix that by adding this
to TestAlien:
211
/**
*@see TestLifeForm#createLifeForm(int)
*/
public LifeForm createLifeForm(int points)
{
return new Alien(points);
}
The “@see” block tag in the javadoc comment shows that this method is related to
the method in the superclass. If we generated the javadoc comments, it would
create a hyperlink to the documentation of the related method, so they essentially
share a comment.
At this point, we still have two syntax errors in TestLifeForm stating that methods
aren’t defined. We have changed jarjar’s type to be LifeForm, so the compiler is
expecting those methods to be in that class. In fact, this is the primary change we
were trying to make: moving the functionality related to life points into LifeForm so
that it can be inherited into both Human and Alien. Use Refactor->Pull Up in the
Alien class to move these things up into LifeForm:
•
•
•
lifePoints (instance variable)
getLifePoints()
takeHit()
Eclipse may give you a warning about this refactoring because we have already made
changes related to it in TestLifeForm. Don’t worry – let it do the refactoring any
way.
Notice that we are leaving recover() in Alien because humans can’t recover; it’s a
behavior that is specific to aliens – not general to all life forms.
Run the tests in TestAlien. They should still be GREEN and you should see that all
three pass, including testHitsHurtRandomly() which is being inherited from
TestLifeForm.
There’s an interesting observation about what we have just done. We have made a
significant change to the architecture of our system using this philosophy: “Change
the architecture of the tests, then let the syntax errors tell you what else needs
to change.” In some cases, that is followed by “let the failing tests lead you
through the final cleanup.” Since our tests are still green, we know we haven’t
broken anything even though the changes that we made reorganized our code
dramatically.
Let’s do one more thing to be sure that we understand how the tests are working.
Put a breakpoint on the line that declares jarjar in testHitHurtsRandomly() and run
the tests in the debugger. Click “Step Into” and you’ll see that you are in the
createLifeForm implementation in TestAlien. Click “Step Over” until you are back in
testHitHurtsRandomly(). Click “Step Over” one more time to get to the next line.
212
Look at the variable jarjar in the variables tab: Notice that the object is actually
of type “Alien” even though jarjar is declared to be a LifeForm. That is our
polymorphism: the object is an instance of a subclass while the variable referring to
it is declared to have the type of the superclass.
Start Making Humans
Now we are ready to start building the Human class. As always, we start with a
test. Create a TestHuman class and add this test to it:
@Test
public void testCreation()
{
Human wellington = new Human(100);
assertEquals(100, wellington.getLifePoints());
}
You can compile this by creating the Human class, making it extend LifeForm (so
that it is a subclass of LifeForm), and creating an empty constructor with an int
parameter. Run the test and you should see RED.
To make this test pass, you only have to build the body of the constructor.
Remember, Human is inheriting the lifePoints instance variable and the
getLifePoints() and takeHit() methods from LifeForm, so you don’t have to declare
them again. Run the test to get to GREEN. REFACTOR as necessary.
What About Taking Hits?
The JUnit frame shows that running TestHuman runs one test: testCreation().
However, our Human class is inheriting more functionality from LifeForm that isn’t
being tested. Just like with TestAlien, we need to make TestHuman be a subclass
of TestLifeForm. Make TestHuman extendTestLifeForm. That should cause a
syntax error because TestHuman doesn’t contain an implementation of the abstract
method createLifeForm(). Fix the problem by doing something similar to what we
did in TestAlien (but create a human instead of an alien). Running the
TestHumantests should now cause two tests to be GREEN.
Let’s watch how life forms are being created again. Put the breakpoint at the
creation of jarjar in testHitHurtsRandomly() again. Click “Step Into” and you’ll see
that this time you are in the createLifeForm() method in the TestHuman class.
Stepping over until you are back in testHitsHurtsRandomly() and on the line after
jarjar’s declaration, you’ll see that this time, the object jarjar is referring to
Human instead of Alien. So, by putting createLifeForm() into the test subclasses,
we are making sure that the test we inherit from TestLifeForm is really testing the
functionality within the different subclasses.
213
Test Suites
Now that we are working on a number of related classes, we’d like to be able to run
all of the tests at once even though they are in different test classes. We can do
that by creating a test suite containing all of the test classes we’d like to run.
Create a class called AllTests and make it look like this:
import org.junit.runner.RunWith;
import org.junit.runners.Suite;
/**
* @author Merlin
*/
@RunWith(Suite.class)
@Suite.SuiteClasses(
{
TestAlien.class,
TestHuman.class
})
public class AllTests
{
}
Running that class as a JUnit test will cause all of the tests in both test classes to
be run. You should see five tests run. Expanding the list of tests will show you
both test classes and the tests inside of them. For the rest of the lab, GREEN
means that ALL of the tests are green.
Weapons
The next piece of functionality we will to add to our system is the ability for any
life form to pick up a weapon. Here’s a test that should be put into TestLifeForm,
since we want all life forms to have weapons.
@Test
public void testWeaponStrength()
{
LifeForm it = createLifeForm(50);
assertEquals(0, it.getCurrentWeaponStrength());
it.pickUpWeapon(15);
assertEquals(15, it.getCurrentWeaponStrength());
}
Make it compile to get to RED. Notice that, since that test has been inherited into
both TestAlien and TestHuman, two tests are failing. In LifeForm, make an
instance variable named currentWeaponStrength that stores the value passed into
pickUpWeapon(). Fill in getCurrentWeaponStrength() and all of your tests should
be GREEN.
214
Let’s Pick Up Carefully
Picking up a weapon that is weaker than the one we are holding wouldn’t be very
bright. Let’s make a test to show that we won’t do that:
@Test
public void pickUpWeakerWeapon()
{
LifeForm it = createLifeForm(50);
it.pickUpWeapon(15);
it.pickUpWeapon(14);
assertEquals(15, it.getCurrentWeaponStrength());
}
RED, GREEN, and REFACTOR.
Using Weapons
In this game, all weapons loose some strength as they are wielded. Think of them
like phasers that lose power with each shot that we take. Let’s write a test that
verifies that our weapon strength decreases by one each time we take a shot:
@Test
public void testShooting()
{
LifeForm it = createLifeForm(50);
LifeForm victim = createLifeForm(30);
it.pickUpWeapon(12);
it.shoot(victim);
assertEquals(11, it.getCurrentWeaponStrength());
}
This test is verifying that, when a weapon fires, its weapon strength decreases by
one. Make it compile and fail to get to RED. Then write the code to get to GREEN
and REFACTOR.
This test has a significant weakness. We are passing a life form into shoot()
because that method should actually shoot the victim. In other words, the shoot()
method fires the weapon at the victim and then decrements the current weapon’s
strength. However, we are not checking to make sure that we actually shot at the
victim. The problem is that it isn’t obvious how we could verify that the victim was
shot. Remember, takeHit() randomizes the amount of damage a hit causes, so
sometimes hits cause no damage at all. That means that we can’t verify a shot by
verifying that the victim was wounded. Suppose we had a method called
getNumberHitsTaken() that counted how many times a life form was hit. That
would let us verify that shoot() hit the victim. However, we should start by building
a test for that new method:
215
@Test
public void testHitCount()
{
LifeForm it = createLifeForm(30);
assertEquals(0, it.getNumberHitsTaken());
it.takeHit(5);
assertEquals(1, it.getNumberHitsTaken());
it.takeHit(5);
assertEquals(2, it.getNumberHitsTaken());
}
RED, GREEN, and REFACTOR
Now that getNumberHitsTaken() works, we can strengthen our testShooting()
method be adding this assert to it:
assertEquals(1, victim.getNumberHitsTaken());
While that will compile right away, running it should get you to RED. Making shoot()
call the victim’s takeHit() method should make everything GREEN. REFACTOR.
Can’t Shoot Without a Weapon
The border case for shooting is when the weapon has no strength (or we haven’t
picked one up, yet). We need a test that verifies that taking a shot with no weapon
strength doesn’t hurt the victim and doesn’t decrease weapon strength below zero.
@Test
public void testShootingNoWeaponStrength()
{
LifeForm it = createLifeForm(50);
LifeForm victim = createLifeForm(30);
it.shoot(victim);
assertEquals(0, it.getCurrentWeaponStrength());
assertEquals(0, victim.getNumberHitsTaken());
}
That should be RED. Make it GREEN and REFACTOR.
Overriding Methods
Sometimes, a subclass needs to modify one of the methods that it inherits from the superclass.
This can be accomplished by declaring a method with the exact same signature in the subclass.
This is called overriding the method and, when the method is called on an instance of the
subclass, the subclass’s method will be executed.
As an example of overriding methods, consider a system that is tracking students at a university.
In this example, there are three types of students: Undergraduates, Gradate Students, and
Extended Studies Students (who are not enrolled in degree programs). These types share much of
their functionality, so they are subclasses of the superclass Student.
In general, students need to be able to calculate their grade point average. All students are
allowed to retake courses in which they received ‘F’ grades. However, for undergraduate students
only, the first two ‘F’s they receive will be dropped from their GPA calculation if their retakes
grades are improvements. In this case, since most types of students use a simple GPA
216
calculation, we would build a method called calculateGPA() that would return a double in the
Student class and allow it to be inherited into both GraduateStudent and ExtendedStudiesStudent.
Since the calculation is different for undergrads, we would override the calculateGPA() method
in the UndergraduateStudent class and make the calculation there include dropping re-taken ‘F’s
appropriately.
In order for polymorphism to work, we have to be very careful when we override methods to
ensure that these characteristics are met:
•
•
the method in the subclass can only return values that the method in the superclass
returns
any parameter value that is accepted by the method in the superclass must be accepted by
the method in the subclass.
In our example, suppose the calculateGPA() method in the superclass is defined to be returning a
real number between 0 and 4.0. There may be code that uses Student that depends on the return
value being in that range. Therefore, allowing calculateGPA() to return values larger than 4.0 if
we were weighting honors courses, would break the substitution principle since those larger
values are not legitimate return values for the superclass. If we needed to change the range, we
would have to change it for the superclass by making tests that verified that the new, larger range
was handled properly by the classes that use Student.
If we obey these rules, our architecture will obey the substitution principle that requires that an
instance of the subclass must always be able to be substituted for an instance of the superclass.
Humans Are Smarter Than Aliens
Aliens may have the ability to recover, but humans invent things that make us
stronger. In this case, humans have developed backpacks that hold up to ten
weapons. That way, when the weapon we are currently shooting gets weak, we can
trade it with a stronger one we picked up in the past. Here’s a summary of how this
changes humans:
-
when we pick up a weapon, if it’s weaker than our current weapon, we put it in
our backpack. If it’s stronger than our current weapon, we hold it and put
our current weapon into the backpack.
-
we need a new method that finds the strongest weapon in the backpack and
swap it with our current weapon, if it is stronger than the current weapon.
Let’s address these one at a time. First, let’s work on picking up a weapon. Notice
that we’re going to be changing the behavior of an existing method we inherited
from LifeForm: pickUpWeapon(). The test for this method is
pickUpWeakerWeapon in TestLifeForm. Since we are changing that functionality,
we’ll also have to override that method in TestHuman. Add this test to TestHuman:
217
/**
*@see TestLifeForm#pickUpWeakerWeapon()
*/
@Test
public void pickUpWeakerWeapon()
{
Human wellington = new Human(100);
assertEquals(0, wellington.numberWeaponsInBackPack());
wellington.pickUpWeapon(15);
wellington.pickUpWeapon(14);
assertEquals(15, wellington.getCurrentWeaponStrength());
assertEquals(1, wellington.numberWeaponsInBackPack());
assertTrue(wellington.isCarryingWeapon(14));
}
Write the simplest code you can in order to get this to compile and then watch it
fail to get to RED. Getting it to pass will require that you add an instance variable
to Human to be the backpack. At this point, the backpack only needs to hold one
weapons’ strength, so just make it be an int.
In order to make this test pass, the Human class is going to have to modify the
value of currentWeaponStrength which is an instance variable it inherited from
LifeForm. Following good information hiding principles, you should have marked that
variable as private. (Fix the other ones if you didn’t!) Using the private modifier
means that only the code in the declaring class can access that variable. Since we
need to see it in the subclass, we’re going to have to change that. However, we
don’t have to give access to everyone. There is a third visibility modifier that
would be more appropriate: protected. Marking a variable (or method) as
protected means that it can be seen by subclasses, but can still be hidden from
other parts of the system.
Use the test to help you address the problem by working on getting one assert to
pass at a time. Remember that you only have to make the test pass – don’t worry
about holding lots of weapons. Get to GREEN and REFACTOR.
Picking Up More Weapons
We’ve said that backpacks can hold up to ten weapons, but our current test only
checks that it can hold one. Here’s a test that will put more than one weapon into
the backpack:
218
@Test
public void pickUpTwoWeakerWeapons()
{
Human wellington = new Human(100);
wellington.pickUpWeapon(15);
wellington.pickUpWeapon(14);
wellington.pickUpWeapon(12);
assertEquals(15,wellington.getCurrentWeaponStrength();
assertEquals(2, wellington.numberWeaponsInBackPack());
assertTrue(wellington.isCarryingWeapon(14));
assertTrue(wellington.isCarryingWeapon(12));
}
If you really wrote the simplest code for the last test, then this one will be RED.
In order to get to GREEN, you’re going to have to refactor the backpack to be an
array of integers. Again, make it pass one assert at a time. Don’t forget to
REFACTOR when it works.
Another Border Case
The border case for our backpack is when we try to pick up a weapon that would
cause us to overfill the backpack. Write a test for that case assuming that, if our
backpack is full and the new weapon is stronger that our current one, we pick it up
and drop our current weapon. If our backpack is full and the new weapon is weaker
than our current one, we just don’t pick it up. (For this lab, we will ignore the
situation that we could check the new weapon against all of the weapons in our
backpack and drop the weakest one.)
RED, GREEN, and REFACTOR
Switching Weapons
As humans pick up weapons and shoot at aliens, they will want to swap their current
weapon with the strongest on in the backpack. This is implemented in a method
called swapBestWeapon() which has this functionality:
•
•
if the backpack is empty or the current weapon is stronger than anything in
the backpack, keep the current weapon
otherwise, swap the current weapon with the strongest weapon in the back
pack
Write appropriate tests for this method and build the code to make them pass.
219
Vocabulary
abstract class
inheritance
protected
abstract method
is-a
specialization
child class
override
subclass
extends
parent class
superclass
generalization
polymorphism
Lab Questions
Content Questions
1.
2.
3.
4.
5.
6.
7.
8.
What is inheritance and why do we use it?
How do you know which class is the superclass and which is the subclass?
How do we denote inheritance in the code?
How do we structure the tests to make sure that we test everything that is inherited in
addition to the code in the subclass?
What does the keyword abstract do?
What is overriding and when do we do it?
What is the substitution principle?
What is polymorphism?
220
Glossary
Abstract Class – A class for which no instances can be created.
Abstract Method – A method with no body whose definition is left to concrete subclasses.
Accessor – A getter or a setter. A method whose purpose is to provide access to private variables.
Array - A data structure that can hold a fixed number of values of the same type.
ASCII - A mapping from integers to characters that includes predominantly English characters.
Assembler – A piece of software that translates programs written in an assembly language into
the language of a target machine.
Assembly language – A programming language where one statement in the language translates
to exactly one machine instruction.
Assignment statement – A Java statement that stores a value into a variable. It is of the form
<variable name> = <expression>;
where the left hand side of the equals sign is the variable whose value is being changed and, when
evaluated, the expression will give the new value that variable is to hold.
Average case analysis – Run-time analysis based on random input.
Benchmarking – Measuring the elapsed run-time of a specific application on a specific machine.
Best case analysis – Run-time analysis based on input that would minimize that run-time.
Big-Oh – A notation for describing run-time that means the run-time is no worse than a given
function.
BigInteger – A class provided by the Java API that can store arbitrarily large integers.
Block tag – A marker beginning with ‘@’ that mark portions of javadoc comments so they can
be formatted into web pages.
boolean – A primitive type that only holds the values true or false.
Border Case – A situation where a small change in input causes a fundamentally different
behavior.
Breakpoint – A way to mark a line of code so the debugger will stop immediately prior to that
line’s execution.
Bubble sort – A sorting algorithm where neighbors are swapped and the large values move
toward their correct positions.
byte – A primitive type that holds integer values from -128 to 127.
Camel capitalization – Capitalizing the first letter of every word except the first word.
char - A primitive type that holds a character.
Child Class – see subclass.
Class – 1) A type we are declaring. 2) The set of objects of a particular type. 3) The java code
defining the class.
Class Diagram – One of the UML diagrams that is used to show the structure of the classes in a
system.
221
Comments – Sections of source code that are meant to help explain the code to people and are
ignored by the compiler.
Compiler – A piece of software that translates programs written in high level languages into the
language of the target machine.
Conditional Statement - A language construct that allows the behavior of the system to change
based on the values of variables.
Constant – A variable whose value cannot be changed.
Constructor – A method whose purpose is to allocate the space for and initialize an object.
Count-controlled loop – A loop that will run a specific number of times.
Dangling else problem – The problem of determining with which if statement an else statement
will be paired.
Debugger – A tool that allows the developer to control the execution of the program allowing
him to watch the values of variables and how they are affected by the code.
Default Constructor – If you do not code a constructor, Java gives you a constructor with no
parameters that just allocates the space for the object.
double - A primitive type that holds a real value with higher precision.
Else block – The statement or block of code following the “else” keyword.
Design – The parts of the system and how they interact with each other.
Exception – (1) a situation that does not normally occur and requires special handling. (2) A
class provided by Java that holds information description an exception that has occurred. (3) An
instance of the class Exception.
Fibonacci Numbers – The sequence of numbers: 1, 1, 2, 3, 5, 8, 13, … where each number is the
sum of the two previous numbers.
Fisher-Yates Shuffle – An algorithm that randomizes the order of a set of objects.
float - A primitive type that hold a real values.
Frame bit – A bit at the beginning or ending of a bar code whose purpose is to allow the machine
reading to bar code to orient the bar code for reading.
Generalization – Superclass
Getter - A method whose purpose is the return the value stored in an instance variable.
Hard-coding - Embedding a specific value throughout the code.
High Level Language – A language where one source code instruction may be translated into
many machine instructions.
Increment - To make larger by 1.
Index – Used as a noun, this is an integer specifying a position in an array. Used as a verb, it is
the act of locating that position.
Infinite Loop – A loop that will never finish running because its condition will never be false.
Information Hiding – The principle of minimizing the portion of the system that can directly
access a variable or method.
222
Inheritance – The process of defining new classes such that they contain functionality of existing
classes.
Initializer – A statement that declares a variable and gives it a value.
Instance – One member of a class.
Instance variable – A variable associated with an object. Each object of a type gets its own copy
of that type’s instance variables.
Insertion sort – A sorting algorithm where the leftmost value in the unsorted portion of the array
gets iteratively swapped to move left until it is in the correct position of the sorted portion.
int - A primitive type that holds an integer value from -2,147,438,648 to 2,147,438,647.
Integrated Development Environment – A tool that combines a variety of tools developer need
including editors, compilers, debuggers, and test environments.
Interpreted – When the translation to machine language is done as the execution of the program
requires the code.
Java API – The Java Applications Programming Interface that specifies the wide variety of
classes that come with the Java platform.
Java Byte Code – The language that is the output of the Java compiler and that runs on Java
Virtual Machines.
Java Virtual Machine – A piece of software that executes Java Byte Code on a particular type of
hardware.
Javadoc – The tool which converts javadoc comments into html design documentation.
Javadoc Comment – A comment in a specific format so that it can be translated into design
documentation.
Keyword – A word that has special meaning to the Java compiler (also called a reserved word).
Local Variable – A variable declared within a method.
long - A primitive type that holds an integer value from -9,223,372,036,854,775,808 to
9,223,372,036,854,775,807.
Loops – Language constructs that allow a section of code to be executed repeatedly.
Machine Language – The language that the processor of the machine executes.
Mantissa – the fractional part of the scientific notation used to store real numbers.
Memory Diagram – A diagram that shows how a program will store information in variables.
Method – A named block of code.
Method Signature – the name of the method and the order and types of the parameters in its
parameter list.
Naming Convention – Guideline for naming entities to support consistency.
Nested Conditionals – When one if statement is contained within either the then block or the else
block of another if statement.
New Statement – A Java statement used to allocate the space for an array or to call a constructor
of an object.
Object – An entity the system needs to know about. A member of a class.
223
Object Code – The output of a compiler or assembler.
Object-Oriented – A type of language which supports the ability to code designs that are based
on the objects the system needs to manage.
Output – The results of running a program.
Overflow – A condition that occurs when a value being stored cannot fit in the variable into
which it is being stored.
Overload – To declare two methods with the same name.
Override – Declaring a method in a subclass that is also declared in the superclass.
Package – A set of related classes.
Parameter – A variable declared in a method’s declaration into which values are passed when
the method is called.
Parent Class – See “Super class”
Parity bit – A bit that is added to a value to provide the ability to detect errors in reading the
value.
Polymorphism – Declaring a variable to have one type while it refers to an object of a subclass
of that type.
Precedence – The order in which arithmetic operations are applied.
Primitive type – A data type that the java compiler knows about so that it can allocate the space
for the variable at the declaration.
Prompt – Output that is designed to ask the user to do something.
Protected – A visibility modifier that makes an instance variable or a method visible to a
subclass, but not the entire system.
Reference type – Any non-primitive type, including arrays.
Response time – The elapsed time between when a request is made and when the system
responds.
Reserved word – A word given specific meaning by the Java compiler (also called a keyword)
Return Type – The data type of the value that a method will return.
Runnable - Contains a main() method and can therefore be run by the Java Virtual Machine.
Run-time analysis – Theoretical, machine-independent, analysis of the amount of time an
algorithm will take as a function of the size of the input.
Scanner – A class provided by Java that allows us to get keyboard input from the user.
Selection sort – A sorting algorithm in which one swaps the largest value in the unsorted portion
into the last position of the unsorted portion of the array increasing the sorted portion.
Source code – A program written in either a high-level language or assembly language.
Sentinel-controlled loop - A loop that runs until a given condition becomes true.
Setter – A method whose purpose is the change the value of an instance method.
short – A primitive type that holds an integer value from -32768 to 32767.
224
Sign bit – A boolean value stored with a number to denote whether that number is positive or
negative.
Sorting problem – Given a set of objects (or primitives) in random order, rearrange them until
they are in increasing order.
Specialization – A subclass.
String – A class that comes with Java that can hold an arbitrarily long sequence of characters.
Strongly Typed – Languages of this type require that variables must be given specific data types
and that the compiler will verify that the data being stored in a variable matches the variable’s
type.
Syntax – The grammatical rules of a language.
Subclass – A class that inherits functionality from another class.
Superclass – A class from which another class inherits.
Target Machine – The machine on which a program will be run. This can be either a physical
machine or a virtual machine.
Then block – The statement or block of code following the condition in an if statement.
token – the input between two delimiters.
toString() – The method that will be called whenever a conversion from an object to a String is
required.
throw – A statement that is used when an exception condition has been detected and handling of
that exception is being passed to the calling method.
throws – A clause that is added to a method declaration to denote the fact that it might throw
exceptions of the specified types.
Try/catch – A Java construct that supports handling of exceptions.
Typecast – To force the compiler to change the type of an expression by preceding it with the
new type in parentheses.
UML – Unified Modeling Language
Unicode – A mapping from integers to characters that includes characters from most languages.
Unified Modeling Language – A standard set of types of diagrams that are used to document
various aspects of the design of a system.
UTF-8 – Unicode encodings using only eight bits for compatibility with ASCII.
Variable – A named location that can store a value of a particular type.
void – The return type used in a method declaration when that method will not return a value.
While Loop – A Java construct that allows a block of code to be executed repeatedly until a
given condition becomes false.
225
Index
@author ..................................................................... 86 @Before .........................................................106, 125 @Ignore ...................................................................178 @param........................................................................ 86 @return ..................................................................... 86 @see.................................................................... 86, 209 @Test........................................................................... 95 inheritance ..........................................................204 comment..................................................................9, 11 javadoc .....................................................................86 compiler.......................................................................... 7 conditional ....................................................................19 conditional statement..............................................114 constant ................................................................ 28, 61 constructor ................................................55, 59, 174 count-­‐controlled loop ..........................................128 A D accessor........................................................................ 77 algorithm ........................................................................ 6 ancestor .....................................................................205 annotation..................................................................... 95 array ........................................................................ 23, 24 allocation................................................................ 25 declaration ............................................................ 24 index......................................................................... 24 initializer ................................................................ 57 memory diagram ................................................ 24 array initializer .......................................................180 Arrays of Objects....................................................163 ASCII ............................................................................130 assembler ....................................................................... 7 assembly language ..................................................... 7 assertEquals() ............................................................. 95 assertFalse()................................................................ 98 assignment statement............................................ 15 average case analysis ...........................................157 Dangling Else ...........................................................116 debug perspective .......................................................12 debugger.......................................................................11 default constructor ..................................................55 descendent ...............................................................205 design............................................................................51 do…while loop......................................................124 double .................................................................... 15, 80 @ B benchmarking .........................................................148 best case analysis...................................................159 BigInteger.............................................................. 85, 89 Big-­‐Oh .........................................................................148 block tag....................................................................... 86 boolean.................................................................. 15, 80 border case ................................ 113, 175, 202, 216 break statement...................................................136 breakpoint ........................................................... 11, 58 Bubble Sort ...............................................................144 byte ................................................................................ 80 C camel capitalization................................................ 14 char ................................................................................ 80 check digit .................................................................173 class ................................................................ 51, 52, 61 class diagram ...........................................................161 E exception ...................................................................188 catching ................................................................195 exceptions testing for ..............................................................189 expected ................................................................189 exponent.......................................................................80 F fibonacci number......................................................74 final............................................................................28 float.................................................................................80 for loop................................................... 25, 128, 136 nested.........................................................................66 For Loop Variable ..................................................103 frame bit ....................................................................172 G generalization .........................................................205 getter ....................................................................... 76, 77 H hard-­‐coding .................................................................28 high-­‐level language .................................................... 7 I IDE..................................................................................... 8 if statement.................................................................19 increment.....................................................................26 index...............................................................................24 226
infinite loop ............................................................132 information hiding ........................................... 74, 76 inheritance................................................................203 initializer ..................................................................... 56 array............................................................... 57, 180 Insertions Sort.........................................................154 instance ........................................................................ 51 instance variable .....................................51, 52, 102 int............................................................................. 15, 80 Integrated Development Environment ............. 8 interpret.......................................................................... 8 J Java API ............................................................128, 139 Java Application Programming Interface ...... 85 Java byte code............................................................... 8 Java Virtual Machine.................................................. 8 javadoc.......................................................................... 86 javadoc comment.............................................. 86, 87 JUnit ............................................................................... 93 test creation .......................................................... 94 JVM .................................................................................... 8 K keywords ...................................................................... 9 L lifetime ......................................................................101 Local Variable..........................................................102 long ......................................................................... 80, 82 loop count-­‐controlled ...............................................128 sentinel-­‐controlled ..........................................131 M mantissa....................................................................... 80 Math.random() ................................................... 22 memory diagram............................................... 14, 15 array.................................................................. 24, 56 array of objects..................................................163 object creation ..................................................... 56 object declaration............................................... 54 primitive types .................................................... 87 reference types.................................................... 88 String......................................................................172 method .......................................................... 37, 49, 61 overriding ............................................................213 parameters ............................................................ 40 return type ............................................................ 42 void return type................................................ 42 Method Overloading...............................................185 Method Parameter ................................................103 method signature...................................................185 mod operator.............................................................176 N naming convention .......................................... 14, 61 methods...................................................................49 new statement.................................................... 24, 87 O object..............................................................................51 object code..................................................................... 8 object-­‐oriented ..........................................................51 operators ........................................................................31 output....................................................... 18, 60, 62, 69 overflow................................................................ 80, 81 overloading the method......................................185 overriding .................................................................213 P package .........................................................................86 parameter ......................................................... 40, 103 parity bit ....................................................................173 polymorphism ...........................................................208 porting............................................................................. 8 precedence rule.........................................................31 primitive types .................................................. 87, 147 private modifier .................................................74 problem decomposition............................119, 149 prompt .........................................................................193 protected modifier .........................................215 public modifier....................................................74 R refactor............................................................... 93, 178 reference types.................................................. 87, 147 reserved words ......................................................... 9 response time..........................................................143 return statement........................................ 43, 136 return type...................................................................42 runnable .............................................................. 57, 192 run-­‐time analysis ...................................................148 S Scanner.......................................................................193 scope .................................................................101, 147 Selection Sort...........................................................149 sentinel-­‐controlled loop .....................................131 setter........................................................................ 76, 77 short ...............................................................................80 shuffle Fisher-Yates ............................................................69 sign bit ...........................................................................80 sorting problem......................................................143 specialization...........................................................205 string ...................................................................... 60, 62 charAt().................................................................128 contains() .............................................................138 227
equals() .................................................................138 equalsIgnoreCase()...........................................139 length()..................................................................129 substring() ...........................................................139 strongly typed ........................................................... 40 subclass ......................................................................204 substitution principle ..........................................214 superclass..................................................................204 symbol ! 133 && ..................................................................133, 134 % 176 || ..................................................................133, 134 syntax.............................................................................. 9 syntax error................................................................... 9 T target machine ............................................................. 7 TDD .......................................... 93, 98, 100, 109, 111 Test Driven Development .................................... 93 test suite ......................................................................211 this............................................................................105 throw statement ...................................................188 throws clause...........................................................189 token ...........................................................................193 toString() .. 60, 69, 162, 163, 170, 174, 175 try/catch block...............................................195 typecast.........................................................................22 U UML..............................................................................161 Unicode .............................................................. 80, 130 Unified Modeling Language...............................161 UTF-­‐8 ..........................................................................130 V variable ................................................................. 14, 61 instance variable.......................................... 51, 52 overflow ..................................................................80 scope ......................................................................101 sizes............................................................................79 types..........................................................................15 visibility modifier .......................................................74 void...............................................................................42 W while loop ..................................................................16 228