Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Java How to Program, Fourth Edition - Deitel H., Deitel P.pdf
Скачиваний:
58
Добавлен:
24.05.2014
Размер:
14.17 Mб
Скачать

19

Data Structures

Objectives

To be able to form linked data structures using references, self-referential classes and recursion.

To be able to create and manipulate dynamic data structures, such as linked lists, queues, stacks and binary trees.

To understand various important applications of linked data structures.

To understand how to create reusable data structures with classes, inheritance and composition.

Much that I bound, I could not free;

Much that I freed returned to me.

Lee Wilson Dodd

‘Will you walk a little faster?’ said a whiting to a snail, ‘There’s a porpoise close behind us, and he’s treading on my tail.’

Lewis Carroll

There is always room at the top.

Daniel Webster

Push on—keep moving.

Thomas Morton

I think that I shall never see

A poem lovely as a tree.

Joyce Kilmer

Chapter 19

Data Structures

1095

Outline

19.1Introduction

19.2Self-Referential Classes

19.3Dynamic Memory Allocation

19.4Linked Lists

19.5Stacks

19.6Queues

19.7Trees

Summary • Terminology • Self-Review Exercises • Answers to Self-Review Exercises • Exercises • Special Section: Building Your Own Compiler

19.1 Introduction

We have studied such fixed-size data structures as singleand double-subscripted arrays. This chapter introduces dynamic data structures that grow and shrink at execution time. Linked lists are collections of data items “lined up in a row”—insertions and deletions can be made anywhere in a linked list. Stacks are important in compilers and operating systems; insertions and deletions are made only at one end of a stack—its top. Queues represent waiting lines; insertions are made at the back (also referred to as the tail) of a queue and deletions are made from the front (also referred to as the head) of a queue. Binary trees facilitate high-speed searching and sorting of data, eliminating of duplicate data items efficiently, representing file system directories, compiling expressions into machine language and many other interesting applications.

We will discuss each of the major types of data structures and implement programs that create and manipulate them. We use classes, inheritance and composition to create and package these data structures for reusability and maintainability. In Chapter 20, “Java Utilities Package and Bit Manipulation,” and Chapter 21, “Collections,” we discuss Java’s predefined classes that implement the data structures discussed in this chapter.

The chapter examples are practical programs that can be used in more advanced courses and in industrial applications. The exercises include a rich collection of useful applications.

We encourage you to attempt the major project described in the special section entitled Building Your Own Compiler. You have been using a Java compiler to translate your Java programs to bytecodes so that you could execute these programs on your computer. In this project, you will actually build your own compiler. It will read a file of statements written in a simple, yet powerful high-level language similar to early versions of the popular language Basic. Your compiler will translate these statements into a file of Simpletron Machine Language (SML) instructions—SML is the language you learned in the Chapter 7 special section, Building Your Own Computer. Your Simpletron Simulator program will then execute the SML program produced by your compiler! Implementing this project by using an object-oriented approach will give you a wonderful opportunity to exercise most of what you have learned in this book. The special section carefully walks you through the specifications of the high-level language and describes the algorithms you will need to con-

1096

Data Structures

Chapter 19

vert each type of high-level language statement into machine language instructions. If you enjoy being challenged, you might attempt the many enhancements to both the compiler and the Simpletron Simulator suggested in the exercises.

19.2 Self-Referential Classes

A self-referential class contains an instance variable that refers to another object of the same class type. For example, the definition

class Node { private int data;

private Node nextNode;

public Node( int data )

{

/* constructor body */ }

public void setData( int data ) {

/* method body */ }

public int getData()

{

/* method body */

}

public

void setNext( Node next ) { /* method body */ }

public

Node getNext()

{

/* method body */

}

}

defines class Node. This type has two private instance variables—integer data and Node reference nextNode. Member nextNode references an object of type Node, an object of the same type as the one being declared here—hence, the term “self-referential class.” Member nextNode is a linknextNode “links” an object of type Node to another object of the same type. Type Node also has five methods: a constructor that receives an integer to initialize data, a setData method to set the value data, a getData method to return the value of data, a setNext method to set the value of nextNode and a getNext method to return the value of member nextNode.

Programs can link self-referential objects together to form such useful data structures as lists, queues, stacks and trees. Figure 19.1 illustrates two self-referential objects linked together to form a list. A backslash—representing a null reference—is placed in the link member of the second self-referential object to indicate that the link does not refer to another object. The backslash is for illustration purposes; it does not correspond to the backslash character in Java. Normally, a null reference indicates the end of a data structure.

Common Programming Error 19.1

Not setting the link in the last node of a list to null is a logic error.

19.3 Dynamic Memory Allocation

Creating and maintaining dynamic data structures requires dynamic memory allocation— the ability for a program to obtain more memory space at execution time to hold new nodes and to release space no longer needed. As we have already learned, Java programs do not explicitly release dynamically allocated memory. Rather, Java performs automatic garbage collection on objects that are no longer referenced in a program.

The limit for dynamic memory allocation can be as large as the amount of available physical memory in the computer or the amount of available disk space in a virtual-memory system. Often, the limits are much smaller, because the computer’s available memory must be shared among many applications.

Chapter 19

Data Structures

1097

15 10

Fig. 19.1 Two self-referential class objects linked together.

Operator new is essential to dynamic memory allocation. Operator new takes as an operand the type of the object being dynamically allocated and returns a reference to a newly created object of that type. For example, the statement

Node nodeToAdd = new Node( 10 );

allocates the appropriate amount of memory to store a Node object and places a reference to this object in nodeToAdd. If no memory is available, new throws an OutOfMemoryError. The 10 is the Node object’s data.

The following sections discuss lists, stacks, queues and trees that use dynamic memory allocation and self-referential classes to create dynamic data structures.

19.4 Linked Lists

A linked list is a linear collection (i.e., a sequence) of self-referential class objects, called nodes, connected by reference links—hence, the term “linked” list. A program accesses a linked list via a reference to the first node of the list. The program accesses each subsequent node via the link reference stored in the previous node. By convention, the link reference in the last node of a list is set to null to mark the end of the list. Data are stored in a linked list dynamically—the list creates each node as necessary. A node can contain data of any type, including objects of other classes. Stacks and queues are also linear data structures and, as we will see, are constrained versions of linked lists. Trees are nonlinear data structures.

Lists of data can be stored in arrays, but linked lists provide several advantages. A linked list is appropriate when the number of data elements to be represented in the data structure is unpredictable. Linked lists are dynamic, so the length of a list can increase or decrease as necessary. The size of a “conventional” Java array, however, cannot be altered, because the array size is fixed at the time the program creates the array. “Conventional” arrays can become full. Linked lists become full only when the system has insufficient memory to satisfy dynamic storage allocation requests. Package java.util contains class LinkedList for implementing and manipulating linked lists that grow and shrink during program execution. We discuss class LinkedList in Chapter 21, “Collections.”

Performance Tip 19.1

An array can be declared to contain more elements than the number of items expected, but this can waste memory. Linked lists can provide better memory utilization in these situations. Linked lists allow the program to adapt at runtime.

Performance Tip 19.2

Insertion into a linked list is fast—only two references have to be modified (after you have located the place to do the insertion). All existing nodes remain at their current locations in memory.

1098

Data Structures

Chapter 19

Linked lists can be maintained in sorted order simply by inserting each new element at the proper point in the list (it does, of course, take time to locate the proper insertion point). Existing list elements do not need to be moved.

Linked list nodes normally are not stored contiguously in memory. Rather, they are logically contiguous. Figure 19.2 illustrates a linked list with several nodes. This diagram presents a singly-linked list—each node contains one reference to the next node in the list. Often, linked lists are implemented as doubly-linked lists—each node contains a reference to the next node in the list and a reference to the previous node in the list. Java’s LinkedList class (see Chapter 21) is a doubly-linked list implementation.

Performance Tip 19.3

Insertion and deletion in a sorted array can be time consuming—all the elements following the inserted or deleted element must be shifted appropriately.

Performance Tip 19.4

The elements of an array are stored contiguously in memory. This allows immediate access to any array element, because the address of any element can be calculated directly as its offset from the beginning of the array. Linked lists do not afford such immediate access to their elements—an element can be accessed only by traversing the list from the front.

Performance Tip 19.5

Using dynamic memory allocation (instead of arrays) for data structures that grow and shrink at execution time can save memory. Keep in mind, however, that references occupy space, and that dynamic memory allocation incurs the overhead of method calls.

The program of Fig. 19.3–Fig. 19.5 uses a List class to manipulate a list of miscellaneous object types. The main method of class ListTest (Fig. 19.5) creates a list of objects, inserts objects at the beginning of the list using method insertAtFront, inserts objects at the end of the list using method insertAtBack, deletes objects from the front of the list using method removeFromFront and deletes objects from the end of the list using method removeFromBack. After each insert and remove operation, ListTest calls List method print to display the current list contents. A detailed discussion of the program follows. If an attempt is made to remove an item from an empty list, an EmptyListException (Fig. 19.4) occurs.

The program of Fig. 19.3–Fig. 19.5 consists of four classes—ListNode (Fig. 19.3), List (Fig. 19.3), EmptyListException (Fig. 19.4) and ListTest (Fig. 19.5). The

List and ListNode classes are placed in package com.deitel.jhtp4.ch19, so they can be reused throughout this chapter. Encapsulated in each List object is a linked list of ListNode objects. The ListNode class (lines 6–38 of Fig. 19.3) consists of package-access members data and nextNode. ListNode member data can refer to any Object. ListNode member nextNode stores a reference to the next ListNode object in the linked list.

[Note: Many of the classes in this chapter are defined in the package com.deitel.jhtp4.ch19. Each such class should be compiled with the -d com- mand-line option to javac. When compiling the classes that are not in this package and when running the programs, be sure to use the -classpath option to javac and java, respectively.]

Chapter 19

Data Structures

1099

firstNode

lastNode

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

H D ... Q

Fig. 19.2 A graphical representation of a linked list.

1// Fig. 19.3: List.java

2 // Class ListNode and class List definitions

3 package com.deitel.jhtp4.ch19;

4

5 // class to represent one node in a list 6 class ListNode {

7

8 // package access members; List can access these directly

9Object data;

10 ListNode nextNode;

11

12// constructor to create a ListNode that refers to object

13ListNode( Object object )

14{

15this( object, null );

16}

17

18// constructor to create ListNode that refers to Object

19// and to next ListNode in List

20ListNode( Object object, ListNode node )

21{

22data = object;

23nextNode = node;

24}

25

26// return Object in this node

27Object getObject()

28{

29return data;

30}

31

32// get next node

33ListNode getNext()

34{

35return nextNode;

36}

37

38 } // end class ListNode

39

Fig. 19.3 Definitions of class ListNode and class List (part 1 of 4).

1100

Data Structures

Chapter 19

40// class List definition

41public class List {

42private ListNode firstNode;

43private ListNode lastNode;

44private String name; // String like "list" used in printing

46// construct an empty List with a name

47public List( String string )

48{

49name = string;

50firstNode = lastNode = null;

51}

52

53// construct empty List with "list" as the name

54public List()

55{

56this( "list" );

57}

58

59// Insert Object at front of List. If List is empty,

60// firstNode and lastNode will refer to same object.

61// Otherwise, firstNode refers to new node.

62public synchronized void insertAtFront( Object insertItem )

63{

64if ( isEmpty() )

65

firstNode = lastNode = new ListNode( insertItem );

66

 

67

else

68firstNode = new ListNode( insertItem, firstNode );

69}

70

71// Insert Object at end of List. If List is empty,

72// firstNode and lastNode will refer to same Object.

73// Otherwise, lastNode's nextNode refers to new node.

74public synchronized void insertAtBack( Object insertItem )

75{

76if ( isEmpty() )

77

firstNode = lastNode = new ListNode( insertItem );

78

 

79

else

80

lastNode = lastNode.nextNode =

81

new ListNode( insertItem );

82

}

83

 

84// remove first node from List

85public synchronized Object removeFromFront()

86throws EmptyListException

87{

88Object removeItem = null;

89

90// throw exception if List is empty

91if ( isEmpty() )

92 throw new EmptyListException( name );

Fig. 19.3 Definitions of class ListNode and class List (part 2 of 4).

Chapter 19

Data Structures

1101

93

94// retrieve data being removed

95removeItem = firstNode.data;

97// reset the firstNode and lastNode references

98if ( firstNode == lastNode )

99

firstNode = lastNode = null;

100

 

101

else

102

firstNode = firstNode.nextNode;

103

 

104// return removed node data

105return removeItem;

106}

107

108// Remove last node from List

109public synchronized Object removeFromBack()

110throws EmptyListException

111{

112Object removeItem = null;

113

114// throw exception if List is empty

115if ( isEmpty() )

116 throw new EmptyListException( name );

117

118// retrieve data being removed

119removeItem = lastNode.data;

121// reset firstNode and lastNode references

122if ( firstNode == lastNode )

123

firstNode = lastNode = null;

124

 

125

else {

126

 

127

// locate new last node

128

ListNode current = firstNode;

129

 

130

// loop while current node does not refer to lastNode

131

while ( current.nextNode != lastNode )

132

current = current.nextNode;

133

 

134

// current is new lastNode

135

lastNode = current;

136current.nextNode = null;

137}

138

139// return removed node data

140return removeItem;

141}

142

143// return true if List is empty

144public synchronized boolean isEmpty()

145{

Fig. 19.3 Definitions of class ListNode and class List (part 3 of 4).

1102

Data Structures

Chapter 19

146return firstNode == null;

147}

148

149// output List contents

150public synchronized void print()

151{

152if ( isEmpty() ) {

153 System.out.println( "Empty " + name );

154return;

155}

156

157 System.out.print( "The " + name + " is: " );

158

159 ListNode current = firstNode;

160

161// while not at end of list, output current node's data

162while ( current != null ) {

163 System.out.print( current.data.toString() + " " );

164current = current.nextNode;

165}

166

167System.out.println( "\n" );

168}

169

170 } // end class List

Fig. 19.3 Definitions of class ListNode and class List (part 4 of 4).

Class List contains private members firstNode (a reference to the first

ListNode in a List) and lastNode (a reference to the last ListNode in a List). The constructors (lines 47–51 and 54–57) initialize both references to null. The most important methods of class List are the synchronized methods insertAtFront (lines 62–69), insertAtBack (lines 74–82), removeFromFront (lines 85–106) and removeFromBack (lines 109–141). These methods are declared synchronized so List objects can be multithread safe when used in a multithreaded program. If one thread is modifying the contents of a List, no other thread can modify the same List object at the same time. Method isEmpty (lines 144–147) is a predicate method that determines whether the list is empty (i.e., the reference to the first node of the list is null). Predicate methods typically test a condition and do not modify the object on which they are called. If the list is empty, method isEmpty returns true; otherwise, it returns false. Method print (lines 150–168) displays the list’s contents. Both isEmpty and print are also synchronized methods.

1 // Fig. 19.4: EmptyListException.java

2 // Class EmptyListException definition

3 package com.deitel.jhtp4.ch19;

4

5 public class EmptyListException extends RuntimeException {

6

Fig. 19.4 Definition of class EmptyListException (part 1 of 2).

Chapter 19

Data Structures

1103

7// initialize an EmptyListException

8public EmptyListException( String name )

9{

10super( "The " + name + " is empty" );

11}

12

13 } // end class EmptyListException

Fig. 19.4 Definition of class EmptyListException (part 2 of 2).

1 // Fig. 19.5: ListTest.java

2 // Class ListTest

3

4// Deitel packages

5import com.deitel.jhtp4.ch19.List;

6 import com.deitel.jhtp4.ch19.EmptyListException;

7

8 public class ListTest {

9

10// test class List

11public static void main( String args[] )

12{

13List list = new List(); // create the List container

15// create objects to store in List

16Boolean bool = Boolean.TRUE;

17Character character = new Character( '$' );

18Integer integer = new Integer( 34567 );

19String string = "hello";

20

21// use List insert methods

22list.insertAtFront( bool );

23list.print();

24list.insertAtFront( character );

25list.print();

26list.insertAtBack( integer );

27list.print();

28list.insertAtBack( string );

29list.print();

30

31// use List remove methods

32Object removedObject;

33

34// remove objects from list; print after each removal

35try {

36

removedObject = list.removeFromFront();

37

System.out.println(

38

removedObject.toString() + " removed" );

39

list.print();

40

 

Fig. 19.5 Manipulating a linked list.

1104

Data Structures

Chapter 19

 

 

 

41

removedObject = list.removeFromFront();

 

42

System.out.println(

 

43

removedObject.toString() + " removed" );

 

44

list.print();

 

45

 

 

46

removedObject = list.removeFromBack();

 

47

System.out.println(

 

48

removedObject.toString() + " removed" );

 

49

list.print();

 

50

 

 

51

removedObject = list.removeFromBack();

 

52

System.out.println(

 

53

removedObject.toString() + " removed" );

 

54list.print();

55}

56

57// process exception if List is empty when attempt is

58// made to remove an item

59catch ( EmptyListException emptyListException ) {

60emptyListException.printStackTrace();

61}

62

63 } // end method main

64

65 } // end class ListTest

The list is: true

The list is: $ true

The list is: $ true 34567

The list is: $ true 34567 hello

$ removed

The list is: true 34567 hello

true removed

The list is: 34567 hello

hello removed

The list is: 34567

34567 removed Empty list

Fig. 19.5 Manipulating a linked list.

Over the next several pages, we discuss each of the methods of class List (Fig. 19.3) in detail. Method insertAtFront (lines 62–69 of Fig. 19.3) places a new node at the front of the list. The method consists of several steps (Fig. 19.6 illustrates the operation):

1. Call isEmpty to determine whether the list is empty (line 64).

Chapter 19

Data Structures

1105

(a)firstNode

7 11

new ListNode

12

(b)firstNode

7 11

new ListNode 12

Fig. 19.6 The insertAtFront operation.

2.If the list is empty, both firstNode and lastNode are set to the ListNode allocated with new and initialized with insertItem (line 65). The ListNode constructor at lines 13–16 calls the ListNode constructor at lines 20–24 to set instance variable data to refer to the insertItem passed as an argument and to set reference nextNode to null.

3.If the list is not empty, the new node is “threaded” (not to be confused with multithreading) or “linked” into the list by setting firstNode to a new ListNode object and initializing that object with insertItem and firstNode (line 68). When the ListNode constructor (lines 20–24) executes, it sets instance variable data to refer to the insertItem passed as an argument and performs the insertion by setting the nextNode reference to the ListNode passed as an argument.

In Fig. 19.6, part (a) shows the list and the new node during the insertAtFront operation and before the program threads the new node into the list. The dotted arrows in part (b) illustrate step 3 of the insertAtFront operation that enables the node containing 12 to become the new list front.

Method insertAtBack (lines 74–82 of Fig. 19.3) places a new node at the back of the list. The method consists of several steps (Fig. 19.7 illustrates the operation):

1.Call isEmpty to determine whether the list is empty (line 76).

2.If the list is empty, both firstNode and lastNode are set to the ListNode allocated with new and initialized with insertItem (line 77). The ListNode constructor at lines 13–16 calls the ListNode constructor at lines 20–24 to set instance variable data to refer to the insertItem passed as an argument and to set reference nextNode to null.

3.If the list is not empty, the new node is threaded into the list by setting LastNode and lastNode.nextNode to the ListNode that was allocated with new and initialized with insertItem (lines 80–81). When the ListNode constructor (lines 13–16) executes, it sets instance variable data to refer to the insertItem passed as an argument and sets reference nextNode to null.

1106

Data Structures

Chapter 19

(a) firstNode

lastNode new ListNode

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

12

 

 

 

 

7

 

 

 

 

11

 

 

 

5

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

(b) firstNode

 

 

 

lastNode

new ListNode

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

12 7 11 5

Fig. 19.7 A graphical representation of the insertAtBack operation.

In Fig. 19.7, part (a) shows the list and the new node during the insertAtBack operation and before the program threads the new node into the list. The dotted arrows in part (b) illustrate the steps of method insertAtBack that enable a new node to be added to the end of a list that is not empty.

Method removeFromFront (lines 85–106 of Fig. 19.3) removes the front node of the list and returns a reference to the removed data. The method throws an EmptyListException (lines 91–92) if the list is empty when the program calls this method. Otherwise, the method returns a reference to the removed data. The method consists of several steps (illustrated in Fig. 19.8):

1.Assign firstNode.data (the data being removed from the list) to reference removeItem (line 95).

2.If the objects to which firstNode and lastNode refer are equal (line 98), the list has only one element prior to the removal attempt. In this case, the method sets firstNode and lastNode to null (line 99) to “dethread” (remove) the node from the list (leaving the list empty).

3.If the list has more than one node prior to removal, then the method leaves reference lastNode as is and simply assigns firstNode.nextNode to reference firstNode (line 102). Thus, firstNode references the node that was the second node prior to the removeFromFront call.

4.Return the removeItem reference (line 105).

In Fig. 19.8, part (a) illustrates the list before the removal operation. Part (b) shows actual reference manipulations.

Chapter 19

Data Structures

1107

(a) firstNode

lastNode

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

12

 

 

 

 

7

 

 

 

 

11

 

 

 

 

5

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

(b) firstNode

 

 

 

 

 

 

 

 

 

lastNode

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

12 7 11 5

removeItem

Fig. 19.8 A graphical representation of the removeFromFront operation.

Method removeFromBack (lines 109–141 of Fig. 19.3) removes the last node of a list and returns a reference to the removed data. The method throws an EmptyListException (lines 94 and 95) if the list is empty when the program calls this method. The method consists of several steps (Fig. 19.9 illustrates the operation):

1.Assign lastNode.data (the data being removed from the list) to reference removeItem (line 119).

2.If the objects to which firstNode and lastNode refer are equal (line 122), the list has only one element prior to the removal attempt. In this case, the method sets firstNode and lastNode to null (line 123) to dethread (remove) that node from the list (leaving the list empty).

3.If the list has more than one node prior to removal, then create the ListNode reference current and assign it firstNode.

4.Now “walk the list” with current until it references the node before the last node. The while loop (lines 131–132) assigns current.nextNode to current as long as current.nextNode is not lastNode.

5.After locating the second-to-last node, assign current to lastNode (line 135) to dethread the last node from the list.

6.Set the current.nextNode to null (line 136) to terminate the list at the current node.

7.Return the removeItem reference (liner 140).

In Fig. 19.9, part (a) illustrates the list before the removal operation. Part (b) shows the actual reference manipulations.

1108

Data Structures

Chapter 19

(a) firstNode

lastNode

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

12 7 11 5

(b) firstNode

current

lastNode

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

12

 

 

 

 

7

 

 

 

 

11

 

 

 

5

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

removeItem

Fig. 19.9 A graphical representation of the removeFromBack operation.

Method print (lines 150–168) first determines whether the list is empty (lines 152– 155). If so, print displays a message indicating that the list is empty and returns control to the calling method. Otherwise, print outputs the data in the list. Line 159 creates ListNode reference current and initializes it with firstNode. While current is not null, there are more items in the list. therefore, line 163 outputs a String representation of current.data. Line 164 moves to the next node in the list by assigning the value of current.nextNode to current. This printing algorithm is identical for linked lists, stacks and queues.

19.5 Stacks

A stack is a constrained version of a linked list—new nodes can be added to a stack and removed from a stack only at the top. For this reason, a stack is referred to as a last-in, firstout (LIFO) data structure. The link member in the bottom (i.e., last) node of the stack is set to null to indicate the bottom of the stack.

Common Programming Error 19.2

Not setting the link in the bottom node of a stack to null is a common logic error.

The primary methods used to manipulate a stack are push and pop. Method push adds a new node to the top of the stack. Method pop removes a node from the top of the stack and returns the data from the popped node.

Stacks have many interesting applications. For example, when a program calls a method, the called method must know how to return to its caller, so the return address of the calling method is pushed onto the program execution stack. If a series of method calls

Chapter 19

Data Structures

1109

occurs, the successive return addresses are pushed onto the stack in last-in, first-out order so that each method can return to its caller. Stacks support recursive method calls in the same manner as they do conventional nonrecursive method calls.

The program execution stack contains the memory for local variables on each invocation of a method during a program’s execution. When the method returns to its caller, the memory for that method’s local variables is popped off the stack and those variables are no longer known to the program.

Compilers use stacks to evaluate arithmetic expressions and generate machine language code to process the expressions. The exercises in this chapter explore several applications of stacks, including using them to develop a complete working compiler. The java.util package of the Java API contains class Stack for implementing and manipulating stacks that can grow and shrink during program execution. We will discuss class Stack in Chapter 20.

We take advantage of the close relationship between lists and stacks to implement a stack class by reusing a list class. We demonstrate two different forms of reusability. First, we implement the stack class by extending class List of Fig. 19.3. Then we implement an identically performing stack class through composition by including a List object as a private member of a stack class. The list, stack and queue data structures in this chapter are implemented to store Object references to encourage further reusability. Thus, any object type can be stored in a list, stack or queue.

The application of Fig. 19.10 and Fig. 19.11 creates a stack class by extending class List of Fig. 19.3. We want the stack to have methods push, pop, isEmpty and print. Essentially, these are the methods insertAtFront, removeFromFront, isEmpty and print of class List. Of course, class List contains other methods (such as insertAtBack and removeFromBack) that we would rather not make accessible through the public interface to the stack class. It is important to remember that all methods in the public interface of class List class also are public methods of the subclass StackInheritance (Fig. 19.10). When we implement the stack’s methods, we have each StackInheritance method call the appropriate List method—method push calls insertAtFront and method pop calls removeFromFront. Class StackInheritance is defined as part of package com.deitel.jhtp4.ch19 for reuse purposes. Note that StackInheritance does not import List, because both classes are in the same package.

1 // Fig. 19.10: StackInheritance.java

2// Derived from class List

3 package com.deitel.jhtp4.ch19;

4

5 public class StackInheritance extends List {

6

7// construct stack

8public StackInheritance()

9{

10super( "stack" );

11}

12

Fig. 19.10 Class StackInheritance extends class List (part 1 of 2).

1110

Data Structures

Chapter 19

13// add object to stack

14public synchronized void push( Object object )

15{

16insertAtFront( object );

17}

18

19// remove object from stack

20public synchronized Object pop() throws EmptyListException

21{

22return removeFromFront();

23}

24

25 } // end class StackInheritance

Fig. 19.10 Class StackInheritance extends class List (part 2 of 2).

Class StackInheritanceTest’s method main (Fig. 19.11) uses class StackInheritance to instantiate a stack of Objects called stack. The program pushes onto the stack (lines 22, 24, 26 and 28) a Boolean object containing true, a Character object containing $, an Integer object containing 34567 and a String object containing hello, then popped off stack. Lines 37–42 pop the objects from the stack in an infinite while loop. When there are no objects left to pop, an method pop throws an EmptyListException, and the program displays the exception’s stack trace, which shows the methods on the program execution stack at the time the exception occurred. Note that the program uses method print (inherited from List) to output the contents of the stack.

1 // Fig. 19.11: StackInheritanceTest.java

2 // Class StackInheritanceTest

3

4// Deitel packages

5import com.deitel.jhtp4.ch19.StackInheritance;

6 import com.deitel.jhtp4.ch19.EmptyListException;

7

8 public class StackInheritanceTest {

9

10// test class StackInheritance

11public static void main( String args[] )

12{

13StackInheritance stack = new StackInheritance();

15// create objects to store in the stack

16Boolean bool = Boolean.TRUE;

17Character character = new Character( '$' );

18Integer integer = new Integer( 34567 );

19String string = "hello";

20

21// use push method

22stack.push( bool );

23stack.print();

24stack.push( character );

Fig. 19.11 A simple stack program (part 1 of 2).

Chapter 19

Data Structures

1111

25stack.print();

26stack.push( integer );

27stack.print();

28stack.push( string );

29stack.print();

30

31// remove items from stack

32try {

33

 

34

// use pop method

35

Object removedObject = null;

36

 

37

while ( true ) {

38

removedObject = stack.pop();

39

System.out.println( removedObject.toString() +

40

" popped" );

41

stack.print();

42}

43}

44

45// catch exception if stack empty when item popped

46catch ( EmptyListException emptyListException ) {

47emptyListException.printStackTrace();

48}

49

50 } // end method main

51

52 } // end class StackInheritanceTest

The stack is: true

The stack is: $ true

The stack is: 34567 $ true

The stack is: hello 34567 $ true

hello popped

The stack is: 34567 $ true

34567 popped

The stack is: $ true

$ popped

The stack is: true

true popped Empty stack

com.deitel.jhtp4.ch19.EmptyListException: The stack is empty

at com.deitel.jhtp4.ch19.List.removeFromFront(List.java:92) at com.deitel.jhtp4.ch19.StackInheritance.pop(

StackInheritance.java:22)

at StackInheritanceTest.main(StackInheritanceTest.java:38)

Fig. 19.11 A simple stack program (part 2 of 2).

1112

Data Structures

Chapter 19

Another way to implement a stack class is by reusing a list class through composition. The class in Fig. 19.12 uses a private object of class List (line 6) in the definition of class StackComposition. Composition enables us to hide the methods of class List that should not be in our stack’s public interface by providing public interface methods only to the required List methods. This technique of implementing each stack method as a call to a List method is called delegating—the stack method invoked delegates the call to the appropriate List method. In particular, StackComposition delegates calls to List methods insertAtFront, removeFromFront, isEmpty and print. In this example, we do not show class StackCompositionTest, because the only difference in this example is that we change the type of the stack from StackInheritance to StackComposition. If you execute the application from the code on the CD that accompanies this book, you will see that the output is identical.

1// Fig. 19.12: StackComposition.java

2 // Class StackComposition definition with composed List object

3 package com.deitel.jhtp4.ch19;

4

5 public class StackComposition {

6 private List stackList;

7

8// construct stack

9public StackComposition()

10{

11stackList = new List( "stack" );

12}

13

14// add object to stack

15public synchronized void push( Object object )

16{

17stackList.insertAtFront( object );

18}

19

20// remove object from stack

21public synchronized Object pop() throws EmptyListException

22{

23return stackList.removeFromFront();

24}

25

26// determine if stack is empty

27public synchronized boolean isEmpty()

28{

29return stackList.isEmpty();

30}

31

32// output stack contents

33public synchronized void print()

34{

35stackList.print();

36}

37

38 } // end class StackComposition

Fig. 19.12 A simple stack class using composition.

Chapter 19

Data Structures

1113

19.6 Queues

Another common data structure is the queue. A queue is similar to a checkout line in a su- permarket—the cashier services the person at the beginning of the line first. Other customers enter the line only at the end and wait for service. Queue nodes are removed only from the head (or front) of the queue and are inserted only at the tail (or end) of the queue. For this reason, a queue is a first-in, first-out (FIFO) data structure. The insert and remove operations are known as enqueue and dequeue.

Queues have many applications in computer systems. Most computers have only a single processor, so only one application at a time can be serviced. Entries for the other applications are placed in a queue. The entry at the front of the queue is the next to receive service. Each entry gradually advances to the front of the queue as applications receive service.

Queues are also used to support print spooling. A multiuser environment may have only a single printer. Many users may send print jobs to the printer. Even when the printer is busy, other people may still send print jobs. These are “spooled” to disk (much as thread is wound onto a spool) where they wait in a queue until the printer becomes available.

Information packets also wait in queues in computer networks. Each time a packet arrives at a network node, it must be routed to the next node on the network along the path to the packet’s final destination. The routing node routes one packet at a time, so additional packets are enqueued until the router can route them.

A file server in a computer network handles file-access requests from many clients throughout the network. Servers have a limited capacity to service requests from clients. When that capacity is exceeded, client requests wait in queues.

The application of Fig. 19.13 and Fig. 19.14 creates a queue by extending class List (Fig. 19.3). We want class QueueInheritance (Fig. 19.13) to have methods enqueue and dequeue, isEmpty and print. Note that these are essentially the List methods insertAtBack and removeFromFront. Of course, class List contains other methods (i.e., insertAtFront and removeFromBack) that we would rather not make accessible through the public interface of the queue. Remember that all methods in the public interface of the List class are also public methods of the subclass QueueInheritance. In the queue’s implementation, each method of class QueueInheritance calls the appropriate List method—method enqueue calls insertAtBack and method dequeue calls removeFromFront. Class QueueInheritance is defined in package com.deitel.jhtp4.ch19 for reuse purposes.

Common Programming Error 19.3

Not setting the link in the last node of a queue to null is a common logic error.

1// Fig. 19.13: QueueInheritance.java

2 // Class QueueInheritance extends class List

3

4// Deitel packages

5 package com.deitel.jhtp4.ch19;

6

7 public class QueueInheritance extends List {

8

Fig. 19.13 Class QueueInheritance extends class List (part 1 of 2).

1114

Data Structures

Chapter 19

9// construct queue

10public QueueInheritance()

11{

12super( "queue" );

13}

14

15// add object to queue

16public synchronized void enqueue( Object object )

17{

18insertAtBack( object );

19}

20

21// remove object from queue

22public synchronized Object dequeue() throws EmptyListException

23{

24return removeFromFront();

25}

26

27 } // end class QueueInheritance

Fig. 19.13 Class QueueInheritance extends class List (part 2 of 2).

Class QueueInheritanceTest method main (Fig. 19.14) uses class QueueInheritance to instantiate a queue of Objects called queue. Lines 22, 24, 26 and 28 enqueue a Boolean object containing true, a Character object containing $, an Integer object containing 34567 and a String object containing hello. Lines 37– 42 use an infinite while loop to dequeue the objects in first-in, first-out order. When there are no objects left to dequeue, method dequeue throws an EmptyListException, and the program displays the exception’s stack trace.

1 // Fig. 19.14: QueueInheritanceTest.java

2 // Class QueueInheritanceTest

3

4// Deitel packages

5import com.deitel.jhtp4.ch19.QueueInheritance;

6 import com.deitel.jhtp4.ch19.EmptyListException;

7

8 public class QueueInheritanceTest {

9

10// test class QueueInheritance

11public static void main( String args[] )

12{

13QueueInheritance queue = new QueueInheritance();

15// create objects to store in queue

16Boolean bool = Boolean.TRUE;

17Character character = new Character( '$' );

18Integer integer = new Integer( 34567 );

19String string = "hello";

Fig. 19.14 Processing a queue (part 1 of 3).

Chapter 19

Data Structures

1115

20

21// use enqueue method

22queue.enqueue( bool );

23queue.print();

24queue.enqueue( character );

25queue.print();

26queue.enqueue( integer );

27queue.print();

28queue.enqueue( string );

29queue.print();

30

31// remove objects from queue

32try {

33

 

34

// use dequeue method

35

Object removedObject = null;

36

 

37

while ( true ) {

38

removedObject = queue.dequeue();

39

System.out.println( removedObject.toString() +

40

" dequeued" );

41

queue.print();

42}

43}

44

45// process exception if queue empty when item removed

46catch ( EmptyListException emptyListException ) {

47emptyListException.printStackTrace();

48}

49

50 } // end method main

51

52 } // end class QueueInheritanceTest

The queue is: true

The queue is: true $

The queue is: true $ 34567

The queue is: true $ 34567 hello

true dequeued

The queue is: $ 34567 hello

$ dequeued

The queue is: 34567 hello

34567 dequeued

The queue is: hello

hello dequeued Empty queue

(continued on next page)

Fig. 19.14 Processing a queue (part 2 of 3).

1116

Data Structures

Chapter 19

(continued from previous page) com.deitel.jhtp4.ch19.EmptyListException: The queue is empty

at com.deitel.jhtp4.ch19.List.removeFromFront(List.java:92) at com.deitel.jhtp4.ch19.QueueInheritance.dequeue(

QueueInheritance.java:24)

at QueueInheritanceTest.main(QueueInheritanceTest.java:38)

Fig. 19.14 Processing a queue (part 3 of 3).

19.7 Trees

Linked lists, stacks and queues are linear data structures (i.e., sequences). A tree is a nonlinear, two-dimensional data structure with special properties. Tree nodes contain two or more links. This section discusses binary trees (Fig. 19.15)—trees whose nodes all contain two links (none, one or both of which may be null). The root node is the first node in a tree. Each link in the root node refers to a child. The left child is the first node in the left subtree, and the right child is the first node in the right subtree. The children of a specific node are called siblings. A node with no children is called a leaf node. Computer scientists normally draw trees from the root node down—exactly the opposite of the way most trees grow in nature.

Common Programming Error 19.4

Not setting to null the links in leaf nodes of a tree is a common logic error.

In our binary tree example, we create a special binary tree called a binary search tree. A binary search tree (with no duplicate node values) has the characteristic that the values in any left subtree are less than the value in the subtree’s parent node, and the values in any right subtree are greater than the value in the subtree’s parent node. Figure 19.16 illustrates a binary search tree with 12 integer values. Note that the shape of the binary search tree that corresponds to a set of data can vary, depending on the order in which the values are inserted into the tree.

B

 

A

 

 

 

D

 

 

 

 

 

 

 

 

C

Fig. 19.15 A graphical representation of a binary tree.

Chapter 19

Data Structures

1117

 

 

 

47

 

 

 

25

 

77

 

11

43

65

93

7

17

31 44

68

 

 

 

 

 

 

 

 

 

 

 

Fig. 19.16 A binary search tree containing 12 values.

The application of Fig. 19.17 and Fig. 19.18 creates a binary search tree of integers and traverses it (i.e., walks through all its nodes) three ways—using recursive inorder, preorder and postorder traversals. The program generates 10 random numbers and inserts each into the tree. Class Tree is defined in package com.deitel.jhtp4.ch19 for reuse purposes.

1// Fig. 19.17: Tree.java

2 // Definition of class TreeNode and class Tree.

3

4// Deitel packages

5 package com.deitel.jhtp4.ch19;

6

7 // class TreeNode definition

8 class TreeNode {

9

10// package access members

11TreeNode leftNode;

12int data;

13TreeNode rightNode;

14

15// initialize data and make this a leaf node

16public TreeNode( int nodeData )

17{

18data = nodeData;

19leftNode = rightNode = null; // node has no children

20}

21

22// insert TreeNode into Tree that contains nodes;

23// ignore duplicate values

24public synchronized void insert( int insertValue )

25{

26// insert in left subtree

27if ( insertValue < data ) {

28

 

29

// insert new TreeNode

30

if ( leftNode == null )

31

leftNode = new TreeNode( insertValue );

32

 

 

 

Fig. 19.17

Definitions of TreeNode and Tree for a binary search tree (part 1 of 4).

1118

Data Structures

Chapter 19

 

 

 

33

// continue traversing left subtree

 

34

else

 

35

leftNode.insert( insertValue );

 

36

}

 

37

 

 

38// insert in right subtree

39else if ( insertValue > data ) {

41

// insert new TreeNode

42

if ( rightNode == null )

43

rightNode = new TreeNode( insertValue );

44

 

45

// continue traversing right subtree

46

else

47

rightNode.insert( insertValue );

48

}

49

 

50

} // end method insert

51

 

52

} // end class TreeNode

53

 

54// class Tree definition

55public class Tree {

56private TreeNode root;

58// construct an empty Tree of integers

59public Tree()

60{

61root = null;

62}

63

64// Insert a new node in the binary search tree.

65// If the root node is null, create the root node here.

66// Otherwise, call the insert method of class TreeNode.

67public synchronized void insertNode( int insertValue )

68{

69if ( root == null )

70

root = new TreeNode( insertValue );

71

 

72

else

73root.insert( insertValue );

74}

75

76// begin preorder traversal

77public synchronized void preorderTraversal()

78{

79preorderHelper( root );

80}

81

82// recursive method to perform preorder traversal

83private void preorderHelper( TreeNode node )

84{

Fig. 19.17 Definitions of TreeNode and Tree for a binary search tree (part 2 of 4).

Chapter 19

Data Structures

1119

85 if ( node == null )

86 return;

87

88// output node data

89System.out.print( node.data + " " );

91// traverse left subtree

92preorderHelper( node.leftNode );

94// traverse right subtree

95preorderHelper( node.rightNode );

96}

97

98// begin inorder traversal

99public synchronized void inorderTraversal()

100{

101inorderHelper( root );

102}

103

104// recursive method to perform inorder traversal

105private void inorderHelper( TreeNode node )

106{

107if ( node == null )

108

return;

109

 

110// traverse left subtree

111inorderHelper( node.leftNode );

113// output node data

114System.out.print( node.data + " " );

116// traverse right subtree

117inorderHelper( node.rightNode );

118}

119

120// begin postorder traversal

121public synchronized void postorderTraversal()

122{

123postorderHelper( root );

124}

125

126// recursive method to perform postorder traversal

127private void postorderHelper( TreeNode node )

128{

129if ( node == null )

130

return;

131

 

132// traverse left subtree

133postorderHelper( node.leftNode );

135// traverse right subtree

136postorderHelper( node.rightNode );

Fig. 19.17 Definitions of TreeNode and Tree for a binary search tree (part 3 of 4).

1120

Data Structures

Chapter 19

138// output node data

139System.out.print( node.data + " " );

140}

141

142 } // end class Tree

Fig. 19.17 Definitions of TreeNode and Tree for a binary search tree (part 4 of 4).

Let us walk through the binary tree program. Method main of class TreeTest (Fig. 19.18) begins by instantiating an empty Tree object and storing it in reference tree (line 11). The program randomly generates 10 integers, each of which is inserted into the binary tree through a call to synchronized method insertNode (line 21). The program then performs preorder, inorder and postorder traversals (these will be explained shortly) of tree.

1// Fig. 19.18: TreeTest.java

2 // This program tests class Tree.

3 import com.deitel.jhtp4.ch19.Tree;

4

5 // Class TreeTest definition

6 public class TreeTest {

7

8// test class Tree

9public static void main( String args[] )

10{

11Tree tree = new Tree();

12int value;

13

14 System.out.println( "Inserting the following values: " );

15

16// insert 10 random integers from 0-99 in tree

17for ( int i = 1; i <= 10; i++ ) {

18

value = ( int ) (

Math.random() * 100 );

19

System.out.print(

value + " " );

20

 

 

21tree.insertNode( value );

22}

23

24// perform preorder traveral of tree

25System.out.println ( "\n\nPreorder traversal" );

26tree.preorderTraversal();

27

28// perform inorder traveral of tree

29System.out.println ( "\n\nInorder traversal" );

30tree.inorderTraversal();

31

32// perform postorder traveral of tree

33System.out.println ( "\n\nPostorder traversal" );

34tree.postorderTraversal();

35System.out.println();

36}

Fig. 19.18 Creating and traversing a binary tree (part 1 of 2).

Chapter 19

Data Structures

1121

37

38 } // end class TreeTest

Inserting the following values: 39 69 94 47 50 72 55 41 97 73

Preorder

traversal

 

 

39 69

47

41 50 55

94

72 73 97

Inorder traversal

 

 

39 41

47

50 55 69

72

73 94 97

Postorder traversal

 

41 55

50

47 73 72

97

94 69 39

Fig. 19.18 Creating and traversing a binary tree (part 2 of 2).

Class Tree (lines 55–142 of Fig. 19.17) has as private data root—a reference to the root node of the tree. The class contains public method insertNode (lines 67–74) to insert a new node in the tree and public methods preorderTraversal (lines 77– 80), inorderTraversal (lines 99–102) and postorderTraversal (lines 121– 124) to begin traversals of the tree. Each of these methods calls a separate recursive utility method to perform the traversal operations on the internal representation of the tree. The Tree constructor (lines 59–62) initializes root to null to indicate that the tree is empty.

The Tree class’s synchronized method insertNode (lines 67–74) first determines if the tree is empty. If so, it allocates a new TreeNode, initializes the node with the integer being inserted in the tree and assigns the new node to the root reference (line 70). If the tree is not empty, insertNode calls TreeNode method insert (lines 24–52). This method uses recursion to determine the location for the new node in the tree and inserts the node at that location. A node can be inserted only as a leaf node in a binary search tree.

The TreeNode method insert compares the value to insert with the data value in the root node. If the insert value is less than the root node data, the program determines if the left subtree is empty (line 30). If so, line 31 allocates a new TreeNode, initializes it with the integer being inserted and assigns the new node to reference leftNode. Otherwise, line 35 recursively calls insert for the left subtree to insert the value into the left subtree. If the insert value is greater than the root node data, the program determines if the right subtree is empty (line 42). If so, line 43 allocates a new TreeNode, initializes it with the integer being inserted and assigns the new node to reference rightNode. Otherwise, line 47 recursively calls insert for the right subtree to insert the value in the right subtree.

Methods inorderTraversal, preorderTraversal and postorderTraversal call helper methods inorderHelper (lines 105–118), preorderHelper

(lines 83–96) and postorderHelper (lines 127–140), respectively, to traverse the tree (Fig. 19.19) and print the node values. The purpose of the helper methods in class Tree is to allow the programmer to start a traversal without the need to obtain a reference to the root node first, then call the recursive method with that reference. Methods inorderTraversal, preorderTraversal and postorderTraversal simply take the private root reference and pass it to the appropriate helper method to initiate a traversal of the tree.

1122

Data Structures

Chapter 19

 

 

27

 

13

 

 

42

6

17

33

48

 

 

 

 

 

 

 

 

Fig. 19.19 A binary search tree.

Method inorderHelper (lines 105–118) defines the steps for an inorder traversal. Those steps are as follows:

1.Traverse the left subtree with a call to inorderHelper (line 111).

2.Process the value in the node (line 114).

3.Traverse the right subtree with a call to inorderHelper (line 117).

The inorder traversal does not process the value in a node until the values in that node’s left subtree are processed. The inorder traversal of the tree in Fig. 19.19 is

6 13 17 27 33 42 48

Note that the inorder traversal of a binary search tree prints the node values in ascending order. The process of creating a binary search tree actually sorts the data—and thus, this process is called the binary tree sort.

Method preorderHelper (lines 83–96) defines the steps for a preorder traversal. Those steps are as follows:

1.Process the value in the node (line 89).

2.Traverse the left subtree with a call to preorderHelper (line 92).

3.Traverse the right subtree with a call to preorderHelper (line 95).

The preorder traversal processes the value in each node as the node is visited. After processing the value in a given node, the preorder traversal processes the values in the left subtree, then the values in the right subtree. The preorder traversal of the tree in Fig. 19.19 is

27 13 6 17 42 33 48

Method postorderHelper (lines 127–140) defines the steps for a postorder traversal. Those steps are as follows:

1.Traverse the left subtree with a postorderHelper (line 133).

2.Traverse the right subtree with a postorderHelper (line 136).

3.Process the value in the node (line 139).

The postorder traversal processes the value in each node after the values of all that node’s children are processed. The postorderTraversal of the tree in Fig. 19.19 is

6 17 13 33 48 42 27

The binary search tree facilitates duplicate elimination. While building a tree, the insertion operation recognizes attempts to insert a duplicate value, because a duplicate follows

Chapter 19

Data Structures

1123

the same “go left” or “go right” decisions on each comparison as the original value did. Thus, the insertion operation eventually compares the duplicate with a node containing the same value. At this point, the insertion operation might simply discard the duplicate value.

Searching a binary tree for a value that matches a key value is fast, especially for tightly packed trees. In a tightly packed tree, each level contains about twice as many elements as the previous level. Figure 19.19 is a tightly packed binary tree. A binary search tree with n elements has a minimum of log2n levels. Thus, at most log2n comparisons are required either to find a match or to determine that no match exists. Searching a (tightly packed) 1000-element binary search tree requires at most 10 comparisons, because 210 > 1000. Searching a (tightly packed) 1,000,000-element binary search tree requires at most 20 comparisons, because 220 > 1,000,000.

The chapter exercises present algorithms for several other binary tree operations, such as deleting an item from a binary tree, printing a binary tree in a two-dimensional tree format and performing a level-order traversal of a binary tree. The level-order traversal of a binary tree visits the nodes of the tree row-by-row, starting at the root node level. On each level of the tree, a level-order traversal visits the nodes from left to right. Other binary tree exercises include allowing a binary search tree to contain duplicate values, inserting string values in a binary tree and determining how many levels are contained in a binary tree.

SUMMARY

Dynamic data structures can grow and shrink at execution time.

Linked lists are collections of data items “lined up in a row”—insertions and deletions can be made anywhere in a linked list.

Stacks are important in compilers and operating systems—insertions and deletions are made only at one end of a stack, its top.

Queues represent waiting lines; insertions are made at the back (also referred to as the tail) of a queue and deletions are made from the front (also referred to as the head) of a queue.

Binary trees facilitate high-speed searching and sorting of data, eliminating duplicate data items efficiently, representing file system directories and compiling expressions into machine language.

A self-referential class contains a reference that refers to another object of the same class type. Self-referential objects can be linked together to form useful data structures such as lists, queues, stacks and trees.

Creating and maintaining dynamic data structures requires dynamic memory allocation—the ability for a program to obtain more memory space at execution time to hold new nodes and to release space no longer needed.

The limit for dynamic memory allocation can be as large as the available physical memory in the computer or the amount of available disk space in a virtual-memory system. Often, the limits are much smaller because the computer’s available memory must be shared among many users.

Operator new takes as an operand the type of the object being dynamically allocated and returns a reference to a newly created object of that type. If no memory is available, new throws an OutOfMemoryError.

A linked list is a linear collection (i.e., a sequence) of self-referential class objects, called nodes, connected by reference links.

A linked list is accessed via a reference to the first node of the list. Each subsequent node is accessed via the link-reference member stored in the previous node.

1124

Data Structures

Chapter 19

By convention, the link reference in the last node of a list is set to null to mark the end of the list.

A node can contain data of any type, including objects of other classes.

Trees are nonlinear data structures.

A linked list is appropriate when the number of data elements to be represented in the data structure is unpredictable. Linked lists are dynamic, so the length of a list can increase or decrease as necessary.

The size of a “conventional” Java array cannot be altered—the size is fixed at creation time.

Linked lists can be maintained in sorted order simply by inserting each new element at the proper point in the list.

List nodes are normally not stored contiguously in memory. Rather, they are logically contiguous.

Methods that manipulate the contents of a list should be declared synchronized so list objects can be multithread safe when used in a multithreaded program. If one thread is modifying the contents of a list, no other thread is allowed to modify the same list at the same time.

A stack is a constrained version of a linked list—new nodes can be added to a stack and removed from a stack only at the top. A stack is referred to as a last-in, first-out (LIFO) data structure.

The primary methods used to manipulate a stack are push and pop. Method push adds a new node to the top of the stack. Method pop removes a node from the top of the stack and returns the data object from the popped node.

Stacks have many interesting applications. When a method call is made, the called method must know how to return to its caller, so the return address is pushed onto the program execution stack. If a series of method calls occurs, the successive return values are pushed onto the stack in last-in, first-out order so that each method can return to its caller.

The program execution stack contains the space created for local variables on each invocation of a method. When the method returns to its caller, the space for that method’s local variables is popped off the stack, and those variables are no longer known to the program.

Stacks are also used by compilers in the process of evaluating arithmetic expressions and generating machine language code to process the expressions.

The technique of implementing each stack method as a call to a List method is called delegat- ing—the stack method invoked delegates the call to the appropriate List method.

A queue is a constrained version of a list.

A queue is similar to a checkout line in a supermarket—the first person in line is serviced first, and other customers enter the line only at the end and wait to be serviced.

Queue nodes are removed only from the head of the queue and are inserted only at the tail of the queue. For this reason, a queue is referred to as a first-in, first-out (FIFO) data structure.

The insert and remove operations for a queue are known as enqueue and dequeue.

Queues have many applications in computer systems. Most computers have only a single processor, so only one user at a time can be serviced. Entries for the other users are placed in a queue. The entry at the front of the queue is the next to receive service. Each entry gradually advances to the front of the queue as users receive service.

Queues are also used to support print spooling. A multiuser environment might have only a single printer. Many users may be generating outputs to be printed. If the printer is busy, other outputs may still be generated. These are “spooled” to disk (much as thread is wound onto a spool) where they wait in a queue until the printer becomes available.

Information packets also wait in queues in computer networks. Each time a packet arrives at a network node, it must be routed to the next node on the network along the path to the packet’s final

Chapter 19

Data Structures

1125

destination. The routing node routes one packet at a time, so additional packets are enqueued until the router can route them.

A file server in a computer network handles file-access requests from many clients throughout the network. Servers have a limited capacity to service requests from clients. When that capacity is exceeded, client requests wait in queues.

A tree is a nonlinear, two-dimensional data structure.

Tree nodes contain two or more links.

A binary tree is a tree whose nodes all contain two links. The root node is the first node in a tree.

Each link in the root node refers to a child. The left child is the first node in the left subtree, and the right child is the first node in the right subtree.

The children of a node are called siblings. A node with no children is called a leaf node.

Computer scientists normally draw trees from the root node down.

A binary search tree (with no duplicate node values) has the characteristic that the values in any left subtree are less than the value in its parent node, and the values in any right subtree are greater than the value in its parent node.

A node can be inserted only as a leaf node in a binary search tree.

An inorder traversal of a binary search tree processes the node values in ascending order.

The process of creating a binary search tree actually sorts the data—and thus this process is called the binary tree sort.

In a preorder traversal, the value in each node is processed as the node is visited. After the value in a given node is processed, the values in the left subtree are processed, then the values in the right subtree are processed.

In a postorder traversal, the value in each node is processed after the values of its children.

The binary search tree facilitates duplicate elimination. As the tree is created, attempts to insert a duplicate value are recognized because a duplicate follows the same “go left” or “go right” decisions on each comparison as the original value did. Thus, the duplicate eventually is compared with a node containing the same value. The duplicate value could simply be discarded at this point.

Searching a binary tree for a value that matches a key value is also fast, especially for tightly packed trees. In a tightly packed tree, each level contains about twice as many elements as the pre-

vious level. So a binary search tree with n elements has a minimum of log2n levels, and thus at most log2n, comparisons would have to be made either to find a match or to determine that no match exists. Searching a (tightly packed) 1000-element binary search tree requires at most 10 comparisons, because 210 > 1000. Searching a (tightly packed) 1,000,000-element binary search tree requires at most 20 comparisons, because 220 > 1,000,000.

TERMINOLOGY

binary search tree

enqueue

binary tree

FIFO (first-in, first-out)

binary tree sort

head of a queue

child node

inorder traversal of a binary tree

children

inserting a node

delegating

leaf node

deleting a node

left child

dequeue

left subtree

duplicate elimination

level-order traversal of a binary tree

dynamic data structures

LIFO (last-in, first-out)

1126

Data Structures

Chapter 19

linear data structure

queue

linked list

 

recursive tree traversal algorithms

node

 

right child

nonlinear data structure

right subtree

null reference

root node

OutOfMemoryError

self-referential class

parent node

stack

pop

 

subtree

postorder traversal of a binary tree

tail of a queue

predicate method

top of a stack

preorder traversal of a binary tree

traversal

program execution stack

tree

push

 

visiting a node

SELF-REVIEW EXERCISES

19.1Fill in the blanks in each of the following statements:

a)

A self-

 

 

 

class is used to form dynamic data structures that can grow and shrink

 

at execution time.

 

 

 

 

 

 

 

 

 

 

 

 

b)

Operator

 

 

 

 

dynamically allocates memory; this operator returns a reference to

 

the allocated memory.

 

 

 

 

 

 

 

 

 

 

 

 

c)

A

 

 

is a constrained version of a linked list in which nodes can be inserted and

 

deleted only from the start of the list; this data structure returns node values in last-in,

 

first-out order.

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

d)

A method that does not alter a linked list, but simply looks at the list to determine whether

 

it is empty is referred to as a

 

 

 

method.

 

 

 

 

 

 

 

e)

A queue is referred to as a

 

data structure because the first nodes inserted are

 

the first nodes removed.

 

 

 

 

 

 

 

 

 

 

 

 

f)

The reference to the next node in a linked list is referred to as a

 

.

 

g)

Automatically reclaiming dynamically allocated memory in Java is called

 

 

.

h)

A

 

 

is a constrained version of a linked list in which nodes can be inserted only

 

at the end of the list and deleted only from the start of the list.

 

 

i)

A

 

 

is a nonlinear, two-dimensional data structure that contains nodes with

 

two or more links.

 

 

 

 

 

 

 

 

 

 

 

 

j)

A stack is referred to as a

 

data structure because the last node inserted is the

 

first node removed.

 

 

 

 

 

 

 

 

 

 

 

 

k)

The nodes of a

 

 

 

 

 

 

 

tree contain two link members.

 

 

l)

The first node of a tree is the

 

 

 

node.

 

 

 

 

 

 

 

m)

Each link in a tree node refers to a

 

 

 

 

 

or

 

 

of that node.

 

 

n)

A tree node that has no children is called a

 

 

node.

 

 

o)

The four traversal algorithms we mentioned in the text for binary search trees are

 

 

,

 

 

 

,

 

 

 

 

 

and

.

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

19.2What are the differences between a linked list and a stack?

19.3What are the differences between a stack and a queue?

19.4Perhaps a more appropriate title for this chapter would have been “Reusable Data Structures.” Comment on how each of the following entities or concepts contributes to the reusability of data structures:

a)classes

b)inheritance

c)composition

Chapter 19

Data Structures

1127

19.5 Manually provide the inorder, preorder and postorder traversals of the binary search tree of Fig. 19.20.

ANSWERS TO SELF-REVIEW EXERCISES

19.1 a) referential. b) new. c) stack. d) predicate. e) first-in, first-out (FIFO). f) link. g) garbage collection. h) queue. i) tree j) last-in, first-out (LIFO). k) binary. l) root. m) child or subtree. n) leaf. o) inorder, preorder, postorder, level order.

19.2It is possible to insert a node anywhere in a linked list and remove a node from anywhere in a linked list. Nodes in a stack may only be inserted at the top of the stack and removed from the top of a stack.

19.3A queue is a FIFO data structure that has references to both its head and its tail so that nodes may be inserted at the tail and deleted from the head. A stack is a LIFO data structure that has a single reference to the top of the stack where both insertion and deletion of nodes are performed.

19.4a) Classes allow us to instantiate as many data structure objects of a certain type (i.e., class)

as we wish.

b)Inheritance enables us to reuse code from a superclass in a subclass so that the derived class data structure is also a base-class data structure.

c)Composition enables us to reuse code by making a class object data structure a member of a composed class; if we make the class object a private member of the composed class, then the class object’s public methods are not available through the composed object’s interface.

19.5The inorder traversal is

11 18 19 28 32 40 44 49 69 71 72 83 92 97 99

The preorder traversal is

49 28 18 11 19 40 32 44 83 71 69 72 97 92 99

The postorder traversal is

11 19 18 32 44 40 28 69 72 71 92 99 97 83 49

EXERCISES

19.6 Write a program that concatenates two linked-list objects of characters. Class ListConcatenate should include a method concatenate that takes references to both list objects as arguments and concatenates the second list to the first list.

 

 

49

 

 

28

 

83

18

40

71

97

11 19

32 44

69 72

92 99

 

 

 

 

 

 

 

 

Fig. 19.20 A 15-node binary search tree.

1128

Data Structures

Chapter 19

19.7Write a program that merges two ordered-list objects of integers into a single ordered list object of integers. Method merge of class ListMerge should receive references to each of the list objects to be merged and should return a reference to the merged list object.

19.8Write a program that inserts 25 random integers from 0 to 100 in order into a linked list object. The program should calculate the sum of the elements and the floating-point average of the elements.

19.9Write a program that creates a linked list object of 10 characters, then creates a second list object containing a copy of the first list, but in reverse order.

19.10Write a program that inputs a line of text and uses a stack object to print the words of the line in reverse order.

19.11Write a program that uses a stack to determine whether a string is a palindrome (i.e., the string is spelled identically backward and forward). The program should ignore spaces and punctuation.

19.12Stacks are used by compilers to help in the process of evaluating expressions and generating machine language code. In this and the next exercise, we investigate how compilers evaluate arithmetic expressions consisting only of constants, operators and parentheses.

Humans generally write expressions like 3 + 4 and 7 / 9 in which the operator (+ or / here) is written between its operands—this is called infix notation. Computers “prefer” postfix notation, in which the operator is written to the right of its two operands. The preceding infix expressions would appear in postfix notation as 3 4 + and 7 9 /, respectively.

To evaluate a complex infix expression, a compiler would first convert the expression to postfix notation and evaluate the postfix version of the expression. Each of these algorithms requires only a single left-to-right pass of the expression. Each algorithm uses a stack object in support of its operation, and in each algorithm, the stack is used for a different purpose.

In this exercise, you will write a Java version of the infix-to-postfix conversion algorithm. In the next exercise, you will write a Java version of the postfix expression evaluation algorithm. In a later exercise, you will discover that code you write in this exercise can help you implement a complete working compiler.

Write class InfixToPostfixConverter to convert an ordinary infix arithmetic expression (assume a valid expression is entered) with single-digit integers such as

(6 + 2) * 5 - 8 / 4

to a postfix expression. The postfix version of the preceding infix expression is (note that no parenthesis are needed)

6 2 + 5 * 8 4 / -

The program should read the expression into StringBuffer infix and use one of the stack classes implemented in this chapter to help create the postfix expression in StringBuffer postfix. The algorithm for creating a postfix expression is as follows:

a)Push a left parenthesis '(' on the stack.

b)Append a right parenthesis ')' to the end of infix.

c)While the stack is not empty, read infix from left to right and do the following: If the current character in infix is a digit, append it to postfix.

If the current character in infix is a left parenthesis, push it onto the stack. If the current character in infix is an operator:

Pop operators (if there are any) at the top of the stack while they have equal or higher precedence than the current operator, and append the popped operators to postfix.

Push the current character in infix onto the stack.

Chapter 19

Data Structures

1129

If the current character in infix is a right parenthesis:

Pop operators from the top of the stack and append them to postfix until a left parenthesis is at the top of the stack.

Pop (and discard) the left parenthesis from the stack.

The following arithmetic operations are allowed in an expression: + addition

- subtraction

* multiplication / division

^ exponentiation % modulus

The stack should be maintained with stack nodes that each contain an instance variable and a reference to the next stack node. Some of the methods you may want to provide are as follows:

a)Method convertToPostfix, which converts the infix expression to postfix notation.

b)Method isOperator, which determines whether c is an operator.

c)Method precedence, which determines if the precedence of operator1 (from the infix expression) is less than, equal to or greater than the precedence of operator2 (from the stack). The method returns true if operator1 has lower precedence than operator2. Otherwise, false is returned.

d)Method stackTop (this should be added to the stack class), which returns the top value of the stack without popping the stack.

19.13Write class PostfixEvaluator, which evaluates a postfix expression (assume it is valid)

such as

6 2 + 5 * 8 4 / -

The program should read a postfix expression consisting of digits and operators into a StringBuffer. Using modified versions of the stack methods implemented earlier in this chapter, the program should scan the expression and evaluate it. The algorithm is as follows:

a)Append a right parenthesis (')') to the end of the postfix expression. When the rightparenthesis character is encountered, no further processing is necessary.

b)When the right-parenthesis character has not been encountered, read the expression from left to right.

If the current character is a digit do the following:

Push its integer value on the stack (the integer value of a digit character is its value in the computer’s character set minus the value of '0' in Unicode).

Otherwise, if the current character is an operator:

Pop the two top elements of the stack into variables x and y. Calculate y operator x.

Push the result of the calculation onto the stack.

c)When the right parenthesis is encountered in the expression, pop the top value of the stack. This is the result of the postfix expression.

[Note: In b) above (based on the sample expression at the beginning of this exercises), if the operator is '/', the top of the stack is 2 and the next element in the stack is 8, then pop 2 into x, pop 8 into y, evaluate 8 / 2 and push the result, 4, back on the stack. This note also applies to operator '-'.] The arithmetic operations allowed in an expression are:

+ addition

- subtraction

* multiplication / division

1130

Data Structures

Chapter 19

^ exponentiation % modulus

The stack should be maintained with one of the stack classes introduced in this chapter. You may want to provide the following methods:

a)Method evaluatePostfixExpression, which evaluates the postfix expression.

b)Method calculate, which evaluates the expression op1 operator op2.

c)Method push, which pushes a value onto the stack.

d)Method pop, which pops a value off the stack.

e)Method isEmpty, which determines whether the stack is empty.

f)Method printStack, which prints the stack.

19.14Modify the postfix evaluator program of Exercise 19.13 so that it can process integer operands larger than 9.

19.15(Supermarket Simulation) Write a program that simulates a checkout line at a supermarket. The line is a queue object. Customers (i.e., customer objects) arrive in random integer intervals of from 1 to 4 minutes. Also, each customer is serviced in random integer intervals of from 1 to 4 minutes. Obviously, the rates need to be balanced. If the average arrival rate is larger than the average service rate, the queue will grow infinitely. Even with “balanced” rates, randomness can still cause long lines. Run the supermarket simulation for a 12-hour day (720 minutes), using the following algorithm:

a)Choose a random integer between 1 and 4 to determine the minute at which the first customer arrives.

b)At the first customer’s arrival time, do the following:

Determine customer’s service time (random integer from 1 to 4). Begin servicing the customer.

Schedule the arrival time of the next customer (random integer 1 to 4 added to the current time).

c)For each minute of the day, consider the following:

If the next customer arrives, proceed as follows: Say so.

Enqueue the customer.

Schedule the arrival time of the next customer.

If service was completed for the last customer, do the following: Say so.

Dequeue next customer to be serviced.

Determine customer’s service completion time (random integer from 1 to 4 added to the current time).

Now run your simulation for 720 minutes and answer each of the following:

a)What is the maximum number of customers in the queue at any time?

b)What is the longest wait any one customer experiences?

c)What happens if the arrival interval is changed from 1 to 4 minutes to 1 to 3 minutes?

19.16Modify the program of Fig. 19.17 and Fig. 19.18 to allow the binary tree to contain duplicates.

19.17Write a program based on the program of Fig. 19.17 and Fig. 19.18 that inputs a line of text, tokenizes the sentence into separate words (you might want to use the StreamTokenizer class from the java.io package), inserts the words in a binary search tree and prints the inorder, preorder and post-order traversals of the tree.

19.18In this chapter, we saw that duplicate elimination is straightforward when creating a binary search tree. Describe how you would perform duplicate elimination when using only a single-sub- scripted array. Compare the performance of array-based duplicate elimination with the performance of binary-search-tree-based duplicate elimination.

Chapter 19

Data Structures

1131

19.19Write a method depth that receives a binary tree and determines how many levels it has.

19.20(Recursively Print a List Backwards) Write a method printListBackwards that recursively outputs the items in a linked list object in reverse order. Write a test program that creates a sorted list of integers and prints the list in reverse order.

19.21(Recursively Search a List) Write a method searchList that recursively searches a linked list object for a specified value. Method searchList should return a reference to the value if it is found; otherwise, null should be returned. Use your method in a test program that creates a list of integers. The program should prompt the user for a value to locate in the list.

19.22(Binary Tree Delete) In this exercise, we discuss deleting items from binary search trees. The deletion algorithm is not as straightforward as the insertion algorithm. There are three cases that are encountered when deleting an item—the item is contained in a leaf node (i.e., it has no children), the item is contained in a node that has one child or the item is contained in a node that has two children.

If the item to be deleted is contained in a leaf node, the node is deleted and the reference in the parent node is set to null.

If the item to be deleted is contained in a node with one child, the reference in the parent node is set to reference the child node and the node containing the data item is deleted. This causes the child node to take the place of the deleted node in the tree.

The last case is the most difficult. When a node with two children is deleted, another node in the tree must take its place. However, the reference in the parent node cannot simply be assigned to reference one of the children of the node to be deleted. In most cases, the resulting binary search tree would not adhere to the following characteristic of binary search trees (with no duplicate values):

The values in any left subtree are less than the value in the parent node, and the values in any right subtree are greater than the value in the parent node.

Which node is used as a replacement node to maintain this characteristic? It is either the node containing the largest value in the tree less than the value in the node being deleted, or the node containing the smallest value in the tree greater than the value in the node being deleted. Let us consider the node with the smaller value. In a binary search tree, the largest value less than a parent’s value is located in the left subtree of the parent node and is guaranteed to be contained in the rightmost node of the subtree. This node is located by walking down the left subtree to the right until the reference to the right child of the current node is null. We are now referencing the replacement node, which is either a leaf node or a node with one child to its left. If the replacement node is a leaf node, the steps to perform the deletion are as follows:

a)Store the reference to the node to be deleted in a temporary reference variable.

b)Set the reference in the parent of the node being deleted to reference the replacement node.

c)Set the reference in the parent of the replacement node to null.

d)Set the reference to the right subtree in the replacement node to reference the right subtree of the node to be deleted.

e)Set the reference to the left subtree in the replacement node to reference the left subtree of the node to be deleted.

The deletion steps for a replacement node with a left child are similar to those for a replacement node with no children, but the algorithm also must move the child into the replacement node’s position in the tree. If the replacement node is a node with a left child, the steps to perform the deletion are as follows:

a)Store the reference to the node to be deleted in a temporary reference variable.

b)Set the reference in the parent of the node being deleted to reference the replacement node.

c)Set the reference in the parent of the replacement node reference to the left child of the replacement node.

1132

Data Structures

Chapter 19

d)Set the reference to the right subtree in the replacement node reference to the right subtree of the node to be deleted.

e)Set the reference to the left subtree in the replacement node to reference the left subtree of the node to be deleted.

Write method deleteNode, which takes as its argument the value to be deleted. Method deleteNode should locate in the tree the node containing the value to be deleted and use the algorithms discussed here to delete the node. If the value is not found in the tree, the method should print a message that indicates whether the value is deleted. Modify the program of Fig. 19.17 and Fig. 19.18 to use this method. After deleting an item, call the methods inorderTraversal, preorderTraversal and postorderTraversal to confirm that the delete operation was performed correctly.

19.23(Binary Tree Search) Write method binaryTreeSearch, which attempts to locate a specified value in a binary search tree object. The method should take as an argument a search key to be located. If the node containing the search key is found, the method should return a reference to that node; otherwise, the method should return a null reference.

19.24(Level-Order Binary Tree Traversal) The program of Fig. 19.17 and Fig. 19.18 illustrated three recursive methods of traversing a binary tree—inorder, preorder and postorder traversals. This exercise presents the level-order traversal of a binary tree, in which the node values are printed level- by-level, starting at the root node level. The nodes on each level are printed from left to right. The level-order traversal is not a recursive algorithm. It uses a queue object to control the output of the nodes. The algorithm is as follows:

a)Insert the root node in the queue.

b)While there are nodes left in the queue, do the following:

Get the next node in the queue.

Print the node’s value.

If the reference to the left child of the node is not null:

Insert the left child node in the queue.

If the reference to the right child of the node is not null:

Insert the right child node in the queue.

Write method levelOrder to perform a level-order traversal of a binary tree object. Modify the program of Fig. 19.17 and Fig. 19.18 to use this method. [Note: You will also need to use queueprocessing methods of Fig. 19.13 in this program.]

19.25 (Printing Trees) Write a recursive method outputTree to display a binary tree object on the screen. The method should output the tree row-by-row, with the top of the tree at the left of the screen and the bottom of the tree toward the right of the screen. Each row is output vertically. For example, the binary tree illustrated in Fig. 19.20 is output as shown in Fig. 19.21.

Note that the rightmost leaf node appears at the top of the output in the rightmost column and the root node appears at the left of the output. Each column of output starts five spaces to the right of the preceding column. Method outputTree should receive an argument totalSpaces representing the number of spaces preceding the value to be output. (This variable should start at zero so the root node is output at the left of the screen.) The method uses a modified inorder traversal to output the tree—it starts at the rightmost node in the tree and works back to the left. The algorithm is as follows:

While the reference to the current node is not null, perform the following: Recursively call outputTree with the right subtree of the current node and

totalSpaces + 5.

Use a for structure to count from 1 to totalSpaces and output spaces. Output the value in the current node.

Set the reference to the current node to refer to the left subtree of the current node. Increment totalSpaces by 5.

Chapter 19

Data Structures

1133

99

97

92

83

72

71

69

49

44

40

32

28

19

18

11

Fig. 19.21 Sample output of recursive method outputTree.

SPECIAL SECTION: BUILDING YOUR OWN COMPILER

In Exercise 7.42 and Exercise 7.43, we introduced Simpletron Machine Language (SML), and you implemented a Simpletron computer simulator to execute programs written in SML. In this section, we build a compiler that converts programs written in a high-level programming language to SML. This section “ties” together the entire programming process. You will write programs in this new high-level language, compile these programs on the compiler you build and run the programs on the simulator you built in Exercise 7.43. You should make every effort to implement your compiler in an object-oriented manner.

19.26 (The Simple Language) Before we begin building the compiler, we discuss a simple, yet powerful high-level language similar to early versions of the popular language Basic. We call the language Simple. Every Simple statement consists of a line number and a Simple instruction. Line numbers must appear in ascending order. Each instruction begins with one of the following Simple commands: rem, input, let, print, goto, if/goto or end (see Fig. 19.22). All commands except end can be used repeatedly. Simple evaluates only integer expressions using the +, -, * and / operators. These operators have the same precedence as in Java. Parentheses can be used to change the order of evaluation of an expression.

Command

Example statement

Description

 

 

 

 

rem

50

rem this is a remark

Any text following the command rem is for

 

 

 

documentation purposes only and is ignored

 

 

 

by the compiler.

input

30

input x

Display a question mark to prompt the user to

 

 

 

enter an integer. Read that integer from the

 

 

 

keyboard and store the integer in x.

let

80

let u = 4 * (j - 56)

Assign u the value of 4 * (j - 56). Note that

 

 

 

an arbitrarily complex expression can appear

 

 

 

to the right of the equal sign.

Fig. 19.22 Simple commands (part 1 of 2).

1134

Data Structures

Chapter 19

 

 

 

Command

Example statement

Description

 

 

 

 

 

print

 

10

print w

Display the value of w.

goto

 

70

goto 45

Transfer program control to line 45.

if/goto

35

if i == z goto 80

Compare i and z for equality and transfer

 

 

 

 

program control to line 80 if the condition is

 

 

 

 

true; otherwise, continue execution with the

 

 

 

 

next statement.

end

 

99

end

Terminate program execution.

Fig. 19.22 Simple commands (part 2 of 2).

Our Simple compiler recognizes only lowercase letters. All characters in a Simple file should be lowercase. (Uppercase letters result in a syntax error unless they appear in a rem statement, in which case they are ignored.) A variable name is a single letter. Simple does not allow descriptive variable names, so variables should be explained in remarks to indicate their use in a program. Simple uses only integer variables. Simple does not have variable declarations—merely mentioning a variable name in a program causes the variable to be declared and initialized to zero. The syntax of Simple does not allow string manipulation (reading a string, writing a string, comparing strings etc.). If a string is encountered in a Simple program (after a command other than rem), the compiler generates a syntax error. The first version of our compiler assumes that Simple programs are entered correctly. Exercise 19.29 asks the reader to modify the compiler to perform syntax error checking.

Simple uses the conditional if/goto and unconditional goto statements to alter the flow of control during program execution. If the condition in the if/goto statement is true, control is transferred to a specific line of the program. The following relational and equality operators are valid in an if/goto statement: <, >, <=, >=, == or !=. The precedence of these operators is the same as in Java.

Let us now consider several programs that demonstrate Simple’s features. The first program (Fig. 19.23) reads two integers from the keyboard, stores the values in variables a and b and computes and prints their sum (stored in variable c).

The program of Fig. 19.24 determines and prints the larger of two integers. The integers are input from the keyboard and stored in s and t. The if/goto statement tests the condition s >= t. If the condition is true, control is transferred to line 90 and s is output; otherwise, t is output and control is transferred to the end statement in line 99, where the program terminates.

1

10

rem

determine and print the sum of two integers

2

15

rem

 

3

20

rem

input the two integers

4

30

input

a

5

40

input b

6

45

rem

 

7

50

rem

add integers and store result in c

8

60

let c = a + b

9

65

rem

 

10

70

rem

print the result

11

80

print

c

12

90

rem

terminate program execution

13

99

end

 

 

 

 

 

Fig. 19.23 Simple program that determines the sum of two integers.

Chapter 19

Data Structures

1135

Simple does not provide a repetition structure (such as Java’s for, while or do/while). However, Simple can simulate each of Java's repetition structures by using the if/goto and goto statements. Figure 19.25 uses a sentinel-controlled loop to calculate the squares of several integers. Each integer is input from the keyboard and stored in variable j. If the value entered is the sentinel value -9999, control is transferred to line 99, where the program terminates. Otherwise, k is assigned the square of j, k is output to the screen and control is passed to line 20, where the next integer is input.

Using the sample programs of Fig. 19.23–Fig. 19.25 as your guide, write a Simple program to accomplish each of the following:

a)Input three integers, determine their average and print the result.

b)Use a sentinel-controlled loop to input 10 integers and compute and print their sum.

c)Use a counter-controlled loop to input 7 integers, some positive and some negative, and compute and print their average.

d)Input a series of integers and determine and print the largest. The first integer input indicates how many numbers should be processed.

e)Input 10 integers and print the smallest.

f)Calculate and print the sum of the even integers from 2 to 30.

g)Calculate and print the product of the odd integers from 1 to 9.

1

10

rem

determine and print the larger of two integers

 

 

 

 

 

2

20

input

s

 

3

30

input t

 

4

32

rem

 

 

5

35

rem

test if s >= t

6

40

if s >=

t goto 90

7

45

rem

 

 

8

50

rem

t

is greater than s, so print t

9

60

print t

 

10

70

goto 99

 

11

75

rem

 

 

12

80

rem

s

is greater than or equal to t, so print s

13

90

print s

 

14

99

end

 

 

 

 

Fig. 19.24

Simple program that finds the larger of two integers.

 

 

 

 

1

10

rem

calculate the squares of several integers

2

20

input j

 

3

23

rem

 

 

4

25

rem

test for sentinel value

5

30

if j ==

-9999 goto 99

6

33

rem

 

 

7

35

rem

calculate square of j and assign result to k

8

40

let k

=

j * j

9

50

print k

 

10

53

rem

 

 

11

55

rem

loop to get next j

12

60

goto 20

 

13

99

end

 

 

 

 

 

 

 

Fig. 19.25 Calculate the squares of several integers.

1136

Data Structures

Chapter 19

19.27 (Building A Compiler; Prerequisite: Complete Exercise 7.42, Exercise 7.43, Exercise 19.12, Exercise 19.13 and Exercise 19.26) Now that the Simple language has been presented (Exercise 19.26), we discuss how to build a Simple compiler. First, we consider the process by which a Simple program is converted to SML and executed by the Simpletron simulator (see Fig. 19.26). A file containing a Simple program is read by the compiler and converted to SML code. The SML code is output to a file on disk, in which SML instructions appear one per line. The SML file is then loaded into the Simpletron simulator, and the results are sent to a file on disk and to the screen. Note that the Simpletron program developed in Exercise 7.43 took its input from the keyboard. It must be modified to read from a file so it can run the programs produced by our compiler.

The Simple compiler performs two passes of the Simple program to convert it to SML. The first pass constructs a symbol table (object) in which every line number (object), variable name (object) and constant (object) of the Simple program is stored with its type and corresponding location in the final SML code (the symbol table is discussed in detail below). The first pass also produces the corresponding SML instruction object(s) for each of the Simple statements (object, etc.). If the Simple program contains statements that transfer control to a line later in the program, the first pass results in an SML program containing some “unfinished” instructions. The second pass of the compiler locates and completes the unfinished instructions and outputs the SML program to a file.

First Pass

The compiler begins by reading one statement of the Simple program into memory. The line must be separated into its individual tokens (i.e., “pieces” of a statement) for processing and compilation. (The StreamTokenizer class from the java.io package can be used.) Recall that every statement begins with a line number followed by a command. As the compiler breaks a statement into tokens, if the token is a line number, a variable or a constant, it is placed in the symbol table. A line number is placed in the symbol table only if it is the first token in a statement. The symbolTable object is an array of tableEntry objects representing each symbol in the program. There is no restriction on the number of symbols that can appear in the program. Therefore, the symbolTable for a particular program could be large. Make the symbolTable a 100-element array for now. You can increase or decrease its size once the program is working.

Each tableEntry object contains three members. Member symbol is an integer containing the Unicode representation of a variable (remember that variable names are single characters), a line number or a constant. Member type is one of the following characters indicating the symbol’s type: 'C' for constant, 'L' for line number or 'V' for variable. Member location contains the Simpletron memory location (00 to 99) to which the symbol refers. Simpletron memory is an array of 100 integers in which SML instructions and data are stored. For a line number, the location is the element in the Simpletron memory array at which the SML instructions for the Simple statement begin. For a variable or constant, the location is the element in the Simpletron memory array in which the variable or constant is stored. Variables and constants are allocated from the end of Simpletron’s memory backwards. The first variable or constant is stored at location 99, the next at location 98, etc.

The symbol table plays an integral part in converting Simple programs to SML. We learned in Chapter 7 that an SML instruction is a four-digit integer comprised of two parts—the operation code and the operand. The operation code is determined by commands in Simple. For example, the simple command input corresponds to SML operation code 10 (read), and the Simple command print corresponds to SML operation code 11 (write). The operand is a memory location containing the data on which the operation code performs its task (e.g., operation code 10 reads a value from the keyboard and stores it in the memory location specified by the operand). The compiler searches symbolTable to determine the Simpletron memory location for each symbol, so the corresponding location can be used to complete the SML instructions.

Chapter 19

Data Structures

1137

Simple file

compiler

SML file

Simpletron

Simulator

 

 

 

output to

disk

output to screen

Fig. 19.26 Writing, compiling and executing a Simple language program.

The compilation of each Simple statement is based on its command. For example, after the line number in a rem statement is inserted in the symbol table, the remainder of the statement is ignored by the compiler because a remark is for documentation purposes only. The input, print, goto and end statements correspond to the SML read, write, branch (to a specific location) and halt instructions. Statements containing these Simple commands are converted directly to SML. (Note: A goto statement may contain an unresolved reference if the specified line number refers to a statement further into the Simple program file; this is sometimes called a forward reference.)

When a goto statement is compiled with an unresolved reference, the SML instruction must be flagged to indicate that the second pass of the compiler must complete the instruction. The flags are stored in a 100-element array flags of type int in which each element is initialized to -1. If the memory location to which a line number in the Simple program refers is not yet known (i.e., it is not in the symbol table), the line number is stored in array flags in the element with the same subscript as the incomplete instruction. The operand of the incomplete instruction is set to 00 temporarily. For example, an unconditional branch instruction (making a forward reference) is left as +4000 until the second pass of the compiler. The second pass of the compiler will be described shortly.

Compilation of if/goto and let statements is more complicated than other statements—they are the only statements that produce more than one SML instruction. For an if/goto statement, the compiler produces code to test the condition and to branch to another line if necessary. The result of the branch could be an unresolved reference. Each of the relational and equality operators can be simulated by using SML’s branch zero and branch negative instructions (or possibly a combination of both).

For a let statement, the compiler produces code to evaluate an arbitrarily complex arithmetic expression consisting of integer variables and/or constants. Expressions should separate each operand and operator with spaces. Exercise 19.12 and Exercise 19.13 presented the infix-to-postfix conversion algorithm and the postfix evaluation algorithm used by compilers to evaluate expressions. Before proceeding with your compiler, you should complete each of these exercises. When a compiler encounters an expression, it converts the expression from infix notation to postfix notation, then evaluates the postfix expression.

How is it that the compiler produces the machine language to evaluate an expression containing variables? The postfix evaluation algorithm contains a “hook” where the compiler can generate SML instructions rather than actually evaluating the expression. To enable this “hook” in the compiler, the postfix evaluation algorithm must be modified to search the symbol table for each symbol it encounters (and possibly insert it), determine the symbol’s corresponding memory location and push the memory location on the stack (instead of the symbol). When an operator is encountered in the postfix expression, the two memory locations at the top of the stack are popped, and machine language for effecting the operation is produced by using the memory locations as operands. The result of each subexpression is stored in a temporary location in memory and pushed back onto the stack so the

1138

Data Structures

Chapter 19

evaluation of the postfix expression can continue. When postfix evaluation is complete, the memory location containing the result is the only location left on the stack. This is popped, and SML instructions are generated to assign the result to the variable at the left of the let statement.

Second Pass

The second pass of the compiler performs two tasks: Resolve any unresolved references and output the SML code to a file. Resolution of references occurs as follows:

a)Search the flags array for an unresolved reference (i.e., an element with a value other than -1).

b)Locate the object in array symbolTable containing the symbol stored in the flags array (be sure that the type of the symbol is 'L' for line number).

c)Insert the memory location from member location into the instruction with the unresolved reference (remember that an instruction containing an unresolved reference has operand 00).

d)Repeat steps (a), (b) and (c) until the end of the flags array is reached.

After the resolution process is complete, the entire array containing the SML code is output to a disk file with one SML instruction per line. This file can be read by the Simpletron for execution (after the simulator is modified to read its input from a file). Compiling your first Simple program into an SML file and executing that file should give you a real sense of personal accomplishment.

A Complete Example

The following example illustrates complete conversion of a Simple program to SML as it will be performed by the Simple compiler. Consider a Simple program that inputs an integer and sums the values from 1 to that integer. The program and the SML instructions produced by the first pass of the Simple compiler are illustrated in Fig. 19.27. The symbol table constructed by the first pass is shown in Fig. 19.28. .

 

 

 

SML location

 

Simple program

and instruction

Description

 

 

 

5 rem sum 1 to x

none

rem ignored

10

input x

00

+1099

read x into location 99

15

rem check y == x

none

rem ignored

20

if y == x goto 60

01

+2098

load y (98) into accumulator

 

 

 

02

+3199

sub x (99) from accumulator

 

 

 

03

+4200

branch zero to unresolved location

25

rem

increment y

none

rem ignored

30

let y = y + 1

04

+2098

load y into accumulator

 

 

 

05

+3097

add 1 (97) to accumulator

 

 

 

06

+2196

store in temporary location 96

 

 

 

07

+2096

load from temporary location 96

 

 

 

08

+2198

store accumulator in y

35

rem

add y to total

none

rem ignored

Fig. 19.27 SML instructions produced after the compiler’s first pass (part 1 of 2).

Chapter 19

 

 

 

Data Structures

1139

 

 

 

 

 

 

 

 

 

SML location

 

 

Simple program

and instruction

Description

 

 

 

 

 

 

 

40

let t = t + y

09

+2095

load t (95) into accumulator

 

 

 

 

10

+3098

add y to accumulator

 

 

 

 

11

+2194

store in temporary location 94

 

 

 

 

12

+2094

load from temporary location 94

 

 

 

 

13

+2195

store accumulator in t

 

45

rem

loop y

none

rem ignored

 

50

goto 20

14

+4001

branch to location 01

 

55

rem

output result

none

rem ignored

 

60

print t

15

+1195

output t to screen

 

99

end

 

16

+4300

terminate execution

 

Fig. 19.27 SML instructions produced after the compiler’s first pass (part 2 of 2).

Symbol

Type

Location

 

 

 

5

L

00

10

L

00

'x'

V

99

15

L

01

20

L

01

'y'

V

98

25

L

04

30

L

04

1

C

97

35

L

09

40

L

09

't'

V

95

45

L

14

50

L

14

55

L

15

60

L

15

99

L

16

Fig. 19.28 Symbol table for program of Fig. 19.27.

Most Simple statements convert directly to single SML instructions. The exceptions in this program are remarks, the if/goto statement in line 20 and the let statements. Remarks do not translate into machine language. However, the line number for a remark is placed in the symbol table in

1140

Data Structures

Chapter 19

case the line number is referenced in a goto statement or an if/goto statement. Line 20 of the program specifies that, if the condition y == x is true, program control is transferred to line 60. Since line 60 appears later in the program, the first pass of the compiler has not as yet placed 60 in the symbol table. (Statement line numbers are placed in the symbol table only when they appear as the first token in a statement.) Therefore, it is not possible at this time to determine the operand of the SML branch zero instruction at location 03 in the array of SML instructions. The compiler places 60 in location 03 of the flags array to indicate that the second pass completes this instruction.

We must keep track of the next instruction location in the SML array because there is not a one- to-one correspondence between Simple statements and SML instructions. For example, the if/goto statement of line 20 compiles into three SML instructions. Each time an instruction is produced, we must increment the instruction counter to the next location in the SML array. Note that the size of Simpletron’s memory could present a problem for Simple programs with many statements, variables and constants. It is conceivable that the compiler will run out of memory. To test for this case, your program should contain a data counter to keep track of the location at which the next variable or constant will be stored in the SML array. If the value of the instruction counter is larger than the value of the data counter, the SML array is full. In this case, the compilation process should terminate, and the compiler should print an error message indicating that it ran out of memory during compilation. This serves to emphasize that, although the programmer is freed from the burdens of managing memory by the compiler, the compiler itself must carefully determine the placement of instructions and data in memory and must check for such errors as memory being exhausted during the compilation process.

A Step-by-Step View of the Compilation Process

Let us now walk through the compilation process for the Simple program in Fig. 19.27. The compiler reads the first line of the program

5 rem sum 1 to x

into memory. The first token in the statement (the line number) is determined using the StreamTokenizer class (see Chapter 10 for a discussion of Java’s string manipulation methods). The token returned by the StreamTokenizer is converted to an integer by using static method Integer.parseInt(), so the symbol 5 can be located in the symbol table. If the symbol is not found, it is inserted in the symbol table.

We are at the beginning of the program and this is the first line, and no symbols are in the table yet. Therefore, 5 is inserted into the symbol table as type L (line number) and assigned the first location in SML array (00). Although this line is a remark, a space in the symbol table is still allocated for the line number (in case it is referenced by a goto or an if/goto). No SML instruction is generated for a rem statement, so the instruction counter is not incremented.

10 input x

is tokenized next. The line number 10 is placed in the symbol table as type L and assigned the first location in the SML array (00 because a remark began the program, so the instruction counter is currently 00). The command input indicates that the next token is a variable (only a variable can appear in an input statement). input corresponds directly to an SML operation code; therefore, the compiler simply has to determine the location of x in the SML array. Symbol x is not found in the symbol table. So, it is inserted into the symbol table as the Unicode representation of x, given type V and assigned location 99 in the SML array (data storage begins at 99 and is allocated backwards). SML code can now be generated for this statement. Operation code 10 (the SML read operation code) is multiplied by 100, and the location of x (as determined in the symbol table) is added to complete the instruction. The instruction is then stored in the SML array at location 00. The instruction counter is incremented by one, because a single SML instruction was produced.

Chapter 19

Data Structures

1141

The statement

 

 

15 rem

check y == x

 

is tokenized next. The symbol table is searched for line number 15 (which is not found). The line number is inserted as type L and assigned the next location in the array, 01. (Remember that rem statements do not produce code, so the instruction counter is not incremented.)

The statement

20 if y == x goto 60

is tokenized next. Line number 20 is inserted in the symbol table and given type L at the next location in the SML array 01. The command if indicates that a condition is to be evaluated. The variable y is not found in the symbol table, so it is inserted and given the type V and the SML location 98. Next, SML instructions are generated to evaluate the condition. There is no direct equivalent in SML for the if/goto; it must be simulated by performing a calculation using x and y and branching according to the result. If y is equal to x, the result of subtracting x from y is zero, so the branch zero instruction can be used with the result of the calculation to simulate the if/goto statement. The first step requires that y be loaded (from SML location 98) into the accumulator. This produces the instruction 01 +2098. Next, x is subtracted from the accumulator. This produces the instruction 02 +3199. The value in the accumulator may be zero, positive or negative. The operator is ==, so we want to branch zero. First, the symbol table is searched for the branch location (60 in this case), which is not found. So, 60 is placed in the flags array at location 03, and the instruction 03 +4200 is generated. (We cannot add the branch location because we have not yet assigned a location to line 60 in the SML array.) The instruction counter is incremented to 04.

The compiler proceeds to the statement

25 rem increment y

The line number 25 is inserted in the symbol table as type L and assigned SML location 04. The instruction counter is not incremented.

When the statement

30 let y = y + 1

is tokenized, the line number 30 is inserted in the symbol table as type L and assigned SML location 04. Command let indicates that the line is an assignment statement. First, all the symbols on the line are inserted in the symbol table (if they are not already there). The integer 1 is added to the symbol table as type C and assigned SML location 97. Next, the right side of the assignment is converted from infix to postfix notation. Then the postfix expression (y 1 +) is evaluated. Symbol y is located in the symbol table, and its corresponding memory location is pushed onto the stack. Symbol 1 is also located in the symbol table, and its corresponding memory location is pushed onto the stack. When the operator + is encountered, the postfix evaluator pops the stack into the right operand of the operator and pops the stack again into the left operand of the operator, then produces the SML instructions

04

+2098

(load y)

05

+3097

(add 1)

The result of the expression is stored in a temporary location in memory (96) with instruction

06 +2196 (store temporary)

and the temporary location is pushed onto the stack. Now that the expression has been evaluated, the result must be stored in y (i.e., the variable on the left side of =). So, the temporary location is loaded into the accumulator and the accumulator is stored in y with the instructions

1142

Data Structures

Chapter 19

 

07

+2096

(load temporary)

 

 

08

+2198

(store y)

 

The reader will immediately notice that SML instructions appear to be redundant. We will discuss this issue shortly.

When the statement

35 rem add y to total

is tokenized, line number 35 is inserted in the symbol table as type L and assigned location 09. The statement

40 let t = t + y

is similar to line 30. The variable t is inserted in the symbol table as type V and assigned SML location 95. The instructions follow the same logic and format as line 30, and the instructions 09 +2095, 10 +3098, 11 +2194, 12 +2094 and 13 +2195 are generated. Note that the result of t + y is assigned to temporary location 94 before being assigned to t (95). Once again, the reader will note that the instructions in memory locations 11 and 12 appear to be redundant. Again, we will discuss this shortly.

The statement

45 rem loop y

is a remark, so line 45 is added to the symbol table as type L and assigned SML location 14.

The statement

50 goto 20

transfers control to line 20. Line number 50 is inserted in the symbol table as type L and assigned SML location 14. The equivalent of goto in SML is the unconditional branch (40) instruction that transfers control to a specific SML location. The compiler searches the symbol table for line 20 and finds that it corresponds to SML location 01. The operation code (40) is multiplied by 100, and location 01 is added to it to produce the instruction 14 +4001.

The statement

55 rem output result

is a remark, so line 55 is inserted in the symbol table as type L and assigned SML location 15.

The statement

60 print t

is an output statement. Line number 60 is inserted in the symbol table as type L and assigned SML location 15. The equivalent of print in SML is operation code 11 (write). The location of t is determined from the symbol table and added to the result of the operation code multiplied by 100.

The statement

99 end

is the final line of the program. Line number 99 is stored in the symbol table as type L and assigned SML location 16. The end command produces the SML instruction +4300 (43 is halt in SML), which is written as the final instruction in the SML memory array.

This completes the first pass of the compiler. We now consider the second pass. The flags array is searched for values other than -1. Location 03 contains 60, so the compiler knows that instruction 03 is incomplete. The compiler completes the instruction by searching the symbol

Chapter 19

Data Structures

1143

table for 60, determining its location and adding the location to the incomplete instruction. In this case, the search determines that line 60 corresponds to SML location 15, so the completed instruction 03 +4215 is produced, replacing 03 +4200. The Simple program has now been compiled successfully.

To build the compiler, you will have to perform each of the following tasks:

a)Modify the Simpletron simulator program you wrote in Exercise 7.43 to take its input from a file specified by the user (see Chapter 16). The simulator should output its results to a disk file in the same format as the screen output. Convert the simulator to be an ob- ject-oriented program. In particular, make each part of the hardware an object. Arrange the instruction types into a class hierarchy using inheritance. Then execute the program polymorphically simply by telling each instruction to execute itself with an executeInstruction message.

b)Modify the infix-to-postfix evaluation algorithm of Exercise 19.12 to process multidigit integer operands and single-letter variable name operands. (Hint: Class StreamTokenizer can be used to locate each constant and variable in an expression, and constants can be converted from strings to integers by using Integer class method parseInt.) [Note: The data representation of the postfix expression must be altered to support variable names and integer constants.]

c)Modify the postfix evaluation algorithm to process multidigit integer operands and variable name operands. Also, the algorithm should now implement the “hook” discussed earlier so that SML instructions are produced rather than directly evaluating the expression. (Hint: Class StreamTokenizer can be used to locate each constant and variable in an expression, and constants can be converted from strings to integers by using Integer class method parseInt.) [Note: The data representation of the postfix expression must be altered to support variable names and integer constants.]

d)Build the compiler. Incorporate parts b) and c) for evaluating expressions in let statements. Your program should contain a method that performs the first pass of the compiler and a method that performs the second pass of the compiler. Both methods can call other methods to accomplish their tasks. Make your compiler as object oriented as possible.

19.28(Optimizing the Simple Compiler) When a program is compiled and converted into SML, a set of instructions is generated. Certain combinations of instructions often repeat themselves, usually in triplets called productions. A production normally consists of three instructions, such as load, add and store. For example, Fig. 19.29 illustrates five of the SML instructions that were produced in the compilation of the program in Fig. 19.27. The first three instructions are the production that adds 1 to y. Note that instructions 06 and 07 store the accumulator value in temporary location 96, then load the value back into the accumulator so instruction 08 can store the value in location 98. Often a production is followed by a load instruction for the same location that was just stored. This code can be optimized by eliminating the store instruction and the subsequent load instruction that operate on the same memory location, thus enabling the Simpletron to execute the program faster. Figure 19.30 illustrates the optimized SML for the program of Fig. 19.27. Note that there are four fewer instructions in the optimized code—a memory-space savings of 25%.

1

04

+2098

(load)

2

05

+3097

(add)

3

06

+2196

(store)

4

07

+2096

(load)

5

08

+2198

(store)

 

 

Fig. 19.29

Unoptimized code from the program of Fig. 19.25.

1144

Data Structures

 

 

Chapter 19

 

 

 

 

 

 

 

 

SML location

 

Simple program

and instruction

Description

 

 

 

 

5 rem sum 1 to x

 

none

rem ignored

10

input

x

00

+1099

read x into location 99

15

rem

check y == x

 

none

rem ignored

20

if y == x goto 60

01

+2098

load y (98) into accumulator

 

 

 

02

+3199

sub x (99) from accumulator

 

 

 

03

+4211

branch to location 11 if zero

25

rem

increment y

 

none

rem ignored

30

let y = y + 1

04

+2098

load y into accumulator

 

 

 

05

+3097

add 1 (97) to accumulator

 

 

 

06

+2198

store accumulator in y (98)

35

rem

add y to total

 

none

rem ignored

40

let t = t + y

07

+2096

load t from location (96 )

 

 

 

08

+3098

add y (98) accumulator

 

 

 

09

+2196

store accumulator in t (96)

45

rem

loop y

 

none

rem ignored

50

goto 20

10

+4001

branch to location 01

55

rem

output result

 

none

rem ignored

60

print t

11

+1196

output t (96) to screen

99

end

 

12

+4300

terminate execution

Fig. 19.30 Optimized code for the program of Fig. 19.27.

19.29 (Modifications to the Simple Compiler) Perform the following modifications to the Simple compiler. Some of these modifications might also require modifications to the Simpletron simulator program written in Exercise 7.43.

a)Allow the modulus operator (%) to be used in let statements. Simpletron Machine Language must be modified to include a modulus instruction.

b)Allow exponentiation in a let statement using ^ as the exponentiation operator. Simpletron Machine Language must be modified to include an exponentiation instruction.

c)Allow the compiler to recognize uppercase and lowercase letters in Simple statements (e.g., 'A' is equivalent to 'a'). No modifications to the Simpletron simulator are required.

d)Allow input statements to read values for multiple variables such as input x, y. No modifications to the Simpletron simulator are required to perform this enhancement to the Simple compiler.

e)Allow the compiler to output multiple values from a single print statement, such as print a, b, c. No modifications to the Simpletron simulator are required to perform this enhancement.

f)Add syntax-checking capabilities to the compiler so error messages are output when syntax errors are encountered in a Simple program. No modifications to the Simpletron simulator are required.

Chapter 19

Data Structures

1145

g)Allow arrays of integers. No modifications to the Simpletron simulator are required to perform this enhancement.

h)Allow subroutines specified by the Simple commands gosub and return. Command gosub passes program control to a subroutine and command return passes control back to the statement after the gosub. This is similar to a method call in Java. The same subroutine can be called from many gosub commands distributed throughout a program. No modifications to the Simpletron simulator are required.

i)Allow repetition structures of the form

for x = 2 to 10 step 2

Simple statements next

This for statement loops from 2 to 10 with an increment of 2. The next line marks the end of the body of the for line. No modifications to the Simpletron simulator are required.

j) Allow repetition structures of the form

for x = 2 to 10

Simple statements next

This for statement loops from 2 to 10 with a default increment of 1. No modifications to the Simpletron simulator are required.

k)Allow the compiler to process string input and output. This requires the Simpletron simulator to be modified to process and store string values. [Hint: Each Simpletron word (i.e., memory location) can be divided into two groups, each holding a two-digit integer. Each two-digit integer represents the Unicode decimal equivalent of a character. Add a machine-language instruction that will print a string beginning at a certain Simpletron memory location. The first half of the Simpletron word at that location is a count of the number of characters in the string (i.e., the length of the string). Each succeeding half word contains one Unicode character expressed as two decimal digits. The machine language instruction checks the length and prints the string by translating each two-digit number into its equivalent character.]

l)Allow the compiler to process floating-point values in addition to integers. The Simpletron Simulator must also be modified to process floating-point values.

19.30(A Simple Interpreter) An interpreter is a program that reads a high-level language program statement, determines the operation to be performed by the statement and executes the operation immediately. The high-level language program is not converted into machine language first. Interpreters execute more slowly than compilers do, because each statement encountered in the program being interpreted must first be deciphered at execution time. If statements are contained in a loop, the statements are deciphered each time they are encountered in the loop. Early versions of the Basic programming language were implemented as interpreters. Most Java programs are run interpretively.

Write an interpreter for the Simple language discussed in Exercise 19.26. The program should use the infix-to-postfix converter developed in Exercise 19.12 and the postfix evaluator developed in Exercise 19.13 to evaluate expressions in a let statement. The same restrictions placed on the Simple language in Exercise 19.26 should be adhered to in this program. Test the interpreter with the Simple programs written in Exercise 19.26. Compare the results of running these programs in the interpreter with the results of compiling the Simple programs and running them in the Simpletron simulator built in Exercise 7.43.

19.31 (Insert/Delete Anywhere in a Linked List) Our linked-list class allowed insertions and deletions at only the front and the back of the linked list. These capabilities were convenient for us when we used inheritance or composition to produce a stack class and a queue class with a minimal amount

1146

Data Structures

Chapter 19

of code simply by reusing the list class. Linked lists are normally more general than those we provided. Modify the linked-list class we developed in this chapter to handle insertions and deletions anywhere in the list.

19.32 (Lists and Queues without Tail References) Our implementation of a linked list (Fig. 19.3) used both a firstNode and a lastNode. The lastNode was useful for the insertAtBack and removeFromBack methods of the List class. The insertAtBack method corresponds to the enqueue method of the Queue class.

Rewrite the List class so that it does not use a lastNode. Thus, any operations on the tail of a list must begin searching the list from the front. Does this affect our implementation of the Queue class (Fig. 19.13)?

19.33(Performance of Binary Tree Sorting and Searching) One problem with the binary tree sort is that the order in which the data is inserted affects the shape of the tree—for the same collection of data, different orderings can yield binary trees of dramatically different shapes. The performance of the binary tree sorting and searching algorithms is sensitive to the shape of the binary tree. What shape would a binary tree have if its data were inserted in increasing order? in decreasing order? What shape should the tree have to achieve maximal searching performance?

19.34(Indexed Lists) As presented in the text, linked lists must be searched sequentially. For large lists, this can result in poor performance. A common technique for improving list-searching performance is to create and maintain an index to the list. An index is a set of references to key places in the list. For example, an application that searches a large list of names could improve performance by creating an index with 26 entries—one for each letter of the alphabet. A search operation for a last name beginning with ‘Y’ would then first search the index to determine where the ‘Y’ entries begin, then “jump into” the list at that point and search linearly until the desired name is found. This would be much faster than searching the linked list from the beginning. Use the List class of Fig. 19.3 as the basis of an IndexedList class.

Write a program that demonstrates the operation of indexed lists. Be sure to include methods insertInIndexedList, searchIndexedList and deleteFromIndexedList.

20

Java Utilities Package

and

Bit Manipulation

Objectives

To understand containers, such as classes Vector and Stack, and the Enumeration interface.

To be able to create Hashtable objects and persistent hash tables called Properties objects.

To understand random number generation with instances of class Random.

To use bit manipulation and BitSet objects.

Nothing can have value without being an object of utility.

Karl Marx

I’ve been in Who’s Who, and I know what’s what, but this is the first time I ever made the dictionary.

Mae West

O! many a shaft at sent

Finds mark the archer little meant!

Sir Walter Scott

There was the Door to which I found no Key;

There was the Veil through which I might not see.

Edward FitzGerald, The Rubáiyát of Omar Khayyám, st. 32

“It's a poor sort of memory that only works backwards,” the Queen remarked.

Lewis Carroll [Charles Lutwidge Dodgson]

Not by age but by capacity is wisdom acquired.

Titus Maccius Plautus, Trinummus, act II, sc. ii, l.88

1148

Java Utilities Package and Bit Manipulation

Chapter 20

Outline

20.1Introduction

20.2Vector Class and Enumeration Interface

20.3Stack Class

20.4Dictionary Class

20.5Hashtable Class

20.6Properties Class

20.7Random Class

20.8Bit Manipulation and the Bitwise Operators

20.9BitSet Class

Summary • Terminology • Self-Review Exercises • Answers to Self-Review Exercises • Exercises

20.1 Introduction

This chapter discusses several utility classes and interfaces in package java.util, including class Vector, interface Enumeration, class Stack, class Dictionary, class Hashtable, class Properties, class Random and class BitSet.

Programs use class Vector to create array-like objects that can grow and shrink dynamically as a program’s data storage requirements change. We consider interface Enumeration, which enables a program to iterate through the elements of a container such as a Vector.

Class Stack offers conventional stack operations push and pop, as well as others we did not consider in Chapter 19.

Class Dictionary is an abstract class that provides a framework for storing keyed data in tables and retrieving that data. The chapter explains the theory of “hashing,” a technique for rapidly storing and retrieving information from tables, and demonstrates the construction and manipulation of hash tables with Java’s Hashtable class. Also, the chapter considers class Properties, which provides support for persistent hash tables— hash tables that can be written to a file with an output stream and read from a file with an input stream.

Class Random provides a richer collection of random number capabilities than is available with static method random of class Math.

The chapter presents an extensive discussion of bit manipulation operators, followed by a discussion of class BitSet that enables the creation of bit-array-like objects for setting and getting individual bit values.

Chapter 21, Collections, introduces a framework for manipulating groups of objects called collections. Objects of type Vector, Stack and Hashtable are collections.

20.2 Vector Class and Enumeration Interface

In most programming languages, including Java, conventional arrays are fixed in size— they cannot grow or shrink in response to an application’s changing storage requirements. Java class Vector provides the capabilities of array-like data structures that can resize themselves dynamically.

Chapter 20

Java Utilities Package and Bit Manipulation

1149

At any time, the Vector contains a certain number of elements less than or equal to its capacity. The capacity is the space that has been reserved for the Vector’s elements. If a Vector requires additional capacity, it grows by a capacity increment that you specify or by a default assumed by the system. If you do not specify a capacity increment, the system will double the size of the Vector each time additional capacity is needed.

Performance Tip 20.1

Inserting additional elements into a Vector whose current size is less than its capacity is a relatively fast operation.

Performance Tip 20.2

It is a relatively slow operation to insert an element into a Vector that needs to grow larger to accommodate the new element.

Performance Tip 20.3

The default capacity increment doubles the size of the Vector. This may seem a waste of storage, but it is actually an efficient way for many Vectors to grow quickly to be “about the right size.” This is much more efficient time-wise than growing the Vector each time by only as much space as it takes to hold a single element. This disadvantage is that the Vector might occupy more space than it requires.

Performance Tip 20.4

If storage is at a premium, use Vector method trimToSize to trim a Vector’s capacity to the Vector’s exact size. This optimizes a Vector’s use of storage. However, adding another element to the Vector will force the Vector to grow dynamically—trimming leaves no room for growth.

Vectors store references to Objects. Thus, a program can store references to any objects in a Vector. To store values of primitive data types in Vectors, use the typewrapper classes (e.g., Integer, Long, Float) from package java.lang to create objects containing the primitive data type values.

Figure 20.1 demonstrates class Vector and several of its methods. The program provides a JButton that enables the user to test each of the methods. The user can type a String into the provided JTextField, and then press a button to see what the method does. Each operation displays a message in a JLabel to indicate the results of the operation.

1// Fig. 20.1: VectorTest.java

2 // Testing the Vector class of the java.util package

3

4 // Java core packages

5 import java.util.*;

6import java.awt.*;

7 import java.awt.event.*;

8

9 // Java extension packages

10 import javax.swing.*;

11

12public class VectorTest extends JFrame {

13private JLabel statusLabel;

Fig. 20.1 Demonstrating class Vector of package java.util (part 1 of 6).

1150

Java Utilities Package and Bit Manipulation

Chapter 20

14private Vector vector;

15private JTextField inputField;

17// set up GUI to test Vector methods

18public VectorTest()

19{

20super( "Vector Example" );

21

22Container container = getContentPane();

23container.setLayout( new FlowLayout() );

25statusLabel = new JLabel();

26vector = new Vector( 1 );

28 container.add( new JLabel( "Enter a string" ) );

29

30inputField = new JTextField( 10 );

31container.add( inputField );

32

33// button to add element to vector

34JButton addButton = new JButton( "Add" );

36

addButton.addActionListener(

37

 

38

new ActionListener() {

39

 

40

public void actionPerformed( ActionEvent event )

41

{

42

// add an element to vector

43

vector.addElement( inputField.getText() );

44

statusLabel.setText( "Added to end: " +

45

inputField.getText() );

46

inputField.setText( "" );

47

}

48}

49); // end call to addActionListener

51 container.add( addButton );

52

53// button to remove element from vector

54JButton removeButton = new JButton( "Remove" );

56

removeButton.addActionListener(

57

 

58

new ActionListener() {

59

 

60

public void actionPerformed( ActionEvent event )

61

{

62

// remove element from vector

63

if ( vector.removeElement( inputField.getText() ) )

64

statusLabel.setText( "Removed: " +

65

inputField.getText() );

 

 

Fig. 20.1 Demonstrating class Vector of package java.util (part 2 of 6).

Chapter 20

Java Utilities Package and Bit Manipulation

1151

 

 

 

66

else

 

67

statusLabel.setText( inputField.getText() +

68

" not in vector" );

 

69

}

 

70}

71); // end call to addActionListener

73 container.add( removeButton );

74

75// button to get first element of vector

76JButton firstButton = new JButton( "First" );

78

firstButton.addActionListener(

79

 

80

new ActionListener() {

81

 

82

public void actionPerformed( ActionEvent event )

83

{

84

// return first element of vector

85

try {

86

statusLabel.setText(

87

"First element: " + vector.firstElement() );

88

}

89

 

90

// catch exception if Vector empty

91

catch ( NoSuchElementException exception ) {

92

statusLabel.setText( exception.toString() );

93

}

94

}

95}

96); // end call to addActionListener

98 container.add( firstButton );

99

100// button to get last element of vector

101JButton lastButton = new JButton( "Last" );

103

lastButton.addActionListener(

104

 

105

new ActionListener() {

106

 

107

public void actionPerformed( ActionEvent event )

108

{

109

// return last element of vector

110

try {

111

statusLabel.setText(

112

"Last element: " + vector.lastElement() );

113

}

114

 

115

// catch exception if Vector empty

116

catch ( NoSuchElementException exception ) {

117

statusLabel.setText( exception.toString() );

118

}

Fig. 20.1 Demonstrating class Vector of package java.util (part 3 of 6).

1152

Java Utilities Package and Bit Manipulation

Chapter 20

 

 

 

119

}

 

120}

121); // end call to addActionListener

123 container.add( lastButton );

124

125// button to determine whether vector is empty

126JButton emptyButton = new JButton( "Is Empty?" );

128

emptyButton.addActionListener(

129

 

130

new ActionListener() {

131

 

132

public void actionPerformed( ActionEvent event )

133

{

134

// determine if Vector is empty

135

statusLabel.setText( vector.isEmpty() ?

136

"Vector is empty" : "Vector is not empty" );

137

}

138}

139); // end call to addActionListener

141 container.add( emptyButton );

142

143// button to determine whether vector contains search key

144JButton containsButton = new JButton( "Contains" );

145

 

146

containsButton.addActionListener(

147

 

148

new ActionListener() {

149

 

150

public void actionPerformed( ActionEvent event )

151

{

152

String searchKey = inputField.getText();

153

 

154

// determine if Vector contains searchKey

155

if ( vector.contains( searchKey ) )

156

statusLabel.setText(

157

"Vector contains " + searchKey );

158

else

159

statusLabel.setText(

160

"Vector does not contain " + searchKey );

161

}

162}

163); // end call to addActionListener

165 container.add( containsButton );

166

167// button to determine location of value in vector

168JButton locationButton = new JButton( "Location" );

170

locationButton.addActionListener(

171

 

Fig. 20.1 Demonstrating class Vector of package java.util (part 4 of 6).

Chapter 20

Java Utilities Package and Bit Manipulation

1153

 

 

 

172

new ActionListener() {

 

173

 

 

174

public void actionPerformed( ActionEvent event )

 

175

{

 

176

// get location of an object in Vector

 

177

statusLabel.setText( "Element is at location " +

178

vector.indexOf( inputField.getText() ) );

 

179

}

 

180}

181); // end call to addActionListener

183 container.add( locationButton );

184

185// button to trim vector size

186JButton trimButton = new JButton( "Trim" );

188

trimButton.addActionListener(

189

 

190

new ActionListener() {

191

 

192

public void actionPerformed( ActionEvent event )

193

{

194

// remove unoccupied elements to save memory

195

vector.trimToSize();

196

statusLabel.setText( "Vector trimmed to size" );

197

}

198}

199);

201 container.add( trimButton );

202

203// button to display vector size and capacity

204JButton statsButton = new JButton( "Statistics" );

206

statsButton.addActionListener(

207

 

208

new ActionListener() {

209

 

210

public void actionPerformed( ActionEvent event )

211

{

212

// get size and capacity of Vector

213

statusLabel.setText( "Size = " + vector.size() +

214

"; capacity = " + vector.capacity() );

215

}

216}

217); // end call to addActionListener

219 container.add( statsButton );

220

221// button to display vector contents

222JButton displayButton = new JButton( "Display" );

224 displayButton.addActionListener(

Fig. 20.1 Demonstrating class Vector of package java.util (part 5 of 6).

1154

Java Utilities Package and Bit Manipulation

Chapter 20

 

 

 

225

 

 

226

new ActionListener() {

 

227

 

 

228

public void actionPerformed( ActionEvent event )

229

{

 

230

// use Enumeration to output Vector contents

231

Enumeration enum = vector.elements();

 

232

StringBuffer buf = new StringBuffer();

 

233

 

 

234

while ( enum.hasMoreElements() )

 

235

buf.append( enum.nextElement() ).append( " " );

236

 

 

237

JOptionPane.showMessageDialog( null,

 

238

buf.toString(), "Display",

 

239

JOptionPane.PLAIN_MESSAGE );

 

240

}

 

241}

242); // end call to addActionListener

244container.add( displayButton );

245container.add( statusLabel );

247setSize( 300, 200 );

248setVisible( true );

250 } // end VectorTest constructor

251

252// execute application

253public static void main( String args[] )

254{

255VectorTest application = new VectorTest();

257 application.setDefaultCloseOperation(

258JFrame.EXIT_ON_CLOSE );

259}

260

261 } // end class VectorTest

Fig. 20.1 Demonstrating class Vector of package java.util (part 6 of 6).

The application’s constructor creates a Vector (line 26) with an initial capacity of one element. This Vector will double in size each time it needs to grow to accommodate more elements. Class Vector provides three other constructors. The no-argument constructor creates an empty Vector with an initial capacity of 10 elements. The constructor that takes two

Chapter 20

Java Utilities Package and Bit Manipulation

1155

arguments creates a Vector with an initial capacity specified by the first argument and a capacity increment specified by the second argument. Each time the Vector needs to grow, it will add space for the specified number of elements in the capacity increment. The constructor that takes a Collection allows creates a copy of a collection’s elements and stores them in the Vector. In Chapter 21, we discuss Collections.

Line 43 calls Vector method addElement to add its argument to the end of the Vector. If necessary, the Vector increases its capacity to accommodate the new element. Class Vector also provides method insertElementAt to insert an element at a specified position in the Vector and method setElementAt to set the element at a specific position in the Vector. Method insertElementAt makes room for the new element by shifting elements. Method setElementAt replaces the element at the specified position with its argument.

Line 63 calls Vector method removeElement to remove the first occurrence of its argument from the Vector. The method returns true if it finds the element in the Vector; otherwise, the method returns false. If the element is removed, all elements after that element in the Vector shift one position toward the beginning of the Vector to fill in the position of the removed element. Class Vector also provides method removeAllElements to remove every element from the Vector and method removeElementAt to remove the element at a specified index.

Line 87 calls Vector method firstElement to return a reference to the first element in the Vector. This method throws a NoSuchElementException if there are no elements currently in the Vector. Line 112 calls Vector method lastElement to return a reference to the last element in the Vector. This method throws a NoSuchElementException if there are no elements currently in the Vector.

Line 135 calls Vector method isEmpty to determine whether the Vector is empty. The method returns true if there are no elements in the Vector; otherwise, the method returns false.

Line 155 calls Vector method contains to determine whether the Vector contains the searchKey specified as an argument. Method contains returns true if searchKey is in the Vector; otherwise, the method returns false. Method contains uses Object method equals to determine whether the searchKey is equal to one of the Vector’s elements. Many classes override method equals to perform the comparisons in a manner specific to those classes. For example, class String defines equals to compare the individual characters in the two Strings being compared. If method equals is not overridden, the original version from class Object is used. This version performs comparisons using operator == to determine whether two references refer to the same object in memory.

Line 178 calls Vector method indexOf to determine the index of the first location in the Vector containing the argument. The method returns –1 if the argument is not found in the Vector. An overloaded version of this method takes a second argument specifying the index in the Vector at which the search should begin.

Performance Tip 20.5

Vector methods contains and indexOf perform linear searches of a Vector’s con- tents, which are inefficient for large Vectors. If a program frequently searches for elements in a collection, consider using a Hashtable (see Section 20.5) or one of the Java Collection API’s Map implementations (see Chapter 21).

1156

Java Utilities Package and Bit Manipulation

Chapter 20

Line 195 calls Vector method trimToSize to reduce the capacity of the Vector to the current number of elements in the Vector (i.e., the Vector’s current size).

Lines 213–214 use Vector methods size and capacity to determine the number of elements currently in the Vector and the number of elements that can be stored in the Vector without allocating more memory, respectively.

Line 231 calls Vector method elements to return an Enumeration that enables the program to iterate through the Vector’s elements. An Enumeration provides two methods—hasMoreElements and nextElement. In line 234, method hasMoreElements returns true if there are more elements in the Vector. In line 235, method nextElement returns a reference to the next element in the Vector. If there are no more elements, method nextElement throws a NoSuchElementException.

For complete information on class Vector and its other methods, see the online Java API documentation.

20.3 Stack Class

In Chapter 19, Data Structures, we learned how to build such fundamental data structures as linked lists, stacks, queues and trees. In a world of software reuse, instead of building data structures as we need them, often we can take advantage of existing data structures. In this section, we investigate class Stack in the Java utilities package (java.util).

In Section 20.2, we discussed class Vector, which implements a dynamically resizable array. Class Stack extends class Vector to implement a stack data structure. As does Vector, class Stack stores references to Objects. To store primitive data types, use the appropriate type-wrapper class to create an object containing the primitive value (Boolean, Byte, Character, Short, Integer, Long, Float or Double). Figure 20.2 provides a GUI that enables the user to test each of the Stack methods.

1// Fig. 20.2: StackTest.java

2 // Testing the Stack class of the java.util package

3

4 // Java core packages

5import java.awt.*;

6 import java.awt.event.*;

7 import java.util.*;

8

9 // Java extension packages

10 import javax.swing.*;

11

12public class StackTest extends JFrame {

13private JLabel statusLabel;

14private JTextField inputField;

15private Stack stack;

16

17// create GUI to manipulate a Stack

18public StackTest()

19{

20super( "Stacks" );

21

Fig. 20.2 Demonstrating class Stack of package java.util (part 1 of 5).

Chapter 20

Java Utilities Package and Bit Manipulation

1157

22 Container container = getContentPane();

23

24statusLabel = new JLabel();

25stack = new Stack();

26

27container.setLayout( new FlowLayout() );

28container.add( new JLabel( "Enter a string" ) );

29inputField = new JTextField( 10 );

30container.add( inputField );

31

32// button to place object on stack

33JButton pushButton = new JButton( "Push" );

35

pushButton.addActionListener(

36

 

37

new ActionListener() {

38

 

39

public void actionPerformed( ActionEvent event )

40

{

41

// put object on Stack

42

statusLabel.setText( "Pushed: " +

43

stack.push( inputField.getText() ) );

44

}

45}

46);

48 container.add( pushButton );

49

50// button to remove top object on stack

51JButton popButton = new JButton( "Pop" );

53

popButton.addActionListener(

54

 

55

new ActionListener() {

56

 

57

public void actionPerformed( ActionEvent event )

58

{

59

// remove element from Stack

60

try {

61

statusLabel.setText( "Popped: " + stack.pop() );

62

}

63

 

64

// process exception if Stack empty

65

catch ( EmptyStackException exception ) {

66

statusLabel.setText( exception.toString() );

67

}

68

}

69}

70);

72 container.add( popButton );

73

Fig. 20.2 Demonstrating class Stack of package java.util (part 2 of 5).

1158

Java Utilities Package and Bit Manipulation

Chapter 20

74// button to look at top element of stack

75JButton peekButton = new JButton( "Peek" );

77

peekButton.addActionListener(

78

 

79

new ActionListener() {

80

 

81

public void actionPerformed( ActionEvent event )

82

{

83

// look at top object on Stack

84

try {

85

statusLabel.setText( "Top: " + stack.peek() );

86

}

87

 

88

// process exception if Stack empty

89

catch ( EmptyStackException exception ) {

90

statusLabel.setText( exception.toString() );

91

}

92

}

93}

94);

96 container.add( peekButton );

97

98// button to determine whether stack is empty

99JButton emptyButton = new JButton( "Is Empty?" );

100

101

emptyButton.addActionListener(

102

 

103

new ActionListener() {

104

 

105

public void actionPerformed( ActionEvent event )

106

{

107

// determine if Stack is empty

108

statusLabel.setText( stack.empty() ?

109

"Stack is empty" : "Stack is not empty" );

110

}

111}

112);

114 container.add( emptyButton );

115

116// button to determine whether search key is in stack

117JButton searchButton = new JButton( "Search" );

118

 

119

searchButton.addActionListener(

120

 

121

new ActionListener() {

122

 

123

public void actionPerformed( ActionEvent event )

124

{

125

// search Stack for specified object

126

String searchKey = inputField.getText();

Fig. 20.2 Demonstrating class Stack of package java.util (part 3 of 5).

Chapter 20

Java Utilities Package and Bit Manipulation

1159

 

 

 

127

int result = stack.search( searchKey );

 

128

 

 

129

if ( result == -1 )

 

130

statusLabel.setText( searchKey + " not found" );

131

else

 

132

statusLabel.setText( searchKey +

 

133

" found at element " + result );

 

134

}

 

135}

136);

138 container.add( searchButton );

139

140// button to display stack contents

141JButton displayButton = new JButton( "Display" );

143

displayButton.addActionListener(

144

 

145

new ActionListener() {

146

 

147

public void actionPerformed( ActionEvent event )

148

{

149

// output Stack contents

150

Enumeration enumeration = stack.elements();

151

StringBuffer buffer = new StringBuffer();

152

 

153

while ( enumeration.hasMoreElements() )

154

buffer.append(

155

enumeration.nextElement() ).append( " " );

156

 

157

JOptionPane.showMessageDialog( null,

158

buffer.toString(), "Display",

159

JOptionPane.PLAIN_MESSAGE );

160

}

161}

162);

164container.add( displayButton );

165container.add( statusLabel );

167setSize( 675, 100 );

168setVisible( true );

169}

170

171// execute application

172public static void main( String args[] )

173{

174StackTest application = new StackTest();

176 application.setDefaultCloseOperation(

177JFrame.EXIT_ON_CLOSE );

178}

179

Fig. 20.2 Demonstrating class Stack of package java.util (part 4 of 5).

1160

Java Utilities Package and Bit Manipulation

Chapter 20

180 } // end class StackTest

Fig. 20.2 Demonstrating class Stack of package java.util (part 5 of 5).

Line 25 creates an empty Stack. Line 43 calls Stack method push to add its argument to the top of the stack. The method returns an Object reference to its argument.

Line 61 calls Stack method pop to remove the top element of the stack. The method returns an Object reference to the removed element. If there are no elements in the

Stack, method pop throws an EmptyStackException.

Line 85 calls Stack method peek to view the top element of the stack without removing the element. Method peek returns an Object reference to the element.

Line 108 calls Stack method empty to determine whether the stack is empty. If it is empty, the method returns true; otherwise, the method returns false.

Line 127 calls Stack method search to determine whether its argument is in the stack. If so, the method returns the position of the element in the stack. Note that the top element is position 1. If the element is not in the stack, –1 is returned.

The entire public interface of class Vector is actually part of class Stack, because Stack inherits from Vector. To prove this, our example provides a button to display the contents of the stack. This button invokes method elements to get an Enumeration of the stack; it then uses the Enumeration to walk through the stack elements.

Testing and Debugging Tip 20.1

Stack extends Vector, so the user may perform operations on Stack objects that are ordinarily not allowed on conventional stack data structures. This could "corrupt" the elements of the Stack and destroy the integrity of the Stack.

20.4 Dictionary Class

A Dictionary maps keys to values. When searching a Dictionary for a value, the program provides a key and the Dictionary returns the corresponding value. Dictionary is an abstract class. In particular, it is the superclass of class Hashtable, which we discuss in Section 20.5. Class Dictionary provides the public interface methods required to maintain a table of key–value pairs where the keys help store and retrieve the values in the table. Each key in the table is unique. The data structure is similar to a dictionary of words and definitions—the word is the key that is used to look up the definition (i.e., the value).

Dictionary method size returns the number of key–value pairs in a Dictionary object. Method isEmpty returns true if a Dictionary is empty, and false otherwise. Method keys returns an Enumeration that a program can use to iterate through a Dictionary’s keys. Method elements returns an Enumeration that a program can use to iterate through a Dictionary’s values. Method get returns the object that corresponds to a given key value. Method put puts an object associated with a given key into the table. Method remove removes an element corresponding to a given key and returns a reference to it.

Chapter 20

Java Utilities Package and Bit Manipulation

1161

20.5 Hashtable Class

Object-oriented programming languages facilitate creating new data types. When a program creates objects of new or existing types, the program then needs to manage those objects efficiently. This includes storing and retrieving objects. Storing and retrieving information with arrays is efficient if some aspect of your data directly matches the key value and if those keys are unique and tightly packed. If you have 100 employees with 9- digit Social Security numbers and you want to store and retrieve employee data by using the Social Security number as a key, it would nominally require an array with 999,999,999 elements, because there are 999,999,999 unique 9-digit numbers. This is impractical for virtually all applications that use Social Security numbers as keys. If the program could have an array that large, the program could get high performance for both storing and retrieving employee records by simply using the Social Security number as the array index.

There are numerous applications that have this problem, namely, that either the keys are of the wrong type (i.e., not nonnegative integers), or they may be of the right type, but sparsely spread over a huge range.

What is needed is a high-speed scheme for converting keys such as Social Security numbers, inventory part numbers and the like into unique array subscripts. Then, when an application needs to store something, the scheme could convert the application key rapidly into a subscript and the record of information could be stored at that slot in the array. Retrieval is accomplished the same way: Once the application has a key for which it wants to retrieve the data record, the application simply applies the conversion to the key—this produces the array subscript where the data is stored and the data is retrieved.

The scheme we describe here is the basis of a technique called hashing. Why the name? When we convert a key into an array subscript, we literally scramble the bits, forming a kind of “mishmashed” number. The number actually has no real significance beyond its usefulness in storing and retrieving this particular number data record.

A glitch in the scheme occurs when collisions occur (i.e., when two different keys “hash into” the same cell (or element) in the array). We cannot store two different data records in the same space, so we need to find an alternative home for all records beyond the first that hash to a particular array subscript. There are many schemes for doing this. One is to “hash again” (i.e., to reapply the hashing transformation to the key to provide a next candidate cell in the array). The hashing process is designed to distribute the values throughout the table, so the assumption is that with just a few hashes an available cell will be found.

Another scheme uses one hash to locate the first candidate cell. If that cell is occupied, successive cells are searched linearly until an available cell is found. Retrieval works the same way: The key is hashed once to determine the initial location to check to see whether it contains the desired data. If it does, the search is finished. If it does not, successive cells are searched linearly until the desired data is found.

The most popular solution to hash table collisions is to have each cell of the table be a hash “bucket,” typically a linked list of all the key–value pairs that hash to that cell. This is the solution that Java’s Hashtable class (from package java.util) implements.

One factor that affects the performance of hashing schemes is called the load factor. This is the ratio of the number of occupied cells in the hash table to the size of the hash table. The closer this ratio gets to 1.0, the greater the chance of collisions.

1162

Java Utilities Package and Bit Manipulation

Chapter 20

Performance Tip 20.6

The load factor in a hash table is a classic example of a space–time trade-off: By increasing the load factor, we get better memory utilization, but the program runs slower, due to increased hashing collisions. By decreasing the load factor, we get better program speed, because of reduced hashing collisions, but we get poorer memory utilization, because a larger portion of the hash table remains empty.

The complexity of programming hash tables properly is too much for most casual programmers. Computer science students study hashing schemes thoroughly in courses called “Data Structures” or “Algorithms.” Recognizing the value of hashing to most programmers, Java provides class Hashtable and some related features to enable programmers to use hashing without having to implement the messy details.

Actually, the preceding sentence is profoundly important in our study of object-ori- ented programming. As discussed in earlier chapters, classes encapsulate and hide complexity (i.e., implementation details) and offer user-friendly interfaces. Crafting classes to do this properly is one of the most valued skills in the field of object-oriented programming.

Figure 20.3 provides a GUI that enables you to test several Hashtable methods. Line 25 creates an empty Hashtable with a default capacity of 101 elements and a default load factor of .75. When the number of occupied slots in the Hashtable becomes more than the capacity times the load factor, the table grows larger. Class Hashtable also provides a constructor that takes one argument specifying the capacity and a constructor that takes two arguments, specifying the capacity and load factor, respectively.

1// Fig. 20.3: HashtableTest.java

2 // Demonstrates class Hashtable of the java.util package.

3

4 // Java core packages

5import java.awt.*;

6 import java.awt.event.*;

7 import java.util.*;

8

9 // Java extensions packages

10 import javax.swing.*;

11

12public class HashtableTest extends JFrame {

13private JLabel statusLabel;

14private Hashtable table;

15private JTextArea displayArea;

16private JTextField lastNameField;

17private JTextField firstNameField;

18

19// set up GUI to demonstrate Hashtable features

20public HashtableTest()

21{

22super( "Hashtable Example" );

23

24statusLabel = new JLabel();

25table = new Hashtable();

26displayArea = new JTextArea( 4, 20 );

Fig. 20.3 Demonstrating class Hashtable (part 1 of 6).

Chapter 20

Java Utilities Package and Bit Manipulation

1163

27 displayArea.setEditable( false );

28

29 JPanel northSubPanel = new JPanel();

30

31northSubPanel.add( new JLabel( "First name" ) );

32firstNameField = new JTextField( 8 );

33northSubPanel.add( firstNameField );

34

35northSubPanel.add( new JLabel( "Last name (key)" ) );

36lastNameField = new JTextField( 8 );

37northSubPanel.add( lastNameField );

38

39JPanel northPanel = new JPanel();

40northPanel.setLayout( new BorderLayout() );

41northPanel.add( northSubPanel, BorderLayout.NORTH );

42northPanel.add( statusLabel, BorderLayout.SOUTH );

44JPanel southPanel = new JPanel();

45southPanel.setLayout( new GridLayout( 2, 5 ) );

46JButton putButton = new JButton( "Put" );

47

 

48

putButton.addActionListener(

49

 

50

new ActionListener() {

51

 

52

// add new key/value pair to hash table

53

public void actionPerformed( ActionEvent event )

54

{

55

Employee employee = new Employee(

56

firstNameField.getText(),

57

lastNameField.getText() );

58

 

59

Object value =

60

table.put( lastNameField.getText(), employee );

61

 

62

// first time this key was added

63

if ( value == null )

64

statusLabel.setText(

65

"Put: " + employee.toString() );

66

 

67

// replaced previous value for this key

68

else

69

statusLabel.setText(

70

"Put: " + employee.toString() +

71

"; Replaced: " + value.toString() );

72

}

73}

74);

76 southPanel.add( putButton );

77

78// button to get value for specific key

79JButton getButton = new JButton( "Get" );

Fig. 20.3 Demonstrating class Hashtable (part 2 of 6).

1164

Java Utilities Package and Bit Manipulation

Chapter 20

 

 

 

80

 

 

81

getButton.addActionListener(

 

82

 

 

83

new ActionListener() {

 

84

 

 

85

// get value for specific key

 

86

public void actionPerformed( ActionEvent event )

87

{

 

88

Object value = table.get( lastNameField.getText() );

89

 

 

90

// value found for key

 

91

if ( value != null )

 

92

statusLabel.setText(

 

93

"Get: " + value.toString() );

 

94

 

 

95

// value not found for key

 

96

else

 

97

statusLabel.setText(

 

98

"Get: " + lastNameField.getText() +

99

" not in table" );

 

100

}

 

101}

102);

104 southPanel.add( getButton );

105

106// button to remove key/value pair from table

107JButton removeButton = new JButton( "Remove" );

109

removeButton.addActionListener(

110

 

111

new ActionListener() {

112

 

113

// remove key/value pair

114

public void actionPerformed( ActionEvent event )

115

{

116

Object value =

117

table.remove( lastNameField.getText() );

118

 

119

// key found

120

if ( value != null )

121

statusLabel.setText( "Remove: " +

122

value.toString() );

123

 

124

// key not found

125

else

126

statusLabel.setText( "Remove: " +

127

lastNameField.getText() + " not in table" );

128

}

129}

130);

132 southPanel.add( removeButton );

Fig. 20.3 Demonstrating class Hashtable (part 3 of 6).

Chapter 20

Java Utilities Package and Bit Manipulation

1165

133

134// button to detetmine whether hash table is empty

135JButton emptyButton = new JButton( "Empty" );

136

 

137

emptyButton.addActionListener(

138

 

139

new ActionListener() {

140

 

141

// determine whether hash table is empty

142

public void actionPerformed( ActionEvent event )

143

{

144

statusLabel.setText( "Empty: " + table.isEmpty() );

145

}

146}

147);

149 southPanel.add( emptyButton );

150

151// button to determine whether hash table contains key

152JButton containsKeyButton = new JButton( "Contains key" );

154

containsKeyButton.addActionListener(

155

 

156

new ActionListener() {

157

 

158

// determine whether hash table contains key

159

public void actionPerformed( ActionEvent event )

160

{

161

statusLabel.setText( "Contains key: " +

162

table.containsKey( lastNameField.getText() ) );

163

}

164}

165);

167 southPanel.add( containsKeyButton );

168

169// button to clear all hash table contents

170JButton clearButton = new JButton( "Clear table" );

172

clearButton.addActionListener(

173

 

174

new ActionListener() {

175

 

176

// clear hash table contents

177

public void actionPerformed( ActionEvent event )

178

{

179

table.clear();

180

statusLabel.setText( "Clear: Table is now empty" );

181

}

182}

183);

185 southPanel.add( clearButton );

Fig. 20.3 Demonstrating class Hashtable (part 4 of 6).

1166

Java Utilities Package and Bit Manipulation

Chapter 20

186

187// button to display hash table elements

188JButton listElementsButton = new JButton( "List objects" );

190

listElementsButton.addActionListener(

191

 

192

new ActionListener() {

193

 

194

// display hash table elements

195

public void actionPerformed( ActionEvent event )

196

{

197

StringBuffer buffer = new StringBuffer();

198

 

199

for ( Enumeration enumeration = table.elements();

200

enumeration.hasMoreElements(); )

201

buffer.append(

202

enumeration.nextElement() ).append( '\n' );

203

 

204

displayArea.setText( buffer.toString() );

205

}

206}

207);

209 southPanel.add( listElementsButton );

210

211// button to display hash table keys

212JButton listKeysButton = new JButton( "List keys" );

214

listKeysButton.addActionListener(

215

 

216

new ActionListener() {

217

 

218

// display hash table KEYS

219

public void actionPerformed( ActionEvent event )

220

{

221

StringBuffer buffer = new StringBuffer();

222

 

223

for ( Enumeration enumeration = table.keys();

224

enumeration.hasMoreElements(); )

225

buffer.append(

226

enumeration.nextElement() ).append( '\n' );

227

 

228

JOptionPane.showMessageDialog( null,

229

buffer.toString(), "Display",

230

JOptionPane.PLAIN_MESSAGE );

231

}

232}

233);

235 southPanel.add( listKeysButton );

236

237Container container = getContentPane();

238container.add( northPanel, BorderLayout.NORTH );

Fig. 20.3 Demonstrating class Hashtable (part 5 of 6).

Chapter 20

Java Utilities Package and Bit Manipulation

1167

239 container.add( new JScrollPane( displayArea ),

240BorderLayout.CENTER );

241container.add( southPanel, BorderLayout.SOUTH );

243setSize( 540, 300 );

244setVisible( true );

245}

246

247// execute application

248public static void main( String args[] )

249{

250HashtableTest application = new HashtableTest();

252 application.setDefaultCloseOperation(

253JFrame.EXIT_ON_CLOSE );

254}

255

256 } // end class HashtableTest

257

258// Employee class to represent first and last name

259class Employee {

260private String first, last;

261

262// initialize an Employee

263public Employee( String firstName, String lastName )

264{

265first = firstName;

266last = lastName;

267}

268

269// convert Employee to String representation

270public String toString()

271{

272return first + " " + last;

273}

274

275 } // end class Employee

Fig. 20.3 Demonstrating class Hashtable (part 6 of 6).

1168

Java Utilities Package and Bit Manipulation

Chapter 20

Lines 59–60 call Hashtable method put to add a key (the first argument) and a value (the second argument) into the Hashtable. Method put returns null if key has not been inserted in the Hashtable previously. Otherwise, method put returns the original value for that key in the Hashtable; this helps the program manage cases in which it intends to replace the value stored for a given key. If either the key or the value is null, a NullPointerException occurs.

Line 88 calls Hashtable method get to locate the value associated with the key specified as an argument. If the key is present in the table, get returns an Object reference to the corresponding value; otherwise, the method returns null.

Lines 116–117 call Hashtable method remove to remove a key–value pair from the table. The method returns a reference to the removed Object. If there is no value mapped to the specified key, the method returns null.

Line 144 calls Hashtable method isEmpty, which returns true if the Hashtable is empty; otherwise it returns false.

Line 162 calls Hashtable method containsKey to determine whether the key specified as an argument is in the Hashtable (i.e., a value is associated with that key). If so, the method returns true; otherwise, the method returns false. Class Hashtable also provides method contains to determine whether the Object specified as its argument is in the Hashtable.

Line 179 calls Hashtable method clear to empty the Hashtable contents. Line 199 calls Hashtable method elements to obtain an Enumeration of the values in the Hashtable. Line 223 calls Hashtable method keys to obtain an Enumeration of the keys in the Hashtable.

For more information on class Hashtable and its methods, see the online Java API documentation.

20.6 Properties Class

A Properties object is a persistent Hashtable object that normally stores key–value pairs of Strings—assuming that you use methods setProperty and getProperty to manipulate the table rather than Hashtable methods put and get. By persistent, we mean that the Hashtable object can be written to an output stream and directed to a file, and read back in through an input stream. In fact, most objects in Java can now be output and input with Java’s object serialization (see Chapter 16). The Properties class extends class Hashtable, so Properties objects have the methods we discussed in Fig. 20.3. The keys and values in a Properties object must be of type String. Class Properties provides some additional methods that are demonstrated in Fig. 20.4.

1// Fig. 20.4: PropertiesTest.java

2 // Demonstrates class Properties of the java.util package.

3

4 // Java core packages

5import java.awt.*;

6 import java.awt.event.*;

7import java.io.*;

8import java.util.*;

Fig. 20.4 Demonstrating class Properties (part 1 of 6).

Chapter 20

Java Utilities Package and Bit Manipulation

1169

9

10// Java extension packages

11import javax.swing.*;

12

13public class PropertiesTest extends JFrame {

14private JLabel statusLabel;

15private Properties table;

16private JTextArea displayArea;

17private JTextField valueField, nameField;

19// set up GUI to test Properties table

20public PropertiesTest()

21{

22super( "Properties Test" );

23

24// create Properties table

25table = new Properties();

27 Container container = getContentPane();

28

29// set up NORTH of window's BorderLayout

30JPanel northSubPanel = new JPanel();

31

32northSubPanel.add( new JLabel( "Property value" ) );

33valueField = new JTextField( 10 );

34northSubPanel.add( valueField );

35

36northSubPanel.add( new JLabel( "Property name (key)" ) );

37nameField = new JTextField( 10 );

38northSubPanel.add( nameField );

39

40JPanel northPanel = new JPanel();

41northPanel.setLayout( new BorderLayout() );

42northPanel.add( northSubPanel, BorderLayout.NORTH );

44statusLabel = new JLabel();

45northPanel.add( statusLabel, BorderLayout.SOUTH );

47 container.add( northPanel, BorderLayout.NORTH );

48

49// set up CENTER of window's BorderLayout

50displayArea = new JTextArea( 4, 35 );

51container.add( new JScrollPane( displayArea ),

52

BorderLayout.CENTER );

53

 

54// set up SOUTH of window's BorderLayout

55JPanel southPanel = new JPanel();

56southPanel.setLayout( new GridLayout( 1, 5 ) );

58// button to put a name/value pair in Properties table

59JButton putButton = new JButton( "Put" );

60

Fig. 20.4 Demonstrating class Properties (part 2 of 6).

1170

Java Utilities Package and Bit Manipulation

Chapter 20

 

 

 

61

putButton.addActionListener(

 

62

 

 

63

new ActionListener() {

 

64

 

 

65

// put name/value pair in Properties table

 

66

public void actionPerformed( ActionEvent event )

67

{

 

68

Object value = table.setProperty(

 

69

nameField.getText(), valueField.getText() );

70

 

 

71

if ( value == null )

 

72

showstatus( "Put: " + nameField.getText() +

73

" " + valueField.getText() );

 

74

 

 

75

else

 

76

showstatus( "Put: " + nameField.getText() +

77

" " + valueField.getText() +

 

78

"; Replaced: " + value.toString() );

79

 

 

80

listProperties();

 

81

}

 

82}

83); // end call to addActionListener

85 southPanel.add( putButton );

86

87// button to empty contents of Properties table

88JButton clearButton = new JButton( "Clear" );

90

clearButton.addActionListener(

91

 

92

new ActionListener() {

93

 

94

// use method clear to empty table

95

public void actionPerformed( ActionEvent event )

96

{

97

table.clear();

98

showstatus( "Table in memory cleared" );

99

listProperties();

100

}

101}

102); // end call to addActionListener

104 southPanel.add( clearButton );

105

106// button to get value of a property

107JButton getPropertyButton = new JButton( "Get property" );

109

getPropertyButton.addActionListener(

110

 

111

new ActionListener() {

112

 

 

 

Fig. 20.4 Demonstrating class Properties (part 3 of 6).

Chapter 20

Java Utilities Package and Bit Manipulation

1171

 

 

113

// use method getProperty to obtain a property value

114

public void actionPerformed( ActionEvent event )

 

115

{

 

116

Object value = table.getProperty(

 

117

nameField.getText() );

 

118

 

 

119

if ( value != null )

 

120

showstatus( "Get property: " +

 

121

nameField.getText() + " " +

 

122

value.toString() );

 

123

 

 

124

else

 

125

showstatus( "Get: " + nameField.getText() +

126

" not in table" );

 

127

 

 

128

listProperties();

 

129

}

 

130}

131); // end call to addActionListener

133 southPanel.add( getPropertyButton );

134

135// button to contents of Properties table to file

136JButton saveButton = new JButton( "Save" );

137

 

138

saveButton.addActionListener(

139

 

140

new ActionListener() {

141

 

142

// use method save to place contents in file

143

public void actionPerformed( ActionEvent event )

144

{

145

// save contents of table

146

try {

147

FileOutputStream output =

148

new FileOutputStream( "props.dat" );

149

 

150

table.store( output, "Sample Properties" );

151

output.close();

152

 

153

listProperties();

154

}

155

 

156

// process problems with file output

157

catch( IOException ioException ) {

158

ioException.printStackTrace();

159

}

160

}

161}

162); // end call to addActionListener

164 southPanel.add( saveButton );

165

Fig. 20.4 Demonstrating class Properties (part 4 of 6).

1172

Java Utilities Package and Bit Manipulation

Chapter 20

166// button to load contents of Properties table from file

167JButton loadButton = new JButton( "Load" );

168

 

169

loadButton.addActionListener(

170

 

171

new ActionListener() {

172

 

173

// use method load to read contents from file

174

public void actionPerformed( ActionEvent event )

175

{

176

// load contents of table

177

try {

178

FileInputStream input =

179

new FileInputStream( "props.dat" );

180

 

181

table.load( input );

182

input.close();

183

listProperties();

184

}

185

 

186

// process problems with file input

187

catch( IOException ioException ) {

188

ioException.printStackTrace();

189

}

190

}

191}

192); // end call to addActionListener

194 southPanel.add( loadButton );

195

196 container.add( southPanel, BorderLayout.SOUTH );

197

198setSize( 550, 225 );

199setVisible( true );

200}

201

202// output property values

203public void listProperties()

204{

205StringBuffer buffer = new StringBuffer();

206String name, value;

207

208 Enumeration enumeration = table.propertyNames();

209

210 while ( enumeration.hasMoreElements() ) {

211 name = enumeration.nextElement().toString();

212 value = table.getProperty( name );

213

214 buffer.append( name ).append( '\t' );

215buffer.append( value ).append( '\n' );

216}

217

218 displayArea.setText( buffer.toString() );

Fig. 20.4 Demonstrating class Properties (part 5 of 6).

Chapter 20

Java Utilities Package and Bit Manipulation

1173

219 } // end method ListProperties

220

221// display String in statusLabel label

222public void showstatus( String s )

223{

224statusLabel.setText( s );

225}

226

227// execute application

228public static void main( String args[] )

229{

230PropertiesTest application = new PropertiesTest();

232 application.setDefaultCloseOperation(

233JFrame.EXIT_ON_CLOSE );

234}

235

236 } // end class PropertiesTest

Fig. 20.4 Demonstrating class Properties (part 6 of 6).

Line 25 uses the no-argument constructor to create an empty Properties table with no default properties. Class Properties also provides an overloaded constructor that receives a reference to a Properties object containing default property values.

Lines 68–69 call Properties method setProperty to store a value for the specified key. If the key does not exist in the table, setProperty returns null; otherwise, it returns the previous value for that key.

Lines 116–117 call Properties method getProperty to locate the value associated with the specified key. If the key is not found in this Properties object, getProperty uses the one in the default Properties object (if there is one). The process continues recursively until there are no more default Properties objects (remember that every Properties object can be initialized with a default Properties object), at which point getProperty returns null. An overloaded version of this method receives two arguments, the second of which is the default value to return if getProperty cannot locate the key.

Line 150 calls Properties method store to save the contents of the Properties object to the OutputStream object specified as the first argument (in this case, a

FileOutputStream). The String argument is a description of the Properties object. Class Properties also provides method list, which takes a PrintStream argument. This method is useful for displaying the set of properties.

1174

Java Utilities Package and Bit Manipulation

Chapter 20

Testing and Debugging Tip 20.2

Use Properties method list to display the contents of a Properties object for debugging purposes.

Line 181 calls Properties method load to restore the contents of the Properties object from the InputStream specified as the first argument (in this case, a

FileInputStream).

Line 208 calls Properties method propertyNames to obtain an Enumeration of the property names. The value of each property can be determined by using method getProperty.

20.7 Random Class

We discussed random-number generation in Chapter 6, Methods, where we used Math class method random. Java provides extensive additional random number generation capabilities in class Random. We briefly walk through the API calls here.

A new random-number generator can be created by using

Random r = new Random();

This form uses the computer’s current time to seed its random-number generator differently during each constructor call and thus generates different sequences of random numbers for each Random object.

To create a pseudorandom-number generator with “repeatability,” use

Random r = new Random( seedValue );

The seedValue argument (type long) is used in the random number calculation to “seed” the random number generator. If the same seedValue is used every time, the Random object produces the same sequence of random numbers.

Testing and Debugging Tip 20.3

While a program is under development, use the form Random(seedValue) that produces a repeatable sequence of random numbers. If a bug occurs, fix the bug and test with the same seedValue; this allows you to reconstruct the exact same sequence of random numbers that caused the bug. Once the bugs have been removed, use the form Random(), which generates a new sequence of random numbers each time the program is run.

The call

r.setSeed( seedValue );

resets r’s seed value at any time. The calls

r.nextInt()

r.nextLong()

generate uniformly distributed random integers. You can use Math.abs to take the absolute value of the number produced by nextInt, thus giving a number in the range from zero through approximately 2 billion. Then use the % operator to scale the number. For example, to roll a six-sided die, if you scale with a 6, you will get a number in the range from