Hands On Java Lab 9 Exercise Lab 9: Arrays Introduction In past exercises, we have dealt with sequences of values by processing the values one at a time. Today, we want examine a new kind of object called an array that can store not just one value, but an entire sequence of values. Like a String, an array is a subscripted object, meaning the values within it can be accessed via a subscript or index. It is important to remember that Java uses zero-base indexing. I.E., if we have an array named theArr holding N objects, the first object has index 0. By storing a sequence of values in an array, we can design operations for the sequence, and then implement such operations in a method that receives the entire sequence through an array parameter. To introduce the use of the array, today's exercise is to write a program that processes the names and scores of students in a course, and assigns letter grades (A, B, C, D or F) based on those scores, using the "curve" method of grading. The format of the input is a series of lines, each having the form:
name score
As usual, we will use object-centered design to design our solution. Analysis With a little thought, it should be evident that this problem requires that we process the sequence of scores more than once. More precisely, to assign letter grades using the "curve" method, we must first compute the average and standard deviation of the scores. Once we have these values, we can determine the letter grade corresponding to each score. We must thus process the sequence of scores at least twice. One way to solve this problem would be to ask the user to retype the values each time we need to process them. Clearly, this is an unacceptable solution. We must have some place where the data values can be held between uses. By using an array we can process a sequence of values multiple times in an efficient manner. Behavior Our program should display a greeting. It should then read a sequence of names and a sequence of scores from the keyboard. It should then compute the average and standard deviation of the sequence of scores, and display these values. Using the average and standard deviation, it should compute the sequence of letter grades that correspond to the sequence of scores. It should then display each student's name, score and letter grade. Objects If we examine our behavioral description for objects, we find these: Description Type Kind Name a greeting String constant none a sequence of names String[] varying nameArr a sequence of scores double[] varying scoreArr the average score double varying average the standard deviation of the scores double varying stdDeviation a sequence of letter grades char[] varying gradeArr We can thus specify the behavior of our program this way:
Input(keyboard): a sequence of names and scores.
Output(screen): the average and standard deviation of the scores,
plus the sequences of names and scores,
and the sequence of corresponding letter grades.
Operations From our behavioral description, we have these operations: Description Defined? Name Package/Module? 1 Display a string yes println() ann.easyio 2 Read a string yes readWord() ann.easyio 3 Read a sequence of names and scores no -- -- 4 Compute the average of a sequence scores no -- -- 5 Compute the standard deviation of a sequence scores no -- -- 6 Compute the sequence of letter grades corresponding to a sequence scores no -- -- 7 Display sequences of names, scores and letter grades no -- -- As you can see, we have a few methods to write. Algorithm We can organize the preceding objects and operations into the following algorithm:
0. Display a greeting.
1. Fill nameArr and scoreArr with values from the keyboard.
2. Output the mean and standard deviation of the values in scoreArr.
3. Compute gradeArr, an array of the letter grades corresponding
to the scores in scoreArr.
4. Display nameArr, scoreArr and gradeArr, value-by-value.
We will thus use three different array objects, nameArr storing String values, scoreArr storing double values, and gradeArr storing char values. Getting Started A skeleton program is provided in Grades.java. Since some of the operations we will be creating are reusable, we store them in a class module, consisting of the implementation file DoubleArrayOps.java, and the documentation file DoubleArrayOps.doc. Make a new project for this exercise (e.g., Grades), and then save copies of these files into the project folder. Also copy the packages ann and hoj and add them to your project. Then open the file Grades.java, and take a few moments to look it over. As indicated by its documentation, Grades.java is a skeleton program that (partially) encodes the algorithm given above. Today's exercise will consist of completing this program. Defining array Objects To define an object named arrObj for storing multiple values of type Type, we can write:
Type arrObj[];
Such a statement declares arrObj as an object capable of storing multiple values of type Type. Since an array can store any type of value, a programmer using an array must specify the type of value they want to store in a given array. There is an important point that should be noted here. When we declare the variable, we have not set aside any memory to hold the values yet. At this point, arrObj holds a special value which is null. If we were to try and use this array, we would get an error. What we are missing is the creation of the array object. That can be accomplished by:
arrObj = new Type[IntegerExpression]; The Type must match what we used to declare arrObj and we must have an integer value which tells us the maximum number of values that our array can hold. As usual, we can combine these two into one statement. One problem with arrays is that once we have created an array, we can not change its size. There are four basic techniques which we can use to deal with this problem, each of which has its own problems. Use a large constant value when we create the array. Since the value should be large enough for any possible data set, we typically end up with a lot of wasted space. Ask the user for the number of values and create it with that size. This requires that the user know the number of data values. If we run out of space, create a larger array and copy the data values. This requires extra code. Use Java's Vector class which essentially expands and shrinks the storage automatically. This class only holds objects. In Grade.java, use this information to define nameArr and scoreArr (as array objects capable of storing String and double objects, respectively), in the places indicated by the comments in Grades.java. For the size use a constant value MAX_SCORES which is set to 1000. Once we have read the data into scoreArr, we will use the subArray() method in DoubleArrayOps to replace it with an array that only has valid data values. Filling an array Object Next, we want to write fillArrays() which fills our array objects with values from the keyboard Design We can describe how this method should behave as follows: Behavior. Our method should receive an empty array of strings and an empty array of doubles from its caller. It should read each name and score from the theKeyboard, appending them to the array of strings and the array of doubles, respectively. When no values remain to be read, our method should pass back the filled array objects. It should return the number of values that were read. Objects. Using the behavioral description, our method needs the following objects: Description Type Kind Movement Name An empty array of strings String[] varying received, passed back nameArr An empty array of doubles double[] varying received, passed back scoreArr keyboard Keyboard varying static theKeyboard a name String varying local name a score double varying local score number of values read in int varying local, returned numberRead Given these objects, we can specify the behavior of our method as follows:
Receive: nameArr, a String[]; and
scoreArr, a double[].
Passback: nameArr and scoreArr, filled with the values from keyboard.
Return: numberRead an int. Since this method seems unlikely to be generally reusable, we will define it within the same file as our main function. Using the above information, place a method stub for fillArrays() after the main function. Operations. In our behavioral description, we have the following operations: Description Defined? Name Package/Module? 1 Receive nameArr and scoreArr yes method call mechanism built-in 2 Read a string from the Keyboard yes readWord() ann.easyio 3 Read a double from from the Keyboard yes readDouble() ann.easyio 4 Append a string to a String[] no 5 Append a double to a double[] no 6 Repeat 3-6 for each line of input no 7 Pass back array objects yes parameter mechanism built-in 8 Return the number read yes return built-in Algorithm. We can organize these objects and operations into the following algorithm:
0. Receive nameArr and scoreArr.
1. Loop
a. Read name
b. If name was "done", terminate repetition.
c. Read score
d. Append name to nameArr.
e. Append score to scoreArr.
f. Add one to the number read.
End loop.
2. Return the number read.
Coding To append values to an array, we need to know the first unused position in the array. But consider the following table: Values read so far Locations filled First free location 0 none 0 1 0 1 2 0, 1 2 3 0, 1, 2 3 The number of values read in is the location that we want to fill. To put a value into a location of an array, we will use the assignment statement with the indexing operator [ ] as show by the following pattern
arrayObject[integerExpression] = newValue;
Such a statement computes the integerExpression and uses that for the location that will be changed. The value newValue replaces whatever used to be in that location. We will continue this loop until the user enters the word "done". We can test for this via the following boolean expression: name.equals("done") Using this information, complete the stub of fillArrays(). Then compile what you have written and make sure that it is free of syntax errors before proceeding. Average Next, we want to write method average() which given a array of double values, returns the average of those values. Design We can describe how this method should behave as follows: Behavior. Our method should receive an array of double values. It should sum the values in the array, and if there is at least one value in the array, our method should return the sum divided by the number of values; otherwise, our method should display an error message and return a default value (e.g., 0.0). Objects. Using the behavioral description, our method needs the following objects: Description Type Kind Movement Name A array of doubles double[] varying received numArr the sum of the values in the array double varying local sum an error message String constant local -- an default return value double constant local 0.0 Given these objects, we can specify the behavior of our method as follows:
Receive: numArr, an array of doubles.
Precondition: numArr has at least one value.
Return: the average of the values in numArr.
Since this method seems likely to be generally reusable, we will define it within a class named DoubleArrayOps. Using this information, place documentation for average() in DoubleArrayOps.doc, and a method stub in DoubleArrayOps.java. Operations. In our behavioral description, we have the following operations: Description Defined? Name Package/Module? 1 Receive numArr yes method call mechanism built-in 2 Find numValues yes .length built-in 3 Sum the values in a double[] no 5 Divide two values yes / built-in 5 Return a double value yes return built-in 6 Display an error message yes println() ann.easyio 7 Select 4 and 5 or 6 and 5, but not both yes if statement built-in Algorithm. We can organize these objects and operations into the following algorithm:
0. Receive numArr.
1. Get numValues.
2. Compute sum, the sum of the values in numArr.
3. If numValues > 0:
Return sum / numValues.
Otherwise
a. Display an error message.
b. Return 0.0 as a default value.
End if.
Refinement for Computing the Sum To sum the values in an array, we need to refine our algorithm. Since there is not a built-in method to accomplish this task we will need to design our own. Design We can describe how this part of the method should behave: Behavior. We should add each of the first numValues data values in the array into the sum. We assume that each of these locations contains a valid value. Objects. Using the behavioral description, we need the following objects: Description Type Kind Movement Name A array of doubles double[] varying received numArr number of values int varying local numValues the sum of the values in the array double varying local sum location in the array int varying local i Operations. In our behavioral description, we have the following operations: Description Defined? Name Package/Module? 1 initialize sum to 0.0 yes declaration built-in 2 get number of values yes .length built-in 3 get value in location i of numArr yes [ ] built-in 4 add value to sum yes += built-in 5 Repeat (3,4), counting from 1 to numValues-1 yes for built=in Algorithm. We can organize these objects and refine our previous algorithm:
0. Receive numArr.
1. Get numValues.
2. Declare and initialize sum.
3. For each i from 0 to numValues - 1
a. add numArri to sum
4. If numValues > 0:
Return sum / numValues.
Otherwise
a. Display an error message.
b. Return 0.0 as a default value.
End if. Note that we use the notation numArri to refer to the value in numArr whose index is i. Coding Step 1 can be performed by using the expression: numArr.length
Step 2 can be performed using a for loop that counts from 0 to the numValues minus one:
for (int i = 0; i < numValues; i++)
...
To access a value v within the array, the subscript operation can be applied to an array. Its pattern is: arrayObject [ index ] Such an expression returns the value that is stored within arrayObject at index index. Note that we can use an expression like this on either the right or left hand side of the assignment operator (=). We can use the method System.err.println() to print our error message. Using this information, complete the stub for average(). Then uncomment the call to average() in the main function, translate and test your program. Verify that your program is correctly computing the average before you proceed. score values expected average value computed none error message and 0.0 15 15.0 10, 20, 30 20.0 3.7, 2.4, 5.6, 8.8 5.125 Standard Deviation We next need to write method standardDev() which given an array of double values, returns the standard deviation of those values. Unless you have had a course in statistics, you may not know how to compute the standard deviation of a sequence of values, so we will consolidate the normal design steps and provide a specification and algorithm for this task. Design The specification for this method is as follows:
Receive: numArr, a double[].
Precondition: numArr is not empty.
Return: the standard deviation of the values in numArr.
Since this method seems likely to be generally reusable, it should also be defined in our DoubleArrayOps class. Using this specification, put documentation for standardDev() in DoubleArrayOps.doc, and a method stub in DoubleArrayOps.java. An algorithm to compute the standard deviation is as follows:
0. Receive numArr.
1. Get numValues.
2. If numValues > 0:
a. Define avg, the average of the values in numArr.
b. Define sumSqrTerms, a double initialized to zero.
c. Define term, a double.
d. For each index i of numArr:
1) Set term to numArri - avg.
2) Add term^2 to sumSqrTerms.
End loop.
e. Return sqrt(sumSqrTerms / numValues).
Otherwise
a. Display an error message.
b. Return 0.0 as a default value.
End if. Coding To compute the average of the values in an array (step 2a), we can use the average() method that we just defined. Step 2d requires a loop similar to the one we used in the previous method. Using this information, complete the stub for standardDev(). Then uncomment the call to standardDev() in the main function, and translate and test what you have written. score values expected standard deviation value computed none error message and 0.0 15 0.0 10, 20, 30 8.16496581 3.7, 2.4, 5.6, 8.8 2.40767004 Your standard deviations should be approximately the ones listed in the table. Make sure that your program is correctly computing the standard deviation before you proceed. Define another array Object This step is easy. In the appropriate place in the main function, define gradeArr as an array of char objects. You don't need to use new since we will create the array in the method we create next. As usual, recompile to check the syntax of what you have written. Computing Letter Grades Eventually we will be working with input files stored on disk. Print a hard copy of each of the three scores files (scores1.data, scores2.data and scores3.data). Take a moment and and look over the distribution of scores in each file. If you were assigning grades, what letter grades would you assign for the scores in scores1.data, scores2.data and scores3.data? Take a moment and write beside each person's name (on the hard copies) what letter grade you would give them, based on their score. Our task is to write a method that computes the appropriate letter grades for the input scores. Since this method needs a sequence of scores to process, and must return a corresponding sequence of letter grades, we can specify what the method must do as follows:
Receive: scoreArr, an array of double values.
Return: gradeArr, an array of char values.
Since this method seems pretty tightly tied to this particular grading problem, we will define it locally, within Grades.java. Using this information, define a stub for computeLetterGrades() following the main function. Students often want grades to be "curved." The "curve" method of grading is based upon the assumption that the scores being graded fall into a normal distribution (i.e., a bell curve). Given a set of scores, the "curve" method determines letter grades as follows: Scores more than 1.5 standard deviations (s-ds) below the mean receive the 'F' (failing) grade. Scores that are between 0.5 and 1.5 s-ds below the mean receive the 'D' grade. Scores that are between 0.5 below and 0.5 s-ds above the mean receive the 'C' grade. Scores that are between 0.5 and 1.5 s-ds above the mean receive the 'B' grade. Scores that are more than 1.5 s-ds above the mean receive the 'A' grade. An algorithm for determining the letter grades corresponding to a array of scores is thus as follows:
0. Receive scoreArr, an array of scores.
1. Get numValues.
2. Define gradeArr, an array of characters
the same length as scoreArr.
3. If numValues > 0:
a. Define avg, the average of the values.
b. Define standardDev, the standard deviation of the values.
c. Define F_CUT_OFF as avg - 1.5 * standardDev.
d. Define D_CUT_OFF as avg - 0.5 * standardDev.
e. Define C_CUT_OFF as avg + 0.5 * standardDev.
f. Define B_CUT_OFF as avg + 1.5 * standardDev.
g. For each index i of scoreArr from 0 to numValues - 1:
If scoreArri < F_CUT_OFF:
Set gradeArri to 'F'.
Else if scoreArri < D_CUT_OFF:
Set gradeArri to 'D'.
Else if scoreArri < C_CUT_OFF:
Set gradeArri to 'C'.
Else if scoreArri < B_CUT_OFF:
Set gradeArri to 'B'.
Else
Set gradeArri to 'A'.
End if.
End loop.
End if.
4. Return gradeArr.
Using the information above, complete the stub for computeLetterGrades(). Then compile what you have written to check for syntax errors. (We'll check for logic errors in the next step.) Display the Names, Scores and Grades The last method is to display the information we have computed. Design and write a method that, given an array of names, an array of scores, and an array of letter grades displays these three array objects in tabular form. For example, with the input sam 3.7
max 2.4
joe 5.6
bob 8.8
done We would expect the output to appear something like the following: Mean score: 5.125
Std. Dev: 2.40767004
sam 3.7 D
max 2.4 D
joe 5.6 C
bob 8.8 A
If you find that you have logic errors in what you have written, use the debugger to find them. Reading the Scores from a File The final task is to convert our program from one that gets its data from the keyboard into one that reads its information from a file. You have been working with files all along as a place where your program code is stored. When you run the Java compiler, it reads the file that you have created and processes it to produce a class file which contains the byte code for your program. The file provides a long term storage facility making it easy for you to modify your code. Similarly, we would like our grades program to have the same capability. That way if we make a mistake entering the grades, we don't have to reenter all the data, but only modify what has changed. It is important for students to realize that the file and the arrays in the program are different. We will read the data from the file and place it into the array. If we make a change to the the data in the array, the data in the file will be unchanged. Once the program finishes, the array object is gone, but the file remains. One design choice we could have made earlier was to have the user retype the data values each time they needed to be processed. While that is not a viable option for keyboard input, we could do this with our file. This is not preferred, however, because files are typically stored on hard disks which have large capacity, but slow access time. An array, on the other hand, is typically stored in random access memory (RAM) which allows much faster access. The only reason we might consider reading the data multiple times is if the amount of data in the file is larger than can be fit comfortably into the RAM of our computer. Consider though, that at the time this was written, a typical home computer had at least 64 million bytes of RAM. If our program had access to all of it, we could store somewhere between 100,000 and 1,000,000 scores in RAM. In the next section, we will go into file access in more depth. For now we will use the class hoj.ReadFile which supports the same methods as the Keyboard class in ann.easyio. This will make it relatively easy to convert our code to work with the file. We will write a method fillArraysFile() which fills our array objects with values from a file. It will have basically the same design as the method fillArrays() which we wrote earlier. Behavior. Our method should receive an empty array of strings and an empty array of doubles from its caller. It should ask the user for the filename. It should open the file. It should read each name and score from the file appending them to the array of strings and the array of doubles, respectively. When no values remain to be read, our method should close the file and pass back the filled array objects. It should return the number of values that were read. The new behavior is in bold. Objects. We need some new objects Description Type Kind Movement Name An input file name String varying local fileName An input file ReadFile varying local theFile Operations. We can see how the new operations fit into the previous behavioral description: Description Defined? Name Package/Module? 1 Receive nameArr and scoreArr yes method call mechanism built-in Print a prompt for the file name yes print() ann.easyio Read a string from the Keyboard yes readWord() ann.easyio Open the file yes ReadFile() hoj 2 Read a string from the file yes readWord() hoj 3 Read a double from from the file yes readDouble() hoj 4 Append a string to a String[] no 5 Append a double to a double[] no 6 Repeat 3-6 for each line of input no Close the file yes close() hoj 7 Pass back array objects yes parameter mechanism built-in 8 Return the number read yes return built-in Again the changes are marked in bold Algorithm. The algorithm for the new method is:
0. Receive nameArr and scoreArr.
1. Prompt for the file name.
2. Read the file name.
3. Declare and open theFile. 4. Loop
a. Read name from theFile.
b. If name was empty, terminate repetition.
c. Read score from theFile.
d. Append name to nameArr.
e. Append score to scoreArr.
f. Add one to the number read.
End loop.
5. Close theFile. 6. Return the number read.
Coding To start with make a copy of fillArrays() just after itself in Grades.java. Change the name of the copy to fillArraysFile() and change the comments to reflect the new functionality. Steps 1 and 2 don't involve anything new. Step 3 is more interesting. Remember that declaring a variable of a certain type and creating an object of that type are different things. In this case, when we create an object of type ReadFile, the constructor requires us to provide the name of the file that we are attempting to open. If the file does not exist, an exception will be thrown. We can either catch the exception or just let the program die. We will let it die for now. If fileName is the name of the String variable holding the file name the user typed in, we can declare and create a new ReadFile object via the code: ReadFile theFile = new ReadFile(fileName); To use the file instead of the keyboard, we need to send the message to theFile instead of theKeyboard. For example if we did something = theKeyboard.readInt(); we would change it to be something = theFile.readInt(); The same message is being sent to a different object. The only additional messages that a ReadFile understands as compared to a Keyboard is close(). Step 5 requires us to send the close() message to the object theFile. Once we close the file, we can no longer read values from it unless we open it again by creating a new ReadFile object. It is always good programming practice to close a file immediately after finishing using it. Using this information, complete the transformation of fillArraysFile(). In your main program, change the call to fillArrays to fillArrayFile. Then compile what you have written and make sure that it is free of syntax errors before proceeding. Trying the predefined data files. Run your program with the input file scores1.data. The output produced should appear something like the following:
Enter the name of the scores file: scores1.data
Mean score: 75
Std. Dev: 11.1803
joan 55 F
joe 60 D
jane 60 D
jim 65 D
janet 70 C
john 70 C
johanna 75 C
jack 75 C
joeline 75 C
jacques 75 C
josh 80 C
janna 80 C
jason 85 B
jadzia 90 B
jon 90 B
jackie 95 A
If you find that you have logic errors in what you have written, use the debugger to find them. When scores2.data or scores3.data are processed using the "curve" method, how do the "curved" grades compare with the grades you would have assigned? What have you learned about grading "on the curve?" Sample data files are provided in scores1.data, scores2.data, and scores3.data. Phrases you should now understand: Array, Zero-based indexing, Index, Subscript, Opening a File, RAM. Submit: Hard copies of your final versions of Grades.java, DoubleArrayOps.java, DoubleArrayOps.doc, and execution records showing the processing of scores1.data, scores2.data and scores3.data, plus the hard copies of these scores that you printed earlier annotated with the grades you assigned. Back to This Lab's Table of Contents Back to the Prelab Questions Forward to the Homework Projects Back to the Table of Contents Back to the Introduction Copyright 2000 by Prentice Hall. All rights reserved.