In past exercises, we have dealt with a sequence of values by processing the values one at a time:
In this lab exercise, we will examine a new kind of object called a list. A list stores not just one value, but an entire sequence of values. A list is a subscripted container (a.k.a. an indexed container): the values within a list can be accessed via a subscript (a.k.a. an index).
Java uses zero-based indexing. That is, if we
have a list named list
holding n elements, the
first element of the list has index 0; the last element has index
n-1.
Java provides a built-in array type which behaves a lot like a primitive and an object. However, it suffers from one major drawback: it has a fixed size. You have to know in advance how much data you need to store before you create the array. Fortunately, Java's implementation of a list has the same functionality of an array and rich library support, plus the ability to grow and shrink as needed.
In this lab exercise you will write a program that processes the names and scores of students in a course. In the first stage, you'll write the code to compute sums, averages, and standard deviations of the grades; you will also write a parser to read in a complete gradebook. In the second stage, you'll modify the code to assign letter grades (A, B, C, D or F) based on the scores, curving the scale to match the actual grades received by the students.
As usual, you will use object-centered design and JUnit testing.
Do this...
edu.institution
.username
.hotj.lab09
Exercise Questions/lab09.txt
.Design/lab09.txt
.GradeBook
.GradeBookParser
.Student
, StudentTest
,
GradeBookTest
, and
GradeBookCLIDriver
.Student
ClassThe Student
class is ready to store student names
and grades so that a gradebook can compute summary data (like the
average and standard deviation of the grades). You will have to
modify this class later on, so it's worthwhile examining its design
now.
A Student
object has two attributes:
String
.double
.Both attributes have been declared as instance variables in the
Student
class.
The only behavior we need from this class now are the basics for any object-generating class:
toString()
method to return the name and grade
as a single String
, useful for printing a
Student
object.The StudentTest
class is not much different than
the other test-case classes you've worked on before. Since the
Student
class is relatively simple, its test-case
class is also relatively simple. One test method tests the
getName()
accessor; another test method tests the
getGrade()
accessor; the last test method tests the
toString()
method. You have to provide the expected
values for this last method, so look at the definition of
Student#toString()
to figure out the right values.
Do this...
Fix the compilation errors in
StudentTest
by replacing the ???
s with
good values. All of the code should then compile successfully. Unit tests
should green bar when run.
If you want to compute an average grade or to assign letter grades, obviously your program has to read the grades in. But is going through the grades once enough? Once was enough in Lab #5 when you computed taxes for purchases, why not for this grade problem as well?
In order to assign letter grades, you need both the average and the standard deviation. The standard deviation requires the average. Each of these three tasks (computing the average, computing the standard deviation, and assigning letter grades) requires processing each individual student. So it appears we have three separate steps that must be ordered like so:
You could ask your user to enter the names and grades three times, but that's a most unacceptable solution.
A list, on the other hand, will store the values you read in so that you can process the values as many times as needed. The user only enters the data once, and the three tasks can be done by processing the students in the list. Consequently, a list will be the main attribute of what we'll call a "gradebook".
We start with the main driver, thinking in terms of this "gradebook".
Behavior of GradeBookDriver#main(String[])
The driver should display a greeting. It should read in a gradebook of students (probably from the keyboard). It will ask the gradebook for the average grade, for the standard deviation of the grades, and for a complete report of all students and their assigned letter grades.
These are the objects in the behavior description:
Objects of GradeBookDriver#main(String[])
Description Type Kind Name the screen Screen
variable theScreen
the keyboard Keyboard
variable theKeyboard
a gradebook parser GradeBookParser
variable parser
the gradebook GradeBook
variable theGradebook
A parser is usually a component of a compiler which translates the individual words of your program into a structure that a computer program can process more easily. However, the term "parser" can be used in a more general sense of reading in some input and turning it into a useful object---this is what the behavior paragraph hints at.
According to this list of objects, you have two classes to declare. Put those on a back burner, and we'll continue with the driver.
Specification of
GradeBookDriver#main(String[])
:output (screen): the average and standard deviation of the scores; the contents of the gradebook (i.e., all of the students).
Because of the parser, we can defer all of the input concerns to that code!
We have these operations:
Operations of GradeBookDriver#main(String[])
Description Predefined? Name Library read in a gradebook no readGradeBook()
GradeBookParser
display prompts and reports yes println()
Screen
compute average of the scores no average()
GradeBook
standard deviation of the scores no standardDeviation()
GradeBook
String
representation of gradebookno toString()
GradeBook
There are three new operations to define!
We can organize the preceding objects and operations into the following algorithm:
Algorithm of GradeBookDriver#main(String[])
- Create
parser
.- Display a greeting.
- Set
theGradebook
to a gradebook read in byparser
.- Print the average of
gradebook
, the standard deviation ofgradebook
, andgradebook
itself to the screen.
This is done already in the
GradeBook#main(String[])
method, although some of the
code is commented out since the necessary methods haven't been
designed or implemented yet. The purpose of this OCD is to indicate
what operations still need to be designed and implemented.
GradeBook
ObjectsThe OCD for the driver demands that you create a class
GradeBook
to represent gradebook objects. So we first
design it.
There's just one attribute:
The Student
class represents student objects, but
what about a sequence of students? This is a problem that
a list solves.
Helpful hint: The examples here assume that
list
is a
local variable. If you're declaring a parameter or an instance
variable, use the usual contextual syntax. That is,
instance variables are still always declared
private
; parameters are declared in parentheses and
are separated with commas.
The declaration of a list adds a new wrinkle to variable declarations:
List<ElementType
>list
;
The new wrinkle is the data type in this declaration.
List
is a special class in Java because different
lists can contain different types of data. Consquently,
List
is a parameterized type---a type
with a "type parameter"! In the case of a List
, the
type parameter specifies the data type of every element in the
list.
So ElementType
is your opportunity (and responsibility) to tell the compiler that
the list is supposed to store that particular type.
If I wanted to store
Coordinates
in a list namedcoordinates
, here's my declaration:List<Coordinate> coordinates;We usually say that
coordinates
is aList
ofCoordinate
s.One of the conventions for naming a list variable is using a plural noun. If I stored away one coordinate, I might call it
coordinate
, but since it stores many coodinates, it's better to call itcoordinates
.
Like any other reference type, just declaring the list variable is not enough; the actual list must be created, and a reference to the list must be placed in the list variable.
Often, you will allocate a list as part of the list's declaration:
List<ElementType
>list
= new ArrayList<ElementType
>();
where
ElementType
is the data type of the elements of the list.list
is the
name of the list.List<Coordinate> coordinates = new ArrayList<Coordinate>();
The new list is an empty list, and you can use methods on the list to add new elements to it. The list will grow as necessary.
There are two things that may look a bit strange:
List
is not the same as
the allocated type ArrayList
.<Coordinate>
is repeated on both sides
of the assignment.Both of these quirks are due to object inheritance (which we don't cover until Lab #11). For now, just take these things as unexplained quirks.
These two classes come from the java.util
package,
so you'll need to add import statements.
GradeBook
ObjectsA gradebook will have a sequence of students in it. You can use a list to represent this sequence.
Do this...
Define an instance variable myStudents
as a
List
of Student
s in the
GradeBook
class. Do not initialize it yet. Compile the code to make sure
things are fine so far.
Now you need to be able to construct GradeBook
objects, so define a constructor:
Behavior of GradeBook(List<Student>)
I am a gradebook object. To construct myself, I will receive a list of students which I will use to initialize by own list of students.
That's fairly straightforward.
Algorithm of GradeBook(List<Student>)
- Receive
students
.- Set
myStudents
to bestudents
.
Do this...
Write this constructor. Uncomment the calls to the constructor in
GradeBookTest#setUp()
. Make sure all of your code compiles.
Before we move on to other methods for the
GradeBook
, let's first examine some of the other
things you can do with a List
.
Once you have all of your students in the list, you'll want to
process them. One way to process all of the elements of a list is
with a counting for
loop; however, you'll need to know
how many students you have when writing this loop. Fortunately,
it's the responsibility of each List
instance to keep
track of this number, known as the size of the
list.
list
.size()
This returns an int
, the number of elements in the
list. You cannot directly change the size of a list (i.e,
there's no setSize()
method), but the size is
automatically changed for you as you add (or remove)
elements to (or from) the list.
Once elements are in a list, you can access each one by its own
index with the get()
method:
list
.get(index
)
As mentioned at the beginning of this lab exercise, a list is an
indexed contained; in this code pattern, index
is the
index of the element you want to access.
As noted at the very beginning of this lab exercise, lists in Java use zero-based indexing. That is, the first element in a list has index 0; the second element has index 1; the third element has index 2; etc.
Question #9.01 What is the index of the
three-hundred twenty-eighth element?
To be answered in Exercise
Questions/lab09.txt
.
When we carry this out to its logical conclusion, we discover
that the last element has index
,
one less than the size.list
.size()-1
Keep firmly in mind that the first valid index is 0,
and the last valid index is
-1.
This is a very common source of errors when programming with a
list. You may hear other programmers talk about an "off by
one error" if you start with index 1 or end with index
list
.size()
.list
.size()
A List
does bounds checking on an
index, and it will complain loudly if you use an invalid index. As
with all loud complaining in Java, that means it throws an
exception if the index is invalid. If you get an
ArrayIndexOutOfBoundsException
, the first thing you
should check is that you're staring with index 0 and ending with
index size()
-1. Also read the exception report itself;
it will tell you what your invalid index was.
Helpful hint: Previous versions of Java did not
allow the List<Coordinate>
declaration; this
meant extra code was needed when using
.
Old Java books and programmers might tell you to do things the old,
obnoxious way. The old way will still work, but it unnecessary.list
.get(
index
)
Since you declared your list variable with an ElementType
,
the data that comes out of the list with
is guaranteed to be of type list
.get(
index
)ElementType
.
For example,
Coordinate first = coordinates.get(0);Assuming that there is, in fact, at least one
Coordinate
in the list,coordinate
will be set equal to the first element from the list.
This is enough to iterate through the elements of a list:
for (int i = 0; i <list
.size(); i++) {ElementType
currentElement
=list
.get(i); ... }
You can use any variable in place of i
.
This loop prints the x-coordinates of the coordinates in my list:
for (int i = 0; i < coordinates.size(); i++) { Coordinate currentCoordinate = coordinates.get(i); theScreen.println(currentCoordinate.getX()); }
Helpful hint: The foreach loop is a new addition to Java. Impress old Java programmers by telling them about it!
Java introduces a special loop called the foreach loop for iterating through the elements of a list when the loop variable is needed only for counting.
for (ElementType
currentElement
:list
) { ... }
It's a much tighter syntax.
for (Coordinate currentCoordinate : coordinates) { theScreen.println(currentCoordinate.getX()); }
These loops are fine for iterating through the elements already
in a list. Sometimes the loop variable is needed, and then
the counting for
loop is preferred. Neither loop works
for input, and so you'll probably end up using a forever loop for
input problems.
GradeBook
OperationsThere's actually an invariant you should enforce on all
GradeBook
objects:
The list of students has at least one student in it.
Invariants must be enforced in constructors and mutators, and you only have one constructor.
Do this...
Add an if
statement to the
GradeBook(List<Student>)
that throws an
IllegalArgumentException
when the size of the list of
students is zero or smaller.
By enforcing this invariant, the computations of the
GradeBook
can assume (correctly!) that the size of the
list is at least 1.
Going back to the OCD for the main driver, the next behavior you have to worry about is averaging the grades in a gradebook.
Here's a possible behavior:
Behavior of GradeBook#average()
I am a
GradeBook
object. I compute the sum of my students' grades; I compute the number of students' grades; I compute the average as the sum divided by the number of students.
Question #9.02 Why don't you have to worry
about dividing by zero?
To be answered in Exercise
Questions/lab09.txt
.
This behavior leads to these objects:
Objects of GradeBook#average()
Description Type Kind Movement Name the sum of the grades double variable local sum
the number of students int variable local numberOfStudents
the average of the grades double variable out average
The specification only has to deal with the returned value:
Specification of
GradeBook#average()
:return: the average of the grades of the students.
The crux of this method is in the operations:
Operations of Gradebook#average()
Description Predefined? Name Library the number of students yes size()
List
the sum of the grades in the gradebook no sum()
GradeBook
divide the sum by the size yes /
built-in
We can organize these objects and operations into the following algorithm:
Algorithm of GradeBook#average()
- Let
size
be the number of students inmyStudents
.- Let
sum
be the sum of the grades of the students.- Let
average
besum
divided bysize
.- Return
average
.
The next step would be to write tests for this method. This has
already been done in the GradeBookTest
class.
Do this...
Uncomment the assertions in
GradeBookTest#testAverage()
.
Do this...
Implement GradeBook#average()
, and now when you compile, you should get complaints
only about the undefined sum()
method.
Inlining variables is an issue unrelated to lists, but it's
already an issue brought up by GradeBook#average()
.
It'll be a bigger issue with some of the other methods you
implement in this lab exercise.
If you're happy using the identifiers suggested in the algorithms in this lab manual and if you're happy to implement the algorithms as written, then you actually can skip this section.
Strictly speaking, all of the variables in
GradeBook#average()
are not necessary. You
could write the method in just one return
statement. You would inline the variables, replace
variable uses with their computations.
For example, one might like to talk about the magnitude of a coordinate---its distance from the origin. A method for this would look like so:
public double magnitude() { double xSquared = getX() * getX(); double ySquared = getY() * getY(); double sum = xSquared + ySquared; double magnitude = Math.sqrt(sum); return magnitude; }Alternately, I could inline the
xSquared
andySquared
variables like so:public double magnitude() { double sum = getX() * getX() + getY() * getY(); double magnitude = Math.sqrt(sum); return magnitude; }The variables are completely unnecessary because I replaced each one (e.g.,
xSquared
) with the expression used to initialize them (e.g.,getX() * getX()
).I could inline two more times. Once:
public double magnitude() { double magnitude = Math.sqrt(getX() * getX() + getY() * getY()); return magnitude; }Twice:
public double magnitude() { return Math.sqrt(getX() * getX() + getY() * getY()); }
You do not have to inline any of your variables. You can pick and choose which variables you inline. Experienced programmers tend to inline as many variables as they can; some will even argue that you must inline them all! However, when you learn how to program, you often want as many clues in your code as possible as to what's going on. The more variables you declare, the more clues you have (assuming you pick good names). So find the best balance for yourself between too many and too few variables.
What is completely unacceptable, though, is trying to
abbreviate the names of the identifiers. Some might try to use
sm
and nos
for
sum
and
numberOfStudents
, respectively. Don't do
this. Most experienced programmers frown on this, and when
learning to program, these abbreviations can put you at a
disadvantage. So do not try to abbreviate the suggested
identifiers in this lab exercise. If you can find an equally
descriptive identifier, then you can use it at your own risk, but
don't abbreviate.
GradeBook
ActionsComputing the sum of the grades uses a very common solution for
processing a list. The general problem is that you want a
single piece of summary information about the
whole list. In this case, it's the sum (a single value) of
the grades in the whole list of students. The pattern for
a solution for this type of problem, computing a summary
value, looks something like this:
initializesummary
for (ElementType
currentElement
:list
) {summary
=summary
combinedWithprocess
(currentElement
); }
summary
is
the summary variable; at the end of the loop, it will have the
summarized answer; during the loop it will have the best answer
so far (i.e., for all of the elements processed so
far).
Suppose---for some strange reason---I wanted the product of the squares of my x-coordinates.
double product = 1.0; for (Coordinate currentCoordinate : coordinates) { product = product * currentCoordinate.getX() * currentCoordinate.getX(); }My summary variable is
product
.
- A product has to be initialized to 1.0 since initializing it to 0 would keep it at zero; so that explains my initialization.
- I processed the current coordinate (i.e.,
currentCoordinate
) by accessing its x-coordinate twice and multiplying those values together.- I "combined it with" the old product with multiplication.
You cannot suddenly and automatically process all of the values in a list with one statement; you have to iterate through each element of the list. While iterating through the loop, you can store away the sum (or product or whatever summary) only as far as you've currently processed the list.
Here's the behavior:
Behavior of GradeBook#sum()
I am a
GradeBook
object. To sum up my grades, I will initialize a sum to 0. Then, for each student in my students list, I will add the grade of the current student to the sum. After all of the students are processed, I will return the sum.
You need the following objects:
Objects of GradeBook#sum()
Description Type Kind Movement Name sum double
variable out sum
my students List
instance variable instance myStudents
current student Student
variable local currentStudent
The specification is simple:
Specification of
GradeBook#sum()
:return:
sum
, the sum of the grades in me (the gradebook).
You have the following operations:
Operations of GradeBook#sum()
Description Predefined? Name Library initialize a variable yes variable declaration built-in iterate through my students yes foreach loop built-in get grade of current student yes getGrade()
Student
add grade to current summ yes +
built-in return the sum yes return
built-in
Here's the algorithm:
Algorithm of GradeBook#sum()
- Initialize
sum
to be 0.- Foreach
currentStudent
ofmyStudents
:End for loop.
- Set
sum
equal tosum
plus the grade ofcurrentStudent
.- Return
sum
.
Question #9.03 Fill in the blank cells of this table comparing the summary-loop pattern above with the algorithm and implied Java code:
Summary-loop Component | Java code |
---|---|
summary |
|
ElementType |
|
currentElement |
|
list |
|
combinedWith | |
process |
To be answered in Exercise
Questions/lab09.txt
.
Do this...
Uncomment GradeBookTest#testSum()
. Write the code for
GradeBook#sum()
. Run
the test-case classes for a green bar. Make absolutely sure
that you're running all of the test-case classes
for this lab.
GradeBook
into a
String
The toString()
method is a standard method for
all classes so that there's a common way for objects to be
printed out. We'll see how inheritance plays a role with this
method in a later lab, but for now it suffices to know that we can
and should define it for our GradeBook
class.
The approach for this method is the same as the approach for the
GradeBook#sum()
method. With
GradeBook#toString()
the summary information is not an
accumulated sum but an accumulated string representing the
gradebook. Consequently, the algorithm for this method is going to
use the summary-loop pattern.
Behavior of GradeBook#toString()
I am a
GradeBook
object. To return aString
representation of myself, I will initialize the gradebook representation to be the empty string. Then, for each student in my students list, I will concatenate theString
representation of the current student plus a newline to the gradebook representation. After all of the students are processed, I will return the gradebook representation.
You need the following objects:
Objects of GradeBook#toString()
Description Type Kind Movement Name the string representation of the gradebook String
variable out gradeBookRepresentation
my students List
instance variable instance myStudents
current student Student
variable local currentStudent
newline String
literal local "\n"
Specification of
GradeBook#toString()
:return:
gradeBookRepresentation
, the string representation of the students in the gradebook.
You have the following operations, also very similar:
Operations of GradeBook#toString()
Description Predefined? Name Library iterate through my students yes foreach loop built-in compute string representation of current student yes toString()
Student
concatenate String
syes +
built-in return the string representation yes return
built-in
We end up with this algorithm:
Algorithm of GradeBook#toString()
- Let
gradeBookRepresentation
be""
.- Foreach
currentStudent
frommyStudents
:End for loop.
- Set
gradeBookRepresentation
equal togradeBookRepresentation
concatenated with the string representation ofstudent
concatenated with a newline character.- Return
gradeBookRepresentation
.
Do this...
Code up this method. Uncomment
GradeBookTest#testComputeLetterGrades()
, and run the unit tests for a green bar.
As the Javadoc for this test method suggests, it tests
GradeBook#toString()
; later it will live up more to
its name and really test
GradeBook#computeLetterGrades()
.
You next need to write
GradeBook#standardDeviation()
which returns the
standard deviation of the grades in the gradebook. The standard
deviation will tell us how spread out the grades are. If the
standard deviation is small, the grades are all fairly close to the
average; if the standard deviation is large, the grades may be far
away from the average.
If you haven't had a course in statistics, you'll have to take my word that this design and algorithm are correct:
Behavior of GradeBook#standardDeviation()
I am a
GradeBook
object. To compute the standard deviation of my grades, I first compute the average of all of my grades, and initialize a sum of squared terms (a summary variable!) to 0.Then, for each student in my list of students, I compute a term: the grade of the current student minus the average. I add the square of this term to the summary-variable sum.
After all of the students are processed, I compute the quotient of the summary-variable sum and the number of terms; the standard deviation is the square root of this quotient. I return this standard deviation.
You need the following objects:
Objects of GradeBook#standardDeviation()
Description Type Kind Movement Name the average grade double
variable local average
sum of squared terms double
variable local sumOfSquaredTerms
current student Student
variable local currentStudent
my students List
instance variable instance myStudents
term double
variable local term
number of terms (i.e., number of students) int
variable local size
the quotient double
variable local quotient
the standard deviation double
variable out standardDeviation
Processing an element from the list in this method is more involved than in the previous ones, but it still follows the summary-loop pattern.
Specification of
GradeBook#standardDeviation()
:return: the standard deviation of the grades.
You have the following operations:
Operations of GradeBook#standardDeviation()
Description Predefined? Name Library compute the average grade yes average()
GradeBook
iterate through my students yes foreach loop built-in get grade of current student yes getGrade()
Student
grade minus average yes -
built-in square the term yes *
built-in add sum and squared term yes +
built-in divide sum and number of terms yes /
built-in compute the square root of quotient yes sqrt()
Math
You end up with this algorithm:
Algorithm of GradeBook#standardDeviation()
- Let
average
be the average grade.- Let
sumOfSquaredTerms
be 0.- Foreach
currentStudent
inmyStudents
:End for loop.
- Set
term
equal to the grade ofcurrentStudent
minusaverage
.- Set
sumOfSquaredTerms
equal tosumOfSquaredTerms
plusterm
timesterm
.- Let
size
be the number of students inmyStudents
.- Let
quotient
besumOfSquaredTerms
divided bysize
.- Let
standardDeviation
be the square root ofquotient
.
Do this...
Uncomment GradeBookTest#standardDeviation()
. Code up
the method. Compile your code,
and run the unit-tests for a green
bar.
You may simplify your code for this algorithm by inlining your variables (see "Inlining Variables" section above). You may not simplify your code by using simplier identifiers; your identifiers have to be at least as descriptive as the ones used in the algorithm here.
We've ignored the driver long enough.
Do this...
Uncomment the code of the driver.
The only code that the compiler will really have a problem with
is the declaration of theGradebook
because it uses a
method from GradeBookParser
that doesn't exist yet.
Depending on your compiler, there may be other compiler errors
because theGradebook
isn't really declared, but those
errors should go away once GradeBookParser
is whipped
into shape.
Switching to internal perspective, this is a possible behavior
paragraph GradeBookParser#readGradeBook()
:
Behavior of GradeBookParser#readGradeBook()
I am a gradebook parser. I will read students into a list of students, and then I will construct a gradebook from that list of students. I will return this gradebook.
This actually breaks down the computation into very small steps.
Objects of GradeBookParser#readGradeBook()
Description Type Kind Movement Name list of students List<Student>
variable local students
a gradebook GradeBook
variable out theGradebook
The list of students will come from a helper method, and so it will be the responsibility of that helper method to deal with individual students.
Specification of
GradeBookParser#readGradeBook()
:return: a list of students read in from the keyboard
Do this...
Add a stub for GradeBookParser#readGradeBook()
. All
of the code should compile. Run your unit tests for a greeen
bar.
Operations of GradeBookParser#readGradeBook()
Description Predefined? Name Library read list of students from the keyboard no readStudents()
GradeBookParser
construct a new gradebook yes GradeBook(List<Student>)
constructorGradeBook
return theGradebook
yes return
statementbuilt-in
The OCD suggests another method:
GradeBookParser#readStudents()
. You can
invoke it now, and that will be the next method that we
design and you implement. Note that it's a method of the same
class, GradeBookParser
, so just simply invoke the
method! Do not invoke it on a class or some other
object.
Algorithm of GradeBookParser#readGradeBook()
- Let
students
be a list of students read in from the keyboard.- Let
theGradebook
be a gradebook built fromstudents
.- Return
theGradebook
.
Do this...
Code up this method; the driver should compile, but the call to
readStudents()
will not.
Behavior of GradeBookParser#readStudents()
I am a gradebook parser. I will first create an empty list of students. I will repeatedly prompt for and read in a student's name and student's grade. I will add the resulting
Student
object to the list of students. I will return my list of students when the name I read in is"done"
.
The word "done"
here is known as a sentinel
value because it signals the end of the input. This
assumes that no student is named "done", which is probably a fair
assumption. (For what it's worth, since our comparison will be case
sensitive, a student named "Done" is acceptable!)
Using the behavioral description, the method needs the following objects:
Objects of GradeBookParser#readStudents()
Description Type Kind Movement Name the screen Screen
variable local theScreen
the keyboard Keyboard
variable local theKeyboard
the list of students List<Student>
variable local students
name of current student String
variable local name
grade of current student double
variable local grade
the new student Student
variable local currentStudent
Specification of
GradeBookParser#readStudents()
:input (the keyboard): read in the names and grades of the students.
output (the screen): display prompts for the input.
return: a list of students entered by the user.
Do this...
Add a stub for this method in the GradeBookParser
class. The code should compile
without error. Run your unit tests
for a greeen bar.
Now we have these operations for the method:
Operations of GradeBookParser#readStudents()
Description Predefined? Name Library create a new list yes ArrayList<Student>()
constructorjava.util.ArrayList
repeatedly... yes forever loop built-in read in a string (one word) yes readWord()
Keyboard
read in a real number yes readDouble()
Keyboard
create new student yes Student(String, double)
constructorStudent
add student to my list of students yes add()
List
return the list of student yes return
built-in
We can organize these objects and operations into the following algorithm:
Algorithm of GradeBookParser#readStudents()
- Declare and initialize
theScreen
andtheKeyboard
.- Let
students
be a new empty list of students.- Loop:
End for loop.
- Prompt for a name.
- Read in
name
.- If
name
equals"done"
,
- Return
students
.- Prompt for grade.
- Read in
grade
.- Let
currentStudent
be a newStudent
usingname
andgrade
.- Add
currentStudent
tomyStudents
.
Question #9.04 Why isn't a break
necessary in this loop?
To be answered in Exercise
Questions/lab09.txt
.
In designing this algorithm, remember that you first write out the computation steps of the loop, then add the condition. In this case, you want to stop the loop as soon as you can, and that's right after you read in the name. Don't read in the grade before making this check; your user will be puzzled why they have to enter a grade for a non-student!
Comparing name
with "done"
cannot be done with the equality operator,
==
. This will compare references, not the
contents of the strings. Since name
is read
in from the keyboard, ==
will always return
false! Instead, use this expression instead:
name.equals("done")
The String#equals(String)
method compares the
contents of the String
objects which is
exactly what you need.
The last step of the algorithm needs some more explanation. The
List
class has an add()
method that will
accomplish this job; it has just one parameter, the object you want
to add to the list:
list
.add(element
);
This method is used a lot in
GradeBookTest#setUp()
.
Do this...
Code up this algorithm in the method, and compile your code. Run your unit tests for green bars and
your driver for your own satisfaction.
Incidentally, it may seem silly to keep re-running your unit tests when working on the driver and the input class, but imagine how wrong things would get if the tests did start to fail! You want to catch these sorts of problems as soon as possible. Unit tests are meant to be run as often as possible even (maybe especially) when you think it's not necessary.
At this point you've finished the first stage of what you set out to do. You have a program that works very nicely to read in student information and to compute a average and standard deviation and to print a report of the whole gradebook. That's a lot of stuff!
However, before you really have a chance to relax, you're informed that your user wants another feature: letter grades. Using the average and standard deviation, you can compute letter grades for the students based on a curve. As noted above, the standard deviation indicates how spread out the grades are from the average. You can create cutoff values for the various grades, placing the middle of the C range at the average, and distancing the other grades based on the standard deviation.
This involves redesigning your classes. Fortunately,
this does not involve too many changes, mostly additions. Let's
start with redesigning the Student
class.
Student
ClassYou need a new attribute (listed last here):
char
.By representing a grade as a single character, you will ignore the "+" and "-" modifiers for a grade; you can add those later once you have this simpler problem worked out.
Do this...
Add an instance variable to Student
for the letter
grade.
You do have to rethink the behaviors of a Student
instance due to this new attribute. Using internal perspective:
'X'
.String
(i.e.,
toString()
), I should return a String
that includes my letter grade.Since you will curve the letter grades, you cannot assign a
letter grade to a student until the entire gradebook is read in. So
there's no point in constructing a new Student
with a
real letter grade. Use 'X'
as a "no grade" value.
Since you have to assign the grade later, you do need a
mutator to set the letter grade instance variable. (The other
attributes still don't get mutators.)
First take care of the simpler changes:
Do this...
Student(String, double)
constructor that sets my letter-grade instance variable to
'X'
.getName()
and getGrade()
accessors already in the class.You will have to change the tests in
StudentTest#testToString()
and
GradeBook#testToString()
. The letter grade of a
student should be part of the Student
's
toString()
. StudentTest#testToString()
tests this directly; GradeBookTest#testToString()
tests this indirectly.
Do this...
Change StudentTest#testToString()
so that expected
results are like "Name 34.3 X"
. Compile, and run the test-case classes for a red
bar.
Do this...
Change GradeBookTest#testToString()
so that each
string from each student has the extra " X"
in it.
This will change later on, but for now this is the correct
expectation. Compile, and run the unit tests for a red bar.
Do this...
Change the String
expression in
Student#toString()
so that the code compiles and the unit tests run for a green bar.
A mutator is typically named with a set
prefix. The
behavior is straightforward:
Behavior of Student#setLetterGrade(char)
I am a
Student
object. To set my letter grade, I will receive a letter grade, and I will set my letter grade to be this received letter grade.
So are the objects:
Objects of Student#setLetterGrade(char)
Description Type Kind Movement Name a letter grade char
variable in letterGrade
my own letter grade char
instance variable instance myLetterGrade
And the operations:
Operations of Student#setLetterGrade(char)
Description Predefined? Name Library receive a value yes parameter built-in set my instance variable yes assignment statement built-in
Normally you probably wouldn't bother listing the assignment operator, but that's the most complicated operation here!
Algorithm of Student#setLetterGrade()
- Receive
letterGrade
.- Set
myLetterGrade
to beletterGrade
.
Do this...
Write the code for the method in the Student
class,
compile, and run the test-case classes still for a
green bar.
GradeBook
ClassYou've redesigned the Student
class so that you can
store away letter grades for each student, but how do you compute
the letter grades? This is the responsibility of the
GradeBook
class. It can figure out the average and
standard deviation of the grades, and that's exactly what's needed
for the grading curve.
There are three sets of data for you to test.
Question #9.05 Look over the distribution of
scores in each data set. If you were assigning grades, what letter
grades would you assign for the scores in each of the data sets? Copy each data set into your
answer file, and enter the grade you would assign to each student.
(Incidentally, these same data sets are used in the
GradeBookTest
test-case class.)
To be answered in Exercise
Questions/lab09.txt
.
Behavior of GradeBook#computeLetterGrades()
I am a
GradeBook
object. I will first compute the average grade and the standard deviation. I will then compute the following cutoffs:
Letter Less than F average - 1.5 * standardDeviation D average - 0.5 * standardDeviation C average + 0.5 * standardDeviation B average + 1.5 * standardDeviation A infinity Then for each of my students, I will compare the current student's grade to the cutoffs; whichever range it fits into, I will set the letter grade of the current student to be the appropriate letter.
Objects of GradeBook#computeLetterGrades()
Description Type Kind Movement Name the average of the grades double
variable local average
the standard deviation of the grades double
variable local standardDeviation
the cutoff for an F double
constant local CUTOFF_F
the cutoff for an D double
constant local CUTOFF_D
the cutoff for an C double
constant local CUTOFF_C
the cutoff for an B double
constant local CUTOFF_B
my students List<Student>
instance variable instance myStudents
current student Student
variable local currentStudent
grade of current student double
variable local grade
This method doesn't receive anything or return anything. It's
really a computational mutator (not unlike
Fraction#simplify()
from a previous lab) since it
changes the state of the students in the list based on data already
in the list.
Do this...
Write a stub for GradeBook#computeLetterGrades()
.
You can actually plug it into the construction of every
GradeBook
object right now!
Do this...
Invoke GradeBook#computeLetterGrades()
as the last
step of the GradeBook(List<Student>)
constructor. Unit tests should still run for a green bar.
Operations of GradeBook#computeLetterGrades()
Description Predefined? Name Library compute the average yes average()
GradeBook
compute the standard deviation yes standardDeviation()
GradeBook
iterate through my students yes foreach loop built-in get grade of current student yes getGrade()
Student
set letter grade of current student yes setLetterGrade(char)
Student
That's a lot of operations and objects, but none of them are new now, most are review. It's just a matter of putting more things together.
Algorithm of GradeBook#computeLetterGrades()
- Let
average
be the average of my grades.- Let
standardDeviation
be the standard deviation of my grades.- Let
CUTOFF_F
beaverage
- 1.5 *standardDeviation
.- Let
CUTOFF_D
beaverage
- 0.5 *standardDeviation
.- Let
CUTOFF_C
beaverage
+ 0.5 *standardDeviation
.- Let
CUTOFF_B
beaverage
+ 1.5 *standardDeviation
.- For each
currentStudent
ofmyStudents
:End loop.
- Let
grade
be the grade ofcurrentStudent
.- If
grade
<CUTOFF_F
:Else if
- Set the letter grade of
currentStudent
to'F'
.grade
<CUTOFF_D
:Else if
- Set the letter grade of
currentStudent
to'D'
.grade
<CUTOFF_C
:Else if
- Set the letter grade of
currentStudent
to'C'
.grade
<CUTOFF_B
:Else
- Set the letter grade of
currentStudent
to'B'
.End if.
- Set the letter grade of
currentStudent
to'A'
.
Now is the time to make
GradeBookTest#testComputeLetterGrade()
live up to its
name. So far it's only really been testing
GradeBook#toString()
.
The second and third collections of grades are rather clustered,
and unless you were a really cruel grader, you'd probably hand out
all As. It turns out that the curve in
GradeBook#computeLetterGrades()
is a very
cruel grader. Your intuition may lead you wrong, so let's be
methodical about the testing:
GradeBookTest#testAverage()
verifies it. (Well, you
have to assume that I've given you correct data for it.)GradeBookTest#testStandardDeviation()
verifies it.
(Again, you have to assume that I've given you correct data for
it.)GradeBook#computeLetterGrades()
is the computation of
the cutoffs. But if you have the average and standard deviation,
then the cutoffs are simple arithmetic.Question #9.06 For each of the instance
variables in GradeBookTest
, use the averages and
standard deviations as indicated in the appropriate test methods,
to complete this chart.
Value | myGradebook1 |
myGradebook2 |
myGradebook3 |
---|---|---|---|
Average | |||
Standard deviation | |||
Cutoff F | |||
Cutoff D | |||
Cutoff C | |||
Cutoff B |
To be answered in Exercise
Questions/lab09.txt
.
Do this...
Change the X
s of the expected
String
s in
GradeBookTest#testComputeLetterGrade()
to the letter
grades as indicated by the cutoffs that you just computed. The
instance variables and data files are related (i.e.,
GradeBookTest#myGradebook1
corresponds to Data Set #1,
etc.) Run your unit tests for a red
bar.
Do this...
Now implement GradeBook#computeLetterGrades()
. Run for a green bar.
Some of those curves are rather vicious, huh? Curving the grades is not always a good thing...
Turn in:
array type, bounds checking, foreach loop, index, indexed container, inline variables, iterate, list, off by one error, parameterized type, parser, sentinel value, sequence of values, size of a list, subscript, subscripted container, zero-based indexing