Indexing and Enumeration

advertisement
Enumeration
COMP 14
Prasun Dewan1
11. Indexing and Enumeration
We often refer to individual elements of a sequence using indexing, e.g.: Question 1, Question 2, Question 3.We will
learn, in this chapter, an analogous concept supported by Java, illustrating it on strings, which are essentially sequences
of characters. We will also look at a new kind of looping statement, called a for loop, which is more convenient than a
while loop in problems involving indexing.
Indexing is only one technique for providing access to the elements of an ordered list. Another powerful technique is to
enumerate the elements in sequence, from the first to the last. It is less flexible than indexing because the elements have
to be accessed in order. However, for several classes of problems, it is the more appropriate technique.
We will look here at one important class of such problems: the task of scanning a string of characters. This kind of
problem arises in a variety of applications. For instance, a Java compiler scans a program to find identifiers, literals,
and operators; and a spelling checker scans a document for errors. We will study a simple instance of this problem scanning a string looking for upper case letters, which nonetheless will illustrate general techniques for scanning and is
perhaps the most complex problem discussed in this book. In solving this problem, we will write new kinds of loops those that are both counter- and event- controlled.
Indexing
To understand the need for indexing, let us return to our discussion of structured objects. Recall these are objects that
can be decomposed into simpler components. For example, an instance of ALoanPair is a structured object that can
be decomposed into the simpler components, carLoan and houseLoan of type Loan. We saw two kinds of
components: logical components or properties, which are visible outside the class of the object; and physical
components or instance variables, which should be visible only inside the class.
An important structured object we have already used is the class String: Each character in a String instance is a
logical component of it. How should these components be accessed from outside the class? Like ALoanPair,
String could define a separate getter method for each of its components:
public char getFirstChar();
public char getSecondChar();
…
A problem with this approach is that it is not clear when we should stop, that is, how many such getter methods we
should define? The number of components defined by the class String is variable, that is, different instances of it can
have different number of components. For instance, “h” has one component, while “hello world” has 11
components. We did not face this problem in the definition of ALoanPair, since all instances these types have the
same number of (logical) components.
The solution is based on the observation that we can identify string components through, not their names, but their
positions or indicies in the string. Instead of making the component identifier part of the name of its getter method,
String makes it a parameter of the getter method. Thus, String does not provide different getter methods for
different components but a single getter method that takes the component index as an argument and returns a character:
public char charAt ( int);
To be consistent with the naming scheme for getter methods of fixed structures, it would have been better to call this
method, getCharAt, but the class String, like several other predefined Java classes we will see later, was defined
before this scheme was established.
String indices start from 0 rather than 1. Thus, if:
1
1
 Copyright Prasun Dewan, 2000.
Enumeration
String s = “hello world”
s.charAt(0) == ‘h’;
s.charAt(1) == ‘e’
In general, we access the ith character of string, s, as:
s.charAt(i-1);
Not all string indices are legal. An index that is smaller (greater) than the index of its first (last) character is illegal.
Thus, both of the following accesses will raise a StringIndexBounds exception :
s.charAt(11)
s.charAt(-1)
The instance function, length(), returns the number of characters in a string. Thus:
"helloworld".length()
==
11
"".length()
==
0
We can use this function to define the range of legal indices of an arbitrary string s:
0 .. (s.length() - 1)
Sub-Strings
Besides individual characters, we may also wish to retrieve sub-strings of a string, that is, sequences of consecutive
characters that appear in the string. The Java function:
public String substring (int beginIndex, int endIndex)
when invoked on a string, s, returns a new string that consists of the character sequence starting at beginIndex and
ending at endIndex – 1, that is:
s.charAt(beginIndex) .. s.charAt(endIndex - 1)
It raises the StringIndexOutOfBounds exception if beginIndex is greater than endIndex. If they are both
equal, it returns the empty string. Thus:
"hello world".substring(4, 7)

"o w"
"hello world".substring(4, 4)

""
"hello world".substring(7, 4) throws StringIndexOutOfBounds
While String provides getter methods to read string characters and sub strings, it provides no setter method. This is
because Java strings are readonly or immutable, that is, they cannot change. A separate class, StringBuffer, which
we will not study in this course, defines mutable strings. The class String does, as we have seen before, provide the
+ operation on strings to create new strings from existing strings. Thus:
“hello” + “world” ==
“hello world”
Here, we do not change either string, but instead, create a new string that stores the result of appending the second
string to the first one.
String provides several other useful methods. Two of them, relevant to the scanning problem we will solve here, are
toUpperCase() and toLowerCase(), which can be invoked on a string to return another string that has the same
letters as the first one, except that they are all in upper case and lower case, respectively. Thus:
"Hello World".toLowerCase()
==
"hello world"
Variable-Size Type vs Variable-Size Instance
A particular string, like all the other objects we have seen so far, has a fixed structure, that is, its size (and, in fact,
contents) is fixed during its lifetime. In contrast, as we have seen above, the type String defines a variable structure,
that is, different instances of it can have different sizes.
Thus, types can define fixed or variable structure; and a type with a variable structure can have instances with fixed or
variable structures. All the primitive types and some of the object types we have seen have fixed structure. String (and
array types we will study in the next section) define variable structures, but their instances have fixed structure. Later,
we will see types with variable structure whose instances also have variable structure. We will refer to these three kinds
of types as fixed, variable, and dynamic types, respectively.
2
Enumeration
For Loop
The following program fragment illustrates string manipulation, printing the characters of a string in separate lines:
int i = 0; // initialization of loop variables
while (i < s.length()) { // continuation condition
System.out.println (s.charAt(i)); // real body
i++; // resetting loop variables
}
This is an example of a counter-controlled loop, since the variable i serves as a counter that is incremented in each
iteration of the loop. It is also an example of an index-controlled loop, which is a special case of a counter-controlled
loop in which the counter is an index .
Let us look at this code in more depth to grasp the general nature of a loop. We can decompose it into the following
components:
1. Continuation condition: the boolean expression that determines if the next iteration of the loop should be executed.
2. Initialization of loop variables: the first assignment of variables that appear in the continuation condition and are
changed in the loop body.
3. Resetting loop variables: preparing for the next iteration by reassigning one or more loop variables
4. Real body: the rest of the while body, which does the "real-work" of each iteration.
The while loop separates the continuing condition from the rest of the components, thereby ensuring we do not forget
to enter it. However, it does not separate the remaining components - we have added comments to do so in the example
above. As a result, we may forget to create one or more of these components. For instance, we may forget to reset the
loop variables, thereby creating an infinite loop.
Java provides another loop, the for loop or for statement, illustrated below, which explicitly separates all four
components:
for (int i = 1; i < s.length(); i++) {
System.out.println (s.charAt(i));
}
This loop is equivalent to the program fragment above. In general, a for statement is of the form:
for (S1;E;S2) S3
It is equivalent to:
S1;
while (E) {
S3;
S2;
}
In other words, the for loop first executes S1 and then essentially executes a while loop whose condition is E and whose
body is S3 followed by S2. S1 and S2 are expected to initialize and reset, respectively, the loop variables.
In comparison to a while loop, a for loop is more compact. More important, its structure reminds us to enter S1 and S2,
that is, initialize and reset the loop variables.
Any of the three parts of a for loop, S1, E, and S2 can, in fact, be missing. A missing S1 or S2 is considered a null
statement, while a missing E is considered true. Thus:
for (;;) {
S3;
}
is equivalent to:
while (true) {
S3;
}
The break statement can be used to exit both kinds of loops.
3
Enumeration
Scanning
Consider a more interesting string-manipulation example, which allows a user to enter a string and prints the upper case
letters in the string, as shown in Figure 1.
Figure 1 Finding Upper Case Letters
This is a simple example of a problem that involves scanning an input stream for tokens. A stream is a sequence of
values of some type, T; and a token in a sequence of consecutive stream elements in which we are interested. The
sequence of tokens generated from the input stream forms another stream called the token stream. Thus, the process of
scanning involves converting an input stream to a token stream.
Input
stream
token
token
Token
stream
Figure 2 The Concept of Scanning
We saw concept of scanning before, when we used DataInputStream.
Recall that an an instance of this class, dataIn, converted the sequence of characters entered by a user in a terminal
window to a sequence of lines, as shown in Figure 3.
Multi-line
input stream
Line 1
Line 2
Line
stream
Figure 3 Scanning the Stream of Input Characters for Lines
In the problem we have to solve, the input stream is the sequence of characters in the next input line and the tokens in
which we are interested are upper case letters. The tokens are one-character long since each uppercase letter forms a
separate token, as shown in Figure 4. Our job, in this chapter, will be to write a scanner for it.
4
Enumeration
J
o
token
h
n
F
token
.
K
e
n
token
n
e
d
y
Input
Line
Uppercase
Letters
Figure 4 Scanning a Line of Characters for Upper Case letters
Scanning problems occur in many familiar domains. As we have seen, interactive programs scan the input for lines,
integers, and other kinds of values. Similarly, file-based programs must often scan a file for various values. Word
processors scan documents for occurences of a particular string. Perhaps the most important application is in compilers,
which scan programs for identifiers, literals,operators, and other tokens.
Scanners
In general, such applications can be conceptually broken into two parts: one that converts the input stream to a token
stream, and another that actually processes the input stream by, for instance, printing it on the screen. While it is
possible to create a single object that performs both tasks, algorithms that do so tend to be messy. Therefore, we will
develop a solution that creates separate objects for the two parts. The object that performs the first task, converting the
input stream to a token stream, is called a scanner. A data input stream is an example of such an object, converting, as
we saw above, a character stream to a line stream.
Before we write our own scanner, let us look at an application that uses a data input stream as a scanner:
DataInputStream dataIn = new DataInputStream (System.in);
int product = 1;
while (true) {
int num = Integer.parseInt (dataIn.readLine());
if (num < 0) break;
product = product*num;
}
System.out.println (product);
This familiar code is fairly straightforward. It processes input lines until a negative sentinel is entered, converting each
line to the corresponding number, and printing out the product of these numbers. The reason it is simple because it is
using a separate object, dataIn, to produce the line tokens.
Let us look in more depth at how dataIn is used in the program to understand how a scanner is constructed and used
First, let us consider how the object is constructed:
DataInputStream dataIn = new DataInputStream (System.in)
System.in is a Java object that represents the stream of characters input by the user. It forms the input stream of
dataIn, and is, therefore, passsed as an argument to the constructor of the object.
The scanner produces elements of the token stream on demand, returning the next element each time the instance
method readLine() is invoked on it. The first invocation returns the first token in the stream, and each subsequent
invocation returns the token following the last one it returned.
Thus we see here how the concept of a scanner allows us to separate the parts of the program that scans for tokens and
the part that processes them. The latter part simply instantiates the scanner, passing it the input stream in the
constructor, and calls a method to repeatedly request successive elements of the output token stream.
If we could create a scanner for the input and token streams of our problem, then we would have a similar separation in
our problem.
5
Enumeration
Enumerations
Let us consider, first, the methods we should be able to invoke on the scanner. We need a method like readLine()
that returns the next token. Our tokens are characters, not lines; thus the return type of this function is a char and not a
String. We are not necessarily reading user input, simply asking for next element in the token stream; therefore, let
us name it nextElement rather than readElement. The method declaration, then is:
public char nextElement();
One of the problems with dataIn is that it does not provide a convenient way to determine if there in another line left
in the token stream. It raises an IOException if we ask for another line and the user has terminated input. This is bad
design, since exceptions should be raised on truly exceptional conditions, not on normal termination of input. To avoid
using exceptions, our algorithm had to implement the notion of a sentinel value, a detail best left out of users of the
scanner.
We will overcome this problem of DataInputStream by defining an explicit scanner operation that lets us know if
there are any more elements left in the token stream:
public boolean hasMoreElements();
Thus, the scanner implements two operations: one that returns the next element in the token stream and another that
indicates if the stream has more elements.
We can now define the interface of our scanner:
public interface CharEnumeration {
public char nextElement();
public boolean hasMoreElements();
}
Again, we are diverging from the design of the class DataInputStream. This class does not implement an interface,
thereby not giving us the associated benefits such as polymorphism and ease of change.
The interface, CharEnumeration, is an example of an enumeration interface. To better understand the nature of
such an interface, here is another example of it, which produces string rather than char tokens
public interface StringEnumeration {
public String nextElement();
public boolean hasMoreElements();
}
Thus is the interface DataInputStream should have implemented.
An enumeration interface defines an operation to return the next element in a stream and another to determine if there
are more elements in the stream. Different enumeration interfaces differ in the type of the elements produced by them.
We will use the same names for the two enumeration operations in all enumeration interfaces we create. Thus, the
headers of these operations will be:
public <Type> nextElement();
public boolean hasMoreElements();
where <Type> is some Java type – primitive or object – defining elements of the token stream.
When we study Vectors, you will see another example of this interface, called Enumeration, which is used
extensively in Java.
Enumeration is an alternative to indexing for accessing elements of (an instance of) a variable type. It is somewhat
more rigid than indexing in that the elements must be accessed sequentially from first to last. Indexing, on the other
hand, allows random access, that is, allows us to access the elements in any order. It is better to use enumeration
instead of indexing in a scanner application because (a) it has a simpler implementation and (b) lack of random access
is not a problem since the enumerated elements are processed in order.
Let us look more closely at CharEnumeration, the interface we must implement in this problem. It allows any
character to be enumerated – not just upper case letters. The reason is that the type of the enumerated elements is
6
Enumeration
char, which includes all characters – upper case letters, lowercase letters, numbers, space, and so on. It will be the
responsibility of the implementation to restrict these characters to uppercase letters. Let us call the implementation,
therefore, AnUpperCaseEnumeration. Like DataInputStream, its constructor takes the input stream as an
argument, which in this example is a string Thus:
new AnUpperCaseEnumeration (s)
creates a scanner that produces all the upper case letters in string s.
Using an Enumeration
Before we define AnUpperCaseEnumeration, let us see how it (and the interface it implements) is used in the top
level of our solution, that is, the main class:
public class UpperCasePrinter {
public static void main (String args[]) {
String input = getInput();
printUpperCase(input);
}
public static String getInput() {
System.out.println("Please enter a string");
return Keyboard.readLine();
}
public static void printUpperCase(String s) {
System.out.println("Upper Case Letters:");
printChars (new AnUpperCaseEnumeration(s));
}
public static void printChars (CharEnumeration charEnumeration) {
while (charEnumeration.hasMoreElements())
System.out.print(charEnumeration.nextElement());
}
}
Figure 5 Using an Enumeration
As before, we have been careful to separate the input and output code in the class. It is the output code that uses the
enumeration. It creates a new instance of AnUpperCaseEnumeration for scanning the input string returned by
getInput(). It then repeatedly invokes nextElement() on the scanner to output the stream of upper case letters
in the string. It stops when there are no more elements in the stream, that is, the method hasMoreElements()
returns false.
We are now ready to develop the scanner implementation, AnUpperCaseEnumeration. The following sections
discuss it in depth. However, to better appreciate the solution given here, it is best to first try to develop one on your
own.
Scanner Data Structures
The scanner's job is to scan each character of the string from left to right, looking for upper case letters. Let us first
consider the variables or data structures it needs to do its job. Clearly, it needs the string to be scanned. It also needs a
marker variable to tell it how much of the string it has scanned so far, that is, which characters it has already looked at.
7
Enumeration
J
o
h
n
F
Scanned
Line
.
K
e
n
n
e
d
y
Input
Line
UnScanned
Line
marker
Figure 6 Scanner Marker
Every time it is asked for a new element in the token stream, the scanner advances the marker.
There is some flexibility in how much of the string is scanned when the scanner is asked for a new element. Two
choices are:
 At next element: The string is scanned to the next element to be returned by the scanner. When the scanner is asked
for the new element, it returns the token at the marked position and advances the marker to the next token. Thus, in
the figure above, it would return the letter 'F' and advance the marker to 'K'.
 At last element: The string is scanned to the last element returned by the scanner. When the scanner is asked for the
new element, it advances the marker to the next token and returns this token. Thus, in the example above, it
advances the marker to the letter 'K' and returns 'K'.
These two approaches correspond to advancing a loop counter at the end or start of the loop body. In both approaches,
when the marker is advanced, there may be no more elements left to return. In this case, we will let the marker go
beyond the end of the input stream.
The first approach makes it easier to determine if a stream has more elements, since all we have to do is check if the
marker is beyond the end of the input stream. The function, hasMoreElements(), thus, can be a pure function
without the side effect of changing the global marker variable. Therefore, we will use it in our solution, and call the
marker variable nextElementPos,since it stores the position of the next element to be returned. If there is a next
element, it stores the index of this element; otherwise it stores an index beyond the string.
Scanner Algorithms
Now that we have identified the data structures of a class, let us next determine the algorithms used
by the operations defined by the class. In general, when tacking a complex problem such as scanning a
stream, it is best to first consider the data structures we need, then the algorithm, and finally the code.
The algorithm for hasMoreElements() is straightforward.
1.
Return true if nextElementPos is beyond end of string; false otherwise.
The algorithm for for nextElement() is more complicated:
1.
2.
3.
Return the element at nextElementPos.
Move nextElementPos beyond the element returned.
Skip to the next upper-case letter or end of string, whichever comes first.
To illustrate these steps, assume that nexElementPos is at ‘F’.
8
Enumeration
J
o
h
n
F
.
K
e
n
n
e
d
y
Input
Line
nextElementPos
Consider what happens when the scanner is asked for the next element. It first returns ‘F’. Next it advances the marker
beyond the returned element:
J
o
h
n
F
.
K
e
n
n
e
d
y
Input
Line
nextElementPos
All it did was increment nextElementPos, since the length of the returned token is 1 character. In general, it would
need to determine the length of the token and add it to the marker.
If the next character were an uppercase letter, it would stop here. However, as we see here, an upper case letter may not
be followed immediately by another upper case letter. Therefore, we must perform the third step of skipping nonuppercase letters.
J
o
h
n
F
.
K
e
n
n
e
d
y
Input
Line
nextElementPos
The next time the scanner is asked for an element, it will return the upper case character at the new position of
nextElementPos.
In this example, there was a remaining (unscanned) upper case letter in the string. If this was not the case,
nextElement() should skip beyond the end of the string, so that hasMoreElements() can return false.
Scanner Code
We are finally ready to code the scanner class:
public class AnUpperCaseEnumeration implements CharEnumeration {
String string;
int nextElementPos = 0;
public AnUpperCaseEnumeration(String theString) {
string = theString;
skipNonUpperCaseChars();
}
9
Enumeration
public boolean hasMoreElements() {
return nextElementPos < string.length();
}
public char nextElement() {
char retVal = string.charAt(nextElementPos);
movePastCurrentElement();
skipNonUpperCaseChars();
return retVal;
}
void movePastCurrentElement() {
nextElementPos++;
}
// keep advancing nextElementPos until we hit the next upper case or go
// beyond the end of the string.
void skipNonUpperCaseChars() {
while (nextElementPos < string.length() && !Character.isUpperCase(string.charAt(nextElementPos)))
nextElementPos++;
}
}
Figure 7 Scanner Code
Each instance of this class enumerates its own stream of uppercase letters. The instance variable, string, stores the
string being scanned, and the instance variable, nextElementPos is the marker for this string. Both variables are
initialized by the constructor, which takes as an argument the string to be scanned. The constructor stores its argument
in string, and advances nextElementPos to the first uppercase character. The other methods follow from the
algorithm described earlier.
The function nextElement() is impure – it returns the character at nextElementPos and also changes this
variable. Thus, it performs the kind of side effect we had banned in functions – changing a global variable. This is an
example of the kind of side effect readLine() also performs – returning a token from the stream and changing the
token marker. Since, as a Java programmer, we are used to such a side effect when processing a stream, we make an
exception for it – allowing it to change a global variable. As we know from using readLine(), it is convenient to
call one method to perform both tasks, since, we rarely perform one without the other.
However, we must be careful to perform them in the correct order. When nextElement() is called,
nextElementPos is at the character the function must return. Therefore, we use the marker to retrieve the character
before we calculate its new value. The retrieved character is stored in the variable retVal, which is then returned at
the end of the function.
nextElement() expects to find another element in the token stream. It is the job of the user of the enumeration to
check that it hasMoreElements() before calling nextElement().
To detect upper case letters, the class uses the method, isUpperCase, of the predefined Java class, Character,
which returns true if its argument is an upper case letter.
Event and Counter-Controlled Loop
Consider the loop in skipNonUpperCaseLeters(). When it exits depends on two factors:
 Counter: The counter, nextElementPos. If it goes past the end of the string, the loop terminates.
 Event: The event of finding an upper case letter. As soon as this event occurs, the loop terminates.
10
Enumeration
Earlier we had seen loops that were either counter-controlled or event-controlled. As we see here, it is possible to have
loops with both kinds of control. Let us look more closely at the condition of the loop:
(nextElementPos < string.length() && !Character.isUpperCase(string.charAt(nextElementPos)))
We need to make sure that:
string.charAt(nextElementPos)
does not give a subscript error, that is, the index, nextElementPos is in the range:
0 .. string.length() – 1
Are we guaranteed that? nextElementPos starts at 0 and is never decremented. Therefore, it will never be less than
0. Can it become greater than string.length() – 1 in this procedure?
If there are no remaining uppercase letters when the procedure is called, then this loop will make it string.length
on the last iteration. If it is then used to index the string, we would indeed get a subscript error. Fortunately, however,
when this happens, the first operand of &&:
nextElementPos < string.length()
becomes false. Since && is a short-circuit operation, it does not evaluate the second operand, and thus does not use
nextElementPos to index the string. As a result, we do make sure that the string index is in the correct range. This
would not be the case if we reversed the order of the operands of && or used the & operation.
In loops that are both event and index-controlled, the event condition often causes a subscript error on the last iteration,
and should therefore be made the second operand of a short-circuit operation.
We see here two more examples of methods that are not declared public. Both skipNonUpperCaseLetters()
and movePastCurrentElement() are created as internal methods because they not meant to be called directly
from outside the class. If they were declared public, we would not follow the principle of least privilege, since there
is no need for other classes to call them. In fact, if these methods were called directly by other classes, the two public
methods could fail to work correctly (see exercise 5).
Why Separate Enumerator
We have now seen both parts of our solution, the scanner user (Figure 5) and the scanner implementation (Figure 6). In
fact, we could have combined these two parts in one class, as shown in Figure 7.
public class UpperCasePrinter {
public static void main (String args[]) {
String input = getInput();
int index = 0;
System.out.println("Upper Case Letters :");
while (index < input.length()) {
char nextChar = input.charAt(index);
if (Character.isUpperCase(nextChar))
System.out.print(nextChar); // token processing
index++;
}
}
public static String getInput() {
System.out.println("Please enter a string");
return Keyboard.readLine();
}
}
Figure 8 Single-Class Solution
Here enumeration and printing of the uppercase characters are combined in the main method. The method creates a
loop that looks at all the characters of the string, from the leftmost to the rightmost, and prints all uppercase characters
it finds.
11
Enumeration
This program is much simpler and easier to understand. Its main disadvantage is that it combines enumeration of
elements of a stream and processing of them. In this example, the processing consists of simply printing the elements.
But we might wish to do other things with the tokens. For instance, we may wish to concatenate all the tokens into a
string. In our original program, we would use the same enumeration interface and class, changing only the token
processing:
String input = getInput();
String s = "";
CharEnumeration charEnumeration = new AnUpperCaseEnumeration(input));
while (charEnumeration.hasMoreElements()) {
s += charEnumeration.nextElement();
}
Figure 9 Reuse of Scanner
In the new version, as enumeration and processing are tightly integrated in the loop, we would not be able to execute
the original enumeration code. We would be forced to create a brand new loop consisting of the old enumeration code
and the new processing code:
String input = getInput();
String s = ""; // processing code
int index = 0;
while (index < input.length()) {
char nextChar = input.charAt(index);
if (Character.isUpperCase(nextChar))
s += nextChar; // processing code
index++;
}
Figure 10 Code Duplication with Single-Class Solution
Of course, we can use copy and paste capabilities of an editor to reuse lines of the previous example. But recall that cut
and paste is more work that direct reuse, and requires us to update all copies of the enumeration code if we decide to
change it.
To better appreciate the problems of combining scanning and processing of tokens, consider the more complex
example of scanning the user input for lines. Currently, Java separates these two tasks, providing a separate class,
DataInputStream, for reading lines, which we can import in different classes that process these tokens. If instead,
it had decided to combine the two tasks, it would have provided us with a loop in which tokens would be generated.
Our task would be add processing code into this loop:
initialize
while there is more input
set next line;
user code to process next line
move next line markers
This means that all of our classes that process lines would have this program fragment. Moreover, as readLine() is
deprecated and replaced by an alternative method, we would have to change all classes that have this code!
Enumerating Vs Scanning
Scanning a stream is only one way to enumerate a sequence of values. The following, alternative, implementation of
CharEnumeration illustrates that there are other ways:
public class AnotherUpperCaseEnumeration implements CharEnumeration {
char nextLetter = 'A';
public boolean hasMoreElements() {
12
Enumeration
return nextLetter <= 'Z';
}
public char nextElement() {
char retVal = nextLetter;
nextLetter = (char) (nextLetter + 1);
return retVal;
}
}
Figure 11 Enumeration Implementation without Scanning
An instance of this class enumerates all upper case letters. Instead of scanning characters in a string, it calculates them
using their ordinal numbers. The first time nextElement() is called, it returns ‘A’. Each subsequent time, it returns
the character following the one it returned last time it was called. The variable nextLetter stores the letter to be
returned by the next call to nextElement().
Once we have written this class, we can output all characters in the output stream by simply calling the print of the
original slution, passing it an instance of this class:
print (new AnotherUppercaseEnumeration());
Recall that the print invokes the two scanner operations on its scanner argument to print all characters produced by it.
We can reuse print because this method expects an instance of the type, CharEnumeration, which is
implemented by AnotherUppercaseEnumeration.
More on Interface as Syntactic Specification
Unlike the case of other interfaces we have seen before such as BMISpreadsheet of Chapter 4, the two
implementations of CharEnumeration have very different behaviors. In one case uppercase letters in a string are
print, while in the other case all uppercase letters in the alphabet are print. One cannot substitute one implementation
of an interface with the other and get the same result. All we are guaranteed is that the two implementations enumerate
a sequence of characters. This is sufficient guarantee for the print() method, which does not really look at which
characters are enumerated. It may not be for other methods. This further highlights the fact that an interface is just a
syntactic specification of a type and not a semantic specification. To take the car analogy, two cars may have identical
ways of pressing the horns, but emit different sounds when you press the horn. Some drivers may consider the horns
equivalent because they are pressed in the same way, while others may not because of the differences in the sounds
emitted. The important thing is that the same action is taken in both cars, which corresponds to writing polymorphic
methods such as print that work with different implementations of an interface.
Summary








13
A structure type is a variable type if different instances of it have different number of components.
Indexing and enumerations are used to access instances of variable types. Enumerations are easier to implement
while indexing allows random access.
For loops are more appropriate than while loops for problems indexing and other kinds of problems. Unlike while
loops, they have special components for initializing and resetting loop variables.
A problem such as scanning that involves processing of a stream should have a separate object for enumerating the
elements of the stream.
When tacking a complex problem such as scanning a stream, it is best to first consider the data structures we need,
then the algorithm, and finally the code.
Loops that are both index-controlled and event-controlled must typically use a short-circuit Boolean operation in
the loop condition to avoid subscript errors.
If we do not declare a constructor in a class, Java automatically adds to the object code of the class an argumentless constructor with a null body.
Different implementations of an interface can have very different external behaviors.
Enumeration
Exercises
1.
2.
3.
4.
What are the advantages of using for loops rather than while loops?
Why should a stream processing application have a separate object for enumerating the elements of the stream?
Write a procedure that prints the alternate characters of its string argument starting from the first one.
What would happen if nextElement() of Figure 7 is called when the enumeration does not have more
elements?
5. Suppose that the two internal methods of AnUpperCaseEnumeration had been declared as public. Show,
using an example, that a user of this class could cause it to output an incorrect token stream.
6. Define a type (both interface and class) that enumerates all odd numbers in a sentinel-terminated list of integers
input by the user. Use –1 as the value of the sentinel.
7. Print the sum of all numbers enumerated by an instance of this type.
8. Write a program that lets the user enter a sequence of words in a line and outputs all the words in it. An
input word is a sequence of lowercase or uppercase letters. An output word should consist only of
lowercase letters. You can assume that each word is followed by exactly one space and that a user will
only enter letters and spaces in an input line. To make sure you do not accidentally include a space in a
word, print a left parenthesis immediately before the first letter and a right parenthesis immediately after
it.
9.
14
Extend your solution to the previous problem by allowing the last word on a line to have no succeeding blank,
allowing more than one blank between words, and allowing non-letters between words.
Enumeration
10.
15
Download