COMP 249 Programming Methodology

advertisement
COMP 249
Programming Methodology
Lecture Notes
Peter Grogono
Winter 2004
Department of Computer Science
Concordia University
Montreal, Quebec
CONTENTS
ii
Contents
1 Introduction
1
1.1
Why Java? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
1
1.2
Review . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
3
2 Inheritance
6
2.1
Classification . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
6
2.2
Inheritance in Java . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
8
2.3
Inherit . . . . or not? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11
2.4
Polymorphism and Dynamic Binding . . . . . . . . . . . . . . . . . . . . . . . . 13
2.5
The protected Modifier . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 16
2.6
The Class Object . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17
2.7
Text Processing . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18
3 From Classes to Interfaces
25
3.1
Hidden Fields . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25
3.2
The Modifier final . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 26
3.3
Abstract Methods . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 26
3.4
Abstract Classes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 27
3.5
When to use abstract . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 32
3.6
Interfaces . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 33
3.7
Collections and Iterators . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 34
3.8
Rules for Interfaces . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 38
3.9
Designing with Abstract Classes and Interfaces . . . . . . . . . . . . . . . . . . 39
3.10 Review . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 40
4 Applets and Dialog Boxes
42
4.1
Writing an Applet . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 42
4.2
Coordinates . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 44
4.3
Colours and Objects . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 45
4.4
Inside class Applet . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 46
4.5
Dialog Boxes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 49
4.6
Event-Driven Programming . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 51
5 Coding Conventions
56
5.1
Comments . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 59
5.2
Javadoc . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 62
5.3
Nested Classes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 62
CONTENTS
iii
6 Graphical User Interface Design
66
6.1
A Graphical User Interface . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 66
6.2
Incremental Development . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 68
6.3
Mouse Events . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 77
6.4
Layout Managers . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 81
6.5
Miscellaneous Utilities . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 84
7 When Bad Things Happen to Good Programs
86
7.1
Printing Messages . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 86
7.2
Assertions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 87
7.3
Exceptions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 88
7.4
Defining your own exceptions . . . . . . . . . . . . . . . . . . . . . . . . . . . . 98
7.5
The finally block . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 102
7.6
When is a number not a number? . . . . . . . . . . . . . . . . . . . . . . . . . . 104
8 Data Transfer
109
8.1
Streams . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 109
8.2
Streams in Java . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 110
8.3
Reading Input
8.4
File Processing . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 113
8.5
Class File . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 121
8.6
I/O Classes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 122
8.7
Serialization . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 128
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 111
9 Advanced GUI Programming
134
9.1
Components . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 134
9.2
Keyboard Events . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 135
10 Threads
137
10.1 Concurrent Processing . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 137
10.2 Threads in Java . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 137
10.3 Timing Problems . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 139
11 Programming Methodology
146
11.1 Preliminaries . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 146
11.2 Containers . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 148
11.3 Unordered Array Containers . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 149
11.4 Ordered array containers . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 152
11.5 Array algorithms . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 156
LIST OF FIGURES
iv
List of Examples
1
The Person Hierarchy . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
8
2
The Person Hierarchy Revisited . . . . . . . . . . . . . . . . . . . . . . . . . . . 11
3
Dynamic Binding . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 14
4
The abstract Box . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 27
5
Boxes with iterators . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 35
6
Hello, Applet . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 43
7
Abstract Painting . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 46
8
Solving a quadratic equation . . . . . . . . . . . . . . . . . . . . . . . . . . . . 50
9
A Graphical User Interface . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 53
10
Feeding the Chicks . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 62
11
Drawing Rectangles . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 78
12
The cosine rule . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 87
13
Checking triangles . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 90
14
The exceptional applet . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 96
15
Using finally . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 102
16
Reading a Text File . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 111
17
Choosing a File . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 120
18
Copying a text file . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 124
19
Filters . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 125
20
Food Preservation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 129
21
Counting with threads . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 137
22
Playing trains . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 141
List of Figures
1
Java Processing . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
2
2
A taxonomy or class hierarchy . . . . . . . . . . . . . . . . . . . . . . . . . . .
7
3
Class Person . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
9
4
Class Student . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
9
5
Class Test . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10
6
Class Professor . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11
7
The Person class hierarchy . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12
8
Dynamic Binding . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15
9
Modifying accessibility . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17
10
Testing the text processor . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19
11
Class Box . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 20
LIST OF FIGURES
v
12
Class Row . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21
13
Class Stack . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21
14
Class Word . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 22
15
Class Space . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 22
16
Method show for class Box . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23
17
Output produced by method show . . . . . . . . . . . . . . . . . . . . . . . . . 24
18
Inheriting abstract and concrete classes . . . . . . . . . . . . . . . . . . . . . . 28
19
Revised class Box . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 29
20
Revised class Row . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 30
21
Revised class Stack . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 30
22
Revised class Word . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 31
23
Revised class Space . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 31
24
Designing a class hierarchy . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 32
25
The interface Collection . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 34
26
The interface Iterator . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 34
27
Class Box with iterators . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 37
28
Class Row with iterators . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 38
29
Class Stack with iterators. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 39
30
HTML for a simple web page . . . . . . . . . . . . . . . . . . . . . . . . . . . . 43
31
Java source for a simple applet . . . . . . . . . . . . . . . . . . . . . . . . . . . 44
32
Java coordinates . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 45
33
Abstract painting . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 47
34
Applet states . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 48
35
Solving a quadratic equation . . . . . . . . . . . . . . . . . . . . . . . . . . . . 52
36
Listeners, events, and components . . . . . . . . . . . . . . . . . . . . . . . . . 53
37
The GUI displayed by the program in Figure 38
38
Implementing a button . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 55
39
Inline comments . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 61
40
Commenting a class with Javadoc . . . . . . . . . . . . . . . . . . . . . . . . . . 63
41
Common Javadoc tags . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 64
42
Feeding the chicks . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 64
43
Nesting for the birds . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 65
44
Adding a button—first part . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 67
45
Adding a button—second part . . . . . . . . . . . . . . . . . . . . . . . . . . . 68
46
A simple graphical user interface . . . . . . . . . . . . . . . . . . . . . . . . . . 69
47
An application with a GUI . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 70
48
Building a panel: step 1 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 70
. . . . . . . . . . . . . . . . . 53
LIST OF FIGURES
vi
49
Building a panel: step 2 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 71
50
Building a panel: step 3 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 71
51
Listening for text . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 72
52
Adding radio buttons
53
Listening for radio buttons
54
Changing the shape . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 74
55
Listening for shapes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 75
56
The complete constructor for FirstPanel . . . . . . . . . . . . . . . . . . . . . 76
57
Drawing Rectangles: first part . . . . . . . . . . . . . . . . . . . . . . . . . . . 78
58
Drawing Rectangles: second part . . . . . . . . . . . . . . . . . . . . . . . . . . 79
59
Drawing Rectangles: third part . . . . . . . . . . . . . . . . . . . . . . . . . . . 80
60
A border layout . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 82
61
Generating a BorderLayout . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 82
62
Exceptional triangles . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 91
63
A Exception subclass for bad triangles . . . . . . . . . . . . . . . . . . . . . . 93
64
Catching the bad triangles . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 93
65
Subclasses of Exception . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 96
66
Subclasses of RuntimeException . . . . . . . . . . . . . . . . . . . . . . . . . . 97
67
Finding your age . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 99
68
Finding your age — with error detection . . . . . . . . . . . . . . . . . . . . . . 100
69
Class Stack . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 101
70
Class StackException . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 102
71
Testing class Stack . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 103
72
Results of the test . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 103
73
Testing finally: first part . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 104
74
Testing finally: second part . . . . . . . . . . . . . . . . . . . . . . . . . . . . 105
75
Testing finally: results . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 106
76
Searching for a word: first version . . . . . . . . . . . . . . . . . . . . . . . . . 106
77
Searching for a word: second version . . . . . . . . . . . . . . . . . . . . . . . . 107
78
Varieties of data transfer . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 109
79
Reading a file . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 112
80
The file "fetzer.txt" . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 113
81
Driver for reading a file . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 113
82
Class for reading a file . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 114
83
Finding the words . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 115
84
Counting the words . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 117
85
Class Entry . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 118
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 73
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . 73
LIST OF FIGURES
vii
86
Method insert . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 118
87
Inserting a word in alphabetical order . . . . . . . . . . . . . . . . . . . . . . . 119
88
Reporting the entries . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 119
89
Concordance for "fetzer.txt" . . . . . . . . . . . . . . . . . . . . . . . . . . . 120
90
Using a FileChooser . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 121
91
Reader class hierarchy . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 123
92
Writer class hierarchy . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 123
93
Copying a file . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 124
94
Input stream class hierarchy . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 125
95
Output stream class hierarchy . . . . . . . . . . . . . . . . . . . . . . . . . . . . 125
96
Writing binary data . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 126
97
Reading binary data . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 127
98
Results from the binary data example . . . . . . . . . . . . . . . . . . . . . . . 127
99
A Serializable class . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 130
100 Test program for food preservation . . . . . . . . . . . . . . . . . . . . . . . . . 131
101 Writing the meal . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 132
102 Reading the meal . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 133
103 Counting with threads . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 138
104 Output from Figure 103 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 139
105 Running the trains . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 142
106 The class Train: public parts . . . . . . . . . . . . . . . . . . . . . . . . . . . 143
107 The class Train: the private parts . . . . . . . . . . . . . . . . . . . . . . . . 144
108 Output from trains . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 145
109 Fence posts . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 148
110 A container . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 149
111 Partitioning an array for binary search . . . . . . . . . . . . . . . . . . . . . . . 152
112 Binary search in action . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 153
113 Moving a block to make space for the new element . . . . . . . . . . . . . . . . 154
114 Find and insert using binary search . . . . . . . . . . . . . . . . . . . . . . . . . 155
115 Moving to fill the hole . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 155
COMP 249
1
Programming Methodology
Introduction
COMP 249 Programming Methodology follows on from COMP 248 Introduction to Programming. We continue to explore Java, but in greater depth and with more advanced features
of the language. As the course title suggests, we also discuss some general principles of
programming that are independent of any particular programming language.
The objective of COMP 249 is to enable you to design and implement programs, applets,
and user interfaces in Java using packages, classes, interfaces, inheritance, methods, and data
structures. The topics that we will discuss include: inheritance; graphical user interfaces;
exception handling; input/output, files, serialization; simple data structures; and program
development. We will also introduce elementary software engineering concepts.
1.1
Why Java?
There are several reasons for using Java as the programming language for introductory Computer Science courses:
Java is an object-oriented language.
Java is probably the simplest general purpose language that is widely used. There are
simpler languages, but they are specialized for particular purposes.
Although Java was developed by Sun Microsystems, it is free, as are compilers, development environments, and other tools.
Java has well-developed facilities for GUIs (graphical user interfaces) and network programming.
In addition to these points, Java is processed in a special way, as shown in Figure 1. The
steps in preparing and running a Java program are as follows. First, the Java program, called
the source code, is created or modified with an editor and stored in a .java file. Second,
the source code is fed to a Java compiler which translates it to bytecodes.
The bytecodes store the program in a compact form. They may be used immediately, but
it is also feasible—and common practice—to send the bytecodes to another computer over
a network. There are two ways to run the Java program in bytecode form. One way is to
use a Java Virtual Machine, or JVM . The JVM interprets the bytecodes one by one to
obtain the effects specified in the original program. The other way is to feed the bytecodes
into another compiler that translates them into executable code.
The key point here is that part of the process is machine independent and the other part
is machine dependent:
The Java source code and the bytecodes are machine independent: they have exactly the
same form on all platforms—PC, Mac, unix workstation, whatever.
The JVM, the bytecode compiler, and the executable code are machine dependent: they
have a different form on each platform.
Note that “platform” here actually means both processor and operating system. Different
JVMs are required for Windows and linux on a PC, and even unix and linux might be
slightly different.
1 INTRODUCTION
2
source code
Editor
.java
-
network
- Compiler
bytecodes
- network
.class
JVM
-
- Compiler
Processor
executable code
Figure 1: Java Processing
This means that A can write a Java program, compile it into bytecodes, and send the bytecodes
to B. It doesn’t matter what kind of system that B has: provided that it has a JVM, it can run
A’s program. The first advantage of bytecodes is portability . Sun advertises this feature as
“compile once, run anywhere” but, in the early days of Java, before portability was achieved,
others used to say “compile once, debug everywhere”. Although portability is a nice idea, it
is still not complete: owners of Macs are not able to use the most recent extensions of Java,
because these extensions have not been implemented in Mac JVMs.
Incidentally, the idea of using bytecodes to achieve portability is not new: it was introduced
in the early 1970s as a way of distributing Pascal.
The second advantage of bytecodes and the JVM is security . When we run a program that
has arrived from somewhere else, we want (or should want) to be completely confident that
it will not do something mischievous to our computer system. If the program is in executable
(.exe format), there are no guarantees: it could wipe out the hard disk in a few seconds.
Java programs expressed in bytecodes, however, have very limited capabilities: there are
only certain things that bytecodes can do, and it is impossible to write code to do anything
else—even by forging bytecodes. To ensure complete security, the JVM actually analyzes the
bytecodes before interpreting them.
Although the advantages of bytecodes are very significant, they have two drawbacks. The
first drawback is that if you mix Java with another language—for example, if you add a few C
functions to your Java program—all security is lost. It is still possible to send your program
over the network, but it is very unlikely that you will persuade any sensible recipient to run
it.
The second drawback is better known: programs interpreted by the JVM are slow. The fastest
way to get things done is to execute raw machine code (that is, executable code, also known
as “native code”). Each bytecode represents an operation that may require anything from 10
to 1,000 or more machine code instructions. Interpreted bytecodes run anything from 2 to
100 times slower than native code, although for modern JVMs, the speed factor is typically
around 5. For many programs, the slow speed is acceptable; for some programs, it is not.
To avoid speed problems, bytecodes can be converted into native code which is executed
directly by the processor. This is done by the bytecode compiler in Figure 1. Even a bytecode
1 INTRODUCTION
3
compiler cannot match the performance of a good C++ compiler, but it may come close.
Modern techniques combine the two approaches. A just in time (JIT) compiler compiles
each instruction when it is first encountered. The program starts up slowly, because it is being
compiled but, as soon as the most frequently used instructions have been compiled, it runs
very fast. For many programs, only a small proportion of the code is needed for a particular
run; JIT compilers do very well for programs of this kind, because they compile only as much
as they need, ignoring uninterpreted instructions altogether.
1.2
Review
In this section, we will review some of the basic ideas and terminology of Java. All or most
of this material should be familiar to you from COMP 248.
1. A program text is a collection of one or more class definitions.
2. A running program is a collection of objects. Each object is an instance of a class.
3. A class has constructors, methods, and instance variables.
4. A method is a piece of code that performs some actions. Other names for method are
function and procedure.
5. A constructor is a special kind of method. Constructors are used to initialize newlycreated instances of the class.
6. An instance variable is an item of data associated with an instance of the class. Since
most variables are actually instance variables, we often say “variable” when we mean
“instance variable”. “Attribute” is another word for instance variable.
7. A feature is a method or a variable. (This usage, which comes from Eiffel, is not
standard. However, it is quicker to say “feature” than to say “method, or instance
variable”.)
8. All instances of a class have the same constructors and methods. (In fact, only one copy
of the code is stored even if there are many instances.)
9. Each instance of the class has its own copies of the instance variables of the class. In
other words, an object that is an instance of a class contains one copy of each of the
instance variables of that class.
10. Methods and variables may have various modifiers (also sometimes know as qualifiers).
11. A public feature is accessible from outside the class. This means that, if o is an instance
of class C, and f is a public feature of class C, then we may write o.f(....) to use the
feature ((....) is an argument list).
12. A private feature is accessible only within the class in which it is defined. We can
use the keywords public and private to control how other parts of the program use
objects in our classes.
1 INTRODUCTION
4
13. A good general rule is that all instance variables should be private. There are
very few circumstances in which it is a good idea make a variable public because, when
we do so, we lose control of the class.
14. Another useful rule is that most methods should be public. However, there are
sometimes good reasons for making a method private and so this rule is not so clearcut as the previous rule.
15. The modifier static may be applied to variables and methods. (It can also be applied
to classes, as we will discuss later.)
16. A static variable is associated with the class, not the instances of the class. As an
example, we could use a static variable to count the number of instances of a class.
17. A static method is accessed through its class name rather than through an object
name. For example, the method sqrt (square root) is a static method of the class
Math. We do not need an instance of Math to use it, because we can write Math.sqrt(2).
18. The modifier final may be applied to variables and methods.
19. A final variable cannot be changed after it has been declared; in fact, it behaves as
a constant.
20. Each variable has a type. There are primitive, or built-in types, and class, or userdefined types.
21. There are four primitive types for integers: byte (8 bits); short (16 bits); int (32 bits);
and long (64 bits).
22. There are two primitive types for floating-point numbers: float (about 7 decimal digits);
and double (about 15 decimal digits).
23. Characters are represented by the primitive type char. Java uses the Unicode character
set, of which ASCII is a subset. A char occupies 16 bits (2 bytes) of memory.
24. Boolean (true/false) values are represented by the primitive type boolean.
25. There is a special type void that has no values. (Strictly speaking, void has one value
that is represented with log2 1 = 0 bits.) It is used to describe the absence of a value:
for example, a function that “returns void” does not actually return any value.
26. For each primitive type, there is a corresponding wrapper class which makes the simple
value look like an object. In most cases, the wrapper class name is the same as the type
name except for an initial capital letter. For example, the wrapper for byte is Byte.
In two cases, the wrapper class has a longer name: Integer wraps int and Character
wraps char.
27. Values of any type can be stored in an array . Array elements are indexed with square
brackets. For example, a[i] denotes element i of array a. If an array has N elements,
their indexes are 0, 1, 2, . . ., N − 1.
28. The body of a method is a sequence of statements. Some statements are simple
(assignments, method invocations) and others are structured (conditionals and loops).
1 INTRODUCTION
5
Although Java is a fairly simple language (at least in comparison with languages such as
PL/I and C++), it has a large number of libraries. The main difficulty in becoming a good
programmer is not learning the language, which should not be hard, but becoming familiar
with the libraries and knowing how to use them effectively.
2 INHERITANCE
2
6
Inheritance
The strength of object oriented programming comes not just from individual classes but also
from the way in which classes are related to one another. One very important way of relating
classes is by inheritance.
2.1
Classification
Centuries of thinking have enabled us to evolve systematic ways of dealing with large amounts
of data. One of the most important techniques is classification. There is even a Classification
Society (http://www.pitt.edu/~csna/). Taxonomy is a fancy name (derived from Greek)
for classification.
One of the oldest and largest taxonomies is the system used for classifying living organisms.
Although most people are familiar with the word, “species” is in fact one of the least-well
defined terms in the taxonomy. But a species is a member of a genus, a genus is a member of
a family, and so on. The complete structure, down to the level of species is:
Kingdom → Phylum → Class → Order → Family → Genus → Species
For example, the classical taxonomy classifies a grey wolf as follows:
Kingdom
Phylum
Sub-phylum
Class
Order
Family
Genus
Species
:
:
:
:
:
:
:
:
Animalia
Chordata
Vertebrata
Mammalia
Carnivora
Canidae
Canis
Canis lupus
The modern subject of cladistics generalizes the classical scheme: the number of levels varies
according to the complexity of the organism. Thus the traditional names of divisions are rarely
used nowadays.
Suppose that you have a pet dog, Fido. Fido is an instance of the species canis domesticus.
Membership of this class means that Fido has certain properties, such as teeth, claws, a tail,
body hair, and so on. Fido shares many properties with a grey wolf, but they differ in that
grey wolves are not suitable as house pets. Your cat (if you have one) has further differences:
it is a member of the family Felidae. Dogs and cats, however, both belong to Carnivora (as
do we).
The differences between your dog and a grey wolf are expressed by putting them into
different species (canis domesticus and canis lupus).
The similarities between your dog and a grey wolf are expressed by putting them into
the same genus (Canis).
Thus a taxonomy allows us to describe both similarities and differences at various levels in a
large group of objects. We can also apply taxonomic techniques to concepts. For example, we
have disciplines (mathematics, physics, computer science, leisure science, etc.) and, within a
discipline, we have subjects (algebra, calculus, set theory, topology, etc.).
2 INHERITANCE
7
Person
6
Student
Instructor
6
Undergraduate
6
Graduate
PartTime
Professor
6
FullTime
Figure 2: A taxonomy or class hierarchy
How is this related to Java? We already know that Java objects are instances of classes. The
classes are organized as a taxonomy. Due to an unfortunate mixture of metaphors, we say
that one class inherits from another. In the biological taxonomy, using this terminology,
we would say that Canidae inherits from Carnivora, which in turn inherits from Mammalia.
This makes sense if we interpret it as “members of Canidae inherit certain properties from
Carnivora”.
The following terminology is conventional in object oriented programming:
A subclass inherits from a superclass.
A subclass is also called a child class.
A superclass is also called a parent class.
A child class is below its parent class.
A parent class is above its child class.
The classes above a class are its ancestors.
The classes below a class are its descendants.
A collection of classes related by inheritance is called a class hierarchy .
The “above” and “below” usage is derived from the conventional way of drawing class hierarchies.
We can view the diagram in Figure 2 as a taxonomy or a class hierarchy. Applying the
terminology we have introduced to this hierarchy, we can say:
Student is a subclass, or child, of Person.
Student is a superclass, or parent, of Undergraduate.
The ancestors of Graduate are Student and Person.
The descendants of Instructor are PartTime and FullTime.
Person is above (or “higher then”) all other classes in the hierarchy.
By convention, the arrows in a class hierarchy point upwards: the arrow from a subclass
points to its superclass.
2 INHERITANCE
8
Note the arrow from FullTime to Professor. If we ignore this arrow, the diagram is a tree
with Person as its root. (A tree has the property that each class has exactly one superclass.
The diagram in Figure 2 is not a tree because FullTime has two superclasses.) FullTime
inherits from both Instructor and Professor; for obvious reasons, this is called multiple
inheritance.
Multiple inheritance introduces a number of complications and, consequently, not all object
oriented languages allow it. In particular, Java does not allow multiple inheritance and
so we will not discuss it further in this course.
2.2
Inheritance in Java
Although there is a close resemblance between a taxonomy and a class hierarchy, there are
also some differences. Consequently, it is important to learn the details of inheritance in Java
and not to rely too much on analogies.
In Java, inheritance is expressed by the keyword extends. If we were implementing the
hierarchy of Figure 2, we would first write
public class Person { .... }
and then we would write
public class Student extends Person { .... }
The keyword extends says:
Class Person is the superclass (parent) of class Student
Class Student is a subclass (child) of class Person
Why is this called “inheritance”? Because class Student inherits the public features of
class Person. If we don’t write any code at all for class Student, an instance of Student
behaves in exactly the same way as an instance of Person (where “behaves” means has the
same attributes, responds to the same methods, and so on).
Example 1: The Person Hierarchy. We can implement the hierarchy of Figure 2 in Java.
The first step is to write a class for Person, as shown in Figure 3. A Person has a name, a
constructor that sets the name, and a method to print the name: there is nothing new here.
The next step is to write a class for Student, as shown in Figure 4. A student has an ID as
well as a a name. Several new ideas appear in this class.
The keyword extends is used to inform the compiler that Student is a subclass of Person.
The constructor has parameters for the name and ID of the new student. The declaration
of the ID appears in this class and presents no new problems. However, the name field
is inherited from Person, and we cannot access it because it is private. The solution
is to use the new keyword super as if it was the name of a method. In this case,
super(newName) calls the constructor for class Person and passes newName to it.
The method printData prints the name and ID of the student. Printing the ID is
straightforward, but again we have a problem when we try to print the name. The
solution in this case is to use the keyword super again, but this time as if it denotes an
object. We can write super.m() to call any method m in the parent class of the current
class.
2 INHERITANCE
9
public class Person
{
private String name;
public Person(String newName)
{
name = newName;
}
public void printName()
{
System.out.println(name);
}
}
Figure 3: Class Person
public class Student extends Person
{
private int id;
public Student(String newName, int newId)
{
super(newName);
id = newId;
}
public void printData()
{
super.printName();
System.out.println("ID: " + id);
}
}
Figure 4: Class Student
The class Test, shown in Figure 5 illustrates both of these classes in action. It constructs
a new person, joe, and prints Joe’s name — nothing new here. Then it constructs a new
student, jill, and prints Jill’s name and ID. The output from this program looks like this:
Joe
Jill
ID: 1234567
2
Class Student inherits all of the features of its superclass Person except the constructor
2 INHERITANCE
10
public class Test
{
public static void main(String[] args)
{
Person joe = new Person("Joe");
joe.printName();
Student jill = new Student("Jill", 1234567);
jill.printData();
}
}
Figure 5: Class Test
Person() (we have seen that Student can invoke their constructor using the name super).
Consequently an instance of Student has the following components (not including constructors):
Feature
Variables:
Methods:
Name
name
id
printName
printData
Accessible?
×
√
√
√
A Student owns, and can access, the variable id and the method printData. It also owns
the variable name, but cannot access it directly because name is declared private in Person.
However, Student can access name indirectly, for example by calling super.printName().
In fact, since printName is declared public in class Person, class Student can use it directly.
There are two ways of confirming this.
First, the program works correctly if we replace the call super.printName() by the
simply printName().
Second, we can add the statement jill.printName() to the main function in class Test.
Both of these changes have the effects we would expect: the program compiles and runs.
If we add the following line to class Student, it will not compile:
System.out.println("Name: " + name);
The reason, of course, is that name is a private attribute of class Person and is therefore not
accessible in class Student.
The next step is to define a class for Professor. Professors are rather like students; the main
difference is that, whereas students have an ID, professors have a salary. The definition of
class Professor, shown in Figure 6, is very similar to class Student.
2 INHERITANCE
11
public class Professor extends Person
{
private int salary;
public Professor(String newName, int newSalary)
{
super(newName);
salary = newSalary;
}
public void printProfData()
{
super.printName();
System.out.println("Salary: " + id);
}
}
Figure 6: Class Professor
2.2.1
Overriding Methods
The number of methods in our example is growing rather rapidly: we now have printName,
printData, and printProfData. Furthermore, all of these methods do much the same thing—
they print information about the object. Can we manage with fewer names?
Indeed we can, and there are compelling reasons for doing so. In fact, we can achieve all of
the current functionality with just one name: print. This is accomplished as follows:
We declare a public method print in the root class, Person. This method is, of course,
inherited by all subclasses of Person.
In each subclass, we define print again. The signature (name and parameter list) must
be the same as before, but the body of the method can be different.
This technique is called overriding : the method in the subclass overrides the inherited
method. As before, we can invoke the method print in the superclass by writing super.print.
Example 2: The Person Hierarchy Revisited. Figure 7 shows the Person hierarchy with
constructors as before and a single function, print, which is defined in the root class Person
and overridden in the subclasses Student and Professor 2
2.3
Inherit . . . . or not?
In Figure 2, Student has two subclasses, Undergraduate and Graduate. In what ways do
graduates differ from undergraduates?
Undergraduates cannot take graduate courses. Graduates cannot take undergraduate
courses for credit. (There are a few exceptions to these rules, but that doesn’t change
the main point.)
Graduates have to complete a thesis, as well as taking courses, to get their degrees.
2 INHERITANCE
12
public class Person
{
private String name;
public Person(String newName)
{
name = newName;
}
public void print()
{
System.out.println("Name: " + name);
}
}
public class Student extends Person
{
private int id;
public Student(String newName, int newId)
{
super(newName);
id = newId;
}
public void print()
{
super.print();
System.out.println(" ID: " + id);
}
}
public class Professor extends Person
{
private int salary;
public Professor(String newName, int newSalary)
{
super(newName);
salary = newSalary;
}
public void print()
{
super.print();
System.out.println("Salary: " + salary);
}
}
Figure 7: The Person class hierarchy
2 INHERITANCE
13
The University gets more money from the government for a graduate than for an undergraduate.
These differences suggest that the differences between graduates and undergraduates are fairly
minor. They could be implemented without inheritance:
We could add a boolean attribute to class Student to tell whether a student is allowed
to take graduate (or undergraduate) courses.
We could add another boolean attribute to tell whether a student is required to complete
a thesis.
We could add a field showing the amount of money received from the government for
the student. The actual amount would depend on whether the student was a graduate
or an undergraduate and on other factors, such as the program in which the student is
registered.
Unless more significant differences come to light, it does not seem necessary to create new
classes for Graduate and Undergraduate.
The situation is different with respect to Student and Professor. In our example, a Student
has an ID and a Professor has a salary. If we developed the example further, other differences
would appear. Although both professors and students are involved with courses, students
take courses and professors teach courses (usually, at least). There are clear differences of
behaviour between students and professors.
In general, we should use inheritance only when the following conditions hold:
There are a number of different entities.
The entities have a number of common features that can be expressed in a class definition.
The entities have significant differences that cannot easily be expressed simply—for example, by the value of a variable.
2.4
Polymorphism and Dynamic Binding
We have already seen that both of the following sequences work correctly when we have defined
the Person hierarchy:
Person joe = new Person("Joe");
joe.print(); // Prints Joe’s name
Student jill = new Student("Jill", 1234567);
jill.print(); // Prints Jill’s name and ID
Professor jane = new Professor("Jane", 65000);
jane.print(); // Prints Jane’s name and salary
Using inheritance in this way is helpful because it reduces the amount of coding that we have
to do. In the Person hierarchy, we have put everything connected with the attribute name into
the root class, Person. The classes Student and Professor have to deal only with specialized
data (id and salary respectively).
We can do more. Consider the following code:
2 INHERITANCE
14
Person jim = new Professor("James", 65000);
jim.print(); // Prints James’s name and salary
The key thing to note in this example is the mixed assignment statement: the left side of
the assignment constructs an instance of Professor and the result is assigned to a variable
of type Person. We can do this because Professor is a subclass of Person.
Assignment Rule: The assignment V = E is allowed if the class of E is the same as, or is
a descendant of, the class of V .
When mixed assignments are introduced, a variable can have two types (or classes).
Definition:
The static type of a variable is the type that appears in its declaration.
Definition:
to it.
The dynamic type of a variable is the type that was most recently assigned
In the example above, the variable jim has static type Person and dynamic type Professor.
A variable can have more than one dynamic type at different times during execution. After
the last assignment in the following sequence, the dynamic type of jim is Student:
Person jim = new Professor("James", 65000);
jim.print(); // Prints name and salary
jim = new Student("Jacques", 1234567);
jim.print(); // Prints name and ID
When we invoke a method of a variable, it is the dynamic type of the variable that determines
which method is used. When we write jim.print(), the print method could come from the
static class of jim (that is, Person) but, in fact, it comes from the dynamic class of jim
(that is, Professor or Student).
In the context of programming languages, binding in means “association of a name and a
property”. In this case, we are binding a method to a name, print. Since the binding depends
on the dynamic type of the variable, it is called dynamic binding .
Example 3: Dynamic Binding. Figure 8 shows a program that illustrates the use of
dynamic binding. Near the beginning of the program, there is a declaration, Person client.
The last statement is client.print(). The interesting and important point that this program
demonstrates is that:
from the source text of the program, we cannot tell which version of print will be
invoked when client.print() is executed.
To see why this is so, consider the code in between the declaration of client and the statement
client.print(). Depending on how the user replies to the questions, any of the following
three statements may be executed:
client = new Professor(name, Integer.parseInt(sal));
client = new Student(name, Integer.parseInt(id));
client = new Person(name);
Consequently, at the end of the program, client might refer to a Professor, a Student, or
a Person. The effect of client.print() depends on which one it actually does refer to. 2
2 INHERITANCE
15
public class Test
{
public static void main(String[] args) throws IOException
{
InputStreamReader isr = new InputStreamReader(System.in);
BufferedReader br = new BufferedReader(isr);
Person client;
System.out.print("Enter your name: ");
String name = br.readLine();
System.out.println("Are you a professor or a student?");
System.out.print("Answer ’p’ or ’s’: ");
String reply = br.readLine();
if (reply.equals("p"))
{
System.out.print("What is your salary? ");
String sal = br.readLine();
client = new Professor(name, Integer.parseInt(sal));
}
else if (reply.equals("s"))
{
System.out.print("What is your ID? ");
String id = br.readLine();
client = new Student(name, Integer.parseInt(id));
}
else
{
client = new Person(name);
}
client.print();
}
}
Figure 8: Dynamic Binding
Well-designed programming languages use the same rules for passing parameters as they do
for assignment. This is true in Java. Suppose we have a method m with a parameter p: we
can pass any object a to m provided that the class of a is the same as, or a subclass of, the
class of p.
For example, we could add the following method to the class Test of Figure 8:
static void display(Person p)
{
p.print();
}
2 INHERITANCE
16
If we write
display(client);
in method main, the program will compile and run correctly, and display() will call print
for the object that is passed to it, whether that object is a Person, a Student, or a Professor.
The example is trivial, but the consequence is important:
we can write methods that perform different actions, depending on the class of the
object passed to them.
Although the behaviour of display depends on the class of its argument, the method that
is actually doing the work is print. The word “polymorphic” is derived from the Greek for
“many shapes”.
Definition: A polymorphic method is a method that behaves in different ways, depending
on the class in which it is actually executed.
2.5
The protected Modifier
Features modified by private are accessible only in the class in which they are declared, and
features modified with public are accessible to all users of the class. For inheritance purposes,
it is often useful to have an intermediate level of access. The modifier protected provides
just such an intermediate level.
Rule: A feature modified with the keyword protected is visible in the class containing its
declaration and all descendants of that class.
In the example we have been using, we could add a declaration to class Person:
public class Person
{
private String name;
protected Date birth;
}
The attribute birth is accessible in class Person and in subclasses of Person, such as Student
and Professor, but is not accessible to any other classes.
The protected modifier can be applied to variables and methods. You should use it in a
class that is designed to be a parent class—in other words, you expect that you or others
will write subclasses for this class. Any feature that should not be public but is likely to be
useful in a subclass should be protected.
When you extend a class, you can make its features more accessible but you cannot make
them less accessible. For example, if a method is declared protected in the parent class and
overridden in the child class, you can declare it as protected (same accessibility) or public
(increased accessibility) but not private (reduced accessibility). Figure 9 shows the effect of
this rule.
2 INHERITANCE
17
Class
Subclass
public
protected
private
public
protected
private
√
√
√
√
√
√
×
×
×
Figure 9: Modifying accessibility
2.6
The Class Object
The declaration
class Student extends Person { .... }
tells us that Student is an extension of a superclass, Person. In fact, all classes are extensions
of the “ancestral” class Object. In other words, when we write
class Person { .... }
the Java compiler interprets this as
class Person extends Object { .... }
It follows that every class that we write has all of the features of class Object. This includes
subclasses, such as Student, because they inherit Person’s features transitively.
What are the features of Object? Here are a few (see page 876 of Lewis & Loftus or
http://java.sun.com/j2se/1.4.2/docs/api/ for a complete list):
protected Object clone();
// Returns an exact copy of the object.
public Boolean equals(Object other);
// Returns ’true’ if ’other’ is the same object as this one.
public String toString();
// Returns a string representation of the object.
public final Class getClass()
// Returns the name of the dynamic class of the object.
These methods are not as useful as they might appear.
If you want to make copies of objects, it is usually necessary to override method clone
with your own method. (You must also implement the interface Cloneable, as we will
discuss later.)
The expression x.equals(y) is true only if x and y are the same object: thus, for
example, two instances of Person with the same name are not equal by default.
2 INHERITANCE
18
The method toString is defined like this:
public String toString()
{
return getClass().getName() + ’@’ + Integer.toHexString(hashCode());
}
In other words, it returns the class of the object and its hash code in hexadecimal. This
is not very useful but it becomes useful if most classes redefine it in a useful way. In
general, we should not include a System.out.print call in a class as we have been doing;
it is better to define an override of the method toString for each class.
getClass is not terribly useful in “production” software because class names should be
hidden secrets of the application. It is sometimes useful in debugging and, in fact, we
use it in Section 2.7 below: see Figure 16 on page 23.
2.7
Text Processing
It is often a good idea to base a complex program or system on a few basic metaphors. For text
processing, one good metaphor is “boxes and glue”. A box is a rectangular region containing
an image. A small box might contain a character, a slightly larger box might contain a word,
and a still larger box might contain a paragraph or a picture. Boxes can be joined together
in various ways: for example, if we join a number of word-boxes horizontally, we obtain a line
of text. If we join a number of line-boxes vertically, we obtain a paragraph of text.
In many cases, boxes are joined directly. In other cases, there must be some white space
between them. The glue is just this white space. Words can be joined together with glue.
By allowing the glue to stretch, we can obtain effects such as centering and justification.
In this small example, we will consider boxes only, not glue. Each box will be an object, and
we will have a hierarchy of box classes. The root of the hierarchy is the class Box and the
subclasses of Box are Row, Stack, Word, and Space. Figure 10 shows a simple application of
these classes: when run, it produces the following output:
Here we go
Here we go
10 by 2
The third line, 10 by 2, is produced by the statement s.showSize(). The output from the
last line, s.show(0), is not shown here; we will discuss it later (see page 22 and Figure 16 on
page 23).
The simplest kind of box is a Space: it produces one blank character in the output string. A
Word is also quite simple: it is constructed with a word, and it produces that word. The other
classes Row and Stack, both provide the method add, but differ in the way that add works.
A Row object grows horizontally and a Stack object grows vertically.
Figure 11 shows the root class, Box. The variables, which are protected so that they can be
accessed by subclasses, give the dimensions of the box and its components. The components
are stored in an ArrayList, which can contain objects of any type;1 In fact, we will store only
1
More precisely, an ArrayList can store instance of Object and any of its descendants—which means, in
practice, objects of any class, because all classes are descendants of Object.
2 INHERITANCE
19
public class Test
{
public static void main(String[] args)
{
Row r = new Row();
r.add(new Word("Here"));
r.add(new Space());
r.add(new Word("we"));
r.add(new Space());
r.add(new Word("go"));
Stack s = new Stack();
s.add(r);
s.add(r);
System.out.print(s);
s.showSize();
s.show(0);
}
}
Figure 10: Testing the text processor
instances of Box and its subclasses in the ArrayList components. The constructor simply
initializes the variables with default values.
All of the methods are intended to be inherited by subclasses. Since we do not permit additions
to a Box, the method add prints an error message. In subclasses, we will override this method
in classes that permit addition.
Figure 12 shows the class Row—a subclass of Box. Class Row inherits most of its behaviour
from its superclass, Box. The only exception is the method add, which overrides the default
version in Box. The width of a row is the sum of the widths of the boxes in it: this explains
the assignment width += b.width. The height of a row is the height of the highest box in it.
This ensures that, for example, if we have one very tall character in a line, the height of that
character becomes the height of the line, and the character will not run into the line above.
The if statement ensures that height gets the correct value. Finally, the new box is added
into the components list.
Class Stack, shown in Figure 13, is very similar to class Row. The method add sums the
heights of the boxes and records the width of the widest box. Since the components of a
Stack must be written one above the other, the method toString is overridden: this version
inserts a new line after each component. A real text processor would insert “leading”2 (glue
for inter-line spacing) instead of just a new line character.
An instance of class Word, shown in Figure 14, stores one word. We assume that the width of
2
Pronounced “ledding”. After setting a line of metal type, the typesetter would insert a thin strip of lead
before starting the next line. When the page was complete, s/he would squeeze it vertically. Lead, being soft,
would contract slightly to give even line spacing.
2 INHERITANCE
20
import java.util.ArrayList;
public class
{
protected
protected
protected
Box
int width;
int height;
ArrayList components;
public Box()
{
width = 0;
height = 0;
components = new ArrayList();
}
public void showSize()
{
System.out.println(width + " by " + height);
}
public void add(Box b)
{
System.out.println("Error: cannot add to this object!");
}
public String toString()
{
String buffer = "";
for (int i = 0; i < components.size(); i++)
{
buffer += components.get(i);
}
return buffer;
}
}
Figure 11: Class Box
the word is the number of characters it has and its height is 1. In a more realistic example, we
would obtain the width by summing the widths of the characters (which are not all the same
in a proportional font) and we would use the exact height of the character (not all characters
have the same height). The method toString returns the word itself.
Class Space (Figure 15) is the simplest of all: it has a width and height of 1, and the method
toString returns just a blank character.
The five classes illustrate, in a simple way, some typical features of object oriented programming.
2 INHERITANCE
21
public class Row extends Box
{
public Row()
{
super();
}
public void add(Box b)
{
width += b.width;
if (height < b.height) height = b.height;
components.add(b);
}
}
Figure 12: Class Row
public class Stack extends Box
{
public Stack()
{
super();
}
public void add(Box b)
{
height += b.height;
if (width < b.width) width = b.width;
components.add(b);
}
public String toString()
{
String buffer = "";
for (int i = 0; i < components.size(); i++)
{
buffer += components.get(i) + "\n";
}
return buffer;
}
}
Figure 13: Class Stack
2 INHERITANCE
22
public class Word extends Box
{
private String text;
public Word(String t)
{
super();
width = t.length();
height = 1;
text = t;
}
public String toString()
{
return text;
}
}
Figure 14: Class Word
public class Space extends Box
{
public Space()
{
super();
width = 1;
height = 1;
}
public String toString()
{
return " ";
}
}
Figure 15: Class Space
The root class, Box, suggests the general form for all of the other classes—its subclasses.
In a typical application, such as the one in Figure 5, the class Box is not actually used
directly: only its subclasses appear in the program.
Subclasses inherit many features from the root class, overriding them only when necessary
to achieve some effect. For example, Stack must insert blank lines into its stringified form.
Working with a set of classes that use one another can become quite confusing. It is often
helpful to write additional methods that are intended purely for debugging. The method show
in Figure 16 is an example: it is declared in class Box and can therefore be invoked for any
2 INHERITANCE
23
public void show(int indent)
{
for (int i = 0; i < indent; i++)
System.out.print("
");
System.out.print(getClass() + " ");
showSize();
for (int i = 0; i < components.size(); i++)
{
Box b = (Box)components.get(i);
b.show(indent + 1);
}
}
Figure 16: Method show for class Box
kind of Box (since it is inherited by subclasses). It prints the name of the subclass, using the
Object method getClass mentioned above and the size of the box. It then calls show for
each of its components.
The variable ArrayList stores a list of Objects. We cannot write
components.get(i).show(level + 1);
because the compiler thinks that the object returned by components.get(i) is an Object,
and Objectss do not provide the method show. It is therefore necessary to get an object from
components, cast it to a Box (which is safe because everything we have put into it is an of
Box or one of its subclasses), and then invoke show on the converted name.
This statement is accepted by the compiler
((Box)components.get(i)).show(level + 1);
Statements like this are confusing for the reader, however, and it is better to write two lines,
as in Figure 16. This form is easier to understand; it may be very slightly slower, but you
should not worry about that.
The parameter indent is a common trick used to obtain output with indentation that shows
the tree structure. At the “top level”, we pass the argument 0 (see Figure 10 on page 19) and,
each time we go “down” a level, we pass a larger value. Figure 17 shows the output produced
by calling show for the structure created in Figure 10.
2 INHERITANCE
24
class Stack 10 by 2
class Row 10 by 1
class Word 4 by 1
class Space 1 by 1
class Word 2 by 1
class Space 1 by 1
class Word 2 by 1
class Row 10 by 1
class Word 4 by 1
class Space 1 by 1
class Word 2 by 1
class Space 1 by 1
class Word 2 by 1
Figure 17: Output produced by method show
3 FROM CLASSES TO INTERFACES
3
25
From Classes to Interfaces
A class describes the behaviour of its instances. Sometimes, we want to specify what we want
to do without describing exactly how to do it. That is what interfaces are for: an interface
prescribes the behaviour of the classes that implement it. Since a Java interface is a kind of
abstract class, we discuss abstract methods, abstract classes, and then interfaces.
3.1
Hidden Fields
Here are partial declarations of two classes:
class Parent
{
protected int bean;
....
}
class Child extends Parent
{
private float bean;
....
}
Note that both the superclass and the subclass have a variable named bean. This is allowed,
and we say that bean in class Parent is hidden in class Child. Although the parent’s bean
is hidden, we can refer to it using super. Assume that the following code is used in one of
Child’s methods:
System.out.println(bean);
// Prints Child’s bean
System.out.println(super.bean); // Prints Parent’s bean
The fact that Java allows hidden variables means that we can declare variables in child classes
without having to know whether the parent class has a variable with the same name. If we
have access to the code for the parent class, however, it is best to avoid declaring variables
with the same name to avoid confusion.
We now have three possible ways of using the same name in a parent class P and a child class
C. Note that the first two are very similar.
1. A variable x is declared in P and C. There are two separate variables, one in each
class. The x in C hides the x in P. The two variables may have the same or different
types. It is best to avoid hidden variables if you can.
2. A method m is declared in P and again in C with a different parameter list. There are
two separate methods, one in each class.
3. A method m is declared in P and again in C with the same parameter list and return
type. The definition of m in C overrides the m in P.
There is an intermediate case: method m might be declared in P and again in C with the same
parameter list and a different return type. This is not allowed and the compiler will report
an error.
3 FROM CLASSES TO INTERFACES
3.2
26
The Modifier final
We can apply the modifier final to a class or any of its features.
A final class cannot be extended.
A final method cannot be overridden by a subclass.
A final variable cannot have its value changed.
The modifier final should be used with great caution, because it may prevent a programmer
doing something legitimate and useful. For example, the Java library defines
public final class String { .... }
This means that you cannot extend String in any way! If you want a string class with some
additional functionality—which is by no means an unreasonable requirement—you must write
your own class from scratch.
There are some good uses of final. For example, if you have a class called validatePassword,
you probably do not want people to extend it or override its methods. You could achieve this
either by declaring the class final, preventing any extensions, or by declaring the sensitive
methods final, to prevent overriding. A better way, of course, would be to declare the
method private in the parent class, making it inaccessible in the child class.
3.3
Abstract Methods
If we think about how the Box hierarchy (Section 2.7) will be used in practice, we see that
we will never actually construct an instance of class Box. The only kind of box that we will
need are the subclasses of Box that provide some specific and useful behaviour: class Row to
make horizontal rows, class Stack to make vertical columns, and so on. This is analogous to
the biological taxonomy: every instance of Mammal that is born, for example, is actually an
instance of a subclass of Mammal—it is a horse, a dog, or a mouse.
What effect does this have on the methods of Box? Some methods are the same in every
subclass: it makes sense to define them in the root class, Box, and let each subclass inherit
them. The method showSize, for example, performs the same task in every class of the
hierarchy, and so it is appropriate to define it in Box.
The method toString is different: it is overridden in every class. More precisely, it is not
overridden in class Row, because Row happens to need the behaviour that we defined in Box.
If we copy the definition of toString to class Row, then every subclass of Box overrides
the definition of toString. Since there are never any instances of Box, the code in Box for
toString is never executed. We should remove it.
If we remove the entire function, we have a problem:
Box b = ....;
....
b.toString();
This code does not compile because toString is no longer a member of Box.
We solve this problem by making toString an abstract method of Box. Instead of having
a body, it has the modifier abstract and a semicolon after the parameter list:
3 FROM CLASSES TO INTERFACES
27
public class Box
{
....
public abstract String toString();
....
}
3.4
Abstract Classes
Classes like this would be dangerous, because they would allow users to create objects with
undefined methods. Consequently, Java does not allow them. If we declare and abstract
method, we must make the class abstract as well:
public abstract class Box
{
....
public abstract String toString();
....
}
A class modified by abstract cannot have instances. It may have, but is not required to
have, abstract methods. Since an abstract class cannot be used directly, we define abstract
classes to serve as roots for class hierarchies. The declaration above is saying, in effect:
Class Box is a class from which you may inherit some useful stuff. If you want
to have instances of your inherited class, you must override toString with a
non-abstract method.
Here is a summary of the ideas we have presented so far.
Definition: An abstract method has the modifier abstract and a semicolon instead of a
body {....}. An abstract method cannot be invoked but may be overridden in subclasses.
Definition:
An abstract class has the modifier abstract and cannot be instantiated.
Rule: Any class may be declared abstract. However, if a class contains any abstract methods, then it must be declared abstract.
Convention: A class or method that is not abstract is called concrete.
The convention allows us to express the rule in this way: a concrete class cannot contain
abstract methods. The rule is a natural consequence of the meaning of abstract: it prevents
us from constructing an object with undefined methods.
Sensibly, Java does not impose regulations on which classes in a hierarchy must be abstract
or concrete: all combinations are allowed, as shown in Figure 18.
Example 4: The abstract Box. Figures 19 through 23 show the classes of the Box hierarchy
with improvements based on the ideas presented above. The changes are as follows:
Class Box has an abstract method, toString, and must therefore be declared as an
abstract class.
3 FROM CLASSES TO INTERFACES
Class
Abstract
28
Concrete
Subclass
Abstract
Usually near the root of a deep
hierarchy
Unusual, but allowed
Concrete
Most common case
Usually near the leaves of a
deep hierarchy
Figure 18: Inheriting abstract and concrete classes
Class Row, which previously inherited its definition of toString from Box, must now
define its own version of toString.
The constructors of classes Row and Stack, which previously had the form { super(); },
have been omitted, because the compiler will generate them automatically.
The calls to super() have been omitted from the constructors for Word and Space because
the compiler will generate them automatically.
2
3 FROM CLASSES TO INTERFACES
import java.util.ArrayList;
public abstract class Box
{
protected int width;
protected int height;
protected ArrayList components;
public Box()
{
width = 0;
height = 0;
components = new ArrayList();
}
public void showSize()
{
System.out.println(width + " by " + height);
}
public void add(Box b)
{
System.out.println("Cannot add a box to this object.");
}
public abstract String toString();
public void show(int level)
{
for (int i = 0; i < level; i++)
System.out.print("
");
System.out.print(getClass() + " ");
showSize();
for (int i = 0; i < components.size(); i++)
{
Box b = (Box)components.get(i);
b.show(level + 1);
}
}
}
Figure 19: Revised class Box
29
3 FROM CLASSES TO INTERFACES
public class Row extends Box
{
public void add(Box b)
{
width += b.width;
if (height < b.height) height = b.height;
components.add(b);
}
public String toString()
{
String buffer = "";
for (int i = 0; i < components.size(); i++)
{
buffer += components.get(i);
}
return buffer;
}
}
Figure 20: Revised class Row
public class Stack extends Box
{
public void add(Box b)
{
height += b.height;
if (width < b.width) width = b.width;
components.add(b);
}
public String toString()
{
String buffer = "";
for (int i = 0; i < components.size(); i++)
{
buffer += components.get(i) + "\n";
}
return buffer;
}
}
Figure 21: Revised class Stack
30
3 FROM CLASSES TO INTERFACES
public class Word extends Box
{
private String text;
public Word(String t)
{
width = t.length();
height = 1;
text = t;
}
public String toString()
{
return text;
}
}
Figure 22: Revised class Word
public class Space extends Box
{
public Space()
{
width = 1;
height = 1;
}
public String toString()
{
return " ";
}
}
Figure 23: Revised class Space
31
3 FROM CLASSES TO INTERFACES
32
P
6
6
6
C1
C2
6
C3
6
C4
Figure 24: Designing a class hierarchy
3.5
When to use abstract
Figure 24 shows a simple class hierarchy with a parent class P and child classes C1 , C2 , C3 ,
and C4 . Suppose that there is a method m that might be used by some or all of these classes.
The following guide lines suggest ways of deciding which declarations of m should be abstract:
m has exactly the same behaviour in each class: define m in P and let the child classes
inherit it.
m has the same behaviour in several of the child classes but different behaviour in a
few of them: define m in P and override the definition in child classes that need special
treatment.
m behaves differently in every child class: declare m as an abstract method in P and
provide definitions in every child class.
The following guidelines suggest ways of deciding whether class P should be declared abstract:
If any of the methods of class P are abstract, then P must be declared abstract.
If you want to create instances of P , it cannot be an abstract class and all of its methods
must be concrete.
If all of the methods of P are abstract, it might be better defined as an interface (see
next section).
3 FROM CLASSES TO INTERFACES
3.6
33
Interfaces
It is sometimes useful to take the “abstract” concept to an extreme, making everything
in a class abstract. This class then becomes a kind of specification: it does not provide any
behaviour by itself, but it enforces some behaviour on its subclasses.
For example, the only important property of the class Person in Figure 3 on page 9 is “having
a name”. This property is quite general and might be applied to objects other than Persons.
The key feature of such a class would be a method that returns the name; child classes would
have the responsibility of defining appropriate variable declarations, constructors, and other
useful methods. The class declaration might look like this:
public abstract class Nameable
{
public abstract String getName();
}
Although it might not look very interesting at first sight, this idea turns out to be a powerful
technique for programming. It is so useful, in fact, that Java provides special syntax for a class
of this kind: it is called an interface. In practice, we would not write a class like Nameable
but would instead write the interface:
public interface Nameable
{
String getName();
}
By default, an interface method is public and abstract. Consequently, the modifiers are
usually omitted, as in the interface Nameable above.
The body of an interface can contain only:
Declarations of abstract methods
Declarations of public and final variables (that is, constants)
We have seen that a class may extend another class. Interfaces are not extended, but implemented . For example, we might say that “class Person implements interface Nameable”.
In the code, we would write
class Person implements Nameable
{
public String getName() { ....}
....
}
Note carefully that:
The class Person must provide a definition for method getName
The definition of getName must have the modifier public
The first condition is essentially the meaning of implements: by writing “implements Nameable”
in the declaration of class Person we are stating that this class provides the method getName.
The second condition shows that, although public is implicit in an interface, the default
accessibility remains package in the class that implements it.
3 FROM CLASSES TO INTERFACES
3.7
34
Collections and Iterators
The Java class library includes many interfaces for many purposes. Two of the simpler examples are the related interfaces Collection and Iterator, which are both parts of the Java
Collections Framework .
A class that implements Collection provides a way of storing a collection of objects, as its
name implies. The classes that implement Collection include ArrayList and Vector. Figure 25 shows part of the declaration of Collection. There are other methods that are not
included here: you can see the complete declaration at
http://java.sun.com/docs/books/tutorial/collections/interfaces/collection.html
public interface Collection
{
int size();
boolean isEmpty();
boolean contains(Object element);
boolean add(Object element);
boolean remove(Object element);
Iterator iterator();
}
Figure 25: The interface Collection
A class that implements Collection does not have to provide the method get(i) for accessing
the i’th object of the collection. Instead, Collection requires a more general mechanism
supported by an iterator . If c is a collection, we can write c.iterator(), thereby obtaining
an iterator.3
public interface Iterator
{
boolean hasNext();
Object next();
}
Figure 26: The interface Iterator
An iterator implements the functions shown in Figure 26. Imagine a collection as a sequence
of objects. When we construct a new iterator iter, it contains a reference to the first object
in the collection. The call iter.next() returns this object and moves the reference to the
next object in sequence. If we call iter.next() repeatedly, we obtain each object in the
collection.
The boolean function hasNext returns true provided that there are objects remaining in the
sequence. When we have extracted the last object, it returns false. These methods ensure
3
This sentence is abbreviated. The full version, if we could be bothered to write it, is: If c is an instance
of a class that implements the interface Collection, we can write c.iterator(), thereby obtaining an object
that is an instance of a class that implements Iterator.
3 FROM CLASSES TO INTERFACES
35
that we can process each object x of any collection c with a loop of the following form (note
that the semicolon after i.hasNext() cannot be omitted):
for (Iterator i = c.iterator(); i.hasNext();)
{
x = i.next();
....
}
We can improve the Box hierarchy by using interfaces. First, we will go through the changes,
and then discuss the advantages of the new version.
Example 5: Boxes with iterators. The changes to class Box in Figure 19 are as follows
(see Figures 27, 28, and 29):
Change the declaration
protected ArrayList components;
to
protected Collection components;
We do not have to change the assignment
components = new ArrayList();
in the constructor. The type of the left side is Collection and the type of the right
side is ArrayList. Since ArrayList implements Collection, it counts as a subclass of
Collection. Consequently, the type of the right side is a subclass of the type of the left
side, and the assignment is allowed.
There is a loop in method show:
for (int i = 0; i < components.size(); i++)
{
Box b = (Box)components.get(i);
b.show(level + 1);
}
This loop does not compile because the interface Collection does not provide the
method get and the expression components.get(i) is not allowed. The solution is
to use an iterator and the new code looks like this:
for (Iterator i = components.iterator(); i.hasNext();)
{
Box b = (Box)i.next();
b.show(level + 1);
}
The statement
Iterator i = components.iterator();
constructs a new iterator, i, for the collection components. The expression
i.hasNext();
is called during each iteration of the loop to see whether there are any more objects in
the collection. The expression
3 FROM CLASSES TO INTERFACES
36
(Box)i.next();
fetches the next object from the collection and casts it to a Box. As a side-effect, it
changes the reference (stored in the iterator i) so that, when next is called again, the
next object in the sequence will be returned.
We know that the calls to hasNext and next will compile correctly, because the object
returned by components.iterator() must support the interface Iterator in Figure 26
on page 34.
Class Box now compiles correctly. The loops in classes Row and Stack must be changed
in the same way. In class Row:
public String toString()
{
String buffer = "";
for (Iterator i = components.iterator(); i.hasNext();)
{
buffer += i.next();
}
return buffer;
}
In class Stack:
public String toString()
{
String buffer = "";
for (Iterator i = components.iterator(); i.hasNext();)
{
buffer += i.next() + "\n";
}
return buffer;
}
After these changes have been made, the program runs in exactly the same way that it did
before. 2
What do we gain by changing the classes of the Box hierarchy in this way? We have increased
the generality of the code, making it more tolerant of changes. For example, we could change
the last assignment of the constructor in class Box from
components = new ArrayList();
to
components = new LinkedHashSet();
and run the program without any other changes. (You don’t have to know what a
LinkedHashSet is—the point here is simply that it’s quite different from an ArrayList and,
for some applications, might be much more efficient.) We could not have done this with the
older version, because LinkedHashSet does not provide the method get, and so the loops
would not have compiled.
3 FROM CLASSES TO INTERFACES
import java.util.*;
public abstract class Box
{
protected int width;
protected int height;
protected Collection components;
public Box()
{
width = 0;
height = 0;
components = new LinkedHashSet();
}
public void showSize()
{
System.out.println(width + " by " + height);
}
public void add(Box b)
{
System.out.println("Cannot add a box to this object.");
}
public abstract String toString();
public void show(int level)
{
for (int i = 0; i < level; i++)
System.out.print("
");
System.out.print(getClass() + " ");
showSize();
for (Iterator i = components.iterator(); i.hasNext();)
{
Box b = (Box)i.next();
b.show(level + 1);
}
}
}
Figure 27: Class Box with iterators
37
3 FROM CLASSES TO INTERFACES
38
import java.util.*;
public class Row extends Box
{
public void add(Box b)
{
width += b.width;
if (height < b.height) height = b.height;
components.add(b);
}
public String toString()
{
String buffer = "";
for (Iterator i = components.iterator(); i.hasNext();)
{
buffer += i.next();
}
return buffer;
}
}
Figure 28: Class Row with iterators
3.8
Rules for Interfaces
A class can extend at most one class but can implement any number of interfaces.
The general form of a class declaration is
class C0 extends C1 implements I1, I2, . . . , In { ....
}
in which C0 and C1 are the names of classes and I1 , I2, . . ., In are the names of interfaces.
All methods in an interface are public abstract, whether or not these modifiers are
explicitly given.
Methods in an interface cannot be static.
All variables in an interface are static final, whether or not these modifiers are explicitly given.
An interface can extend another interface, using the keyword extends.
What happens if a class implements two interfaces, and the interfaces require methods with
the same name? There are two cases:
If the methods have different parameter lists, the class must implement two methods, one
for each parameter list.
If the methods have the same parameter lists, the class must implements one method
with the common parameter list.
The remaining case, in which the methods required by the interfaces have the same parameter
list but different return types, is not allowed.
3 FROM CLASSES TO INTERFACES
39
import java.util.*;
public class Stack extends Box
{
public void add(Box b)
{
height += b.height;
if (width < b.width) width = b.width;
components.add(b);
}
public String toString()
{
String buffer = "";
for (Iterator i = components.iterator(); i.hasNext();)
{
buffer += i.next() + "\n";
}
return buffer;
}
}
Figure 29: Class Stack with iterators.
An interface is not a class, not even an abstract class. Consequently, there are no instances
of an interface, even when one interface extends another. This is why an interface cannot
have static methods or non-final variables, necause neither of these make sense without
the presence of a class.
3.9
Designing with Abstract Classes and Interfaces
Abstract classes and interfaces make sense only when there are a number of classes involved.
In simple programs, the need for them does not arise. Repeating what we have said earlier
(Section 2.1), the problem that we are solving should have:
some common behaviour that can be captured in a parent class
some specialized behaviour that can be captured in child classes
If these two conditions are satisfied, it is worth considering a hierarchy of classes as a solution.
The presence of switch statements in early versions of your program is a hint that subclasses
may enable you to improve the design, especially if many switch statements have the same
case labels. For example, if you have written
switch (shape)
{
case Circle: ....
case Square: ....
3 FROM CLASSES TO INTERFACES
40
case Triangle: ....
case Heptadecagon: ....
}
then you should seriously consider writing an abstract class Shape with subclasses Circle,
Square, Triangle, and Heptadecagon.
When you have decided on a set of classes, and structured them as a tree, you can start
thinking about what features they will need and where those features should be defined. In
general, features should be placed as high in the tree (that is, as close to the root) as possible.
Classes that are not intended to have instances should be abstract. Whenever you introduce
an abstract class, ask yourself whether it should really be an interface.
It is not easy at first to appreciate the value of interfaces. This is partly because they are not
very useful in small programs; their usefulness becomes obvious only in the design of complex
programs and libraries. For example, the Java libraries make extensive use of interfaces.
An interface has two aspects:
a class that implements an interface must provide definitions for all of the methods
declared in the interface
a class that uses an interface in a declaration can safely assume that the corresponding
object provides all of the methods in the interface
As an example, consider the interface Collection that we discussed in Section 3.7 (see Figure 25 on page 34). Anyone who decides to write a collection class must define all of the methods in the interface declaration. Conversely, when we declare a variable with type Collection,
we know that any object referred to by that variable provides those methods.
An important design goal in programming and software engineering is loose coupling . Parts
of a program are tightly coupled if changing one part forces us to change other parts; they
are loosely coupled if we can make changes to one part at a time. Interfaces loosen coupling
by making parts of the program independent from one another.
In the first version of Box, everything depends on the decision to make components and
ArrayList. If we change the type of components, we have to rewrite all the loops and
possibly other code. By changing the type to Collection, we remove this coupling. We can
write the loops to work with any class that implements Collection.
3.10
Review
Rule: Constructors are not inherited.
This rule is reasonable: a constructor is closely matched to a class and does not make much
sense in another class.
Rule: If a class declaration contains no constructors at all, the compiler will generate a
default constructor that has no parameters and invokes the default constructor of the
parent class.
Thus, if you write:
class C extends P { .... }
then the compiler will generate code equivalent to:
3 FROM CLASSES TO INTERFACES
41
class C extends P
{
C() { super(); }
....
}
Rule: If a constructor does not contain a call to the parent class constructor (using super),
the compiler will insert the statement “super();” at the front of it.
A consequence of this rule is that the following code will not compile:
class P
{
P(int newX) { x = newX; }
}
class C extends P
{
C(int newY) { y = newY; }
....
}
The reason is that the compiler generates this constructor for class C
C(int newY)
{
super();
y = newY;
}
and then complains because class P has no default constructor.
The best way to avoid these problems is to include an explicit call to super in any child
class. Alternatively, you could ensure that all of your classes have default constructors,
either written explicitly or generated by the compiler.
4 APPLETS AND DIALOG BOXES
4
42
Applets and Dialog Boxes
An applet is a Java program that is intended to run on a website. The name “applet” suggests
“small application”. Applets do in fact tend to be small because it takes a long time to send
a large program over a network.
Sun has introduced two libraries for applets and Graphical User Interfaces (GUIs). The first
was the Abstract Windowing Toolkit (AWT), a library that is somewhat low-level and
tedious to use. The Java 2 Platform introduced a new library called Swing , which radically
enhances the limited capabilities of AWT. The result is somewhat confusing, because some of
the AWT is still used, but other parts of it have been replaced by improved versions in Swing.
Sun’s solution to this is the word deprecated , indicating a component that you can still use,
but you are advised to use a newer and better component that replaces it. A small number
of classes and a much larger number of features within classes are deprecated.
The AWT classes are implemented using the native GUI objects for a particular system. For
example, an AWT button looks like a Windows button on a Windows system and like a Mac
button on a Mac system. AWT classes are sometimes called heavyweight.
The Swing classes are implemented entirely in Java and their appearance is independent of
the platform: a button looks the same on any system. Swing classes are sometimes called
lightweight.
We discuss AWT first, showing how to write simple applets. In Section 4.6, we will examine
the Swing classes.
4.1
Writing an Applet
Before writing an applet, it helps to understand how applets are written and executed.
Applets are prepared at the server location:
1. The provider writes a Java program app.java.
2. The Java compiler translates the program into bytecodes app.class.
3. The provider writes an HTML script app.html that contains a link to the applet.
4. The files app.class and app.html are stored on the server. (The names do not have
to be the same but it does not matter if they are.)
Applets are executed by a user at a client site:
1. The user enters the URL "..../app.html" into a browser.
2. The browser constructs a request and sends it over the network to the server.
3. The server sends the file app.html to the client.
4. The browser parses app.html, finds the applet link, constructs a request, and sends it
to the server.
5. The server sends the file app.java to the client.
6. The browser uses the local system’s JVM to run the applet app.java.
4 APPLETS AND DIALOG BOXES
43
<html>
<body>
<applet code="Hello.class" width="200" height="200"
alt="This applet requires Java.">
</applet>
</body>
</html>
Figure 30: HTML for a simple web page
Example 6: Hello, Applet. Figure 30 shows the HTML required for a very simple web
page. The link to the applet starts with the tag <applet> and ends with the tag </applet>.
The fields between the tags are:
code="Hello.class"
gives the name of the bytecode file
width="200"
gives the desired width of the applet window in pixels
height="200"
gives the desired height of the applet window in pixels
alt="This applet requires Java."
is a message that the browser will display if the JVM has not been installed (in practice,
this message is often a link to a plug-in)
Figure 31 shows the source code for the applet displayed on this web page. The applet is
written as a class that extends the library class Applet. It does not contain a method main but
it must contain a method called paint. This method is called by the browser when the applet
is started, when the applet window is resized, and on various other occasions. Each time it is
called, and an instance of the library class Graphics is passed to it. Class Graphics contains
many methods that are useful for applets; in this example, only one method, drawString, is
used.
Note the import statements: an applet must import java.applet.Applet and it will normally
use AWT facilities, obtained from java.awt.*. 2
The advantages of applets include:
Since bytecodes are fairly compact, .class files are relatively small, and can be transmitted across networks without long delays.
It is very hard for bad guys to tamper with .class files because the byte codes are
checked thoroughly by the JVM before being executed. Consequently, applets can be
trusted to a much greater extent than executables (.exe files), Word files with macros,
etc.
A limitation of applets they provide client-side execution only. An applet will run on your
browser but it can interact with the server only in very limited ways. If you want the server
to do something, you need server-side execution, which requires different techniques. Some
quite simple actions, such as displaying the number of times a site has been visited, require
4 APPLETS AND DIALOG BOXES
44
import java.applet.Applet;
import java.awt.*;
public class Hello extends Applet
{
public void paint(Graphics canvas)
{
canvas.drawString("Hello", 100, 100);
}
}
Figure 31: Java source for a simple applet
server-side actions. Obviously, services that enable two-way interaction between server and
client, as when you buy books from Amazon, require server-side actions.
Running Applets You do not have to be operating a server site to try out applets. Here
are two ways to test applets:
Most Java IDE’s can run applets. In eclipse, select Run, then Run as, and then Java
applet.
You can use your browser to run an applet. Use a text editor to create an HTML file
similar to Figure 30 and store it somewhere (for example, you could put it in the same
folder as your Java source code). Compile the applet, so that you have a .class file.
Then open the HTML file in your browser (Netscape, Mozilla, Opera, Internet Explorer,
etc.). The easiest way to do this is to double-click on the HTML file name, but you can
also use Open from the File menu of the browser.
4.2
Coordinates
When the browser starts the applet, the applet is given a region of the browser window. The
size of this region is determined by the HTML code: in Figure 30, it is 200 × 200 pixels. The
statements in the paint method draw objects; the objects will be visible only if they lie inside
the area allocated.
Figure 32 shows the coordinate system used for applets. The units are in pixels. The X axis
goes to the right, which is conventional. The Y axis goes downwards, which is the opposite of
the usual convention in mathematics. The origin is at the top left corner, and the coordinates
of visible objects are usually positive. An object with negative coordinates may be partly
visible if it is large enough. For example, if an object in a 20 × 20 bounding box is positioned
at (−10, −10), its bottom right quarter will be visible.
Each object is defined by the position of its reference point, its width, and its height.
Figure 32 shows a rectangular object with its reference point at x = 30 and y = 20, with
40 pixels, and height 20 pixels.
The bounding box of an object is a rectangular area just large enough to totally enclose it.
The reference point of an object is usually the top left corner of its bounding box, which is
4 APPLETS AND DIALOG BOXES
s
0
0
10
20
10
20
(30, 20)
30
30
s
45
40
50
60
width
70
80
X
-
6
height
?
40
50
Y ?
Figure 32: Java coordinates
not necessarily part of the object. Here, for example, is a circle, its bounding box, and its
r
reference point (shown as a small, solid circle):
4.3
Colours and Objects
The Graphics class contains a number of methods for drawing objects of various kinds. Here
are a few of them:
public abstract void setColor(Color c)
sets the current colour to the colour c. All objects are drawn with this colour until setColor is called again. Colours are defined in the class Color; for example,
Color.black.
The colours provided are: black, blue, cyan, darkGray, gray, green, lightGray,
magenta, orange, pink, red, white, and yellow. You can create interesting colours
with the constructor Color(float r, float g, float b), in which r, g, and b are
the intensities of red, green, and blue in the colour. Each value must be between 0
and 1. The program in Figure 33 uses this constructor to generate random colours.
public abstract void drawLine(int x1, int y1, int x2, int y2)
draws a straight line from (x1 , y1) to (x2 , y2).
public void drawRect(int x, int y, int w, int h)
draws a rectangle with reference point (x, y), width w, and height h.
public abstract void drawOval(int x, int y, int w, int h)
draws an oval (ellipse) with reference point (x, y), width w, and height h. If w = h, the
object is a circle. The reference point is outside the object.
public abstract void drawArc(int x, int y, int w, int h, int s, int a)
draws part of the oval that would be drawn by drawOval(x, y, w, h). The object is
an arc that starts at angle s and continues through an angle a. Angles are measured
counterclockwise from the positive X axis, as shown here:
4 APPLETS AND DIALOG BOXES
46
90â—¦
180â—¦
0â—¦
270â—¦
public abstract void drawPolygon(int[] xs, int[] ys, int n)
draws a closed polygon defined by arrays of X and Y coordinates. Each pair of coordinates defines a point: the first point is at (xs0 , ys0) and the last point is at (xsn−1 , ysn−1 ).
The polygon is closed by joining the last point to the first point.
public abstract void drawString(String s, int x, int y)
draws the text in the string s. The baseline of the leftmost character is at position
(x, y). Note that the reference point is the bottom left corner (instead of the top left
corner) and that the parameters are backwards (x and y are not the first parameters,
as they are for other objects).
Several of the draw methods have corresponding fill versions. The parameters are the
same, but the object is “filled” (shaded with the current colour) instead of being drawn as an
outline. These methods include: fillPolygon, fillRect, fillOval, and fillArc.
Example 7: Abstract Painting. Figure 33 shows an applet that draws images that looks
like Mondrian painting on an off-day. This applet uses resize to set its size, independently of
the HTML code.4 Most of the work is done by the recursive method split, which is passed a
Graphics object canvas, and a rectangular area defined by a reference point (x, y), a width,
w, and a height, h.
If the width or height exceeds a preset minimum MIN, the rectangular area is split into two
parts. If w > h, the area is split vertically. A random number 0 ≤ wnew < w is chosen
and the new areas have widths wnew and w − wnew . Otherwise, w ≤ h, and the area is split
horizontally. A random number 0 ≤ hnew < h is chosen and the new areas have heights hnew
and h − hnew .
If both dimensions are greater than MIN, split constructs a new colour with random r, g,
and b values and draws the given area with this colour. 2
4.4
Inside class Applet
So far, the only use we have made of class Applet is to override its method paint. In fact,
Applet has a number of other methods that we can override. Some of these methods are
called by the browser that is running the applet and others are called by the applet itself.
The parent class Applet provides reasonable default behaviour for each method (usually by
doing nothing), but sometimes we would like to provide more specialized behaviour.
The methods defined in Applet fall into two groups: methods that are called by the browser
and methods that are available for the applet writer. The following methods are called by the
browser.
4
This can cause problems with some browsers: the image may be displayed repeatedly as the browser and
applet compete to control its size.
4 APPLETS AND DIALOG BOXES
47
import java.applet.Applet;
import java.awt.*;
import java.util.Random;
public class Mondrian extends Applet
{
static final int WIDTH = 600;
static final int HEIGHT = 600;
static final int MIN = WIDTH / 10;
static Random gen = new Random();
public void paint(Graphics canvas)
{
resize(WIDTH, HEIGHT);
split(canvas, 0, 0, WIDTH, HEIGHT);
}
public static void split(Graphics canvas, int x, int y, int w, int h)
{
if (w >= MIN && h >= MIN)
{
if (w > h)
{
int wnew = (int)(w * gen.nextDouble());
split(canvas, x, y, wnew, h);
split(canvas, x + wnew, y, w - wnew, h);
}
else
{
int hnew = (int)(h * gen.nextDouble());
split(canvas, x, y, w, hnew);
split(canvas, x, y + hnew, w, h - hnew);
}
}
else
{
Color c = new Color(
gen.nextFloat(),
gen.nextFloat(),
gen.nextFloat() );
canvas.setColor(c);
canvas.fillRect(x, y, w, h);
}
}
}
Figure 33: Abstract painting
4 APPLETS AND DIALOG BOXES
48
start
ready
init
-
inactive
@
@
R
@
@
I
@
@
active
destroy - finished
stop
Figure 34: Applet states
public void init()
Called by the browser or applet viewer to inform this applet that it has been loaded into
the system. It is always called before the first time that the method start is called.
public void start()
Called by the browser or applet viewer to inform this applet that it should start its
execution. It is called after the method init and each time the applet is revisited in a
Web page.
public void stop()
Called by the browser or applet viewer to inform this applet that it should stop its
execution. It is called when the Web page that contains this applet has been replaced
by another page, and also just before the applet is to be destroyed.
public void destroy()
Called by the browser or applet viewer to inform this applet that it is being reclaimed
and that it should destroy any resources that it has allocated. The method stop will
always be called before destroy.
An applet overrides init and destroy if it has initialization and finalization to perform. For
example, an applet with threads would use the method init to create the threads5 and the
method destroy to kill them. An applet overrides the methods start and stop if it has
any operation that it wants to perform each time the Web page containing it is visited. For
example, an applet with animation might use the method start to resume animation, and
the method stop to suspend the animation. Figure 34 summarizes the life-cycle of an applet.
The class Mondrian in Figure 33 is written in such a way that a new random image is created
every time paint is called. This behaviour might not be appropriate: if we wanted to obtain
the same image every time paint is called, we would have to rewrite it as follows:
Override init to generate data for a random image and store it.
Override paint to draw the image from the stored data.
The revised program would be more complicated because we would have to create a data
structure to store the data.
The following methods are available to the applet writer.
public void resize(int width, int height)
Requests that this applet be resized.
5
We will discuss threads later in the course.
4 APPLETS AND DIALOG BOXES
49
public boolean isActive()
Determines if this applet is active (see Figure 34).
public String getParameter(String name)
Returns the value of the named parameter in the HTML tag that invokes the applet.
For example, if the applet is specified as
<applet code="Clock" width=50 height=50>
<param name=Color value="blue">
</applet>
then a call to getParameter("Color") returns the value "blue".
public void showStatus(String message)
Requests that the string message be displayed in the status window (assuming that
the browser has a status window—most do).
public URL getDocumentBase()
Gets the URL of the document in which this applet is embedded
public URL getCodeBase()
Gets the URL of the directory which contains this applet.
public AudioClip getAudioClip(URL url)
Returns the AudioClip object specified by the URL argument. This method always
returns immediately, whether or not the audio clip exists. The audio data is not loaded
until the applet starts to play the clip.
public void play(URL url)
Plays the audio clip at the specified absolute URL. Nothing happens if the audio data
cannot be found.
public Image getImage(URL url)
Returns an Image object that can then be painted on the screen. The URL that is
passed as an argument must specify an absolute URL. This method always returns
immediately, whether or not the image exists. The image data is not loaded until the
applet starts to draw the image.
4.5
Dialog Boxes
Working with AWT classes is tedious because they are low-level and require a lot of attention
to detail. Large parts of the functionality of AWT has been superseded by the Swing toolkit,
which sensibly leaves more of the work for the computer to do.
To use Swing classes, we must import from javax.swing. As usual, we can import just the
classes that we need, or the whole collection with javax.swing.*.
Swing classes start with a capital “J”. Some Swing classes are in fact extensions of corresponding AWT classes: for example, JApplet is an extension of Applet. Be sceptical about
“extension”, however, because Swing changes the behaviour: it is not true to say that a
JApplet can do everything an Applet can do, because JApplet redefines Applet’s behaviour
to be consistent with the Swing architecture.
4 APPLETS AND DIALOG BOXES
50
Swing classes are not necessarily related to applets: we can use them in ordinary Java programs
as well. In fact, one of the simplest and most useful Swing classes is JOptionPane, which we
examine next.
The class JOptionPane has a very large number of methods. However, for almost all purposes,
it is necessary to understand only three of them:
showMessageDialog sends information to the user.
showConfirmDialog asks a question for which the expected answers are “yes” or “no”.
showInputDialog asks the user to enter some data and return it as a String.
Example 8: Solving a quadratic equation. Figure 35 shows a program that solves
quadratic equations interactively. It uses each of the three kinds of dialog box provided by
JOptionPane.
The program is organized as a do-while loop. This ensures that the loop body is executed
at least once.
The statement
String input = JOptionPane.showInputDialog("Enter coefficients: ");
displays this box:
The user has entered the string "2 1 -36" and can now either click on OK or press the
return key. The variable input gets the string in exactly the form that the user entered
it.
If the user presses the cancel button, showInputDialog returns a null reference. The
program should check input == null but it does not.
The program uses the function split from class String to separate the user’s input into
three parts. It uses split in a rather simple-minded way: the program works correctly
only if the user puts exactly one blank between each number.6 The three numbers
(still in string form) are stored in the array coefficients.
The method Double.parseDouble is used to convert the three strings into numbers a, b,
and c. We assume that the user has entered valid numbers. If not, the program will fail
while trying to parse the numbers.
The program computes the discriminant of the equation, ∆ = b2 − 4ac, and considers
three cases: ∆ < 0, ∆ = 0, and ∆ > 0, In each case, it calls showMessageDialog to
display the roots of the equation, if it has any. For the values given, the call is
6
The argument passed to split is actually a regular expression, which allows more polite behaviour if you
know how to write regular expressions.
4 APPLETS AND DIALOG BOXES
51
JOptionPane.showMessageDialog(null,
"Real roots: " + r1 + " and " + r2);
which yields this box:
The program calls showConfirmDialog to find out whether the user wants to waste any
more time solving quadratic equations. This call displays:
The user clicks on one of the buttons and the method returns an integer with one of three
values: YES_OPTION, NO_OPTION, or CANCEL_OPTION. The program repeats the loop if the
reply is YES_OPTION and terminates otherwise.
2
The first parameter passed to showMessageDialog and showConfirmDialog is null in Figure 35. In general, this parameter is a reference to a window that is the “parent” of the dialog
box (which is the “child” window). (These uses of “parent” and “child” have nothing to do
with inheritance: they are windows terminology.) In simple programs, there is no parent
window, and we just pass null.
4.6
Event-Driven Programming
In all of the programs that we have encountered so far, it is the program that maintains
control. There may be some interaction between the user and the program, but only in the
sense that the program displays a prompt, waits for the user to supply an answer, and then
continues.
A program with a GUI cannot operate in this way. The GUI provides a number of controls,
in the form of buttons, menus, checkboxes, radio-buttons, and so on. In principle, the user
can make use of any control at any time. (In practice, there are some limitations, of course:
you cannot close a file that you have not opened.)
This implies that GUI programs must be written in a different way from other programs: they
must wait for the user to do something, and then take the appropriate action. The user’s actions are called events and the style of programming is called event-driven programming .
This style of programming is based on the Hollywood principle. When you call a film
studio to offer your services as an actor, the usual response is “Don’t call us, we’ll call you”.
From then on, all you can do is sit in your chair, waiting for the phone to ring. That is how
a GUI program works.
4 APPLETS AND DIALOG BOXES
52
import javax.swing.JOptionPane;
public class Interact
{
public static void main(String[] args)
{
int again;
do
{
String input = JOptionPane.showInputDialog("Enter coefficients: ");
String coefficients[] = input.split(" ");
double a = Double.parseDouble(coefficients[0]);
double b = Double.parseDouble(coefficients[1]);
double c = Double.parseDouble(coefficients[2]);
double discriminant = b * b - 4 * a * c;
if (discriminant < 0)
JOptionPane.showMessageDialog(null, "Imaginary roots");
else if (discriminant == 0)
JOptionPane.showMessageDialog(null, "Equal roots: " + b / (2 * a));
else
{
double s = Math.sqrt(discriminant);
double r1 = (- b + s) / (2 * a);
double r2 = (- b - s) / (2 * a);
JOptionPane.showMessageDialog(null,
"Real roots: " + r1 + " and " + r2);
}
again = JOptionPane.showConfirmDialog(null, "Again?");
} while (again == JOptionPane.YES_OPTION);
}
}
Figure 35: Solving a quadratic equation
Figure 36 shows the basic structure of an event-driven program. Each of the ovals represents
an object.
A Listener is an object that is waiting for a particular event. For example, if the GUI
has a button, the program will have a listener that responds when the user clicks the
mouse on that button.
When the event occurs, the listener constructs an Event object that contains information
about the event. For simple events, such as a button click, there is not much information
and the important fact is simply that the event exists.
The event has some effect on the Component that owns it. A Component is the object
corresponding to a particular part of the GUI display. For example, a menu is a component.
4 APPLETS AND DIALOG BOXES
Listener
-
53
Event
-
Component
Figure 36: Listeners, events, and components
Event-driven programming is harder than conventional programming because we cannot
predict the order of events and we must therefore assume that any event may occur at any
time. One technique for keeping sane is to keep track of the various states that the program
can be in and to process events only if they are compatible with the current state.
Event-driven programming is also easier than conventional programming because we do not
have to plan the entire sequence of activities in advance. All we have to do is write a chunk
of code corresponding to each event. In many cases, these chunks are quite small and simple.
Example 9: A Graphical User Interface.
Figure 38 shows an applet with a button. When this applet is executed, and the user has
clicked on the button “Press here” twice, the window managed by the applet looks like
Figure 37. Each time the user clicks on the button, the counter is incremented. This is a
very simple example of a GUI. Figure 38 shows the program that creates this behaviour. It
contains many new features, and for now we will focus only on a few of the important ones.
The class SimpleButton extends the Swing class JApplet and overrides the method init
to set up the GUI.
The class has three variables one (counter) is just an integer, and the other two are
components that will appear in the applet’s window. There is a JButton, which causes
an event when the user clicks on it, and a JLabel, which is simply a string of text in the
window.
The method init initializes the button by creating a new instance of JButton. The
parameter passed to the constructor is the text that will appear on the button. There
Figure 37: The GUI displayed by the program in Figure 38
4 APPLETS AND DIALOG BOXES
54
must be a listener associated with the button, so that events can be detected. The listener
is provided by the statement button.addActionListener(new ButtonListener());.
The message is initialized by constructing a new instance of JLabel. The parameter
passed to the constructor is the text that will appear on the label.
The components for the window now exist, but we still have to specify how they will
appear in the window. A JApplet has an instance of Container that “contains” all of the
components in the window. To obtain access to the container, we call getContentPane.
The window must have a layout. There are many ways of specifying a layout, and we
will examine some in detail later. For now, we will obtain a simple layout by invoking
pane.setLayout(new FlowLayout());, which has the effect of placing the components
above one another.
The Container method add puts the components into the container, defining their position in the window.
The nested class ButtonListener implements the interface ActionListener and is required to implement just one method: actionPerformed.
actionPerformed is called when the user has performed the action that this listener is
waiting for: in this case, the user has clicked on the button. It is passed an instance
of ActionEvent which contains information about the event. In this case, there is no
useful information other than the fact that the event has occurred; consequently, we do
not need to use the object event.
actionPerformed performs any actions that are required in response to this event. It
can access variables and invoke methods of the enclosing class SimpleButton. In this
example, it performs three actions:
1. increment the variable SimpleButton.counter
2. changes the message in the variable SimpleButton.message
3. requests that the image of the applet be updated by invoking the inherited method
SimpleButton.repaint
2
4 APPLETS AND DIALOG BOXES
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
public class SimpleButton extends JApplet
{
private int counter;
private JButton button;
private JLabel message;
public void init()
{
counter = 0;
button = new JButton("Press here");
button.addActionListener(new ButtonListener());
message = new JLabel("Nothing has happened yet.");
Container pane = getContentPane();
pane.setLayout(new FlowLayout());
pane.add(button);
pane.add(message);
}
private class ButtonListener implements ActionListener
{
public void actionPerformed(ActionEvent event)
{
counter++;
message.setText("You have pressed " + counter + " times.");
repaint();
}
}
}
Figure 38: Implementing a button
55
5 CODING CONVENTIONS
5
56
Coding Conventions
Code must be written in such a way that the compiler can compile it and people can read
it. Compare
String calc (double m[]){
double tm=0.0; for (int t=0;t<MAX_TASKS;t++) tm+=m[t];
int i=int(floor(12.0*(tm-MIN)/(MAX-MIN))); return let[i];}
and
String calculateGrade (double marks[])
{
double totalMark = 0.0;
for (int task = 0; task < MAX_TASKS; task++)
totalMark += marks[task];
int gradeIndex =
int(floor(12.0 * (totalMark - minMarks) / (maxMarks - minMarks)));
return(letterGrades[gradeIndex]);
}
These two function definitions are identical as far as the compiler is concerned. For a human
reader, however, the difference is like night and day. Although a programmer can understand
what calc() is doing in a technical sense, the function has very little “meaning” to a person.
The second version of the function differs in only two ways from the first: it makes use
of “white space” (tabs, line breaks, and blanks) to format the code, and it uses descriptive
identifiers. With these changes, however, we can quite easily guess what the program is doing,
and might even find mistakes in it without any further documentation.
In practice, we would include comments even in a function as simple as this. But note how
the choice of identifiers reduces the need for comments.
The three basic rules of coding are:
Use a clear and consistent layout.
Choose descriptive and mnemonic names for constants, types, variables, and functions.
Use comments when the meaning of the code by itself is not completely obvious and
unambiguous.
These are generalities; in the following sections we look at specific issues.
Layout The most important rule for layout is that code must be indented according to its
nesting level. The body of a function must be indented with respect to its header; the body
of a for, while, or switch statement must be indented with respect to its first line; and
similarly for if statements and other nested structures.
You can choose the amount of indentation but you should be consistent. A default tab
character (eight spaces) is too much: three or four spaces is sufficient. Most editors and
programming environments allow you to set the width of a tab character appropriately.
Bad indentation makes a program harder to read and can also be a source of obscure bugs.
A programmer reading
5 CODING CONVENTIONS
57
while (p != null)
p.processChar();
p = p.next();
assumes that p++ is part of the loop. In fact, it isn’t, and this loop would cause the program
to “hang” unaccountably.
Although indentation is essential, there is some freedom in placement of opening and closing
braces. Many experienced programmers position the braces for maximum visibility, as in this
example:
Entry addEntry (Entry root, String name)
// Add a name to the binary search tree of file descriptors.
{
if (root == NULL)
{
root = new Entry(name);
if (root == NULL)
giveUp("No space for new entry", "");
return root;
}
else
{
if (name < root.getName())
return addEntry(root.left, name);
else if (name > root.getName())
return addEntry(root.right, name);
else
// No action needed for duplicate entries.
return root;
}
}
Other programmers prefer to reduce the number of lines by moving the opening braces (“{”)
to the previous line, like this:
Entry addEntry (Entry root, String name) {
// Add a name to the binary search tree of file descriptors.
if (root == NULL) {
root = new Entry(name);
if (root == NULL)
giveUp("No space for new entry", "");
return root;
} else {
if (name < root.getName())
return addEntry(root.left, name);
else if (name > root.getName())
return addEntry(root.right, name);
else
// No action needed for duplicate entries.
5 CODING CONVENTIONS
58
return root;
}
}
The amount of paper that you save by writing in this way is minimal and, on the downside,
it is much harder to find corresponding pairs of braces.
In addition to indentation, you should use blank lines to separate parts of the code. Here are
some places where a blank line is often a good idea but is not essential:
between method declarations in a class declaration;
between variable declarations in a class declaration;
between major sections of a complicated function.
Here are some places where some white space is essential. Put one or more blank lines between:
public, protected, and private sections of a class declaration
class declarations (if you have more than one class declaration in a file, which is not
usually a good idea)
function and method declarations
Naming Conventions Various kinds of name occur within a program:
constants
types
local variables
instance variables
functions
methods
It is easier to understand a program if you can guess the kind of a name without having to
look for its declaration which may be far away or even in a different file. There are various
conventions for names. You can use:
one of the conventions described here
your own convention
your employer’s convention
You may not have an option: some employers require their programmers to follow the company
style even if it is not a good style.
As a general rule, the length of a name should depend on its scope. Names that are used
pervasively in a program, such as global constants, must have long descriptive names. A
name that has a small scope, such as the index variable of a one-line for statement, can be
short: one letter is often sufficient.
Another rule that is used by almost all Java programmers is that constant names are written
with upper case letters and may include underscores.
5 CODING CONVENTIONS
5.1
59
Comments
Comments are an essential part of a program but you should not over use them. The following
rule will help you to avoid comments:
Comments should not provide information that can be easily inferred from the code.
There are at least two ways of applying this rule.
Use it to eliminate pointless comments:
counter++;
// Increment counter.
// Loop through all values of index.
for (index = 0; index < MAXINDEX; index++)
{ .... }
Use it to improve existing code. When there is a comment in your code that is not pointless, ask yourself “How can I change the code so that this comment becomes pointless?”
You can often improve variable declarations by applying this rule. Change
int np;
int flag;
double xcm;
double ycm;
int state;
//
//
//
//
//
Number of pixels counted.
1 if there is more input,
X-coordinate of centre of
Y-coordinate of centre of
0 = closed, 1 = ajar, 2 =
otherwise 0.
mass.
mass.
open.
to
int pixelCount;
bool moreInput;
Point massCentre;
int doorStatus;
final int CLOSED = 0;
final int AJAR = 1;
final int OPEN = 2;
There should usually be a comment of some kind at the following places:
At the beginning of each file (header or implementation), there should be a comment
giving the project that the file belongs to and the purpose of this file.
Each class declaration should have a comment explaining what the class is for. It is often
better to describe an instance rather than the class itself:
/**
* An instance of this class is a chess piece that has a position,
* has a colour, and can move in a particular way.
*/
class ChessPiece
{
....
Each method or function should have comments explaining what it does and how it works.
In many cases, it is best to put a comment explaining what the function does in the header
file or class declaration and a different comment explaining how the function works with
5 CODING CONVENTIONS
60
the implementation. This is because the header file will often be read by a client but the
implementation file should be read only by the owner. For example:
class MathLibrary
{
public:
/**
* Return square root of x.
*/
double sqrt (double x);
....
};
MathLibrary::sqrt (double x)
{
// Use Newton-Raphson method to compute square root of x.
....
}
Each constant and variable should have a comment explaining its role unless its name
makes its purpose obvious.
Block Comments You can use “block comments” at significant places in the code: for
example, at the beginning of a file. Here is an example of a block comment:
/***************************************/
/*
*/
/*
The Orbiting Menagerie
*/
/*
Author: Peter Grogono
*/
/*
Date:
24 May 1984
*/
/*
(c) Peter Grogono
*/
/*
*/
/***************************************/
Problems with block comments include:
they take time to write;
they take time to maintain;
they become ugly if they are not maintained properly (alignment is lost, etc.);
they take up a lot of space.
Nevertheless, if you like block comments and have the patience to write them, they effectively
highlight important divisions in the code.
Inline Comments “Inline comments” are comments that are interleaved with code in the
body of a method or function. Use inline comments whenever the logic of the function is not
obvious. Inline comments must respect the alignment of the code. You can align them to the
current left margin or to one indent with respect to the current left margin. Comments must
always confirm or strengthen the logical structure of the code; they must never weaken it.
Figure 39 shows an example of a function with inline comments.
5 CODING CONVENTIONS
61
void collide (Ball a, Ball b, double time)
{
// Process a collision between two balls.
// Local time increment suitable for ball impacts.
double DT = PI / (STEPS * OM_BALL);
// Move balls to their positions at time of impact.
a.pos += a.vel * a.impactTime;
b.pos += b.vel * a.impactTime;
// Loop while balls are in contact.
int steps = 0;
while (true)
{
// Compute separation between balls and force separating them.
Vector sep = a.pos - b.pos;
double force = (DIA - sep.norm()) * BALL_FORCE;
Vector separationForce;
if (force > 0.0)
{
Vector normalForce = sep.normalize() * force;
// Find relative velocity at impact point and deduce tangential force.
Vector aVel = a.vel - a.spinVel * (sep * 0.5);
Vector bVel = b.vel + b.spinVel * (sep * 0.5);
Vector rVel = aVel - bVel;
Vector tangentForce = rVel.normalize() * (BALL_BALL_FRICTION * force);
separationForce = normalForce + tangentForce;
}
// Find forces due to table.
Vector aTableForce = a.ballTableForce();
Vector bTableForce = b.ballTableForce();
if (
separationForce.iszero() &&
aTableForce.iszero() &&
bTableForce.iszero() && steps > 0)
// No forces: collision has ended.
break;
// Effect of forces on ball a.
a.acc = (separationForce + aTableForce) / BALL_MASS;
a.vel += a.acc * DT;
....
}
Figure 39: Inline comments
5 CODING CONVENTIONS
5.2
62
Javadoc
Javadoc is a documentation tool for Java. If you follow some simple conventions when writing
comments in your programs, you can run the source code through Javadoc and obtain a set
of web pages documenting your program. The Java API reference pages on the Java website
are generated in this way.
The basic rules are as follows.
Use this format for comments that you want to be processed by Javadoc (note the two *s
in the first line):
/**
* one or more lines of comments
*/
The format can be abbreviated to a single line for short documents:
/** a one line comment */
Write a comment in this format immediately before each class, method, and variable
declaration.
Use tags for particular kinds of comments.
Figure 40 shows the class OceanLiner with Javadoc comments. There are no inline comments
because the code in the methods is very straightforward. Complicated methods should include
inline comments for maintenance programmers; these comments are not processed by Javadoc.
Javadoc comments can contain HTML tags and Javadoc tags. For example, to obtain italics
or bold text, use HTML tags: <i>italics</i> or <b>bold</b>. Javadoc tags start with
the symbol @. Figure 40 uses the tags @param and @return. Figure 41 shows some of the
common tags. A # followed by the name of a feature generates a link to that feature; links
are generated (rather unnecessarily) in the comments for methods revenue and expense in
Figure 40.
After finding a tag, Javadoc associates text with the tag until it finds another tag. It is
therefore best to put general comments first, and then tagged comments. For example, when
documenting a method, write a description of the method, then a @param comment for each
parameter, and finally a @return tag for the returned value.
5.3
Nested Classes
The development of programs with a GUI requires many small classes. Since putting all of
these classes into the global name space would get confusing, Java provides a way of hiding
classes within classes. Class ButtonListener in Figure 38 is an examples of a nested class:
it is nested inside class SimpleButton.
A nested class is a class that is declared inside another class.
A nested class is always private.
Methods in a nested class can access all of the features of the enclosing class, even if they
are private.
Example 10: Feeding the Chicks. Figure 42 shows a program intended to be used for
birds feeding their chicks. The bird uses the method addFood to add food to the nest and the
method feedChick to feed the chick(s) in the nest.
5 CODING CONVENTIONS
/** An ocean liner is a ship that carries passengers. */
public class OceanLiner extends Ship
{
/**
* The number of passengers on the ship.
*/
private int numPassengers;
/**
* Constructor for an ocean liner.
* @param w the weight of the ocean liner.
* @param n the number of passengers carried on the ocean liner.
*/
public OceanLiner(double w, int n)
{
super(w);
numPassengers = n;
}
/**
* Calculate the revenue for a particular voyage.
* @see #expense
* @param distance the length of the voyage.
* @return the revenue.
*/
public double revenue(double distance)
{
return 0.25 * distance * numPassengers;
}
/**
* Calculate the expense for a particular voyage.
* @see #revenue
* @param distance the length of the voyage.
* @return the expense.
*/
public double expense(double distance)
{
return 0.01 * distance * weight;
}
}
Figure 40: Commenting a class with Javadoc
63
5 CODING CONVENTIONS
Tag
@see
@author
@version
@param
@return
@throws
@exception
64
Used in
Anywhere
Class
Class
Method
Method
Method
Method
Description
Refers to another component
Identifies programmer(s)
Identifies version(s)
Introduces a parameter description
Describes the value returned by the method
Documents exceptions
Documents exceptions
Figure 41: Common Javadoc tags
public class Test
{
public static void main(String[] args)
{
Nest twigs = new Nest();
twigs.addFood(2);
twigs.feedChick();
}
}
Figure 42: Feeding the chicks
Figure 43 shows the class Nest. Since a Nest may contain Chicks, the class Chick is declared
private as if it was a feature of Nest. The advantage of this arrangement is that the chicks are
hidden from predators: they can be accessed only through the methods of Nest. Specifically,
the owner of the nest can call feedChick which in turn called the Chick method eat.
Note that Chick.eat can access the variable food even though it is declared private. Methods in a nested class have the same privileges as methods in the enclosing class. 2
Warning When you write a class with nested inner classes, you write only one source
(.java) file. However, the compiler creates one output file for each class. For example,
compiling class Nest produces the files Nest.class and Nest$Chick.class. If you want to
put a program with nested classes on a web page for use as an applet, it is important to enure
that all of the .class files are installed, not just the outer classes.
5 CODING CONVENTIONS
public class Nest
{
private int food;
private Chick c;
public Nest()
{
food = 0;
c = new Chick();
}
public void addFood(int n)
{
food += n;
show();
}
public void feedChick()
{
c.eat();
show();
}
public void show()
{
System.out.println("Food = " + food);
}
private class Chick
{
public void eat()
{
if (food > 0)
food--;
else
System.out.println("Help! - no food.");
}
}
}
Figure 43: Nesting for the birds
65
6 GRAPHICAL USER INTERFACE DESIGN
6
66
Graphical User Interface Design
This section is in two parts. First, we describe development of Graphical User Interfaces
(GUIs) and, second, we discuss layout manages for GUIs.
6.1
A Graphical User Interface
We introduced a simple Swing applet in Figure 38 on page 55 and it is now time to review this example in detail. Note first that one of the features of class SimpleButton is the
nested class ButtonListener. A class is needed, because we must implement the interface ActionListener, but this class should be accessible only inside the class that uses it:
SimpleButton. A private, nested class is the natural solution.
The class SimpleButton inherits from JApplet. Its variables of SimpleButton correspond to
the components of the the applet window. In this example, there are two components, and
JButton and a JLabel. There may also be other variables, such as counter, depending on
what the applet does.
SimpleButton and overrides method init from JApplet. No other methods are required for
the simple behaviour of this applet. Recall that init is called once when the applet starts
executing (Figure 34 on page 48). Method init divides naturally into two parts. The first
part contains statements that initialize the instance variables of the class:
counter = 0;
button = new JButton("Press here");
button.addActionListener(new ButtonListener());
message = new JLabel("Nothing has happened yet.");
The constructors for both JButton and JLabel require a string that appears when the component is displayed in the applet window. Both strings can be changed later by invoking the
method setText on the corresponding object. A control, such as a button, must be associated
with a listener. This is not done by the constructor, but by the method addListener.
The second part of method init ensures that the objects are displayed by the applet and
controls their arrangement in the window. All applets have a Container that stores their
components and determines where they are drawn. The method getContentPane returns the
Container for the applet and the method Container.add puts components into it.
Container pane = getContentPane();
pane.setLayout(new FlowLayout());
pane.add(button);
pane.add(message);
Container has a method setLayout that must be given a LayoutManager. One kind of
LayoutManager is a FlowLayout, which puts components into the window in much the same
way that a text processor puts words into a document: components are put in a row from left
to right; if the row fills up, a new row is started. There are methods that change the spacing
between rows and between components but, for many purposes, the default values are good
enough.
To complete the applet, we must provide a listener for the button. We do this by writing a
class that implements ActionListener and providing it with a method
6 GRAPHICAL USER INTERFACE DESIGN
67
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
public class SimpleButton extends JApplet
{
private int counter;
private JLabel display;
private JLabel which;
private JButton inc;
private JButton dec;
public void init()
{
counter = 0;
inc = new JButton("Increment");
inc.addActionListener(new ButtonListener());
dec = new JButton("Decrement");
dec.addActionListener(new ButtonListener());
display = new JLabel("");
which = new JLabel("");
Container c = getContentPane();
c.setLayout(new FlowLayout());
c.add(inc);
c.add(dec);
c.add(display);
c.add(which);
}
Figure 44: Adding a button—first part
public void actionPerformed(ActionEvent event)
This method can perform any actions that are required in response to the user clicking
on the button. It has access to the instance variables and methods of the enclosing class
SimpleButton and can change variables and call methods. If any of the actions change the
contents of the applet window, the action should call repaint to update the window display.
Most applets have more than one button. There are two ways of adding more buttons. One
way is to add a nested class implementing ButtonListener for each button. Another way is
to use a single class and to obtain information from the ActionEvent to decide which button
was pressed.
Figure 44 illustrates the second method. The applet has two buttons, inc and dec, that in-
6 GRAPHICAL USER INTERFACE DESIGN
68
private class ButtonListener implements ActionListener
{
public void actionPerformed(ActionEvent event)
{
String s = event.getActionCommand();
if (s.equals("Increment"))
{
counter++;
}
else
{
counter--;
}
display.setText("Counter = " + counter);
which.setText("You pressed " + s);
repaint();
}
}
}
Figure 45: Adding a button—second part
crement and decrement the counter, respectively. In Figure 45, the method actionPerformed
calls event.getActionCommand to obtain the text associated with the button pressed, which
should be either "Increment" or "Decrement", and increments or decrements the counter
accordingly.
6.2
Incremental Development
There are a number of advantages to building a complex program in small steps.
When something goes wrong, the error was probably introduced in a recent increment,
making it easy to diagnose.
Design errors become apparent during development, when they can still be corrected,
instead of at the end of implementation, which may be too late.
We develop Java code for the GUI shown in Figure 46, adding features one at a time. Figure 47
shows the top-level (“main”) class for a simple application with a GUI. The principal object
needed for a GUI is a JFrame. The first line of method main constructs a JFrame. The String
passed to the constructor becomes the title of the GUI.
The JFrame method setDefaultCloseOperation determines what will happen when the
user closes the window. If this method is not called, the effect is HIDE_ON_CLOSE, which
means that the window disappears but the application keeps running. The allowed arguments
are: HIDE_ON_CLOSE, DO_NOTHING_ON_CLOSE, DISPOSE_ON_CLOSE, and EXIT_ON_CLOSE. In
Figure 47, we have requested EXIT_ON_CLOSE, which will terminate the application when the
user closes the window.
6 GRAPHICAL USER INTERFACE DESIGN
69
The components of a JFrame are stored in a Container. We obtain the Container for the
frame by calling getContentPane.
The only component of the GUI we are building is a FirstPanel: this is a new class that we
will define soon. In main, we simply construct an instance of it and use the method add to
put it into the Container.
We could specify the size of the window, but it is easier to call pack, which “packs” the
components of the GUI and builds a window just large enough to contain them.
Finally, we call show to display the window on the screen. The GUI is now active and will
remain so until the user closes the window.
Figure 48 shows the first version of class FirstPanel. The constructor creates an instance of
JPanel and suggests its size. The method is called setPreferredSize because some operating
systems do not allow arbitrary window sizes. Java, however, will attempt to create a window
of the desired size.
The method getPanel is used by the method main to obtain a reference to the JPanel that
we have created. Developing the GUI consists of adding components to this panel. We will
not worry about the layout of the components yet.
Adding a message The first thing we will add to the panel is a message. A message is
simply text that we can change later: it is not a control that the user can click on. Figure 49
shows the new code. There is a new variable, message, and it is initialized as a new JLabel.
We can set properties of the message and, in this case, we specify that it should be in a
sans-serif font, set in bold with a size of 24 points. It is possible to specify exact fonts, such as
arial, but a font may not be available on all platforms. By specifying a “font family”, such as
sans-serif, we are asking Java to choose a font that has the desired characteristics (no serifs).
We use the method add to add the message to the panel.
Changing the text Next, we will add a control that allows the user to change the text of
the message. We make the following additions to the code:
Declare a new variable, JTextField change. A text field provides a small window in
which the user can enter text.
Construct an instance of the JTextField and assign it to change. Passing 25 to the
constructor sets the width (number of columns) of the text window.
Figure 46: A simple graphical user interface
6 GRAPHICAL USER INTERFACE DESIGN
import java.awt.*;
import javax.swing.*;
public class FirstGUI
{
public static void main(String[] args)
{
JFrame frame = new JFrame("Building a GUI");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
Container contents = frame.getContentPane();
FirstPanel panel = new FirstPanel();
contents.add(panel.getPanel());
frame.pack();
frame.show();
}
}
Figure 47: An application with a GUI
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
public class FirstPanel
{
private JPanel panel;
public FirstPanel()
{
panel = new JPanel();
panel.setPreferredSize(new Dimension(300,100));
}
public JPanel getPanel()
{
return panel;
}
}
Figure 48: Building a panel: step 1
70
6 GRAPHICAL USER INTERFACE DESIGN
public class FirstPanel
{
private JPanel panel;
private JLabel message;
public FirstPanel()
{
message = new JLabel("This is the message.");
message.setFont(new Font("SansSerif", Font.BOLD, 24));
panel = new JPanel();
panel.add(message);
panel.setPreferredSize(new Dimension(300,100));
}
}
Figure 49: Building a panel: step 2
public class FirstPanel
{
private JPanel panel;
private JLabel message;
private JTextField change;
public FirstPanel()
{
message = new JLabel("This is the message.");
message.setFont(new Font("SansSerif", Font.BOLD, 24));
change = new JTextField(25);
change.addActionListener(new TextListener());
panel = new JPanel();
panel.add(message);
panel.add(change);
panel.setPreferredSize(new Dimension(300,100));
}
}
Figure 50: Building a panel: step 3
71
6 GRAPHICAL USER INTERFACE DESIGN
72
private class TextListener implements ActionListener
{
public void actionPerformed(ActionEvent event)
{
message.setText("Your message: " + change.getText());
}
}
Figure 51: Listening for text
Add a listener to respond to events in the text window.
Add change to the panel.
We also need an additional class to listen for events. Figure 51 shows this class. It is a
private, nested class, similar to the listener class we saw previously (Figure 38 on page 55),
but with two new features:
The call change.getText() returns a String containing the text entered by the user.
The call message.setText(s) updates the message with the given string.
Changing the colour We now add controls to the GUI to modify the text in the message.
The first control allows the user to change the colour of the text (black by default) to red,
green, or blue. Since we can display only one colour at a time, the appropriate choice of
control is a set of radio buttons. Radio buttons are named after the channel selectors on a
radio, which allow one channel to be selected at a time. They appear on the screen as a set
of small circles; at any time, one of the circles has a block blob in it, indicating the current
selection.
Figure 52 shows the code that must be added to class FirstPanel for the radio buttons (the
code we have seen before is omitted).
There are three instances of JRadioButton, each with a label. The true argument in
the constructor for the red button indicates that this button will be selected initially.
The buttons must be added to a ButtonGroup. (Note that ButtonGroup is a Swing class
even though it is not prefixed with “J”.)
The listener for radio buttons is an instance of ColourListener, which will be defined
shortly. Each button is linked to the same listener—we do not need a separate listener
for each button.
The buttons are added to the panel.
Figure 53 shows the class ColourListener. The new feature here is the ActionEvent method
getSource, which returns an Object indicating which component caused the event. In this
case, the course should be one of the three buttons, red, green, or blue, and we set the colour
of the message accordingly.
Changing the shape The last control that we will add to the GUI sets the shape of the
message text to italic or bold. Since text can be both bold and italic, the appropriate controls
6 GRAPHICAL USER INTERFACE DESIGN
public class FirstPanel
{
private JRadioButton red;
private JRadioButton green;
private JRadioButton blue;
public FirstPanel()
{
red = new JRadioButton("Red", true);
green = new JRadioButton("Green");
blue = new JRadioButton("Blue");
ButtonGroup colours = new ButtonGroup();
colours.add(red);
colours.add(green);
colours.add(blue);
ColourListener leftEar = new ColourListener();
red.addActionListener(leftEar);
green.addActionListener(leftEar);
blue.addActionListener(leftEar);
panel.add(red);
panel.add(green);
panel.add(blue);
}
}
Figure 52: Adding radio buttons
private class ColourListener implements ActionListener
{
public void actionPerformed(ActionEvent event)
{
Object source = event.getSource();
if (source == red)
message.setForeground(Color.red);
else if (source == green)
message.setForeground(Color.green);
else if (source == blue)
message.setForeground(Color.blue);
}
}
Figure 53: Listening for radio buttons
73
6 GRAPHICAL USER INTERFACE DESIGN
74
public class FirstPanel
{
private JCheckBox bold;
private JCheckBox italic;
public FirstPanel()
{
bold = new JCheckBox("Bold", true);
italic = new JCheckBox("Italic");
ShapeListener rightEar = new ShapeListener();
bold.addItemListener(rightEar);
italic.addItemListener(rightEar);
panel.add(bold);
panel.add(italic);
}
}
Figure 54: Changing the shape
are check boxes. A check box can be on or off independently of other check boxes. Figure 54
shows the code that must be added to class FirstPanel.
We add two variables, one for each checkbox: bold and italic.
We initialize these variables by calling the appropriate constructors. Each constructor
has the text associated with the checkbox. The second (optional) argument is true if the
box should be checked initially and false (or omitted) otherwise.
One listener is sufficient for both checkboxes. The listener is an instance of ShapeListener
(defined below) and we call addItemListener to link the checkboxes to their listeners.
We add the buttons to the panel.
The ShapeListener implements the interface ItemListener (not ActionListener, as in
the preceding cases). The CheckBox method isSelected determines whether a checkbox is
selected.
The shapes provided by class Font have an important property: they are additive. Consequently, we set the int variable style to Font.PLAIN and then, if bold is selected, add
Font.BOLD to it, and, if italic is selected, add Font.ITALIC.
Figure 56 shows the complete code for the constructor FirstPanel. The remainder of the
class consists of declarations for each of the variables and the private nested classes for
listeners.
6 GRAPHICAL USER INTERFACE DESIGN
private class ShapeListener implements ItemListener
{
public void itemStateChanged(ItemEvent event)
{
int style = Font.PLAIN;
if (bold.isSelected())
style += Font.BOLD;
if (italic.isSelected())
style += Font.ITALIC;
message.setFont(new Font("SansSerif", style, 24));
}
}
Figure 55: Listening for shapes
75
6 GRAPHICAL USER INTERFACE DESIGN
public FirstPanel()
{
message = new JLabel("This is the message.");
message.setFont(new Font("SansSerif", Font.BOLD, 24));
change = new JTextField(25);
change.addActionListener(new TextListener());
red = new JRadioButton("Red", true);
green = new JRadioButton("Green");
blue = new JRadioButton("Blue");
ButtonGroup colours = new ButtonGroup();
colours.add(red);
colours.add(green);
colours.add(blue);
ColourListener leftEar = new ColourListener();
red.addActionListener(leftEar);
green.addActionListener(leftEar);
blue.addActionListener(leftEar);
bold = new JCheckBox("Bold", true);
italic = new JCheckBox("Italic");
ShapeListener rightEar = new ShapeListener();
bold.addItemListener(rightEar);
italic.addItemListener(rightEar);
panel = new JPanel();
panel.add(message);
panel.add(change);
panel.add(red);
panel.add(green);
panel.add(blue);
panel.add(bold);
panel.add(italic);
panel.setPreferredSize(new Dimension(300,100));
}
Figure 56: The complete constructor for FirstPanel
76
6 GRAPHICAL USER INTERFACE DESIGN
6.3
77
Mouse Events
There are two interfaces that specify mouse events. A class that implements MouseListener
must implement the following methods:
void mouseClicked(MouseEvent e)
// The mouse button has been clicked on a component.
void mousePressed(MouseEvent e)
// The mouse button has been pressed on a component.
void mouseReleased(MouseEvent e)
// The mouse button has been released on a component.
void mouseEntered(MouseEvent e)
// The mouse enters a component.
void mouseExited(MouseEvent e)
// The mouse exits a component.
A class that implements MouseMotionListener must implement the following methods:
void mouseDragged(MouseEvent e)
// A mouse button is pressed on a component
// and then the mouse is dragged.
void mouseMoved(MouseEvent e)
// The mouse cursor has been moved onto a component
// but no buttons have been pushed.
In most cases, the listener will not need all of the methods. In fact, it is quite common to use
only one of them. However, even methods that are not used must be implemented, if only
with an empty body ({}).
Each of the mouse listener methods is passed an instance of MouseEvent that contains information about a mouse event. This class provides a number of methods, including these.
int getButton()
// Returns which, if any, of the mouse buttons has changed state.
Point getPoint()
// Returns the x,y position of the event
// relative to the source component.
int getX()
// Returns the horizontal x position of the event
// relative to the source component.
int getY()
// Returns the vertical y position of the event
// relative to the source component.
6 GRAPHICAL USER INTERFACE DESIGN
78
A click is defined as pressing and then releasing a mouse button. Consequently, the
program is not notified until the button has been released, completing the “click”.
The methods mousePressed and mouseReleased provide finer control by distinguishing
button-down and button-up events.
The mouse “enters a component” when the hot-point of the cursor moves inside the
component’s image on the screen. Similarly, the mouse “exits a component” when the
hot-point moves outside the image.
“Dragging” the mouse means that the user holds down a button and moves the mouse.
Dragging will generate a large number of calls to mouseDragged.
The value returned by MouseEvent.getButton is MouseEvent.BUTTONn, where n = 1, 2,
or 3.
The coordinates returned by getPoint, getX, and getY are relative to the window that
responded to the event, with (0, 0) at the top-left corner (see Section 4.2).
Example 11: Drawing Rectangles.
Programs for drawing and “painting” usually provide a way of drawing rectangles: the user
drags the mouse, and the program responds by drawing a rectangle with one corner at the
mouse cursor position when the mouse button was first pressed and the diagonally opposite
corner at the current position of the mouse cursor. Figure 57 shows a minimal subclass of
JApplet, Figure 58 shows the corresponding subclass of JPanel class that implement a simple
form of this behaviour, and Figure 59 shows the nested listener class that responds to mouse
events.
Class Mouse defines two instances of Point: p1 is the top left corner of the rectangle and
p2 is the bottom right corner. The method paintComponent draws the rectangle provided
that both points are non-null. The call to the super method clears the canvas; if it is
omitted, the rectangles pile up on top of each other. This class also has a listener, ml that
listens to mouse events. Note that the listener is added as both a MouseListener and as a
MouseMotionListener.
The listener class is called MyListener and it implements both interfaces, MouseListener
and MouseMotionListener. When the user presses down a mouse button, the listener sets
the first point, p1. As the user drags the mouse, the listener sets the second point p2, and
calls repaint to draw the rectangle. 2
import javax.swing.*;
public class Test extends JApplet
{
public void init()
{
getContentPane().add(new Mouse());
}
}
Figure 57: Drawing Rectangles: first part
6 GRAPHICAL USER INTERFACE DESIGN
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
public class Mouse extends JPanel
{
private Point p1;
private Point p2;
public Mouse()
{
MyListener ml = new MyListener();
addMouseListener(ml);
addMouseMotionListener(ml);
}
public void paintComponent(Graphics canvas)
{
super.paintComponent(canvas);
canvas.setColor(Color.blue);
if (p1 != null && p2 != null)
{
canvas.drawRect(p1.x, p1.y, p2.x - p1.x, p2.y - p1.y);
}
}
}
Figure 58: Drawing Rectangles: second part
79
6 GRAPHICAL USER INTERFACE DESIGN
private class MyListener implements MouseListener, MouseMotionListener
{
// Methods for MouseListener
public void mousePressed(MouseEvent ev)
{
p1 = ev.getPoint();
}
public void mouseReleased(MouseEvent ev)
{
System.out.println(
"(" + p1.x + ", " + p1.y +
") (" + p2.x + ", " + p2.y + ").");
}
public void mouseClicked(MouseEvent ev) {}
public void mouseEntered(MouseEvent ev) {}
public void mouseExited(MouseEvent ev) {}
// Methods for MouseMotionListener
public void mouseDragged(MouseEvent ev) {}
{
p2 = ev.getPoint();
repaint();
}
public void mouseMoved(MouseEvent ev) {}
}
}
Figure 59: Drawing Rectangles: third part
80
6 GRAPHICAL USER INTERFACE DESIGN
6.4
81
Layout Managers
As we have seen, The GUI part of an application has a complicated structure:
There is a JFrame that contains all information related to the GUI.
The JFrame has a Container that contains all of the components of the GUI.
A JPanel is one kind of component that may contain several components of its own.
In addition to all this, a Container may contain a LayoutManager. Which layout manage you
choose depends on how much work you are willing to do. If you leave all of the decisions to
the layout manager, you will not have to write much code but you will not have much control
over the appearance of your GUI. If you are willing to spend additional time on design, you
can obtain more precise control of your GUI by using a more complex layout manager.
Flow Layout The simplest kind of layout manager, and the one that we have been using
up until now, is the Flow Layout Manager . To request a flow layout, write
panel.setlayout(new FlowLayout());
in which panel is an instance of JPanel.
After establishing the layout, components are added to the panel with the method add. The
effect is similar to that of a word processor adding words to a line. The first component is
placed at the top left of the window; the next component is placed on its right; and so on.
When there is no room to the right, the process starts again at the left side, underneath the
first component.
The layout manager tries to make the window look neat. Spaces between components, and
between components and a window border, are uniform. When the user resizes the window,
the layout manager is called to compute new coordinates.
Border Layout The Border Layout Manager divides the panel into five areas: centre,
north, south, east and west. Figure 60 shows how these areas are arranged in the window and
Figure 61 shows the source code that produces this window.
The north and south areas have the same width extend right across the window.
The west, centre, and east areas have the same height and also extend right across the
window.
It is not necessary to provide a component for each area. If an area is omitted, it is given
zero area.
Although the example shows a single button, each of the five areas can be further subdivided.
Grid Layout The Grid Layout Manager places the components on a rectangular grid.
The constructor needs to know the number of rows and columns for the grid:
panel.setlayout(new GridLayout(rows, columns));
6 GRAPHICAL USER INTERFACE DESIGN
82
Figure 60: A border layout
import java.awt.*;
import javax.swing.*;
public class BorderPanel extends JPanel
{
public BorderPanel()
{
setLayout(new BorderLayout());
JButton
JButton
JButton
JButton
JButton
centre = new JButton("Centre");
north = new JButton("North");
south = new JButton("South");
east = new JButton("East");
west = new JButton("West");
add(centre, BorderLayout.CENTER);
add(north, BorderLayout.NORTH);
add(south, BorderLayout.SOUTH);
add(east, BorderLayout.EAST);
add(west, BorderLayout.WEST);
}
}
Figure 61: Generating a BorderLayout
Components are added to the grid in the usual way. The first component is put at the top
left corner, the next component to its right, and so on until the end of the first row; then the
second row is filled.
If there are fewer than rows × columns components, some cells of the grid are left empty. If
there are more than rows × columns components, cells are added to the grid to accommodate
all of the components.
Box Layout The layout managers that we have seen so far are “automatic” in the sense
that they compute the positions and dimensions of their components without assistance from
the programmer. The remaining layout managers provide more precise control, but at the
expense of extra coding.
The Box Layout Manager arranges components into horizontal and vertical rows. Since it
6 GRAPHICAL USER INTERFACE DESIGN
83
does not put any space in between components, we have to insert the space explicitly. There
are two kinds of space: a rigid area has a fixed size and glue has a variable size. When the
window is reshaped by the user, the components and rigid areas stay the same size, and the
glue stretches.
Space is specified by calling static methods of class Box:
public static Component createRigidArea(Dimension d)
// Creates an invisible component with the specified size.
public static Component createHorizontalStrut(int width)
// Creates an invisible, fixed-width component with the given width.
public static Component createVerticalStrut(int height)
// Creates an invisible, fixed-height component with the given height.
public static Component createGlue()
// Creates glue that can expand vertically or horizontally.
public static Component createHorizontalGlue()
// Creates horizontal glue.
public static Component createVerticalGlue()
// Creates vertical glue.
An instance of Dimension is created by calling the constructor with values for the width and
the height. For example, the call
Box.createRigidArea(new Dimension(60,20))
creates a component that is exactly and always 60 pixels wide and 40 pixels high.
The call
Box.createGlue()
creates a component that has, by default, zero dimensions. However, if this component is
added to a window which is not filled by its other components, it will expand to fill as much
space as necessary.
Let X denote a fixed size component and G denote glue. Then the sequence GXG will cause
X to be centered, because the glue will expand equally on each side of it. Conversely, the
sequence X1GX2 will put X1 on the extreme left and X2 on the extreme right of the window,
because the glue G will expand as much as possible.
The constructor for BoxLayout, unlike the other layout manager constructors, expects two
arguments: the current container, which is usually this, and a direction, specified as either
BoxLayout.X AXIS or BoxLayout.Y AXIS.
6 GRAPHICAL USER INTERFACE DESIGN
84
Card Layout A Card Layout Manager treats each component in the container as a
card. Only one card is visible at a time, and the container acts as a stack of cards. The first
component added to a CardLayout object is the visible component when the container is
first displayed. There are methods to move through the stack sequentially or to jump to a
specified card.
Grid Bag Layout A Grid Bag Layout Manager generalizes a grid layout manager by
removing the restriction that grid elements have the same size. A grid bag layout is associated
with a set of constraints that determine how the available space is managed. Refer to the
Swing API Reference pages for more details.
6.5
Miscellaneous Utilities
There are various methods that work with most components and that can be used to make
GUIs more attractive and friendly to use.
Tool Tips A tool tip is a small message that appears near a component when the user
moves the mouse near the component. Swing makes it very easy to add tool tips. If cpt is
a GUI component (button, checkbox, etc.) and tip is a string that helps the user to use the
component, all you have to do is write
cpt.setToolTipText(tip);
Hot Keys A hot key is a key that you can use instead of a mouse action. Many users,
especially those who can type, prefer hot keys to mouse actions because it is faster to type a
key (or pair of keys) and reduces the risk of repetitive strain injury (RSI). The Java term for
hot key is “mnemonic” and the method that sets a hot key is setMnemonic. If you call
JButton button = new JButton("Edit");
button.setMnemonic(’E’);
then the button would be displayed as Edit (with E underlined). The first occurrence of the
letter in the button name is underlined; if the mnemonic letter does not occur in the name,
nothing is underlined.
Enabling and Disabling At a given time, an application may not be able to respond to
all of the controls of a complex GUI. Controls that cannot be processed should be disabled ;
when they can be processed they are enabled . To enable a control c, call
c.setEnabled(true);
To disable it, call
c.setEnabled(false);
6 GRAPHICAL USER INTERFACE DESIGN
85
Calling setEnabled(true) does not prevent a lightweight7 component from receiving events.
You can use isEnabled when processing events for an object which might be disabled. The
listening method for a control ctl might look something like this:
public void actionPerformed(ActionEvent event)
{
if (ctl.isEnabled())
{
// Perform action
....
}
}
7
Swing components are lightweight.
7 WHEN BAD THINGS HAPPEN TO GOOD PROGRAMS
7
86
When Bad Things Happen to Good Programs
Even the most carefully written programs go wrong sometimes. It is essential to distinguish
two ways of going wrong:
Logical Errors are programming mistakes that must be corrected. For example, a programmer may √
have reasoned that a quantity d is non-negative and that it is therefore safe to
compute d. If the reasoning turns out to be false, so that in some runs d < 0, there
is no point in having the program recover gracefully, because it is wrong and must be
corrected.
Anticipated problems are situations that are unlikely but nevertheless may occur. For example, a programmer may expect a matrix to be non-singular (and therefore invertible)
but cannot guarantee that it is not singular. The program must be written to detect
the problem—a singular matrix—and recover gracefully.
We discuss three mechanisms for managing these problems. The first, printing a message,
applies to both kinds of problem. The second, assertions, helps to detect logical errors. The
third and most complex, exception handling, deals with anticipated problems.
7.1
Printing Messages
For very simple programs, the easiest approach to managing run-time problems is to print a
message:
void LetsTryIt()
{
....
if (numItems > MAX_NUM_ITEMS)
{
System.out.println("I can’t cope with all this stuff!");
return;
}
....
}
In practical situations, however, printing a message has few advantages and many disadvantages:
In the example above, the function returns without having completed its task. The calling
function must detect this somehow and take appropriate action. But some action has
already been taken—there is a message on the screen!
Much software is “embedded” and cannot print messages. Consider the anti-skid computer in your car, the controller of your dishwasher, or even your pacemaker.8
A message is always directed to the user , who may be completely clueless about what has
gone wrong or how to fix it. Most people do not know what to do when their favourite
word-processor pops up a window saying “Attempted illegal access to drive K: abort,
retry, or cancel?”.
8
If you don’t have a car, a dishwasher, or a pacemaker, consider someone else’s.
7 WHEN BAD THINGS HAPPEN TO GOOD PROGRAMS
87
An anticipated problem should be fixed by the program itself. A program that encounters
an unexpected singular matrix should choose an alternative computation strategy, not
print a message.
In summary, printing a message is not a helpful way of dealing with a problem in most cases.
7.2
Assertions
Note: the assertion mechanism described in this section was added to Java recently (Java 2 SDK 1.4) and may not be implemented in the version you are using.
If you are using a CLI, compile with
javac -source 1.4 myClass.java
and run with
java -ea myCLass
In Eclipse:9
select Window/Preferences/Compiler/Compliance and Classfiles
select 1.4 in the Compliance Level box
check Use default compliance settings
click Apply and answer Yes when asked whether you want a full rebuild
An assertion is a statement that the programmer believes to be true when the program is
run. Java assertions are introduced with the keyword assert.
Example 12: The cosine rule. The cosine rule for triangles says that
c2 = a2 + b2 − 2ac cos C
where a, b, and c are the lengths of the sides of the triangle and C is the angle opposite the
side c. This implies that the value of a2 + b2 − 2ac cos C must be positive for any real triangle,
but may be negative if the lengths are “impossible”—try to draw a triangle with sides of
length 3, 4, and 96. A programmer who has good reasons for believing that a, b, and C are
values for a possible triangle might write:
csq = a * a + b * b - 2 * a * c * cos(C);
assert csq > 0 : "csq is negative!";
c = sqrt(csq);
If the programmer’s belief is correct, the condition csq > 0 will be true in every run, and the
assert statement will have no effect. If there is a logical error in the program, and in some
runs csq ≤ 0, the program will stop and print a message of the form:
9
The steps given will enable you to compile programs with assert statements but the assertions do not
seem to be checked. Please tell me when you have discovered how to check assertions with Eclipse!
7 WHEN BAD THINGS HAPPEN TO GOOD PROGRAMS
88
java.lang.AssertionError: csq is negative!
at Football.doTriangle:45
at ....
2
An obvious objection to assertions is that we could achieve the same effect by printing a
message. In Example 12, for instance, the programmer could have written:
csq = a * a + b * b - 2 * a * c * cos(C);
if (csq <= 0)
{
System.out.println("csq is negative!");
....
}
c = sqrt(csq);
The assert statement has several advantages over the print version:
It is not clear what the program should do after printing the message, as indicated by
“....” above. Carrying on, as shown here, is obviously unacceptable. Executing return
just passes the responsibility for cleaning up to the caller.
We can turn off all assertion checking by omitting the compiler option -ea. This is simpler
than going through the code removing (or commenting out) all the print statements.
The assert statement is easier and clearer to read and understand than a conditional
print statement.
The failure of an assertion gives a clear signal that the code must be corrected.
It is a good idea to include assert statements in your program whenever you strongly believe
that a condition is true but cannot be absolutely certain. Well-placed assertions can save
large amounts of debugging time!
The assert statement consists of the keyword “assert”, a condition (boolean expression,
and and optional string. If the string is present, it is preceded by a colon:
assert condition ;
assert condition :
7.3
string ;
Exceptions
There are many situations in real-world programming in which an event is unlikely or unwanted but cannot be logically prevented. We can always handle such situations with an if
statement:
if (badThing)
{
.... // code to handle abnormal case
}
else
.... // code to handle normal case
}
7 WHEN BAD THINGS HAPPEN TO GOOD PROGRAMS
89
but this approach has disadvantages:
The symmetry of the if statement makes the two cases look similar, although in fact
they are not.
This solution assumes that both cases can be handled “in line”, without escaping to a
higher level.
The code for the “normal” action is mixed up with the code for the unusual case.
To appreciate the second point, consider the singular matrix problem again. Inverting a
matrix involves division; if the matrix is singular, we will eventually encounter a zero divisor.
So, somewhere deep inside the computation, there will be a statement like this:
if (div != 0)
{
quotient = thing / div;
....
}
else
{
// the matrix must be singular
....
}
What is the else part supposed to do? Perhaps it should return, but then the calling function
has a problem.
The ideal solution to this problem would be a mechanism that allowed us to transfer control
to a point in the program where the problem can be addressed at an appropriate level. For
example, it could get rid of the singular matrix and look for another one. The Java exception
mechanism does just this.
An exception is an unusual, sometimes undesirable, but anticipated problem. Here are some
examples of exceptions:
A list which should not be empty turns out to be empty.
We need the logarithm of a number, but the number happens to be negative.
The program must open and read a file, but the file cannot be opened.
We want to display an image on the screen, but the window provided is unexpectedly too
small.
An object should have been created, but a reference to it is found to be null.
An exception handling mechanism is a programming device that helps programmers to
deal with exceptions. An exception handling mechanism must provide a way to:
attempt an operation under the assumption that it might fail
indicate that something has gone wrong
deal with the exceptional case when the operation does not succeed
Java provides a try statement for the first requirement, a throw statement for the second,
and a catch statement for the third. The syntax is shown here:
7 WHEN BAD THINGS HAPPEN TO GOOD PROGRAMS
90
try
{
.... // try something that might go wrong
}
catch (Exception e)
{
.... // process the exception
}
In the normal course of events, the statements in the try block (also sometimes called a “try
clause”) are executed and the catch block has no effect. If a throw statement is executed
in the try block, however, the catch block will be executed. The throw statement “throws”
an object and the catch block tries to “catch” it. The object thrown is an instance of class
Exception or, more often, one of its subclasses.
Example 13: Checking triangles. Figure 62 shows a simple program that uses exceptions.
It is actually a little more complex than strictly necessary, to make the point that a throw
statement can be a long way away from the try block that exercises it.
The function main makes a number of calls to a method getCornerAngle. These calls are executed within a try block; the corresponding catch block prints a message if getCornerAngle
signals an exception.
The method getCornerAngle generates three random numbers, intended to be the lengths of
the sides of a triangle. It passes these lengths to the method checkTriangle.
The method checkTriangle uses the cosine rule to
of the angle opposite side
find the cosine
a2 + b 2 − c 2 c. If the triangle could be drawn, the value of cannot exceed 1. If it does
2bc
exceed 1, the method throws an exception, otherwise it calls acos to find the required angle.
When executed, the program prints:
Checking triangles....
Triangle: 4.0005340576171875 6.6851067543029785
Triangle: 3.6230335235595703 9.487439155578613
Triangle: 1.904538869857788 1.2348318099975586
Triangle: 1.0544824600219727 1.1842137575149536
Invalid triangle!
4.1792731285095215
7.830860614776611
2.521760940551758
0.11531829833984375
2
Any method that contains a throw statement must declare that it does so. Consequently,
checkTriangle is declared as
private static double checkTriangle(double a, double b, double c)
throws Exception
{
....
Furthermore, any method that calls a method that throws an exception must also declare the
exception. We must write
7 WHEN BAD THINGS HAPPEN TO GOOD PROGRAMS
91
import java.lang.Math;
import java.util.Random;
public class Cosine
{
public static void main(String[] args)
{
System.out.println("Checking triangles....");
try
{
for (int i = 0; i < 100; i++)
{
double angle = getCornerAngle();
}
}
catch (Exception badAngle)
{
System.out.println("Invalid triangle!");
}
}
static Random gen = new Random();
private static double getCornerAngle()
throws Exception
{
double a = 5 * gen.nextFloat();
double b = 10 * gen.nextFloat();
double c = 10 * gen.nextFloat();
System.out.println("Triangle: " + a + "
return checkTriangle(a, b, c);
}
" + b + "
" + c);
private static double checkTriangle(double a, double b, double c)
throws Exception
{
double cosC = (a*a + b*b - c*c) / (2 * b * c);
if (Math.abs(cosC) > 1)
{
throw new Exception();
}
else
{
return Math.acos(cosC);
}
}
}
Figure 62: Exceptional triangles
7 WHEN BAD THINGS HAPPEN TO GOOD PROGRAMS
92
private static double getCornerAngle()
throws Exception
{
....
because getCornerAngle calls checkTriangle which has a throw statement, even though
getCornerAngle does not contain a throw statement itself.
There are many ways of improving this simple example. Here is one: we can pass information
about the problem in the thrown object. If we change the throw in checkTriangle to
throw new Exception("Invalid sides: " + a + "
" + b + "
" + c);
the thrown object will contain a string. When we catch the object, we can obtain the string
from it:
catch (Exception badAngle)
{
System.out.println(badAngle.getMessage());
}
This version of the program prints:
Checking triangles....
Triangle: 0.6202700734138489 6.459812641143799 4.334151268005371
Triangle: 0.0597420334815979 5.9604878425598145 5.255805969238281
Triangle: 3.2054343223571777 2.624075412750244 1.9788628816604614
Invalid sides: 3.2054343223571777 2.624075412750244 1.9788628816604614
It is common practice to inherit from Exception rather than to use it directly. Figure 63
shows an exception class that is suitable for triangles. It uses super in the constructor to
assign an appropriate message for the exception. In addition, it stores the lengths of the sides.
The throw statement must be rewritten as
throw new TriangleException(a, b, c);
and the methods getCornerAngle and checkTriangle must declare throw TriangleException.
Figure 64 shows the new code for catching the exception: note that it can retrieve the information that caused the problem from the exception object.
Handling an exception and calling a method
throw/catch and method invocation:
There is a useful analogy between
The statement throw new TriangleException(a, b, c) is similar to a call with throw
as a method name and new TriangleException(a, b, c) as the argument. In fact, we
can make the analogy closer by writing the throw statement in the following (legal) way:
throw(new TriangleException(a, b, c));
The catch block is similar to a method definition. In
7 WHEN BAD THINGS HAPPEN TO GOOD PROGRAMS
93
public class TriangleException extends Exception
{
public double a;
public double b;
public double c;
public TriangleException(double a, double b, double c)
{
super("Invalid triangle");
this.a = a;
this.b = b;
this.c = c;
}
}
Figure 63: A Exception subclass for bad triangles
catch (TriangleException badAngle)
{
System.out.println("Invalid sides: " +
badAngle.a + " " +
badAngle.b + " " +
badAngle.c);
}
Figure 64: Catching the bad triangles
catch (TriangleException badAngle)
{
System.out.println("Invalid sides: " +
badAngle.a + " " +
badAngle.b + " " +
badAngle.c);
}
we can read catch as a method name, (TriangleException badAngle) as a parameter
list, and the code between { and } as the body of the method.
This analogy suggests, correctly, that there are rules relating the objects that throw throws
to the objects that catch catches. The rule is simple and should not be surprising:
The statement
catch (P e) { .... }
will catch the exception object thrown by
throw new C()
if C is a subclass of P.
7 WHEN BAD THINGS HAPPEN TO GOOD PROGRAMS
94
For example, the statement
catch (TriangleException badAngle) { .... }
will intercept an object of class TriangleException or any of its subclasses but will ignore
objects of class Exception, ArithmeticException, etc.
Exception Propagation If a catch block does not catch an exception, the exception
continues on to higher levels of the program. We say the exception is propagated .
Propagation allows us to construct a hierarchy of catch domains, with the most specific
exceptions handled quickly, at low levels, and more general exceptions handled later, after
propagating through inner layers.
In fact, all Java programs behave as if they are coded as follows:
try
{
main();
}
catch (Exception e)
{
.... // report an unhandled exception to the user
}
For example, if you rewrite the program in Figure 62 so that main is defined as
public static void main(String[] args)
{
System.out.println("Checking triangles....");
for (int i = 0; i < 100; i++)
{
double angle = getCornerAngle();
}
}
and run it, you will be told:
java.lang.Error: Unresolved compilation problem:
Unhandled exception type TriangleException
at Cosine.main(Cosine.java:27)
Exception in thread "main"
Multiple catch Blocks There may be more than one catch block after a try block. If
an exception is thrown, the system tries to match it against each of the catch block in turn.
When this code is executed:
7 WHEN BAD THINGS HAPPEN TO GOOD PROGRAMS
95
try
{
catchFishToEat();
}
catch (SharkException s)
{
.... handle sharks
}
catch (BootException b)
{
.... handle boots
}
the possible outcomes are:
The method catchFishToEat completes successfully, yielding a salmon (if you are lucky).
The method catchFishToEat throws an exception that is either a SharkException or a
subclass of SharkException. The first catch block takes care of this case and the second
catch block is not used.
The method catchFishToEat throws an exception that is either a BootException or a
subclass of BootException. The first catch block ignores this exception and it is passed
to the second catch block, which handles it.
The method catchFishToEat throws an exception that is in neither of the above categories. The exception is propagated to the next level of exception handling.
The finally Block A try/catch pair may be followed by a finally block that is executed
whether or not an exception is thrown. When this code is executed:
try
{
flyKite();
}
catch (BrokenStringException)
{
runAfterKite();
}
finally
{
takeHomeKite();
}
the string may break, throwing the exception BrokenStringException, or it may not. In
either case, the method takeHomeKite is called.
Checked and Unchecked Exceptions The Java compiler may check that an exception
that is thrown is also caught. More precisely, it performs this check for checked exceptions
but not for unchecked exceptions. Most exceptions are checked, but some are not. Any
method that may throw a checked exception must declare the exception with a throws clause.
7 WHEN BAD THINGS HAPPEN TO GOOD PROGRAMS
96
AclNotFoundException
ActivationException
AlreadyBoundException
ApplicationException
AWTException
BackingStoreException
BadLocationException
CertificateException
ClassNotFoundException
CloneNotSupportedException
DataFormatException
DestroyFailedException
ExpandVetoException
FontFormatException
GeneralSecurityException
GSSException
IllegalAccessException
InstantiationException
InterruptedException
IntrospectionException
InvalidMidiDataException
InvalidPreferencesFormatException
InvocationTargetException
IOException
LastOwnerException
LineUnavailableException
MidiUnavailableException
MimeTypeParseException
NamingException
NoninvertibleTransformException
NoSuchFieldException
NoSuchMethodException
NotBoundException
NotOwnerException
ParseException
ParserConfigurationException
PrinterException
PrintException
PrivilegedActionException
PropertyVetoException
RefreshFailedException
RemarshalException
RuntimeException
SAXException
ServerNotActiveException
SQLException
TooManyListenersException
TransformerException
UnsupportedAudioFileException
UnsupportedCallbackException
UnsupportedFlavorException
UnsupportedLookAndFeelException
URISyntaxException UserException XAException
Figure 65: Subclasses of Exception
The methods getCornerAngle and checkTriangle in Figure 62 declare the checked exception
Exception.
The most important class of unchecked exceptions are those thrown by the JVM when it
encounters a problem such as a zero divisor. Most of these exceptions are subclasses of
RuntimeException. The reason that they are not checked is that almost any method can
raise exceptions of this kind and programming would be very tedious if we had to write
throws RuntimeException in every method declaration. Apart from being tedious, it would
not contribute significantly to program correctness or ease of debugging. On the other hand,
declaring user-defined exceptions is a good idea because it provides a warning that a method
may throw an exception instead of returning normally.
Standard Exceptions Java uses exceptions extensively in the standard system and libraries. Figure 65 shows the direct subclasses of Exception. Figure 66 shows the direct
subclasses of one of these class, RuntimeException: these exceptions are unchecked for the
reasons just given.
Java makes extensive use exceptions in input/output processing. For example, methods that
read data from an input stream (keyboard or disk, for example) throw an IOException if
anything goes wrong. The problem may be minor (there is no more data in the file) or major
(the disk drive has failed).
Example 14: The exceptional applet. Figure 67 shows an applet that computes your
age. It displays a fixed message, "Enter the year in which you were born.", allows the
user to enter a string text, converts text to an integer year, subtracts year from 2004, and
7 WHEN BAD THINGS HAPPEN TO GOOD PROGRAMS
97
ArithmeticException
ArrayStoreException
BufferOverflowException
BufferUnderflowException
CannotRedoException
CannotUndoException
ClassCastException
CMMException
ConcurrentModificationException
DOMException
EmptyStackException
IllegalArgumentException
IllegalMonitorStateException
IllegalPathStateException
IllegalStateException
ImagingOpException
IndexOutOfBoundsException
MissingResourceException NegativeArraySizeException NoSuchElementException
NullPointerException
ProfileDataException
ProviderException
RasterFormatException
SecurityException
SystemException
UndeclaredThrowableException
UnmodifiableSetException
UnsupportedOperationException
Figure 66: Subclasses of RuntimeException
displays the answer:
The problem with this applet is that it does not respond graciously to errors in the input. If
the user enters "197t" instead of "1975", the applet dies. If it is running in a browser, the
user sees no response and may need a couple of minutes to realize what has gone wrong. If
we run the applet in a debugging environment, we get a message like this:
java.lang.NumberFormatException: For input string: "197t"
The problem is that Integer.parseInt(text) has noticed the error in the number and has
thrown an exception.
We can improve the applet by catching the exception and processing it appropriately. Figure 68 shows the new version of class YearListener (the JLabel errorDisplay has been
added to the method init but this change is not shown here). Using the new version, erro-
7 WHEN BAD THINGS HAPPEN TO GOOD PROGRAMS
98
neous input yields the sort of polite response that we expect from a user-friendly program:
2
7.4
Defining your own exceptions
A stack is a container with an important property: it is possible to retrieve only one of the
stored items, and that item is the one most recently inserted. Imagine a stack of heavy plates:
the only plate that you can remove from it is the top plate, which is also the plate most
recently added to the stack. This property is sometimes called last in, first out, or “LIFO”.
There are two things which can go wrong with a stack. One is that we may attempt to
retrieve an item when there aren’t any left—the stack is empty. This problem is called stack
underflow . The other is that we might try to add an item to a stack that is already full,
causing stack overflow . If the stack has unlimited capacity, the second problem cannot
occur.10 It does occur for a stack with limited capacity, which we call a bounded stack .
Suppose we are implementing a class for stacks. What should it do when underflow or overflow
occurs? Of the three possible solutions discussed at the beginning of this chapter (page 86),
two are inappropriate. Printing a message may not be useful or possible (the software may
be embedded in a system without a screen or printer). Asserting that underflow or overflow
cannot occur might be appropriate in some situations, but is not a general solution. Consequently, it is best to design the stack class to throw exceptions when things go wrong, and let
the user of the class decide what to do with them.
The class Stack shown in Figure 69 has three operations: a constructor, a method push that
inserts items in the stack and check for overflow, and a method pop that retrieves items from
the stack and checks for underflow. The “items” in this case are Strings; they could be any
Java type, including Object.
The items are stored in an array. The largest number of items that can be stored is maxEntries.
The number of entries is indicated by a stack pointer , sp.
It is important to establish a convention for how the stack pointer works and to ensure that
all methods follow it. The convention used in class Stack is that sp is the index of the next
free location of the array. An advantage of this convention is that sp is also the number of
entries: its initial value is 0, which is both the index of the first location of the array and the
initial number of entries. The LIFO property is maintained by always adding or removing
the “top” item (that is, the item with largest index) of the array. The convention should
10
However, some other problem will eventually occur if we go on putting things in the stack—probably
memory exhaustion.
7 WHEN BAD THINGS HAPPEN TO GOOD PROGRAMS
99
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
public class Age extends JApplet
{
private JLabel instructions;
private JTextField userData;
private JLabel ageDisplay;
public void init()
{
setSize(new Dimension(400, 100));
instructions = new JLabel("Enter the year in which you were born.");
userData = new JTextField(10);
userData.addActionListener(new YearListener());
ageDisplay = new JLabel("");
Container c = getContentPane();
c.setLayout(new FlowLayout());
c.add(instructions);
c.add(userData);
c.add(ageDisplay);
}
private class YearListener implements ActionListener
{
public void actionPerformed(ActionEvent event)
{
String text = userData.getText();
int year = Integer.parseInt(text);
int age = 2004 - year;
ageDisplay.setText("You are " + age + " years old");
}
}
}
Figure 67: Finding your age
7 WHEN BAD THINGS HAPPEN TO GOOD PROGRAMS
100
private class YearListener implements ActionListener
{
public void actionPerformed(ActionEvent event)
{
try
{
String text = userData.getText();
int year = Integer.parseInt(text);
int age = 2004 - year;
ageDisplay.setText("You are " + age + " years old");
errorDisplay.setText("");
}
catch (NumberFormatException e)
{
errorDisplay.setText("Invalid year, bozo - try again!");
ageDisplay.setText("");
}
finally
{
repaint();
}
}
}
}
Figure 68: Finding your age — with error detection
described in a comment at the beginning of the class declaration.11
The ordering of actions is important. The method push first stores the new item and then
increments sp. The method pop first decrements sp and then returns the corresponding item.
Both methods also check the stack status and throw exceptions if there is something wrong:
push does not allow an insertion when sp == maxEntries because the new item would be off
the end of the array; pop does not allow a retrieval if the stack is empty.
Figure 70 shows the new class for the exceptions that can be thrown by Stack. It consists of two constructors, both of which pass a string to the constructor of the superclass,
Exception. Stack.pop describes its problem as "underflow". Stack.push describes its
problem as "overflow" and also provides the object that caused the problem.
Figure 71 shows a program that tests Stack.
First, the program constructs a stack called greetings with three items.
Then it attempts to insert four items into greetings. The fourth push fails, throwing
an overflow exception which is caught in the catch block.
The statement in the catch block prints a message.
11
The comments are omitted from Figure 69 so that the class fits on one page. They are included in the
version on the web page.
7 WHEN BAD THINGS HAPPEN TO GOOD PROGRAMS
public class Stack
{
private String messages[];
private int maxEntries;
private int sp;
public Stack(int size)
{
messages = new String[size];
maxEntries = size;
sp = 0;
}
public void push(String text) throws StackException
{
if (sp == maxEntries)
{
throw new StackException("overflow", text);
}
else
{
messages[sp] = text;
sp++;
}
}
public String pop() throws StackException
{
if (sp == 0)
{
throw new StackException("underflow");
}
else
{
sp--;
return messages[sp];
}
}
}
Figure 69: Class Stack
101
7 WHEN BAD THINGS HAPPEN TO GOOD PROGRAMS
102
public class StackException extends Exception
{
public StackException(String description)
{
super("Stack error: " + description);
}
public StackException(String description, String item)
{
super("Stack error: " + description + " on item <" + item + ">");
}
}
Figure 70: Class StackException
After the catch block has executed, the program enters a finally block that tries to
print out all the items in the stack.
The for loop pops and prints the three items in the stack and then fails, throwing an
underflow exception.
The underflow exception is caught by the second catch block, which prints a message,
and the program terminates.
Figure 72 shows the output generated by the program.
7.5
The finally block
In Section 7.3 on page 95, we said that the statements in a finally block are “executed
whether or not an exception is thrown”. We can make a stronger claim than that: the
statements in a finally block are always executed, however the corresponding try block
terminates.
Example 15: Using finally. The program shown in Figures 73 and 74 illustrate the effect
of finally. Figure 73 sets up the tests, and the try/catch/finally blocks in Figure 74 do
the interesting work.
The code in the try block of method solve tries to solve the quadratic equation ax2 +bx+c = 0
given the values of the coefficients a, b, and c. It can terminate in various ways: if a = 0,
it returns −c/b; if b2 − 4ac < 0, it throws an exception; and, if b2 − 4ac ≥ 0, it returns the
smaller of the two real roots.
If a = 0 and b = 0, the program evaluates −c/0. Evaluating this expression does not throw
an exception, a point that we discuss further in Section 7.6.
Figure 75 shows the output produced by the program. The message "The ’finally’ block."
is printed in all cases, whether the try block returns a value or raises an exception. The
message "After the ’finally’ block." is printed only when the exception is raised, the
catch block has been executed, and execution continues after the finally block. 2
7 WHEN BAD THINGS HAPPEN TO GOOD PROGRAMS
103
public class TestStack
{
public static void main(String[] args)
{
Stack greetings = new Stack(3);
try
{
greetings.push("Hi");
greetings.push("Hello");
greetings.push("Good morning");
greetings.push("Aaargh!");
}
catch (StackException se)
{
System.out.println(se.getMessage());
}
finally
{
System.out.println("Items on stack:");
try
{
for (int i = 0; i < 10; i++)
{
System.out.println("<" + greetings.pop() + ">");
}
}
catch (StackException se)
{
System.out.println(se.getMessage());
}
}
}
}
Figure 71: Testing class Stack
Stack error: overflow on item <Aaargh!>
Items on stack:
<Good morning>
<Hello>
<Hi>
Stack error: underflow
Figure 72: Results of the test
7 WHEN BAD THINGS HAPPEN TO GOOD PROGRAMS
104
public class Finally
{
public static void main(String[] args)
{
test(0, 3, 9);
test(0, 0, 1);
test(1, 2, 3);
test(1, -4, 2);
}
public static void test(double a, double b, double c)
{
System.out.println("Coefficients: " + a + " " + b + " " + c);
System.out.println("Result: " + solve(a, b, c) + "\n");
}
}
Figure 73: Testing finally: first part
The finally block can only be used after a try block. However, the catch block is not
required. A try block followed directly by a catch block can be used to ensure that certain
statements are always executed. Consider the function shown in Figure 76. Since we want
to close the file before returning, we have to write input.close() twice, before each return
statement. We can use a finally block to avoid the repetition, as shown in Figure 77. The
revised code also handles the case in which the file was not opened successfully, by not closing
the file.
7.6
When is a number not a number?
Since Java makes extensive use of exceptions, we might expect numeric calculations that fail
would throw exceptions. For example, dividing by zero, passing a negative number to a square
root or log function, trying to obtain a result that is too large to be represented, and other
numeric problems, could raise exceptions. Surprisingly, this does not happen: Java does not
signal numeric errors by throwing exceptions.
There is (naturally!) one exception to this rule: the expression m/n, in which m and n both
have type int or Integer, throws ArithmeticException if n = 0.
Other problems with integers are not reported at all. If you write
int i = 1000000;
System.out.println(i * i);
the program will print
-727379968
because 1, 000, 000 × 1, 000, 000 = 1012 ≈ 236 cannot be represented with 32 bits, and Java
prints just the result that it obtains, ignoring overflow.
7 WHEN BAD THINGS HAPPEN TO GOOD PROGRAMS
105
public static double solve(double a, double b, double c)
{
try
{
if (a == 0)
return -c / b;
else
{
double disc = b * b - 4 * a * c;
if (disc < 0)
throw new Exception("Exception: no real roots.");
else
return (-b - Math.sqrt(disc)) / (2 * a);
}
}
catch (Exception e)
{
System.out.println(e.getMessage());
}
finally
{
System.out.println("The ’finally’ block.");
}
System.out.println("After the ’finally’ block.");
return 0;
}
Figure 74: Testing finally: second part
Arithmetic with floating-point numbers (types float and double) is different. As mentioned
above, no exceptions are thrown. Instead, there are several special values that a float or
double variable may have. These are:
POSITIVE_INFINITY
NEGATIVE_INFINITY
NaN
in which NaN stands for “Not a Number”.
The result of dividing by zero is either POSITIVE_INFINITY or NEGATIVE_INFINITY, depending on the signs of the operands. You can work with “infinite” values according to the
usual “rules” of mathematics. For example, if x is finite, then x + POSITIVE_INFINITY gives
POSITIVE_INFINITY. However, results which are indeterminate give NaN:
POSITIVE_INFINITY + NEGATIVE_INFINITY −→ NaN
The values POSITIVE_INFINITY and NEGATIVE_INFINITY are printed as Infinity and -Infinity
respectively.
Expressions that “overflow” (that is, yield results that are too large to be represented) give
infinite values. Expressions that “underflow” (that is, yield results that are too small to be
represented), however, give zero. If you write
7 WHEN BAD THINGS HAPPEN TO GOOD PROGRAMS
Coefficients: 0.0 3.0 9.0
The ’finally’ block.
Result: -3.0
Coefficients: 0.0 0.0 1.0
The ’finally’ block.
Result: -Infinity
Coefficients: 1.0 2.0 3.0
Exception: no real roots.
The ’finally’ block.
After the ’finally’ block.
Result: 0.0
Coefficients: 1.0 -4.0 2.0
The ’finally’ block.
Result: 0.5857864376269049
Figure 75: Testing finally: results
public boolean search(String file, String word)
{
Stream input = null;
input = new Stream(file);
while ( ! input.eof() )
{
if (input.next().equals(word))
{
input.close();
return true;
}
}
input.close();
return false;
}
Figure 76: Searching for a word: first version
106
7 WHEN BAD THINGS HAPPEN TO GOOD PROGRAMS
107
public boolean search(String file, String word)
{
Stream input = null;
try
{
input = new Stream(file);
while ( ! input.eof() )
{
if (input.next().equals(word))
return true;
}
return false;
}
finally
{
if (input != null)
input.close();
}
}
Figure 77: Searching for a word: second version
System.out.println(Math.exp(1000));
System.out.println(Math.-exp(1000));
System.out.println(Math.exp(-1000));
Java will print
Infinity
-Infinity
0.0
Values that are undefined, but not infinite, always give NaN. The following expressions all give
NaN:
0.0 / 0.0
sqrt(x) with x < 0
log(x)with x < 0
acos(x) with |x| > 1
For example, the statement
System.out.println(Math.log(-2));
prints
NaN
7 WHEN BAD THINGS HAPPEN TO GOOD PROGRAMS
108
How do you find out that you have got a NaN? One thing that you cannot do is use equality
comparison: Java defines X == NaN to be false for all values of X, including NaN. You have
to use the special predicate, isNaN, defined in various numeric classes, including Float and
Double. Similarly, you have to use isInfinite to test for positive or negative infinite values.
For example, if you are evaluating a complicated expression, compExp, with square roots,
inverse sines and cosines, logarithms, and so on, you might not be sure that everything would
work. You could write:
compExp = ....
if (Double.isNaN(compExp))
throw new ComplicatedExpressionEvaluationFailedException("Bombed!");
If you need a NaN for some reason, perhaps to indicate that a calculation has failed, you can
use the standard constants Float.NaN and Double.NaN. Returning NaN to indicate failure is
a Java convention; in practice, it might be more useful to throw an exception.
8 DATA TRANSFER
109
Operation
Using the keyboard
Displaying results
Printing text results
Printing graphical results
Saving data on disk
Restoring data from disk
Moving data within program
Transmitting data over network
Receiving data from network
Saving digital images
Source
Sink
keyboard
program
program
program
program
disk file
program
program
network
camera
program
window
printer
plotter
disk file
program
program
network
program
disk file
Figure 78: Varieties of data transfer
8
Data Transfer
Many programming tasks require the transfer of data from one place to another. For each
task, there is a source that produces data and a sink that consumes the data. Figure 78
gives some examples of data transfer operations. An object that transfers data from one place
to another is called a stream.
8.1
Streams
A stream is an object that manages the flow of data from one place to another. We can
classify streams in various ways.
Input/Output The first distinction is between input and output streams. An input
stream object behaves like a source: in other words, it appears to generate data. In reality, the data is coming from somewhere, such as the keyboard, a disk file, or a digital
camera, but the program is not concerned about the actual source: all it has to do is to
consume the data. Typically, an input stream is processed by read or get statements that
obtain data and store it somewhere in the program.
An output stream behaves like a sink: it other words, it appears to consume data. Typical
sinks include windows, printers, and disk files. Typically, an output stream is processed by
write or put statements that take data from the program and send it to the stream.
Text/Binary The second distinction is between text and binary streams. A text stream
(also called character stream) is a sequence of characters. Text streams are things that you
can read. System.out is a text stream that we have already used extensively to send text to
a window.
A binary stream (also called byte stream) is a sequence of bytes (binary data) that may
be text but does not have to be. For example, we can send a floating-point (double or float)
8 DATA TRANSFER
110
number in a binary stream but, if we want to send it in a text stream we must first convert
it into a character string, possibly losing precision.
(In 1963, Ed Lorenz restarted a weather forecasting program from printed data, using six
places of decimals rather than the exact binary numbers. To his surprise, the forecast
was completely different. This observation led to the Lorenz attractor and chaos theory.
http://astronomy.swin.edu.au/~pbourke/fractals/lorenz/.)
The same distinction occurs with disk files. A file containing text data must be transferred
by a text stream and a file containing binary data must be transferred by a binary stream.
Here is a quick test: open a file using a text editor (e.g., notepad, vi, emacs, the editor in
your Java IDE). If you can read what you see, you have opened a text file; if the window is
full of garbage, you have opened a binary file. Files with extensions such as .java, .txt, and
.html are text files. Files with extensions such as .class, .gif, .doc, and .pdf12 are binary
files.
Data/Filter A data stream is simply a channel for moving data from one place to another. A filter (also called processing stream) modifies the data in some way. The actions
performed by a filter are usually quite simple. One example of a filter action is buffering : the
user enters one key at a time, but the program reads input line by line. The conversion from
individual keystrokes to lines is performed by a filter (called, for example, BufferedReader).
8.2
Streams in Java
There are two factors that make stream processing in Java more complicated than it is in
some other languages. The first factor is that Java provides a large number of stream classes.
There are four families, with each family arranged as an inheritance hierarchy. The second
factor is that, since many things can go wrong with stream processing, many of the methods
in these classes throw exceptions. Consequently, you have to understand both inheritance and
exception handling before you can understand how to use streams.
An additional complication is that Java streams are usually set up in two steps. In the first
step, we create a general-purpose stream and connect it to a source or a sink. In the second
step, we pass the general stream to the constructor of a specialized stream or filter that serves
our particular purpose. In an earlier example (Figure 8 on page 15), we did this without
explaining the details. The relevant lines from that program are:
public class Test
{
public static void main(String[] args) throws IOException
{
InputStreamReader isr = new InputStreamReader(System.in);
BufferedReader br = new BufferedReader(isr);
....
String reply = br.readLine();
....
}
}
12
In fact, about 99% of the bytes in a typical .pdf file are characters. However .pdf files also contain control
symbols, images, and other binary data, and so must be processed as binary streams.
8 DATA TRANSFER
111
The method main must declare throws IOException because BufferedReader.readLine
declares this exception. Note also that we first convert System.in, which is an instance of
InputStream, first to an InputStreamReader and then to a BufferedReader. These two
lines could be written as one, although the two-line version is perhaps clearer:
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
We do this because an InputStream is a general class that has only a few methods and can
read either binary data or text. An InputStreamReader handles text only, which is what we
want for this example. (In general, “Reader” and “Writer” indicate specialization for text
processing.) The InputStreamReader provides methods for reading one character at a time.
Converting it to a BufferedReader allows us to read a line at a time.
There are a few particular streams that are easy to manage. The standard stream System.out
is easy to use, and we have been using it right from the start. The corresponding input
stream is System.in; since it throws exceptions, we have not been using it directly. The class
Keyboard in the text book is a “wrapper” that enables us to use System.in but hides the
ugly details.
8.3
Reading Input
Many programs have to read data from a text source, typically a disk file. There are various
ways of doing this: the program can swallow the whole file at a gulp, and process it in
memory; it can read and process one line at a time; or it can read and process each character
individually. The Java classes and methods are set up in such a way that it is often easiest to
read and process one line at a time. This approach has the advantage that people understand
it easily because we tend to see text in terms of lines.
We have seen above how to convert a general input stream into a buffered reader that returns
a line of text at a time. The other tool that we will need is a tokenizer that splits each line
into tokens.
A token is a semantic unit of text. We say “token” rather than “word” because a token
may be a word, a number, a date, or any piece of text with a particular format and simple
meaning. An instance of StringTokenizer is an object that converts a line into a sequence of
tokens. A StringTokenizer is a rather simple-minded object13 and cannot perform complex
parsing tasks: we tell it which characters separate tokens and it gives us back the others.
Example 16: Reading a Text File. Figure 79 shows a simple program that reads a text
file. Method main calls method read, which does the work. Since read calls readLine, which
throws IOException, read may also throw IOException. Thus main tries to read the file,
but reports problems if the attempt fails.
The argument passed to read is a String giving the name of the file to be read. A more
useful program would engage in a dialogue with the user to obtain the name of the file. The
effect of constructing a FileReader with a file name is to create an object capable of reading
the file. We convert this object to a BufferedReader so that we can read the data line by
line.
The remainder of read consists of a loop that reads a line from the file and prints it. At the
end of the file, readLine returns null; this value is used to terminate the loop.
13
There is also a StreamTokenizer, which is a bit smarter.
8 DATA TRANSFER
112
import java.io.*;
public class Reading
{
public static void main(String[] args)
{
try
{
read("fetzer.txt");
}
catch (IOException e)
{
System.out.println(e.getMessage());
}
}
public static void read(String fileName) throws IOException
{
FileReader fr = new FileReader(fileName);
BufferedReader br = new BufferedReader(fr);
while (true)
{
String line = br.readLine();
if (line == null)
break;
System.out.println(line);
}
}
}
Figure 79: Reading a file
Figure 80 gives a sample of elegant prose that we will use for testing this program and later
variants of it. This version of the program prints the file exactly as shown: it simply copies
the file to the screen. 2
We can demonstrate the use of exception handling by giving the name of a non-existent file.
If we change the call to read in main:
read("futzer.txt");
the program prints
futzer.txt (The system cannot find the file specified)
The constructor FileReader(fileName) failed, because the named file does not exist (or is
not in the current directory), and it threw an exception containing the text above.
8 DATA TRANSFER
113
That a system’s behavior can be affected by something
is necessary, of course, but, in addition, the something
must be functioning as a sign for that system. That such
a sign stands for that which it stands for that system
must make a difference to the (actual or potential)
behavior of that system, where this difference can
be specified in terms of the various ways that such
a system would behave, were such a sign to stand for
something other than that for which it stands for that
system (or, would have behaved, had such a sign stood
for something other than that for that system).
James Fetzer
Figure 80: The file "fetzer.txt"
import java.io.*;
public class Reading
{
public static void main(String[] args)
{
try
{
Concordance co = new Concordance("fetzer.txt");
co.read();
}
catch (IOException e)
{
System.out.println(e.getMessage());
}
}
}
Figure 81: Driver for reading a file
8.4
File Processing
We will now modify the program in various ways to demonstrate different ways of file processing. A limitation of the version of Figure 79 is that both methods are static; consequently,
there are no objects and we cannot have any instance variables. The first improvement is to
introduce a new class for reading the file and to create an instance of it. This revision yields
the two classes shown in Figure 81 and Figure 82. The program does exactly the same as
before.
8 DATA TRANSFER
114
import java.io.*;
public class Concordance
{
private BufferedReader br;
public Concordance(String fileName) throws IOException
{
FileReader fr = new FileReader(fileName);
br = new BufferedReader(fr);
}
public void read() throws IOException
{
while (true)
{
String line = br.readLine();
if (line == null)
break;
System.out.println(line);
}
}
}
Figure 82: Class for reading a file
The next improvement to the program is to split the text up into individual words for analysis.
The StringTokenizer class is well-suited to this. In order to use it, we must first import it:
import java.io.*;
import java.util.StringTokenizer;
Figure 83 shows the revised loop in method read. It is not possible to “reuse” a StringTokenizer
because the information is passed to it via the constructor. Consequently, we construct a new
StringTokenizer for each line. Here is a selection of the words printed by this version of the
program:
That
a
system’s
behavior
necessary,
system.
(actual
or
potential)
8 DATA TRANSFER
115
while (true)
{
String line = br.readLine();
if (line == null)
break;
StringTokenizer toks = new StringTokenizer(line);
while (toks.hasMoreTokens())
System.out.println(toks.nextToken());
}
Figure 83: Finding the words
The StringTokenizer is using space as the separator between words, and is retaining other
characters such as commas and periods, which are not really part of the word. We can change
this behaviour by passing a list of separators to its constructor. By changing one line:
StringTokenizer toks = new StringTokenizer(line, " \n\t\r,.’()");
we obtain a “clean” list of words (but note the "s"):
That
a
system
s
necessary
system
actual
As a first step in analyzing the input file, we will store the words in an array instead of merely
printing them. We declare an array that is large enough:
private final int MAXWORDS = 1000;
private String[] words = new String[MAXWORDS];
private int numWords = 0;
We modify the inner loop of method read:
StringTokenizer toks = new StringTokenizer(line, " \n\t\r,.’()");
while (toks.hasMoreTokens())
{
words[numWords] = toks.nextToken();
numWords++;
}
We add a new function to write the report:
public void report()
{
System.out.println(numWords + " words stored.");
}
8 DATA TRANSFER
116
Finally, we modify the try block in method main to read the file and then report on its
contents:
try
{
Concordance co = new Concordance("fetzer.txt");
co.read();
co.report();
}
Suppose our goal is to record the distinct words used in the text, rather than all of them.
We might also be interested in their frequency: how often does "the" occur? How often does
"system" occur? and so on. This requires two steps. First, the array must contain the words
and their frequencies; second, we should store the words in alphabetical order, because that
makes it easier to recognize repeated words.
In Figure 84, we have replaced the array of words by an array of entries. The file scanning
loop has new code: when we read a word, we call the new method insert to update the array
of entries.
Figure 85 shows the new class Entry. It is a private class nested within class Concordance
because no one else needs to know about it. Each instance contains a word and a counter.
The constructor stores a new word and sets the counter to 1, because the word has been seen
once. The method inc adds 1 to the counter.
Figure 86 shows the function insert. If the given word is in the array, insert increments its
counter. If the word is not in the array, insert appends it to the end of the array.
The report we get when we run this program suggests that it is working correctly: instead of
displaying "109 words stored.", the new version displays "52 entries stored."
As previously mentioned, we can improve both the usefulness and the efficiency of the program
by storing the words in alphabetical order. The coding is quite tricky and must be done
carefully; Figure 87 shows the new version of method insert.
We assume that the array entries stores the words in alphabetical order and we write
the code so as to maintain the ordering.
We start with a loop that compares the word given with each word of the array entries.
Since we cannot use the comparison operators (<, >=, etc.) for strings, we use compareTo.
As long as the words in the array precede (are smaller than) the new word, we move past
them (i++).
If we come to a matching word, the new word is already in the array; we increment its
counter and return.
If we come to a word that alphabetically follows the new word, we have to insert the new
word into the array. This is the tricky part!
The first step is to move all the entries after the current entry up one place to make room
for the new word. The loop must run backwards (largest index first) to avoid copying
words on top of one another. It must start by moving the last entry in the array and
finish by moving the current entry (indexed by i). The result is:
for (int j = numEntries; j > i; j--)
entries[j] = entries[j-1];
8 DATA TRANSFER
117
import java.io.*;
import java.util.StringTokenizer;
public class Concordance
{
private BufferedReader br;
private final int MAXENTRIES = 1000;
private Entry[] entries = new Entry[MAXENTRIES];
private int numEntries = 0;
public Concordance(String fileName) throws IOException
{
FileReader fr = new FileReader(fileName);
br = new BufferedReader(fr);
}
public void read() throws IOException
{
while (true)
{
String line = br.readLine();
if (line == null)
break;
StringTokenizer toks =
new StringTokenizer(line, " \n\t\r,.’()");
while (toks.hasMoreTokens())
insert(toks.nextToken());
}
}
public void report()
{
System.out.println(numEntries + " entries stored.");
}
private class Entry { .... }
private void insert(String word { .... }
}
Figure 84: Counting the words
8 DATA TRANSFER
118
private class Entry
{
private String word;
private int count;
private Entry(String w)
{
word = w;
count = 1;
}
private void inc()
{
count++;
}
}
Figure 85: Class Entry
private void insert(String word)
{
for (int i = 0; i < numEntries; i++)
{
if (entries[i].word.equals(word))
{
entries[i].inc();
return;
}
}
entries[numEntries] = new Entry(word);
numEntries++;
}
Figure 86: Method insert
Finally, we can store the new word and increment the entry counter, as before:
entries[i] = new Entry(word);
numEntries++;
The method report is changed to print the words and their counters, as shown in Figure 88.
When we run this version of the program, it prints the following list:
Fetzer 1
James 1
That 2
a 7
8 DATA TRANSFER
119
private void insert(String word)
{
int i = 0;
while (i < numEntries)
{
int ord = entries[i].word.compareTo(word);
if (ord < 0)
i++;
else if (ord == 0)
{
entries[i].inc();
return;
}
else
break;
}
for (int j = numEntries; j > i; j--)
entries[j] = entries[j-1];
entries[i] = new Entry(word);
numEntries++;
}
Figure 87: Inserting a word in alphabetical order
public void report()
{
for (int i = 0; i < numEntries; i++)
System.out.println(
entries[i].word.toLowerCase() + " " +
entries[i].count);
}
Figure 88: Reporting the entries
actual 1
addition 1
....
The problem we have encountered is that compareTo does not compare strings for their
alphabetical order: it simply compares the codes used by the computer to represent characters.
The ASCII and Unicode representations put all upper case letters before lower case letters,
with the result that our list starts with capitalized words. The easiest way to fix this problem
is to use compareToIgnoreCase instead of compareTo. With this change, we obtain a useful
list. For consistency, we use toLowerCase to ensure that all words are displayed without
capital letters. The result is shown in Figure 89, arranged in columns to save space.
8 DATA TRANSFER
120
a7
actual 1
addition 1
affected 1
as 1
be 3
behave 1
behaved 1
behavior 2
but 1
by 1
can 2
course 1
difference 2
fetzer 1
for 8
functioning 1
had 1
have 1
in 2
is 1
it 2
james 1
make 1
make 1
must 2
necessary 1
of 3
or 2
other 2
potential 1
s1
sign 4
something 4
specified 1
stand 1
stands 3
stood 1
such 4
system 7
terms 1
than 2
that 11
the 3
this 1
to 2
various 1
ways 1
were 1
where 1
which 2
would 2
Figure 89: Concordance for "fetzer.txt"
There remains a small defect in this program: it includes the word "s" which, in the original
text was the apostrophe s of a genitive. However, if we removed the character “’” from the
separators passed to StringTokenizer, we would have obtained "system’s" in the concordance. The only way to fix this problem is to check for special cases, such as apostrophized
characters, while reading the input. Another problem with this program is that it does not
handle hyphenated words: we have the choice of retaining them as written, or adding “-”
to the list of separators, which would have the effect of treating a hyphenated word as two
separate words.
A list of the words used in a given text, together with their frequencies, is called a concordance for the text; hence the name for this program.
Example 17: Choosing a File. As a final step, we will make the concordance program a
bit more “user friendly” by allowing the user to select a file to read. Fortunately, the Swing
library includes a class FileChooser which makes this very easy to do. Figure 90 shows the
main method modified to incorporate a FileChooser. The steps involved are:
Construct a new FileChooser. The (optional) argument passed to the constructor specifies the directory that will be shown when the FileChooser window is opened.
Call showOpenDialog to display the FileChooser window:
8 DATA TRANSFER
121
import java.io.*;
import javax.swing.*;
....
public static void main(String[] args)
{
JFileChooser fc = new JFileChooser("C:\\eclipse\\workspace\\Reading");
int status = fc.showOpenDialog(null);
if (status == JFileChooser.APPROVE_OPTION)
{
try
{
Concordance co = new Concordance(fc.getSelectedFile());
co.read();
co.report();
}
catch (IOException e)
{
System.out.println(e.getMessage());
}
}
}
Figure 90: Using a FileChooser
This method returns an integer status whose value depends on the user’s actions.
If the status is APPROVE_OPTION, the user has chosen a file.
The file is obtained by calling getSelectedFile.
The only other changed required is that the constructor Concordance now receives a file
rather than a file name. Its declaration must be changed to
public Concordance(File f) throws IOException
{
FileReader fr = new FileReader(f);
br = new BufferedReader(fr);
}
No other change is required, because the constructor FileReader accepts either a file or a file
name as its argument. 2
Note: the material in this section is based on Sun’s tutorial web pages.
8.5
Class File
Instances of the class File are objects that represent disk files. They do not have any capabilities of their own and must be passed to the constructors of I/O classes, described in
Section 8.6, in order to do file processing.
8 DATA TRANSFER
122
A pathname is a string of character used to identify a directory or a file. These are Windows
pathnames denoting directories as they would be written in a Java program. Note that the
backslashes have to be repeated, to indicate that we really mean “\” and we are not using
backslash as an escape character.
C:\\Program Files\\PCTeX\PCTeX5\\texmf
F:\\Courses\COMP249\\doc
These are pathnames denoting files:
C:\\Program Files\\PCTeX\PCTeX5\\dvips.exe
F:\\Courses\COMP249\\doc\\249notes9.tex
Here are some of the ways in which we could construct a Java File object corresponding to
the last of these:
File
File
File
File
File
file1
file2
dir
file3
file4
=
=
=
=
=
File("F:\\Courses\COMP249\\doc\\249notes9.tex");
File("F:\\Courses\COMP249\\doc", "249notes9.tex");
File("F:\\Courses\COMP249\\doc");
File(dir, "249notes9.tex");
File("249notes9.tex");
We obtain:
file1 by passing the complete pathname to the constructor
file2 by passing the directory path and the relative file path to the constructor
file3 by passing a File object representing the directory and the relative file path to
the constructor
file4 by passing the relative pathname to the constructor
All of these, except the last one, will succeed provided that the file exists. The last one will
succeed only if the Java program is running in the same directory as the file we want.
We cannot read or write with a File object, but there are a number of other things that we
can do with it. These include system operations, such as deleting the file, renaming it, or
testing whether it can be altered.
The most common use of a File object is to pass it to the constructor of an i/o class, creating
a stream.
8.6
I/O Classes
I/O classes are divided into two hierarchies: character streams and byte streams.
8.6.1
Character Streams
The classes Reader and Writer are abstract superclasses for character streams. Reader
provides the method declarations and partial implementation for streams that read 16-bit
Unicode characters (these streams are called readers), and Writer provides the method
declarations and partial implementation for streams that write 16-bit characters (called writers). Subclasses of Reader and Writer implement specialized streams and are divided into
8 DATA TRANSFER
123
two categories: data streams, shown in gray in Figures 91 and 92), and filters, shown in white.
(Recall that a data stream simply moves data from one place to another, but a filter performs
some processing on the data.)
Figure 91: Reader class hierarchy
Class Reader provides methods for reading characters and arrays of characters:
int read();
int read(char cbuf[]);
int read(char cbuf[], int offset, int length);
Figure 92: Writer class hierarchy
Class Writer provides methods for writing characters and arrays of characters.
int write(int c);
int write(char cbuf[]);
int write(char cbuf[], int offset, int length);
The subclasses of Reader and Writer provide additional features to support various kinds of
file processing.
The simplest class in the Reader hierarchy is FileReader and the simplest class in the Writer
hierarchy is FileWriter. The constructors FileReader and FileWriter attempt to open the
files they are given, throwing IOException if they do not succeed. Opening a file for reading
fails if the file does not exist. Opening a file for writing usually succeeds, failing only if there
8 DATA TRANSFER
124
import java.io.*;
public class Copy
{
public static void copy(File inputFile, File outputFile) throws IOException
{
FileReader in = new FileReader(inputFile);
FileWriter out = new FileWriter(outputFile);
int c;
while ((c = in.read()) != -1)
out.write(c);
in.close();
out.close();
}
}
Figure 93: Copying a file
is no more space on the disk. On most systems, opening an existing file for writing destroys
the data in that file.
Example 18: Copying a text file. Figure 93 shows a method that copies any text file
using these classes only. The arguments of the static method copy are Files, created as
described in Section 8.5 above. A file is opened , then used for data transfer, then closed .
Note carefully the termination condition of the while loop in Figure 93. Each call to read
yields a 16-bit character, which the method stores in a 32-bit integer. A valid character cannot
possibly be negative, because a negative number has ‘1’ in its most significant bit position.
Java can therefore use negative numbers to report unusual cases. In particular, when there is
no more data in the file, read returns the special value −1. 2
8.6.2
Printing
PrintWriter is a subclass of Writer (not shown in Figure 92). “Printing” is a somewhat
misleading term in programming. A PrintWriter can send data to a printer but, in most
cases, the data goes to a window. PrintWriter has methods print and println that are
overloaded with most common types (boolean, char, int, float, double, String, and so
on). There is even an overload for Object, so it is possible to print any object that has a
toString method. A PrintWriter makes it very easy to output data and one is provided by
default: it is called System.out.
8.6.3
Byte Streams
To read and write binary data in the form of 8-bit bytes, you should use byte streams which
are subclasses classes InputStream and OutputStream. These streams are typically used
to read and write binary data such as images and sounds. Two of the byte stream classes,
8 DATA TRANSFER
125
ObjectInputStream and ObjectOutputStream, are used for object serialization, which we discuss in Section 8.7. As with Reader and Writer, subclasses of InputStream and OutputStream
provide input and output that falls into two categories, as shown in Figures 94 and 95. Data
streams are gray and processing streams are white.
Figure 94: Input stream class hierarchy
The methods provided by class InputStream are analogous to those of Reader but work with
bytes instead of characters:
int read()
int read(byte cbuf[])
int read(byte cbuf[], int offset, int length)
Figure 95: Output stream class hierarchy
The methods provided by class OutputStream are analogous to those of Writer but work
with bytes instead of characters:
int write(int c)
int write(byte cbuf[])
int write(byte cbuf[], int offset, int length)
Example 19: Filters. Figures 97 and 96 illustrate the use of filters to transfer binary data.
The classes DataInputStream and DataOutputStream are concrete subclasses of the abstract
classes FilterInputStream and FilterOutputStream, respectively. Using these classes, we
can use methods such as writeInt and writeDouble to write variables of type int and
double, and corresponding methods readInt and readDouble to read these values.
8 DATA TRANSFER
126
import java.io.*;
public class DataIODemo
{
public static void main(String[] args) throws IOException
{
DataOutputStream out = new DataOutputStream(
new FileOutputStream("invoice1.txt"));
double[] prices = { 19.99, 9.99, 15.99, 3.99, 4.99 };
int[] units = { 12, 8, 13, 29, 50 };
String[] descs =
{
"Java T-shirt",
"Java Mug",
"Duke Juggling Dolls",
"Java Pin",
"Java Key Chain"
};
for (int i = 0; i < prices.length; i ++)
{
out.writeDouble(prices[i]);
out.writeChar(’\t’);
out.writeInt(units[i]);
out.writeChar(’\t’);
out.writeChars(descs[i]);
out.writeChar(’\n’);
}
out.close();
}
Figure 96: Writing binary data
The code in Figure 96 writes data to a DataOutputStream. Each line consists of a price
(double), a quantity (int), and a description (String). The items are separated by tabs,
written as ’\t’ by the method writeChar.
Important Note: The file is not a text file and the data is not a String! The
numbers are written in binary format, just as they are stored in memory. The
tabs are unnecessary, and it is not clear why they were included in this example.
The second part of the program, shown in Figure 97, reads the file created by the first part.
The actual reading is carried out in a try block, because the reading methods throw exceptions
if they encounter invalid data. (This program, however, ignores the exceptions.) Note the
use of readChar to read the tab characters, even though their values are not needed. Also
note that it is not possible to read a String in byte mode: the program uses a loop to read
characters until a line break (indicated by the character lineSep is encountered.
8 DATA TRANSFER
127
DataInputStream in = new DataInputStream(
new FileInputStream("invoice1.txt"));
double price;
int unit;
StringBuffer desc;
double total = 0.0;
try
{
while (true)
{
price = in.readDouble();
in.readChar();
unit = in.readInt();
in.readChar();
char chr;
desc = new StringBuffer(20);
char lineSep = System.getProperty("line.separator").charAt(0);
while ((chr = in.readChar()) != lineSep)
desc.append(chr);
System.out.println("You’ve ordered " +
unit + " units of " +
desc + " at $" + price);
total = total + unit * price;
}
}
catch (EOFException e)
{
}
System.out.println("For a TOTAL of: $" + total);
in.close();
}
}
Figure 97: Reading binary data
You’ve ordered 12 units of Java T-shirt at $19.99
You’ve ordered 8 units of Java Mug at $9.99
You’ve ordered 13 units of Duke Juggling Dolls at $15.99
You’ve ordered 29 units of Java Pin at $3.99
You’ve ordered 50 units of Java Key Chain at $4.99
For a TOTAL of: $892.8800000000001
Figure 98: Results from the binary data example
8 DATA TRANSFER
128
The reading part is not told many records the file contains. The while loop will iterate until
the last record has been read; on the next iteration, the statement price = in.readDouble()
will fail because there is no double to read, readDouble will throw an exception, and the
program will print the total.
Figure 98 shows the output generated by this program. 2
8.7
Serialization
So far, we have looked at various ways of reading and writing simple data types—numbers
and strings and stuff. It is useful to be able to read and write entire objects. It is clear that
we could do this with the tools that we already have. An object is composed of its instance
variables, and we could use a text or (preferably) a byte stream to read or write one variable
at a time. If an instance variable is itself an object, we could use the same principle to read
and write it. This technique would work, but it is unnecessary.
Java provides a simple way of reading and writing objects of any class, provided that they
have one property that will be defined in a moment. Transferring an object requires that it be
converted to a stream of bytes. This operation is called serializing the object. Consequently,
reading and writing objects is called serialization. The property that a class must have to
enable its instances to be serialized is that it must implement the interface Serializable.
That is all: it does not have to provide any methods for serialization.
Serializable objects are written to an ObjectOutputStream and read from an ObjectInputStream.
Writing objects to a stream is easy. This code writes an instance of Date (which contains the
current time, date, etc.) and writes it to a stream called "today".
FileOutputStream out = new FileOutputStream("today.dat");
ObjectOutputStream s = new ObjectOutputStream(out);
s.writeObject("Time now");
s.writeObject(new Date());
s.flush();
ObjectOutputStream implements the DataOutput interface (also implemented by DataOutoputStream
above) that defines many methods for writing primitive data types, including the ability to
write strings, as in this example. You can use these methods to write primitive data types to
an ObjectOutputStream.
The method writeObject method throws a NotSerializableException if it’s given an object
that is not serializable. An object is serializable only if its class implements the Serializable
interface.
Reading from an ObjectInputStream is not much harder: this code reads the file that we
just wrote:
FileInputStream in = new FileInputStream("today.dat");
ObjectInputStream s = new ObjectInputStream(in);
String s = (String)s.readObject();
Date date = (Date)s.readObject();
ObjectInputStream implements the DataInput interface (which is also implemented by class
DataInputStream above) that defines methods for reading primitive data types. Obviously,
8 DATA TRANSFER
129
the objects must be read from the stream in the same order in which they were written. When
reading objects, we have to use a cast, because the type returned by readObject is Object.
Objects may refer to other objects to any number of levels; all of the subobjects will be written
and read. The method writeObject serializes the specified object, traverses its references to
other objects recursively, and writes them all. The method readObject deserializes the next
object in the stream and traverses its references to other objects recursively to deserialize
all objects that are reachable from it. In this way, relationships between the objects are
maintained when they are saved to or restored from disk files.
8.7.1
The transient Attribute
By default, writeObject and readObject transfer all of the fields (instance variables) of a
class, public, protected, and private. Static fields are not transferred. Sometimes, we do
not want to transfer all fields, for several reasons:
the data might be sensitive, such as a password
the variable might not be part of the object—for example, when saving a car, we might
want to save the wheels but not the owner
the variable might be something that is stored for convenience and easily recomputed
If we do not want a variable to be transferred during serialization, we write the keyword
transient in front of it.
Example 20: Food Preservation. Figure 99 shows a serializable class for meals. Figure 100
shows a program designed to test serialization of meals. Its first action is to create a meal
and display it in a dialog box:
Then it asks the user whether this meal should be saved to disk:
8 DATA TRANSFER
130
import java.io.*;
public class Meal implements Serializable
{
private String appetizer;
private String mainCourse;
private String dessert;
private double cost;
private boolean onExpenseAccount;
private transient int beer;
public Meal(
String soup,
String main,
String pudding,
double dollars,
boolean expenses,
int cans)
{
appetizer = soup;
mainCourse = main;
dessert = pudding;
cost = dollars;
onExpenseAccount = expenses;
beer = cans;
}
public Meal()
{
}
public String toString()
{
NumberFormat money = NumberFormat.getCurrencyInstance();
String description =
"Appetizer: " + appetizer + "\n"
+ "Main course: " + mainCourse + "\n"
+ "Dessert: " + dessert + "\n"
+ "Drinks: " + beer + " beers." + "\n"
+ "Cost: " + money.format(cost) + "\n";
if (onExpenseAccount)
description += "On expenses - yay!";
else
description += "Do I really have to pay?";
return description;
}
}
Figure 99: A Serializable class
8 DATA TRANSFER
131
import java.io.*;
import javax.swing.JOptionPane;
public class Test
{
public static void main(String[] args)
{
Meal dinner =
new Meal("caviar", "coq au vin", "pralines", 146.50, true, 17);
JOptionPane.showMessageDialog(null, "Your meal:\n" + dinner);
int doWrite = JOptionPane.showConfirmDialog(null,
"Do you want to save your meal?");
if (doWrite == JOptionPane.YES_OPTION)
writeMeal(dinner);
int doRead = JOptionPane.showConfirmDialog(null,
"Do you want to restore your meal?");
if (doRead == JOptionPane.YES_OPTION)
{
dinner = readMeal();
JOptionPane.showMessageDialog(null, "Your meal, sir:\n" + dinner);
System.out.println();
}
}
Figure 100: Test program for food preservation
After the meal has been saved successfully, the program asks the user whether the meal should
be restored from the disk:
8 DATA TRANSFER
132
public static void writeMeal(Meal supper)
{
try
{
FileOutputStream out = new FileOutputStream("food");
ObjectOutputStream s = new ObjectOutputStream(out);
s.writeObject(supper);
s.flush();
out.close();
}
catch (FileNotFoundException fe)
{
System.out.println(fe.getMessage());
}
catch (IOException ioe)
{
System.out.println(ioe.getMessage());
}
}
Figure 101: Writing the meal
If the meal is restored successfully, the program displays the following dialog box:
Note that the components of the meal are the same, except for the beer, which seems to have
got lost. This is because the field beer is declared transient, and is not written to the file.
When the file is restored, beer has the default value for type int, which is zero.
The method readMeal is written to return an instance of Meal. It will not compile unless
all paths through it lead to a statement that returns a Meal. If any exceptions are thrown,
statements after the catch blocks will be executed. Since we have no Meal defined at this
point, we return a default Meal. To make this possible, we must have declared a default
constructor in class Meal. 2
The exception handlers in readmeal and writeMeal are required because the serialization
methods throw a number of exceptions. Some of these are listed below; for the complete list,
consult the Java API.
8 DATA TRANSFER
133
public static Meal readMeal()
{
try
{
FileInputStream in = new FileInputStream("food");
ObjectInputStream m = new ObjectInputStream(in);
Meal dinner = (Meal)m.readObject();
in.close();
return dinner;
}
catch (ClassNotFoundException cfe)
{
System.out.println(cfe.getMessage());
}
catch (FileNotFoundException fe)
{
System.out.println(fe.getMessage());
}
catch (IOException ioe)
{
System.out.println(ioe.getMessage());
}
return new Meal();
}
}
Figure 102: Reading the meal
ObjectInputStream.readObject throws
ClassNotFoundException: Class of a serialized object cannot be found.
InvalidClassException: Something is wrong with a class used by serialization.
StreamCorruptedException: Control information in the stream is inconsistent.
OptionalDataException: Primitive data was found in the stream instead of objects.
IOException: Any of the usual Input/Output related exceptions.
ObjectOutputStream.writeobject throws
InvalidClassException: Something is wrong with a class used by serialization.
NotSerializableException: Some object to be serialized does not implement the java.io.Serializable
interface.
IOException: Any exception thrown by the underlying OutputStream.
9 ADVANCED GUI PROGRAMMING
9
134
Advanced GUI Programming
The GUIs that we have looked at so far may seem complicated but are in fact quite simple
when compared to the GUIs of typical commercial applications: think of the GUIs provided
by eclipse, MS Word, Netscape, etc. In this section, we will briefly review some of the ways
of constructing industrial-strength GUIs.
A large GUI must be organized in a systematic way. Typically, this is done with a containment hierarchy .14 As usual, a hierarchy is a tree. The root of the tree corresponds to the
entire GUI. Branches of the tree are parts of the window managed by the GUI. Leaves of the
tree are the basic components of the GUI—the buttons, checkboxes, and so on.
Each part of the tree may have its own layout manager. Some parts might use simple boxes,
and other parts more complex grids. Designing a large GUI is hard work: there may be a
lot of information to present, and it should be presented in a way that is easy for the user to
understand and use.
9.1
Components
We have seen some simple components. The following subsections describe some of the other
components provided by the Swing libraries.
Borders Any Swing component can be given a border . Borders are created by the BorderfFactory
class and come in various designs: empty, line, etched, bevel, titled, matte, and compound.
Some borders have additional attributes, such as colour and justification for titles. The best
GUI designs are simple and uncluttered; do not use fancy borders unless there is a good reason
to do so, and do not use too many border types for a single application.
It is sometimes helpful to show a state change by changing the border of a window. For
example, a “lowered bevel” can be changed to a “raised bevel” to indicate that a button is
active.
Scrolling Windows that must show large objects, such as extended test or big pictures, are
often provided with scroll bars. Scroll bars are narrow strips, usually at the right side and
bottom of a window, with controls that the user can drag to move the contents of the window.
The Swing class JScrollPane provides a window with scrollbars that can be customized for
particular applications.
Splitting A split pane is a window divided into two parts, either horizontally or vertically.
The user can control the relative area of the two parts by moving the dividing line with
the mouse. Split panes are often used with text editors, so that two regions of text can be
displayed at the same time. In Swing, each part is a component that can be formatted as
desired.
The Swing class is called JSplitPane and the constructor specifies whether the division is
horizontal (JSplitPane.HORIZONTAL SPLIT) or vertical (JSplitPane.VERTICAL SPLIT).
14
There is an example of a containment hierarchy on page 527 of the course text.
9 ADVANCED GUI PROGRAMMING
135
Lists List boxes allow the user to select from items in a list. The Swing class is called JList
and its constructor is passed an array of Strings describing the items in the list. The corresponding listener is an instance of ListSelectionListener and the method getSelectedValue,
applied to the JList itself, returns the selected string.
Combo Boxes Combo boxes are very similar to list boxes. The main difference is that
the selectable items in a combo box are not displayed normally, but become visible when
the user clicks the mouse on the combo box. Combo boxes are often used when the number
of options is large—days of the month, for example—and displaying them all would waste
valuable screen space.
if the combo box is uneditable, the user can select only items listed by the program. If the
combo box is editable, the user can either select an item or enter an item that is not in the
list.
Sliders A slider is useful for setting numbers that can vary over a wide range. The slider
appears as a calibrated line with a marker on it; the user slides the marker with the mouse
to obtain the desired value.
9.2
Keyboard Events
The components that we have seen so far “hide” keyboard events in the sense that all the
programmer sees is the end result—new text in a JTextField, for example. It is also possible
for a component to respond to keyboard events directly. For instance, we can write programs
that use the arrow keys to move objects around. A component that responds to keyboard
events must implement the interface KeyListener and provide three methods:
void keyPressed(KeyEvent event: the user has pressed a key
void keyReleased(KeyEvent event: the user has released a key
void keyTyped(KeyEvent event: the user has typed a key
Method keyTyped is a high-level method that is platform independent: it tells you which key
the user typed. The other methods are low-level and have system dependencies. You can find
out, for example, whether the user has pressed the alt key, but this information may not be
useful on keyboards that do not have an alt key.
For example, if the user enters ’A’, the following events are generated:
keyPressed with VK_SHIFT
keyPressed with VK_A
keyReleased with VK_A
keyReleased with VK_SHIFT
keyTyped with ’A’
The VK_ constants are virtual key codes. There are virtual key codes for all of the keys on the
keyboard, including shift, alt, ctrl, ins, del, F (control) keys, arrow keys, and probably
some keys that your keyboard doesn’t have. Many of these keys do not have Unicodes and
do not generate keyTyped events. For example, F keys have no Unicodes.
9 ADVANCED GUI PROGRAMMING
136
Virtual key codes correspond to positions on a North American keyboard , not the label
on the key. For example, VK_Q is the leftmost key on the second row, which is Q on a North
American keyboard and A on a French keyboard.
Here are a few of the more commonly used virtual keys:
VK_ALT
VK_ESC
VK_CAPS_LOCK VK_CONTROL VK_DELETE VK_DOWN VK_END VK_ENTER
VK_Fn VK_LEFT VK_NUM_LOCK VK_RIGHT VK_SHIFT VK_UP
Unlike mouse events, where the listening component is selected by the mouse position, it is
not obvious which component receives keyboard events. In fact, it is the component that has
the focus. This component is usually highlighted in some way: in Windows, for example, its
top border is blue. The user can set the focus by clicking on the component with the mouse,
but there is no system-independent way for the program to set the focus.
10 THREADS
10
10.1
137
Threads
Concurrent Processing
A central processing unit (CPU) executes a sequence of instructions. It normally executes a
single task. There are two ways of getting a computer to do two things at once:
Use two or more CPUs at the same time.
Simulate the execution of several tasks with a single CPU.
Although multiprocessors are important, we will focus on the second option: creating the
illusion of multi-tasking.
Modern CPUs run quite fast, executing around a billion operations per second. If the processor performs one task for 10 ms (milliseconds), and another for 10 ms, and so on, it will
appear to slow-witted people that the computer is doing several things at once. This is called
concurrent processing , even though the tasks are not, in a strict sense, running concurrently. When the processor switches from one task to another, we say that it has performed
a context switch. For efficient multi-tasking, it is important to keep context switches short.
Activity for 10 ms followed by a 250 ms context switch is not going to be useful.
An operating system (OS) can execute several tasks (user programs and its own programs)
concurrently. Since it is important that these tasks do not interfere with one another, the
OS maintains memory partitions that prevent one task from accessing the memory of another
task. Context switching is slow because of the work required to set up these partitions. We
say that the OS runs processes and that processes are heavy or heavyweight.
A single program may also run tasks concurrently. Since security is less important, the
program can use a single region of memory for all of the tasks, which considerably lightens the
load of context switching. We say that the program is running threads and that threads are
light or lightweight. Java provides facilities for running many threads or multithreading .
10.2
Threads in Java
Creating a new thread in a Java program is a simple operation.
Construct a new instance of Thread or a class that extends Thread.
Call the object’s start method. The start method creates a separate process for the
thread, calls the thread’s run method, and returns.
What the thread object does depends on its run method, which you should override.
Example 21: Counting with threads. Figure 103 shows a program that uses two threads
to count at different rates. The method run is implemented as an endless loop. At each
iteration, the object prints its counter (variable count), increments it (variable count), and
then becomes inactive (“sleeps”) for the specified number of milliseconds. The output from
this program, shown in Figure 104, verifies that the counters are stepped independently.
The try/catch blocks are required because sleep throws InterruptedException. 2
A thread stops when its run method returns. If we change the loop condition in Count.run
to
while (count < 50)
10 THREADS
138
public class Count extends Thread
{
private int iden;
private int delay;
private int count;
public Count(int id, int time)
{
iden = id;
delay = time;
count = 0;
}
public void run()
{
try
{
while (true)
{
System.out.println(iden + " -> " + count);
count++;
sleep(delay);
}
}
catch (InterruptedException e)
{
return;
}
}
public static void main(String[] args)
{
new Count(1, 150).start();
new Count(2, 375).start();
}
}
Figure 103: Counting with threads
10 THREADS
139
1 -> 0
2 -> 0
1 -> 1
1 -> 2
2 -> 1
1 -> 3
1 -> 4
1 -> 5
2 -> 2
1 -> 6
1 -> 7
2 -> 3
......
Figure 104: Output from Figure 103
each thread stops when its counter reaches 50. When both threads have stopped, the program
terminates. In any program with threads, the main method waits until all threads have
terminated and then terminates the program.
Early versions of Java had methods stop, suspend, and resume that could be invoked on
thread objects. However, these methods were found to be unsafe. They are still available (so
that old code does not break), but you should not use them.
One way to stop a thread is to interrupt it. We can rewrite main as follows:
public static void main(String[] args)
{
Count c1 = new Count(1, 150);
c1.start();
new Count(2, 375).start();
JOptionPane.showConfirmDialog(null, "Stop 1");
c1.interrupt();
}
This method is suspended, waiting for user input, after the JOptionPane has been displayed,
and both counters run. When the user presses any of the buttons of the JOptionPane,
showConfirmDialog returns, c1.interrupt is executed, and the first counter stops.
10.3
Timing Problems
Concurrency introduces a new problem into programming: timing problems. The problems
arise when threads share variables, as the following example shows.
Suppose that we are simulating a bank in which several tellers are working. Each teller has
its own thread so that it can work independently of the others. Also suppose that, in order to
make a withdrawal from an account, a teller must first get the current balance, then subtract
an amount from it, and then set the new balance. Now consider the following scenario, in
which two tellers are accessing the same account, joe.
10 THREADS
140
Teller 1
amt = joe.get();
Teller 2
amt = joe.get();
amt -= 500;
amt -= 500;
joe.set(amt);
joe.set(amt);
Both tellers execute joe.get() and obtain Joe’s current balance, which is, perhaps, $1,500.
Both tellers perform the withdrawal, obtaining amt = $1, 000. Both tellers then execute
joe.set(amt) with this amount leaving Joe with $1,000. The correct balance is $500, because
two withdrawals have been processed.
It is admittedly unlikely that this exact sequence of events would occur. With computers executing millions of instructions every second, however, even extremely unlikely events happen
from time to time. In fact, many serious computer-related errors and accidents have been
caused by “race conditions” such as these.
The solution is to prevent two threads from accessing a variable concurrently. Teller 1 must
be able to prevent and other tellers from accessing Joe’s account while the withdrawal is in
progress. This is done by locking the account. Teller 1 locks Joe’s account, and other threads
cannot access it while the lock is in force.
(In an old-fashioned bank, Teller 1 would have gone to the safe, removed Joe’s account folder
from it, recorded the transaction, and returned the folder to the safe. Since there is only one
folder, this effectively prevents and other tellers from accessing Joe’s account, and thereby
prevents the kind of error that we are discussing. Now that computers have replace folders
and safes, we have to provide an equivalent mechanism in the software.)
To apply a lock in Java, we declare a method to be synchronized. In the banking program, we
could define withdraw as follows. The comments indicate roughly the effect of synchronized:
public synchronized void withdraw(Account client, Money amount)
{
// Apply a lock to this object, preventing other threads from accessing it.
Money balance = client.get();
balance -= amount;
client.set(balance);
// Remove the lock from this object.
}
We have used three statements here to indicate how interleaving can occur with threads. It
might appear that we would not need synchronization if we could complete the task with one
statement. Surely this code would be safe:
public void withdraw(Account client, Money amount)
{
client.set(client.get() - amount);
}
Unfortunately, this does not help. The Java compiler will generate a sequence of byte codes
corresponding to client.set(client.get() - amount); and, during execution, the system could switch from this thread to another thread in the middle of the sequence. So,
10 THREADS
141
even if it contains only one statement, any method that accesses shared data must be
synchronized.
Synchronizing slows programs down, because it forces some threads to wait while others do
their work. Locking an entire method may cause performance problems if the method is long.
For this reason, Java also provides synchronization of variables. Here is another way of solving
the banking problem:
synchronized (client)
{
Money balance = client.get();
balance -= amount;
client.set(balance);
}
The effect of the synchronized block is to ensure that balance is not accessed by any thread
other than this one while the statements within the block are executed.
Example 22: Playing trains. fig:runtrain shows a class for controlling trains. It creates
two trains, a freight train and an express train, and then starts them. Figure 106 shows
the public parts of a class for trains and Figure 107 shows the private parts.
The trains run on a track that is divided into sections. The static array occupied indicates
which sections are occupied by a train. Trains try to move through the sections from 0 to
LEN, but they cannot enter a section if it is occupied by the train in front.
In order to synchronize the trains, we introduce a unique object called lock, which is an
instance of class Lock. The first train to be constructed creates the lock, and the other trains
just access it.
Each train has its own thread of control. It’s run method tries to move through the sections
from the initial value of pos until pos becomes LEN - 1. The time it spends in each section
depends on its speed. When a train is ready to enter the next section, it calls move.
On entry to move, the train is in section pos. It first uses the lock to wait until the next
section, pos + 1, is not occupied. It then indicates that its current section, pos, increments
pos, and indicates that its new section is occupied. Finally, it uses the lock again to notify
all of the other trains that the value of occupied has changed.
Figure 105 shows a test program for trains. It constructs a fast express at section 0 and a
slower freight train in front if it at section 10. The express catches up with the freight and then
has to wait behind it at each section. Figure 108 shows the output written by the program.
2
There are several things to note about the synchronization techniques used in this example.
The method wait suspends the thread—that is, stops it executing. wait should always
be called in a while loop. When the object is “notified” that something is happened,
wait returns and the while condition is tested again. If it is still true, the object continue
to wait. When the while condition becomes false, the thread continues with the next
statement.
wait throws the checked InterruptedException if it is interrupted. This means that it
must be called from within a try block. In the trains program, we do not interrupt the
threads and so we know that the interrupt can never occur.
10 THREADS
142
There are two methods, notify and notifyAll, that notify waiting objects that something has happened. notify wakes up one waiting thread (and you have no control over
which one) and notifyAll wakes up all waiting threads. In most cases, it is best to use
notifyAll because waking up one thread may not be sufficient–the thread you wake up
may be waiting for a different condition.
All of these methods—wait, notify, and notifyAll—can be called only when the
object that calls them is locked. In the train program, the methods Wait and NotifyAll
in class Lock are synchronized, ensuring that the calls wait and notifyAll are called
by a locked object.
A shared object, in this case lock, is required to ensure the correct execution of threads
that access a shared data structure.
There are other things to know about threads, but this section covers the main points and is
sufficient for simple applications of threads.
public class Controller
{
public static void main(String[] args)
{
Train freight = new Train("freight", 10, 20);
Train express = new Train("express", 0, 50);
freight.start();
express.start();
}
}
Figure 105: Running the trains
10 THREADS
143
public class Train extends Thread
{
public static final int LEN = 50;
public static boolean occupied[] = new boolean[LEN];
public static Lock lock;
public Train(String iName, int iPos, int iSpeed)
{
name = iName;
pos = iPos;
speed = iSpeed;
occupied[pos] = true;
if(lock == null)
{
lock = new Lock();
}
}
public void run()
{
while (true)
{
try
{
sleep(1000 / speed);
if (pos == LEN - 1)
{
exitLastSection();
return;
}
move();
}
catch (InterruptedException e) {}
}
}
Figure 106: The class Train: public parts
10 THREADS
144
private class Lock
{
Lock()
{ }
synchronized public void Wait()
{
try
{
wait();
}
catch(Exception e){}
}
synchronized public void NotifyAll()
{
notifyAll();
}
}
private String name;
private int pos;
private int speed;
private void exitLastSection()
{
occupied[pos] = false;
lock.NotifyAll();
}
private void move() throws InterruptedException
{
while (occupied[pos + 1])
{
lock.Wait();
}
occupied[pos] = false;
pos++;
occupied[pos] = true;
System.out.println(name + ": " + pos);
lock.NotifyAll();
}
}
Figure 107: The class Train: the private parts
10 THREADS
145
express:
freight:
express:
express:
freight:
express:
express:
freight:
....
freight:
express:
express:
freight:
express:
freight:
express:
....
express:
freight:
express:
express:
1
11
2
3
12
4
5
13
18
16
17
19
18
20
19
47
49
48
49
Figure 108: Output produced by the train program. The first part shows the express moving
behind the freight. The second part shows the express catching up: it enters section 18 when
the freight train is in section 19. The last block shows that the express remains behind the
freight train until the end of the run, although it can go faster.
11 PROGRAMMING METHODOLOGY
11
146
Programming Methodology
Early computers were programmed in “machine code”, the native instructions of the machine.
Instructions were executed one after the other, except that a “jump instruction” could transfer
control to an arbitrary point in the program. The first high-level programming languages
provided a simple abstraction from the machine language: they were equipped with a “goto”
statement that provided the same kind of interruption of control flow.
By the mid-sixties, it became clear that the undisciplined use of goto statements encouraged
programmer errors and made programs unnecessarily hard to read and maintain. Furthermore,
Bohm and Jacopini proved in 1966 that all programs can be written using only three control
structures:
sequence: do one thing after another
alternative: do something or something else
loop: do the same thing several times
These three structures permeate computer science: data structures (COMP 352) and formal
language theory (COMP 335) are just two of the places where you will meet them again.
In modern programming syntax, the three control structures look like this:
Sequence:
S1 ;
S2;
. .
Sn ;
.
.
Alternative:
if (B)
S1 ;
else
S2;
Loop:
while (B)
S;
in which Si stands for a statement or sequence of statements and B stands for a condition or
Boolean expression—an expression whose value is true or false.
With these control structures are tools that we can use to write clear and correct programs.
Adopting a systematic and careful approach to coding saves a lot of time in the long run.
Taking a few minutes longer to write the program is much more productive than spending
hours in futile debugging sessions. The notes that follow provide a few simple rules, and some
examples, of effective program development.
11.1
Preliminaries
Parts—possibly all—of this section will seem elementary. That’s because it is elementary.
Nevertheless, knowing these ideas and absorbing them into the knowledge you have “at your
fingertips” will help you to become a better programmer.
11 PROGRAMMING METHODOLOGY
The while Rule
147
When a while statement terminates, the loop condition is false.
while (B)
S;
// B is false here
This follows directly from the definition of the while statement; it is more useful than it looks,
especially when B is a complicated expression.
The do-nothing rule Always consider the effect of a while statement when the condition
is false on entry. In this case, the body of the loop is never executed. Write your loops in
such a way that non-execution is correct.
As a simple example, consider summing the numbers 1, 2, 3, . . ., n. The code is
s=0
i=1
while i ≤ n
s += i
i++
If n = 0, the initial value of i ≤ n is 1 ≤ 0, which is false. The loop body is never executed
and the result, s = 0, is correct.
Notation In the previous example, and subsequently in this section, we use pseudocode
rather than correct Java. The differences are minor: we omit semicolons; we omit parentheses
around conditions; and we use indentation rather than curly brackets for short blocks of code.
The pseudocode does not distinguish between = used as an assignment operator and = used
as a comparison. This is consistent with the use of mathematical symbols for other binary
operators (6=, ∧, ∨, ⇒, etc.) and hopefully will note cause confusion.
for and while The for statement is not included in the “primitive” statements because it
is easily defined in terms of while. The statement
for (i = m; i < n; i++)
S
is equivalent to the statement
i=m
while i < n
S
i++
The for statement is more expressive and has the advantage that there is a lot of information
in its first line. Nevertheless, the while statement is more general, and we will use it for
development, converting to for only when we recognize the particular pattern above.
Fenceposts Figure 109 shows a row of fence posts. We can also view it as a diagram of a
container holding objects a, b, c, d, e, and f . The numbers on top of the fenceposts are used
to count the number of objects contained. The first post corresponds to zero objects stored,
and is numbered 0. The last post corresponds to the container being full and is numbered 6.
11 PROGRAMMING METHODOLOGY
148
There are two ways of numbering the objects in the container. The conventional way—the
way we learn as children—is to number them a = 1, b = 2, and so on, with f = 6. The
numbering we use in Computer Science is different: we start counting from zero: a = 0, b = 1,
and so on until f = 5.
Many people find it perverse to count from zero when we are so used to counting from 1. But
counting from zero does have some advantages: in particular, it reduces the number of times
we have to write +1 or −1 in programs. Whether you like it or not, counting from zero is an
established convention, and you will have to get used to it, if only to work with arrays—the
index of the first element of an array in Java, and many other languages, is 0.
0
1
a
2
b
3
c
4
d
5
e
6
f
Figure 109: Fence posts
If we write n for the number of objects in Figure 109, we have 0 ≤ n ≤ 6. If we write i
for the index of an object, we have 0 ≤ i ≤ 5 or, equivalently, 0 ≤ i < 6. The difference
between 0 ≤ n ≤ 6 and 0 ≤ i < 6 is annoying, but it cannot be avoided, because the number
of fenceposts is one greater than the maximum number of objects.
In most cases, we work with indexes, not numbers. Accordingly, we adopt the convention of
using intervals of the form p ≤ q < r (known in mathematics as a semi-closed or semi-open
interval .) The combination of counting from zero and using semi-closed intervals leads to
clean and uncluttered code, as we will see.
As a first example of semi-closed intervals, recall that a common form of the for statement
is
for (i = 0; i < N ; i++)
S
corresponding to the interval 0 ≤ i < N .
11.2
Containers
A container in programming is a data structure used to store a number of objects usually, but
not necessarily, of the same type. The Java library class Container is a container according
to this definition. In many cases, each object has a key that is used to identify it and some
data that is stored. For example, a telephone directory is a container in which the key is a
person’s name and the data is that person’s telephone number (and usually a short form of
their address).
We will work with very simple containers, in which the key is an integer and there is no data.
All of the code that we develop is easily extended to the general case. All that is required
is that keys be comparable: given keys k1 and k2, we can decide whether k1 < k2, k1 = k2,
or k1 > k2 . We will assume no duplicates (or unique keys)—that is, a container never
contains two or more objects with the same key.
11 PROGRAMMING METHODOLOGY
149
It might seem pointless to design containers knowing that the Java libraries provide a variety
of containers that are easy to use. However, someone has to write the libraries, and that is a
job best left to CS graduates who might get it right.
We require a container to implement four methods:
Initialize: create an empty container
Insert: insert an object into a container
Find: determine whether an object is in a container
Delete: remove an object from a container
The most interesting methods are find and insert. Most of the discussion is about these
two, but we will mention the others when necessary.
11.3
Unordered Array Containers
The simplest container stores the objects in an array without sorting them. Figure 110 shows
the conventions that we will use throughout the discussion.
The name of the array is A.
The size of the array is MAX.
The elements of the array are indexed by 0, 1, 2, . . ., MAX − 1.
The first N elements of the array are occupied by objects.
The objects actually stored in the array are indexed by 0, 1, 2, . . ., N − 1.
A
0
1
2
7
10
3
N
MAX
5
Figure 110: A container
Finding an object The first method that we consider is find. Given an integer k, we want
to determine whether it occurs in the array A. Here is a formal statement of the problem:
∃ i . 0 ≤ i < N ∧ A[i] = k.
This statement is either true, in which case it must provide a value of i (that is, the value
that ‘∃i’ claims must exist) or it is false because there is no value of i that satisfies the
conditions.
It is not hard to write code to find k. This is the linear search algorithm:
i=0
while i < N ∧ A[i] 6= k
i++
Applying the while rule tells us that, after the loop has terminated:
¬(i < N ∧ A[i] 6= k)
11 PROGRAMMING METHODOLOGY
150
which, by deMorgan’s laws, is equivalent to
i ≥ N ∨ A[i] = k
In programming terms, there are two cases: either k does not occur in the array and i ≥ N ,
or k does occur in the array and A[i] = k.
The while condition i < N ∧ A[i] 6= k looks unsafe. Suppose that k does not occur in the
array. During the last iteration, i++ sets i to N . Accessing A[i] when i = N is incorrect
and will cause an exception to be thrown in a Java program. The condition is safe, however,
because the second term is evaluated only if the first term is true. In fact,
i < N ∧ A[i] 6= k
is evaluated as if we had written
if i < N
return A[i] 6= k
else
return false
In other words, A[i] is will never be evaluated if i < N is false. This ensures the correctness of
the code and is a “trick” that is often used in writing conditions in Java and other programming
languages.
The “or” operator is evaluated in a similar way. In the condition P || Q, if P is true, the
entire expression must be true and there is no need to evaluate Q.
These conventions are appropriate for programming because they yield concise and readable
code. We should note, however, that they have the interesting consequence of making the
operators && and || non-commutative. The expressions
i < N && A[i] != k
and
A[i] != k && i < N
are not equivalent because, if i = N, the first will yield false and the second will throw an
exception.
Although linear search is very simple, there is room for improvement. The condition i < N is
executed during every cycle of the loop even though it is usually false. In fact, if k is found,
i < N is always false and, if k is not found, it becomes true only at the final iteration. We
need the test i < N for the case when k does not occur in the array. This case will never arise
if we put k in the array before we start. We can use the next free slot, with index N , to store
it. The revised code is:
i=0
A[N ] = k
while A[i] 6= k
i++
The while rule tells us that, when the loop terminates, A[i] = k. There are still two cases: if
i < N , we have found a k that was already in the array; if i = N , we have found the k that
we planted.
The value A[N ] = k, which forces the loop to terminate without having to test i < N , is
called a sentinel .
11 PROGRAMMING METHODOLOGY
151
Inserting an object The insert problem is defined here as: given a new object k, add it
to the store unless it is already in the store, in which case, do nothing.
If we use the sentinel, the insertion is easy, because we have already stored the new object;
all we have to do is increment the store size, N . The code is:
i=0
A[N ] = k
while A[i] 6= k
i++
if i = N
N ++
Final notes:
1. We must address the possibility that the array will be full when we insert an object.
Normally, we would say that the array is full when N = MAX. However, that would not
leave a space for the sentinel. Using a sentinel reduces the number of spaces we can use
for storing from MAX to MAX − 1. This is a reasonable price to pay for a substantial
performance improvement.
2. We note how the do-nothing rule applies to this example: if N = 0, the algorithm:
sets A[0] = k
loops while A[0] 6= k—that is, never
increments N so that N = 1 gives the number of elements stored
3. We can use the for/while relation to rewrite the final version in a slightly simpler way.
Note the empty for loop body:
A[N ] = k
for (i = 0; A[i] 6= k; i++;)
{ }
if i = N
N ++
1
Deletion In order to delete an element, we must first find it. Removing the element creates
a “hole” in the array that we must fill up. We could fill it up by moving a block of elements
but it is easier just to move the last element into the hole.
A[N ] = k
for (i = 0; A[i] 6= k; i++;)
{ }
if i = N
Error: deleting an element that is not there
else
N −−
A[i] = A[N ]
11 PROGRAMMING METHODOLOGY
11.4
152
Ordered array containers
We can improve the performance of find significantly if we keep the objects ordered. That
is, we require
i < j ⇒ A[i] < A[j].
Note that this condition implies that there are no duplicates, as we have been assuming. If
we allowed duplicate keys, we would have to write the ordering condition as
i < j ⇒ A[i] ≤ A[j].
The technique that we use for searching in an ordered array is analogous to the way most
people use a telephone directory15 — they open the directory roughly in the middle and then
compare the names at the top of the page to the name they are looking for. Then they choose
a page either before the open page or after it, depending on the result of the comparison.
As before, we are looking for an entry A[i] = k such that 0 ≤ i < N . It is helpful to generalize
the problem as follows: we look for an entry A[i] = k such that L ≤ i < R. If we can solve
this problem, we can solve the original problem by setting L = 0 and R = N . (The letters L
and R are meant to suggest “left” and “right”, respectively.)
Assume that, if k is in the array, it is between L and R. Using the usual “half-closed” interval,
this means that A[L] ≤ k < A[R].
m
L
< A[m]
R
> A[m]
Figure 111: Partitioning an array for binary search
We divide this interval into three parts, as shown in Figure 111. First, the midpoint, m,
where m = (L + R)/2; second, the values to the left of the midpoint, L ≤ i < m; and, third,
the values to the right of the midpoint, m < i < R. We can decide which of these intervals
contains k by comparing A[m] to k:
while true
m = (L + R)/2
if A[m] = k
break
else if A[m] > k
R=m
else
L=m+1
Writing a while loop with a break statement is often the most natural approach for an
algorithm that, like this one, has two reasons for termianting the loop. It is important to
note, however, that the while rule does not apply when there is a break statement. In
fact, applying it here would tell us that, after the while loop terminates, !true is true (!)—in
other words, that the loop does not termiante.
15
Actually “most people” probably use the internet these days. But there may be a few elderly or conservative
characters who find numbers in the phone book.
11 PROGRAMMING METHODOLOGY
0
2
1
5
2
6
153
3
8
4
9
5
12
6
14
7
(a) The test array
L R m A[m] k ∼
k=5 0 7 3
8
5 >
0 3 1
5
5 =
k = 14 0 7 3
8
14 <
4 7 5
12 14 <
6 7 6
14 14 =
k=7 0 7 3
8
7 >
0 3 1
5
7 <
2 3 2
6
7 <
3 3 3
8
7 >
k=0 0 7 3
8
0 >
0 3 1
5
0 >
0 1 0
2
0 >
0 0 0
2
0 >
k = 17 0 7 3
8
17 <
4 7 5
12 17 <
6 7 6
14 17 <
7 7 7
?
17 ?
(b) The results
Figure 112: Binary search in action
It is helpful to execute an algorithm like this by hand to obtain insight into how it works.
The examples are based on the following configuration of the store. The boxes show the
contents of the array and the numbers above are the indexes. The valid indexes are i such
that 0 ≤ i < 7. Figure 112 shows how the algorithm works with various values of k. At the
top, (a) is a test array. Underneath, (b) shows results obtained with this array. The symbol
∼ in the last column shows the result of comparing A[m] and k. For example, “>” stands for
A[m] > k.
The first two experiments (k = 5 and k = 14) look for values that are in the array. The
symbol in the last column of the last row is =, indicating that A[m] = k and m is the result
that we need.
The last three experiments (k = 7, k = 0, and k = 17) are more interesting: the value sought
is not in the array. In each case, the algorithm reaches a state in which L = R. Consider
what this means: our original assumption is
if k is in the array, then A[L] ≤ k < A[R].
If L = R, the condition becomes A[L] ≤ k < A[L], or A[L] < A[L], which is false. This tells
us that the assumption that k is in the array leads to a contradiction, and therefore the
11 PROGRAMMING METHODOLOGY
154
assumption must be false. (This is analogous to proof by contradiction.)
In the last example, k = 17, and we obtain the final values L = m = R = 7. We should not
evaluate A[7], because 7 is not a valid index.
We can now see, however, that the algorithm terminates in one of two ways: either A[m] = k,
and we have found the key, or L = R, and the key does not occur in the array. We can write
an improved version of the algorithm:
L=0
R=N
while L < R
m = (L + R)/2
if A[m] = k
break
else if A[m] > k
R=m
else
L=m+1
The loop terminates either with A[m] = k or with L = R. We also note that the solution
now satisfies the do-nothing rule. If the array is empty, L = R = 0, and the loop terminates
immediately with the result that the key is not in the array. This is the binary search
algorithm.
Insertion Inspection of the last three experiments shows that the final value of L and R
is the correct place to insert the missing key, if that is what we want to do. Insertion into
an ordered array is more difficult that insertion into an unordered array, because we must
respect the ordering. Before inserting the new value, we must shift all of the other values up
one place,16 as shown in Figure 113.
m
0
< A[m]
N
> A[m]
@
R
@
@
R
@
Figure 113: Moving a block to make space for the new element
There are several points to note:
We must move the last element first, to avoid overwriting.
There are two forms the move statement might take. Here are the statements and the
range of the index, i.
A[i] = A[i − 1]
A[i + 1] = A[i]
16
m<i≤N
m≤i<N
The same situation arose in the word concordance example: see Figure 87 on page 119.
11 PROGRAMMING METHODOLOGY
155
Since we are going backwards, the first version produces a neater looking while loop.
We initialize with i = N and the termination condition is i > m. If we use the second
form, we have to intialize with i = N − 1 and terminate with i ≥ m.
The loop is
for (i = N ; i > m; i−−)
A[i] = A[i − 1]
Figure 114 shows the complete code to check for a key and insert it if it is not present.
L=0
R=N
while L < R
m = (L + R)/2
if A[m] = k
break
else if A[m] > k
R=m
else
L = m+1
if L = R
for (i = N ; i > m; i−−)
A[i] = A[i − 1]
N ++
Figure 114: Find and insert using binary search
Deletion Deletion starts off in the same way as insertion: we first have to find the element
to be deleted. We cannot simply move the last element into the “hole” because we must
maintain the ordering; we have to do a block move instead (see Figure 115). This time, we
increment indexes to avoid overwriting.
After locating the element at m, we delete it by executing
for (i = m + 1; i < N ; i++)
A[i − 1] = A[i]
N −−
m
0
< A[m]
N
> A[m]
Figure 115: Moving to fill the hole
11 PROGRAMMING METHODOLOGY
156
Using Recursion We can write the binary search procedure as a recursive method. The
following version returns either the index of the element or −1 if the element is not in the
array. The array is assumed to be global (in practice, it might be an instance varaible of a
class).
int find(int k, int L, int R)
if L ≥ R
return −1
m = (L + R)/2
if A[m] = k
return k
else if A[m] > k
return find(k, L, M )
else // A[m] < k
return find(k, M + 1, R)
The recursive version does not differ significantly in performance from the loop version. It
does reveal, however, the recursive structure of binary search: the same algorithm is begin
applied to different sections of the array.
Summary Programming often involves trade-offs. Using an unordered array, finding and
inserting random values takes roughly the same time. Using an ordered array, finding is
much faster, but inserting takes longer because we have to move values. The following table
summarizes the performance of the algorithms. The table entries are times (expressed in
“order notation”) assuming a store size of N entries. Note that deletion takes the same time
in both cases. Although binary search allows us to find the element to be deleted faster, the
search time is dominated by the time taken to move the block.
Unordered
Ordered
Find
Insert
Delete
O(N )
O(N )
O(N )
O(log2 N )
O(N )
O(N )
Whether we use an ordered or an unordered array depends on the circumstances. If the store
is active, meaning that there are roughly the same number of insertions, deletions, and finds,
then an unordered array is probably best. If the store is quiet, meaning that there are many
finds but only occasional insertions and deletions, then an ordered array will be much faster.
In fact, we can have the best of both: there are data structures that enable all operations to
be implemented in O(log2 N ) time.
11.5
Array algorithms
Many operations on arrays can ber performed by processing the elements sequentially, as in
linear search. In this section, we discuss various applications of this principle. The general
rule is: obtain and store all of the information needed in one step.
Finding the Maximum The basic operation of finding the largest element of an array is
very simple. Assume that the array A has N elements and we want to store the value of the
largest element in m:
11 PROGRAMMING METHODOLOGY
157
m = A[0]
for (i = 1; i < N ; i++)
if m < A[i]
m = A[i]
Nevertheless, there are a couple of points to note:
1. Many programmers write the body of the loop in the form:
if A[i] > m
m = A[i]
The effect is the same, but the reversal of m and A[i] makes it slightly harder to read—
and more prone to error when you are writing it.
2. An alternative technique is to set m to a very small value and then to iterate over all
elements of the array:
m = −1000
for (i = 0; i < N ; i++)
if m < A[i]
m = A[i]
This version is fine provided that there is a sensible initial value for m. If we know
that all elements of the array are positive, we can initialize m = 0. If we do not know
anything about the values, there is no safe way to initialize m.
3. The do-nothing rule presents problems for maximum problems because there is no reasonable value for the maximum of an empty set. An advantage of the second version
(initializing m to a small value) is that it does not fail if the array is empty (N = 0).
4. It is useful to have a precise idea of what has been accomplished at each step of the
loop. In this case, we can say that “m is the largest element found so far” or, more
concisely, “largest so far”.
It is clear that, if m is the “largest so far”, and we execute
if m < A[i]
m = A[i]
on the current element, then m is still the “largest so far”.
“Finding the maximum” provides a pattern for finding other kinds of extremal values. The
pattern is:
m = a small initial value
for (i = 0; i < N ; i++)
c = current value
if m < c
m=c
Maximal step For example, the “maximal step” problem is to find the greatest step in an
array, where “step” is defined as A[i] − A[i − 1]. If an array has steps, it must have at least
two elements, so we assume the existence of A[0] and A[1]. This code assigns the value of the
largest step to s.
11 PROGRAMMING METHODOLOGY
158
m = A[1] − A[0]
for (i = 2; i < N ; i++)
s = A[i] − A[i − 1]
if m < s
m=s
Longest plateau The longest plateau problem was introduced by David Gries as an
example of reasoning about programs; it is another “find the maximum” problem:
The array A has N elements, may have duplicate elements, and is sorted. A
plateau of length p is a sequence of p consecutive elements with the same value.
Find the length of the longest plateau in A.
For example, suppose the elements of A are
1 2 3 3 3
4 5 6 6 6
6 7 8 8 9
then the length of the longest plateau is 4 because there is a sequence of four sixes.
Our first thought is perhaps some kind of nested loop: starting at each point in the array,
find the length of the plateau starting at that point, and record the longest.
p=1
for (i = 0; i < N ; i++)
j =i+1
while A[j] = A[i]
j++
if p < j − i
p=j−i
With careful reasoning, we can do better than this.
1. Since the array is sorted, no number before a plateau can be equal to any number after
the plateau. Consequently, if A[i] = A[i − p], there must be a plateau of length p + 1.
For example, if A[i] = A[i − 1], there is a plateau of length 2.
2. If we know that there is a plateau of length p, there is no point in looking for plateaus
of length less than p.
3. Combining 1 and 2, we see that the only useful comparison is A[i] = A[i − p], because
comparing closer elements of the array gives us no further information.
Putting all this together gives the following solution:
p=1
for (i = 1; i < N ; i++)
if A[i] = A[i − p]
p++
11 PROGRAMMING METHODOLOGY
159
Maximal subvector The following example, which we will not develop in detail, is an
actual case history of solving a problem by careful reasoning. The problem is to find the
largest sum of consecutive elements in an array. If all of the elements are positive, the answer
is obviously the sum of all elements. The problem is interesting only if the array contains
negative numbers. If the array is
3 5
−7 4
−8 9 2 4
−1
−2 5 6
the maximum sum is 9 + 2 + 4 − 1 − 2 + 5 + 6 = 23.
Ulf Grenander needed to solve this problem in order to implement a pattern-matching algorithm. He first noted that there is an “obvious” solution, which is to enumerate all possible
sums and note the largest. In the following code, m is the value we are seeking and s is a
sum.
m=0
for (i = 0; i < N ; i++)
for (j = i; j < N ; j++)
s=0
for (k = i; k ≤ j; k++)
s = s + A[k]
m = max(m, s)
He we have written m = max(m, s) as a short form of
if m < s
m=s
We will call this the “naive” solution. It is slow, requiring time O(N 3) (it is called a cubic
algorithm). Grenander quickly found an improved version. He noticed that many sums are
formed over and over again. If we already have the sum from A[L] to A[R], say, we do not
need to compute it again to find the sum from A[L] to A[R + 1]. This insight leads to the
following code.
m=0
for (i = 0; i < N ; i++)
s=0
for (j = i; j < N ; j++)
s = s + A[j]
m = max(m, s)
This is much better: it needs time O(N 2) (it is a quadratic algorithm). Grenander showed
the problem and the quadratic solution to Michael Shamos who, by the following day, had a
solution considerably longer than the previous solutions (and so we will not show it here) that
required time O(N log2 N ). For a few days, this was believed to be the best possible solution.
Then Shamos showed the problem to Jay Kadane, who thought for a minute and then wrote
down this solution:
m=0
h=0
for (i = 0; i < N ; i++)
h = max(h + A[i], 0)
m = max(h, m)
11 PROGRAMMING METHODOLOGY
160
This version is obviously linear: it requires time O(N ). To understand it, read h as “max
ending here” and m as “max so far”.
Kadane found this solution by thinking: what information do we need to collect to solve the
problem in a single scan of the array? He also noticed that any maximal sum must be bounded
either by one end of the array or a negative number.
Simple-minded optimization may speed up code by a constant factor, but it will never achieve
the kinds of improvement we can obtain by changing the order from, say, O(N 2) to O(N ).
The following table compares the performance of the two algorithms on two computers.
N
10
100
1,000
10,000
100,000
1,000,000
3
3
3
49
35
95
System 1
microseconds
milliseconds
seconds
minutes
days
years
200
2
20
3.2
32
5.4
System 2
milliseconds
seconds
seconds
minutes
minutes
hours
In this imaginary competition, System 1 is a Cray-1 supercomputer running the naive (O(N 3))
algorithm in fortran and System 2 is a trs-8017 running the linear (O(N )) algorithm in
basic. The morale is that the algorithm that you use is more important than the computer
that you run it on.
Signal processing Smoothing is a technique used in signal processing to remove high
frequency components from signals. A signal is a stream of numbers. In digital signal
processing, the numbers are usually integers with 8, 12, 16, or more bits. Smoothing replaces
each signal value by the average of signal values taken during a small interval. We will suppose
that the length of this interval is L samples. Then the output corresponding to the signal at
time t, st is
at =
st−L+1 + st−L+2 + · · · + st−1 + st
.
L
We can compute a value like this using a cyclic array . The array contains L values which,
at any given time, are the L most recent samples. When the next sample, st+1 , arrives we
have to compute the new average
at+1 =
st−L+2 + st−L+3 + · · · + st + st+1
.
L
To avoid repeating the entire sum, we add the difference to the array:
at+1 = at + (at+1 − at )
st−L+2 + st−L+3 + · · · + st + st+1 st−L+1 + st−L+2 + · · · + st−1 + st
= at +
−
L
L
st+1 − st−L+2
= at +
.
L
17
An early, primitive, and slow PC sold by Radio Shack.
11 PROGRAMMING METHODOLOGY
161
To implement this, we compute st+1 − st−L+2 , which is the difference between the newest
sample and the oldest stored sample, divide the result by L, and add the result to the average.
We must also replace the oldest sample by the newest sample.
In the following code, a is the running average, x is the new sample, and S is the cyclic array
of stored samples.
i = (i = L ? 0 : i + 1)
a = a + (x − S[i])/L
S[i] = x
Download