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