Objectives
In this chapter you’ll learn:
• To use arrays to store data in and retrieve data from lists and tables of values.
• To declare arrays, initialize arrays and refer to individual elements of arrays.
• To use the enhanced for
statement to iterate through arrays.
• To pass arrays to methods.
• To declare and manipulate multidimensional arrays.
• To write methods that use variable-length argument lists.
• To read command-line arguments into a program.
Now go, write it before them in a table, and note it in a book.
—Isaiah 30:8
To go beyond is as wrong as to fall short.
—Confucius
Begin at the beginning, ... and go on till you come to the end: then stop.
—Lewis Carroll
Outline
7.1 Introduction
7.2 Arrays
7.3 Declaring and Creating Arrays
7.4 Examples Using Arrays
7.5 Case Study: Card Shuffling and Dealing Simulation
7.6 Enhanced for
Statement
7.7 Passing Arrays to Methods
7.8 Case Study: Class GradeBook
Using an Array to Store Grades
7.9 Multidimensional Arrays
7.10 Case Study: Class GradeBook
Using a Two-Dimensional Array
7.11 Variable-Length Argument Lists
7.12 Using Command-Line Arguments
7.13 (Optional) Software Engineering Case Study: Collaboration Among Objects
7.14 Wrap-Up
This chapter introduces data structures—collections of related data items. Arrays are data structures consisting of related data items of the same type. Arrays are fixed-length entities—they remain the same length once they are created, although an array variable may be reassigned such that it refers to a new array of a different length. We study several of the Java API’s built-in data structures in Chapter 16.
After discussing how arrays are declared, created and initialized, we present a series of practical examples that demonstrate several common array manipulations. We also present a case study that examines how arrays can help simulate the shuffling and dealing of playing cards for use in an application that implements a card game. The chapter then introduces Java’s enhanced for
statement, which allows a program to access the data in an array more easily than does the counter-controlled for
statement presented in Section 5.3. Two sections of the chapter enhance the case study of class GradeBook
in Chapters 3–5. In particular, we use arrays to enable the class to maintain a set of grades in memory and analyze student grades from multiple exams in a semester—two capabilities that were absent from previous versions of the class. These and other chapter examples demonstrate the ways in which arrays allow programmers to organize and manipulate data.
An array is a group of variables (called elements or components) containing values that all have the same type. Recall that types are divided into two categories—primitive types and reference types. Arrays are objects, so they are considered reference types. As you’ll soon see, what we typically think of as an array is actually a reference to an array object in memory. The elements of an array can be either primitive types or reference types (including arrays, as we’ll see in Section 7.9). To refer to a particular element in an array, we specify the name of the reference to the array and the position number of the element in the array. The position number of the element is called the element’s index or subscript.
Figure 7.1 shows a logical representation of an integer array called c
. This array contains 12 elements. A program refers to any one of these elements with an array-access expression that includes the name of the array followed by the index of the particular element in square brackets ([]
). The first element in every array has index zero and is sometimes called the zeroth element. Thus, the elements of array c
are c[0]
, c[1]
, c[2]
and so on. The highest index in array c
is 11, which is 1 less than 12—the number of elements in the array. Array names follow the same conventions as other variable names.
Fig. 7.1. A 12-element array.
An index must be a nonnegative integer. A program can use an expression as an index. For example, if we assume that variable a
is 5
and variable b
is 6
, then the statement
c[ a + b ] += 2;
adds 2
to array element c[ 11 ]
. Note that an indexed array name is an array-access expression. Such expressions can be used on the left side of an assignment to place a new value into an array element.
Common Programming Error 7.1
Using a value of type long
as an array index results in a compilation error. An index must be an int
value or a value of a type that can be promoted to int
—namely, byte
, short
or char
, but not long
.
Let us examine array c
in Fig. 7.1 more closely. The name of the array is c
. Every array object knows its own length and maintains this information in a length
field. The expression c.length
accesses array c
’s length
field to determine the length of the array. Note that, even though the length
member of an array is public
, it cannot be changed because it is a final
variable. This array’s 12 elements are referred to as c[0]
, c[1]
, c[2]
,..., c[ 11 ]
. The value of c[0]
is -45
, the value of c[1]
is 6
, the value of c[2]
is 0
, the value of c[7]
is 62
and the value of c[ 11 ]
is 78
. To calculate the sum of the values contained in the first three elements of array c
and store the result in variable sum
, we would write
sum = c[ 0 ] + c[ 1 ] + c[ 2 ];
To divide the value of c[ 6 ]
by 2
and assign the result to the variable x
, we would write
x = c[ 6 ] / 2;
Array objects occupy space in memory. Like other objects, arrays are created with keyword new
. To create an array object, the programmer specifies the type of the array elements and the number of elements as part of an array-creation expression that uses keyword new
. Such an expression returns a reference that can be stored in an array variable. The following declaration and array-creation expression create an array object containing 12 int
elements and store the array’s reference in variable c
:
int c[] = new int[ 12 ];
This expression can be used to create the array shown in Fig. 7.1. This task also can be performed in two steps as follows:
In the declaration, the square brackets following the variable name c
indicate that c
is a variable that will refer to an array (i.e., the variable will store an array reference). In the assignment statement, the array variable c
receives the reference to a new array of 12 int
elements. When an array is created, each element of the array receives a default value—zero for the numeric primitive-type elements, false
for boolean
elements and null
for references (any nonprimitive type). As we’ll soon see, we can provide specific, nondefault initial element values when we create an array.
Common Programming Error 7.2
In an array declaration, specifying the number of elements in the square brackets of the declaration (e.g., int c[ 12 ];
) is a syntax error.
A program can create several arrays in a single declaration. The following String
array declaration reserves 100 elements for b
and 27 elements for x
:
String b[] = new String[ 100 ], x[] = new String[ 27 ];
In this case, the class name String
applies to each variable in the declaration. For readability, we prefer to declare only one variable per declaration, as in:
String b[] = new String[ 100 ]; // create array b
String x[] = new String[ 27 ]; // create array x
When an array is declared, the type of the array and the square brackets can be combined at the beginning of the declaration to indicate that all the identifiers in the declaration are array variables. For example, the declaration
double[] array1, array2;
indicates that array1
and array2
are each “array of double
” variables. The preceding declaration is equivalent to:
double array1[];
double array2[];
or
double[] array1;
double[] array2;
The preceding pairs of declarations are equivalent—when only one variable is declared in each declaration, the square brackets can be placed either after the type or after the array variable name.
Common Programming Error 7.3
Declaring multiple array variables in a single declaration can lead to subtle errors. Consider the declaration int[] a, b, c;
. If a
, b
and c
should be declared as array variables, then this declaration is correct—placing square brackets directly following the type indicates that all the identifiers in the declaration are array variables. However, if only a
is intended to be an array variable, and b
and c
are intended to be individual int
variables, then this declaration is incorrect—the declaration int a[], b, c;
would achieve the desired result.
A program can declare arrays of any type. Every element of a primitive-type array contains a value of the array’s declared type. Similarly, in an array of a reference type, every element is a reference to an object of the array’s declared type. For example, every element of an int
array is an int
value, and every element of a String
array is a reference to a String
object.
This section presents several examples that demonstrate declaring arrays, creating arrays, initializing arrays and manipulating array elements.
The application of Fig. 7.2 uses keyword new
to create an array of 10 int
elements, which are initially zero (the default for int
variables). Line 8 declares array
—a reference capable of referring to an array of int
elements. Line 10 creates the array object and assigns its reference to variable array
. Line 12 outputs the column headings. The first column contains the index (0–9) of each array element, and the second column contains the default value (0) of each array element.
Fig. 7.2. Initializing the elements of an array to default values of zero.
The for
statement in lines 15–16 outputs the index number (represented by counter
) and the value of each array element (represented by array[ counter ]
). Note that the loop control variable counter
is initially 0
—index values start at 0, so using zero-based counting allows the loop to access every element of the array. The for
’s loop-continuation condition uses the expression array.length
(line 15) to determine the length of the array. In this example, the length of the array is 10, so the loop continues executing as long as the value of control variable counter
is less than 10. The highest index value of a 10-element array is 9, so using the less-than operator in the loop-continuation condition guarantees that the loop does not attempt to access an element beyond the end of the array (i.e., during the final iteration of the loop, counter
is 9
). We’ll soon see what Java does when it encounters such an out-of-range index at execution time.
A program can create an array and initialize its elements with an array initializer, which is a comma-separated list of expressions (called an initializer list) enclosed in braces ({
and }
); the array length is determined by the number of elements in the initializer list. For example, the declaration
int n[] = { 10, 20, 30, 40, 50 };
creates a five-element array with index values 0
, 1
, 2
, 3
and 4
. Element n[0]
is initialized to 10
, n[ 1 ]
is initialized to 20
, and so on. This declaration does not require new
to create the array object. When the compiler encounters an array declaration that includes an initializer list, it counts the number of initializers in the list to determine the size of the array, then sets up the appropriate new
operation “behind the scenes.”
The application in Fig. 7.3 initializes an integer array with 10 values (line 9) and displays the array in tabular format. The code for displaying the array elements (lines 14–15) is identical to that in Fig. 7.2 (lines 15–16).
Fig. 7.3. Initializing the elements of an array with an array initializer.
The application in Fig. 7.4 creates a 10-element array and assigns to each element one of the even integers from 2 to 20 (2
, 4
, 6
,..., 20
). Then the application displays the array in tabular format. The for
statement at lines 12–13 calculates an array element’s value by multiplying the current value of the control variable counter
by 2
, then adding 2
.
Fig. 7.4. Calculating the values to be placed into the elements of an array.
Line 8 uses the modifier final
to declare the constant variable ARRAY_LENGTH
with the value 10
. Constant variables (also known as final
variables) must be initialized before they are used and cannot be modified thereafter. If you attempt to modify a final
variable after it is initialized in its declaration (as in line 8), the compiler issues the error message
cannot assign a value to final variable variableName
If an attempt is made to access the value of a final
variable before it is initialized, the compiler issues the error message
variable variableName might not have been initialized
Good Programming Practice 7.1
Constant variables also are called named constants or read-only variables. Such variables often make programs more readable than programs that use literal values (e.g., 10
)—a named constant such as ARRAY_LENGTH
clearly indicates its purpose, whereas a literal value could have different meanings based on the context in which it is used.
Common Programming Error 7.4
Assigning a value to a constant after the variable has been initialized is a compilation error.
Common Programming Error 7.5
Attempting to use a constant before it is initialized is a compilation error.
Often, the elements of an array represent a series of values to be used in a calculation. For example, if the elements of an array represent exam grades, a professor may wish to total the elements of the array and use that sum to calculate the class average for the exam. The examples using class GradeBook
later in the chapter, namely Fig. 7.14 and Fig. 7.18, use this technique.
The application in Fig. 7.5 sums the values contained in a 10-element integer array. The program declares, creates and initializes the array at line 8. The for
statement performs the calculations. [Note: The values supplied as array initializers are often read into a program rather than specified in an initializer list. For example, an application could input the values from a user or from a file on disk (as discussed in Chapter 14, Files and Streams). Reading the data into a program makes the program more reusable, because it can be used with different sets of data.]
Fig. 7.5. Computing the sum of the elements of an array.
Many programs present data to users in a graphical manner. For example, numeric values are often displayed as bars in a bar chart. In such a chart, longer bars represent proportionally larger numeric values. One simple way to display numeric data graphically is with a bar chart that shows each numeric value as a bar of asterisks (*
).
Professors often like to examine the distribution of grades on an exam. A professor might graph the number of grades in each of several categories to visualize the grade distribution. Suppose the grades on an exam were 87, 68, 94, 100, 83, 78, 85, 91, 76 and 87. Note that there was one grade of 100, two grades in the 90s, four grades in the 80s, two grades in the 70s, one grade in the 60s and no grades below 60. Our next application (Fig. 7.6) stores this grade distribution data in an array of 11 elements, each corresponding to a category of grades. For example, array[ 0 ]
indicates the number of grades in the range 0–9, array[ 7 ]
indicates the number of grades in the range 70–79 and array[ 10 ]
indicates the number of 100 grades. The two versions of class GradeBook
later in the chapter (Fig. 7.14 and Fig. 7.18) contain code that calculates these grade frequencies based on a set of grades. For now, we manually create the array by looking at the set of grades.
Fig. 7.6. Bar chart printing program.
The application reads the numbers from the array and graphs the information as a bar chart. The program displays each grade range followed by a bar of asterisks indicating the number of grades in that range. To label each bar, lines 16–20 output a grade range (e.g., "70-79: "
) based on the current value of counter
. When counter
is 10
, line 17 outputs 100
with a field width of 5, followed by a colon and a space, to align the label "100: "
with the other bar labels. The nested for
statement (lines 23–24) outputs the bars. Note the loop-continuation condition at line 23 (stars < array[ counter ]
). Each time the program reaches the inner for
, the loop counts from 0
up to array[ counter ]
, thus using a value in array
to determine the number of asterisks to display. In this example, array[ 0 ]
–]array[ 5 ]
contain zeroes because no students received a grade below 60. Thus, the program displays no asterisks next to the first six grade ranges. Note that line 19 uses the format specifier %02d
to output the numbers in a grade range. This specifier indicates that an int
value should be formatted as a field of two digits. The 0
flag in the format specifier indicates that values with fewer digits than the field width (2
) should begin with a leading 0
.
Sometimes, programs use counter variables to summarize data, such as the results of a survey. In Fig. 6.7, we used separate counters in our die-rolling program to track the number of occurrences of each side of a die as the program rolled the die 6000 times. An array version of the application in Fig. 6.7 is shown in Fig. 7.7.
Fig. 7.7. Die-rolling program using arrays instead of switch
.
Fig. 7.7 uses the array frequency
(line 10) to count the occurrences of each side of the die. The single statement in line 14 of this program replaces lines 23–46 of Fig. 6.7. Line 14 uses the random value to determine which frequency
element to increment during each iteration of the loop. The calculation in line 14 produces random numbers from 1 to 6, so the array frequency
must be large enough to store six counters. However, we use a seven-element array in which we ignore frequency[ 0 ]
—it is more logical to have the face value 1 increment frequency[ 1 ]
than frequency[ 0 ]
. Thus, each face value is used as an index for array frequency
. We also replaced lines 50–52 from Fig. 6.7 by looping through array frequency
to output the results (lines 19–20).
Our next example uses arrays to summarize the results of data collected in a survey:
Forty students were asked to rate the quality of the food in the student cafeteria on a scale of 1 to 10 (where 1 means awful and 10 means excellent). Place the 40 responses in an integer array, and summarize the results of the poll.
This is a typical array-processing application (see Fig. 7.8). We wish to summarize the number of responses of each type (i.e., 1 through 10). The array responses
(lines 9–11) is a 40-element integer array of the students’ responses to the survey. We use an 11-element array frequency
(line 12) to count the number of occurrences of each response. Each element of the array is used as a counter for one of the survey responses and is initialized to zero by default. As in Fig. 7.7, we ignore frequency[ 0 ]
.
Fig. 7.8. Poll analysis program.
The for
loop at lines 16–17 takes the responses one at a time from array responses
and increments one of the 10 counters in the frequency
array (frequency[ 1 ]
to frequency[ 10 ]
). The key statement in the loop is line 17, which increments the appropriate frequency
counter, depending on the value of responses[ answer ]
.
Let’s consider several iterations of the for
loop. When control variable answer
is 0
, the value of responses[ answer ]
is the value of responses[ 0 ]
(i.e., 1
), so the program interprets ++frequency[ responses[ answer ] ]
as
++frequency[ 1 ]
which increments the value in array element 1. To evaluate the expression, start with the value in the innermost set of square brackets (answer
). Once you know answer
’s value (which is the value of the loop control variable in line 16), plug it into the expression and evaluate the next outer set of square brackets (i.e., responses[ answer ]
, which is a value selected from the responses
array in lines 9–11). Then use the resulting value as the index for the frequency
array to specify which counter to increment.
When answer
is 1
, responses[ answer ]
is the value of responses[ 1 ]
(2
), so the program interprets ++frequency[ responses[ answer ] ]
as
++frequency[ 2 ]
which increments array element 2
.
When answer
is 2
, responses[ answer ]
is the value of responses[ 2 ]
(6
), so the program interprets ++frequency[ responses[ answer ] ]
as
++frequency[ 6 ]
which increments array element 6
, and so on. Regardless of the number of responses processed in the survey, the program requires only an 11-element array (ignoring element zero) to summarize the results, because all the response values are between 1 and 10 and the index values for an 11-element array are 0 through 10.
If the data in the responses
array had contained invalid values, such as 13, the program would have attempted to add 1
to frequency[ 13 ]
, which is outside the bounds of the array. Java disallows this. When a Java program executes, the JVM checks array indices to ensure that they are valid (i.e., they must be greater than or equal to 0 and less than the length of the array). If a program uses an invalid index, Java generates a so-called exception to indicate that an error occurred in the program at execution time. A control statement can be used to prevent such an “out-of-bounds” error from occurring. For example, the condition in a control statement could determine whether an index is valid before allowing it to be used in an array-access expression.
An exception indicates that an error has occurred in a program. A programmer often can write code to recover from an exception and continue program execution, rather than abnormally terminating the program. When a program attempts to access an element outside the array bounds, an ArrayIndexOutOfBoundsException
occurs. Exception handling is discussed in Chapter 13.
Error-Prevention Tip 7.2
When writing code to loop through an array, ensure that the array index is always greater than or equal to 0 and less than the length of the array. The loop-continuation condition should prevent the accessing of elements outside this range.
The examples in the chapter thus far have used arrays containing elements of primitive types. Recall from Section 7.2 that the elements of an array can be either primitive types or reference types. This section uses random number generation and an array of referencetype elements, namely objects representing playing cards, to develop a class that simulates card shuffling and dealing. This class can then be used to implement applications that play specific card games.
We first develop class Card
(Fig. 7.9), which represents a playing card that has a face (e.g., "Ace"
, "Deuce"
, "Three"
,..., "Jack"
, "Queen"
, "King"
) and a suit (e.g., "Hearts"
, "Diamonds"
, "Clubs"
, "Spades"
). Next, we develop the DeckOfCards
class (Fig. 7.10), which creates a deck of 52 playing cards in which each element is a Card
object. We then build a test application (Fig. 7.11) that demonstrates class DeckOfCards
’s card shuffling and dealing capabilities.
Fig. 7.9. Card
class represents a playing card.
Fig. 7.10. DeckOfCards
class represents a deck of playing cards that can be shuffled and dealt one at a time.
Fig. 7.11. Card shuffling and dealing.
Card
Class Card
(Fig. 7.9) contains two String
instance variables—face
and suit
—that are used to store references to the face name and suit name for a specific Card
. The constructor for the class (lines 10–14) receives two String
s that it uses to initialize face
and suit
. Method toString
(lines 17–20) creates a String
consisting of the face
of the card
, the String " of "
and the suit
of the card. Recall from Chapter 6 that the +
operator can be used to concatenate (i.e., combine) several String
s to form one larger String
. Card
’s toString
method can be invoked explicitly to obtain a string representation of a Card
object (e.g., "Ace of Spades"
). The toString
method of an object is called implicitly when the object is used where a String
is expected (e.g., when printf
outputs the object as a String
using the %s
format specifier or when the object is concatenated to a String
using the +
operator). For this behavior to occur, toString
must be declared with the header shown in Fig. 7.9.
DeckOfCards
Class DeckOfCards
(Fig. 7.10) declares an instance variable array named deck
of Card
objects (line 7). Like primitive-type array declarations, the declaration of an array of objects includes the type of the elements in the array, followed by the name of the array variable and square brackets (e.g., Card deck[]
). Class DeckOfCards
also declares an integer instance variable currentCard
(line 8) representing the next Card
to be dealt from the deck
array and a named constant NUMBER_OF_CARDS
(line 9) indicating the number of Card
s in the deck (52).
The class’s constructor instantiates the deck
array (line 19) to be of size NUMBER_OF_CARDS
. When first created, the elements of the deck
array are null
by default, so the constructor uses a for
statement (lines 24–26) to fill the deck
array with Card
s. The for
statement initializes control variable count
to 0
and loops while count
is less than deck.length
, causing count
to take on each integer value from 0 to 51 (the indices of the deck
array). Each Card
is instantiated and initialized with two String
s—one from the faces
array (which contains the String
s "Ace"
through "King"
) and one from the suits
array (which contains the String
s "Hearts"
, "Diamonds"
, "Clubs"
and "Spades"
). The calculation count % 13
always results in a value from 0 to 12 (the 13 indices of the faces
array in lines 15–16), and the calculation count / 13
always results in a value from 0 to 3 (the four indices of the suits
array in line 17). When the deck
array is initialized, it contains the Card
s with faces "Ace"
through "King"
in order for each suit ("Hearts"
then "Diamonds"
then "Clubs"
then "Spades"
).
Method shuffle
(lines 30–46) shuffles the Card
s in the deck. The method loops through all 52 Card
s (array indices 0 to 51). For each Card
, a number between 0 and 51 is picked randomly to select another Card
. Next, the current Card
object and the randomly selected Card
object are swapped in the array. This exchange is performed by the three assignments in lines 42–44. The extra variable temp
temporarily stores one of the two Card
objects being swapped. The swap cannot be performed with only the two statements
deck[ first ] = deck[ second ];
deck[ second ] = deck[ first ];
If deck[ first ]
is the "Ace"
of "Spades"
and deck[ second ]
is the "Queen"
of "Hearts"
, after the first assignment, both array elements contain the "Queen"
of "Hearts"
and the "Ace"
of "Spades"
is lost—hence, the extra variable temp
is needed. After the for
loop terminates, the Card
objects are randomly ordered. A total of only 52 swaps are made in a single pass of the entire array, and the array of Card
objects is shuffled!
Method dealCard
(lines 49–56) deals one Card
in the array. Recall that currentCard
indicates the index of the next Card
to be dealt (i.e., the Card
at the top of the deck). Thus, line 52 compares currentCard
to the length of the deck
array. If the deck
is not empty (i.e., currentCard
is less than 52), line 53 returns the “top” Card
and postincrements currentCard
to prepare for the next call to dealCard
—otherwise, null
is returned. Recall from Chapter 3 that null
represents a “reference to nothing.”
The application of Fig. 7.11 demonstrates the card dealing and shuffling capabilities of class DeckOfCards
(Fig. 7.10). Line 9 creates a DeckOfCards
object named myDeckOfCards
. Recall that the DeckOfCards
constructor creates the deck with the 52 Card
objects in order by suit and face. Line 10 invokes myDeckOfCards
’s shuffle
method to rearrange the Card
objects. The for
statement in lines 13–19 deals all 52 Card
s in the deck and prints them in four columns of 13 Card
s each. Lines 16–18 deal and print four Card
objects, each obtained by invoking myDeckOfCards
’s dealCard
method. When printf
outputs a Card
with the %-20s
format specifier, the Card
’s toString
method (declared in lines 17–20 of Fig. 7.9) is implicitly invoked, and the result is output left justified in a field of width 20.
for
StatementIn previous examples, we demonstrated how to use counter-controlled for
statements to iterate through the elements of an array. In this section, we introduce the enhanced for
statement, which iterates through the elements of an array or a collection without using a counter (thus avoiding the possibility of “stepping outside” the array). This section discusses how to use the enhanced for
statement to loop through an array. We show how to use the enhanced for
statement with collections in Chapter 16. The syntax of an enhanced for
statement is:
for ( parameter : arrayName )
statement
where parameter has two parts—a type and an identifier (e.g., int number
)—and arrayName is the array through which to iterate. The type of the parameter must be consistent with the type of the elements in the array. As the next example illustrates, the identifier represents successive values in the array on successive iterations of the enhanced for
statement.
Figure 7.12 uses the enhanced for
statement (lines 12–13) to sum the integers in an array of student grades. The type specified in the parameter to the enhanced for
is int
, because array
contains int
values—the loop selects one int
value from the array during each iteration. The enhanced for
statement iterates through successive values in the array one by one. The enhanced for
header can be read as “for each iteration, assign the next element of array
to int
variable number
, then execute the following statement.” Thus, for each iteration, identifier number
represents an int
value in array
. Lines 12–13 are equivalent to the following counter-controlled repetition used in lines 12–13 of Fig. 7.5 to total the integers in array
:
Fig. 7.12. Using the enhanced for
statement to total integers in an array.
The enhanced for
statement simplifies the code for iterating through an array. Note, however, that the enhanced for
statement can be used only to obtain array elements—it cannot be used to modify elements. If your program needs to modify elements, use the traditional counter-controlled for
statement.
The enhanced for
statement can be used in place of the counter-controlled for
statement whenever code looping through an array does not require access to the counter indicating the index of the current array element. For example, totaling the integers in an array requires access only to the element values—the index of each element is irrelevant. However, if a program must use a counter for some reason other than simply to loop through an array (e.g., to print an index number next to each array element value, as in the examples earlier in this chapter), use the counter-controlled for
statement.
This section demonstrates how to pass arrays and individual array elements as arguments to methods. At the end of the section, we discuss how all types of arguments are passed to methods. To pass an array argument to a method, specify the name of the array without any brackets. For example, if array hourlyTemperatures
is declared as
double hourlyTemperatures[] = new double[ 24 ];
then the method call
modifyArray( hourlyTemperatures );
passes the reference of array hourlyTemperatures
to method modifyArray
. Every array object “knows” its own length (via its length
field). Thus, when we pass an array object’s reference into a method, we need not pass the array length as an additional argument.
For a method to receive an array reference through a method call, the method’s parameter list must specify an array parameter. For example, the method header for method modifyArray
might be written as
void modifyArray( int b[] )
indicating that modifyArray
receives the reference of an integer array in parameter b
. The method call passes array hourlyTemperature
’s reference, so when the called method uses the array variable b
, it refers to the same array object as hourlyTemperatures
in the caller.
When an argument to a method is an entire array or an individual array element of a reference type, the called method receives a copy of the reference. However, when an argument to a method is an individual array element of a primitive type, the called method receives a copy of the element’s value. Such primitive values are called scalars or scalar quantities. To pass an individual array element to a method, use the indexed name of the array element as an argument in the method call.
Figure 7.13 demonstrates the difference between passing an entire array and passing a primitive-type array element to a method. The enhanced for
statement at lines 16–17 outputs the five elements of array
(an array of int
values). Line 19 invokes method modifyArray
, passing array
as an argument. Method modifyArray
(lines 36–40) receives a copy of array
’s reference and uses the reference to multiply each of array
’s elements by 2. To prove that array
’s elements were modified, the for
statement at lines 23–24 outputs the five elements of array
again. As the output shows, method modifyArray
doubled the value of each element. Note that we could not use the enhanced for
statement in lines 38–39 because we are modifying the array’s elements.
Fig. 7.13. Passing arrays and individual array elements to methods.
Figure 7.13 next demonstrates that when a copy of an individual primitive-type array element is passed to a method, modifying the copy in the called method does not affect the original value of that element in the calling method’s array. Lines 26–28 output the value of array[ 3 ]
(8) before invoking method modifyElement
. Line 30 calls method modifyElement
and passes array[ 3 ]
as an argument. Remember that array[ 3 ]
is actually one int
value (8) in array
. Therefore, the program passes a copy of the value of array[ 3 ]
. Method modifyElement
(lines 43–48) multiplies the value received as an argument by 2, stores the result in its parameter element
, then outputs the value of element
(16). Since method parameters, like local variables, cease to exist when the method in which they are declared completes execution, the method parameter element
is destroyed when method modifyElement
terminates. Thus, when the program returns control to main
, lines 31–32 output the unmodified value of array[ 3 ]
(i.e., 8).
The preceding example demonstrated the different ways that arrays and primitive-type array elements are passed as arguments to methods. We now take a closer look at how arguments in general are passed to methods. Two ways to pass arguments in method calls in many programming languages are pass-by-value and pass-by-reference (also called call-by-value and call-by-reference). When an argument is passed by value, a copy of the argument’s value is passed to the called method. The called method works exclusively with the copy. Changes to the called method’s copy do not affect the original variable’s value in the caller.
When an argument is passed by reference, the called method can access the argument’s value in the caller directly and modify that data, if necessary. Pass-by-reference improves performance by eliminating the need to copy possibly large amounts of data.
Unlike some other languages, Java does not allow programmers to choose pass-by-value or pass-by-reference—all arguments are passed by value. A method call can pass two types of values to a method—copies of primitive values (e.g., values of type int
and double
) and copies of references to objects (including references to arrays). Objects themselves cannot be passed to methods. When a method modifies a primitive-type parameter, changes to the parameter have no effect on the original argument value in the calling method. For example, when line 30 in main
of Fig. 7.13 passes array[ 3 ]
to method modifyElement
, the statement in line 45 that doubles the value of parameter element
has no effect on the value of array[ 3 ]
in main
. This is also true for reference-type parameters. If you modify a reference-type parameter by assigning it the reference of another object, the parameter refers to the new object, but the reference stored in the caller’s variable still refers to the original object.
Although an object’s reference is passed by value, a method can still interact with the referenced object by calling its public
methods using the copy of the object’s reference. Since the reference stored in the parameter is a copy of the reference that was passed as an argument, the parameter in the called method and the argument in the calling method refer to the same object in memory. For example, in Fig. 7.13, both parameter array2
in method modifyArray
and variable array
in main
refer to the same array object in memory. Any changes made using the parameter array2
are carried out on the same object that is referenced by the variable that was passed as an argument in the calling method. In Fig. 7.13, the changes made in modifyArray
using array2
affect the contents of the array object referenced by array
in main
. Thus, with a reference to an object, the called method can manipulate the caller’s object directly.
Performance Tip 7.1
Passing arrays by reference makes sense for performance reasons. If arrays were passed by value, a copy of each element would be passed. For large, frequently passed arrays, this would waste time and consume considerable storage for the copies of the arrays.
GradeBook
Using an Array to Store GradesThis section further evolves class GradeBook
, introduced in Chapter 3 and expanded in Chapters 4–5. Recall that this class represents a grade book used by a professor to store and analyze a set of student grades. Previous versions of the class process a set of grades entered by the user, but do not maintain the individual grade values in instance variables of the class. Thus, repeat calculations require the user to reenter the same grades. One way to solve this problem would be to store each grade entered in an individual instance of the class. For example, we could create instance variables grade1
, grade2
,..., grade10
in class GradeBook
to store 10 student grades. However, the code to total the grades and determine the class average would be cumbersome, and the class would not be able to process any more than 10 grades at a time. In this section, we solve this problem by storing grades in an array.
GradeBook
The version of class GradeBook
(Fig. 7.14) presented here uses an array of integers to store the grades of several students on a single exam. This eliminates the need to repeatedly input the same set of grades. Array grades
is declared as an instance variable in line 7—therefore, each GradeBook
object maintains its own set of grades. The class’s constructor (lines 10–14) has two parameters—the name of the course and an array of grades. When an application (e.g., class GradeBookTest
in Fig. 7.15) creates a GradeBook
object, the application passes an existing int
array to the constructor, which assigns the array’s reference to instance variable grades
(line 13). The size of the array grades
is determined by the class that passes the array to the constructor. Thus, a GradeBook
object can process a variable number of grades. The grade values in the passed array could have been input from a user or read from a file on disk (see Chapter 14). In our test application, we simply initialize an array with a set of grade values (Fig. 7.15, line 10). Once the grades are stored in instance variable grades
of class GradeBook
, all the class’s methods can access the elements of grades
as often as needed to perform various calculations.
Fig. 7.14. GradeBook
class using an array to store test grades.
Fig. 7.15. GradeBookTest
creates a GradeBook
object using an array of grades, then invokes method processGrades
to analyze them.
Method processGrades
(lines 37–51) contains a series of method calls that output a report summarizing the grades. Line 40 calls method outputGrades
to print the contents of the array grades
. Lines 134–136 in method outputGrades
use a for
statement to output the students’ grades. A counter-controlled for
must be used in this case, because lines 135–136 use counter variable student
’s value to output each grade next to a particular student number (see Fig. 7.15). Although array indices start at 0, a professor would typically number students starting at 1. Thus, lines 135–136 output student + 1
as the student number to produce grade labels "Student 1: "
, "Student 2: "
, and so on.
Method processGrades
next calls method getAverage
(line 43) to obtain the average of the grades in the array. Method getAverage
(lines 86–96) uses an enhanced for
statement to total the values in array grades
before calculating the average. The parameter in the enhanced for
’s header (e.g., int grade
) indicates that for each iteration, the int
variable grade
takes on a value in the array grades
. Note that the averaging calculation in line 95 uses grades.length
to determine the number of grades being averaged.
Lines 46–47 in method processGrades
calls methods getMinimum
and getMaximum
to determine the lowest and highest grades of any student on the exam, respectively. Each of these methods uses an enhanced for
statement to loop through array grades
. Lines 59–64 in method getMinimum
loop through the array. Lines 62–63 compare each grade to lowGrade;
if a grade is less than lowGrade
, lowGrade
is set to that grade. When line 66 executes, lowGrade
contains the lowest grade in the array. Method getMaximum
(lines 70–83) works similarly to method getMinimum
.
Finally, line 50 in method processGrades
calls method outputBarChart
to print a distribution chart of the grade data using a technique similar to that in Fig. 7.6. In that example, we manually calculated the number of grades in each category (i.e., 0–9, 10–19,..., 90–99 and 100) by simply looking at a set of grades. In this example, lines 107–108 use a technique similar to that in Fig. 7.7 and Fig. 7.8 to calculate the frequency of grades in each category. Line 104 declares and creates array frequency
of 11 int
s to store the frequency of grades in each grade category. For each grade
in array grades
, lines 107–108 increment the appropriate element of the frequency
array. To determine which element to increment, line 108 divides the current grade
by 10 using integer division. For example, if grade
is 85
, line 108 increments frequency[ 8 ]
to update the count of grades in the range 80–89. Lines 111–125 next print the bar chart (see Fig. 7.15) based on the values in array frequency
. Like lines 23–24 of Fig. 7.6, lines 121–122 of Fig. 7.14 use a value in array frequency
to determine the number of asterisks to display in each bar.
GradeBookTest
That Demonstrates Class GradeBook
The application of Fig. 7.15 creates an object of class GradeBook
(Fig. 7.14) using the int
array gradesArray
(declared and initialized in line 10). Lines 12–13 pass a course name and gradesArray
to the GradeBook
constructor. Line 14 displays a welcome message, and line 15 invokes the GradeBook
object’s processGrades
method. The output summarizes the 10 grades in myGradeBook
.
Software Engineering Observation 7.1
A test harness (or test application) is responsible for creating an object of the class being tested and providing it with data. This data could come from any of several sources. Test data can be placed directly into an array with an array initializer, it can come from the user at the keyboard, it can come from a file (as you’ll see in Chapter 14), or it can come from a network (as you’ll see in Chapter 19). After passing this data to the class’s constructor to instantiate the object, the test harness should call upon the object to test its methods and manipulate its data. Gathering data in the test harness like this allows the class to manipulate data from several sources.
Multidimensional arrays with two dimensions are often used to represent tables of values consisting of information arranged in rows and columns. To identify a particular table element, we must specify two indices. By convention, the first identifies the element’s row and the second its column. Arrays that require two indices to identify a particular element are called two-dimensional arrays. (Multidimensional arrays can have more than two dimensions.) Java does not support multidimensional arrays directly, but it does allow the programmer to specify one-dimensional arrays whose elements are also one-dimensional arrays, thus achieving the same effect. Figure 7.16 illustrates a two-dimensional array a
that contains three rows and four columns (i.e., a three-by-four array). In general, an array with m rows and n columns is called an m-by-n array.
Fig. 7.16. Two-dimensional array with three rows and four columns.
Every element in array a
is identified in Fig. 7.16 by an array-access expression of the form a[ row ][ column ]
; a
is the name of the array, and row
and column
are the indices that uniquely identify each element in array a
by row and column number. Note that the names of the elements in row 0 all have a first index of 0
, and the names of the elements in column 3 all have a second index of 3
.
Like one-dimensional arrays, multidimensional arrays can be initialized in their declarations with array initializers. For example, a two-dimensional array named b
with two rows and two columns could be declared and initialized with nested array initializers as follows:
int b[][] = { { 1, 2 }, { 3, 4 } };
The initializer values are grouped by row in braces. So 1
and 2
initialize b[ 0 ][ 0 ]
and b[ 0 ][ 1 ]
, respectively, and 3
and 4
initialize b[ 1 ][ 0 ]
and b[ 1 ][ 1 ]
, respectively. The compiler counts the number of nested array initializers (represented by sets of braces within the outer braces) in the array declaration to determine the number of rows in array b
. The compiler counts the initializer values in the nested array initializer for a row to determine the number of columns in that row. As we’ll see momentarily, this means that rows can have different lengths.
Multidimensional arrays are maintained as arrays of one-dimensional arrays. Therefore array b
in the preceding declaration is actually composed of two separate one-dimensional arrays—one containing the values in the first nested initializer list {1, 2}
and one containing the values in the second nested initializer list {3, 4}
. Thus, array b
itself is an array of two elements, each a one-dimensional array of int
values.
The manner in which multidimensional arrays are represented makes them quite flexible. In fact, the lengths of the rows in array b
are not required to be the same. For example,
int b[][] = { { 1, 2 }, { 3, 4, 5 } };
creates integer array b
with two elements (determined by the number of nested array initializers) that represent the rows of the two-dimensional array. Each element of b
is a reference to a one-dimensional array of int
variables. The int
array for row 0
is a one-dimensional array with two elements (1
and 2
), and the int
array for row 1
is a one-dimensional array with three elements (3
, 4
and 5
).
A multidimensional array with the same number of columns in every row can be created with an array-creation expression. For example, the following lines declare array b
and assign it a reference to a three-by-four array:
int b[][] = new int[ 3 ][ 4 ];
In this case, we use the literal values 3
and 4
to specify the number of rows and number of columns, respectively, but this is not required. Programs can also use variables to specify array dimensions, because new
creates arrays at execution time—not at compile time. As with one-dimensional arrays, the elements of a multidimensional array are initialized when the array object is created.
A multidimensional array in which each row has a different number of columns can be created as follows:
The preceding statements create a two-dimensional array with two rows. Row 0
has five columns, and row 1
has three columns.
Figure 7.17 demonstrates initializing two-dimensional arrays with array initializers and using nested for
loops to traverse the arrays (i.e., manipulate every element of each array).
Fig. 7.17. Initializing two-dimensional arrays.
Class InitArray
’s main
declares two arrays. The declaration of array1
(line 9) uses nested array initializers to initialize the first row of the array to the values 1, 2 and 3, and the second row to the values 4, 5 and 6. The declaration of array2
(line 10) uses nested initializers of different lengths. In this case, the first row is initialized to have two elements with the values 1 and 2, respectively. The second row is initialized to have one element with the value 3. The third row is initialized to have three elements with the values 4, 5 and 6, respectively.
Lines 13 and 16 call method outputArray
(lines 20–31) to output the elements of array1
and array2
, respectively. Method outputArray
specifies the array parameter as int array[][]
to indicate that the method receives a two-dimensional array. The for
statement (lines 23–30) outputs the rows of a two-dimensional array. In the loop-continuation condition of the outer for
statement, the expression array.length
determines the number of rows in the array. In the inner for
statement, the expression array[ row ].length
determines the number of columns in the current row of the array. This condition enables the loop to determine the exact number of columns in each row.
for
StatementsMany common array manipulations use for
statements. As an example, the following for
statement sets all the elements in row 2
of array a
in Fig. 7.16 to zero:
for ( int column = 0; column < a[ 2 ].length; column++)
a[ 2 ][ column ] = 0;
We specified row 2
; therefore, we know that the first index is always 2
(0
is the first row, and 1
is the second row). This for
loop varies only the second index (i.e., the column index). If row 2
of array a
contains four elements, then the preceding for
statement is equivalent to the assignment statements
a[ 2 ][ 0 ] = 0;
a[ 2 ][ 1 ] = 0;
a[ 2 ][ 2 ] = 0;
a[ 2 ][ 3 ] = 0;
The following nested for
statement totals the values of all the elements in array a
:
This nested for
statements total the array elements one row at a time. The outer for
statement begins by setting the row
index to 0
so that the first row’s elements can be totaled by the inner for
statement. The outer for
then increments row
to 1
so that the second row can be totaled. Then, the outer for
increments row
to 2
so that the third row can be totaled. The variable total
can be displayed when the outer for
statement terminates. In the next example, we show how to process a two-dimensional array in a similar manner using nested enhanced for
statements.
GradeBook
Using a Two-Dimensional ArrayIn Section 7.8, we presented class GradeBook
(Fig. 7.14), which used a one-dimensional array to store student grades on a single exam. In most semesters, students take several exams. Professors are likely to want to analyze grades across the entire semester, both for a single student and for the class as a whole.
GradeBook
Figure 7.18 contains a version of class GradeBook
that uses a two-dimensional array grades
to store the grades of a number of students on multiple exams. Each row of the array represents a single student’s grades for the entire course, and each column represents a grade on one of the exams the students took during the course. An application such as GradeBookTest
(Fig. 7.19) passes the array as an argument to the GradeBook
constructor. In this example, we use a ten-by-three array containing ten students’ grades on three exams. Five methods perform array manipulations to process the grades. Each method is similar to its counterpart in the earlier one-dimensional array version of class GradeBook
(Fig. 7.14). Method getMinimum
(lines 52–70) determines the lowest grade of any student for the semester. Method getMaximum
(lines 73–91) determines the highest grade of any student for the semester. Method getAverage
(lines 94–104) determines a particular student’s semester average. Method outputBarChart
(lines 107–137) outputs a bar chart of the distribution of all student grades for the semester. Method outputGrades
(lines 140–164) outputs the two-dimensional array in a tabular format, along with each student’s semester average.
Fig. 7.18. GradeBook
class using a two-dimensional array to store grades.
Methods getMinimum
, getMaximum
, outputBarChart
and outputGrades
each loop through array grades
by using nested for
statements—for example, the nested enhanced for
statement from the declaration of method getMinimum
(lines 58–67). The outer enhanced for
statement iterates through the two-dimensional array grades
, assigning successive rows to parameter studentGrades
on successive iterations. The square brackets following the parameter name indicate that studentGrades
refers to a one-dimensional int
array—namely, a row in array grades
containing one student’s grades. To find the lowest overall grade, the inner for
statement compares the elements of the current one-dimensional array studentGrades
to variable lowGrade
. For example, on the first iteration of the outer for
, row 0 of grades
is assigned to parameter studentGrades
. The inner enhanced for
statement then loops through studentGrades
and compares each grade
value with lowGrade
. If a grade is less than lowGrade
, lowGrade
is set to that grade. On the second iteration of the outer enhanced for
statement, row 1 of grades
is assigned to studentGrades
, and the elements of this row are compared with variable lowGrade
. This repeats until all rows of grades
have been traversed. When execution of the nested statement is complete, lowGrade
contains the lowest grade in the two-dimensional array. Method getMaximum
works similarly to method getMinimum
.
Method outputBarChart
in Fig. 7.18 is nearly identical to the one in Fig. 7.14. However, to output the overall grade distribution for a whole semester, the method here uses a nested enhanced for
statement (lines 115–119) to create the one-dimensional array frequency
based on all the grades in the two-dimensional array. The rest of the code in each of the two outputBarChart
methods that displays the chart is identical.
Method outputGrades
(lines 140–164) also uses nested for
statements to output values of the array grades
and each student’s semester average. The output in Fig. 7.19 shows the result, which resembles the tabular format of a professor’s physical grade book. Lines 146–147 print the column headings for each test. We use a counter-controlled for
statement here so that we can identify each test with a number. Similarly, the for
statement in lines 152–163 first outputs a row label using a counter variable to identify each student (line 154). Although array indices start at 0, note that lines 147 and 154 output test + 1
and student + 1
, respectively, to produce test and student numbers starting at 1 (see Fig. 7.19). The inner for
statement in lines 156–157 uses the outer for
statement’s counter variable student
to loop through a specific row of array grades
and output each student’s test grade. Note that an enhanced for
statement can be nested in a counter-controlled for
statement, and vice versa. Finally, line 161 obtains each student’s semester average by passing the current row of grades
(i.e., grades[ student ]
) to method getAverage
.
Method getAverage
(lines 94–104) takes one argument—a one-dimensional array of test results for a particular student. When line 161 calls getAverage
, the argument is grades[ student ]
, which specifies that a particular row of the two-dimensional array grades
should be passed to getAverage
. For example, based on the array created in Fig. 7.19, the argument grades[ 1 ]
represents the three values (a one-dimensional array of grades) stored in row 1
of the two-dimensional array grades
. Recall that a two-dimensional array is an array whose elements are one-dimensional arrays. Method getAverage
calculates the sum of the array elements, divides the total by the number of test results and returns the floating-point result as a double
value (line 103).
GradeBookTest
That Demonstrates Class GradeBook
The application in Fig. 7.19 creates an object of class GradeBook
(Fig. 7.18) using the two-dimensional array of int
s named gradesArray
(declared and initialized in lines 10–19). Lines 21–22 pass a course name and gradesArray
to the GradeBook
constructor. Lines 23–24 then invoke myGradeBook
’s displayMessage
and processGrades
methods to display a welcome message and obtain a report summarizing the students’ grades for the semester, respectively.
Fig. 7.19. Creates GradeBook
object using a two-dimensional array of grades, then invokes method processGrades
to analyze them.
With variable-length argument lists, you can create methods that receive an unspecified number of arguments. An argument type followed by an ellipsis (...
) in a method’s parameter list indicates that the method receives a variable number of arguments of that particular type. This use of the ellipsis can occur only once in a parameter list, and the ellipsis, together with its type, must be placed at the end of the parameter list. While programmers can use method overloading and array passing to accomplish much of what is accomplished with “varargs,” or variable-length argument lists, using an ellipsis in a method’s parameter list is more concise.
Figure 7.20 demonstrates method average
(lines 7–16), which receives a variable-length sequence of double
s. Java treats the variable-length argument list as an array whose elements are all of the same type. Hence, the method body can manipulate the parameter numbers
as an array of double
s. Lines 12–13 use the enhanced for
loop to walk through the array and calculate the total of the double
s in the array. Line 15 accesses numbers.length
to obtain the size of the numbers
array for use in the averaging calculation. Lines 29, 31 and 33 in main
call method average
with two, three and four arguments, respectively. Method average
has a variable-length argument list (line 7), so it can average as many double
arguments as the caller passes. The output shows that each call to method average
returns the correct value.
Fig. 7.20. Using variable-length argument lists.
Placing an ellipsis indicating a variable-length argument list in the middle of a method parameter list is a syntax error. An ellipsis may be placed only at the end of the parameter list.
On many systems it is possible to pass arguments from the command line (these are known as command-line arguments) to an application by including a parameter of type String[]
(i.e., an array of String
s) in the parameter list of main
, exactly as we have done in every application in the book. By convention, this parameter is named args
. When an application is executed using the java
command, Java passes the command-line arguments that appear after the class name in the java
command to the application’s main
method as String
s in the array args
. The number of arguments passed in from the command line is obtained by accessing the array’s length
attribute. For example, the command "java MyClass a b"
passes two command-line arguments, a
and b
, to application MyClass
. Note that command-line arguments are separated by white space, not commas. When this command executes, MyClass
’s main
method receives the two-element array args
(i.e., args.length
is 2
) in which args[ 0 ]
contains the String "a"
and args[ 1 ]
contains the String "b"
. Common uses of command-line arguments include passing options and file names to applications.
Figure 7.21 uses three command-line arguments to initialize an array. When the program executes, if args.length
is not 3
, the program prints an error message and terminates (lines 9–12). Otherwise, lines 14–32 initialize and display the array based on the values of the command-line arguments.
Fig. 7.21. Initializing an array using command-line arguments.
The command-line arguments become available to main
as String
s in args
. Line 16 gets args[ 0 ]
—a String
that specifies the array size—and converts it to an int
value that the program uses to create the array in line 17. The static
method parseInt
of class Integer
converts its String
argument to an int
.
Lines 20–21 convert the args[ 1 ]
and args[ 2 ]
command-line arguments to int
values and store them in initialValue
and increment
, respectively. Lines 24–25 calculate the value for each array element.
The output of the first execution shows that the application received an insufficient number of command-line arguments. The second execution uses command-line arguments 5, 0 and 4 to specify the size of the array (5), the value of the first element (0) and the increment of each value in the array (4), respectively. The corresponding output shows that these values create an array containing the integers 0, 4, 8, 12 and 16. The output from the third execution shows that the command-line arguments 10, 1 and 2 produce an array whose 10 elements are the nonnegative odd integers from 1 to 19.
In this section, we concentrate on the collaborations (interactions) among objects. When two objects communicate with each other to accomplish a task, they are said to collaborate—objects do this by invoking one another’s operations. A collaboration consists of an object of one class sending a message to an object of another class. Messages are sent in Java via method calls.
In Section 6.19, we determined many of the operations of the classes in our system. In this section, we concentrate on the messages that invoke these operations. To identify the collaborations in the system, we return to the requirements document in Section 2.8. Recall that this document specifies the range of activities that occur during an ATM session (e.g., authenticating a user, performing transactions). The steps used to describe how the system must perform each of these tasks are our first indication of the collaborations in our system. As we proceed through this and the remaining Software Engineering Case Study sections, we may discover additional collaborations.
We identify the collaborations in the system by carefully reading the sections of the requirements document that specify what the ATM should do to authenticate a user and to perform each transaction type. For each action or step described in the requirements document, we decide which objects in our system must interact to achieve the desired result. We identify one object as the sending object and another as the receiving object. We then select one of the receiving object’s operations (identified in Section 6.19) that must be invoked by the sending object to produce the proper behavior. For example, the ATM displays a welcome message when idle. We know that an object of class Screen
displays a message to the user via its displayMessage
operation. Thus, we decide that the system can display a welcome message by employing a collaboration between the ATM
and the Screen
in which the ATM
sends a displayMessage
message to the Screen
by invoking the displayMessage
operation of class Screen
. [Note: To avoid repeating the phrase “an object of class...,” we refer to an object by using its class name preceded by an article (e.g., “a,” “an” or “the”)—for example, “the ATM
” refers to an object of class ATM
.]
Figure 7.22 lists the collaborations that can be derived from the requirements document. For each sending object, we list the collaborations in the order in which they first occur during an ATM session (i.e., the order in which they are discussed in the requirements document). We list each collaboration involving a unique sender, message and recipient only once, even though the collaborations may occur at several different times throughout an ATM session. For example, the first row in Fig. 7.22 indicates that the ATM
collaborates with the Screen
whenever the ATM
needs to display a message to the user.
Fig. 7.22. Collaborations in the ATM system.
Let’s consider the collaborations in Fig. 7.22. Before allowing a user to perform any transactions, the ATM must prompt the user to enter an account number, then to enter a PIN. It accomplishes each of these tasks by sending a displayMessage
message to the Screen
. Both of these actions refer to the same collaboration between the ATM
and the Screen
, which is already listed in Fig. 7.22. The ATM
obtains input in response to a prompt by sending a getInput
message to the Keypad
. Next, the ATM must determine whether the user-specified account number and PIN match those of an account in the database. It does so by sending an authenticateUser
message to the BankDatabase
. Recall that the BankDatabase
cannot authenticate a user directly—only the user’s Account
(i.e., the Account
that contains the account number specified by the user) can access the user’s PIN on record to authenticate the user. Figure 7.22 therefore lists a collaboration in which the BankDatabase
sends a validatePIN
message to an Account
.
After the user is authenticated, the ATM
displays the main menu by sending a series of displayMessage
messages to the Screen
and obtains input containing a menu selection by sending a getInput
message to the Keypad
. We have already accounted for these collaborations, so we do not add anything to Fig. 7.22. After the user chooses a type of transaction to perform, the ATM
executes the transaction by sending an execute
message to an object of the appropriate transaction class (i.e., a BalanceInquiry
, a Withdrawal
or a Deposit
). For example, if the user chooses to perform a balance inquiry, the ATM
sends an execute
message to a BalanceInquiry
.
Further examination of the requirements document reveals the collaborations involved in executing each transaction type. A BalanceInquiry
retrieves the amount of money available in the user’s account by sending a getAvailableBalance
message to the BankDatabase
, which responds by sending a getAvailableBalance
message to the user’s Account
. Similarly, the BalanceInquiry
retrieves the amount of money on deposit by sending a getTotalBalance
message to the BankDatabase
, which sends the same message to the user’s Account
. To display both measures of the user’s balance at the same time, the BalanceInquiry
sends a displayMessage
message to the Screen
.
A Withdrawal
sends a series of displayMessage
messages to the Screen
to display a menu of standard withdrawal amounts (i.e., $20, $40, $60, $100, $200). The Withdrawal
sends a getInput
message to the Keypad
to obtain the user’s menu selection. Next, the Withdrawal
determines whether the requested withdrawal amount is less than or equal to the user’s account balance. The Withdrawal
can obtain the amount of money available in the user’s account by sending a getAvailableBalance
message to the BankDatabase
. The Withdrawal
then tests whether the cash dispenser contains enough cash by sending an isSufficientCashAvailable
message to the CashDispenser
. A Withdrawal
sends a debit
message to the BankDatabase
to decrease the user’s account balance. The BankDatabase
in turn sends the same message to the appropriate Account
. Recall that debiting funds from an Account
decreases both the totalBalance
and the availableBalance
. To dispense the requested amount of cash, the Withdrawal
sends a dispenseCash
message to the CashDispenser
. Finally, the Withdrawal
sends a displayMessage
message to the Screen
, instructing the user to take the cash.
A Deposit
responds to an execute
message first by sending a displayMessage
message to the Screen
to prompt the user for a deposit amount. The Deposit
sends a getInput
message to the Keypad
to obtain the user’s input. The Deposit
then sends a displayMessage
message to the Screen
to tell the user to insert a deposit envelope. To determine whether the deposit slot received an incoming deposit envelope, the Deposit
sends an isEnvelopeReceived
message to the DepositSlot
. The Deposit
updates the user’s account by sending a credit
message to the BankDatabase
, which subsequently sends a credit
message to the user’s Account
. Recall that crediting funds to an Account
increases the totalBalance
but not the availableBalance
.
Now that we have identified a set of possible collaborations between the objects in our ATM system, let us graphically model these interactions using the UML. The UML provides several types of interaction diagrams that model the behavior of a system by modeling how objects interact. The communication diagram emphasizes which objects participate in collaborations. [Note: Communication diagrams were called collaboration diagrams in earlier versions of the UML.] Like the communication diagram, the sequence diagram shows collaborations among objects, but it emphasizes when messages are sent between objects over time.
Figure 7.23 shows a communication diagram that models the ATM
executing a BalanceInquiry
. Objects are modeled in the UML as rectangles containing names in the form objectName : ClassName
. In this example, which involves only one object of each type, we disregard the object name and list only a colon followed by the class name. [Note: Specifying the name of each object in a communication diagram is recommended when modeling multiple objects of the same type.] Communicating objects are connected with solid lines, and messages are passed between objects along these lines in the direction shown by arrows. The name of the message, which appears next to the arrow, is the name of an operation (i.e., a method in Java) belonging to the receiving object—think of the name as a “service” that the receiving object provides to sending objects (its “clients”).
Fig. 7.23. Communication diagram of the ATM executing a balance inquiry.
The solid filled arrow in Fig. 7.23 represents a message—or synchronous call—in the UML and a method call in Java. This arrow indicates that the flow of control is from the sending object (the ATM
) to the receiving object (a BalanceInquiry
). Since this is a synchronous call, the sending object may not send another message, or do anything at all, until the receiving object processes the message and returns control to the sending object. The sender just waits. For example, in Fig. 7.23, the ATM
calls method execute
of a BalanceInquiry
and may not send another message until execute
has finished and returns control to the ATM
. [Note: If this were an asynchronous call, represented by a stick arrowhead, the sending object would not have to wait for the receiving object to return control—it would continue sending additional messages immediately following the asynchronous call. Asynchronous calls are implemented in Java using a technique called multithreading, which is discussed in Chapter 18.]
Figure 7.24 shows a communication diagram that models the interactions among objects in the system when an object of class BalanceInquiry
executes. We assume that the object’s accountNumber
attribute contains the account number of the current user. The collaborations in Fig. 7.24 begin after the ATM
sends an execute
message to a BalanceInquiry
(i.e., the interaction modeled in Fig. 7.23). The number to the left of a message name indicates the order in which the message is passed. The sequence of messages in a communication diagram progresses in numerical order from least to greatest. In this diagram, the numbering starts with message 1
and ends with message 3
. The BalanceInquiry
first sends a getAvailableBalance
message to the BankDatabase
(message 1
), then sends a getTotalBalance
message to the BankDatabase
(message 2
). Within the parentheses following a message name, we can specify a comma-separated list of the names of the parameters sent with the message (i.e., arguments in a Java method call)—the BalanceInquiry
passes attribute accountNumber
with its messages to the BankDatabase
to indicate which Account
’s balance information to retrieve. Recall from Fig. 6.27 that operations getAvailableBalance
and getTotalBalance
of class BankDatabase
each require a parameter to identify an account. The BalanceInquiry
next displays the availableBalance
and the totalBalance
to the user by passing a displayMessage
message to the Screen
(message 3
) that includes a parameter indicating the message
to be displayed.
Fig. 7.24. Communication diagram for executing a balance inquiry.
Note that Fig. 7.24 models two additional messages passing from the BankDatabase
to an Account
(message 1.1
and message 2.1
). To provide the ATM
with the two balances of the user’s Account
(as requested by messages 1
and 2
), the BankDatabase
must pass a getAvailableBalance
and a getTotalBalance
message to the user’s Account
. Such messages passed within the handling of another message are called nested messages. The UML recommends using a decimal numbering scheme to indicate nested messages. For example, message 1.1
is the first message nested in message 1
—the BankDatabase
passes a getAvailableBalance
message during BankDatabase
’s processing of a message by the same name. [Note: If the BankDatabase
needed to pass a second nested message while processing message 1
, the second message would be numbered 1.2
.] A message may be passed only when all the nested messages from the previous message have been passed. For example, the BalanceInquiry
passes message 3
only after messages 2
and 2.1
have been passed, in that order.
The nested numbering scheme used in communication diagrams helps clarify precisely when and in what context each message is passed. For example, if we numbered the messages in Fig. 7.24 using a flat numbering scheme (i.e., 1
, 2
, 3
, 4
, 5
), someone looking at the diagram might not be able to determine that BankDatabase
passes the getAvailableBalance
message (message 1.1
) to an Account
during the BankDatabase
’s processing of message 1
, as opposed to after completing the processing of message 1
. The nested decimal numbers make it clear that the second getAvailableBalance
message (message 1.1
) is passed to an Account
within the handling of the first getAvailableBalance
message (message 1
) by the BankDatabase
.
Communication diagrams emphasize the participants in collaborations, but model their timing a bit awkwardly. A sequence diagram helps model the timing of collaborations more clearly. Figure 7.25 shows a sequence diagram modeling the sequence of interactions that occur when a Withdrawal
executes. The dotted line extending down from an object’s rectangle is that object’s lifeline, which represents the progression of time. Actions occur along an object’s lifeline in chronological order from top to bottom—an action near the top happens before one near the bottom.
Fig. 7.25. Sequence diagram that models a Withdrawal
executing.
Message passing in sequence diagrams is similar to message passing in communication diagrams. A solid arrow with a filled arrowhead extending from the sending object to the receiving object represents a message between two objects. The arrowhead points to an activation on the receiving object’s lifeline. An activation, shown as a thin vertical rectangle, indicates that an object is executing. When an object returns control, a return message, represented as a dashed line with a stick arrowhead, extends from the activation of the object returning control to the activation of the object that initially sent the message. To eliminate clutter, we omit the return-message arrows—the UML allows this practice to make diagrams more readable. Like communication diagrams, sequence diagrams can indicate message parameters between the parentheses following a message name.
The sequence of messages in Fig. 7.25 begins when a Withdrawal
prompts the user to choose a withdrawal amount by sending a displayMessage
message to the Screen
. The Withdrawal
then sends a getInput
message to the Keypad
, which obtains input from the user. We have already modeled the control logic involved in a Withdrawal
in the activity diagram of Fig. 5.22, so we do not show this logic in the sequence diagram of Fig. 7.25. Instead, we model the best-case scenario in which the balance of the user’s account is greater than or equal to the chosen withdrawal amount, and the cash dispenser contains a sufficient amount of cash to satisfy the request. For information on how to model control logic in a sequence diagram, please refer to the web resources and recommended readings listed at the end of Section 2.8.
After obtaining a withdrawal amount, the Withdrawal
sends a getAvailableBalance
message to the BankDatabase
, which in turn sends a getAvailableBalance
message to the user’s Account
. Assuming that the user’s account has enough money available to permit the transaction, the Withdrawal
next sends an isSufficientCashAvailable
message to the CashDispenser
. Assuming that there is enough cash available, the Withdrawal
decreases the balance of the user’s account (i.e., both the totalBalance
and the availableBalance
) by sending a debit
message to the BankDatabase
. The BankDatabase
responds by sending a debit
message to the user’s Account
. Finally, the Withdrawal
sends a dispenseCash
message to the CashDispenser
and a displayMessage
message to the Screen
, telling the user to remove the cash from the machine.
We have identified the collaborations among objects in the ATM system and modeled some of these collaborations using UML interaction diagrams—both communication diagrams and sequence diagrams. In the next Software Engineering Case Study section (Section 8.18), we enhance the structure of our model to complete a preliminary object-oriented design, then we begin implementing the ATM system in Java.
7.1 A(n) ____________ consists of an object of one class sending a message to an object of another class.
a. association
b. aggregation
c. collaboration
d. composition
7.2 Which form of interaction diagram emphasizes what collaborations occur? Which form emphasizes when collaborations occur?
7.3
Create a sequence diagram that models the interactions among objects in the ATM system that occur when a Deposit
executes successfully, and explain the sequence of messages modeled by the diagram.
7.1 c.
7.2 Communication diagrams emphasize what collaborations occur. Sequence diagrams emphasize when collaborations occur.
7.3 Figure 7.26 presents a sequence diagram that models the interactions between objects in the ATM system that occur when a Deposit
executes successfully. Figure 7.26 indicates that a Deposit
first sends a displayMessage
message to the Screen
to ask the user to enter a deposit amount. Next the Deposit
sends a getInput
message to the Keypad
to receive input from the user. The Deposit
then instructs the user to enter a deposit envelope by sending a displayMessage
message to the Screen
. The Deposit
next sends an isEnvelopeReceived
message to the DepositSlot
to confirm that the deposit envelope has been received by the ATM. Finally, the Deposit
increases the totalBalance
attribute (but not the availableBalance
attribute) of the user’s Account
by sending a credit
message to the BankDatabase
. The BankDatabase
responds by sending the same message to the user’s Account
.
Fig. 7.26. Sequence diagram that models a Deposit
executing.
This chapter began our introduction to data structures, exploring the use of arrays to store data in and retrieve data from lists and tables of values. The chapter examples demonstrated how to declare an array, initialize an array and refer to individual elements of an array. The chapter introduced the enhanced for
statement to iterate through arrays. We also illustrated how to pass arrays to methods and how to declare and manipulate multidimensional arrays. Finally, the chapter showed how to write methods that use variable-length argument lists and how to read arguments passed to a program from the command line.
We continue our coverage of data structures in Chapter 15 with generics, which provide the means to create general models of methods and classes that can be declared once, but used with many different data types. In Chapter 16, Collections, we introduce the Java Collections Framework, which uses generics to allow you to specify the exact types of objects that a particular data structure will store. Chapter 16 also introduces Java’s predefined data structures, which you can use instead of building your own. Chapter 16 discusses many data structures classes, including Vector
and ArrayList
, which are array-like data structures that can grow and shrink in response to a program’s changing storage requirements. The Collections API also provides class Arrays
, which contains utility methods for array manipulation. Chapter 16 uses several static
methods of class Arrays
to perform such manipulations as sorting and searching the data in an array. You’ll be able to use some of the Arrays
methods discussed in Chapter 16 after reading the current chapter, but some of the Arrays
methods require knowledge of concepts presented later in the book.
We have now introduced the basic concepts of classes, objects, control statements, methods and arrays. In Chapter 8, we take a deeper look at classes and objects.