Hands on Testing Java: Lab #9

Lists

Introduction

In past exercises, we have dealt with a sequence of values by processing the values one at a time:

  1. Read a value.
  2. Process that value.
  3. Throw away that value.
  4. Repeat.

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.

Getting Prepared

Do this...

The Student Class

The 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:

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:

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.

Problem Analysis

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:

  1. Compute the average.
  2. Compute the standard deviation.
  3. Compute the letter grades.

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".

Design of Main Driver

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 gradebook no 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[])
  1. Create parser.
  2. Display a greeting.
  3. Set theGradebook to a gradebook read in by parser.
  4. Print the average of gradebook, the standard deviation of gradebook, and gradebook 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 Objects

The 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.

Defining List Objects

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 named coordinates, here's my declaration:

List<Coordinate> coordinates;

We usually say that coordinates is a List of Coordinates.

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 it coordinates.

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

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:

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.

Back to GradeBook Objects

A 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 Students 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>)
  1. Receive students.
  2. Set myStudents to be students.

Do this...
Write this constructor. Uncomment the calls to the constructor in GradeBookTest#setUp(). Make sure all of your code compiles.

Other List Operations

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.

The Size of 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.

Accessing Elements of a 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 list.size()-1, one less than the size.

Keep firmly in mind that the first valid index is 0, and the last valid index is list.size()-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().

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 list.get( index). Old Java books and programmers might tell you to do things the old, obnoxious way. The old way will still work, but it unnecessary.

Since you declared your list variable with an ElementType, the data that comes out of the list with list.get( index) is guaranteed to be of type 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.

More GradeBook Operations

Enforcing Invariants

There'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.

Average of the Grades in a Gradebook

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()
  1. Let size be the number of students in myStudents.
  2. Let sum be the sum of the grades of the students.
  3. Let average be sum divided by size.
  4. 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

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 and ySquared 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.

More GradeBook Actions

Computing the Sum of the Grades

Computing 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:

initialize summary
for (ElementType currentElement : list) {
  summary = summary combinedWith process(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.

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()
  1. Initialize sum to be 0.
  2. Foreach currentStudent of myStudents:
    1. Set sum equal to sum plus the grade of currentStudent.
    End for loop.
  3. 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.

Turning a 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 a String 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 the String 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 Strings yes + built-in
return the string representation yes return built-in

We end up with this algorithm:

Algorithm of GradeBook#toString()
  1. Let gradeBookRepresentation be "".
  2. Foreach currentStudent from myStudents:
    1. Set gradeBookRepresentation equal to gradeBookRepresentation concatenated with the string representation of student concatenated with a newline character.
    End for loop.
  3. 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().

Computing the Standard Deviation of the Grades

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()
  1. Let average be the average grade.
  2. Let sumOfSquaredTerms be 0.
  3. Foreach currentStudent in myStudents:
    1. Set term equal to the grade of currentStudent minus average.
    2. Set sumOfSquaredTerms equal to sumOfSquaredTerms plus term times term.
    End for loop.
  4. Let size be the number of students in myStudents.
  5. Let quotient be sumOfSquaredTerms divided by size.
  6. Let standardDeviation be the square root of quotient.

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.

The Driver

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.

The Gradebook Parser

Reading in a Gradebook

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>) constructor GradeBook
return theGradebook yes return statement built-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()
  1. Let students be a list of students read in from the keyboard.
  2. Let theGradebook be a gradebook built from students.
  3. Return theGradebook.

Do this...
Code up this method; the driver should compile, but the call to readStudents() will not.

Reading in a List of Students

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>() constructor java.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) constructor Student
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()
  1. Declare and initialize theScreen and theKeyboard.
  2. Let students be a new empty list of students.
  3. Loop:
    1. Prompt for a name.
    2. Read in name.
    3. If name equals "done",
      1. Return students.
    4. Prompt for grade.
    5. Read in grade.
    6. Let currentStudent be a new Student using name and grade.
    7. Add currentStudent to myStudents.
    End for loop.

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.

Enter the Maintenance Stage

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.

Redesigning the Student Class

You need a new attribute (listed last here):

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:

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

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.

Letter-Grade Mutator

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()
  1. Receive letterGrade.
  2. Set myLetterGrade to be letterGrade.

Do this...
Write the code for the method in the Student class, compile, and run the test-case classes still for a green bar.

Redesign the GradeBook Class

You'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.

Computing and Assigning Letter Grades Based on a Curve

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()
  1. Let average be the average of my grades.
  2. Let standardDeviation be the standard deviation of my grades.
  3. Let CUTOFF_F be average - 1.5 * standardDeviation.
  4. Let CUTOFF_D be average - 0.5 * standardDeviation.
  5. Let CUTOFF_C be average + 0.5 * standardDeviation.
  6. Let CUTOFF_B be average + 1.5 * standardDeviation.
  7. For each currentStudent of myStudents:
    1. Let grade be the grade of currentStudent.
    2. If grade < CUTOFF_F:
      1. Set the letter grade of currentStudent to 'F'.
      Else if grade < CUTOFF_D:
      1. Set the letter grade of currentStudent to 'D'.
      Else if grade < CUTOFF_C:
      1. Set the letter grade of currentStudent to 'C'.
      Else if grade < CUTOFF_B:
      1. Set the letter grade of currentStudent to 'B'.
      Else
      1. Set the letter grade of currentStudent to 'A'.
      End if.
    End loop.

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:

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 Xs of the expected Strings 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...

Submit

Turn in:

Terminology

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