ΑΡΙΣΤΟΤΕΛΕΙΟ ΠΑΝΕΠΙΣΤΗΜΙΟ ΘΕΣΣΑΛΟΝΙΚΗΣ ΣΧΟΛΗ ΘΕΤΙΚΩΝ ΕΠΙΣΤΗΜΩΝ ΤΜΗΜΑ ΠΛΗΡΟΦΟΡΙΚΗΣ ΠΤΥΧΙΑΚΗ ΕΡΓΑΣΙΑ Επισκόπηση γλώσσας προγραµµατισµού JAVA Ονοµατεπώνυµο: Ευθυµίου Αφροδίτη Α.Ε.Μ:884 Επιβλέπουσα Καθηγήτρια: Βακάλη Αθηνά
2
ΠΕΡΙΕΧΟΜΕΝΑ 1. Εισαγωγή... 11 1.1. Μια νέα γλώσσα προγραµµατισµού... 11 1.2. Χαρακτηριστικά της Java... 12 1.3. Java και World Wide Web... 14 1.4. Εκτέλεση προγραµµάτων... 15 1.5. Είδη προγραµµάτων Java... 16 1.6. Ένα απλό παράδειγµα κώδικα Java... 17 1.6.1. Αυτόνοµη εφαρµογή Java... 17 1.6.2. Java Applet... 18 2. Τα βασικά της γλώσσας... 19 2.1. Κώδικας προγραµµάτων... 19 2.2. Σχόλια... 19 2.3. Τύποι δεδοµένων και σταθερές... 20 2.3.1. Βασικοί Τύποι. 20 2.3.2. Τύποι αναφοράς. 22 2.3.3. Ειδικοί τύποι.. 23 2.4. Εκφράσεις... 23 2.4.1. Αριθµητικοί τελεστές και τελεστές σύγκρισης... 25 2.4.2. Τελεστές bit προς bit 26 2.4.3. Λογικοί τελεστές. 27 2.4.4. Τελεστές εκχώρησης. 28 2.5. Εντολές... 28 2.5.1. Εντολές εκφράσεων... 29 2.5.2. Εντολές επιλογής... 29 2.5.3. Εντολές βρόχων.. 30 2.5.4. Άλλες εντολές.. 32 2.6. Λυµένες Ασκήσεις... 33 2.7. Ασκήσεις... 46 3. Πίνακες... 49 3.1. ηλώσεις πινάκων... 49 3.2. ηµιουργία πινάκων... 49 3.3. Αρχικοποίηση πινάκων... 50 3.4. Πίνακες πολλαπλών διαστάσεων... 50 3.5. Όρια πινάκων... 51 3.6. Αντιγραφή πινάκων... 52 3.7. Λυµένες Ασκήσεις...... 53 3.8. Ασκήσεις...... 64 4. Αντικείµενα και Κλάσεις... 67 4.1. Αντικειµενοστρεφής προγραµµατισµός... 67 4.2. Αντικείµενα και κλάσεις... 67 4.2.1. Υπερφόρτωση µεθόδων. 70 3
4.2.2. Κατασκευή αντικειµένων... 71 4.2.3. Καταστροφή αντικειµένων... 73 4.2.4. Μετατροπείς ορατότητας.. 74 4.3. Ιεραρχίες κλάσεων... 74 4.3.1. Κληρονοµικότητα και κατασκευαστές. 76 4.3.2. Υπο-τύποι και µετατροπή τύπων.. 77 4.3.3. Υπερκάλυψη µεταβλητών και υπερίσχυση µεθόδων.. 78 4.3.4. Αφηρηµένες και τελικές κλάσεις.. 80 4.4 Υποκλάσεις... 81 4.4.1. Η σχέση is-a. 81 4.4.2. Η δεσµευµένη λέξη extends.. 82 4.4.3. Απλή κληρονοµικότητα... 83 4.4.4. Οι συναρτήσεις δηµιουργίας δεν κληρονοµούνται.. 83 4.4.5. Πολυµορφισµός. 83 4.4.6. Ορίσµατα µεθόδων και ετερογενείς συλλογές... 84 4.4.7. Ο τελεστής instanceof.. 85 4.4.8. Μετατροπές αντικειµένων... 85 4.5. Interfaces... 86 4.6. Πακέτα... 87 4.6.1. ηµιουργία πακέτων... 88 4.6.2. Πρόσβαση σε µέλη κλάσεων... 89 4.7. Λυµένες Ασκήσεις... 97 4.8. Ασκήσεις... 104 5. Εξαιρέσεις (exceptions)... 107 5.1. Εξαιρέσεις... 107 5.2. Παράδειγµα Εξαίρεσης... 107 5.3. Χειρισµός εξαίρεσης... 108 5.3.1. Οι εντολές try και catch.. 108 5.3.2. Η εντολή finally.. 108 5.3.3. Ξανά στο παράδειγµα.. 109 5.3.4. Συνήθεις εξαιρέσεις.. 110 5.4. Κατηγορίες εξαίρεσης... 110 5.5. Ο κανόνας «ήλωσε ή αντιµετώπισε»... 111 5.6. ηµιουργώντας τις δικές σας εξαιρέσεις... 111 5.6.1. Παράδειγµα... 112 5.7. Λυµένες Ασκήσεις... 114 5.8. Ασκήσεις... 117 6. Ρεύµατα Εισόδου/Εξόδου και Αρχεία... 119 6.1. Ρεύµατα εισόδου/εξόδου στη Java... 119 6.1.1. Βασικά σχετικά µε τα ρεύµατα.. 119 6.1.2. Μέθοδοι InputStream.. 120 6.1.3. Μέθοδοι OutputStream.. 120 6.2. Βασικές κλάσεις ρευµάτων... 121 6.2.1. FileInputStream και FileOutputStream... 121 6.2.2. BufferedInputStream και BufferedOutputStream. 121 4
6.2.3. DataInputStream και DataOutputStream... 122 6.2.4. PipedInputStream και PipedOutputStream. 122 6.3. Ρεύµατα εισόδου URL... 122 6.3.1. Ανοίγοντας ένα ρεύµα εισόδου.. 123 6.4. Αναγνώστες και Εγγραφής... 123 6.4.1. Unicode 123 6.4.2. Μετατροπές Byte <-> χαρακτήρων... 124 6.4.3. Αναγνώστες και Εγγραφείς µε ενδιάµεση µνήµη. 124 6.4.4. Ανάγνωση συµβολοσειράς από την είσοδο. 124 6.4.5. Χρησιµοποιώντας άλλες µετατροπές χαρακτήρων... 124 6.5. Αρχεία... 125 6.5.1. ηµιουργία ενός νέου αντικειµένου File. 125 6.6. Έλεγχοι και βοηθητικές συναρτήσεις σε αρχεία... 125 6.6.1. Ονόµατα αρχείων.. 125 6.6.2. Έλεγχοι σε αρχεία. 125 6.6.3. Γενική πληροφορία αρχείων και βοηθητικές συναρτήσεις. 126 6.6.4. Βοηθητικές συναρτήσεις καταλόγων 126 6.7. Αρχεία τυχαίας προσπέλασης... 126 6.7.1. ηµιουργία ενός αρχείο τυχαίας προσπέλασης 126 6.7.2. Πρόσβαση στην πληροφορία.. 126 6.7.3. Προσθήκη πληροφορίας.. 127 6.8. Serializable... 127 6.8.1. Γράφοι αντικειµένων. 127 6.9. ιαβάζοντας και γράφοντας σε ένα ρεύµα αντικειµένων... 128 6.9.1. Εγγραφή.. 128 6.9.2. Ανάγνωση. 128 6.10. Λυµένες Ασκήσεις... 129 6.11. Ασκήσεις... 133 7. Νήµατα (threads)... 135 7.1. Νήµατα και χρήση νηµάτων στη Java... 135 7.1.1. Τι είναι τα νήµατα;... 135 7.1.2. Τρία µέρη ενός νήµατος.. 135 7.1.3. ηµιουργία ενός νήµατος 136 7.1.4. Εκκίνηση ενός Thread. 137 7.1.5. Χρονοπρογραµµατισµός του νήµατος.. 137 7.2. Βασικός έλεγχος νηµάτων... 139 7.2.1. Τερµατισµός ενός νήµατος Deprecated... 139 7.2.2. Έλεγχος ενός νήµατος.. 140 7.2.3. Βάζοντας τα νήµατα σε αναµονή... 140 7.3. Άλλοι τρόποι δηµιουργίας νηµάτων... 141 7.3.1. Ποιον τρόπο να χρησιµοποιήσουµε;... 142 7.4. Χρήση της synchronized στη Java... 143 7.4.1. Εισαγωγή.. 143 7.4.2. Το πρόβληµα.. 143 7.4.3. Η ένδειξη κλειδώµατος Object... 144 7.4.4. Ελευθέρωση της ένδειξης κλειδώµατος... 146 7.4.5. Βάζοντάς τα όλα µαζί... 146 5
7.4.6. Αδιέξοδο... 147 7.5. Αλληλεπίδραση νηµάτων wait() και notify()... 148 7.5.1. Εισαγωγή.. 148 7.5.2. Το πρόβληµα.. 148 7.5.3. Η λύση.. 148 7.5.4. Η αλήθεια!... 149 7.6. Βάζοντάς τα όλα µαζί... 150 7.6.1. Παραγωγός.. 151 7.6.2. Καταναλωτής... 151 7.6.3. Η κλάση SyncStack. 152 7.7. Λυµένες Ασκήσεις..... 157 7.8. Ασκήσεις...... 157 8. Ανάπτυξη GUI σε Java... 159 8.1. Το πακέτο java.awt... 159 8.2. Ανάπτυξη γραφικών διεπαφών µε το χρήστη... 160 8.2.1. Containers και Components... 160 8.2.2. Τοποθέτηση components... 160 8.2.3. Μέγεθος component... 160 8.3. Frames..... 160 8.3.1. ηµιουργία ενός απλού Frame... 160 8.3.2. Εκτέλεση προγράµµατος... 161 8.4. Panels... 161 8.4.1. ηµιουργία panels 162 8.5. Container Layouts..... 162 8.5.1. Layout Managers... 163 8.6. Ένα απλό παράδειγµα..... 163 8.6.1. Η µέθοδος main()... 163 8.6.2. new Frame( GUI Example ).. 164 8.6.3. f.setlayout(new FlowLayout())... 164 8.6.4. new Button( Press me )... 164 8.6.5. f.add(b1)... 164 8.6.6. f.pack()... 164 8.6.7. f.setvisible(true)... 164 8.7. Layout Managers..... 165 8.7.1. Flow Layout Managers... 165 8.7.2. Border Layout Manager... 166 8.7.3. Grid Layout Manager... 167 8.7.4. CardLayout... 169 8.7.5. Άλλοι Layout Managers... 170 8.8. Containers..... 170 8.8.1. Frames... 170 8.8.2. Panels... 171 8.8.3. ηµιουργία panels και σύνθετων layouts... 171 8.9. Λυµένες Ασκήσεις...... 173 8.10. Ασκήσεις........ 183 6
9. Το µοντέλο συµβάντων του AWT... 185 9.1. Τι είναι ένα συµβάν..... 185 9.1.1. Πηγές συµβάντων... 185 9.1.2. Χειριστές συµβάντων... 185 9.1.3. Πώς γίνεται η επεξεργασία των συµβάντων... 185 9.2. Μοντέλο συµβάντων JDK 1.0 εναντίον JDK 1.1..... 185 9.2.1. Ιεραρχικό µοντέλο (JDK 1.0)... 185 9.2.2. Μοντέλο εξουσιοδότησης (JDK 1.1)... 186 9.3. Συµπεριφορά των GUI στη Java..... 188 9.3.1. Κατηγορίες συµβάντων... 188 9.3.2. Ένα περισσότερο σύνθετο παράδειγµα... 189 9.3.3. Πολλοί listeners... 191 9.4. Προσαρµοστές συµβάντων..... 192 9.5. Ασκήσεις..... 193 10. Η βιβλιοθήκη Components του AWT... 195 10.1. ιευκολύνσεις του AWT..... 195 10.2. Buttons... 195 10.3. Checkboxes..... 195 10.4. CheckboxGroups Radio buttons..... 196 10.5. Choice..... 197 10.6. Canvas..... 197 10.7. Label..... 199 10.8. TextField..... 200 10.9. TextArea..... 200 10.9.1. TextComponent... 200 10.10. List..... 201 10.11. Frame..... 202 10.12. Panel..... 202 10.13. Dialog..... 202 10.14. FileDialog..... 203 10.15. ScrollPane..... 204 10.16. Menus....... 205 10.16.1. Το help menu 205 10.17. MenuBar..... 205 10.18. Menu..... 206 10.19. MenuItem..... 206 10.20. CheckboxMenuItem..... 207 10.21. PopupMenu..... 207 10.22. Έλεγχος των οπτικώναπόψεων..... 208 10.22.1. Χρώµατα.. 208 10.22.2. Γραµµατοσειρές. 208 10.23. Εκτύπωση...... 209 10.24. Λυµένες Ασκήσεις.... 211 10.25. Ασκήσεις......... 222 7
11. Εισαγωγή στα Java Applets... 225 11.1. Τι είναι ένα applet... 225 11.1.1. Φόρτωµα ενός applet. 225 11.1.2. Περιορισµοί ασφαλείας σε ένα applet... 225 11.1.3. Παράδειγµα Hello World!.. 225 11.2. Συγγραφή ενός applet... 226 11.2.1. Ιεραρχία κλάσης Applet 226 11.2.2. Βασικές µέθοδοι Applet... 227 11.2.3. Εµφάνιση ενός Applet... 227 11.2.4. Η µέθοδος paint() και τα Graphics... 227 11.3. Μέθοδοι applet και κύκλος ζωής ενός applet... 228 11.3.1. init(). 228 11.3.2. start().. 228 11.3.3. stop().. 228 11.4. Ζωγραφική AWT... 229 11.4.1. Η µέθοδος paint(graphics g).. 229 11.4.2. Η µέθοδος repaint()... 229 11.4.3. Η µέθοδος update(graphics g).. 229 11.5. O appletviewer... 230 11.5.1. Τι είναι ο appletviewer;. 230 11.5.2. Κλήση applets µε τον appletviewer.. 216 11.5.3. appletviewer και ιεραρχία Applet.. 231 11.6. Χρήση appletviewer..... 231 11.6.1. Σύνοψη.. 231 11.6.2. Παράδειγµα.. 232 11.7. Η tag applet..... 232 11.7.1. Σύνταξη. 232 11.7.2. Περιγραφή. 232 11.8. Επιπρόσθετες διευκολύνσεις Applet..... 234 11.9. Μία απλή δοκιµή Image..... 234 11.10. AudioClips... 235 11.10.1. Παίζοντας ένα clip 235 11.11. Μία απλή δοκιµή ήχου..... 235 11.12. Επανάληψη µε ένα audio clip.... 236 11.12.1. Φόρτωση ενός Audio Clip.. 236 11.12.2. Παίξιµο ενός Audio Clip. 236 11.12.3. Σταµάτηµα ενός Audio Clip.. 236 11.13. Μία απλή δοκιµή επανάληψης...... 236 11.14. Είσοδος από το ποντίκι..... 237 11.15. Μία απλή δοκιµή µε το ποντίκι..... 237 11.16. Ανάγνωση παραµέτρων..... 238 11.17. Κώδικας που λειτουργεί µε δύο τρόπους..... 239 11.17.1. Applet/Εφαρµογή... 239 11.18. Λυµένες Ασκήσεις... 241 11.19. Ασκήσεις..... 250 Παράρτηµα Α... 253 Χρήσιµες µέθοδοι στη JAVA... 253 8
Βιβλιογραφία... 255 Ηλεκτρονική Βιβλιογραφία... 255 9
10
1. Εισαγωγή 1.1. Μια νέα γλώσσα προγραµµατισµού Οι εφαρµογές υπολογιστών που αναπτύσσονται τα τελευταία χρόνια, οποιοδήποτε και αν είναι το αντικείµενό τους και το είδος χρηστών τους, σχεδόν σίγουρα θα τρέχουν σε πολλές µηχανές, συνδεδεµένες σε κάποιο τοπικό ή ευρύτερο δίκτυο διασύνδεσης. Η αυξανόµενη σηµασία των δικτύων υπολογιστών θέτει νέες απαιτήσεις από τα προγραµµατιστικά εργαλεία και δηµιουργεί απαιτήσεις για ένα νέο και συνεχώς αυξανόµενο σύνολο εφαρµογών. Το αναπτυσσόµενο λογισµικό απαιτείται συνήθως να λειτουργεί σωστά σε πολλά διαφορετικά περιβάλλοντα και αρχιτεκτονικές υπολογιστών, καθώς και να συνεργάζεται µε άλλες εφαρµογές. Το λογισµικό πρέπει να εκµεταλλεύεται τις δυνατότητες της σύγχρονης δικτυακής δοµής υπολογιστικών συστηµάτων και να είναι σε θέση να προσπελαύνει τις κατανεµηµένες πηγές πληροφοριών, να συνδυάζει τις αντλούµενες πληροφορίες και να τις παρουσιάζει στο χρήστη σε κατάλληλη µορφή. Το πρόβληµα που δηµιουργείται είναι ότι οι απαιτήσεις για ταχύτητα και µεταφερσιµότητα είναι συνήθως αντικρουόµενες, ενώ στο θέµα της ασφάλειας δεν έχει δοθεί η απαραίτητη σηµασία. Υπάρχουν γλώσσες προγραµµατισµού οι οποίες είναι µεν µεταφέρσιµες αλλά και αργές, λόγω του ότι τα προγράµµατα ερµηνεύονται αντί να µεταγλωττίζονται. Οι γλώσσες αυτές είναι αρκετά διαδεδοµένες, τόσο λόγω της υψηλή λειτουργικότητάς τους όσο και τη µεταφερσιµότητάς τους. Επίσης, υπάρχουν γλώσσες προγραµµατισµού οι οποίες είναι γρήγορες, αλλά η ταχύτητά τους απορρέει από το ότι είναι σχεδιασµένες για συγκεκριµένες υπολογιστικές αρχιτεκτονικές. Από µεθοδολογικής πλευράς η ανάπτυξη λογισµικού τα τελευταία χρόνια έχει προσανατολιστεί κυρίως προς τον αντικειµενοστρεφή προγραµµατισµό (object-oriented programming), ο οποίος επιχειρεί να δαµάσει τη συνεχώς αυξανόµενη πολυπλοκότητα ανάπτυξης λογισµικού. Σύµφωνα µε τον αντικειµενοστρεφή προγραµµατισµό, το λογισµικό δοµείται σε αυτόνοµες µονάδες οι οποίες έχουν σαφή λειτουργικότητα και διαπροσωπεία. Η Java είναι µια νέα γλώσσα προγραµµατισµού και επιχειρεί να δώσει λύση στα προβλήµατα που αναφέρθηκαν παραπάνω. Αναπτύχθηκε πρόσφατα από την εταιρεία Sun και στο λίγο χρόνο από την ανάπτυξή της έχει γνωρίσει αρκετά µεγάλη διάδοση. Αρχικά ήταν προσανατολισµένη στην ανάπτυξη λογισµικού για ηλεκτρονικές συσκευές οικιακής χρήσης, στη συνέχεια όµως εξελίχθηκε σε µια ολοκληρωµένη γλώσσα, η οποία έχει αρκετά από τα χαρακτηριστικά των µοντέρνων γλωσσών προγραµµατισµού και υποστηρίζει τον αντικειµενοστρεφή προγραµµατισµό. Η επιτυχία της γλώσσας έγκειται στο ότι µπορεί να χρησιµοποιηθεί για προγραµµατισµό ασφαλών, υψηλής απόδοσης εφαρµογών σε Internet, οι οποίες µπορούν να τρέξουν αυτούσιες σε διαφορετικά προγραµµατιστικά περιβάλλοντα και αρχιτεκτονικές, καθώς και ότι παρέχει τη δυνατότητα µεταφοράς δυναµικού περιεχοµένου σε εφαρµογές πολυµέσων. 11
1.2. Χαρακτηριστικά της Java Η εταιρεία Sun περιγράφει τη Java ως µια απλή, αντικειµενοστρεφή, κατανεµηµένη, ερµηνευόµενη, συµπαγή, ασφαλή, ανεξάρτητη αρχιτεκτονικής, µεταφέρσιµη, υψηλής απόδοσης, υποστηρίζουσα πολλαπλά νήµατα, και δυναµική γλώσσα. Επίσης, η Java υποστηρίζει εφαρµογές πολυµέσων. Όµως, η έννοια που αποδίδεται στα περισσότερα από αυτά τα χαρακτηριστικά δεν είναι προφανής. Ας τα εξετάσουµε ένα προς ένα. Απλή: Η Java είναι µια απλή (simple) γλώσσα, µε την έννοια ότι η εκµάθησή της είναι σχετικά εύκολη. Περιέχει λίγες προγραµµατιστικές δοµές µε καλά ορισµένη σηµασιολογία. Μοιάζει αρκετά µε τις ευρέως διαδεδοµένες γλώσσες προγραµµατισµού C και C++ και, ως εκ τούτου, η µετάβαση από αυτές τις γλώσσες στη Java είναι αρκετά εύκολη. Ορισµένα χαρακτηριστικά των C και C++ έχουν αφαιρεθεί από τη Java, όπως η διαχείριση µνήµης από τον προγραµµατιστή. Άλλα πάλι χαρακτηριστικά υπάρχουν στη Java από το σχεδιασµό της, όπως για παράδειγµα η αυτόµατη διαχείριση της µνήµης, ο χειρισµός πολλαπλών νηµάτων εκτέλεσης, κ.λπ. Αντικειµενοστρεφής: Ως αντικειµενοστρεφής (object-oriented) γλώσσα, η Java εστιάζει τη δραστηριότητα του προγραµµατιστή στον ορισµό αντικειµένων και λειτουργιών πάνω σε αυτά. Στη Java ιδιαίτερη σηµασία έχει η έννοια της κλάσης. Μια κλάση περιγράφει µια συλλογή δεδοµένων καθώς και τις λειτουργίες που αυτά επιδέχονται. Κάθε κλάση προέρχεται από κάποια άλλη ήδη ορισµένη κλάση µέσω κληρονοµικότητας (inheritance). Κατ αυτό τον τρόπο, ορίζεται µια ιεραρχία κλάσεων, η οποία έχει στην κορυφή της τη βασική κλάση Object. Μια κλάση είναι απλά µια περιγραφή της µορφής των αντικειµένων τα οποία περιέχει. Αντικείµενα µιας κλάσης µπορούν να οριστούν και να χρησιµοποιηθούν σε ένα πρόγραµµα Java. Κάθε αντικείµενο δηµιουργείται κατά τη διάρκεια εκτέλεσης ενός προγράµµατος Java και υπάρχει µέχρι να καταστραφεί. Κατανεµηµένη: Η Java χαρακτηρίζεται ως κατανεµηµένη (distributed) γλώσσα προγραµµατισµού γιατί υποστηρίζει την ανάπτυξη κατανεµηµένων εφαρµογών δικτύων. Συγκεκριµένα, η Java επιτρέπει την προσπέλαση αντικειµένων που βρίσκονται σε αποµακρυσµένες θέσεις στο δίκτυο, καθώς και τη δικτυακή επικοινωνία µε άλλες εφαρµογές. Ερµηνευόµενη: Η Java είναι ερµηνευόµενη (interpreted) γλώσσα. Ο µεταγλωττιστής δεν παράγει τελικό κώδικα, για κάποιο συγκεκριµένο υπολογιστή, αλλά ενδιάµεσο κώδικα σε µορφή bytes, που ονοµάζεται bytecode. Ο ενδιάµεσος κώδικας στη συνέχεια εκτελείται από ένα διερµηνέα (interpreter) της Java. Το πλεονέκτηµα είναι ότι ο κώδικας µπορεί να εκτελεστεί σε πολλά διαφορετικά περιβάλλοντα υπολογιστών, αν φυσικά κάποιος διερµηνέας Java είναι διαθέσιµος σε αυτά. Ο διερµηνέας της γλώσσας µαζί µε το σύστηµα εκτέλεσης προγραµµάτων (runtime system) υλοποιεί µια εικονική µηχανή Java (Java Virtual Machine, JVM). Ο διερµηνέας έχει αναπτυχθεί για αρκετά περιβάλλοντα, συµβάλλοντας έτσι στη µεταφερσιµότητα του εκτελέσιµου κώδικα της γλώσσας. Προγράµµατα Java µπορούν επίσης να εκτελεστούν από τους περισσότερους σύγχρονους περιηγητές του WWW, καθένας από τους οποίους έχει ενσωµατωµένη µια εικονική µηχανή Java για την αναγνώριση και εκτέλεση του ενδιάµεσου κώδικα. Συµπαγής: Η Java έχει σχεδιαστεί ώστε να είναι δυνατή η ανάπτυξη συµπαγούς (compact) και αξιόπιστου λογισµικού. Η γλώσσα έχει ένα ισχυρό σύστηµα τύπων, το 12
οποίο επιτρέπει εκτενείς ελέγχους κατά τη διάρκεια της µετάφρασης των προγραµµάτων, βοηθώντας έτσι την ανάπτυξη αξιόπιστου λογισµικού. Ένα άλλο στοιχείο το οποίο βοηθάει την ανάπτυξη αξιόπιστου λογισµικού είναι το µοντέλο µνήµης. Η Java δεν παρέχει στον προγραµµατιστή εντολές για παραχώρηση και αποδέσµευση µνήµης, ή χρησιµοποίηση δεικτών προς τη µνήµη. Η διαχείριση της µνήµης γίνεται εξ ολοκλήρου από το σύστηµα εκτέλεσης. Κατά τη δηµιουργία ενός αντικειµένου, το σύστηµα εκτέλεσης αναλαµβάνει να δεσµεύσει το απαραίτητο ποσό µνήµης για την αποθήκευση του αντικειµένου. Επιπλέον, όταν το σύστηµα εκτέλεσης εντοπίσει κάποιο αντικείµενο το οποίο είναι αδύνατο να προσπελαστεί από οποιοδήποτε σηµείο του εκτελούµενου προγράµµατος, αναλαµβάνει να αποδεσµεύσει τη µνήµη που καταλαµβάνει το αντικείµενο και να την κάνει διαθέσιµη στο υπόλοιπο πρόγραµµα. Ασφαλής: Η Java είναι µια ασφαλής (safe) γλώσσα. Προγράµµατα τα οποία έχουν αναπτυχθεί µε αυτήν µπορούν να εκτελεστούν από πολλούς χρήστες µε διαφορετικά δικαιώµατα πρόσβασης στο σύστηµα, χωρίς να είναι δυνατό να υπάρξουν ανεπιθύµητες παρενέργειες, εκούσιες ή ακούσιες. Αυτό επιτυγχάνεται µε ένα ειδικό πρόγραµµα που ονοµάζεται ελεγκτής ενδιάµεσου κώδικα (bytecode verifier), ο οποίος ερευνά τον ενδιάµεσο κώδικα και εντοπίζει ύποπτα σηµεία, των οποίων η εκτέλεση θα µπορούσε να προκαλέσει ανεπιθύµητες ενέργειες στο υπολογιστικό περιβάλλον του χρήστη. Αυτό είναι αναγκαίο να γίνει, καθ όσον ο ενδιάµεσος κώδικας µπορεί να παραχθεί µε πολλούς τρόπους και όχι απαραίτητα µε µετάφραση πηγαίου κώδικα από έναν έµπιστο µεταφραστή. Ανεξάρτητη αρχιτεκτονικής: Τα προγράµµατα Java µεταφράζονται σε ενδιάµεσο κώδικα, ο οποίος δεν είναι προσανατολισµένος σε µια συγκεκριµένη αρχιτεκτονική ή τύπο υπολογιστή. Ο κώδικας είναι µεταφέρσιµος, µια και µπορεί να εκτελεστεί από διερµηνείς της γλώσσας που έχουν αναπτυχθεί για πολλές αρχιτεκτονικές και περιβάλλοντα υπολογιστών. Για παράδειγµα, τέτοιοι διερµηνείς έχουν αναπτυχθεί για µηχανές της Sun και της IBM, καθώς και για τα λειτουργικά συστήµατα Unix, Windows 95, Windows NT, κ.λ.π. Μεταφέρσιµη: Το γεγονός ότι η Java είναι ανεξάρτητη αρχιτεκτονικής καθιστά τα προγράµµατα Java µεταφέρσιµα. Οι προδιαγραφές της Java καθορίζουν σαφώς τα µεγέθη των διαφόρων πρωταρχικών τύπων δεδοµένων καθώς και τα αποτελέσµατα των διαφόρων λειτουργιών πάνω σε µεταβλητές αυτών των τύπων. Κατά συνέπεια, ένα πρόγραµµα Java θα δώσει τα ίδια αποτελέσµατα όταν δεχθεί τα ίδια δεδοµένα, ανεξάρτητα της αρχιτεκτονικής στην οποία θα εκτελεστεί. Υψηλής απόδοσης: Η Java σαν ερµηνευόµενη γλώσσα δεν µπορεί να φτάσει την απόδοση γλωσσών προγραµµατισµού όπως η C και η C++, που υλοποιούνται σε µεταγλωττιστές. Παρ όλα αυτά, η απόδοση της γλώσσας είναι αρκετά υψηλή. Κατά µέσο όρο η εκτέλεση προγραµµάτων Java είναι περίπου 20% αργότερη από την εκτέλεση των αντίστοιχων προγραµµάτων σε C ή C++. Σε µερικές περιπτώσεις όµως, η απόδοση θεωρείται σηµαντικός παράγοντας. Γι αυτό το λόγο έχουν αναπτυχθεί µεταγλωττιστές τελευταίας στιγµής (just-in-time compilers), που εµπεριέχονται στους διερµηνείς και µεταφράζουν τον ενδιάµεσο κώδικα σε κώδικα εκτελέσιµο από τη συγκεκριµένη µηχανή. Το αποτέλεσµα είναι σηµαντική βελτίωση της απόδοσης εκτέλεσης προγραµµάτων. Υποστηρίζουσα πολλαπλά νήµατα: Η Java υποστηρίζει πολλαπλά νήµατα εκτέλεσης (multi threaded). Τα πολλαπλά νήµατα εκτέλεσης αποτελούν ένα προγραµµατιστικό µοντέλο για ανάπτυξη λογισµικού, που απαιτεί την ταυτόχρονη 13
εκτέλεση πολλών διεργασιών. Οι διεργασίες µοιράζονται τον ίδιο χώρο µνήµης για την εκτέλεσή τους και τη διαχείριση δεδοµένων. Τα πολλαπλά νήµατα εκτέλεσης αποτελούν επίσης και µια αποδοτική λύση σε πολλά προβλήµατα, αφού συνήθως η εναλλαγή εκτέλεσης ανάµεσα στα διάφορα νήµατα δεν είναι ιδιαίτερα δαπανηρή. Παραθυρικές εφαρµογές πού χρησιµοποιούν πολλαπλά νήµατα εκτέλεσης παρουσιάζουν βελτιωµένη απόκριση προς το χρήστη και, από προγραµµατιστικής άποψης, είναι συνήθως ευκολότερες να αναπτυχθούν. Η υποστήριξη πολλαπλών νηµάτων εκτέλεσης απαιτεί την ύπαρξη µηχανισµών συγχρονισµού µεταξύ των νηµάτων. Αυτό είναι απαραίτητο προκειµένου να αποφεύγεται η ταυτόχρονη προσπέλαση του ιδίου αντικειµένου, κάτι που επιβάλλεται συνήθως στις περισσότερες εφαρµογές. υναµική: Η Java είναι µια δυναµική (dynamic) γλώσσα, η οποία έχει σχεδιαστεί για προσαρµογή σε ένα δυναµικά εξελισσόµενο περιβάλλον. Κλάσεις µπορούν να µεταφερθούν δυναµικά από άλλα µέρη του δικτύου και να εκτελεστούν τοπικά. Κατ αυτό τον τρόπο, προγράµµατα Java µπορούν να µεταφέρουν και να εκτελέσουν κλάσεις από άλλα µέρη του δικτύου και δεν είναι απαραίτητο αυτές οι κλάσεις να ενσωµατωθούν στο πρόγραµµα κατά τη διάρκεια της µετάφρασής του. Υποστήριξη πολυµέσων: Η Java καθιστά δυνατή τη µεταφορά εκτελέσιµου περιεχοµένου σε εφαρµογές πολυµέσων. Πριν τη Java, οι εφαρµογές πολυµέσων περιείχαν κείµενο, ακίνητη ή κινούµενη εικόνα, ήχο, κ.λ.π. Αυτό πού έλειπε ήταν η δυνατότητα εκτέλεσης προγραµµάτων στο περιβάλλον του χρήστη. Αυτό το κενό καλύπτεται από τη Java, η οποία επιτρέπει να µεταφερθεί εκτελέσιµος κώδικας και να εκτελεσθεί στον περιηγητή του χρήστη. Ένα πλεονέκτηµα είναι ότι κατ αυτό τον τρόπο οι δυνατότητες των περιηγητών µπορούν να επεκταθούν χωρίς περιορισµούς, κάτι που απορρέει επίσης από το ότι η γλώσσα είναι δυναµική. 1.3. Java και World Wide Web To Internet έχει εξελιχθεί ραγδαία σε έναν άµορφο ωκεανό από δεδοµένα, αποθηκευµένα µε πολλές διαφορετικές κωδικοποιήσεις σε ένα πλήθος υπολογιστών. Κατά καιρούς, διάφορα πρωτόκολλα αποθήκευσης και µεταφοράς δεδοµένων έχουν αναπτυχθεί µε σκοπό να επιβάλλουν κάποια µορφή τάξης σε αυτό το χάος. Μια από τις ταχύτερα αναπτυσσόµενες περιοχές του Internet είναι το WWW (World Wide Web), το οποίο χρησιµοποιεί ένα σύστηµα οργάνωσης και κωδικοποίησης των πληροφοριών σε υπερκείµενα (hypertext), προκειµένου να διευκολύνει την πλοήγηση των χρηστών µέσα στον ωκεανό των δεδοµένων. Η έννοια του υπερκειµένου δεν είναι κατά κανένα τρόπο νέα, αλλά η πραγµάτωση της χρειάστηκε αρκετές δεκαετίες. Η πραγµατική δύναµη του υπερκειµένου αποκαλύφθηκε πρόσφατα, µε την προσθήκη της δυνατότητας δηµιουργίας υπερσυνδέσµων µεταξύ κειµένων που βρίσκονται σε διαφορετικούς υπολογιστές, συνδεδεµένους µέσω δικτύου. Η πρώτη πρακτική, αν και απλοϊκή, υλοποίηση ενός συστήµατος υπερµέσων (hypermedia) σε περιβάλλον δικτύου αναπτύχθηκε το 1945 από τον Tim Berners-Lee στο CERN, στα τέλη της δεκαετίας του 1980. Μέσα σε λίγα χρόνια αναπτύχθηκε η γλώσσα περιγραφής υπερκειµένων HTML (Hyper Text Markup Language), το πρωτόκολλο µεταφοράς δεδοµένων HTTP (Hyper Text Transport Protocol) και τελικά το WWW. Οι περιηγητές (browsers) του WWW είναι τα προγράµµατα που αναλαµβάνουν την περιήγηση των χρηστών στο σύστηµα υπερκειµένων. Εκτός από τη λειτουργία της προσκόµισης δεδοµένων στο χρήστη, οι περιηγητές αντιλαµβάνονται το είδος των 14
δεδοµένων ώστε να µπορέσουν να τα επιδείξουν µε το σωστό τρόπο, αν αυτό είναι δυνατό. Η πιο συχνή κωδικοποίηση που συναντάται σήµερα στο WWW είναι φυσικά αρχεία κειµένου, που περιέχουν εντολές µορφοποίησης στη γλώσσα HTML. Τα αρχεία αυτά είναι γνωστά και ως σελίδες HTML, λόγω της απεικόνισής τους από τους περιηγητές ως µορφοποιηµένες σελίδες. H δύναµη της HTML δεν περιορίζεται στη δυνατότητα µορφοποίησης του κειµένου, αλλά και στον απλό τρόπο ορισµού υπερσυνδέσµων προς άλλα δεδοµένα που βρίσκονται αποθηκευµένα σε υπολογιστές του Internet. Παρά το γεγονός ότι µια µεγάλη ποικιλία διαφορετικών µορφών εγγράφων µπορούν να περιέχονται στις σελίδες HTML, οι σελίδες αυτές στερούνται ακόµα δυναµικής συµπεριφοράς. Τα στοιχεία που περιέχονται σε αυτές είναι παθητικά: πρόκειται για ηλεκτρονικά έγγραφα που µπορεί κανείς να διαβάσει, να δει, να ακούσει, να παρακολουθήσει, αλλά δεν είναι σε θέση να αλληλεπιδρούν µε τον αναγνώστη, εκτός από αυτό τον προφανή τρόπο. Η ενσωµάτωση εκτελέσιµων προγραµµάτων στις σελίδες HTML είναι κάτι που επιζητείται εδώ και πολλά χρόνια, εµποδίζεται όµως από µια σειρά δύσκολων προβληµάτων, από τα οποία σηµαντικότερα είναι τα εξής δυο: Μεταφερσιµότητα: Ο εκτελέσιµος κώδικας θα πρέπει να είναι σε µια µορφή που να αναγνωρίζεται από κάθε περιβάλλον υπολογιστή, για τον οποίο υπάρχουν περιηγητές. Ασφάλεια: Πρέπει να εξασφαλίζεται ότι η εκτέλεση του κώδικα δεν είναι δυνατό να βλάψει, εκούσια ή ακούσια, το υπολογιστικό σύστηµα στο οποίο τρέχει ο περιηγητής. Η γλώσσα Java αποτελεί µια λύση στο πρόβληµα δυναµικής συµπεριφοράς, επιχειρώντας να ξεπεράσει αυτά τα δυο προβλήµατα, µε αρκετά µεγάλη επιτυχία. Για το λόγο αυτό, η Java συνδέεται άµεσα µε το WWW. 1.4. Εκτέλεση προγραµµάτων Στο Σχήµα 1.1 απεικονίζεται η διαδικασία µεταγλώττισης και εκτέλεσης προγραµµάτων Java, πολλά στοιχεία της οποίας έχουν ήδη αναφερθεί στη συζήτηση για τα χαρακτηριστικά της γλώσσας. Στο σχήµα αυτό, µε γκρι χρώµα παριστάνονται τα τµήµατα του συστήµατος που σχετίζονται µε την υλοποίηση της γλώσσας Java. Τα τµήµατα αυτά µπορεί κανείς να τα προµηθευθεί στο εµπόριο ή στο Internet. Στο πρώτο στάδιο γίνεται η µεταγλώττιση του πηγαίου κώδικα Java, που έχει γράψει ο προγραµµατιστής. Για το σκοπό αυτό χρησιµοποιείται ο µεταγλωττιστής Java (Java compiler), που µεταφράζει τον πηγαίο κώδικα σε ενδιάµεσο κώδικα (bytecode). Ο ενδιάµεσος κώδικας αποθηκεύεται σε ένα ή περισσότερα αρχεία, τα οποία είναι δυνατό να είτε να χρησιµοποιηθούν τοπικά, είτε να µεταφερθούν σε άλλους υπολογιστές µέσω του δικτύου. Στο δεύτερο στάδιο γίνεται η εκτέλεση του ενδιάµεσου κώδικα από την εικονική µηχανή της Java (Java Virtual Machine), που φαίνεται διαγραµµισµένη στο Σχήµα 1.1, µέσα σε πλαίσιο. Αν και ο διερµηνέας της Java δεν είναι παρά ένα µόνο τµήµα της εικονικής µηχανής, συχνά όλη η µηχανή ονοµάζεται καταχρηστικά διερµηνέας Java. Το πρώτο τµήµα της εικονικής µηχανής είναι ο φορτωτής κλάσεων (class loader), που αναλαµβάνει το φόρτωµα του ενδιάµεσου κώδικα που υλοποιεί κάποια ζητούµενη κλάση. Στην περίπτωση που αυτή η κλάση δε βρίσκεται τοπικά, στην βιβλιοθήκη κλάσεων της Java, ο φορτωτής κλάσεων αναλαµβάνει τη µεταφορά της µέσω του δικτύου. Αµέσως µετά το φόρτωµα µιας κλάσης, ακολουθεί ο έλεγχος 15
ορθότητας του κώδικά της, που πραγµατοποιείται από τον ελεγκτή ενδιάµεσου κώδικα (bytecode verifier). Αυτός εξασφαλίζει ότι ο ενδιάµεσος κώδικας που φορτώθηκε δε θα θέσει σε κίνδυνο την ασφάλεια του υπολογιστικού συστήµατος. Σχήµα 1.1 Στη συνέχεια, ακολουθεί η κυρίως φάση της εκτέλεσης του ενδιάµεσου κώδικα, που πραγµατοποιείται εναλλακτικά από το διερµηνέα (interpreter) της Java, ή τον µεταγλωττιστή τελευταίας στιγµής (just-in-time compiler). Και τα δυο αυτά τµήµατα επικοινωνούν µε το σύστηµα χρόνου εκτέλεσης (runtime system), που αναλαµβάνει την κατασκευή και την καταστροφή αντικειµένων, τη διαχείριση της µνήµης, και άλλες σηµαντικές λειτουργίες. Το σύστηµα χρόνου εκτέλεσης αλληλεπιδρά µε το λειτουργικό σύστηµα (operating system) του υπολογιστή, µέσω του οποίου αποκτά πρόσβαση στο υλικό (hardware) και επικοινωνεί µε το χρήστη. 1.5. Είδη προγραµµάτων Java Τα προγράµµατα που αναπτύσσονται σε Java µπορούν να εκτελεστούν είτε ως αυτόνοµες εφαρµογές, είτε ως τµήµατα σελίδων HTML, µέσα από έναν περιηγητή. Παρακάτω περιγράφουµε τα δύο αυτά είδη προγραµµάτων. Αυτόνοµες εφαρµογές µπορούν να εκτελεστούν απ ευθείας από το διερµηνέα της γλώσσας. Αυτό φυσικά προϋποθέτει την ύπαρξη του διερµηνέα, αλλά διερµηνείς της Java έχουν αναπτυχθεί γιά πολλά περιβάλλοντα. Από προγραµµατιστικής πλευράς ο προγραµµατιστής αρκεί να ορίσει µια κλάση η οποία να περιέχει µια µέθοδο µε όνοµα main. Όταν ο διερµηνέας κληθεί µε το όνοµα αυτής της κλάσης, η εκτέλεση θα αρχίσει από αυτή τη µέθοδο. Εφαρµογές που εκτελούνται µέσω ενός περιηγητή ονοµάζονται applets. Ο περιηγητές πρέπει να είναι σε θέση να αναγνωρίσει και να εκτελέσει ενδιάµεσο 16
κώδικα Java. Ένα applet είναι ένα αντικείµενο που προέρχεται από κάποια υποκλάση της Applet. Σελίδες HTML µπορούν να περιέχουν applets, που µπορούν να µεταφέρονται µέσω του δικτύου και να εκτελούνται. Οι σελίδες προσδιορίζουν τις συντεταγµένες της οθόνης όπου θα εµφανιστεί και θα εκτελεστεί το applet. Ένα applet περιέχει ορισµένες µεθόδους που καλούνται όταν συµβούν κάποια γεγονότα, όπως π.χ. το πάτηµα του πλήκτρου του ποντικιού πάνω στο applet. 1.6. Ένα απλό παράδειγµα κώδικα Java Στη συνέχεια δίνεται ένα παράδειγµα προγράµµατος Java. Το πρόγραµµα είναι ένα από τα απλούστερα που µπορούν να γραφούν και εµφανίζει στο χρήστη το µήνυµα Hello world!. Στις επόµενες δυο παραγράφους δίνεται και εξηγείται ο κώδικας του προγράµµατος, κατ αρχήν ως ανεξάρτητη εφαρµογή και στη συνέχεια ως applet. 1.6.1. Αυτόνοµη εφαρµογή Java Ο κώδικας του απλού προγράµµατος Java που θα αποτελέσει αυτόνοµη εφαρµογή είναι ακόλουθος: public class HelloWorld public static void main (String [] args) System.out.println("Hello world!"); Όπως κάθε πρόγραµµα Java, το παραπάνω αποτελείται από τη δήλωση µιας κλάσης. Το όνοµα της κλάσης που ορίζεται εδώ είναι HelloWorld. Η κλάση περιέχει τον ορισµό µιας µεθόδου µε όνοµα main. Το σώµα της main περιέχει µια µόνο εντολή, που εκτυπώνει το µήνυµα Hello World!. Η σύνταξη του προγράµµατος θυµίζει τη σύνταξη προγραµµάτων C και C++. Οι υπόλοιπες λεπτοµέρειες του προγράµµατος θα γίνουν κατανοητές στα επόµενα κεφάλαια. Τα προγράµµατα Java αποθηκεύονται σε αρχεία, των οποίων το όνοµα τελειώνει σε.java. Ας υποθέσουµε ότι ο παραπάνω κώδικας είναι αποθηκευµένος σε ένα αρχείο µε όνοµα hello.java. Προκειµένου να εκτελεσθεί το πρόγραµµα, το επόµενο βήµα είναι η εκτέλεση της εντολής: javac hello.java µε την οποία καλείται ο µεταγλωττιστής Java, το όνοµα του οποίου είναι javac. Ο µεταγλωττιστής ελέγχει την ορθότητα του προγράµµατος. Αν αυτό είναι σωστό, τότε δηµιουργείται ένα αρχείο µε όνοµα HelloWorld.class, το οποίο περιέχει τον ενδιάµεσο εκτελέσιµο κώδικα της κλάσης HelloWorld. Ένα ενδιαφέρον σηµείο είναι ότι το όνοµα του παραγόµενου αρχείου προέρχεται από το όνοµα της κλάσης που ορίζεται και όχι από το όνοµα του αρχείου που περιέχει τον ορισµό της. Το αρχείο HelloWorld.class περιέχει ενδιάµεσο κώδικα και χρειάζεται τον διερµηνέα της Java για να εκτελεστεί. Με την εντολή: java HelloWorld εκτελείται το αρχείο HelloWorld.class και το µήνυµα τυπώνεται στη οθόνη. Στην παραπάνω εντολή, java είναι το όνοµα του διερµηνέα της Java. Ο διερµηνέας δέχεται ως παράµετρο το όνοµα µιας κλάσης και όχι το όνοµα του αρχείου που την περιέχει. Με άλλα λόγια, ο διερµηνέας της Java εκτελεί κλάσεις ξεκινώντας πάντα από τη µέθοδο 17
main. Το αρχείο HelloWorld.class µπορεί να µεταφερθεί και να εκτελεστεί σε διαφορετικό περιβάλλον υπολογιστή χωρίς να χρειαστεί άλλη επεξεργασία. Το µόνο που απαιτείται είναι η ύπαρξη ενός διερµηνέα της γλώσσας. 1.6.2. Java Applet Ο παρακάτω κώδικας υλοποιεί το ίδιο πρόγραµµα και αποτελεί παράδειγµα ορισµού κλάσης που θα χρησιµοποιηθεί ως applet: import java.awt.*; import java.applet.*; public class HelloWorld extends Applet public void paint (Graphics g) g.drawstring("hello World!"); Η γενική δοµή του προγράµµατος είναι παρόµοια µε αυτή του πρώτου παραδείγµατος. Οι εντολές import κάνουν διαθέσιµους στο µεταφραστή διάφορους ορισµούς κλάσεων από τη βιβλιοθήκη κλάσεων της Java, όπως π.χ. η κλάση Applet. Η κλάση HelloWorld που ορίζεται στο παράδειγµα κληρονοµεί την κλάση Applet, µε χρήση της λέξης extends. Περιέχει µια µέθοδο µε όνοµα paint, που ζωγραφίζει το µήνυµα Hello World!. Το πρόγραµµα αυτό πρόκειται να εκτελεστεί µέσα από έναν περιηγητή, ο οποίος θα καλεί τη µέθοδο paint κάθε φορά που θα πρέπει να σχεδιάσει το applet στην οθόνη. Η µετάφραση γίνεται όπως στο προηγούµενο παράδειγµα. Ας υποθέσουµε ότι ο παραπάνω κώδικας είναι αποθηκευµένος στο αρχείο hello.java. Με την εντολή: javac hello.java µεταφράζεται ο κώδικας που περιέχεται σε αυτό. Όµως, αυτή τη φορά, το αρχείο HelloWorld.class που προκύπτει δεν µπορεί να εκτελεστεί αυτόνοµα, γιατί δεν περιέχει µέθοδο main. Προκειµένου να εκτελεσθεί, χρειάζεται να ενσωµατωθεί σε µια σελίδα HTML. Ο παρακάτω κώδικας περιγράφει µια απλή σελίδα HTML που περιέχει το applet: <HTML> <HEAD> <TITLE>My first Java applet</title> </HEAD> <BODY> <APPLET CODE=HelloWorld WIDTH=200 HEIGHT=100></APPLET> </BODY> </HTML> Η εντολή <APPLET> προσδιορίζει το applet που περιέχεται, του οποίου το όνοµα δίνεται στην παράµετρο CODE, καθώς και τις διαστάσεις που θα έχει στην σελίδα HTML. Όταν το αρχείο που περιέχει τον παραπάνω κώδικα HTML διαβασθεί από έναν περιηγητή µε δυνατότητα εκτέλεσης Java applets, όπως π.χ. ο Netscape Navigator, το µήνυµα Hello world! θα εµφανιστεί στη σελίδα HTML. 18
2. Τα βασικά της γλώσσας 2.1. Κώδικας προγραµµάτων Η γλώσσα Java προέρχεται από την κοινότητα του Internet. Προκειµένου να καλύψει τις ανάγκες αυτής της κοινότητας, η οποία απαρτίζεται από άτοµα διαφορετικών εθνοτήτων που µιλούν διαφορετικές γλώσσες, η Java πρέπει να είναι σε θέση να χειρίζεται έναν µεγάλο αριθµό γλωσσών. Για το λόγο αυτό, η Java υποστηρίζει το διεθνές σύστηµα κωδικοποίησης χαρακτήρων Unicode (βλ. http://www.unicode.org/). Τα αρχεία προγραµµάτων Java µπορούν να είναι γραµµένα σε Unicode. Αυτό φυσικά δεν αποκλείει τη χρήση απλών χαρακτήρων ASCII, που αποτελούν ούτως ή άλλως τµήµα του συστήµατος Unicode. 2.2. Σχόλια Η Java υποστηρίζει δυο ειδών σχόλια. Το πρώτο είδος είναι αυτό της γλώσσας C. Τα σχόλια αυτά αποτελούνται από µια ή περισσότερες γραµµές κειµένου και περικλείονται από τις ακολουθίες χαρακτήρων /* και */ όπως στο ακόλουθο παράδειγµα: /* This is a first comment */ /* This is a second comment that is written in three lines */ Τα σχόλια αυτού του είδους δεν µπορούν να είναι φωλιασµένα, δηλαδή δεν επιτρέπονται σχόλια µέσα σε σχόλια. Το δεύτερο είδος είναι αυτό των σχολίων µιας γραµµής της γλώσσας C++. Τα σχόλια αυτά αρχίζουν µε την ακολουθία χαρακτήρων // και εκτείνονται ως το τέλος της γραµµής. Παραδείγµατα τέτοιων σχολίων είναι τα: // This is a first single line comment // And this is a // second one // Κατά σύµβαση, ένα σχόλιο του πρώτου είδους που αρχίζει µε την ακολουθία χαρακτήρων /** επισηµαίνει την ύπαρξη πληροφοριών τεκµηρίωσης. Κατάλληλα προγράµµατα που ονοµάζονται γεννήτορες τεκµηρίωσης, όπως το javadoc της Sun, επεξεργάζονται τέτοια σχόλια και κατασκευάζουν αυτόµατα την τεκµηρίωση του κώδικα. Στο εσωτερικό τέτοιου είδους σχολίων, τυχόν κενά στην αρχή των γραµµών ακολουθούµενα από το χαρακτήρα * αγνοούνται από το γεννήτορα. Επίσης, γραµµές που αρχίζουν µε το χαρακτήρα @ ερµηνεύονται ως ειδικές οδηγίες προς το γεννήτορα. Παράδειγµα σχολίου τεκµηρίωσης είναι αυτό που ακολουθεί: /** * This class represents a client of the system. * 19
* @author Antreas Gewrgiou * @version 1.0 alpha, January 13, 2020 * @see Server */ Ανάλογα µε το γεννήτορα που χρησιµοποιείται, τα σχόλια τεκµηρίωσης µπορούν επίσης να περιέχουν οδηγίες σε κατάλληλη γλώσσα µορφοποίησης (π.χ. HTML). Οι οδηγίες αυτές ενσωµατώνονται στην τεκµηρίωση από το γεννήτορα. 2.3. Τύποι δεδοµένων και σταθερές Το σύστηµα τύπων µιας γλώσσας προγραµµατισµού αποσκοπεί στην πρόληψη σφαλµάτων που µπορούν να παρουσιαστούν κατά την εκτέλεση των προγραµµάτων. Η βασική λειτουργία του συστήµατος τύπων είναι η συσχέτιση τύπων µε τις µεταβλητές ενός προγράµµατος και µε τις εκφράσεις που συναντώνται σε αυτό. Το σύστηµα τύπων της Java συνδυάζει χαρακτηριστικά που συναντιούνται σε γλώσσες µε στατικά αλλά και µε δυναµικά συστήµατα τύπων. Είναι στατικό µε την έννοια ότι κάθε µεταβλητή συσχετίζεται µε ένα συγκεκριµένο και µη τετριµµένο τύπο, ο οποίος είναι γνωστός κατά τη µεταγλώττιση. Είναι δυναµικό µε την έννοια ότι κατά το χρόνο εκτέλεσης διατηρούνται πληροφορίες σχετικές µε τον τύπο των αντικειµένων, ώστε να υποστηρίζεται ο πολυµορφισµός µε ασφαλή τρόπο. Οι τύποι στη Java χωρίζονται σε δυο µεγάλες κατηγορίες: τους βασικούς τύπους και τους τύπους αναφοράς. Στην ενότητα αυτή θα γίνει µια εισαγωγή επίσης στον τύπο συµβολοσειράς και στους τύπους πινάκων που, αν και στην πραγµατικότητα είναι τύποι αντικειµένων, χαίρουν ιδιαίτερης µεταχείρισης από τους µεταγλωττιστες της Java. 2.3.1. Βασικοί Τύποι Οι βασικοί τύποι της Java χρησιµοποιούνται για την αναπαράσταση θεµελιωδών τύπων δεδοµένων, όπως είναι οι αριθµοί, οι χαρακτήρες και οι λογικές τιµές. Συνοπτικά δίνονται στον Πίνακα 2.1. Στον ίδιο πίνακα περιγράφονται επίσης οι δυνατές τιµές που µπορούν να πάρουν τα αντίστοιχα δεδοµένα. Αξίζει να σηµειωθεί ότι όλοι οι τύποι ακεραίων αφορούν προσηµασµένους αριθµούς σε αναπαράσταση συµπληρώµατος ως προς 2. Επίσης, οι αριθµοί κινητής υποδιαστολής ακολουθούν το πρότυπο IEEE 754. Τύπος Περιγραφή Αρχική τιµή boolean Λογικές τιµές: true ή false false char Χαρακτήρες, 16-bit Unicode '\0' byte Ακέραιος αριθµός 8-bit 0 short Ακέραιος αριθµός 16-bit 0 int Ακέραιος αριθµός 32-bit 0 long Ακέραιος αριθµός 64-bit 0 float Αριθµός κινητής υποδιαστολής 32-bit 0.0 double Αριθµός κινητής υποδιαστολής 64-bit 0.0 Πίνακας 2.1. Βασικοί τύποι της Java. 20
Μεταβλητές µπορούν να ορισθούν στο εσωτερικό µεθόδων ή κλάσεων (που θα περιγραφούν σε επόµενο κεφάλαιο) µε τρόπο παρόµοιο όπως στη γλώσσα C. Για παράδειγµα, ο κώδικας: int x; double d1, d2; δηλώνει τρεις µεταβλητές µε ονόµατα x, d1 και d2. Η πρώτη έχει τύπο int, ενώ οι δυο επόµενες έχουν τύπο double. Επίσης, οι µεταβλητές µπορούν να αρχικοποιηθούν κατά τη δήλωσή τους, όπως για παράδειγµα στον παρακάτω κώδικα: int x = 1; double d1 = 3.14, d2 = -8.0; Αν µια µεταβλητή δηλωθεί χωρίς να αρχικοποιηθεί, τότε η αρχικοποίηση γίνεται συχνά µε αυτόµατο τρόπο. Οι τιµές που χρησιµοποιούνται για τις αυτόµατες αρχικοποιήσεις δίνονται στην τελευταία στήλη του Πίνακα 2.1. Οι µοναδικές τιµές του τύπου boolean είναι οι σταθερές true και false. Οι σταθερές τύπου χαρακτήρα είτε περιέχονται µεταξύ απλών εισαγωγικών, είτε δίνονται βάσει του κωδικού Unicode σε δεκαεξαδική µορφή. Με τη χρήση του ειδικού χαρακτήρα \ (backslash) προκύπτουν οι ειδικές ακολουθίες διαφυγής (escape sequences), που έχουν θέση ενός χαρακτήρα όταν βρίσκονται µεταξύ εισαγωγικών. Μερικές χρήσιµες ακολουθίες διαφυγής δίνονται στον Πίνακα 2.2. Παραδείγµατα σταθερών τύπου χαρακτήρα είναι τα ακόλουθα: 'a' '\n' \u00ff Ακολουθία διαφυγής Ερµηνεία \n Ο χαρακτήρας τερµατισµού γραµµής (LF) \r Ο χαρακτήρας επιστροφής (CR) \0 Ο κενός (µηδενικός) χαρακτήρας \' Ο χαρακτήρας ' (απλό εισαγωγικό) \" Ο χαρακτήρας " (διπλό εισαγωγικό) \ unnnn Ο χαρακτήρας µε κωδικό Unicode nnnn (σε δεκαεξαδικό) Πίνακας 2.2. Μερικές ακολουθίες διαφυγής. Οι ακέραιες σταθερές µπορούν να γραφούν στο δεκαδικό σύστηµα, το οκταδικό ή το δεκαεξαδικό. Οι δεκαδικές σταθερές αποτελούνται από µια ακολουθία δεκαδικών ψηφίων, από τα οποία το πρώτο δεν είναι το 0. Για παράδειγµα: 9327 Οι οκταδικές σταθερές αρχίζουν µε το ψηφίο 0, του οποίου έπεται µια ακολουθία οκταδικών ψηφίων. Για παράδειγµα: 21
03477 1855 στο δεκαδικό. Οι δεκαεξαδικές σταθερές αρχίζουν µε την ακολουθία 0x, της οποίας έπεται µια ακολουθία δεκαεξαδικών ψηφίων. Για παράδειγµα: 0xFF77 65399 στο δεκαδικό. Όλες οι ακέραιες σταθερές είναι τύπου int, εκτός αν ακολουθούνται από το γράµµα L. Στην περίπτωση αυτή είναι τύπου long. Για παράδειγµα: 563L. Τέλος, οι σταθερές κινητής υποδιαστολής µπορούν να γραφούν σε δεκαδική µορφή, µε ή χωρίς εκθέτη. Είναι τύπου double, εκτός αν ακολουθούνται από το γράµµα F. Στην περίπτωση αυτή είναι τύπου float. Παραδείγµατα: 3.14159 τύπου double 3.14159F τύπου float 9.05e8 τύπου double, ίση µε: 9.05 X10 8 42e-14F τύπου float, ίση µε: 42 X10 14 2.3.2. Τύποι αναφοράς Όλοι οι τύποι που δεν είναι βασικοί ανήκουν στην κατηγορία των τύπων αναφοράς. Στην κατηγορία αυτή ανήκουν όλοι οι τύποι αντικειµένων και, κατά συνέπεια, όλοι οι τύποι δεδοµένων που µπορεί να ορίσει ο προγραµµατιστής. Οι τιµές που µπορεί να πάρει ένα δεδοµένο που ανήκει σε ένα τύπο αναφοράς είναι στην ουσία αναφορές σε αντικείµενα: όχι τα ίδια τα αντικείµενα αλλά δείκτες σε αυτά. Μπορούν να παραλληλισθούν µε τους δείκτες στη C, µε τη διαφορά ότι δεν απαιτείται ειδική µεταχείριση προκειµένου να προσπελάσει κανείς τα αντικείµενα στα οποία δείχνουν. Η Java δεν επιτρέπει την κατασκευή αναφορών παρά µόνο µε την εκχώρηση ή το πέρασµα αντικειµένων. Το όνοµα τύποι αναφοράς έχει προέλθει από το διαφορετικό τρόπο µε τον οποίο τα δεδοµένα που ανήκουν σε αυτούς τους τύπους διακινούνται κατά την εκτέλεση των προγραµµάτων. Όταν ένα δεδοµένο που ανήκει σε κάποιο βασικό τύπο πρόκειται να εκχωρηθεί σε µια µεταβλητή ή να περαστεί ως όρισµα σε µια µέθοδο, η τιµή του απλώς αντιγράφεται. Αντίθετα, όταν ένα δεδοµένο που ανήκει σε κάποιο τύπο αναφοράς πρόκειται να εκχωρηθεί σε µια µεταβλητή ή να περαστεί ως όρισµα σε µια µέθοδο, αυτό που αντιγράφεται είναι η αναφορά στο αντικείµενο, όχι το ίδιο το αντικείµενο. Με αυτό τον τρόπο, δηµιουργούνται περισσότερες αναφορές στο ίδιο αντικείµενο, το οποίο µπορεί να προσπελασθεί µέσω οποιασδήποτε από αυτές. Η ειδική τιµή null υποδηλώνει ότι ένα δεδοµένο που ανήκει σε κάποιο τύπο αναφοράς δεν αναφέρεται σε κανένα αντικείµενο. Αυτή είναι και η τιµή που χρησιµοποιείται για την αυτόµατη αρχικοποίηση δεδοµένων που ανήκουν σε τύπους αναφοράς. Πριν χρησιµοποιηθούν αυτά τα δεδοµένα για οτιδήποτε µη τετριµµένο, είναι απαραίτητο να ληφθεί µέριµνα ώστε να αναφέρονται σε υπαρκτά αντικείµενα. 22
2.3.3. Ειδικοί τύποι Οι τύποι που θα περιγραφούν στην ενότητα αυτή ανήκουν στην κατηγορία των τύπων αναφοράς. Πρόκειται στην πραγµατικότητα για τύπους που έχουν ιδιαίτερη σηµασία για τη γλώσσα Java και για τους οποίους χρησιµοποιείται ιδιαίτερη σύνταξη. Πάντως, αν παραβλέψει κανείς την ιδιαίτερη σύνταξη, οι τύποι αυτοί δεν είναι παρά απλοί τύποι αντικειµένων, όπως εκείνοι που θα περιγραφούν στο επόµενο κεφάλαιο. Ο τύπος συµβολοσειράς ονοµάζεται String. Οι συµβολοσειρές στη Java είναι ακολουθίες χαρακτήρων Unicode. Σταθερές τύπου συµβολοσειράς µπορούν να γραφούν ανάµεσα σε διπλά εισαγωγικά. Ο µεταγλωττιστής της Java τις µετατρέπει αυτόµατα σε κατάλληλα αντικείµενα τύπου String. Εκτός από κοινούς χαρακτήρες, οι σταθερές u963 συµβολοσειρές µπορούν να περιέχουν ακολουθίες διαφυγής. Παραδείγµατα: "Java" "Hello world!\n" "So long \"Java\" world...\n" Οι τύποι πινάκων ανήκουν επίσης στην κατηγορία των ειδικών τύπων. Πρόκειται για τύπους αντικειµένων µε ειδική σύνταξη που διευκολύνει τη δήλωση και τη χρήση των πινάκων. Ένας τύπος πίνακα προκύπτει από κάποιο έγκυρο τύπο, µε την προσθήκη δυο κενών αγκυλών []. Οι αγκύλες µπορούν να τοποθετηθούν εναλλακτικά είτε µετά το όνοµα του τύπου, ή µετά το όνοµα της µεταβλητής που ορίζεται. Παραδείγµατα δήλωσης πινάκων είναι τα ακόλουθα: int myarray []; int [] yourarray; String hisarray []; Πολυδιάστατοι πίνακες µπορούν να δηλωθούν µε παρόµοιο τρόπο: double [][] mymatrix2d; int yourmatrix3d [][][]; Περισσότερα για την αρχικοποίηση και τη χρήση των πινάκων γράφονται στο επόµενο κεφάλαιο. Στο σηµείο αυτό αξίζει να παρατηρηθεί ότι οι µεταβλητές των ειδικών τύπων που περιγράφηκαν στην ενότητα αυτή πρέπει να αρχικοποιούνται πριν χρησιµοποιηθούν. Σε αντίθετη περίπτωση, όπως όλες οι µη αρχικοποιηµένες µεταβλητές τύπων αναφοράς, έχουν την (άχρηστη) τιµή null. Επίσης, αξίζει να σηµειωθεί ότι οι διαστάσεις των πινάκων δεν καθορίζονται κατά τη δήλωσή τους. 2.4. Εκφράσεις Οι εκφράσεις στη Java είναι τµήµατα του κώδικα που, όταν εκτελούνται, παράγουν κάποιο αποτέλεσµα. Το αποτέλεσµα που προκύπτει από την εκτέλεση µιας έκφρασης ονοµάζεται τιµή της έκφρασης, και για το λόγο αυτό η εκτέλεση µιας έκφρασης ονοµάζεται συχνά και αποτίµηση της έκφρασης. Η τιµή µιας έκφρασης στη Java µπορεί να ανήκει σε κάποιον από τους βασικούς τύπους, π.χ. να είναι αριθµητική, να ανήκει σε κάποιο τύπο αναφοράς, ή να ανήκει στον ειδικό τύπο void. Ο τύπος κάθε έκφρασης είναι πάντα γνωστός κατά 23
τη µεταγλώττιση του κώδικα και η τιµή που θα προκύψει κατά την αποτίµηση στο χρόνο εκτέλεσης θα ανήκει υποχρεωτικά στον ίδιο τύπο. Ο τύπος void είναι κενός, δεν περιέχει δηλαδή καµιά τιµή. Η αποτίµηση µιας έκφρασης µε τύπο void γίνεται µόνο για τις παρενέργειες που µπορεί να έχει. Το αποτέλεσµα δεν µπορεί να χρησιµοποιηθεί γιατί δεν υπάρχει. Η σύνταξη των εκφράσεων της Java µοιάζει πολύ µε αυτή της γλώσσας C. Ιδιαίτερη έµφαση έχει δοθεί στην απλότητα και τη συνέπεια των εκφράσεων. Οι περισσότεροι τελεστές της γλώσσας C υποστηρίζονται, µε την ίδια σηµασία και την ίδια σειρά προτεραιότητας, όπως φαίνεται στον Πίνακα 2.3. Στον πίνακα αυτό, µεγαλύτεροι αριθµοί προτεραιότητας συµβολίζουν στενότερο δέσιµο ενός τελεστή µε τα τελούµενά του. Για παράδειγµα, η έκφραση: c = a + b * 2 ερµηνεύεται ως: c = (a + (b * 2)) γιατί ο τελεστής + έχει µεγαλύτερη προτεραιότητα από τον τελεστή = (11 έναντι 1) και ο τελεστής * έχει µεγαλύτερη προτεραιότητα από τον τελεστή + (12 έναντι 11). Στην περίπτωση τελεστών µε δυο τελούµενα, ο Πίνακας 2.3 δίνει εκτός από την προτεραιότητα και την προσεταιριστικότητα του τελεστή. Η προσεταιριστικότητα ενός τελεστή µπορεί να είναι από αριστερά προς τα δεξιά ή από τα δεξιά προς τα αριστερά, και καθορίζεται από το βέλος που φαίνεται δίπλα στον αριθµό προτεραιότητας. Υποδεικνύει τον τρόπο δεσίµατος των ορισµάτων στην περίπτωση τελεστών µε την ίδια προτεραιότητα. Για παράδειγµα, η έκφραση: a + b 3 ερµηνεύεται ως: (a + b) 3 γιατί οι τελεστές + και -, παρά του ότι έχουν την ίδια προτεραιότητα, έχουν προσεταιριστικότητα από αριστερά προς τα δεξιά. Αντίθετα η έκφραση: a += b = 0 ερµηνεύεται ως: a += (b = 0) Η σειρά αποτίµησης των υπο-εκφράσεων µιας σύνθετης έκφρασης, όταν δεν υπάρχουν άλλοι περιορισµοί που να υπαγορεύονται από την προτεραιότητα και την προσεταιριστικότητα των τελεστών, είναι από αριστερά προς τα δεξιά. 24
Προτεραιότητα Τελεστής Περιγραφή ++ -- Αύξηση / µείωση κατά 1 + - Πρόσηµο συν / πλην 13 ~ Συµπλήρωµα bit προς bit! Λογική άρνηση ( type ) Μετατροπή τύπου 12 * / % Πολλαπλασιασµός, διαίρεση, υπόλοιπο 11 + - Πρόσθεση, αφαίρεση + Παράθεση συµβολοσειρών << Αριστερή ολίσθηση 10 >> εξιά ολίσθηση µε επέκταση προσήµου >>> εξιά ολίσθηση χωρίς επέκταση προσήµου 9 < > =< >= Αριθµητική σύγκριση instanceof Σύγκριση τύπου 8 ==!= Ισότητα, ανισότητα 7 & Σύζευξη, λογική ή bit προς bit 6 ^ Αποκλειστική διάζευξη, λογική ή bit προς bit 5 ιάζευξη, λογική ή bit προς bit 4 && Λογική σύζευξη υπό συνθήκη 3 Λογική διάζευξη υπό συνθήκη 2?: Εναλλακτική αποτίµηση υπό συνθήκη = Εκχώρηση *= /* %== 1 += -= <<= >>= Εκχώρηση µε πράξη >>>= &= ^= = Πίνακας 2.3. Προτεραιότητα τελεστών στη Java. Από τον Πίνακα 2.3 λείπουν ο τελεστής κατασκευής αντικειµένων new και ο τελεστής πρόσβασης µεταβλητών και κλήσης µεθόδων. (τελεία). Και οι δυο, καθώς και οι υπόλοιποι τελεστές που δέχονται τελούµενα τύπων αναφοράς, θα συζητηθούν στο επόµενο κεφάλαιο. 2.4.1. Αριθµητικοί τελεστές και τελεστές σύγκρισης Οι αριθµητικοί τελεστές +, -, *, / και % χρησιµοποιούνται για τις συνήθεις αριθµητικές πράξεις. Τα τελούµενα πρέπει να ανήκουν σε κάποιο αριθµητικό τύπο (ακέραιο ή κινητής υποδιαστολής). Το ίδιο συµβαίνει και µε το αποτέλεσµα. Ο τελεστής + χρησιµοποιείται, εκτός από την πρόσθεση αριθµητικών τιµών, και για την παράθεση (συνένωση) συµβολοσειρών. Για παράδειγµα, το αποτέλεσµα από την αποτίµηση της έκφρασης: "Ja" + "va" είναι η συµβολοσειρά: 25
"Java" Οι τελεστές ++ και -- υλοποιούν αντίστοιχα την αύξηση και τη µείωση κατά ένα µεταβλητών αριθµητικού τύπου. Κάθε ένας από αυτούς µπορεί να τοποθετηθεί είτε πριν, είτε µετά το όνοµα της µεταβλητής. Η λειτουργία είναι λίγο διαφορετική στις δυο αυτές περιπτώσεις. Αν ο τελεστής τοποθετηθεί µετά τη µεταβλητή, η τιµή της έκφρασης είναι η αρχική τιµή της µεταβλητής, πριν την αύξηση ή τη µείωση. Αν τοποθετηθεί πριν, η τιµή της έκφρασης είναι η τελική τιµή της µεταβλητής, µετά την αύξηση ή τη µείωση. Και στις δυο περιπτώσεις, πάντως, η αύξηση ή η µείωση πραγµατοποιούνται. Για παράδειγµα, αν η µεταβλητή a περιέχει την τιµή 42, τότε η τιµή της έκφρασης a++ είναι 42 ενώ η τιµή της έκφρασης ++a είναι 42. Και στις δυο περιπτώσεις, µετά την αποτίµηση της έκφρασης, η µεταβλητή a θα περιέχει την τιµή 43. Οι τελεστές σύγκρισης ισότητας == και ανισότητας!= χρησιµοποιούνται τόσο για τελούµενα που ανήκουν σε βασικούς τύπους, όσο και για τελούµενα που ανήκουν σε τύπους αναφοράς. Στην πρώτη περίπτωση, οι τελεστές αυτοί εξετάζουν την ισότητα (ανισότητα) των τιµών των τελουµένων. Στη δεύτερη περίπτωση, εξετάζουν το αν τα δυο τελούµενα αναφέρονται στο ίδιο αντικείµενο. Οι υπόλοιποι τελεστές σύγκρισης <, >, <= και >= χρησιµοποιούνται µόνο για αριθµητικά τελούµενα. 2.4.2. Τελεστές bit προς bit Οι τελεστές αυτοί χειρίζονται τελούµενα που ανήκουν σε αριθµητικούς τύπους όχι ως αριθµητικές τιµές αλλά ως ακολουθίες από bit. Για παράδειγµα, αν έχει προηγηθεί η δήλωση: short a = 42; τότε η τιµή της µεταβλητής a παριστάνεται από την παρακάτω ακολουθία bit, στο δυαδικό σύστηµα: 0000000000101010 Το πλήθος των bit είναι 16 γιατί πρόκειται για ακέραιο αριθµό τύπου short. Οι τελεστές ~, &, και ^ υλοποιούν αντίστοιχα το συµπλήρωµα, τη σύζευξη, τη διάζευξη και την αποκλειστική διάζευξη bit προς bit, όταν τα τελούµενα είναι αριθµητικά. Για παράδειγµα, η τιµή της έκφρασης: ~a µε βάση την παραπάνω δήλωση είναι -43, που σε παράσταση συµπληρώµατος ως προς 2 αντιστοιχεί στην παρακάτω ακολουθία: 1111111111010101 Ας σηµειωθεί ότι η τιµή κάθε bit της ακολουθίας έχει αντιστραφεί, δηλαδή έχει αντικατασταθεί από το συµπλήρωµά της. Οι τελεστές <<, >> και >>> υλοποιούν την ολίσθηση ακολουθιών bit. Το πρώτο τελούµενο παρέχει την ακολουθία bit ενώ το 26
δεύτερο τελούµενο τον αριθµό των ολισθήσεων. Για παράδειγµα, το αποτέλεσµα της έκφρασης: a << 3 µε βάση την παραπάνω δήλωση είναι 336, που αντιστοιχεί στην παρακάτω ακολουθία: 0000000101010000 Ας σηµειωθεί ότι η ακολουθία αυτή προήλθε από την αρχική µε ολίσθηση τριών θέσεων προς τα αριστερά. 2.4.3. Λογικοί τελεστές Οι λογικοί τελεστές χρησιµοποιούνται για τελούµενα τύπου boolean. Οι κυριότεροι από αυτούς είναι οι τελεστές!, & και που υλοποιούν αντίστοιχα τη λογική άρνηση, τη λογική σύζευξη και τη λογική διάζευξη. Για παράδειγµα, η τιµή της έκφρασης: true & false είναι false. Οι τελεστές && και υλοποιούν επίσης τη λογική σύζευξη και τη λογική διάζευξη, µε τη διαφορά ότι η αποτίµηση των ορισµάτων διακόπτεται µόλις γίνει γνωστό το αποτέλεσµα. Με άλλα λόγια, ο τελεστής && αποτιµά το δεύτερο όρισµα µόνο αν το πρώτο όρισµα έχει τιµή true (ειδάλλως το αποτέλεσµα θα είναι false) και ο τελεστής αποτιµά το δεύτερο όρισµα µόνο αν το πρώτο όρισµα έχει τιµή false (ειδάλλως το αποτέλεσµα θα είναι true). Αυτό είναι σηµαντικό στην περίπτωση που η αποτίµηση του δεύτερου ορίσµατος εµπεριέχει παρενέργειες. Για παράδειγµα, η αποτίµηση των εκφράσεων: false & (b = true) false && (b = true) οδηγεί και στις δυο περιπτώσεις στην τιµή false. Στην πρώτη περίπτωση, όµως, γίνεται η εκχώρηση της τιµής true στη µεταβλητή b, ενώ στη δεύτερη δε γίνεται, γιατί το δεύτερο όρισµα δε χρειάζεται να αποτιµηθεί. Ο πιο πολύπλοκος λογικός τελεστής είναι αυτός της επιλογής υπό συνθήκη. Ο τελεστής αυτός δέχεται τρία ορίσµατα, που διαχωρίζονται µε τους χαρακτήρες? (ερωτηµατικό) και : (άνω και κάτω τελεία). Το πρώτο από αυτά πρέπει να είναι τύπου boolean, ενώ τα άλλα δυο πρέπει να ανήκουν στον ίδιο τύπο. Αφού αποτιµηθεί το πρώτο όρισµα, επιλέγεται ένα από τα άλλα δυο µε τον εξής τρόπο: αν η τιµή του πρώτου ορίσµατος είναι true επιλέγεται το δεύτερο όρισµα, διαφορετικά επιλέγεται το τρίτο όρισµα. Στη συνέχεια, η τιµή της συνολικής έκφρασης είναι η τιµή του ορίσµατος που επιλέχθηκε. Το όρισµα που δεν επιλέγεται δεν αποτιµάται καθόλου. Για παράδειγµα, η έκφραση: (1 + 1 == 2)? 42 : a++ 27
έχει τιµή 42, γιατί το πρώτο όρισµα έχει τιµή true, πράγµα που οδηγεί στην επιλογή του δεύτερου ορίσµατος. Η τιµή της µεταβλητής a δεν αυξάνεται γιατί το τρίτο όρισµα δεν αποτιµάται. 2.4.4. Τελεστές εκχώρησης Με τον όρο εκχώρηση εννοούµε την ανάθεση µιας τιµής σε µια µεταβλητή. Ο τελεστής απλής εκχώρησης = χρησιµοποιείται για να αναθέσει την τιµή του δεύτερου ορίσµατός του στη µεταβλητή που δηλώνεται από το πρώτο όρισµα. Για παράδειγµα, αν υποτεθεί ότι η µεταβλητή x έχει δηλωθεί µε τύπο int και περιέχει την τιµή 5, τότε, µετά την αποτίµηση της έκφρασης: x = 42 η τιµή της µεταβλητής x θα είναι 42. Οι τελεστές εκχώρησης µε πράξη χρησιµοποιούν για τον υπολογισµό της τιµής που θα ανατεθεί, εκτός από το δεύτερο όρισµα, και την τρέχουσα τιµή της µεταβλητής. Για παράδειγµα, η έκφραση: x += 5 είναι ισοδύναµη µε την έκφραση: x = x + 5 µε τη διαφορά ότι το όρισµα x αποτιµάται µόνο µια φορά. 2.5. Εντολές Οι εντολές στη γλώσσα Java, όπως και στη C ή τη C++, εµφανίζονται µέσα σε τµήµατα κώδικα που περικλείονται από άγκιστρα. Τέτοια τµήµατα κώδικα επιτρέπονται µόνο στο κύριο σώµα µεθόδων, όπως θα περιγραφεί αναλυτικά στο επόµενο κεφάλαιο. Τα τµήµατα κώδικα µπορούν να περιέχουν δηλώσεις µεταβλητών. Στην περίπτωση αυτή, οι µεταβλητές είναι ορατές µόνο στο εσωτερικό του τµήµατος: δεν είναι δυνατό να χρησιµοποιηθούν έξω από αυτό. Επίσης, τα τµήµατα κώδικα είναι δυνατό να περιέχουν φωλιασµένα άλλα τµήµατα κώδικα. Παράδειγµα τµήµατος κώδικα είναι το επόµενο, που εµπεριέχει τη δήλωση δυο µεταβλητών x και d, καθώς και ένα άλλο τµήµα κώδικα, στο πρώτο σκέλος της εντολής if. int x = 1; double d = 3.14159; if (x <= 5) System.out.println(x); return d + 2.5; else return 8.1 - d; 28
Η σύνταξη των εντολών στη Java δε διαφέρει πολύ από αυτή στη γλώσσα C. Στις επόµενες ενότητες περιγράφονται οι εντολές της Java, µε τη βοήθεια παραδειγµάτων. 2.5.1. Εντολές εκφράσεων Κάθε έκφραση µπορεί να θεωρηθεί ως µια εντολή που, όταν εκτελείται, παράγει κάποιο αποτέλεσµα. Με την προσθήκη του χαρακτήρα ; (semicolon) στο τέλος µιας έκφρασης, αυτή µετατρέπεται σε απλή εντολή. Όταν αυτή η εντολή εκτελεσθεί, θα γίνει η αποτίµησή της και το αποτέλεσµα θα αγνοηθεί. Οι εντολές εκφράσεων χρησιµοποιούνται κυρίως για τις παρενέργειες που προκαλούν, π.χ. για την αλλαγή τιµών σε µεταβλητές του προγράµµατος. Παραδείγµατα εντολών εκφράσεων είναι τα ακόλουθα: x = 1; a = flag? 5 + a : 8 + x; Ειδική περίπτωση εκφράσεων είναι η κλήση µεθόδων, που θα περιγραφεί στο επόµενο κεφάλαιο. Όµως, αν και πρωθύστερη, κρίνεται σκόπιµη µια µικρή εισαγωγή στη µέθοδο System.out.println, που χρησιµοποιείται για την εκτύπωση τιµών στην οθόνη. Η µέθοδος αυτή δέχεται ως παράµετρο µια τιµή που µπορεί να ανήκει σε µια ποικιλία τύπων, συµπεριλαµβανοµένων όλων των βασικών τύπων. Παράδειγµα χρήσης αυτής της µεθόδου σε µια εντολή έκφρασης είναι το ακόλουθο: System.out.println("Hello world!"); που εκτυπώνει τη συµβολοσειρά Hello world!. Η µέθοδος αυτή αποτελεί τµήµα του συστήµατος εισόδου-εξόδου της Java, που θα περιγραφεί µε λεπτοµέρειες στο κατάλληλο κεφάλαιο. 2.5.2. Εντολές επιλογής Η συχνότερα χρησιµοποιούµενη εντολή επιλογής είναι η εντολή if. Η εντολή αυτή αποτιµά µια λογική συνθήκη και, ανάλογα µε την τιµή της, εκτελεί ένα από τα δυο πιθανά σκέλη της. Η σύνταξή της είναι: if ( συνθήκη ) εντολή else εντολή Στην περίπτωση που η συνθήκη είναι µια έκφραση τύπου boolean, αν αυτή έχει τιµή true εκτελείται η πρώτη εντολή, διαφορετικά η δεύτερη εντολή. Αν η συνθήκη είναι µια έκφραση διαφορετικού βασικού τύπου, τότε θεωρείται αληθής αν είναι µη µηδενική, διαφορετικά ψευδής. Επίσης, αν είναι µια έκφραση τύπου αναφοράς, θεωρείται ψευδής αν έχει τιµή null, αληθής διαφορετικά. Η ύπαρξη του σκέλους else είναι προαιρετική: αν αυτό λείπει τότε θεωρείται κενό. Σηµειώνεται επίσης ότι οποιοδήποτε από τα σκέλη της εντολής if µπορεί να αποτελείται από µια σύνθετη εντολή, να είναι δηλαδή ένα τµήµα κώδικα που περικλείεται από άγκιστρα. Παράδειγµα τέτοιας εντολής είναι το ακόλουθο: 29
if (x > 0) System.out.println("It was positive."); else System.out.println("It was not positive."); x = 1 - x; System.out.println("But now it is!"); Μερικές φορές η επιλογή δεν καθορίζεται από µια λογική συνθήκη, αλλά γίνεται βάση της τιµής µιας ακεραίας τιµής. Στην περίπτωση αυτή µπορεί να χρησιµοποιηθεί η εντολή switch, που εξηγείται αµέσως µε το ακόλουθο παράδειγµα: switch (count) case 1: System.out.println("I have one."); break; case 2: case 3: System.out.println("I have two or three."); break; default: System.out.println("I have a few..."); break; Στο παραπάνω παράδειγµα, η έκφραση count αποτιµάται. Αν η τιµή της είναι 1, τότε ο έλεγχος µεταφέρεται στην εντολή που ακολουθεί την ετικέτα case 1. Από το σηµείο αυτό, εκτελούνται οι επόµενες εντολές µέχρι να συναντηθεί η εντολή break ή το άγκιστρο που κλείνει την εντολή switch. Και στις δυο περιπτώσεις, η εκτέλεση της εντολής switch τερµατίζεται. Η ύπαρξη της εντολής break δεν είναι υποχρεωτική, αν όµως λείπει τότε συνεχίζονται να εκτελούνται εντολές που πιθανώς ανήκουν σε άλλες ετικέτες. Αυτό δεν είναι πάντα ανεπιθύµητο, όπως φαίνεται στην περίπτωση της ετικέτας case 2 που δεν περιέχει εντολή, αλλά χρησιµοποιεί τις εντολές της ετικέτας case 3. Η ειδική ετικέτα default χρησιµοποιείται όταν η τιµή της έκφρασης επιλογής δεν προβλέπεται σε κάποια άλλη ετικέτα. Για παράδειγµα, ο έλεγχος θα µεταφερθεί εκεί αν η τιµή της µεταβλητής count είναι ίση µε 42. 2.5.3. Εντολές βρόχων Η Java υποστηρίζει τις εντολές βρόχων των γλωσσών C και C++. Υποστηρίζονται συγκεκριµένα τρεις εντολές βρόχων. Η πρώτη από αυτές είναι η εντολή while, που επαναλαµβάνει την εκτέλεση της εντολής που βρίσκεται στο κύριο σώµα της όσο η τιµή της συνθήκης της είναι true. Για παράδειγµα, στο παρακάτω τµήµα κώδικα: int i = 0; while (i < 10) 30
System.out.println(++i); η εκτέλεση της εντολής while προκαλεί την εκτύπωση των αριθµών 1 ως 10. Σηµειώνεται ότι, αν η συνθήκη είναι εξ αρχής ψευδής, το κύριο σώµα της εντολής while δεν εκτελείται καµιά φορά. υο ειδικές εντολές µπορούν να χρησιµοποιηθούν στο εσωτερικό κάθε βρόχου. Η εντολή break προκαλεί την άµεση έξοδο από το βρόχο, ενώ η εντολή continue προκαλεί την άµεση επανάληψη του βρόχου. Για παράδειγµα, στο παρακάτω τµήµα κώδικα: int i = 0; while (i < 10) System.out.println(++i); if (i!= 3) continue; break; µόνο οι αριθµοί 1, 2 και 3 θα εκτυπωθούν ο αναγνώστης καλείται να εξετάσει γιατί. Παρόµοια είναι η εντολή do while, µε τη διαφορά ότι η λογική συνθήκη ελέγχεται αφού εκτελεσθεί το κύριο σώµα. Κατ αυτό τον τρόπο, το κύριο σώµα εκτελείται τουλάχιστον µια φορά. Για παράδειγµα, στο παρακάτω τµήµα κώδικα: int i = 0; do System.out.println(i); i = 10 - i; while (i > 0); η εκτέλεση της εντολής do while προκαλεί την εκτύπωση των αριθµών 0 και 10. Η πιο πολύπλοκη εντολή βρόχου είναι η εντολή for, που µοιάζει πολύ µε την αντίστοιχη της C/C++. Η σύνταξή της είναι η ακόλουθη: for ( αρχικοποίηση ; συνθήκη ; ενηµέρωση ) εντολή Το τµήµα αρχικοποίησης µπορεί να αποτελείται από µια ή περισσότερες εντολές ή δηλώσεις και αρχικοποιήσεις µεταβλητών, χωρισµένες µε κόµµατα. Εκτελείται µια µόνο φορά, πριν αποτιµηθεί για πρώτη φορά η συνθήκη. Το τµήµα ενηµέρωσης αποτελείται από µια ή περισσότερες εντολές, χωρισµένες µε κόµµατα, και εκτελείται µετά από κάθε επανάληψη του βρόχου και πριν την αποτίµηση της συνθήκης. Για παράδειγµα, η ακόλουθη εντολή: for (int i = 0 ; i < 10 ; i++) 31
System.out.println(i); είναι ισοδύναµη µε το τµήµα κώδικα (1) που δόθηκε πιο πάνω ως παράδειγµα της εντολής while. Οι εντολές βρόχων δέχονται προαιρετικά ετικέττες, που µπορούν να χρησιµοποιηθούν ως ορίσµατα των εντολών break και continue. Μια τέτοια εντολή χωρίς όρισµα αναφέρεται στον πιο εσωτερικό βρόχο. Αντίθετα, η χρήση ορίσµατος την κάνει να αναφέρεται στο βρόχο που φέρει την αντίστοιχη ετικέττα. Η χρήση ετικεττών είναι ιδιαίτερα χρήσιµη στην περίπτωση φωλιασµένων βρόχων. Για παράδειγµα, το παρακάτω τµήµα κώδικα: int [][] a = ; /* initialization */ outer: for (int i = 0 ; i < 10 ; i++) for (int j = 0 ; j < 10 ; j++) if (a[i][j] == 42) continue; System.out.println(a[i][j]); if (a[i][j] == 0) break outer; εκτυπώνει το δισδιάστατο πίνακα a, διαστάσεων 10 10, παραλείποντας τα στοιχεία που έχουν τιµή 42. Όταν βρεθεί κάποιο στοιχείο µε τιµή 0, η εκτύπωση σταµατά. 2.5.4. Άλλες εντολές Η Java υποστηρίζει και άλλες εντολές που θα περιγραφούν αναλυτικότερα σε επόµενα κεφάλαια. Πρόκειται για την εντολή return, που χρησιµοποιείται για την επιστροφή τιµής από µέθοδο, και τις εντολές try, catch και finally που χρησιµοποιούνται για το χειρισµό εξαιρέσεων. 32
2.6. Λυµένες Ασκήσεις 2.6.1. Γράψτε ένα πρόγραµµα το οποίο να εµφανίζει τις παρακάτω γραµµές: The Java programming language was developed by James Gosling at Sun Microsystems in 1991. When the World Wide Web appeared on the Internet in 1993, the language was enhanced to facilitate programming on the web. χρησιµοποιώντας την µέθοδο println(). Επαναλάβεται την άσκηση και µε τη µέθοδο print(). Λύση 1: import java.io.*; public class A261 public static void main(string[] args) System.out.println("The Java programming language was developed by"); System.out.println("James Gosling at Sun Microsystems in 1991,"); System.out.println("When the wworld Wide Web appered on the Internet in 1993,"); System.out.println("the language was enhanced to facilitate programming on the web."); Η εκτέλεση και τα αποτελέσµατα του παραπάνω προγράµµατος εικονίζονται στα παρακάτω σχήµατα: Λύση 2: import java.io.*; public class A261B public static void main(string[] args) 33
System.out.print("The Java programming language was developed by\n"); System.out.print("James Gosling at Sun Microsystems in 1991,\n"); System.out.print("When the wworld Wide Web appered on the Internet in 1993,\n"); System.out.print("the language was enhanced to facilitate programming on the web.\n"); Η εκτέλεση και τα αποτελέσµατα του παραπάνω προγράµµατος εικονίζονται στο παρακάτω σχήµα: Όπως βλέπουµε οι εκτελέσεις των δύο προγραµµάτων δίνουν το ίδιο αποτέλεσµα. Η διαφορά µεταξύ των µεθόδων println() και print() είναι ο χαρακτήρας νέας γραµµής \n. 2.6.2. Γράψτε ένα πρόγραµµα στο οποίο να δηλώσετε 8 µεταβλητές σύµφωνα µε τους οκτώ βασικούς τύπους δεδοµένων που συναντάµε στην Java. ώστε τιµές σε αυτές τις µεταβλητές και στη συνέχεια τυπώστε τις. Λύση: public class PrintType public static void main(string[] args) byte i=127; short j=32767; int k=2147483647; long l=9223372036854775807l; float x=3.14159265f; double y=3.14159265358979238; char c='a'; boolean b=false; 34
System.out.println("i = " + i); System.out.println("j = " + j); System.out.println("k = " + k); System.out.println("l = " + l); System.out.println("x = " + x); System.out.println("y = " + y); System.out.println("c = " + c); System.out.println("b = " + b); Η εκτέλεση και τα αποτελέσµατα του παραπάνω προγράµµατος εικονίζονται στο παρακάτω σχήµα: Προσοχή όταν χρησιµοποιούµε αριθµούς τύπου long και float πρέπει να τερµατίζονται κατά την αρχικοποίηση από το γράµµα L και F αντίστοιχα. 2.6.3. Γράψτε ένα πρόγραµµα το οποίο να υπολογίζει και να εκτυπώνει το συνηµίτονο, το ηµίτονο και την εφαπτοµένη των 60. Λύση: public class A263 public static void main(string[] args) double theta=60; theta=theta*math.pi/180.; System.out.println("To cos ton 60 moirwn einai : " + Math.cos(theta)); System.out.println("To sin ton 60 moirwn einai : " + Math.sin(theta)); System.out.println("To tan ton 60 moirwn einai : " + Math.tan(theta)); 35
Η εκτέλεση και τα αποτελέσµατα του παραπάνω προγράµµατος εικονίζονται στο παρακάτω σχήµα: Το όρισµα στις τριγωνοµετρικές µεθόδους πρέπει να εκφράζεται σε rads. 2.6.4. Γράψτε ένα πρόγραµµα στο οποίο να εισάγετε από την γραµµή εντολών ένα string έναν int και έναν double. Στη συνέχεια να τα τυπώσετε. Λύση: public class InLine public static void main(string[] arguments) String mystring=arguments[0]; int myint=integer.parseint(arguments[1]); double mydouble=double.parsedouble(arguments[2]); System.out.println("Mystring = " + mystring); System.out.println("Myint = " + myint); System.out.println("Mydouble = " + mydouble); Η εκτέλεση του παραπάνω προγράµµατος παράγει το ακόλουθο αποτέλεσµα όταν στη γραµµή εντολών δακτυλογραφηθούν η συµβολοσειρά Hello ο ακέραιος 123 και ο αριθµός κινητής υποδιαστολής διπλής ακρίβειας 3.14159: Παρατηρούµε πως αυτός είναι ένας απλός τρόπος που µπορούµε να χρησιµοποιήσουµε για να εισάγουµε δεδοµένα στο πρόγραµµά µας. Εκµεταλλευόµαστε τον πίνακα συµβολοσειράς String[] arguments ο οποίος αποτελεί 36
το όρισµα της µεθόδου main(). Τα Hello, 123 και 3.14159 εισάγονται ως συµβολοσειρές στο πρόγραµµα και αποθηκεύονται στα πρώτα στοιχεία του πίνακα arguments (δηλαδή το Hello στο arguments[0], το 123 στο arguments[1] και το 3.14159 στο arguments[2]). Στη συνέχεια για να µετατρέψουµε την συµβολοσειρά 123 σε ακέραιο χρησιµοποιούµε όπως φαίνεται στο παράδειγµα την µέθοδο parseint() της κλάσης Integer. Για να µετατρέψουµε την συµβολοσειρά 3.14159 σε αριθµός κινητής υποδιαστολής διπλής ακρίβειας χρησιµοποιούµε όπως φαίνεται στο παράδειγµα την µέθοδο parsedouble() της κλάσης Double. Περισσότερα για τις κλάσεις Integer και Double µπορείτε να βρείτε στις ιστοσελίδες: http://java.sun.com/j2se/1.4.2/docs/api/java/lang/integer.html http://java.sun.com/j2se/1.4.2/docs/api/java/lang/double.html 2.6.5. Να γράψετε ένα πρόγραµµα το οποίο να υπολογίζει την αριθµητική τιµή της συνάρτησης για διάφορες τιµές του x. Οι τιµές του x να εισάγονται στο πρόγραµµα από την γραµµή εντολών. Εκτελέστε το πρόγραµµα δίνοντας τέσσερα δικά σας παραδείγµατα. Λύση: public class Fx public static void main(string[] args) double x=double.parsedouble(args[0]); double fx=math.sqrt((4.*math.pow(x,3)- 3.*Math.pow(x,2)+2.*x-1)/5.); System.out.println("Gia x = " + x + " f(x) = " + fx); Η εκτέλεση του παραπάνω προγράµµατος µε τέσσερα τυχαία παραδείγµατα είναι η ακόλουθη: 37
Παρατηρείστε πως για την τιµή x=-34.789 η τιµή της παράστασης δεν ορίζεται γιατί το υπόριζο είναι αρνητικό. Γιαυτό το λόγο εκτυπώνεται το NaN. Σε τέτοιες περιπτώσεις πρέπει να προστατεύουµε το πρόγραµµα όπως θα δούµε αργότερα µε την εντολή if. 2.6.6. Να γράψετε ένα πρόγραµµα στο οποίο να εισάγετε το όνοµά σας την ηλικία σας και το τρέχον έτος και να σας επιστρέφει το έτος γέννησης σας. Η εισαγωγή των δεδοµένων να γίνει από το πληκτρολόγιο. Λύση: Στα προηγούµενα δύο παραδείγµατα µάθαµε πως να εισάγουµε δεδοµένα στο πρόγραµµά µας από την γραµµή εντολών. Ένας άλλος τρόπος εισαγωγής δεδοµένων στο πρόγραµµα περιγράφεται σε αυτό το παράδειγµα. Το πρόγραµµα κατά την εκτέλεση σταµατά και περιµένει την εισαγωγή των δεδοµένων. Αυτός είναι πιο σωστός τρόπος αλληλεπίδρασης προγράµµατος-χρήστη. Μια πιθανή λύση στο πρόβληµα είναι η ακόλουθη: import java.io.*; public class BirthYear public static void main(string[] args)throws IOException InputStreamReader reader = new InputStreamReader(System.in); BufferedReader input = new BufferedReader(reader); System.out.print("Dose to onoma su: "); String onoma = input.readline(); System.out.print("Dose tin ilikia su: "); String ilikia = input.readline(); int xronia = Integer.parseInt(ilikia); System.out.println("Dose to trehon etos: "); String trehon_etos = input.readline(); int etos = Integer.parseInt(trehon_etos); int etos_genesis = etos - xronia; System.out.println("To onoma su einai : " + onoma); System.out.println("Eisai : " + xronia + " eton."); System.out.println("Genithikes to : " + etos_genesis); 38
Η εκτέλεση του παραπάνω προγράµµατος είναι η ακόλουθη: Το αντικείµενο reader της κλάσης InputStreamReader διαβάζει τα δεδοµένα από το πληκτρολόγιο. Το αντικείµενο input της κλάσης BufferedReader µετατρέπει κάθε byte πληροφορίας από το αντικείµενο reader σε χαρακτήρες. Η µέθοδος readline() µεταφέρει τα δεδοµένα που δακτυλογραφούνται στις συµβολοσειρές µας (π.χ onoma, ilikia, trexon_etos) για περαιτέρω επεξεργασία. Τέλος το throws IOException είναι απαραίτητο για να χρησιµοποιηθεί η µέθοδος readline(). 2.6.7. Να γραφεί ένα πρόγραµµα το οποίο να συγκρίνει µεταξύ τους δύο ακεραίους αριθµούς. Οι δύο ακέραιοι να εισαχθούν στο πρόγραµµα από το πληκτρολόγιο. Να χρησιµοποιήσετε την εντολή if. Λύση 1: import java.io.*; public class ExampleF1 public static void main(string[] args) throws IOException InputStreamReader reader = new InputStreamReader(System.in); BufferedReader input = new BufferedReader(reader); System.out.print("Dosate ton proto akeraio: "); String arithmos1 = input.readline(); int n1 = Integer.parseInt(arithmos1); System.out.print("Dosate ton deutero akeraio: "); String arithmos2 = input.readline(); int n2 = Integer.parseInt(arithmos2); if(n1<n2) System.out.println(n1 + " < " + n2); if(n1==n2) System.out.println(n1 + " = " + n2); 39
if(n1>n2) System.out.println(n1 + " > " + n2); Η εκτέλεση του προγράµµατος εικονίζεται παρακάτω: Λύση 2: εύτερη πιθανή λύση µε την χρήση της εντολής if-else και else-if. Και η δεύτερη λύση δίνει ακριβώς τα ίδια αποτελέσµατα όπως και η πρώτη. import java.io.*; public class ExampleF2 public static void main(string[] args) throws IOException InputStreamReader reader = new InputStreamReader(System.in); BufferedReader input = new BufferedReader(reader); System.out.print("Dosate ton proto akeraio: "); String arithmos1 = input.readline(); int n1 = Integer.parseInt(arithmos1); System.out.print("Dosate ton deutero akeraio: "); String arithmos2 = input.readline(); int n2 = Integer.parseInt(arithmos2); if(n1<n2) System.out.println(n1 + " < " + n2); else if(n1==n2) 40
else System.out.println(n1 + " = " + n2); System.out.println(n1 + " > " + n2); 2.6.8. ίδεται η συνάρτηση: όπου n ακέραιος. Να γραφεί ένα πρόγραµµα το οποίο να υπολογίζει την τιµή της για κάθε n το οποίο εισάγεται από το πληκτρολόγιο. Να γίνει διερεύνηση ώστε να αποκλεισθούν µη πραγµατικές τιµές και τυχόν απειρισµοί της συνάρτησης. Η παραπάνω συνάρτηση έχει πραγµατικές τιµές όταν η υπόριζος ποσότητα είναι >=0. Επίσης η συνάρτηση απειρίζεται όταν n=30. Λύση: import java.io.*; public class ExampleFx public static void main(string[] args) throws IOException InputStreamReader reader = new InputStreamReader(System.in); BufferedReader input = new BufferedReader(reader); System.out.print("Dosate enan akeraio arithmo: "); String arithmos = input.readline(); int n = Integer.parseInt(arithmos); if((math.pow(n,3)+math.pow(n,2)+n+1.)<0) System.out.println("H parastasi den einai pragmatiki"); return; if(n==30) System.out.println("H parastasi apirizetai"); return; double fx=math.sqrt(math.pow(n,3)+math.pow(n,2)+n+1.)/(n- 30); System.out.print("H timi tis parastasis gia n =" + n +" einai:" + fx); 41
Η εκτέλεση του προγράµµατος µε µερικά παραδείγµατα εικονίζεται παρακάτω. 2.6.9. Να γραφεί ένα πρόγραµµα το οποίο ως είσοδο να έχει έναν ακέραιο από το 1 έως το 7 και ως έξοδο να τυπώνει την αντίστοιχη ηµέρα της εβδοµάδας. Να χρησιµοποιήσετε την εντολή switch. Λύση: import java.io.*; public class ExampleSwitch public static void main(string[] args) throws IOException InputStreamReader reader = new InputStreamReader(System.in); BufferedReader input = new BufferedReader(reader); System.out.print("Dosate enan arithmo apo to 1 eos to 7 : "); String arithmos = input.readline(); int n = Integer.parseInt(arithmos); if(n<1 n>7) System.out.println("Lathos Arithmos."); return; switch(n) case 1: System.out.println("H prwti mera tis vdomadas einai i Kyriaki."); break; case 2: 42
System.out.println("H deuteri mera tis vdomadas einai i Deutera."); break; case 3: System.out.println("H triti mera tis vdomadas einai i Triti."); break; case 4: System.out.println("H tetarti mera tis vdomadas einai i Tetarti."); break; case 5: System.out.println("H pempti mera tis vdomadas einai i Pempti."); break; case 6: System.out.println("H ekti mera tis vdomadas einai i Paraskevi."); break; case 7: System.out.println("H evdomi mera tis vdomadas einai i Savvato."); break; Η εκτέλεση του παραπάνω προγράµµατος δίνει το παρακάτω αποτέλεσµα: 2.6.10. Να γράψετε ένα πρόγραµµα το οποίο να τυπώνει δέκα τυχαίους ακεραίους αριθµούς. Να χρησιµοποιήσετε την κλάση Random. Να χρησιµοποιήσετε τις εντολές for, while και do-while σε τρία διαφορετικά προγράµµατα. Για να χρησιµοποιήσετε τυχαίους αριθµούς πρέπει αρχικά να εισάγετε την κλάση java.util.random. Στη συνέχεια να την χρησιµοποιήσετε ώστε να κατασκευάσετε ένα αντικείµενο το οποίο να δηµιουργεί τυχαίους αριθµούς (στο 43
παράδειγµά µας το ονοµάζουµε rndm). Στη συνέχεια το αντικείµενο αυτό µε την µέθοδο nextint() δηµιουργεί τυχαίους ακεραίους αριθµούς από το 2147483648 έως το 2147483647. Εάν θέλετε αντί για ακεραίους να δηµιουργήσετε τυχαίους αριθµούς τύπου float ή double από το 0 έως το 1 µπορείτε να χρησιµοποιήσετε τις µεθόδους nextfloat() και nextdouble() αντίστοιχα. Λύση 1: import java.util.random; public class ExampleFor public static void main(string[] args) Random rndm = new Random(); for(int i=1; i<=10; i++) int n = rndm.nextint(); System.out.println(i + " : " + n); Η εκτέλεση του προγράµµατος εικονίζεται παρακάτω. Κάθε φορά που το εκτελείτε δηµιουργούνται νέοι τυχαίοι αριθµοί. Λύση 2: Μια πιθανή λύση µε την χρήση της εντολής while είναι η ακόλουθη: import java.util.random; public class ExampleWhile public static void main(string[] args) Random rndm = new Random(); 44
int i=1; while(i<=10) int n = rndm.nextint(); System.out.println(i + " : " + n); i++; Λύση 3: Μια πιθανή λύση µε την χρήση της εντολής do-while είναι η ακόλουθη: import java.util.random; public class ExampleDoWhile public static void main(string[] args) Random rndm = new Random(); int i=1; do int n = rndm.nextint(); System.out.println(i + " : " + n); i++; while(i<=10); Η εκτέλεση των παραπάνω προγραµµάτων δίνει το ίδιο αποτέλεσµα όπως και η πρώτη λύση. 2.6.11. Να γράψετε ένα πρόγραµµα το οποίο να υπολογίζει το παραγοντικό ενός θετικού ακεραίου αριθµού n ο οποίος να εισάγετε από το πληκτρολόγιο. Προστατέψτε το πρόγραµµά σας από εισαγωγή αρνητικών ακεραίων αριθµών. ίνονται: n! = 1*2*3* *(n-1)*n 0! = 1 Λύση: import java.io.*; public class Paragontiko public static void main(string[] args) throws IOException InputStreamReader reader = new InputStreamReader(System.in); BufferedReader input = new BufferedReader(reader); 45
System.out.print("Dosate ton akerai n : "); String arithmos = input.readline(); int n = Integer.parseInt(arithmos); if(n<0) System.out.println("O arithmos n pou dosate einai < 0."); return; double result = 1.; for(int i=1; i<=n; i++) result*=i; System.out.println(n + "! = " + result); Προσέξτε τον έλεγχο για αρνητικούς ακεραίους ο οποίος υλοποιείται µε µια απλή εντολή if. Ακολουθούν παραδείγµατα εκτέλεσης του προγράµµατος. 2.7. Ασκήσεις 1. Παραγοντικό είναι ένας αριθµός που υπολογίζεται ως µία επαναλαµβανόµενη ακολουθία πολλαπλασιασµών. Ο παραγοντικός αριθµός γράφεται ως ένας αριθµός µε θαυµαστικό. Για παράδειγµα 4! που είναι ίσο µε 1 * 2 * 3 * 4 = 24. Γράψτε µία εφαρµογή που θα εκτυπώνει τα παραγοντικά των αριθµών 2, 4, 6 και 10. 2. Για κάθε δεδοµένο ορθογώνιο τρίγωνο το µήκος της υποτείνουσας δίνεται από τον τύπο c 2 = a 2 + b 2. Γράψτε ένα πρόγραµµα Java που να υπολογίζει την υποτείνουσα µε βάση τις δύο άλλες πλευρές ενός ορθογωνίου τριγώνου. Υπόδειξη: δείτε την κλάση java.lang.math. 3. Ένα σώµα εκτελεί πλάγια βολή. Το σώµα εκτοξεύεται µε αρχική ταχύτητα v0=100m/sec. Να γράψετε ένα πρόγραµµα το οποίο να υπολογίζει για πόσο χρόνο το σώµα µένει στον αέρα και ποιο είναι το βεληνεκές του όταν εκτοξεύεται υπό γωνία θ ίση µε 30º, 40º, 45º, 50º και 60º. Αγνοείστε την τριβή του αέρα. Στα αποτελέσµατά σας να αναγράψετε και µονάδες. ίνονται: g=9.81m/sec2, t=(2 v0 sinθ)/(g) και R=(v0 cosθ)t 4. Να αναπτύξετε προγράµµατα τα οποία να επαληθεύουν κάθε µία από τις ακόλουθες τριγωνοµετρικές ισότητες: sin(a-b) = sina cosb - cosa sinb cos(a+b) = cosa cosb sina sinb cos(a-b) = cosa cosb + sina sinb sin(2a) = 2 sina cosa cos(2a) = cos2a sin2a tan(2a) = (2 tana)/(1- tan2a) sin(3a) = 3 sina 4 sin3a cos(3a) = 4 cos3a 3 cosa tan(3a) = (3 tana tan3a)/(1 3 tan2a) 46
Σε κάθε πρόγραµµα να υπολογίσετε ξεχωριστά το κάθε µέλος των παραπάνω ισοτήτων και να το εκτυπώσετε για διάφορες τιµές των µεταβλητών A και B. Οι µεταβλητές A και B να δίνονται από την γραµµή εντολών. ώστε µερικά παραδείγµατα.. 5. Να γράψετε ένα πρόγραµµα το οποίο να δέχεται από το πληκτρολόγιο την θερµοκρασία σε βαθµούς Κελσίου και να την τυπώνει σε βαθµούς Fahrenheit. ίνεται ότι F=1.8C+32. 6. Να αναπτύξετε προγράµµατα τα οποία να επαληθεύουν κάθε µία από τις ακόλουθες ισότητες: log(ab) = loga + logb log(a/b) = loga logb log(an) = n loga Σε κάθε πρόγραµµα να υπολογίσετε ξεχωριστά το κάθε µέλος των παραπάνω ισοτήτων και να το εκτυπώσετε για διάφορες τιµές των µεταβλητών A, B και n. Οι µεταβλητές A, Β και n να δίνονται από το πληκτρολόγιο. ώστε µερικά παραδείγµατα. 7. Να αναπτύξετε προγράµµατα τα οποία να εµφανίζουν τα επόµενα σχήµατα: Α) * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * Β) * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * 47
Γ) * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * ) Ε) Ν Ι Κ Ο Σ Ι Κ Ο Σ Κ Ο Σ Ο Σ Σ Ν Ι Κ Ο Σ Ν Ι Κ Ο Ν Ι Κ Ν Ι Ν 8. Να αναπτύξετε προγράµµατα το οποίο να δέχεται 3 ακέραιους αριθµούς και τους τυπώνει τξινοµηµένα. 9. Να αναπτύξετε προγράµµατα το οποίο να τυπώνει τα πολλαπλάσια του 3 µέχρι το 30. 10. Να αναπτύξετε προγράµµατα το οποίο να τυπώνει όλους του πρώτου αριθµούς από το 1 µέχρι το 100. (Πρώτος είναι ο αριθµός που διαιρείται µε το 1 και µε τον εαυτό του µόνο). 48
3. Πίνακες 3.1. ηλώσεις πινάκων Μπορείτε να δηλώσετε πίνακα οποιουδήποτε τύπου είτε βασικού είτε κλάσης: char s[]; Point p[]; Στη Java ένας πίνακας είναι µια κλάση και όπως και µε τους άλλους τύπους κλάσης η δήλωση δε δηµιουργεί και το αντικείµενο. Συνεπώς οι δηλώσεις αυτές δεν δηµιουργούν πίνακες, απλά µεταβλητές αναφοράς που µπορούν να χρησιµοποιηθούν για αναφορά σε έναν πίνακα. Στην επόµενη σελίδα θα δούµε πώς δηµιουργούµε και αρχικοποιούµε έναν πραγµατικό πίνακα. Η µορφή που φαίνεται πιο πάνω, µε τις αγκύλες µετά το όνοµα της µεταβλητής, είναι η συνήθης για C και C++ και είναι η ίδια και στη Java. Η µορφή αυτή οδηγεί σε σύνθετες µορφές δηλώσεων και µπορεί να είναι δύσκολη στην ανάγνωση, συνεπώς η Java επιτρέπει µία εναλλακτική µορφή µε τις αγκύλες στα αριστερά. char [] s; Point [] p; Η επιλογή σχετικά µε το ποια από τις δύο µορφές θα χρησιµοποιείτε είναι δική σας, αρκεί να είστε συνεπείς στην απόφασή σας. 3.2. ηµιουργία πινάκων Μπορείτε να δηµιουργήσετε πίνακες, όπως και όλα τα αντικείµενα, µε χρήση της δεσµευµένης λέξης new, ως εξής: s = new char[20]; p = new Point[100]; Η πρώτη γραµµή δηµιουργεί έναν πίνακα 20 τιµών τύπου char. Η δεύτερη γραµµή δηµιουργεί ένα πίνακα µε 100 µεταβλητές τύπου Point. Στην πράξη δε δηµιουργεί 100 αντικείµενα τύπου Point. Αυτά θα πρέπει να δηµιουργηθούν ξεχωριστά και ανεξάρτητα το καθένα, για παράδειγµα ως εξής: p[0] = new Point(); p[1] = new Point(); Σηµείωση - Υπάρχουν πιο κοµψοί τρόποι για αρχικοποίηση πίνακα που θα εισαχθούν στη συνέχεια. 49
3.3. Αρχικοποίηση πινάκων Όταν δηµιουργείτε έναν πίνακα, κάθε στοιχείο του αρχικοποιείται. Στην περίπτωση του πίνακα s που αποτελείται από char κάθε τιµή αρχικοποιείται στο µηδενικό χαρακτήρα (\u0000, null). Στην περίπτωση του πίνακα p κάθε τιµή αρχικοποιείται σε null, δηλώνοντας ότι δεν αναφέρεται (ακόµα) σε ένα αντικείµενο Point. Μετά την ανάθεση p[0] = new Point() το πρώτο στοιχείο του πίνακα αναφέρεται σε πραγµατικό αντικείµενο. Σηµείωση Η αρχικοποίηση όλων των µεταβλητών, συµπεριλαµβανοµένων των στοιχείων ενός πίνακα, είναι ουσιώδης για την ασφάλεια του συστήµατος. Τίποτα δεν πρέπει να χρησιµοποιηθεί σε µη αρχικοποιηµένη κατάσταση. Ο µεταγλωττιστής δεν µπορεί να ελέγξει µέσα από τη ροή τα στοιχεία ενός πίνακα. Η Java επιτρέπει µία συντόµευση για τη δηµιουργία πινάκων µε αρχικές τιµές. String names[] = Georgianna, Jen, Simon, Tom ; Ο πιο πάνω κώδικας είναι ισοδύναµος µε τον επόµενο: String names[]; names = new String[4]; names[0] = Georgianna ; names[1] = Jen ; names[2] = Simon ; names[3] = Tom ; Η συντόµευση αυτή µπορεί να χρησιµοποιηθεί για οποιονδήποτε τύπο στοιχείου και απαιτεί αναφορά σε κάθε στοιχείο σε κάθε θέση στο µπλοκ αρχικοποίησης. Για παράδειγµα: Color palette[] = Color.blue, Color.red, Color.white 3.4. Πίνακες πολλαπλών διαστάσεων Η Java δεν παρέχει πίνακες πολλαπλών διαστάσεων ρητά, αλλά καθώς ένας πίνακας µπορεί να δηλωθεί ώστε να έχει οποιοδήποτε τύπο βάσης µπορούµε να δηµιουργήσουµε πίνακες από πίνακες (από πίνακες...) int twodim [][] = new int [4][]; twodim[0] = new int[5]; twodim[1] = new int[5]; 50
Το αντικείµενο που δηµιουργείται από την κλήση στο new είναι απλά ένας πίνακας. Ο πίνακας περιέχει τέσσερα στοιχεία. Κάθε ένα από αυτά τα στοιχεία είναι µία null αναφορά σε ένα στοιχείο τύπου array of int (πίνακες από ακεραίους). Συνεπώς κάθε ένα από τα τέσσερα στοιχεία θα πρέπει να δηµιουργηθεί ξεχωριστά. Κάθε ένα από τα στοιχεία αυτά είναι ένα array of int. Σηµείωση Αν και η µορφή της δήλωσης επιτρέπει οι αγκύλες να βρίσκονται στα αριστερά ή στα δεξιά του ονόµατος της µεταβλητής η ευκολία αυτή δεν µεταφέρεται σε άλλες απόψεις του συντακτικού των πινάκων. Για παράδειγµα, το new int [] [4] δεν είναι επιτρεπτή. Εξαιτίας αυτού του διαχωρισµού είναι πιθανό να δηµιουργηθούν µη ορθογώνιοι πίνακες από πίνακες. ηλαδή τα στοιχεία του twodim µπορούν να αρχικοποιηθούν ως εξής: twodim[0] = new int[2]; twodim[1] = new int[4]; twodim[2] = new int[6]; twodim[3] = new int[8]; Καθώς αυτός ο τύπος αρχικοποίησης είναι διαφορετικός από αυτούς που έχουµε συνηθίσει και οι ορθογώνιοι πίνακες από πίνακες είναι η πιο συνηθισµένη µορφή, η Java παρέχει τη συντόµευση για δηµιουργία δισδιάστατων πινάκων. Για παράδειγµα η εντολή: int matrix[][] = new int [4][5]; µπορεί να χρησιµοποιηθεί για να δηµιουργηθεί ένας πίνακας τεσσάρων πινάκων πέντε ακεραίων ο καθένας. 3.5. Όρια πινάκων Στη Java όλοι οι δείκτες των πινάκων ξεκινούν από το µηδέν. Το πλήθος των στοιχείων σε ένα πίνακα αποθηκεύεται ως µέρος του αντικειµένου πίνακα. Η τιµή αυτή χρησιµοποιείται για να πραγµατοποιήσει ελέγχους ορίων σε όλες τις προσβάσεις κατά το χρόνο εκτέλεσης. Αν λάβει χώρα µία πρόσβαση εκτός ορίων τότε παρουσιάζεται µία εξαίρεση. Οι εξαιρέσεις είναι θέµα εποµένου κεφαλαίου. Το µέγεθος ενός πίνακα µπορεί να καθοριστεί κατά το χρόνο εκτέλεσης χρησιµοποιώντας τη µεταβλητή µέλος length. Για παράδειγµα, για επαναληπτική διάσχιση ενός πίνακα µπορούµε να χρησιµοποιήσουµε τον επόµενο κώδικα int list[] = new int [10]; for (int i = 0; i < list.length; i++) // do the work Παρατηρήστε ότι το όριο του βρόχου καθορίζεται µε σύγκριση µε το list.length αντί µε την άµεση τιµή 10. Αυτή η προσέγγιση είναι πιο εύρωστη για αντιµετώπιση της συντήρησης του προγράµµατος. 51
3.6. Αντιγραφή πινάκων Από τη στιγµή που δηµιουργείται ένας πίνακας δεν µπορεί να αλλάξει το µέγεθός του. Παρ όλα αυτά µπορείτε να χρησιµοποιήσετε την ίδια µεταβλητή αναφοράς για να αναφερθείτε σε ένα εντελώς διαφορετικό πίνακα. int elements[] = new int[6]; elements = new int[10]; Στην περίπτωση αυτή ο πρώτος πίνακας ουσιαστικά χάνεται, εκτός και αν υπάρχει κάποια άλλη αναφορά προς αυτόν. Είναι δυνατό να αντιγράψουµε έναν πίνακα µε αποδεκτή απόδοση. Η Java παρέχει µία ειδική µέθοδο στην κλάση System. Η µέθοδος λέγεται arraycopy() και χρησιµοποιείται ως εξής: // original array int elements[] = new int 1, 2, 3, 4, 5, 6;... // new larger array int hold[] = new int 10, 9. 8, 7, 6, 5, 4, 3, 2, 1; // copy all the elements array to the hold array // starting with the 0th index System.arraycopy(elements, 0, hold, 0, elements.length); 52
3.7. Λυµένες Ασκήσεις 3.7.1. Γράψτε ένα πρόγραµµα στο οποίο να δηµιουργήσετε δέκα τυχαίους αριθµούς διπλής ακρίβειας και να τους αποθηκεύσετε σε κατάλληλο πίνακα. Στη συνέχεια χρησιµοποιείστε τον πίνακα για να τους εκτυπώσετε. Εκτυπώστε επίσης τον συνολικό αριθµό των στοιχείων του πίνακα που δηµιουργήσατε χρησιµοποιώντας την µεταβλητή length. Λύση: import java.util.random; public class TestArray public static void main(string[] args) Random rndm = new Random(); double[] x = new double[10]; for(int i=0; i<10; i++) x[i]=rndm.nextdouble(); for(int i=0; i<10; i++) System.out.println("x[" + i + "] = " + x[i]); System.out.println("O arithmos stoixeion tou pinaka einai : " + x.length); Η εκτέλεση του παραπάνω προγράµµατος είναι η ακόλουθη: 53
Προσέξτε πως σε έναν πίνακα µε δέκα στοιχεία αυτά αριθµούνται από το 0 έως το 9. 3.7.2. Γράψτε ένα πρόγραµµα στο οποίο να ορίσετε ένα κατάλληλο String και να αποθηκεύσετε την λέξη Ioannina. Στην συνέχεια να ορίσετε έναν κατάλληλο πίνακα χαρακτήρων και να αποθηκεύσετε σε κάθε στοιχείο τον αντίστοιχο χαρακτήρα του String που ορίσατε (χρησιµοποιείστε την µέθοδο tochararray() της κλάσης String). Στη συνέχεια εκτυπώστε το String και το µήκος του (χρησιµοποιώντας την µέθοδο length() της κλάσης String). Εκτυπώστε επίσης τον συνολικό αριθµό των στοιχείων του πίνακα χαρακτήρων που δηµιουργήσατε (χρησιµοποιώντας την µεταβλητή length) και ένα-ένα τα στοιχεία του. Λύση: public class CharArray public static void main(string[] args) String name = "Ioannina"; char[] a = name.tochararray(); System.out.println("name = \"" + name + "\""); System.out.println("name.length = " + name.length()); System.out.println("a.length = " + a.length); for(int i=0; i<a.length; i++) System.out.println("a[" + i +"] - " + a[i]); Η εκτέλεση του παραπάνω προγράµµατος είναι η ακόλουθη: 54
3.7.3. Γράψτε ένα πρόγραµµα στο οποίο να δηµιουργήσετε δέκα τυχαίους αριθµούς διπλής ακρίβειας και να τους αποθηκεύσετε σε κατάλληλο πίνακα. Χρησιµοποιώντας τον πίνακα να τους εκτυπώσετε. Ανακατατάξτε τον πίνακα σε φθίνουσα σειρά. Εκτυπώστε εκ νέου τον πίνακα. Λύση: import java.util.random; public class TestOrder public static void main(string[] args) Random rndm = new Random(); double[] x = new double[10]; for(int i=0; i<10; i++) x[i]=rndm.nextdouble(); System.out.println("Pinakas 10 tixaion arithmon :"); for(int i=0; i<10; i++) System.out.println("x[" + i +"] = " + x[i]); double temp=0.; for(int i=0; i<10; i++) for(int j=i+1; j<10; j++) if(x[j]>x[i]) temp = x[i]; x[i] = x[j]; x[j] = temp; System.out.println(); System.out.println("Epanektupwsi stoiheivn seira 1"); for(int i=0; i<10; i++) System.out.println("x[" + i +"] = " + x[i]); 55
Η εκτέλεση του παραπάνω προγράµµατος είναι η ακόλουθη: 56
3.7.4. Γράψτε ένα πρόγραµµα στο οποίο να δηµιουργήσετε τον κατάλληλο διδιάστατο πίνακα ακεραίων αριθµών και αποθηκεύσετε σε αυτόν τα παρακάτω δεδοµένα: Εκτυπώστε τον αριθµό των στηλών και των γραµµών του (χρησιµοποιείστε την µεταβλητή length). Εκτυπώστε τα στοιχεία του υπό µορφή στηλών-γραµµών. Λύση: public class TwoDimArray public static void main(string[] args) int[][] a = 15, 22, 43, 10, 56, 98, 34, 43, 82, 65, 19, 39; System.out.println("a.length = " + a.length); System.out.println("a[0].length = " + a[0].length); for(int i=0; i<a.length; i++) for(int j=0; j<a[0].length; j++) System.out.print(a[i][j] + " "); System.out.println(); Η εκτέλεση του παραπάνω προγράµµατος είναι η ακόλουθη: 57
3.7.5. Γράψτε µια µέθοδο η οποία να υπολογίζει την τιµή της συνάρτησης f (n) = 3n2 + 2n + 4. Χρησιµοποιείστε την µέθοδο σε ένα πρόγραµµα για να τυπώσετε την τιµή της συνάρτησης για n=1, 2, 3.10. Λύση: public class Fn public static void main(string[] args) for(int n=1; n<=10; n++) System.out.println("n=" + n + " f(n)=" + fi(n)); static double fi(int i) return Math.sqrt(3*i*i+2*i+4); Η εκτέλεση του παραπάνω προγράµµατος είναι η ακόλουθη: 58
3.7.6. Γράψτε µια µέθοδο η οποία να υπολογίζει την τιµή της συνάρτησης του παραγοντικού. Χρησιµοποιείστε την µέθοδο σε ένα πρόγραµµα για να τυπώσετε την τιµή της συνάρτησης για n=1, 2, 3...8. Λύση: Παρακάτω δίνονται τρεις διαφορετικές λύσεις στο πρόβληµα. Στην πρώτη µέθοδο f1() χρησιµοποιείται η εντολή for ενώ στη δεύτερη µέθοδο f2() η εντολή while. Ενδιαφέρον παρουσιάζει η τρίτη λύση µε την µέθοδο f3() η οποία για να υπολογίσει το παραγοντικό καλεί τον εαυτό της. Όταν µία µέθοδος καλεί τον εαυτό της ονοµάζεται Recursive. public class TestFactorial public static void main(string[] args) for(int n=0; n<=8; n++) System.out.println(n + "! = " + f1(n) + "\t" + f2(n) + "\t" + f3(n)); static double f1(int n) double result=1.; for (int i=1; i<=n; i++) result +=i; return result; static double f2(int n) double result=1; while(n>1) result+=n--; return result; static double f3(int n) if (n<2) return 1.; return n*f3(n-1); 59
Η εκτέλεση του παραπάνω προγράµµατος είναι η ακόλουθη: 3.7.7. Γράψτε µια µέθοδο: static void print_array(double[] x) η οποία να τυπώνει τα στοιχεία ενός πίνακα και µία µέθοδο: static void order_array(double[] x) η οποία να ανακατατάσσει κατ αύξουσα σειρά τα στοιχεία ενός πίνακα αριθµών. Στη συνέχεια γράψτε ένα πρόγραµµα στο οποίο να δηµιουργήσετε δέκα τυχαίους αριθµούς διπλής ακρίβειας και να τους αποθηκεύσετε σε κατάλληλο πίνακα. Εκτυπώστε τον πίνακα χρησιµοποιώντας την µέθοδο print_array(). Ανακατατάξτε τον πίνακα κατ αύξουσα σειρά χρησιµοποιώντας την µέθοδο order_array(). Εκτυπώστε εκ νέου τον πίνακα χρησιµοποιώντας την µέθοδο print_array(). Λύση: import java.util.random; public class ArrayOrder public static void main(string[] args) Random rndm = new Random(); double[] x = new double[10]; for(int i=0; i<10; i++) x[i]=rndm.nextdouble(); System.out.println("Pinakas 10 tixaion arithmwn :"); print_array(x); order_array(x); System.out.println(); System.out.println("Epanektupwsi stoixeiwn se auxousa seira :"); 60
print_array(x); static void print_array(double[] x) int n = x.length; for(int i=0; i<n; i++) System.out.println("x[" + i + "] = " + x[i]); static void order_array(double[] a) int n = a.length; double temp=0.; for(int i=0; i<n; i++) for(int j=i+1; j<n; j++) if(a[j]<a[i]) temp = a[i]; a[i] = a[j]; a[j] = temp; 61
Η εκτέλεση του παραπάνω προγράµµατος είναι η ακόλουθη: 3.7.8. Γράψτε µια µέθοδο: static int max(int i, int j) η οποία να επιστέφει τον µεγαλύτερο αριθµητικά από δύο ακεραίους αριθµούς και µία µέθοδο: static int max(int i, int j, int k) η οποία να επιστέφει τον µεγαλύτερο αριθµητικά από τρεις ακεραίους αριθµούς. Στη συνέχεια γράψτε ένα πρόγραµµα στο οποίο να δηµιουργήσετε τρεις τυχαίους ακεραίους αριθµούς. Εκτυπώστε τον µεγαλύτερο από τους δύο πρώτους ακεραίους χρησιµοποιώντας την πρώτη µέθοδο max(). Στη συνέχεια εκτυπώστε τον µεγαλύτερο από τους τρεις ακεραίους χρησιµοποιώντας την δεύτερη µέθοδο max(). Στη Java είναι δυνατόν να έχουµε µεθόδους µε τον ίδιο όνοµα αλλά µε διαφορετικό αριθµό ορισµάτων. Αυτό ονοµάζεται Overloading. 62
Λύση: import java.util.random; public class TestMax public static void main(string[] args) Random rndm = new Random(); int n1 = rndm.nextint(); int n2 = rndm.nextint(); int n3 = rndm.nextint(); System.out.print("O megaluteros apo tous arithmous: "); System.out.println(n1 + " " + n2); System.out.println("einai o : " + max(n1,n2)); System.out.print("O megaluteros apo tous arithmous: "); System.out.println(n1 + " " + n2 + " " + n3); System.out.println("einai o : " + max(n1,n2,n3)); static int max(int i, int j) if(i<j) return j; else return i; static int max(int i, int j, int k) return max(max(i,j),k); Η εκτέλεση του παραπάνω προγράµµατος είναι η ακόλουθη: 63
3.8 Ασκήσεις 1. ηµιουργήστε µία κλάση BasicArray. Στη µέθοδο main() ορίστε δύο µεταβλητές µε όνοµα thisarray και thatarray. Θα πρέπει να είναι τύπου array of int. 2. ηµιουργήστε ένα ακόµα πίνακα µε δέκα int τιµές στο εύρος από 1 έως 10. Αναθέστε την αναφορά αυτού του τρίτου πίνακα στο thisarray. 3. Χρησιµοποιήστε ένα βρόχο for() για να εκτυπώσετε όλες τις τιµές του thisarray. Πώς θα ελέγξετε το όριο του βρόχου; 4. Μεταγλωττίστε και εκτελέστε το πρόγραµµα. Πόσες τιµές εκτυπώθηκαν; Ποιες είναι οι τιµές αυτές; 5. Για κάθε στοιχείο του thisarray θέστε την τιµή του να είναι το παραγοντικό της τιµής του δείκτη. Εκτυπώστε τις τιµές του πίνακα. 6. Μεταγλωττίστε και εκτελέστε το πρόγραµµα. 7. Αναθέστε την τιµή του thisarray στη µεταβλητή του thatarray. Εκτυπώστε όλα τα στοιχεία του thatarray. 8. Μεταγλωττίστε και εκτελέστε το πρόγραµµα. Πόσες τιµές παρουσιάζονται για το thatarray; Ποιες είναι αυτές οι τιµές και πώς προέκυψαν; 9. Τροποποιείστε ορισµένα από τα στοιχεία του thisarray. Εκτυπώστε τις τιµές του thatarray. 10. Μεταγλωττίστε και εκτελέστε το πρόγραµµα. Τι παρατηρείτε για το thatarray; 11. ηµιουργείστε έναν πίνακα από 20 ακεραίους. Αναθέστε την αναφορά του νέου πίνακα στη µεταβλητή thatarray. Εκτυπώστε τις τιµές και των δύο πινάκων. 12. Μεταγλωττίστε και εκτελέστε το πρόγραµµα. Πόσες τιµές παρουσιάζονται για κάθε πίνακα; Ποιες είναι αυτές οι τιµές; 13. Αντιγράψτε τις τιµές του thisarray στο thatarray. Ποια κλήση µεθόδου θα χρησιµοποιήσετε για να το επιτύχετε; Πώς θα περιορίσετε το πλήθος των µεθόδων που αντιγράφονται; Τι γίνεται στα στοιχεία 10 έως 19 του thatarray; 14. Εκτυπώστε τις τιµές του thatarray. 15. Μεταγλωττίστε και εκτελέστε το πρόγραµµα. Είχατε δίκιο σχετικά µε τις τιµές που παρουσιάζονται; Αν όχι, καταλαβαίνετε τι έχει συµβεί και τι ήταν αυτό που παρανοήσατε; 16. Αλλάξτε ορισµένες από τις µεταβλητές στο thatarray. Εκτυπώστε αµφότερα τα thisarray και thatarray. 17. Μεταγλωττίστε και εκτελέστε το πρόγραµµα. Είναι οι τιµές αυτές που αναµένατε; 18. ηµιουργείστε µία κλάση Array2D. Στη µέθοδο main() δηλώστε µία µεταβλητή που ονοµάζεται twod, τύπου πίνακα από πίνακα από ακέραιους. ηλώστε µία δεύτερη µεταβλητή και ονοµάστε την oned, τύπου πίνακα από ακεραίους. 19. ηµιουργήστε ένα πίνακα µε στοιχεία τύπου array of int. Ο πίνακας θα πρέπει να έχει τέσσερα στοιχεία. Αναθέστε την αναφορά στον πίνακα αυτό στο στοιχείο [0] της µεταβλητής twod. 20. Γράψτε δύο ένθετους βρόχους for για να εκτυπώσετε όλες τις τιµές του twod. Παρουσιάστε την έξοδο ως µήτρα. Η µέθοδος System.out.print θα σας φανεί χρήσιµη εδώ. 21. Μεταγλωττίστε και εκτελέστε το πρόγραµµα. Θα πρέπει να βρείτε ότι προκαλείται µία εξαίρεση δείκτη null καθώς τα στοιχεία [1] έως [3] του twod δεν είναι αρχικοποιηµένα. 64
22. ηµιουργήστε επιπλέον πίνακες ακεραίων που να περιέχουν πέντε, έξι και επτά στοιχεία αντίστοιχα. Αναθέστε τις αναφορές τους στα στοιχεία [1], [2] και [3] του twod, αντίστοιχα. Βεβαιωθείτε ότι ο σχετικός κώδικας έχει εισαχθεί πριν τον ένθετο βρόχο for που περιγράψαµε στο 3. 23. Μεταγλωττίστε και εκτελέστε το πρόγραµµα. Αυτή τη φορά θα πρέπει να δείτε µία µη ορθογώνια παρουσίαση µηδενικών τιµών. 24. Αναθέστε σε κάθε στοιχείο των πινάκων του twod µία διακριτή µη µηδενική τιµή. 25. Αναθέστε, µε τη σειρά, τις αναφορές στα τέσσερα στοιχεία του πίνακα twod στη µεταβλητή oned. Αµέσως µετά από κάθε ανάθεση µε χρήση ενός βρόχου for παρουσιάστε τα περιεχόµενα του oned. Τοποθετείστε το σχετικό κώδικα πριν τον κώδικα που περιγράψαµε στο 3. 26. Μεταγλωττίστε και εκτελέστε το πρόγραµµα. Παρατηρήστε ότι οι ανεξάρτητοι πίνακες που παρουσιάζονται µε εκτύπωση των τιµών του oned είναι οι ίδιοι µε τα στοιχεία του πίνακα στο twod. 27. ηµιουργήστε µία εφαρµογή που έχει ένα πίνακα λέξεων (µε οκτώ χαρακτήρες µήκος το πολύ) που θα αλλάζει τη σειρά των χαρακτήρων και θα παρουσιάζει τη νέα λέξη στο χρήστη. 28. Επιτρέψτε στο χρήστη να δει τον αναγραµµατισµό και δώστε του 5 προσπάθειες. 65
66
4. Αντικείµενα και Κλάσεις 4.1. Αντικειµενοστρεφής προγραµµατισµός Το µοντέλο αντικειµενοστρεφούς ανάπτυξης λογισµικού (object-oriented software development) αποσκοπεί στην οργάνωση του λογισµικού σε ένα σύνολο από αλληλεπιδρώντα αντικειµένα (objects). Η επιλογή αυτών των αντικειµένων υπαγορεύεται συνήθως από το φυσικό κόσµο και το πρόβληµα που πρόκειται να επιλυθεί. Κάθε αντικείµενο περιέχει κάποια δεδοµένα, που χαρακτηρίζουν την κατάστασή του, και υλοποιεί κάποια συµπεριφορά µέσω κατάλληλων τµηµάτων κώδικα. Βασικό χαρακτηριστικό του αντικειµενοστρεφούς προγραµµατιστικού µοντέλου, εποµένως, είναι η αφαιρετική από κοινού αντιµετώπιση της κατάστασης και της συµπεριφοράς των αντικειµένων, που ονοµάζεται κελυφοποίηση (encapsulation). Συχνά στην ανάπτυξη λογισµικού παρουσιάζεται η ανάγκη για την ύπαρξη περισσοτέρων αντικειµένων που είναι της ίδιας ή παρόµοιας µορφής. Για το λόγο αυτό, εξέχουσα θέση στο µοντέλο αντικειµενοστρεφούς ανάπτυξης λογισµικού κατέχει η έννοια της κλάσης (class). Οι κλάσεις είναι κατηγορίες αντικειµένων µε την ίδια συµπεριφορά: αντικείµενα που προέρχονται από την ίδια κλάση είναι δυνατό να διαφέρουν µόνο στην κατάσταση. Εξέχουσα θέση έχουν επίσης οι έννοιες της κληρονοµικότητας (inheritance) και του πολυµορφισµού υπο-τύπων (subtype polymorphism), που θα περιγραφούν σε επόµενες ενότητες. Με τη δηµιουργία κλάσεων και ιεραρχιών από κλάσεις, όπως θα δούµε στη συνέχεια, το µοντέλο αντικειµενοστρεφούς ανάπτυξης λογισµικού συµβάλλει άµεσα προς την κατεύθυνση της επαναχρησιµοποίησης λογισµικού (software reusability). Κατά την ανάλυση και τη σχεδίαση, σκεφτείτε και αντιµετωπίστε τα αντικείµενα µε βάση τη διαπροσωπεία (interface) τους, και όχι µε βάση την υλοποίησή (implementation) τους. Η διαπροσωπεία ενός αντικειµένου είναι µια περιγραφή του τρόπου µε το οποίο αυτό αλληλεπιδρά µε άλλα αντικείµενα και οφείλει να είναι εύκολα κατανοητή. Η υλοποίηση ενός αντικειµένου, φυσικά, µπορεί να είναι εξαιρετικά πολύπλοκη. Για κάθε αντικείµενο, προσπαθείστε να κρύψετε όσο το δυνατόν µεγαλύτερο τµήµα από την υλοποίησή του. Με αυτό τον τρόπο, µπορείτε αργότερα να αλλάξετε την υλοποίηση του αντικειµένου χωρίς κακά συνεπακόλουθα. Προσπαθείστε να χρησιµοποιείτε αντικείµενα που έχουν ήδη υλοποιηθεί. Εξειδικεύστε τα και δηµιουργείστε νέα αντικείµενα µόνο αν δε γίνεται διαφορετικά. Προσπαθείστε να ελαχιστοποιήσετε τη διασύνδεση και την αλληλεπίδραση µεταξύ αντικειµένων. Τα αντικείµενα πρέπει να είναι όσο το δυνατόν αυτοτελή. 4.2. Αντικείµενα και κλάσεις Προκειµένου να χρησιµοποιηθεί ένα αντικείµενο στη Java είναι απαραίτητο να ορισθεί µια κλάση που να το περιγράφει. Οι κλάσεις, όπως αναφέρθηκε στην προηγούµενη ενότητα, είναι κατηγορίες αντικειµένων µε την ίδια συµπεριφορά. Αν και η κατάστασή τους είναι δυνατό να διαφέρει από αντικείµενο σε αντικείµενο, οφείλει γενικά να έχει την ίδια µορφή. Οι κλάσεις ορίζονται στη Java µε χρήση της 67
δήλωσης class. Κάθε κλάση χαρακτηρίζεται από ένα µοναδικό όνοµα. Τα περιεχόµενα µιας κλάσης ονοµάζονται µέλη της κλάσης (class members) και µπορούν να είναι µεταβλητές (variables), που χαρακτηρίζουν την κατάσταση των αντικειµένων, ή µέθοδοι (methods), που χαρακτηρίζουν τη συµπεριφορά τους. Στην επόµενη δήλωση ορίζεται µια κλάση µε όνοµα Counter, που υλοποιεί ένα µετρητή. Περιέχει τη µεταβλητή value, στην οποία φυλάσσεται η τρέχουσα τιµή του µετρητή και η οποία αρχικοποιείται στο 0. Επίσης, περιέχει δυο µεθόδους inc και get, για την αύξηση και την επιστροφή της τιµής του µετρητή αντίστοιχα. Ας σηµειωθεί ότι στη δήλωση των µεθόδων inc και get συµπεριλαµβάνεται ο κώδικας που τις υλοποιεί. Η κλάση Counter είναι µια πολύ απλή κλάση. Ορισµοί πιο πολύπλοκων κλάσεων µπορούν να περιέχουν πολλές σελίδες κώδικα. class Counter int value = 0; void inc () value++; int get () return value; Αφού ορισθεί η κλάση Counter, είναι δυνατή η κατασκευή αντικειµένων που προέρχονται από αυτή την κλάση. Τα αντικείµενα αυτά ονοµάζονται και στιγµιότυπα (instances) της κλάσης και, στην προκειµένη περίπτωση, είναι ανεξάρτητοι µετρητές. Η διαδικασία κατασκευής αντικειµένων περιγράφεται στην επόµενη ενότητα. Η χρήση των αντικειµένων µέσα στον κώδικα γίνεται µέσω µεταβλητών τύπου αναφοράς, που περιέχουν αναφορές στα αντικείµενα. Οι µεταβλητές που δηλώνονται στο εσωτερικό µιας κλάσης χαρακτηρίζουν την κατάσταση των αντικειµένων της κλάσης. Μπορούν να είναι οποιουδήποτε έγκυρου τύπου και µπορούν να αρχικοποιηθούν. Οι µέθοδοι υλοποιούν τη συµπεριφορά του αντικειµένου και είναι κώδικα που µπορούν να κληθούν. έχονται έναν αριθµό παραµέτρων (στην περίπτωση της κλάσης Counter και οι δυο µέθοδοι δε δέχονται παραµέτρους) και επιστρέφουν ένα αποτέλεσµα κάποιου τύπου (η µέθοδος get επιστρέφει αποτέλεσµα τύπου int), εκτός αν δηλωθούν ως void, οπότε δεν επιστρέφουν κανένα αποτέλεσµα (όπως η µέθοδος inc). Στο εσωτερικό των µεθόδων, οι µεταβλητές των αντικειµένων είναι ορατές και προσπελαύνονται µε τη χρήση µόνο του ονόµατός τους. Αν υποτεθεί ότι η µεταβλητή c είναι µια αναφορά σε ένα αντικείµενο της κλάσης Counter, τότε η προσπέλαση της µεταβλητής value αυτού του αντικειµένου γίνεται µε την έκφραση c.value, όπως για παράδειγµα στην εντολή: if (c.value > 10) c.value = 0; Φυσικά, στην κλάση Counter οι µέθοδοι inc και get παρέχουν τις επιθυµητές λειτουργίες και η µεταβλητή value θα ήταν καλό να µην είναι προσπελάσιµη, αλλά στο θέµα αυτό θα επανέλθουµε αργότερα. Με παρόµοιο τρόπο γίνεται η κλήση µεθόδων. Για παράδειγµα η κλήση της µεθόδου get για το αντικείµενο στο οποίο αναφέρεται η µεταβλητή c γίνεται µε την έκφραση c.get(). Στο εσωτερικό των παρενθέσεων τοποθετούνται οι πραγµατικές τιµές 68
των παραµέτρων, αν αυτές υπάρχουν. Η τιµή αυτής της έκφρασης είναι αυτή που επιστρέφεται από το σώµα της µεθόδου, µέσω της εντολής return, και είναι του ίδιου τύπου µε αυτόν που δηλώνεται στον ορισµό της µεθόδου. Μέθοδοι που δεν επιστρέφουν αποτέλεσµα µπορούν να χρησιµοποιηθούν ως εντολές εκφράσεων. Ένα εκτενέστερο παράδειγµα που χρησιµοποιεί την κλάση Counter είναι το ακόλουθο: c.inc(); System.out.println(c.get()); Προκειµένου να φανεί η χρήση παραµέτρων, ας ξαναγράψουµε την κλάση Counter, συµπεριλαµβάνοντας επιπλέον µια ακόµα µέθοδο: void set (int newvalue) value = newvalue; Η µέθοδος αυτή τοποθετεί µια νέα τιµή στο µετρητή. Η παράµετρος newvalue έχει τύπο int και καθορίζει ακριβώς ποια θα είναι αυτή η τιµή. Για να κληθεί η µέθοδος set πρέπει να δοθεί µια συγκεκριµένη πραγµατική τιµή στην παράµετρο newvalue, όπως φαίνεται στον κώδικα που ακολουθεί: c.set(5); if (c.get()!= 5) // die a horrible death, everything was a lie! Ο κώδικας µιας µεθόδου είναι δυνατό να περιέχει τη δήλωση νέων µεταβλητών. Οι µεταβλητές αυτές ονοµάζονται τοπικές (local) και είναι ορατές µόνο µέσα στον κώδικα της συγκεκριµένης µεθόδου. Πρέπει να αρχικοποιηθούν ρητά µέσα στο πρόγραµµα και δε διατηρούν την τιµή τους µεταξύ διαδοχικών κλήσεων. Με άλλα λογια, η διάρκεια ζωής µιας τοπικής µεταβλητής συµπίπτει µε το χρόνο κατά τον οποίο πραγµατοποιείται η κλήση της µεθόδου. Για παράδειγµα, ας θεωρήσουµε µια υποθετική παραλλαγή της µεθόδου set, η οποία θα αλλάζει την τιµή του µετρητή και θα επιστρέφει την παλιά τιµή. Για το λόγο αυτό απαιτείται η τοπική µεταβλητή oldvalue, στην οποία φυλάσσεται η παλιά τιµή του µετρητή για να µη χαθεί από την εκχώρηση. int set (int newvalue) int oldvalue = value; value = newvalue; return oldvalue; Οι µεταβλητές διαφορετικών αντικειµένων που προέρχονται από µια κλάση είναι εντελώς ανεξάρτητες. Μερικές φορές όµως, είναι χρήσιµο κάποιες µεταβλητές να είναι κοινές για όλα τα αντικείµενα µιας κλάσης. Επίσης, είναι χρήσιµο η κλήση κάποιων µεθόδων να µην αναφέρεται σε συγκεκριµένα αντικείµενα. Αυτές οι ανάγκες υλοποιούνται µέσω των µεταβλητών και των µεθόδων που δηλώνονται ως static. Ας θεωρήσουµε την ακόλουθη έκδοση του µετρητή Counter: class CounterLimited 69
static int limit = 100; int value = 0; void inc () if (++value >= limit) value = 0; int get () return value; static void setlimit (int l) limit = l; Στους µετρητές που προέρχονται από αυτή την κλάση, η τιµή µηδενίζεται όταν υπερβεί κάποιο όριο. Η τιµή αυτού του άνω όριου καθορίζεται από τη µεταβλητή limit και είναι κοινή για όλα τα αντικείµενα της κλάσης. Η µέθοδος setlimit, που πάλι είναι ανεξάρτητη αντικειµένου, µπορεί να χρησιµοποιηθεί για την αλλαγή της τιµής του άνω ορίου. Τόσο η µεταβλητή limit όσο και η µέθοδος setlimit µπορούν να προσπελασθούν είτε µέσω µιας αναφοράς σε ένα αντικείµενο τύπου CounterLimited, όπως και οι κοινές µεταβλητές και µέθοδοι, είτε µέσω του ονόµατος της ίδιας της κλάσης, όπως για παράδειγµα: CounterLimited.setLimit(1000); if (CounterLimited.limit!= 1000) // go get a beer and forget about Java! Αν η αρχικοποίηση µεταβλητών που δηλώνονται ως static δεν είναι δυνατό να πραγµατοποιηθεί κατά τη δήλωσή τους, η Java υποστηρίζει τµήµατα κώδικα που δηλώνονται ως static και συµπεριλαµβάνονται στον ορισµό των κλάσεων. Σε κάθε κλάση µπορεί να ορισθεί ένα τέτοιο τµήµα, που µπορεί να προσπελαύνει µόνο µεταβλητές και µεθόδους δηλωµένες ως static. Το τµήµα αυτό θα εκτελεσθεί την πρώτη φορά που θα φορτωθεί η κλάση. Παράδειγµα τέτοιου τµήµατος κώδικα δίνεται στην επόµενη παραλλαγή της κλάσης Counter: class NewCounterLimited static int limit; static System.out.println("What do you think a fair limit is?"); limit = user.askaboutthelimit(); // rest of the class as usual... Το τµήµα κώδικα που δηλώνεται ως static αρχικοποιεί τη µεταβλητή limit ρωτώντας το χρήστη για την επιθυµητή τιµή της, µέσω µιας υποθετικής µεθόδου. 4.2.1. Υπερφόρτωση µεθόδων Με τον όρο υπερφόρτωση µεθόδων (method overloading) εννοούµε τη δυνατότητα ορισµού περισσότερων µεθόδων µε το ίδιο όνοµα µέσα στην ίδια κλάση. Κάτι τέτοιο είναι ιδιαίτερα χρήσιµο στην περίπτωση που οι µέθοδοι υλοποιούν παρόµοιες λειτουργίες για ορίσµατα που ανήκουν σε διαφορετικούς τύπους. Στην περίπτωση αυτή, η επιλογή της µεθόδου που καλείται γίνεται από το µεταγλωττιστή, ανάλογα µε τον τύπο των ορισµάτων. 70
Η Java υποστηρίζει υπερφόρτωση µεθόδων, αρκεί οι υπερφορτωµένες µέθοδοι να διαφέρουν στον αριθµό ή/και τον τύπο των ορισµάτων. Αυτός ο τύπος υπερφόρτωσης συχνά ονοµάζεται παραµετρικός πολυµορφισµός (parametric polymorphism). Παράδειγµα υπερφορτωµένης µεθόδου είναι η println, που χρησιµοποιείται για την εκτύπωση δεδοµένων. Στην πραγµατικότητα, η κλάση PrintStream της Java περιέχει πολλές παραλλαγές της µεθόδου, µια για κάθε διαφορετικό τύπο ορίσµατος του οποίου η εκτύπωση είναι επιθυµητή: class PrintStream... void println (int i) /* print an int */ void println (double d) /* print a double */ void println (String s) /* print a string */... Η υπερφόρτωση µεθόδων είναι ένα πολύτιµο εργαλείο της Java. εν πρέπει όµως να γίνεται κατάχρηση. Αν κάποιες µέθοδοι δεν υλοποιούν παρόµοιες λειτουργίες, η υπερφόρτωση του ίδιου ονόµατος οδηγεί συνήθως σε παρερµηνείες και πρέπει να αποφεύγεται. 4.2.2. Κατασκευή αντικειµένων Αφού ορισθεί µια κλάση, είναι δυνατό να κατασκευασθούν αντικείµενα που προέρχονται από αυτήν. Για να κατασκευασθεί ένα αντικείµενο, απαιτούνται συνήθως δυο βήµατα: ήλωση µιας µεταβλητής µε τύπο αναφοράς, το οποίο θα αναφέρεται στο αντικείµενο που θα κατασκευασθεί, π.χ. Counter c; Υπενθυµίζεται ότι η δήλωση αυτή δεν κατασκευάζει κανένα αντικείµενο. Η τιµή της µεταβλητής c, αν αρχικοποιηθεί αυτόµατα, παίρνει την τιµή null. Κατασκευή του αντικειµένου µε χρήση του τελεστή new και εκχώρηση της αναφοράς στη µεταβλητή. c = new Counter(); Φυσικά, τα δυο βήµατα µπορούν να συνενωθούν σε µια δήλωση µε αρχικοποίηση: Counter c = new Counter(); Στη συνέχεια, το αντικείµενο µπορεί να χρησιµοποιηθεί µέσω της αναφοράς που βρίσκεται στη µεταβλητή c. Ας σηµειωθεί ότι µπορούν να υπάρχουν περισσότερες αναφορές στο ίδιο αντικείµενο, όπως γίνεται αντιληπτό στο ακόλουθο τµήµα κώδικα: Counter other = c; // refers to the same counter c.set(42); if (other.get()!= 42) // it's the end of the world as we know it... Η κατασκευή ενός αντικειµένου µε τον τελεστή new προκαλεί την αρχικοποίηση των µεταβλητών του. Οι τιµές που χρησιµοποιούνται για την αρχικοποίηση µπορούν να καθορισθούν κατά τη δήλωση των µεταβλητών. Αν δε 71
συµβεί αυτό, οι µεταβλητές αρχικοποιούνται αυτόµατα. Μερικές φορές όµως, η κατασκευή ενός αντικειµένου απαιτεί κάποια ιδιαίτερη αρχικοποίηση, που δεν είναι δυνατό να πραγµατοποιηθεί µε τον τρόπο αυτό. Την ανάγκη αυτή καλύπτει ένα ιδιαίτερο είδος µεθόδου, που ονοµάζεται κατασκευαστής (constructor). Οι κατασκευαστές είναι µέθοδοι που έχουν το όνοµα της αντίστοιχης κλάσης. Μπορούν να έχουν παραµέτρους, αλλά δεν επιστρέφουν αποτέλεσµα και αυτό δε χρειάζεται να δηλωθεί. Στο ακόλουθο τµήµα κώδικα, ορίζεται πάλι η κλάση Counter συµπεριλαµβάνοντας έναν κατασκευαστή. Αυτός δέχεται ως παράµετρο την αρχική τιµή του µετρητή. class Counter int value; Counter (int v) value = v;... Στη συνέχεια, για να κατασκευαστεί ένα αντικείµενο απαιτείται ο καθορισµός της παραµέτρου του κατασκευαστή, δηλαδή της αρχικής τιµής του µετρητή, όπως σε κάθε κλήση µεθόδου. Για παράδειγµα: Counter c = new Counter(1); Οι κατασκευαστές είναι δυνατό να υπερφορτωθούν, όπως και οι απλές µέθοδοι. Για παράδειγµα, ας θεωρήσουµε την ακόλουθη παραλλαγή της κλάσσης Counter, µε δυο κατασκευαστές: ο πρώτος δέχεται ως παράµετρο την αρχική τιµή του µετρητή, ενώ ο δεύτερος δε δέχεται παραµέτρους και θεωρεί ότι η αρχική τιµή είναι 0. Και στις δυο περιπτώσεις, ο κατασκευαστής ανακοινώνει την κατασκευή ενός νέου αντικειµένου. class HappyCounter int value; HappyCounter (int v) System.out.println("Creating a new counter.") value = v; HappyCounter () System.out.println("Creating a new counter.") value = 0;... ιαπιστώνει κανείς ότι, στον παραπάνω ορισµό, ο δεύτερος κατασκευαστής είναι µια υποπερίπτωση του πρώτου, όπου η τιµή της παραµέτρου v είναι 0. Η περίπτωση αυτή, όπου ένας κατασκευαστής µπορεί να χρησιµοποιηθεί για την 72
υλοποίηση ενός άλλου, εµφανίζεται συχνά στην πράξη και υποστηρίζεται από τη Java. Ο δεύτερος κατασκευαστής θα µπορούσε να γραφεί ως: HappyCounter () this(0); όπου η εντολή this(0); είναι µια κλήση στον πρώτο κατασκευαστή µε τιµή παραµέτρου 0. Τέτοιου είδους κλήσεις είναι υποχρεωτικά οι πρώτες εντολές µέσα στο σώµα ενός κατασκευαστή. Έχοντας τους δυο κατασκευαστές για την κλάση HappyCounter, είναι δυνατή η επιλογή του κατασκευαστή που θα χρησιµοποιηθεί: HappyCounter c1 = new HappyCounter(); // value = 0 HappyCounter c2 = new HappyCounter(42); // value = 42 Η ειδική µεταβλητή this µπορεί να χρησιµοποιηθεί οπουδήποτε στο κύριο σώµα µιας µεθόδου. Περιέχει πάντα µια αναφορά στο ίδιο το αντικείµενο, για το οποίο καλείται η µέθοδος. Για παράδειγµα, η µέθοδος get της κλάσης Counter θα µπορούσε να γραφεί ως: int get () return this.value; αν και κάτι τέτοιο θα ήταν µάλλον περιττό. 4.2.3. Καταστροφή αντικειµένων Αντίθετα µε την κατασκευή αντικειµένων, που πρέπει να γίνει ρητά µέσα στον κώδικα µε χρήση του τελεστή new, η καταστροφή των αντικειµένων γίνεται αυτόµατα από το περιβάλλον εκτέλεσης της Java όταν τα αντικείµενα πάψουν να χρησιµοποιούνται. Το περιβάλλον εκτέλεσης θεωρεί ότι ένα αντικείµενο παύει να χρησιµοποιείται όταν δεν υπάρχει καµιά µεταβλητή που να περιέχει αναφορά σε αυτό. Αν συµβαίνει κάτι τέτοιο, τότε δεν υπάρχει τρόπος πρόσβασης στο αντικείµενο και αυτό µπορεί ασφαλώς να καταστραφεί. Το τµήµα του περιβάλλοντος εκτέλεσης της Java που αναλαµβάνει την παρακολούθηση της χρήσης των αντικειµένων και την καταστροφή τους όταν πάψουν να χρησιµοποιούνται ονοµάζεται συλλέκτης σκουπιδιών (garbage collector). Ο συλλέκτης σκουπιδιών εκτελείται συνήθως παράλληλα µε το πρόγραµµα και απελευθερώνει τη µνήµη που καταλαµβάνεται από άχρηστα αντικείµενα. Μπορεί όµως να ενεργοποιηθεί ρητά µε κλήση της µεθόδου: System.gc(). Μερικές φορές η καταστροφή των αντικειµένων από το συλλέκτη σκουπιδιών δεν είναι αρκετή. Ορισµένα αντικείµενα είναι δυνατό να χρειάζονται ειδική µεταχείριση κατά την καταστροφή τους, π.χ. για να κλείσουν κάποια αρχεία ή για να απελευθερώσουν πόρους του συστήµατος που έχουν στην κατοχή τους. Για το λόγο αυτό η Java υποστηρίζει τη µέθοδο finalize, που καλείται όταν ένα αντικείµενο καταστρέφεται. Η µέθοδος αυτή µπορεί να περιέχει κώδικα που αναλαµβάνει την εκκαθάριση των ανοικτών λογαριασµών των αντικειµένων, πριν αυτά καταστραφούν. Όµως, επειδή η διαδικασία της συλλογής σκουπιδιών ελέγχεται απευθείας από το περιβάλλον εκτέλεσης, ο ακριβής χρόνος της κλήσης της µεθόδου finalize δεν είναι γνωστός. 73
4.2.4. Μετατροπείς ορατότητας Ο κώδικας που βρίσκεται στο εσωτερικό των µεθόδων µιας κλάσης έχει πάντα πρόσβαση στα µέλη της κλάσης. Όµως, η δυνατότητα πρόσβασης στα µέλη µιας κλάσης από κώδικα που δεν ανήκει σε µεθόδους της δεν είναι πάντα χρήσιµη. Μερικές φορές, στο πλαίσιο της κελυφοποίησης των αντικειµένων, είναι σκόπιµη η απόκρυψη της υλοποίησης µιας κλάσης και η παροχή προς τα έξω µόνο ορισµένων µεθόδων, που αποτελούν τη διαπροσωπεία της κλάσης. Για το λόγο αυτό η Java υποστηρίζει ένα σύνολο µετατροπέων ορατότητας (visibility modifiers). Μέχρι τώρα, σε όλες τις κλάσεις που ορίστηκαν δε χρησιµοποιήθηκε κανένας µετατροπέας ορατότητας. Τα µέλη µιας κλάσης που ορίζονται κατ αυτό τον τρόπο είναι ορατά από κάθε τµήµα κώδικα, είτε ανήκει στην κλάση αυτή είτε όχι.5 Κατά συνέπεια, η τιµή της µεταβλητής value κάποιου αντικειµένου που προέρχεται από την κλάση Counter είναι δυνατό να µεταβληθεί µε αυθαίρετο τρόπο, χωρίς να κληθούν οι µέθοδοι της κλάσης. Αυτό είναι ανεπιθύµητο στην περίπτωση αυτή και θα διορθωθεί στην ακόλουθη παραλλαγή της κλάσης, που χρησιµοποιεί τον µετατροπέα ορατότητας private. class Counter private int value; Counter () value = 0; Counter (int v) value = v; void inc () value++; int get () return value; Στη νέα έκδοση της κλάσης, η µεταβλητή value δηλώνεται ως private. Αυτό σηµαίνει ότι είναι ορατή µόνο από το εσωτερικό της κλάσης, και κατά συνέπεια, το επόµενο τµήµα κώδικα οδηγεί σε σφάλµα µεταγλώττισης: Counter c = new Counter(); c.value = 42; // compilation error! Φυσικά είναι δυνατός ο ορισµός µεθόδων µε το µετατροπέα private. Στην περίπτωση αυτή, οι µέθοδοι µπορούν να καλούνται µόνο µέσα από την κλάση Counter. Εκτός από τον private, υπάρχουν και άλλοι µετατροπείς ορατότητας. Η περιγραφή τους όµως αναβάλλεται για λίγο, γιατί πρέπει να προηγηθεί ο ορισµός των ιεραρχιών κλάσεων και του µηχανισµού οργάνωσης κώδικα σε πακέτα. 4.3. Ιεραρχίες κλάσεων Ένα από τα βασικά χαρακτηριστικά του µοντέλου αντικειµενοστρεφούς ανάπτυξης προγραµµάτων είναι η υποστήριξη της κληρονοµικότητας (inheritance). Η κληρονοµικότητα, µεσω της οποίας οι κλάσεις σχηµατίζουν ιεραρχίες, καλύπτει πρωτίστως την ανάγκη εξειδίκευσης των κλάσεων, χρησιµοποιώντας κατά τον καλύτερο δυνατό τρόπο ήδη ορισµένες κλάσεις. Ας τα δούµε όλα από την αρχή µε ένα παράδειγµα. Ας υποτεθεί ότι η κλάση Counter έχει ήδη ορισθεί και χρησιµοποιηθεί σε έναν αριθµό εφαρµογών µε µεγάλη επιτυχία. Κάποια στιγµή όµως, σε µια νέα εφαρµογή παρουσιάζεται η ανάγκη για µετρητές που, εκτός από µια 74
µέθοδο inc για την αύξηση της τιµής τους, απαιτούν και µια µέθοδο dec για τη µείωση. Η προφανής λύση του προβλήµατος είναι να προστεθεί µια νέα µέθοδος στην κλάση Counter. Αυτό όµως δεν είναι πάντα επιθυµητό, και, σε µερικές περιπτώσεις δεν είναι καν εφικτό. Μερικά πιθανά επιχειρήµατα µιας υποθετικής οµάδας ανάπτυξης λογισµικού κατά u964 της τροποποίησης της κλάσης Counter είναι τα ακόλουθα: Η κλάση Counter έχει χρησιµοποιηθεί αρκετά στις εφαρµογές µας. Για ποιο λόγο να επέµβουµε σε κώδικα που λειτουργεί µε επιτυχία, µε κίνδυνο οι αλλαγές που θα γίνουν να καταστρέψουν την ευστάθεια των ήδη υπάρχουσων εφαρµογών; Σηµειώνεται ότι οι εφαρµογές Java είναι ιδιαίτερα ευπαθείς σε τέτοιες µεταβολές, αφού ο κώδικας των κλάσεων φορτώνεται δυναµικά. Η κλάση Counter είναι µια αυτοτελής κλάση και κάλυψε πλήρως τις ανάγκες µας µέχρι τώρα. Σε πληθώρα περιπτώσεων, η µέθοδος dec είναι άχρηστη. Γιατί λοιπόν να επιβαρύνουµε την κλάση µε κάτι που συνήθως είναι περιττό; Η κλάση Counter δεν είναι καν δική µας. Έχει αναπτυχθεί από την εταιρεία X έχουµε αγοράσει τα δικαιώµατα χρήσης της. Πώς θα τροποποιήσουµε κώδικα που δε µας ανήκει; Γίνεται αµέσως αντιληπτό ότι αυτό που πρέπει να γίνει δεν είναι η τροποποίηση της κλάσης Counter, αλλά η δηµιουργία µιας νέας εξειδικευµένης κλάσης CounterDec, που θα παρέχει επιπλέον τη µέθοδο dec. Χωρίς το µηχανισµό της κληρονοµικότητας, κάτι τέτοιο θα σήµαινε το γράψιµο αρκετού κώδικα. Ας δούµε όµως πώς κάτι τέτοιο είναι άµεσα υλοποιήσιµο στη Java, µε ελάχιστο κόπο: class CounterDec extends Counter void dec () if (value > 0) value--; Με τη δήλωση extends Counter, καθορίζεται ότι η κλάση CounterDec ορίζεται ως µια επέκταση της Counter. Αυτό σηµαίνει ότι κληρονοµεί όλα τα µέλη της Counter και, στην ουσία, ότι τα αντικείµενα της κλάσης CounterDec µπορούν να χρησιµοποιηθούν οπουδήποτε είναι δυνατή η χρήση αντικειµένων της κλάσης Counter. Η κλάση Counter ονοµάζεται βάση (base class) ενώ η κλάση CounterDec ονοµάζεται παραγόµενη (derived class). Επιπλέον, στην CounterDec δηλώνεται η µέθοδος dec, που χρησιµοποιεί τη µεταβλητή value της κλάσης βάσης. Με το µηχανισµό κληρονοµικότητας που µόλις περιγράφηκε είναι δυνατή η κατασκευή ιεραρχιών κλάσεων. Η κλάση βάσης βρίσκονται ένα βήµα υψηλότερα στην ιεραρχία από τις αντίστοιχες παραγόµενες κλάσεις, που µπορούν µε τη σειρά τους να χρησιµοποιηθούν ως κλάσεις βάσεις για νέες παραγωγές, κ.ο.κ. Η ιεραρχία κλάσεων της Java έχει τη µορφή δέντρου. Στην κορυφή της βρίσκεται η ειδική κλάση Object, που µπορεί να θεωρηθεί ως ο πατέρας όλων των κλάσεων, αφού όλες την κληρονοµούν. Ας υποθέσουµε ότι η κλάση CounterDecSet επεκτείνει µε τη σειρά της την κλάση CounterDec µε την προσθήκη της µεθόδου set. Επιπλέον, ας υποτεθεί ότι ορίζεται και µια κλάση µε όνοµα Account. Η ιεραρχία κλάσεων για µια εφαρµογή που χρησιµοποιεί µόνο αυτές τις δυο κλάσεις απεικονίζεται στο Σχήµα 4.1. 75
Σχήµα 4.1. Παράδειγµα ιεραρχίας κλάσεων. 4.3.1. Κληρονοµικότητα και κατασκευαστές Ο ορισµός κατασκευαστών για κλάσεις που κληρονοµούν παρουσιάζει ορισµένες ιδιαιτερότητες, αφού εκτός από την αρχικοποίηση των µελών της παραγόµενης κλάσης πρέπει να αρχικοποιηθούν και τα µέλη της κλάσης βάσης. Ας δούµε πώς θα µπορούσαν να ορισθούν κατασκευαστές για την κλάση CounterDec. class CounterDec extends Counter CounterDec () super(); CounterDec (int v) super(v); void dec () if (value > 0) value--; Η ειδική µεταβλητή super αντιστοιχεί σε µια αναφορά στο αντικείµενο σαν αυτό να ανήκε στην κλάση βάση. Όπως και η ειδική µεταβλητή this, µπορεί να χρησιµοποιηθεί στην πρώτη εντολή των κατασκευαστών για την αρχικοποίηση της κλάσης βάσης. Οι δυο κατασκευαστές που ορίσθηκαν πιο πάνω αντιστοιχούν στους δυο κατασκευαστές της κλάσης Counter. Η εντολή super() είναι µια κλήση στον 76
κατασκευαστή της Counter χωρίς παραµέτρους, ενώ η super(v) είναι µια κλήση στο δεύτερο κατασκευαστή της Counter, µε παράµετρο v. Αν τα µέλη της κλάσης βάσης δεν αρχικοποιηθούν σε κάποιο κατασκευαστή µε κατάλληλη κλήση κατασκευαστή µέσω της super, τότε ο µεταγλωττιστής της Java αυτόµατα εισάγει µια κλήση στον κατασκευαστή χωρίς ορίσµατα της κλάσης βάσης. Αν στην κλάση βάση ορίζονται κατασκευαστές, αλλά κανείς από αυτούς δεν είναι χωρίς ορίσµατα, τότε συµβαίνει σφάλµα µεταγλώττισης. 4.3.2. Υπο-τύποι και µετατροπή τύπων Όπως ήδη αναφέρθηκε, η κληρονοµικότητα στη Java χρησιµοποιείται για την εξειδίκευση κλάσεων. Η παραγόµενη κλάση είναι µια εξειδικευµένη επέκταση της κλάσης βάσης, τα αντικείµενά της όµως δεν παύουν να µπορούν να χρησιµοποιηθούν σαν να ήταν αντικείµενα της κλάσης βάσης. Για παράδειγµα, ένα αντικείµενο της κλάσης CounterDec µπορεί να χρησιµοποιηθεί στο παρακάτω τµήµα κώδικα: CounterDec c = new CounterDec(); c.inc(); if (c.get() < 10) // do something όπου οι εξειδικευµένη µέθοδος dec δε χρησιµοποιείται. Για το λόγο αυτό, αφού τα αντικείµενα της παραγόµενης κλάσης διατηρούν όλες τις ιδιότητες της κλάσης βάσης, η παραγόµενη κλάση µπορεί να θεωρηθεί ως ένας υπό-τύπος (subtype) της κλάσης βάσης. Η σχέση υπό-τύπου είναι πολύ σηµαντική για τη γλώσσα Java γιατί επιτρέπει σε αντικείµενα να προσπελαύνονται µέσω αναφορών που ανήκουν σε διαφορετικούς τύπους. Αυτό είναι χρήσιµο π.χ. όταν ο τύπος κάποιου αντικειµένου δεν είναι απόλυτα γνωστός, αλλά είναι µόνο γνωστό ότι ανήκει σε µια κλάση που κληρονοµεί κάποια άλλη. Αναφορές σε αντικείµενα που ανήκουν σε υπό-τύπους µιας κλάσης µπορούν να εκχωρηθούν σε µεταβλητές του τύπου αναφοράς της κλάσης βάσης. Τα ίδια τα αντικείµενα δεν υπόκεινται σε καµιά µεταβολή, είναι όµως ορατά σαν να ήταν αντικείµενα της κλάσης βάσης. Στο παράδειγµα που ακολουθεί, γίνεται αναφορά σε ένα αντικείµενο της κλάσης CounterDec µέσω δυο µεταβλητών. Η πρώτη, cd, έχει τύπο CounterDec ενώ η δεύτερη, cb, έχει τύπο Counter. CounterDec cd = new CounterDec(); Counter cb = cd; cd.inc(); // this is OK, CounterDec has method inc cb.inc(); // so is this cd.dec(); // this is OK, CounterDec has method dec cb.dec(); // but this is wrong, compilation error! Επίσης, µε τον ίδιο τρόπο, αντικείµενα της κλάσης CounterDec µπορούν να περάσουν ως πραγµατικές παράµετροι σε µεθόδους, των οποίων τα ορίσµατα έχουν δηλωθεί ως τύπου Counter. Μια αναφορά σε ένα αντικείµενο της παραγόµενης κλάσης, µπορεί λοιπόν να εκληφθεί ως αναφορά σε αντικείµενο της κλάσης βάσης. Η µετατροπή γίνεται µε συνοπτική διαδικασία κατά τη µεταγλώττιση. Η αντίστροφη πορεία, δηλαδή από µια αναφορά σε αντικείµενο της κλάσης βάσης προς µια αναφορά σε αντικείµενο της παραγόµενης κλάσης, είναι δυνατό να γίνει µόνο υπό όρους. Για την ακρίβεια, µια τέτοια µετατροπή έχει νόηµα µόνο αν η το αντικείµενο 77
της αναφοράς είναι στην πραγµατικότητα αντικείµενο της παραγόµενης κλάσης. Ο έλεγχος για να διαπιστωθεί κάτι τέτοιο δεν µπορεί να γίνει κατά τη µεταγλώττιση του προγράµµατος, γίνεται όµως στο χρόνο εκτέλεσης. Για το λόγο αυτό, η µετατροπή γίνεται µε ρητή δήλωση του τύπου της παραγόµενης κλάσης, µέσα στον κώδικα. Στο παράδειγµα που ακολουθεί κατασκευάζονται δυο αντικείµενα. Το πρώτο προέρχεται από την κλάση CounterDec ενώ το δεύτερο από την κλάση Counter. Και στα δυο γίνεται αρχικά αναφορά από δυο µεταβλητές τύπου Counter. Counter cb1 = new Counter(); Counter cb2 = new CounterDec(); CounterDec cd; // this will be OK, cb1 refers to a CounterDec cd = (CounterDec) cb1; // this is a run-time error, cb2 refers to a Counter! cd = (CounterDec) cb2; Στη συνέχεια γίνεται µετατροπή των δυο αναφορών σε αντικείµενα της κλάσης CounterDec. Ο µεταγλωττιστής δε θα διαµαρτυρηθεί, γιατί αφού η κλάση CounterDec είναι επέκταση της Counter, είναι δυνατό να πρόκειται στην πραγµατικότητα για τέτοια αντικείµενα. Στο χρόνο εκτέλεσης, η µετατροπή της πρώτης αναφοράς θα γίνει χωρίς πρόβληµα, γιατί στην πραγµατικότητα επρόκειτο για αντικείµενο της κλάσης CounterDec που ήταν ορατό µέσω αναφοράς σε Counter. Η δεύτερη µετατροπή, όµως, θα οδηγήσει σε σφάλµα εκτέλεσης: το αντικείµενο δεν µπορεί να εκληφθεί ως CounterDec γιατί, πολύ απλά, δεν είναι τέτοιο. 4.3.3. Υπερκάλυψη µεταβλητών και υπερίσχυση µεθόδων Η επέκταση κλάσεων µε την προσθήκη νέων µελών είναι δυνατό να προκαλέσει προβλήµατα. Τί θα συµβεί αν η παραγόµενη κλάση δηλώνει ένα µέλος που έχει το ίδιο όνοµα µε ένα µέλος της κλάσης βάσης; Ας υποτεθεί ότι η κλάση UselessCounter επεκτείνει την κλάση Counter ως εξής: class UselessCounter extends Counter String value = "hello"; Μια νέα µεταβλητή προστίθεται, το όνοµα της οποίας συµπίπτει µε το όνοµα ενός µέλους της κλάσης Counter, που κληρονοµείται στην UselessCounter. Όταν µια παραγόµενη κλάση ορίζει µια νέα µεταβλητή, το όνοµα της οποίας συµπίπτει µε το όνοµα µιας κληρονοµούµενης µεταβλητής, τότε η νέα µεταβλητή υπερκαλύπτει την παλιά. Και οι δυο µεταβλητές συνυπάρχουν στα αντικείµενα της παραγόµενης κλάσης, αλλά οι νέες µεταβλητές επισκιάζουν τις παλιές: UselessCounter u = new UselessCounter(); Counter c = new Counter(); // this is OK, refering to the new variable u.value = "look"; // this is also OK, refering to the old variable 78
c.value = 5; // But this is wrong, compilation error! // value refers to the old variable now! c = u; c.value = "hi"; Επίσης, η λειτουργικότητα της κλάσης Counter δε θα µεταβληθεί, γιατί οι µέθοδοι που ορίζονται σε αυτή δε βλέπουν τη νέα µεταβλητή. Γενικά η επισκίαση µεταβλητών στις παραγόµενες κλάσεις δεν είναι ιδιαίτερα χρήσιµη και είναι καλό να αποφεύγεται, αφού µπορεί εύκολα να οδηγήσει σε παρεξηγήσεις. Ας θεωρήσουµε τώρα µια διαφορετική επέκταση της κλάσης Counter, λίγο πιο χρήσιµη αυτή τη φορά. Στην κλάση DoubleCounter η µέθοδος inc αυξάνει την τιµή του µετρητή κατά 2, αντί κατά 1. class DoubleCounter extends Counter void inc () value += 2; Όταν µια παραγόµενη κλάση ορίζει µια νέα µέθοδο, το όνοµα της οποίας συµπίπτει µε το όνοµα µιας µεθόδου που κληρονοµείται από την κλάση βάση, τότε υπάρχουν δυο περιπτώσεις. Αν η νέα µέθοδος διαφέρει από την παλιά στον αριθµό ή/και στον τύπο των παραµέτρων, τότε πρόκειται για απλή υπερφόρτωση µεθόδου, όπως περιγράφηκε σε προηγούµενη ενότητα. Αν όµως αυτό δε συµβαίνει, τότε η νέα µέθοδος υπερισχύει της παλιάς. Αυτό ονοµάζεται υπερίσχυση µεθόδου (method overriding). Ας θεωρήσουµε το ακόλουθο τµήµα κώδικα: Counter c = new Counter(); DoubleCounter d = new DoubleCounter(); c.inc(); // value of c becomes 1, old inc d.inc(); // value of d becomes 2, new inc Στην περίπτωση του αντικειµένου c, καλείται η µέθοδος inc της κλάσης Counter και η τιµή του µετρητή αυξάνει κατά 1. Στην περίπτωση του αντικειµένου d όµως, η µέθοδος inc της κλάσης DoubleCounter υπερισχύει και η τιµή αυξάνει κατά 2. Είναι χαρακτηριστικό ότι αντίθετα µε την υπερφόρτωση µεθόδων, η επίλυση της οποίας γίνεται κατά τη µεταγλώττιση του προγράµµατος, η επίλυση της υπερίσχυσης µεθόδων γίνεται δυναµικά στο χρόνο εκτέλεσης. Αυτό γίνεται αντιληπτό µε το ακόλουθο παράδειγµα: Counter c = new Counter(); Counter d = new DoubleCounter(); c.inc(); // value of c becomes 1, old inc d.inc(); // value of d becomes 2, new inc Το παράδειγµα µοιάζει µε το προηγούµενο, µε τη µόνη διαφορά ότι και τα δυο αντικείµενα προσπελαύνονται µέσω αναφορών τύπου Counter. Παρ όλα αυτά, στην περίπτωση του αντικειµένου DoubleCounter, η νέα µέθοδος inc υπερισχύει και πάλι της παλιάς και η τιµή του µετρητή αυξάνει κατά 2. Η υπερίσχυση µεθόδων είναι ένα 79
πολύτιµο εφόδιο στη γλώσσα Java, καθώς επιτρέπει στις εξειδικευµένες κλάσεις να παρακάµψουν, µέχρι ενός σηµείου, τη συµπεριφορά που τους επιβάλλεται από τις κλάσεις βάσεις. Στην περίπτωση που χρειάζεται να κληθεί η µέθοδος της κλάσης βάσης, µέσα από τον κώδικα µεθόδου της παραγόµενης κλάσης, είναι δυνατό να χρησιµοποιηθεί η ειδική µεταβλητή super. Για παράδειγµα, ο ορισµός της µεθόδου inc της κλάσης DoubleCounter θα µπορούσε να είναι ισοδύναµα: void inc () super.inc(); super.inc(); 4.3.4. Αφηρηµένες και τελικές κλάσεις Οποιαδήποτε µέθοδος στον ορισµό µιας κλάσης µπορεί να δηλωθεί χρησιµοποιώντας το µετατροπέα abstract. Στην περίπτωση αυτή, η µέθοδος ονοµάζεται αφηρηµένη (abstract). Οι αφηρηµένες µέθοδοι δεν έχουν σώµα παρά µόνο επικεφαλίδα. Ουσιαστικά καθορίζεται µόνο ο τρόπος χρήσης και όχι η υλοποίησή τους. Κλάσεις που περιέχουν ή κληρονοµούν αφηρηµένες µεθόδους ονοµάζονται επίσης αφηρηµένες και πρέπει να ορίζονται µε το µετατροπέα abstract. Ας θεωρήσουµε το ακόλουθο παράδειγµα: abstract class DisplayableCounter extends Counter abstract void display (); Η κλάση DisplayableCounter περιγράφει ένα µετρητή που περιέχει µια µέθοδο για την εµφάνιση της τιµής του στο χρήστη. Η µέθοδος αυτή είναι αφηρηµένη και κατά συνέπεια ολόκληρη η κλάση είναι αφηρηµένη. Η κατασκευή αντικειµένων που προέρχονται από µια αφηρηµένη κλάση είναι φυσικά αδύνατη, καθώς εκρεµεί η υλοποίηση κάποιων µεθόδων. Κατά συνέπεια, το ακόλουθο τµήµα κώδικα οδηγεί σε σφάλµα µεταγλώττισης: c = new DisplayableCounter(); // compilation error! Ένας πιθανός λόγος που η εµφάνιση της τιµής δεν υλοποιείται αµέσως είναι ότι υπάρχουν πολλοί διαφορετικοί τρόποι για την υλοποίησή της. Για παράδειγµα, αν το λειτουργικό σύστηµα και η εφαρµογή που αναπτύσσεται επιτρέπει τη χρήση παραθύρων µε γραφικά, η τιµή θα µπορούσε να εµφανίζεται µέσα σε ένα νέο παράθυρο, µε κατάλληλο µήνυµα. Ας προχωρήσουµε όµως σε µια απλούστερη υλοποίηση, µε την οποία η τιµή του µετρητή απλώς εκτυπώνεται στην οθόνη: class SimpleDCounter extends DisplayableCounter void display () System.out.println(get()); Στην κλάση SimpleDCounter καθορίζεται µια υλοποίηση για τη µέθοδο display και η κλάση παύει να είναι αφηρηµένη. Στη συνέχεια µπορεί να χρησιµοποιηθεί: SimpleDCounter c = new SimpleDCounter(); if (c.get() < 100) 80
c.display(); Η χρήση µπορεί να γίνει επίσης µέσω αναφοράς σε τύπο: DisplayableCounter: DisplayableCounter c = new SimpleDCounter(); c.display(); Μερικές φορές είναι επιθυµητό να εξασφαλισθεί ότι µια κλάση δε θα εξειδικευθεί περισσότερο. Η Java επιτρέπει τη δήλωση µιας κλάσης ή επιµέρους µελών της µε τον µετατροπέα final, απαγορεύοντας έτσι την περαιτέρω εξειδίκευσή της. Μια κλάση που έχει δηλωθεί ως final είναι αδύνατο να επεκταθεί. Μια µέθοδος που έχει δηλωθεί ως final είναι αδύνατο να επανορισθεί σε παραγόµενη κλάση. Επίσης, το ίδιο µπορεί να συµβεί και µε µεταβλητές µιας κλάσης. Αν µια µεταβλητή δηλωθεί ως final, τότε η τιµή της δεν µπορεί να µεταβληθεί, πρόκειται εποµένως για σταθερά. Ας θεωρήσουµε το ακόλουθο παράδειγµα: final class LimitedCounter extends Counter static final int LIMIT = 100; void inc () if (value < LIMIT) value++; Η κλάση LimitedCounter δηλώνεται ως final και κατά συνέπεια η παρακάτω δήλωση οδηγεί σε σφάλµα µεταγλώττισης: class DoubleLCounter extends LimitedCounter // wrong! void inc () super.inc(); super.inc(); Επίσης, η µεταβλητή LIMIT ορίζεται ως final, κατά συνέπεια η τιµή της δεν µπορεί να µεταβληθεί και το παρακάτω τµήµα κώδικα οδηγεί πάλι σε σφάλµα µεταγλώττισης: LimitedCounter.LIMIT = 1000; // wrong! 4.4 Υποκλάσεις 4.4.1. Η σχέση is-a Στον προγραµµατισµό συχνά δηµιουργείτε ένα µοντέλο ενός πράγµατος, για παράδειγµα ενός εργαζοµένου, και στη συνέχεια χρειάζεστε µία περισσότερο εξειδικευµένη έκδοση του αρχικού µοντέλου. Για παράδειγµα, µπορεί να χρειαζόµαστε ένα µοντέλο για έναν διευθυντή. Σαφώς ο διευθυντής στην πράξη είναι ένας (is-a) εργαζόµενος, αλλά είναι ένας εργαζόµενος µε επιπρόσθετα χαρακτηριστικά. Οι ακόλουθες κλάσεις παρουσιάζουν ακριβώς αυτό: public class Employee 81
private String name; private Date hiredate; private Date dateofbirth; private String jobtitle; private int grade;... public class Manager private String name; private Date hiredate; private Date dateofbirth; private String jobtitle; private int grade; private String department; private Employee[] subordinates;... Στο παράδειγµα αυτό υπάρχουν διπλότυπα ανάµεσα στις κλάσεις Manager και Employee. Στην πραγµατικότητα θα πρέπει να υπάρχει ένα πλήθος µεθόδων που θα είναι εφαρµόσιµες στο Employee και πιθανότατα οι περισσότερες από αυτές θα είναι εφαρµόσιµες και για την κλάση Manager χωρίς µεταβολές. Συνεπώς, αυτό που χρειαζόµαστε είναι ένας τρόπος να δηµιουργούµε µία νέα κλάση από µία ήδη υπάρχουσα: αυτό ονοµάζεται δηµιουργία υποκλάσεων. 4.4.2. Η δεσµευµένη λέξη extends Στις αντικειµενοστρεφείς γλώσσες παρέχονται ειδικοί µηχανισµοί που επιτρέπουν στον προγραµµατιστή να ορίζει µια κλάση µε όρους µίας προηγούµενα ορισµένης κλάσης. Αυτό επιτυγχάνεται στη Java µε χρήση της δεσµευµένης λέξης extends ως εξής: public class Employee private String name; private Date hiredate; private Date dateofbirth; private String jobtitle; private int grade;... public class Manager extends Employee private String department; private Employee[] subordinates;... Με τον τρόπο αυτή η κλάση Manager ορίζεται ώστε να έχει όλες τις µεταβλητές και τις µεθόδους που έχει η κλάση Employee. Όλες οι µεταβλητές και οι µέθοδοι 82
κληροδοτούνται από τον ορισµό της γονικής κλάσης (parent class). Το µόνο που πρέπει να κάνει ο προγραµµατιστής είναι να ορίσει τα επιπρόσθετα χαρακτηριστικά ή όπως θα δούµε στη συνέχεια να καθορίσει ποιες µεταβολές πρέπει να γίνουν. Σηµείωση Η προσέγγιση αποτελεί µία σηµαντική βελτίωση σε ό,τι αφορά τη συντήρηση και συνεπώς και την αξιοπιστία. Αν γίνει µία διόρθωση στην κλάση Employee, η κλάση Manager διορθώνεται χωρίς να χρειάζεται να ενδιαφερθεί σχετικά ο προγραµµατιστής. 4.4.3. Απλή κληρονοµικότητα Η Java επιτρέπει σε µία κλάση να επεκτείνει µόνο µία άλλη κλάση. Ο περιορισµός αυτός ονοµάζεται απλή κληρονοµικότητα. Η συζήτηση για τα σχετικά πλεονεκτήµατα της απλής και της πολλαπλής κληρονοµικότητας είναι θέµα εκτεταµένων συζητήσεων ανάµεσα στους αντικειµενοστρεφείς προγραµµατιστές. Η Java επιβάλλει την απλή κληρονοµικότητα γιατί θεωρείται ότι προσφέρει πιο αξιόπιστο κώδικα, αν και ορισµένες φορές αυτό γίνεται κάνοντας πιο δύσκολη τη ζωή των προγραµµατιστών. Στη συνέχεια θα δούµε µία διευκόλυνση που ονοµάζεται διεπαφή (interface) και η οποία επιτρέπει ορισµένα από τα πλεονεκτήµατα της πολλαπλής κληρονοµικότητας. 4.4.4. Οι συναρτήσεις δηµιουργίας δεν κληρονοµούνται Είναι σηµαντικό να γνωρίζουµε ότι παρ όλο που µία υποκλάση κληρονοµεί όλες τις µεθόδους και τις µεταβλητές από τη γονική κλάση της, δεν κληρονοµεί τις συναρτήσεις δηµιουργίας. Υπάρχουν µόνο δύο τρόποι µε τους οποίους µπορείτε να αποκτήσετε µία συνάρτηση δηµιουργίας: είτε γράφετε µία συνάρτηση δηµιουργίας, είτε επειδή δεν έχετε γράψει καµία συνάρτηση δηµιουργίας, η κλάση έχει µία εξ ορισµού συνάρτηση δηµιουργίας. 4.4.5. Πολυµορφισµός Περιγράφοντας ότι ο Manager είναι ένας Employee δεν είναι απλά ένας βολικός τρόπος για να περιγράψουµε τη συσχέτιση ανάµεσα στις δύο κλάσεις. Θυµηθείτε ότι έχουµε πει ότι ένας Manager λαµβάνει όλα τα χαρακτηριστικά, τόσο τα µέλη όσο και τις µεθόδους, της γονικής κλάσης Employee. Αυτό σηµαίνει ότι κάθε πράξη που είναι επιτρεπτή σε ένα Employee είναι επίσης επιτρεπτή σε ένα Manager. Αν ο Employee έχει µεθόδους raisesalary() και fire() τότε το ίδιο ισχύει και για την κλάση Manager. Αυτό µας οδηγεί στην ιδέα ότι τα αντικείµενα είναι πολυµορφικά, δηλαδή έχουν «πολλές µορφές». Ένα συγκεκριµένο αντικείµενο µπορεί να έχει τη µορφή ενός Manager, αλλά επίσης έχει και τη µορφή ενός Employee. Προκύπτει ότι στη Java υπάρχει µία κλάση η οποία είναι η γονική κλάση όλων των άλλων. Αυτή είναι η κλάση java.lang.object. Συνεπώς, στην πράξη, οι προηγούµενοι ορισµοί µας ήταν απλά συντοµογραφίες των: public class Employee extends Object και public class Manager extends Employee Η κλάση Object ορίζει ένα πλήθος χρήσιµων µεθόδων, συµπεριλαµβανοµένης της tostring(), στην οποία οφείλεται το γεγονός ότι τα πάντα στη Java µπορούν να µετατραπούν σε αναπαράσταση συµβολοσειράς (ακόµα και αν αυτό µπορεί να είναι 83
περιορισµένης χρήσης). Η Java, όπως και οι περισσότερες αντικειµενοστρεφείς γλώσσες µάς επιτρέπει να αναφερόµαστε σε ένα αντικείµενο χρησιµοποιώντας µία µεταβλητή που είναι τύπου της γονικής κλάσης. Συνεπώς µπορούµε να πούµε: Employee e = new Manager(); Χρησιµοποιώντας τη µεταβλητή e ως έχει µπορείτε να προσπελαύνετε µόνο εκείνα τα µέρη του αντικειµένου που αποτελούν µέρος του Employee, ενώ τα µέρη που είναι εξειδικευµένα στο Manager είναι κρυφά. Αυτό γίνεται γιατί σε ό,τι αφορά το µεταγλωττιστή η e είναι τύπου Employee και όχι Manager. 4.4.6. Ορίσµατα µεθόδων και ετερογενείς συλλογές Μπορεί να µας φαίνεται µη λογικό να δηµιουργούµε ένα στιγµιότυπο της κλάσης Manager και στη συνέχεια να το αναθέτουµε σε µία µεταβλητή τύπου Employee. Αυτό είναι αλήθεια, αλλά υπάρχουν λόγοι για τους οποίους µπορεί να θέλετε να πετύχετε το ίδιο αποτέλεσµα. Χρησιµοποιώντας την προσέγγιση αυτή µπορείτε να γράφετε µεθόδους που δέχονται αντικείµενα που ανήκουν σε µία «γενικευµένη» κλάση, στην περίπτωση αυτή Employee, και λειτουργούν ορθά για οποιαδήποτε υποκλάση της. Συνεπώς, µπορείτε να παράγετε µία µέθοδο σε µία κλάση εφαρµογής που δέχεται ένα εργαζόµενο και συγκρίνει το µισθό του µε κάποιο όριο για να υπολογίσει τις κρατήσεις για την εφορία. Με χρήση των πολυµορφικών διευκολύνσεων αυτό γίνεται αρκετά απλά: public TaxRate findtaxrate(employee e) // do calculations and return tax rate for e // Σε άλλο σηµείο της κλάσης εφαρµογής Manager m = new Manager();... TaxRate t = findtaxrate(m); Αυτό είναι επιτρεπτό γιατί ένας Manager είναι ένας Employee. Μία ετερογενής συλλογή είναι µία συλλογή ανόµοιων πραγµάτων. Στις αντικειµενοστρεφείς γλώσσες µπορείτε να δηµιουργήσετε συλλογές πραγµάτων που έχουν µία κοινή προγονική κλάση. Συνεπώς µπορείτε να έχετε το ακόλουθο: Employee [] staff = new Employee[1024]; staff[0] = new Manager(); staff[1] = new Employee(); και ούτω κάθ εξής. Μπορείτε ακόµα να γράψετε µία µέθοδο ταξινόµησης που βάζει τους εργαζόµενους κατά σειρά ηλικίας, ή κατά σειρά µισθού, χωρίς να χρειάζεται να ανησυχείτε για το γεγονός ότι ορισµένοι από αυτούς είναι διευθυντές. Σηµείωση Επειδή στη Java κάθε κλάση είναι υποκλάση της Object, µπορείτε να χρησιµοποιήσετε ένα πίνακα από Object ως περιέκτη οποιωνδήποτε αντικειµένων. Το µόνο που δεν µπορεί να προστεθεί σε ένα array of Object είναι βασικές µεταβλητές (int, float κλπ). Ακόµα καλύτερη από ένα array of Object είναι η κλάση Vector που έχει σχεδιαστεί για την αποθήκευση ετερογενών συλλογών αντικειµένων. 84
4.4.7. Ο τελεστής instanceof εδοµένου του ότι µπορείτε να µεταβιβάζετε αντικείµενα µε χρήση αναφορών στη γονική τους κλάση, ορισµένες φορές θέλετε να γνωρίζετε τι είναι αυτό που έχετε στην πράξη. Αυτός είναι ο σκοπός του τελεστή instanceof. Έστω ότι έχετε την πιο κάτω ιεραρχία κλάσεων: public class Employee extends Object public class Manager extends Employee public class Contractor extends Employee Σηµείωση Θυµηθείτε ότι αν και είναι επιτρεπτό να γράψετε extends Object είναι πλεονάζον. Εδώ παρουσιάζεται απλά ως υπενθύµιση. Αν λάβετε ένα αντικείµενο µέσω µίας αναφοράς τύπου Employee µπορεί να προκύψει (ή να µην προκύψει) ότι είναι τύπου Manager ή Contractor. Αν ενδιαφέρεστε σχετικά µπορείτε να το ελέγξετε ως εξής χρησιµοποιώνταςτον instanceof public void method(employee e) if (e instanceof Manager) // call him Sir else if (e instanceof Contractor) // keep company secrets else // plain employee, talk freely over a drink 4.4.8. Μετατροπές αντικειµένων Σε περιπτώσεις στις οποίες έχετε λάβει µία αναφορά σε µία γονική κλάση, αλλά έχετε καθορίσει µε χρήση του τελεστή instanceof ότι το αντικείµενο είναι στην πράξη µίας συγκεκριµένης κλάσης µπορείτε να επαναφέρετε τη συνολική λειτουργικότητα του αντικειµένου µετατρέποντας (casting) την αναφορά. public void method(employee e) if (e instanceof Manager) Manager m = (Manager)e; System.out.println( Είναι ο διευθυντής του τµήµατος + m.department); // υπόλοιπες πράξεις 85
Αν δεν κάνετε τη µετατροπή, η προσπάθειά σας να αναφερθείτε στο e.department θα αποτύχει, καθώς ο µεταγλωττιστής δεν γνωρίζει κανένα µέλος µε το όνοµα department στην κλάση Employee. Αν δεν κάνετε έλεγχο µε χρήση του instanceof έχετε τον κίνδυνο να αποτύχει η µετατροπή. Γενικά κάθε προσπάθεια µετατροπής µίας αναφοράς αντικειµένου υπόκειται στους ακόλουθους ελέγχους: Μετατροπές προς τα πάνω στην ιεραρχία των κλάσεων είναι πάντα επιτρεπτές και στην πράξη δεν απαιτούν τον τελεστή µετατροπής (cast operator), αλλά γίνονται µε απλή ανάθεση. Για µετατροπές προς τα κάτω, ο µεταγλωττιστής πρέπει να ικανοποιηθεί µε το γεγονός ότι η µετατροπή είναι τουλάχιστον δυνατή. Για παράδειγµα µία προσπάθεια µετατροπής µίας αναφοράς σε Manager σε αναφορά σε Contractor είναι σαφώς µη επιτρεπτή, εφόσον ένας Contractor δεν είναι Manager. Η κλάση προς την οποία γίνεται η µετατροπή πρέπει να είναι µία υποκλάση του τρέχοντος τύπου της αναφοράς. Αν ο µεταγλωττιστής έχει επιτρέψει τη µετατροπή, τότε ο τύπος της αναφοράς ελέγχεται κατά το χρόνο εκτέλεσης. Αν αποδειχθεί ότι, ίσως επειδή είχε παραληφθεί ο έλεγχος µε τον instanceof από τον πηγαίο κώδικα, το αντικείµενο δεν είναι του τύπου που απαιτείται από τη µετατροπή, τότε λαµβάνει χώρα µία εξαίρεση (Exception). Οι εξαιρέσεις είναι µία µορφή λάθους κατά την εκτέλεση και θα τις εξετάσουµε στη συνέχεια. 4.5. Interfaces Ένα interface στη Java µπορεί να θεωρηθεί ως µια αφηρηµένη κλάση, η οποία δε δηλώνει µεταβλητές και όλες οι µέθοδοί της είναι αφηρηµένες. Τα interfaces, όπως και οι κλάσεις, ορίζουν ιεραρχίες κληρονοµικότητας και είναι δυνατό να δηλωθούν µεταβλητές µε τύπο αναφοράς σε αυτές. Με την έννοια αυτή, είναι ισοδύναµες µε πλήρως αφηρηµένες κλάσεις. Ας θεωρήσουµε τo ακόλουθο interface, to οποίo περιγράφει αντικείµενα που διαθέτουν µια µέθοδο εµφάνισης, µε το όνοµα display. interface Displayable void display (); Στη συνέχεια, ας θεωρήσουµε την κλάση SimpleDCounter, που ορίζεται ως: class SimpleDCounter extends Counter implements Displayable void display () System.out.println(get()); Η κλάση αυτή υλοποιεί µια µέθοδο display, µε την ίδια επικεφαλίδα που ορίζεται στο interface Displayable. Είναι εποµένως δυνατό να θεωρηθεί ως µια υλοποίηση του interface και αυτό φαίνεται στον ορισµό του µε τη δήλωση implements Displayable. 86
Στη συνέχεια, τα αντικείµενα της κλάσης µπορούν να χρησιµοποιηθούν µέσω αναφορών τύπου Displayable, όπως φαίνεται στο ακόλουθο τµήµα κώδικα: SimpleDCounter c = new SimpleDCounter(); Displayable d = c; d.display(); Οι ιεραρχίες των interfaces µπορούν να θεωρηθούν ως ορθογώνιες προς την ιεραρχία των κλάσεων. Τα interfaces καλύπτουν την ανάγκη για πολλαπλή κληρονοµικότητα, δηλαδή την επέκταση περισσότερων της µιας κλάσης, που δεν υποστηρίζεται από τη Java. Μια κλάση µπορεί να επεκτείνει µόνο µια κλάση βάση, µπορεί όµως να υλοποιεί οποιονδήποτε αριθµό από interfaces. Για παράδειγµα: interface Incrementable void inc (); SimpleDCounter extends Counter implements Displayable, Incrementable void display () System.out.println(get()); Η κλάση SimpleDCounter τώρα υλοποιεί δυο interfaces. Η µέθοδος inc που απαιτείται από το interface Incrementable κληρονοµείται στην κλάση SimpleDCounter από την Counter. Όπως αναφέρθηκε στην αρχή, τα interfaces δεν είναι δυνατό να περιέχουν µεταβλητές. Μοναδική εξαίρεση είναι οι σταθερές, δηλαδή µεταβλητές που δηλώνονται ως static final. 4.6. Πακέτα Τα πακέτα (packages) στη Java παρέχουν ένα µηχανισµό οργάνωσης των κλάσεων. Κάθε πακέτο περιέχει έναν αριθµό κλάσεων και χαρακτηρίζεται το όνοµά του. Οι ορισµοί αυτών των κλάσεων µπορούν να περιέχονται σε ένα ή περισσότερα αρχεία, που ονοµάζονται µονάδες µεταγλώττισης (compilation units). Τα ονόµατα των πακέτων απαρτίζονται από µια ή περισσότερες λέξεις, χωρισµένες µε τελείες. Για παράδειγµα, ονόµατα πακέτων που χρησιµοποιούνται συχνά στην πράξη είναι τα java.lang και java.io. Προκειµένου να χρησιµοποιηθούν τα περιεχόµενα ενός πακέτου, πρέπει να προηγείται το όνοµα του πακέτου. Το ακόλουθο τµήµα κώδικα χρησιµοποιεί την κλάση Stack που βρίσκεται στο πακέτο: java.util: java.util.stack s = new java.util.stack(); MyObject m = new MyObject(); s.push(m); Φυσικά, τα µεγάλα ονόµατα γίνονται κουραστικά µετά από λίγο. Για το λόγο αυτό τα περιεχόµενα ενός πακέτου µπορούν να εισαχθούν στον κώδικα, ώστε να 87
µην απαιτείται η χρήση του ονόµατος του πακέτου. Αυτό γίνεται µε µια δήλωση import, που πρέπει να βρίσκεται στην αρχή του αρχείου: import java.util.stack;... Stack s = new Stack();... Η παραπάνω δήλωση import εισάγει την κλάση Stack από το πακέτο java.util. Η δήλωση έχει πληροφοριακό χαρακτήρα, δηλαδή ο κώδικας της κλάσης Stack δεν εισάγεται στην πραγµατικότητα και το µέγεθος του µεταγλωττισµένου αρχείου δε µεταβάλλεται. Με παρόµοιο τρόπο, µπορούν να εισαχθούν όλα τα περιεχόµενα του πακέτου java.util, χρησιµοποιώντας τον ειδικό χαρακτήρα * ως εξής: import java.util.*; Αν απαιτείται η εισαγωγή περισσότερων πακέτων, είναι δυνατό να χρησιµοποιηθούν περισσότερες δηλώσεις import, πάντα στην αρχή του αρχείου. Στην περίπτωση που από διαφορετικά πακέτα εισάγονται κλάσεις µε το ίδιο όνοµα, οι κλάσεις αυτές δεν είναι δυνατό να προσπελασθούν απλώς µε το όνοµά τους, αλλά απαιτείται χρήση του πλήρους ονόµατος. 4.6.1. ηµιουργία πακέτων Εκτός από το να χρησιµοποιούν υπάρχοντα πακέτα, η Java επιτρέπει στους προγραµµατιστές να δηµιουργούν τα δικά τους πακέτα, στα οποία να οργανώνουν τις δικές τους κλάσεις. Για να γίνει κάτι τέτοιο, πρέπει στη γραµµή της µονάδας µεταγλώττισης να υπάρχει µια δήλωση package, που καθορίζει το όνοµα του πακέτου, στο οποίο θα συµπεριληφθούν οι κλάσεις και τα λοιπά στοιχεία που δηλώνονται στη συνέχεια. Για παράδειγµα, ας θεωρήσουµε το ακόλουθο πακέτο µε όνοµα mine.counters, που συµπεριλαµβάνει τις κλάσεις των µετρητών. Προκειµένου να είναι σε θέση να χρησιµοποιηθεί, το πακέτο αυτό πρέπει πρώτα να µεταγλωττισθεί. Στη συνέχεια, τα αρχεία.class που θα προκύψουν πρέπει να τοποθετηθούν σε ένα directory µε όνοµα counters, που θα βρίσκεται κάτω από ένα directory µε όνοµα mine, κάπου στο µονοπάτι των πακέτων της Java. package mine.counters; class Counter... class CounterDec extends Counter...... interface Displayable... class SimpleDCounter extends Counter implements Displayable... Όλες οι δηλώσεις κάθε αρχείου Java αποτελούν τµήµα κάποιου πακέτου. Αν η µονάδα µεταγλώττισης δεν αρχίζει µε µια δήλωση package, τότε οι δηλώσεις τοποθετούνται σε ένα γενικό ανώνυµο πακέτο, το οποίο είναι πάντα διαθέσιµο. 88
4.6.2. Πρόσβαση σε µέλη κλάσεων Αφού περιγράφηκε ο µηχανισµός οργάνωσης κλάσεων σε πακέτα, είναι πάλι σκόπιµο να αναφερθούµε στην πρόσβαση στα µέλη των κλάσεων και στους µετατροπείς ορατότητας, καθώς η εικόνα που δόθηκε σε προηγούµενη ενότητα δεν ήταν απόλυτα ακριβής. Στη Java, οι κλάσεις που ανήκουν στο ίδιο πακέτο θεωρείται ότι αποτελούν µια ενότητα. Για το λόγο αυτό, η έννοια του πακέτου έχει µεγάλη σηµασία όσον αφορά στην ορατότητα των µελών των κλάσεων. Μεγάλη σηµασία έχει επίσης η έννοια της κληρονοµικότητας, καθώς οι παραγόµενες κλάσεις ορισµένες φορές χρειάζεται να έχουν πρόσβαση στην υλοποίηση των κλάσεων βάσεων. Η Java υποστηρίζει τέσσερις µετατροπείς ορατότητας για τα µέλη των κλάσεων που, µαζί µε την απουσία µετατροπέα, αποτελούν πέντε διαφορετικούς τρόπους για να δηλωθεί ένα µέλος. Η λειτουργία των µετατροπέων περιγράφεται στη συνέχεια, από τον περισσότερο προς τον λιγότερο περιοριστικό. Τα µέλη µιας κλάσης είναι πάντοτε ορατά µέσα από τις µεθόδους της ίδιας κλάσης και το γεγονός αυτό δεν µπορεί να µεταβληθεί από κανένα µετατροπέα ορατότητας. Για το λόγο αυτό, δεν αναφέρεται ρητά στην παρακάτω περιγραφή. private: τα µέλη µιας κλάσης που δηλώνονται µε το µετατροπέα private δεν είναι πουθενά ορατά έξω από την κλάση. private, protected: τα µέλη που δηλώνονται µε χρήση των δυο µετατροπέων private και protected είναι ορατά µόνο από παραγόµενες κλάσεις, είτε αυτές βρίσκονται στο ίδιο είτε σε άλλα πακέτα. Αυτός είναι ο µοναδικός επιτρεπτός συνδυασµός µετατροπέων ορατότητας. απουσία µετατροπέα: τα µέλη που δηλώνονται απουσία µετατροπέα είναι ορατά µόνο από τις κλάσεις που ανήκουν στο ίδιο πακέτο. protected: τα µέλη που δηλώνονται µε το µετατροπέα protected είναι ορατά από τις κλάσεις που ανήκουν στο ίδιο πακέτο και επιπλέον από παραγόµενες κλάσεις εκτός πακέτου. public: τα µέλη που δηλώνονται µε το µετατροπέα public είναι ορατά από όλες τις κλάσεις, ανεξαρτήτως πακέτου. Ο µετατροπέας αυτός είναι ιδιαίτερα χρήσιµος για τη διαπροσωπεία µιας κλάσης. Στο σηµείο αυτό αξίζει να γίνουν δυο παρατηρήσεις, σχετικές µε την ορατότητα µελών και την κληρονοµικότητα. Η πρώτη παρατήρηση αφορά στην υπερίσχυση µεθόδων: δεν είναι δυνατό να περιορισθεί η ορατότητα µιας µεθόδου της παραγόµενης κλάσης που υπερισχύει µιας µεθόδου της κλάσης βάσης. Το αντίστροφο όµως είναι δυνατό. Η δεύτερη παρατήρηση αφορά στη χρήση του µετατροπέα protected: τα µέλη της κλάσης βάσης είναι ορατά από τις παραγόµενες κλάσεις µόνο όταν πρόκειται για αντικείµενα που προέρχονται από τις τελευταίες. 89
Παράδειγµα: Βιβλιοθήκη σχηµάτων Σχήµα 6.1. Ιεραρχία κλάσεων σχηµάτων. Στο παράδειγµα που ακολουθεί υλοποιείται µια µικρή βιβλιοθήκη σχηµάτων στις δυο διαστάσεις. Η ιεραρχία των κλάσεων που ορίζονται απεικονίζεται στο Σχήµα 3.2, καθώς και οι µέθοδοι κάθε κλάσης. Οι κύριες κλάσεις που υλοποιούν τη βιβλιοθήκη είναι οι Shape, Point, Circle και Polygon. Η διαπροσωπεία Canvas περιγράφει αντικείµενα που διαθέτουν µεθόδους σχεδίασης, και η κλάση MyScreen είναι µια υποθετική υλοποίηση αυτής της διαπροσωπείας. Οι ορισµοί όλων αυτών των στοιχείων γίνονται σε ένα νέο πακέτο µε όνοµα shapes. Επίσης, για τις ανάγκες του παραδείγµατος υλοποιείται η κλάση ExampleShapes, που χρησιµοποιεί τη βιβλιοθήκη. Ο κώδικας για όλα αυτά τα στοιχεία δίνεται στις επόµενες παραγράφους. Κλάση Shape Η αφηρηµένη αυτή κλάση υλοποιεί ένα σχήµα στο χώρο δυο διαστάσεων. Τα σχήµατα, όπως συνηθίζεται µε ορισµένες κατηγορίες αντικειµένων στη Java, δεν είναι δυνατό να µεταβληθούν µετά την κατασκευή τους, αλλά οι µέθοδοι επεξεργασίας επιστρέφουν ως αποτέλεσµα νέα σχήµατα. Οι µέθοδοι που ορίζονται στην κλάση 90
Shape είναι οι ακόλουθες. Από αυτές, µόνο η µέθοδος moverel υλοποιείται, µέσω της moveto και των µεθόδων της κλάσης Point. Point getreference () Επιστρέφει το σηµείο αναφοράς του σχήµατος, που συνήθως είναι κάποιο από τα χαρακτηριστικά του σηµεία (π.χ. στον κύκλο είναι το κέντρο του). Shape rotate (Point origin, double angle) Περιστρέφει το σχήµα κατά γωνία angle (σε ακτίνια) γύρω από το σηµείο origin. Shape moveto (Point target) Μετακινεί το σχήµα ώστε το σηµείο αναφοράς του να βρεθεί στο target. Shape moverel (Point displacement) Μετακινεί το σχήµα κατά σχετική απόσταση displacement από την τρέχουσα θέση του. void draw (Canvas c) Σχεδιάζει το σχήµα στο αντικείµενο µε δυνατότητες σχεδίασης c. Ο κώδικας της κλάσης ακολουθεί. package shapes; abstract public class Shape abstract public Point getreference (); abstract public Shape rotate (Point origin, double angle); abstract public Shape moveto (Point target); public Shape moverel (Point displacement) return moveto(getreference().plus(displacement)); abstract static public Shape draw (Canvas c); Κλάση Point Η κλάση αυτή κληρονοµεί την Shape και υλοποιεί ένα σηµείο. Τα επιπλέον µέλη που ορίζονται είναι τα εξής: double coordx, double coordy. Οι συντεταγµένες του σηµείου. Point (double x, double y). Κατασκευαστής σηµείου µε συντεταγµένες (x, y). double getx (), double gety (). Συναρτήσεις που επιστρέφουν τις συντεταγµένες του σηµείου. Point plus (Point p), Point minus (Point p). Το διανυσµατικό άθροισµα και διαφορά αντίστοιχα του τρέχοντος σηµείου και του σηµείου p. double distance (), double distance (Point p) Η απόσταση του τρέχοντος σηµείου από την αρχή των αξόνων ή από το σηµείο p, αντίστοιχα. Point ORIGIN Ένα σταθερό σηµείο που περιγράφει την αρχή των αξόνων. Οι αφηρηµένες µέθοδοι της κλάσης Shape υλοποιούνται µε κατάλληλο τρόπο. Το σηµείο αναφοράς που επιστρέφεται από την getreference για ένα αντικείµενο τύπου Point είναι προφανώς το ίδιο το σηµείο. Ο κώδικας της κλάσης ακολουθεί. Γίνεται χρήση των µαθηµατικών συναρτήσεων της κλάσης Math, που θα περιγραφούν σε επόµενο κεφάλαιο. 91
package shapes; final public class Point extends Shape protected double coordx, coordy; final static public Point ORIGIN = new Point(0.0, 0.0); public Point (double x, double y) coordx = x; coordy = y; public Point getreference () return this; public Shape rotate (Point origin, double angle) Point delta = minus(origin); double radius = delta.distance(); double arg = Math.atan2(delta.coordY, delta.coordx); return new Point( origin.coordx + radius * Math.cos(arg + angle), origin.coordy + radius * Math.sin(arg + angle)); public Shape moveto (Point target) return target; public double getx () return coordx; public double gety () return coordy; public Point plus (Point p) return new Point(coordX + p.coordx, coordy + p.coordy); public Point minus (Point p) return new Point(coordX - p.coordx, coordy - p.coordy); public double distance () return Math.sqrt(coordX * coordx + coordy * coordy); public double distance (Point p) return minus(p).distance(); static public void draw (Canvas c) c.drawpoint(coordx, coordy); Κλάση Circle Η κλάση Circle είναι η υποκλάση της Shape που υλοποιεί έναν κύκλο. Τα επιπλέον µέλη που ορίζονται στην Circle είναι τα εξής: Point center, double radius. Το κέντρο και η ακτίνα του κύκλου αντίστοιχα. Circle (Point c, double r) Κατασκευαστής κύκλου µε κέντρο c και ακτίνα r. 92
Point getcenter (), double getradius (). Συναρτήσεις που επιστρέφουν το κέντρο και την ακτίνα του κύκλου. Το σηµείο αναφοράς του κύκλου είναι προφανώς το κέντρο του. Οι υπόλοιπες αφηρηµένες µέθοδοι της κλάσης Shape υλοποιούνται µε κατάλληλο τρόπο. Ο κώδικας της κλάσης Circle ακολουθεί. package shapes; final public class Circle extends Shape protected Point center; protected double radius; public Circle (Point c, double r) center = c; radius = r; public Point getreference () return center; public Shape rotate (Point origin, double angle) Point newcenter = (Point) center.rotate(origin, angle); return new Circle(newCenter, radius); public Shape moveto (Point target) return new Circle(target, radius); public Point getcenter () return center; public double getradius () return radius; static public void draw (Canvas c) c.drawcircle(center.getx(), center.gety(), radius); Κλάση Polygon Η κλάση αυτή υλοποιεί πολύγωνα και φυσικά κληρονοµεί την κλάση Shape. Οι κορυφές των πολυγώνων ορίζονται ως ένας πίνακας σηµείων. Τα επιπλέον µέλη της κλάσης Polygon είναι τα ακόλουθα: Point [ ] vertex. Ο πίνακας των σηµείων που αποτελούν τις κορυφές του πολυγώνου. Polygon (Point [ ] v). Κατασκευαστής πολυγώνου µε κορυφές v. int getvertices ( ). Επιστρέφει τον αριθµό των κορυφών του πολυγώνου. Point getvertex (int i). Επιστρέφει την i-οστή κορυφή του πολυγώνου. Το σηµείο αναφοράς ενός πολυγώνου είναι το κέντρο βάρους του. Οι υπόλοιπες αφηρηµένες µέθοδοι της κλάσης Shape υλοποιούνται µε κατάλληλο τρόπο. Ο κώδικας της κλάσης Polygon ακολουθεί: 93
package shapes; public class Polygon extends Shape protected Point [] vertex; public Polygon (Point [] v) vertex = v; public Point getreference () double sumx = 0.0; double sumy = 0.0; for (int i = 0 ; i < vertex.length ; i++) sumx += vertex[i].coordx; sumy += vertex[i].coordy; return new Point(sumX/vertex.length, sumy/vertex.length); public Shape rotate (Point origin, double angle) Point [] newvertex = new Point [vertex.length]; for (int i = 0 ; i < vertex.length ; i++) newvertex[i] = (Point) vertex[i].rotate(origin, angle); return new Polygon(newVertex); public Shape moveto (Point target) return moverel(target.minus(getreference())); public Shape moverel (Point displacement) Point [] newvertex = new Point [vertex.length]; for (int i = 0 ; i < vertex.length ; i++) newvertex[i] = vertex[i].plus(displacement); return new Polygon(newVertex); public int getvertices () return vertex.length; public Point getvertex (int i) return vertex[i]; static public void draw (Canvas c) double x1 = vertex[0].getx(); double y1 = vertex[0].gety(); for (int i = 1 ; i < vertex.length ; i++) double x2 = vertex[i].getx(), y2 = vertex[i].gety(); c.drawline(x1, y1, x2, y2); x1 = x2; y1 = y2; 94
c.drawline(x1, y1, vertex[0].getx(), vertex[0].gety()); Interface Canvas Η διαπροσωπεία αυτή περιγράφει αντικείµενα που έχουν τη δυνατότητα σχεδίασης. Ορίζει τρεις µεθόδους, για τη σχεδίαση σηµείων, γραµµών και κύκλων: void drawpoint (double x, double y). Σχεδιάζει ένα σηµείο µε συντεταγµένες (x, y). void drawline (double x1, double y1, double x2, double y2). Σχεδιάζει το ευθύγραµµο τµήµα που ενώνει τα σηµεία (x1, y1) και (x2, y2). void drawcircle (double x, double y, double r). Σχεδιάζει έναν κύκλο µε κέντρο (x, y) και ακτίνα r. Ο κώδικας του interface ακολουθεί: package shapes; interface Canvas public void drawpoint (double x, double y); public void drawline (double x1, double y1, double x2, double y2); public void drawcircle (double x, double y, double r); Κλάση MyScreen Η κλάση αυτή είναι µια υποθετική υλοποίηση της διαπροσωπείας Canvas. Η περιγραφή της υλοποίησης των µεθόδων της παραλείπεται στον κώδικα που ακολουθεί. package shapes; class MyScreen implements Canvas /* Various other things... */ public void drawpoint (double x, double y) /* Forget about the implementation... */ public void drawline (double x1, double y1, double x2, double y2) /* Forget about the implementation... */ public void drawcircle (double x, double y, double r) /* Forget about the implementation... */ Κλάση ExampleShapes Η κλάση αυτή παρέχει τη µέθοδο main που απαιτείται για την εκτέλεση του παραδείγµατος. Στη µέθοδο αυτή ορίζονται δυο σχήµατα, ένα τρίγωνο u954 και ένας κύκλος, και στη συνέχεια σχεδιάζονται, αφού περιστραφούν. Ο κώδικας της κλάσης ακολουθεί. 95
import shapes.*; class ExampleShapes MyScreen scr = new MyScreen(); static public void main (String [] args) Point [] v1 = new Point(1.0, 1.0), new Point(3.0, 1.0), new Point(2.0, 2.0) Shape s1 = new Polygon(v1); Shape s2 = new Circle(new Point(2.0, 2.0), 1.0); s1.rotate(point.origin, Math.PI / 2).draw(scr); s2 = s2.moverel(-1.0, -2.0).draw(scr); 96
4.7. Λυµένες Ασκήσεις 4.7.1. ηµιουργήστε µια κλάση αντικειµένων Point η οποία να περιγράφει σηµεία στο επίπεδο. Να περιλαµβάνει µια µέθοδο κατασκευής, τις µεθόδους πρόσβασης στις συντεταγµένες x και y του σηµείου, µια µέθοδο ελέγχου ισότητας δύο αντικειµένων και µια µέθοδο εκτύπωσης ενός αντικειµένου. Στη συνέχεια γράψτε ένα πρόγραµµα στο οποίο να χρησιµοποιήσετε την κλάση που φτιάξατε και να δηµιουργήσετε δύο σηµεία στο επίπεδο. Με την βοήθεια των δύο αυτών σηµείων να ελέγξετε την κλάση των αντικειµένων που δηµιουργήσατε. Λύση: public class Point public static void main(string[] args) private double x,y; public Point(double a, double b) x=a; x=b; public double x() return x; public double y() return y; public boolean equals(point p) return (_x==p.x && y==p.y); public String tostring() return new String("(" + x + ", " + y + ")"); Ένα πρόγραµµα το οποίο χρησιµοποιεί την παραπάνω κλάση είναι το επόµενο. class TestPoint public static void main(string[] args) Point a =new Point(4,5); 97
System.out.println("a.x = " + a.x() + " a.y = " + a.y()); System.out.println("a = " + a); Point b = new Point(2,3); System.out.println("b = " + b); if(a.equals(b)) System.out.println("To a isoute me to b"); else System.out.println("To a einai diaforetiko tou b"); b=new Point(4,5); System.out.println("b = " + b); if(a.equals(b)) System.out.println("To a isuteme to b"); else System.out.println("To a einai diaforetiko tou b"); Η εκτέλεση του παραπάνω προγράµµατος είναι η ακόλουθη: Στο παραπάνω πρόγραµµα δηµιουργείται το σηµείο a(4,5) το οποίο εκτυπώνετε µε την χρήση πρώτα των µεθόδων x() και y() πρόσβασης στις συντεταγµένες x και y και στη συνέχεια µε την µέθοδο εκτύπωσης tostring(). Κατόπιν ορίζεται το σηµείο b(2,3) και ελέγχεται η ισότητά του µε το σηµείο a µε χρήση της µεθόδου equals(). Τα a και b είναι άνισα. Τέλος ορίζεται εκ νέου το σηµείο b(4,5) και ελέγχεται πάλι η ισότητά του µε το σηµείο a. Αυτή τη φορά τα a και b είναι ίσα. 4.7.2. ηµιουργήστε µια κλάση αντικειµένων Complex η οποία να περιγράφει µιγαδικούς αριθµούς. Να περιλαµβάνει µια µέθοδο κατασκευής, τις µεθόδους πρόσβασης στο πραγµατικό και φανταστικό µέρος του µιγαδικού αριθµού, µια µέθοδο ελέγχου ισότητας δύο αντικειµένων, µία µέθοδο υπολογισµού του µέτρου και µία της γωνίας του µιγαδικού αριθµού και µια µέθοδο εκτύπωσης ενός αντικειµένου. Στη συνέχεια γράψτε ένα πρόγραµµα στο οποίο να χρησιµοποιήσετε την κλάση που φτιάξατε και να δηµιουργήσετε δύο µιγαδικούς αριθµούς τον a=(2)+i(2) και τον b=(- 98
2)+i(-2). Να τυπώσετε τον καθένα τους, το µέτρο και τη γωνία τους. Τέλος να ελέγξετε την ισότητά τους. Λύση: public class Complex private double re, im; public Complex(double x, double y) re=x; im=y; public double re() return re; public double im() return im; public boolean equals(complex a) return (re==a.re && im==a.im); public double metro() return Math.sqrt(re*re + im*im); public double gonia() double result = Math.atan2(re,im); if(result>=0) return result; else return result+2.*math.pi; public String tostring() return new String("(" + re + ") + (" + im + ")i"); Ένα πρόγραµµα το οποίο χρησιµοποιεί την παραπάνω κλάση είναι το επόµενο. class TestComplex public static void main(string[] args) Complex a = new Complex(2,2); Complex b = new Complex(-2,-2); System.out.println("a = " + a); System.out.println(" a = " + a.metro()); 99
System.out.println("theta of a = " + a.gonia()*180/math.pi + "degs"); System.out.println("b = " + b); System.out.println(" b = " + b.metro()); System.out.println("theta of b = " + b.gonia()*180/math.pi + "degs"); Η εκτέλεση του παραπάνω προγράµµατος είναι η ακόλουθη: Στο παραπάνω πρόγραµµα δηµιουργούνται οι µιγαδικοί αριθµοί a=(2)+i(2) και b=(-2)+i(-2). Στη συνέχεια εκτυπώνετε ο καθένας τους µε την χρήση της µεθόδου tostring(). Eπίσης εκτυπώνετε το µέτρο και η γωνία τους µε τη χρήση των µεθόδων metro(), gonia(). Τέλος ελέγχετε η ισότητά τους µε τη µέθοδο equals(). 4.7.3. ηµιουργήστε µια κλάση αντικειµένων Line η οποία να περιγράφει ευθείες στο επίπεδο. Μια ευθεία στο επίπεδο ορίζεται από ένα σηµείο της και από την κλίση της. Για την δηµιουργία αυτής της κλάσης πρέπει να χρησιµοποιήσετε την κλάση Point του παραδείγµατος 5.1.1. Η κλάση που θα δηµιουργήσετε πρέπει να περιλαµβάνει µια µέθοδο κατασκευής, τις µεθόδους πρόσβασης στο σηµείο της ευθείας και στη κλήση της, µία µέθοδο υπολογισµού της τοµής της ευθεία µε τον άξονα των y (χρησιµοποιείστε την εξίσωση yaxis=y0-mx0 µε m=κλίση), µια µέθοδο ελέγχου ισότητας δύο αντικειµένων (χρησιµοποιείστε ως κριτήριο ισότητας την κλίση και το σηµείο τοµής της ευθεία µε τον άξονα των y), και µια µέθοδο εκτύπωσης ενός αντικειµένου όπου θα εκτυπώνετε την ευθεία ως y=(m)x+yaxis. Στη συνέχεια γράψτε ένα πρόγραµµα στο οποίο να χρησιµοποιήσετε την κλάση που φτιάξατε και να δηµιουργήσετε µια ευθεία η οποία ορίζετε από το σηµείο a(5,-4) και έχει κλίση m=-2. Εκτυπώστε το σηµείο a, η κλίση της ευθείας και το σηµείο τοµής της µε τον άξονα y. Τέλος εκτυπώστε η εξίσωση της ευθείας. 100
Λύση: public class Line private Point pline; private double slope; public Line(Point p, double s) pline=p; slope=s; public double slope() return slope; public double yaxis() return (pline.y()-slope*pline.x()); public boolean equals(line a) return (slope()==a.slope() && yaxis()==a.yaxis()); public String tostring() String s = new String("y = (" + slope + ")x + (" + yaxis() + ")"); return s; Ένα πρόγραµµα το οποίο χρησιµοποιεί την παραπάνω κλάση είναι το επόµενο. public class TestLine1 public static void main(string[] args) Point a = new Point(5,-4); Line line1 = new Line(a,-2); System.out.println("Simeio eytheias : " + a); System.out.println("Klisi eytheias : " + line1.slope()); System.out.println("tomi me axona y : " + line1.yaxis()); System.out.println("Exiswsi eytheias : " + line1); 101
Η εκτέλεση του παραπάνω προγράµµατος είναι η ακόλουθη: Στο παραπάνω πρόγραµµα δηµιουργείται µία ευθεία η οποία ορίζετε από το σηµείο a(5,-4) και έχει κλίση m=-2. Στη συνέχεια εκτυπώνετε το σηµείο a, η κλίση της ευθείας και το σηµείο τοµής της µε τον άξονα y. Τέλος εκτυπώνετε η εξίσωση της ευθείας. Αφήνεται ως άσκηση η δηµιουργία µιας δεύτερης ευθείας και ο έλεγχος της ισότητάς της µε την πρώτη. 4.7.4. ηµιουργήστε µια κλάση αντικειµένων Student η οποία να περιγράφει φοιτητές. Ένας φοιτητής έχει το όνοµα το επώνυµό και τον αριθµό µητρώου του. Η κλάση που θα δηµιουργήσετε πρέπει να περιλαµβάνει µια µέθοδο κατασκευής, τις µεθόδους πρόσβασης στο όνοµα, το επώνυµό και τον αριθµό µητρώου του φοιτητή και µια µέθοδο εκτύπωσης ενός αντικειµένου. Στη συνέχεια να δηµιουργήσετε την κλάση JavaStudent η οποία να επεκτείνει την κλάση Student µε τον βαθµό κάθε φοιτητή στο µάθηµα της Java. Η νέα κλάση πρέπει να περιλαµβάνει µια µέθοδο κατασκευής, µια µέθοδο πρόσβασης στο βαθµό του φοιτητή και µια µέθοδο εκτύπωσης ενός αντικειµένου. Στη συνέχεια γράψτε ένα πρόγραµµα στο οποίο να δηµιουργήσετε τέσσερα αντικείµενα της κλάσης JavaStudent. Εισάγετε κατάλληλα δεδοµένα σε αυτά και εκτυπώστε τα. Λύση: public class Student protected String onoma; protected String eponimo; protected int AM; public Student(String onoma, String eponimo, int AM) this.onoma=onoma; this.eponimo=eponimo; this.am=am; public String onoma() 102
return onoma; public String eponimo() return eponimo; public int AM() return AM; public String tostring() String a = new String(onoma + " " + eponimo + " AM : " + AM); return a; Μια άλλη πιθανή λύση για την κλάση Student είναι η ακόλουθη: public class JavaStudent protected double bathmos; public JavaStudent(String onoma, String eponimo, int AM, double bathmos) super(onoma,eponimo,am); this.bathmos=bathmos; public double bathmos() return bathmos; public String tostring() String a = new String(super.toString() + "Bathmos : " + bathmos); return a; Ένα πρόγραµµα το οποίο χρησιµοποιεί τις παραπάνω κλάσεις είναι το επόµενο. public class TestJavaStudent public static void main(string[] args) JavaStudent a = new JavaStudent("Nikolaos", "Apostolou", 3145, 8.5); JavaStudent b = new JavaStudent("Ioannis", "Dalas", 3256, 5.0); 103
JavaStudent c = new JavaStudent("Georgios", "Ladas", 3372, 7.0); JavaStudent d = new JavaStudent("Maria", "Nikolaou", 3484, 6.5); System.out.println(a); System.out.println(b); System.out.println(c); System.out.println(d); Η εκτέλεση του παραπάνω προγράµµατος είναι η ακόλουθη: 4.8. Ασκήσεις 1. ηµιουργείστε µια κλάση που να έχει τις κατάλληλες µεθόδους και κάθε αντικείµενο τις θα αντιπροσωπεύει ένα φοιτητή και θα αποθηκεύει τους βαθµούς 4 µαθηµάτων του. Στο τέλος εκτυπώστε 10 φοιτητές σε αύξουσα σειρά ανάλογα µε τον µέσο όρο των µαθηµάτων του. 2. Επαναλάβετε την προηγούµενη άσκηση κάνοντας χρήση της δεσµευµένης λέξης this. 3. ηµιουργείστε 2 κλάσεις, η πρώτη κλάση θα έχει πολλαπλούς κατασκευαστές και ένα private πεδίο και η δεύτερη κλάση θα έχει τη main µέθοδο που τους καλεί. Επίσης η main θα αλλάζει την τιµή ενός πεδίου private όχι αναφερόµενη απευθείας σε αυτό, αλλά καλώντας τη µέθοδο που του αλλάζει τη τιµή. 4. ηµιουργείστε κλάσεις οι οποίες θα ορίζουν και θα υπολογίζουν τον όγκο ενός κυλίνδρου (η άσκηση να λυθεί χωρίς κληρονοµικότητα). 5. Επαναλάβετε την ποιο πάνω άσκηση µε τη χρήση κληρονοµικότητας. 6. ηµιουργείστε ένα σύνολο κλάσεων που ορίζουν έναν τραπεζικό λογαριασµό (Bank Account). Καθορίστε τι θα πρέπει να κάνουν οι λογαριασµοί, ποια δεδοµένα πρέπει να αποθηκεύουν και ποιες µεθόδους θα πρέπει να χρησιµοποιήσετε. 104
7. Τροποποιήστε την προηγούµενη άσκηση, ώστε να µπορείτε να δηµιουργήσετε µία υπό-κλάση του Account για τα χαρακτηριστικά του Checking Account. 8. Τα Checking Accounts θα πρέπει να παρέχουν προστασία από υπέραναλήψεις. 9. ηµιουργήστε µία απλή εφαρµογή που χρησιµοποιεί τις ασκήσεις 1, 2 και 3 για να παρέχετε υπηρεσίες για on-line άνοιγµα λογαριασµών. 10. ηµιουργήστε ένα πρόγραµµα το οποίο θα προσοµοιώνει ένα ΑΤΜ. εν θα υλοποιηθούν οι λειτουργίες του, παρά µόνο το αρχικό µενού το οποίο θα χειρίζεται ο χρήστης. Αρχικά θα ζητάει από τον χρήστη ένα κωδικό (χάριν ευκολίας ο κωδικός θα είναι πάντα '1234') και στη συνέχεια θα του παρουσιάζει το ακόλουθο µενού επιλογών: 1. Deposit Money 2. Draw Money 3. Transfer Money to another account 4. Check balance 5. Exit Please select 1-5 and press enter: Σε περίπτωση λάθους εισαγωγής θα ειδοποιεί το χρήστη και θα επανεµφανίζει το µενού. Όταν ο χρήστης επιλέξει µια λειτουργία θα εµφανίζεται στην οθόνη το όνοµα της λειτουργίας (π.χ. Depositing Money) και εν συνεχεία θα επανεµφανιστεί το µενού. Αν ο χρήστης βάλει λάθος κωδικό το πρόγραµµα θα συνεχίσει να ζητάει κωδικό. 105
106
5. Εξαιρέσεις (exceptions) 5.1. Εξαιρέσεις Τι είναι µία εξαίρεση (exception); Στη Java, η κλάση Exception ορίζει απλές συνθήκες λάθους που µπορεί να συναντήσουν τα προγράµµατά σας. Αντί να αφήνετε το πρόγραµµα να τερµατιστεί, µπορείτε να γράψετε κώδικα που να χειρίζεται τις εξαιρέσεις και να συνεχίζει την εκτέλεση του προγράµµατος. Τι είναι τότε ένα λάθος (error); Στη Java η κλάση Error ορίζει αυτά που θεωρούνται πολύ σοβαρές περιπτώσεις λαθών και από τις οποίες δε θα πρέπει να προσπαθήσετε να επανακάµψετε. Στις περισσότερες περιπτώσεις είναι καλύτερο να αφήσετε το πρόγραµµα να τερµατιστεί όταν εµφανιστεί ένα λάθος. Η γλώσσα Java υλοποιεί εξαιρέσεις µε τον τρόπο που το κάνει και η C++ ώστε να σας βοηθήσει να δηµιουργήσετε καλό κώδικα. Όταν συµβαίνει ένα λάθος στο πρόγραµµά σας ο κώδικας που ανακαλύπτει το λάθος «πετάει» µία εξαίρεση. Με τον τρόπο αυτό σηµατοδοτεί προς την τρέχουσα εκτελούµενη διεργασία ότι συνέβη ένα λάθος. Μπορείτε να συλλάβετε την εξαίρεση που πετάχτηκε και στη συνέχεια, αν είναι δυνατό, να επανακάµψετε. java.lang.object java.lang.throwable java.lang.error java.lang.exception 5.2. Παράδειγµα Εξαίρεσης Έστω η πιο κάτω επέκταση του προγράµµατος HelloWorld.java (η οποία ανακυκλώνεται σε διάφορα µηνύµατα): public class HelloWorld public static void main (String args[]) int i=0; String greetings[] = Hello world!, No, I mean it!, HELLO WORLD!! ; while (i < 4) System.out.println(greetings[i]); i++; 107
5.3. Χειρισµός εξαίρεσης Κανονικά το πρόγραµµα τερµατίζει µε µήνυµα λάθους όταν «πετάξετε» µία εξαίρεση, όπως θα κάνει και το πιο πάνω πρόγραµµα µετά από τέσσερεις κύκλους: $ java HelloWorld Hello world! No, I mean it! HELLO WORLD!! java.lang.arrayindexoutofboundsexception: 3 at HelloWorld.main(HelloWorld.java:12) Όµως η σηµασία του χειρισµού των εξαιρέσεων είναι ότι µπορείτε να γράψετε κώδικα που να συλλαµβάνει τις εξαιρέσεις, να τις χειρίζεται και να συνεχίζει την εκτέλεση του προγράµµατος. Η γλώσσα Java έχει διευκολύνσεις που σας επιτρέπουν να καταλάβετε ποια εξαίρεση «πετάχτηκε» και στη συνέχεια να προσπαθήσετε να ανακάµψετε. 5.3.1. Οι εντολές try και catch Για να αντιµετωπίσετε µία συγκεκριµένη εξαίρεση, χρησιµοποιήστε την εντολή try µε τον κώδικα που µπορεί να πετάξει την εξαίρεση. Για να συλλάβετε και να δράσετε πάνω στην εξαίρεση που προέκυψε χρησιµοποιήστε την εντολή catch για να καθορίσετε τόσο την εξαίρεση που θα συλλάβετε όσο και τον κώδικα που θα εκτελεστεί αν προκύψει η συγκεκριµένη εξαίρεση. try // Κώδικας που µπορεί να προκαλέσει µία συγκεκριµένη εξαίρεση catch (MyExceptionType e) // Κώδικας που θα εκτελσθεί αν προκύψει η MyExceptionType 5.3.2. Η εντολή finally Η εντολή finally ορίζει ένα τµήµα κώδικα που θέλετε να εκτελείτε πάντα ανεξάρτητα από το αν έχει συλληφθεί ή όχι µία εξαίρεση. Ο επόµενος κώδικας προέρχεται από το βιβλίο Low Level Security in Java του Frank Yellin. try startfaucet(); waterlawn(); finally stopfaucet(); 108
Η Java εγγυάται ότι θα κλείσει η βρύση, ακόµα και αν συµβεί µία εξαίρεση όταν ανοίγει η βρύση, ή κατά το πότισµα. Ο κώδικας µέσα στα άγκιστρα µετά το try ονοµάζεται προστατευόµενος κώδικας. Η µοναδική φορά που η εντολή finally δε θα εκτελεστεί είναι αν εκτελεστεί η µέθοδος System.exit() µέσα στον προστατευόµενο κώδικα. Στην περίπτωση αυτή το πρόγραµµα τερµατίζει άµεσα. 5.3.3. Ξανά στο παράδειγµα Το επόµενο παράδειγµα είναι επανάληψη της µεθόδου main(). Η εξαίρεση που παράγεται στην προηγούµενη έκδοση του προγράµµατος συλλαµβάνεται και η τιµή του δείκτη του πίνακα γίνεται και πάλι 0. public static void main (String args[]) int i=0; String greetings[] = Hello world!, No, I mean it!, HELLO WORLD!! ; while (i < 4) try System.out.println(greetings[i]); catch (ArrayIndexOutOfBoundsException e) System.out.println( Re-setting Index Value ); i = -1; catch (Exception e) System.out.println(e.toString()); finally System.out.println( This is always printed ); i++; // end while // end main Το µήνυµα που παρουσιάζεται στην οθόνη εναλλάσσεται καθώς εκτελείται ο βρόχος: Hello world! This is always printed No, I mean it! This is always printed HELLO WORLD!! This is always printed Re-setting Index Value 109
This is always printed 5.3.4. Συνήθεις εξαιρέσεις Η γλώσσα Java παρέχει ορισµένες προκαθορισµένες εξαιρέσεις. Οι επόµενες είναι µερικές από τις πιο συνηθισµένες εξαιρέσεις που µπορεί να συναντήσετε: ArithmeticException Τυπικά το αποτέλεσµα µία διαίρεσης µε το µηδέν για ακεραίους nt i = 12/0; NullPointerException Γίνεται προσπάθεια για προσπέλαση σε ένα αντικείµενο ή µέθοδο πριν αυτό στιγµιοτυπηθεί Image im [] = new Image[4]; System.out.println(im[0].toString); NegativeArraySizeException Γίνεται προσπάθεια να δηµιουργηθεί ένας πίνακας µε αρνητικό µέγεθος ArrayIndexOutOfBoundsException Γίνεται προσπάθεια για προσπέλαση ενός στοιχείου ενός πίνακα πέρα από τον αρχικό ορισµό του µεγέθους του πίνακα SecurityException Συνήθως εµφανίζεται σε ένα browser, όταν η κλάση SecurityManager πετάει µία εξαίρεση για τα applets που προσπαθούν Να προσπελάσουν ένα τοπικό αρχείο Να ανοίξουν ένα socket σε υπολογιστή άλλον από αυτόν ο οποίος εξυπηρετεί το applet Εκτελεί ένα άλλο πρόγραµµα στο περιβάλλον εκτέλεσης 5.4. Κατηγορίες εξαίρεσης Υπάρχουν τρεις µεγάλες κατηγορίες εξαιρέσεων στη Java. Στην πράξη, η Java ορίζει την κλάση java.lang.throwable, η οποία λειτουργεί ως γονική κλάση για όλα τα αντικείµενα που µπορούν να «πετακτούν» και να «συλληφθούν» µε χρήση των µηχανισµών διαχείρισης των εξαιρέσεων. Υπάρχουν τρεις σηµαντικές υποκλάσεις αυτών, που φαίνονταιστο επόµενο διάγραµµα: Throwable Exception Error RuntimeException 110
Η κλάση Throwable δεν πρέπει να χρησιµοποιείται, αλλά αντί αυτής, µία από τις άλλες τρεις θα περιγράφουν µία συγκεκριµένη εξαίρεση. Στη συνέχεια περιγράφεται ο σκοπός κάθε µίας από αυτές. Η Error δηλώνει ένα σοβαρό πρόβληµα από το οποίο η ανάκαµψη είναι δύσκολη, αν όχι αδύνατη. Ένα παράδειγµα είναι να µην υπάρχει άλλη διαθέσιµη µνήµη. εν αναµένεται από ένα πρόγραµµα να µπορεί να αντιµετωπίσει τέτοιου είδους συνθήκες, αν και είναι δυνατό µε προσοχή. Η RuntimeException θα πρέπει να χρησιµοποιείται για να δηλώσει ένα πρόβληµα σχεδίασης ή υλοποίησης. ηλαδή, θα πρέπει να χρησιµοποιείται για να δηλώσει συνθήκες κατά τη φάση της εκτέλεσης που δε θα έπρεπε να συµβούν αν το πρόγραµµα λειτουργούσε σωστά. Αυτό εφαρµόζεται για παράδειγµα σε δείκτες πινάκων που βγαίνουν εκτός ορίων, και στην αναφορά µέσω µίας null µεταβλητής αντικειµένου. Επειδή ένα καλά ορισµένο και υλοποιηµένο πρόγραµµα δεν προκαλεί ποτέ αυτού του είδους οι εξαιρέσεις, είναι σύνηθες να µην τις ελέγχουµε. Αυτό έχει ως αποτέλεσµα ένα µήνυµα κατά το χρόνο εκτέλεσης και επιβεβαιώνει ότι θα ληφθεί µέριµνα για τη διόρθωση του σφάλµατος, αντί για να το κρύβει ώστε να µη γίνει αντιληπτό. Άλλες εξαιρέσεις δηλώνουν µία δυσκολία κατά το χρόνο εκτέλεσης που µπορεί να εµφανιστεί λόγω επιδράσεων του περιβάλλοντος και µπορούµε να το χειριστούµε. Παραδείγµατα περιλαµβάνουν αρχεία που δεν βρίσκονται και URL που δεν έχουν σχηµατιστεί ορθά, που αµφότερα θα µπορούσαν να συµβούν αν ο χρήστης έκανε ένα λάθος πληκτρολόγησης. Εφόσον αυτά µπορεί να συµβούν ως αποτέλεσµα ενός λάθους η Java ενθαρρύνει τον προγραµµατιστή να τα χειριστεί. 5.5. Ο κανόνας «ήλωσε ή αντιµετώπισε» Για να ενθαρρύνει τους προγραµµατιστές να γράφουν εύρωστο κώδικα η Java απαιτεί από ότι αν µία µέθοδος κάνει µια πράξη η οποία µπορεί να έχει ως αποτέλεσµα µία Exception, διακριτή από Error ή RuntimeException, τότε πρέπει να δώσει ξεκάθαρα τις πράξεις που κάνει όταν συµβεί το πρόβληµα. Υπάρχουν δύο πράγµατα που µπορεί να κάνει ο προγραµµατιστής για να ικανοποιήσει αυτή την απαίτηση. Ένα µπλοκ try catch όπου στο catch παρουσιάζονται τα ονόµατα οποιασδήποτε υπερκλάσης της εξαίρεσης που «πετάει» ο κώδικας θεωρείται ότι αντιµετωπίζει την κατάσταση, ακόµα και αν ο κώδικας µέσα στο catch είναι κενός. Εναλλακτικά, µπορείτε να δηλώσετε ότι η εξαίρεση δε χειρίζεται από αυτή τη µέθοδο, και συνεπώς θα την «πετάξει» στη µέθοδο που την κάλεσε. Αυτό γίνεται σηµειώνοντας στη δήλωση της µεθόδου την πρόταση throws, ως εξής: public void troublesome() throws IOException Μετά τη δεσµευµένη λέξη throws υπάρχει µία λίστα µε όλες τις εξαιρέσεις που µπορεί να προκύψουν µέσα στη µέθοδο, αλλά τις οποίες δε χειρίζεται η µέθοδος. Αν και εδώ παρουσιάζεται µία µόνο εξαίρεση, µπορούµε να χρησιµοποιήσουµε µία λίστα εξαιρέσεων που διαχωρίζονται µε κόµµα αν είναι πολλές οι πιθανές εξαιρέσεις που δε χειρίζεται η µέθοδος. 5.6. ηµιουργώντας τις δικές σας εξαιρέσεις Μπορείτε επίσης να δηµιουργείτε τις δικές σας εξαιρέσεις ως εξής: 111
public class MyException extends Exception private String reason; public MyException(String cause) reason = cause; public String getreason() return reason; Για να «πετάξετε» µία εξαίρεση που έχετε δηµιουργήσει χρησιµοποιείται το πιο κάτω: throw new MyException(); 5.6.1. Παράδειγµα Έστω ένα πρόγραµµα εξυπηρέτη-εξυπηρετούµενου (client-server). Στον κώδικα του εξυπηρετούµενου µπορείτε να δοκιµάσετε να συνδεθείτε στον εξυπηρέτη και να περιµένετε να σας απαντήσει µέσα σε πέντε δευτερόλεπτα. Αν ο εξυπηρέτης δεν απαντήσει ο κώδικάς σας µπορεί να «πετάξει» µία εξαίρεση (όπως η ορισµένη από το χρήστη ServerTimedOutException): public void connectme (String servername) throws ServerTimedOutException int success; int porttoconnect = 80; success = open(servername, porttoconnect); if (success == -1) throw new ServerTimedOutException(); Για να συλλάβετε την εξαίρεσή σας, χρησιµοποιείτε µία εντολή try: public void findserver()... try connectme(defaultserver); catch (ServerTimedOutException e) g.drawstring( Server timed out, trying alternate, 5, 5); try 112
... connectme(alternateserver); catch (ServerTimedOutException e) g.drawstring( No server currently available, 5,5); 113
5.7. Λυµένες Ασκήσεις 5.7.1. ηµιουργήστε τρεις κλάσεις για το χειρισµό εξαιρέσεων (exceptions): µια χωρίς διαχείριση, µια µε µε αναλυτική διαχείριση εξαιρέσεων και µια µε συγκεντρωτική διαχείριση εξαιρέσεων. Λύση: // Χειρισµός εξαιρέσεων (Exception) --------------------------------------------------- Ενα απλό πρόγραµµα χωρίς διαχείριση εξαιρέσεων public class except1 public static void main() int a=0,b[]; String s="4"; //String s="4.5"; // Προκαλεί NumberFormatException //String s="-4"; // Προκαλεί NegativeArraySizeException //String s="8"; // Προκαλεί ArithmeticException //String s="2"; // Προκαλεί ArrayIndexOutOfBoundsException a=integer.parseint(s); b=new int[a]; for(int i=0;i<a;i++)b[i]=a/(i-5); System.out.println(s+" "+b[0]+" "+b[1]+" "+b[2]); --------------------------------------------------- Το ίδιο πρόγραµµα µε αναλυτική διαχείριση εξαιρέσεων public class except2 public static void main() int a=0,b[]; //String s="4"; //String s="4.5"; //String s="-4"; //String s="8"; String s="2"; try a=integer.parseint(s); catch(numberformatexception e) 114
try a=4; b=new int[a]; catch(negativearraysizeexception e) b=new int[50]; try for(int i=0;i<a;i++)b[i]=a/(i-5); catch(arithmeticexception e) b[0]=15; try System.out.println(s+" "+b[0]+" "+b[1]+" "+b[2]); catch(arrayindexoutofboundsexception e) System.out.println("..."); --------------------------------------------------- Το ίδιο πρόγραµµα µε συγκεντρωτική διαχείριση εξαιρέσεων public class except3 public static void main() int a=0,b[]=new int[20]; //String s="4"; //String s="4.5"; String s="-4"; //String s="8"; //String s="2"; try a=integer.parseint(s); b=new int[a]; for(int i=0;i<a;i++)b[i]=a/(i-5); System.out.println(s+" "+b[0]+" "+b[1]+" "+b[2]); catch(exception e) 115
System.out.println("Πρόβληµα..."); // Εµφάνιση µηνύµατος System.out.println(e.getMessage()); // Εµφάνιση του µηνύµατος της εξαίρεσης System.out.println(e.toString()); // Εµφάνιση του ονόµατος και του µηνύµατος της εξαίρεσης e.printstacktrace(); // Εµφάνιση της θέσης της εξαίρεσης 5.7.2. ηµιουργήστε µια κλάση για την παραγωγή εξαιρέσεων (exceptions). Λύση: // ηµιουργία µιας εξαίρεσης // Παραγωγή εξαιρέσεων (Exception) public class except4 public static void main() int a=0,b[]; String s="4"; //String s="4.5"; //String s="-4"; //String s="8"; //String s="2"; a=integer.parseint(s); b=new int[a]; try for(int i=0;i<a;i++)b[i]=f(a,i); catch(exeresi e) // Σύλληψη της εξαίρεσης System.out.println("+++"); System.out.println(e.getMessage()); System.out.println(e.toString()); e.printstacktrace(); System.out.println(s+" "+b[0]+" "+b[1]+" "+b[2]); static int f(int p, int q)throws exeresi // Αποστολή της εξαίρεσης if(q==2)throw new exeresi(); // ηµιουργία της εξαίρεσης (new) return p/(q-5); 116
5.8. Ασκήσεις ιαχείριση εξαιρέσεων 1. Χρησιµοποιείστε το παράδειγµα εξαίρεσης για να δηµιουργήσετε µία εξαίρεση όταν ο δείκτης του πίνακα βγαίνει έξω από τα όρια του µεγέθους του πίνακα, ή τροποποιήστε ένα δικό σας πρόγραµµα ώστε να προκαλεί µία εξαίρεση. 2. Χρησιµοποιείστε τις try και catch για να ανακάµψετε από την εξαίρεση. 3. Χρησιµοποιείστε το παράδειγµα εξαίρεσης για να δηµιουργήσετε µία εξαίρεση όταν γίνεται διαίρεση µε 0 κάποιου ακεραίου. ηµιουργήστε τη δική σας εξαίρεση 1. Χρησιµοποιώντας την κλάση Bank που δηµιουργήσατε στις ασκήσεις του κεφαλαίου 4 και προσθέστε τις πιο κάτω εξαιρέσεις: a. AccountOverdrawnException όταν γίνεται προσπάθεια να γίνει ανάληψη περισσότερων χρηµάτων από ένα λογαριασµό, από το ποσό που περιέχει ο λογαριασµός. b. InvalidDepositException όταν το ποσό προς κατάθεση δεν είναι αποδεκτό (π.χ. είναι αρνητικό) 117
118
6. Ρεύµατα Εισόδου/Εξόδου και Αρχεία 6.1. Ρεύµατα εισόδου/εξόδου στη Java Το κεφάλαιο αυτό εξετάζει πώς αντιµετωπίζει η Java την είσοδο/έξοδο bytes και χαρακτήρων (συµπεριλαµβανοµένων των stdin, stdout και stderr) µε χρήση της ιδέας των ρευµάτων (streams). Στη συνέχεια θα εξετάσουµε ορισµένες ειδικές λεπτοµέρειες σχετικά µε το χειρισµό των αρχείων και των δεδοµένων σε αυτά. 6.1.1. Βασικά σχετικά µε τα ρεύµατα Ένα ρεύµα είναι είτε µία πηγή από bytes είτε ένας προορισµός των bytes. Η σειρά είναι σηµαντική. Συνεπώς για παράδειγµα ένα πρόγραµµα που επιθυµεί να διαβάσει από το πληκτρολόγιο µπορεί να χρησιµοποιήσει για το λόγο αυτό ένα ρεύµα. ύο βασικές κατηγορίες των ρευµάτων είναι τα ρεύµατα εισόδου και τα ρεύµατα εξόδου. Μπορείτε να διαβάσετε από ένα ρεύµα εισόδου, αλλά δεν µπορείτε να γράψετε σε αυτό. Αντίστροφα µπορείτε να γράψετε σε ένα ρεύµα εξόδου, αλλά δεν µπορείτε να διαβάσετε από αυτό. Σαφώς για να διαβάσετε bytes από ένα ρεύµα εισόδου, θα πρέπει να υπάρχει µία πηγή χαρακτήρων η οποία να έχει συσχετισθεί µε το ρεύµα αυτό. Στο πακέτο java.io ορισµένα ρεύµατα είναι «κοµβικά» ρεύµατα, δηλαδή διαβάζουν από ή γράφουν σε ένα συγκεκριµένο σηµείο όπως ένα αρχείο στο δίσκο ή σε µία περιοχή µνήµης. Άλλα ρεύµατα καλούνται φίλτρα. Ένα ρεύµα εισόδου φίλτρο δηµιουργείται µε µία σύνδεση σε ένα υπάρχον ρεύµα εισόδου, έτσι ώστε όταν προσπαθείτε να διαβάσετε από ένα ρεύµα εισόδου φίλτρο αυτό σας παρέχει τους χαρακτήρες που έχουν αρχικά προέλθει από ένα άλλο ρεύµα εισόδου. 119
6.1.2. Μέθοδοι InputStream int read() int read(byte[]) int read(byte[], int, int) Αυτές οι τρεις µέθοδοι παρέχουν πρόσβαση στα δεδοµένα από µία σωλήνωση (pipe). Η απλή µέθοδος read επιστρέφει έναν ακέραιο ο οποίος περιέχει είτε ένα byte που έχει διαβαστεί από το ρεύµα είτε 1 που δηλώνει τη συνθήκη τέλους αρχείου. Οι άλλες δύο διαβάζουν και τοποθετούν το αποτέλεσµα στον πίνακα byte και επιστρέφουν το πλήθος των bytes που διάβασαν. Τα δύο int ορίσµατα στην τρίτη µέθοδο δηλώνουν το υπό-εύρος που θα πρέπει να «γεµίσει» στον πίνακα-στόχο. Σηµείωση Για λόγους απόδοσης διαβάζετε πάντα δεδοµένα στο µεγαλύτερο προβλεπόµενο τµήµα. void close() Όταν έχετε ολοκληρώσει µε ένα ρεύµα πρέπει να το κλείσετε. Αν έχετε δηµιουργήσει µία «στοίβα» ρευµάτων, µε χρήση ρευµάτων φίλτρων, θα πρέπει να κλείσετε το ρεύµα που βρίσκεται στην κορυφή της στοίβας. Η πράξη αυτή κλείνει και τα κατώτερα ρεύµατα. int available() Η µέθοδος αυτή αναφέρει το πλήθος των bytes που είναι άµεσα διαθέσιµα από το ρεύµα. Μία πράξη read αµέσως µετά την κλήση αυτή µπορεί στην πράξη να επιστρέψει περισσότερα bytes. skip(long) Η µέθοδος αυτή αγνοεί τον καθορισµένο αριθµό χαρακτήρων από το ρεύµα. marksupported() mark(int) reset() Οι µέθοδοι αυτοί χρησιµοποιούνται για να πραγµατοποιήσουν πράξεις «push back» σε ένα ρεύµα, εφόσον αυτό τις υποστηρίζει. Η µέθοδος marksupported() επιστρέφει true αν οι πράξεις mark() και reset() είναι λειτουργικές στο συγκεκριµένο ρεύµα. Η µέθοδος mark(int) χρησιµοποιείται για να δηλώσει ότι πρέπει να σηµειωθεί το συγκεκριµένο σηµείο στο ρεύµα και θα πρέπει να δεσµευθεί ενδιάµεση µνήµη (buffer) τόσο µεγάλη όσο τουλάχιστον το καθορισµένο πλήθος χαρακτήρων. Μετά από διαδοχικές πράξεις read() η κλήση της reset() επιστρέφει το ρεύµα εισόδου σε ένα προηγούµενο σηµείο. 6.1.3. Μέθοδοι OutputStream write(int) write(byte[]) write(byte[], int, int) 120
Οι µέθοδοι αυτοί γράφουν σε ένα ρεύµα εξόδου. Όπως και µε την είσοδο µπορείτε πάντα να προσπαθείτε να γράφετε όσο το δυνατό µεγαλύτερα µπλοκ χαρακτήρων. close() Τα ρεύµατα εξόδου θα πρέπει να κλείνουν όταν έχετε ολοκληρώσει µαζί τους. Και πάλι αν υπάρχει µία στοίβα ρευµάτων, θα πρέπει να κλείσετε το ρεύµα που βρίσκεται στην κορυφή και αυτό θα κλείσει και τα υπόλοιπα. flush() Ορισµένες φορές ένα ρεύµα εξόδου µπορεί να αποθηκεύει εγγραφές προτού τις επικυρώσει. Η µέθοδος flush() σάς επιτρέπει να αναγκάσετε την επικύρωση αυτή να συµβεί. 6.2. Βασικές κλάσεις ρευµάτων Στο πακέτο java.io περιγράφονται αρκετές κλάσεις ρευµάτων. Στη συνέχεια παρουσιάζεται η ιεραρχία των κλάσεων στο πακέτο αυτό. Οι πιο συνηθισµένες περιγράφονται στη συνέχεια. 6.2.1. FileInputStream και FileOutputStream Οι δύο αυτές κλάσεις είναι ρεύµατα κόµβοι και, όπως δηλώνει και το όνοµά τους, χρησιµοποιούν αρχεία σε δίσκους. Οι συναρτήσεις δηµιουργίαςγια τις κλάσεις αυτές σάς επιτρέπουν να καθορίσετε τη διαδροµή προς το αρχείο µε το οποίο συνδέονται. Για να δηµιουργήσετε ένα FileInputStream πρέπει το σχετιζόµενο αρχείο να υπάρχει και να είναι αναγνώσιµο. Αν δηµιουργήσετε ένα FileOutputStream αν το αρχείο εξόδου υπάρχει θα γράψετε πάνω του. FileInputStream infile = new FileInputStream( myfile.dat ); FileOutputStream outfile = new FileOutputStream( results.dat ); Προς το τέλος του κεφαλαίου θα παρουσιαστούν δύο ακόµα κλάσεις που εµπλουτίζουν της δυνατότητας της Java για είσοδο/έξοδο σε αρχεία. 6.2.2. BufferedInputStream και BufferedOutputStream Είναι ρεύµατα φίλτρων που µπορούν να χρησιµοποιηθούν για να αυξηθεί η απόδοση των πράξεων εισόδου/εξόδου. 121
6.2.3. DataInputStream και DataOutputStream Αυτά τα ρεύµατα φίλτρα επιτρέπουν ανάγνωση και εγγραφή των βασικών τύπων της Java µέσω ρευµάτων. Παρέχονται πλήθος µεθόδων για τους διάφορους βασικούς τύπους. Για παράδειγµα: Μέθοδοι DataInputStream byte readbyte() long readlong() double readdouble() Μέθοδοι DataOutputStream void writebyte(byte) void writelong(long) void writedouble(double) Σηµειώστε ότι οι µέθοδοι του DataInputStream «ζευγαρώνουν» µε µεθόδους του DataOutputStream. Τα ρεύµατα αυτά έχουν µεθόδους για ανάγνωση και εγγραφή συµβολοσειρών, αλλά οι µέθοδοι αυτοί δε θα πρέπει να χρησιµοποιούνται, καθώς τις έχουν αντικαταστήσει οι µέθοδοι των Readers και Writers που θα δούµε στη συνέχεια. 6.2.4. PipedInputStream και PipedOutputStream Τα ρεύµατα σωλήνωσης (piped stream) µπορούν να χρησιµοποιηθούν για επικοινωνία µεταξύ ρευµάτων. Ένα αντικείµενο PipedInputStream σε ένα νήµα λαµβάνει την είσοδό του από ένα συµπληρωµατικό αντικείµενο PipedOutputStream σε ένα άλλο νήµα. Τα ρεύµατα σωλήνωσης πρέπει να έχουν ένα τµήµα εισόδου και ένα τµήµα εξόδου για να είναι χρήσιµα. 6.3. Ρεύµατα εισόδου URL Εκτός από τη βασική πρόσβαση σε αρχεία η Java σάς παρέχει τη δυνατότητα να χρησιµοποιείτε οµοιόµορφους προσδιοριστές τοποθέτησης (uniform resource locators URLs) ως µέσο πρόσβασης αντικειµένων µέσω ενός δικτύου. Μπορείτε να χρησιµοποιήσετε έµµεσα ένα αντικείµενο URL όταν προσπελαύνετε ήχο και εικόνα µε χρήση της µεθόδου getdocumentbase() για τα applets. String imagefile = new String( images/duke/t1.gif ); images[0] = getimage(getdocumentbase(), imagefile); Όµως µπορείτε να παρέχετε ένα URL άµεσα ως εξής: java.net.url imagesource; try imagesource = new URL( http://mysite.com/~info ); catch (MalformedURLException e) images[0] = getimage(imagesource, Duke/T1.gif ); 122
6.3.1. Ανοίγοντας ένα ρεύµα εισόδου Μπορείτε να ανοίξετε επίσης ένα ρεύµα εισόδου µε βάση ένα κατάλληλο URL αποθηκεύοντας ένα αρχείο δεδοµένων κάτω από τον κατάλογο βάσης των κειµένων (document base directory). InputStream is; String datafile = new String( Data/data.1-96 ); byte buffer[] = new byte[24]; try // το new URL «πετάει» την MalformedURLExcpetion // το URL.openStream() «πετάει» την IOException is = (new URL(getDocumentBase(), datafile)).openstream(); catch (Exception e) Τώρα µπορείτε να χρησιµοποιήσετε το is για να διαβάσετε πληροφορία, όπως και µε ένα αντικείµενο FileInputStream. try is.read(buffer, 0, buffer.length); catch (IOException e1) Προσοχή Να θυµάστε ότι οι περισσότεροι χρήστες µπορεί να έχουν ρυθµίσει την ασφάλεια στους πλοηγούς (browsers) τους να µην επιτρέπουν σε applets πρόσβαση σε αρχεία. 6.4. Αναγνώστες και Εγγραφείς 6.4.1. Unicode Η Java χρησιµοποιεί Unicode για την αναπαράσταση συµβολοσειρών και χαρακτήρων και επίσης παρέχει 16-bit εκδόσεις των ρευµάτων που επιτρέπουν αντίστοιχη αντιµετώπιση στους χαρακτήρες. Αυτές οι 16-bit εκδόσεις ονοµάζονται αναγνώστες (Readers) και εγγραφείς (Writers) και όπως και τα ρεύµατα βρίσκονται στο πακέτο java.io. Οι πιο σηµαντικές εκδόσεις είναι οι InputStreamReader και OutputStreamWriter. Αυτές οι κλάσεις χρησιµοποιούνται για διεπαφή ανάµεσα σε ρεύµατα bytes και αναγνώστες και εγγραφείς χαρακτήρων. Όταν δηµιουργείτε ένα InputStreamReader ή έναν OutputStreamWriter, ορίζονται επίσης κανόνες για τη µετατροπή ανάµεσα στα 16 bit του Unicode και άλλες αναπαραστάσεις εξειδικευµένες στην πλατφόρµα. 123
6.4.2. Μετατροπές Byte - χαρακτήρων Εξ ορισµού αν απλά δηµιουργήσετε ένα αναγνώστη ή ένα εγγραφέα που συνδέεται µε ένα ρεύµα τότε οι κανόνες µετατροπής θα µετατρέπουν ανάµεσα σε bytes µε χρήση την εξ ορισµού κωδικοποίηση χαρακτήρων της πλατφόρµας και Unicode. Συνεπώς, στις αγγλόφωνες χώρες η κωδικοποίηση byte που θα χρησιµοποιείται θα είναι ISO 8859-1. Μπορείτε να καθορίσετε εναλλακτική κωδικοποίηση byte χρησιµοποιώντας µία από τις υποστηριζόµενες µορφές κωδικοποίησης. Μπορείτε να βρείτε τις υποστηριζόµενες µορφές κωδικοποίησης στην τεκµηρίωση για το εργαλείο native2ascii. 6.4.3. Αναγνώστες και Εγγραφείς µε ενδιάµεση µνήµη Επειδή η µετατροπή ανάµεσα σε µορφές είναι όπως και άλλες πράξεις εισόδου/εξόδου περισσότερο αποδοτική όταν γίνεται σε µεγάλα τµήµατα, είναι γενικά καλή ιδέα να βάλουµε σε αλυσίδα ένα BufferedReader ή BufferedWriter όποιον ταιριάζει µαζί µε ένα InputStreamReader ή OutputStreamWriter. Να θυµάστε να χρησιµοποιείτε τη µέθοδο flush() στο BufferedWriter. 6.4.4. Ανάγνωση συµβολοσειράς από την είσοδο Το παράδειγµα που ακολουθεί δείχνει µία απλή τεχνική που θα πρέπει να χρησιµοποιείτε για να διαβάσετε πληροφορία String από το πρότυπο κανάλι εισόδου. public class CharInput public static void main(string args[]) String s; InputStreamReader ir; BufferedReader in; ir = new InputStreamReader(System.in); in = new BufferedReader(ir); while ((s = in.readline())!= null) System.out.println( Read: + s); 6.4.5. Χρησιµοποιώντας άλλες µετατροπές χαρακτήρων Αν χρειάζεται να διαβάσετε είσοδο από κωδικοποίηση χαρακτήρων που είναι διαφορετική από την τοπική σας, ίσως για την ανάγνωση µέσω µίας δικτυακής σύνδεσης µε υπολογιστή διαφορετικού τύπου, µπορείτε να δηµιουργήσετε ένα InputSreamReader δίνοντας ρητά την κωδικοποίηση χαρακτήρων ως εξής: ir = new InputSreamReader(System.in, 8859-1 ); Σηµείωση Αν διαβάζετε χαρακτήρες από µία δικτυακή σύνδεση, θα πρέπει γενικά να χρησιµοποιείτε αυτή τη µορφή. Αν όχι, το πρόγραµµά σας θα προσπαθεί πάντα να µετατρέψει τους χαρακτήρες που διαβάζει σα να ήταν στην τοπική αναπαράσταση, 124
πράγµα που πιθανότατα να µην είναι σωστό. Το ISO 8859-1 είναι το σχήµα κωδικοποίησης Latin-1 που απεικονίζεται στο ASCII. 6.5. Αρχεία Πριν πραγµατοποιήσετε είσοδο/έξοδο σε ένα αρχείο πρέπει να λάβετε τη βασική πληροφορία σχετικά µε το αρχείο αυτό. Η κλάση File παρέχει ορισµένες βοηθητικές συναρτήσεις για αντιµετώπιση αρχείων και τη λήψη της βασικής πληροφορίας για τα αρχεία αυτά. 6.5.1. ηµιουργία ενός νέου αντικειµένου File File myfile; myfile = new File( mymotd ); myfile = new File( /, mymotd ); // Πιο χρήσιµο αν ο κατάλογος ή το όνοµα αρχείου είναι µεταβλητή File mydir = new File( / ); myfile = new File(myDir, mymotd ); Η συνάρτηση δηµιουργίας που θα χρησιµοποιήσετε εξαρτάται από τα άλλα αντικείµενα αρχείων που προσπελαύνετε. Για παράδειγµα, αν στην εφαρµογή σας χρησιµοποιείτε µόνο ένα αρχείο, χρησιµοποιήστε την πρώτη συνάρτηση δηµιουργίας. Αν, όµως, χρησιµοποιείτε διάφορα αρχεία από ένα κοινό κατάλογο, η χρήση της δεύτερης ή τρίτης συνάρτησης δηµιουργίας µπορεί να είναι πιο απλή. Σηµείωση Μπορείτε να χρησιµοποιήσετε ένα αντικείµενο File ως όρισµα συνάρτησης δηµιουργίας για αντικείµενα FileInputStream και FileOutputStream, αντί για ένα String. Αυτό επιτρέπει µεγαλύτερη ανεξαρτησία από τις συµβάσεις του τοπικού συστήµατος αρχείων και γενικά συνιστάται. 6.6. Έλεγχοι και βοηθητικές συναρτήσεις σε αρχεία Από τη στιγµή που έχετε δηµιουργήσει ένα αρχείο File µπορείτε να χρησιµοποιήσετε οποιαδήποτε από τις πιο κάτω µεθόδους για να συλλέξετε πληροφορία σχετικά µε το αρχείο. 6.6.1. Ονόµατα αρχείων String getname() String getpath() String getabsolutepath() String getparent() boolean renameto(file newname) 6.6.2. Έλεγχοι σε αρχεία boolean exists() boolean canwrite() boolean canread() boolean isfile() boolean isdirectory() 125
boolean isabsolute() 6.6.3. Γενική πληροφορία αρχείων και βοηθητικές συναρτήσεις long lastmodified() long length() boolean delete() 6.6.4. Βοηθητικές συναρτήσεις καταλόγων boolean mkdir() String[] list() 6.7. Αρχεία τυχαίας προσπέλασης Συχνά δε θέλετε να διαβάσετε από ένα αρχείο ξεκινώντας από την αρχή και φτάνοντας στο τέλος. Μπορεί να θέλετε να προσπελάσετε ένα αρχείο κειµένου (text file) ως βάση δεδοµένων, όπου µετακινήστε διαβάζοντας µία εγγραφή, µετά µία άλλη, µετά άλλη κάθε µία από τις οποίες βρίσκεται σε διαφορετικό σηµείο του αρχείου. Η γλώσσα Java παρέχει µία κλάση RandomAccessFile για το χειρισµό αυτού του τύπου εισόδου ή εξόδου. 6.7.1. ηµιουργία ενός αρχείο τυχαίας προσπέλασης Για να ανοίξετε ένα αρχείο τυχαίας προσπέλασης (random access file) έχετε τις εξής επιλογές: Να χρησιµοποιήσετε το όνοµα του αρχείου myrafile = new RandomAccessFile(String name, String mode); Να χρησιµοποιήσετε ένα αντικείµενο File myrafile = new RandomAccessFile(File file, String mode); Το όρισµα mode καθορίζει αν έχετε πρόσβαση στο αρχείο µόνο για ανάγνωση ( r ) ή για ανάγνωση και εγγραφή ( rw ). Για παράδειγµα µπορείτε να ανοίξετε ένα αρχείο βάσης δεδοµένων για ενηµέρωση ως εξής: RandomAccessFile myrafile; myrafile = new RandomAccessFile( db/stock.dbf, rw ); 6.7.2. Πρόσβαση στην πληροφορία Τα αντικείµενα RandomAccessFile αναµένουν να διαβάσουν και να γράψουν πληροφορία µε τον ίδιο τρόπο όπως τα αντικείµενα εισόδου και εξόδου δεδοµένων. Έχετε πρόσβαση σε όλες τις πράξεις read() και write() που βρίσκονται στις κλάσεις DataInputStream και DataOutputStream. Η γλώσσα Java παρέχει αρκετές µεθόδους που σας βοηθούν να µετακινηθείτε µέσα σε ένα αρχείο: long getfilepointer() Επιστρέφει την τρέχουσα θέση του δείκτη αρχείου (file pointer). void seek(long pos) Θέτει το δείκτη αρχείου στην καθοριζόµενη απόλυτη θέση. Η θέση δίνεται ως µετατόπιση byte (byte-offset) από την αρχή του αρχείου. Η θέση 0 δηλώνει την αρχή του αρχείου. long length() Επιστρέφει το µήκος του αρχείου. Η θέση length() δηλώνει το τέλος του αρχείου. 126
6.7.3. Προσθήκη πληροφορίας Μπορείτε να χρησιµοποιήσετε αρχεία τυχαίας προσπέλασης για να προσθέσετε πληροφορία σε ένα αρχείο εξόδου µε µορφή append. myrafile = new RandomAccessFile( java.log, rw ); myrafile.seek(myrafile.length()); // Οι επόµενες εγγραφές θα γραφτούν στο τέλος του αρχείου 6.8. Serializable Στο JDK 1.1 έγινε η προσθήκη της διεπαφής java.io.serializable και οι κατάλληλες αλλαγές στη Java Virtual Machine για υποστήριξη της δυνατότητας αποθήκευσης ενός αντικειµένου Java σε ένα ρεύµα. Η αποθήκευση ενός αντικειµένου κάποιου τύπου σε µόνιµο αποθηκευτικό χώρο ονοµάζεται µονιµότητα (persistence). Λέµε ότι ένα αντικείµενο είναι µόνιµο όταν είναι δυνατό να αποθηκεύσουµε το αντικείµενο αυτό στο δίσκο, ή σε µία ταινία, ή να αποσταλεί σε άλλον υπολογιστή και να αποθηκευθεί εκεί είτε στη µνήµη είτε στο δίσκο. Η διεπαφή java.io.serializable δεν έχει µεθόδους και λειτουργεί µόνο ως «σηµάδι» ου δηλώνει ότι η κλάση που την υλοποιεί µπορεί να θεωρηθεί για σειριοποίηση (serialization). Τα αντικείµενα από κλάσεις που δεν υλοποιούν το Serializable δεν µπορούν να αποθηκεύσουν ή να ανακτήσουν την κατάστασή τους. 6.8.1. Γράφοι αντικειµένων Όταν σειριοποιούµε ένα αντικείµενο διατηρούνται µόνο τα δεδοµένα του αντικειµένου οι µέθοδοι και οι συναρτήσεις δηµιουργίας της κλάσης δεν αποτελούν µέρος του ρεύµατος σειριοποίησης. Όταν µία µεταβλητήδεδοµένων είναι αντικείµενο, τα µέλη δεδοµένων του αντικειµένου αυτού επίσης σειριοποιούνται. Το δέντρο, ή η δοµή των δεδοµένων ενός αντικειµενων, συµπεριλαµβανοµένων αυτών των υποαντικειµένων αποτελούν το γράφο του αντικειµένου. Ορισµένες κλάσεις αντικειµένων δεν είναι σειριοποιήσιµες γιατί η φύση των δεδοµένων που αναπαριστούν αλλάζει συνεχώς. Για παράδειγµα: τα ρεύµατα, όπως java.io.fileinputstream και java.io.fileoutputstream και το java.lang.thread. Αν ένα σειριοποιήσιµο αντικείµενο περιέχει µία αναφορά σε ένα µη σειριοποιήσιµο αντικείµενο αποτυγχάνει ολόκληρη η διαδικασία σειριοποίησης. Αν ο γράφος του αντικειµένου περιέχει µία αναφορά σε µη σειριοποιήσιµο αντικείµενο το αντικείµενο συνεχίζει να µπορεί να σειριοποιηθεί αν η αναφορά έχει σηµειωθεί µε τη δεσµευµένη λέξη transient. public class MyClass implements Serializable public transient Thread mythread; private String customerid; private int total; Σηµειώστε επίσης ότι οι τροποποιητές πρόσβασης στα πεδία (public, protected, private) δεν επηρεάζουν τα πεδία δεδοµένων που σειριοποιούνται και τα δεδοµένα γράφονται στο ρεύµα σε µορφή byte, µε τα Strings να αναπαρίστανται ως χαρακτήρες UTF. Με χρήση της δεσµευµένης λέξης transient τα δεδοµένα δεν σειριοποιούνται: 127
public class MyClass implements Serializable public transient Thread mythread; private transient String customerid; private int total; 6.9. ιαβάζοντας και γράφοντας σε ένα ρεύµα αντικειµένων 6.9.1. Εγγραφή Η εγγραφή και η ανάγνωση ενός αντικειµένου σε ένα ρεύµα αρχείου είναι απλή διαδικασία. Έστω το ακόλουθο τµήµα κώδικα που στέλνει ένα στιγµιότυπο του αντικειµένου java.util.date σε ένα αρχείο. Date d = new Date(); FileOutputStream f = new FileOutputStream( date.ser ); ObjectOutputStream s = new ObjectOutputStream(f); τry s.writeobject(d); s.close; catch (IOException e) e.printstacktrace(); 6.9.2. Ανάγνωση Η ανάγνωση ενός αντικειµένου είναι εξίσου απλή µε την εγγραφή µε ένα προβληµατικό σηµείο η µέθοδος readobject() επιστρέφει τα στοιχεία από το ρεύµα ως τύπου Object και πρέπει να γίνει µετατροπή (cast) στο κατάλληλο όνοµα κλάσης πριν να µπορούν να εκτελεστούν µέθοδοι στο αντικείµενο αυτό. Date d = new Date(); FileInputStream f = new FileInputStream( date.ser ); ObjectInputStream s = new ObjectInputStream(f); try d = (Date)s.readObject(); catch (IOException e) e.printstacktrace(); System.out.println( Date serialized at: + d); 128
6.10. Λυµένες Ασκήσεις 6.10.1. Γράψτε ένα απλό πρόγραµµα όπου θα του δίνεται το όνοµά σας και θα τυπώνεται και πάλι πίσω στην οθόνη. Λύση: import java.io.*; public class Buffer //onoma klasis public static void main (String [] args) throws IOException System.out.print("Enter your name: "); BufferedReader br=new BufferedReader(new ΙnputStreamReader(System.in)); System.out.println("You entered: "+ br.readline()); 6.10.2. Γράψτε ένα απλό πρόγραµµα όπου θα του δίνεται το όνοµά και το επίθετό σας και θα ρωτά τον χρήστη αν θέλει να δώσει και την ηλικία του. Σε περίπτωση που τη δίνει θα τυπώνεται όνοµα, επίθετο και ηλικία αλλιώς µόνο όνοµα και επίθετο. Λύση: import java.io.*; public class Buffer //onoma klasis public static void main (String [] args) throws IOException System.out.print("Enter your name: "); BufferedReader br=new BufferedReader(new InputStreamReader(System.in)); /* opws vlepete parapanw, gia na dhmiourghsoume ena antikeimeno ths klashs BufferedReader, o constructor ths zhtaei ena antikeimeno ths klashs InputStreamReader. Gia na dhmiourghsoume ena antikeimeno ths klashs InputStreamReader o constructor * ths zhtaei to keyboard input tou xrhsth dhl. To System.in Ean p.x. o xrhsths dwsei Alex tote h lexh Alex metatrepetai se bytes kai o constructor ths InputStreamReader to metatrepei se xarakthres Ara to antikeimeno new InputStreamReader(System.in) emperiexei xarakthres. To antikeimeno auto o constructor ths BufferedReader to metatrepei se line h' array. Gia na metatrapei ayto to line h' array se String xrhsimopoioume th methodo readline() ths BufferedReader*/ //Twra tha apothikeysoume to input tou xrhsth String onomapouedwse=br.readline(); 129
System.out.print("Enter your surname: "); String epwnymopouedwse=br.readline(); System.out.print("If you want to enter your age, enter yes. If you don't, enter no: "); String apanthshpouedwse=br.readline(); if(apanthshpouedwse.equals("yes"))//ean eipe yes System.out.print("Enter your age: "); String ilikiapouedwse=br.readline(); System.out.println("Your name is: "+onomapouedwse); System.out.println("Your surname is: "+epwnymopouedwse); System.out.println("Your age is: "+ilikiapouedwse); else if (apanthshpouedwse.equals("no"))//ean eipe no System.out.println("Your name is: "+onomapouedwse); System.out.println("Your surname is: "+epwnymopouedwse); else//den edwse oute yes oute no System.out.println("Bad input!"); //telos ths main //telos ths klashs 6.10.3. ηµιουργήστε µια κλάση που διαβάζει δεδοµένα ένα-προς-ένα από ένα αρχείο που το ονοµάζουµε 1.txt. Λύση: import java.io.*; public class showdatainputstream public static void main()throws IOException int rl=7,k; FileInputStream fis=new FileInputStream("1.txt"); DataInputStream dis=new DataInputStream(fis); byte a; a=dis.readbyte(); System.out.println(a); short b; b=dis.readshort(); System.out.println(b); int c; c=dis.readint(); System.out.println(c); long d; d=dis.readlong(); System.out.println(d); float e; e=dis.readfloat(); System.out.println(e); double f; f=dis.readdouble(); System.out.println(f); String s; s=dis.readutf(); System.out.println(s); s=dis.readutf(); System.out.println(s); dis.close(); fis.close(); 130
System.out.println("---"); 6.10.4. ηµιουργήστε µια κλάση που γράφει δεδοµένα σε ένα αρχείο που το ονοµάζουµε text.txt. Λύση: import java.io.*; public class FileOutputStreamClass public static void main() string filename="text.txt"; fileoutputstream fos=null; string str="12345abcdefghijklmnopqrstuxyz"; try fos=new FileOutputStream(fileName); for(int i=0; i<str.length(); i++) fos.write((int)str.charat(i)); fos.close(); catch(ioexception e) System.out.println(e.toString()); System.out.println("Το String \n"+str+"\n αποθηκεύτηκε στο "+filename); 6.10.5. Κατασκευάστε ένα πρόγραµµα που θα ρωτάει τον χρήστη να φτιάξει ένα κατάλογο των επαφών του. Οι επαφές αυτές θα αποθηκεύονται σε ένα αρχείο. Λύση: import java.io.*; public class Buffer //onoma klasis public static void main (String [] args) throws IOException boolean prostheseepafi=true; while(prostheseepafi) System.out.print("Enter name: "); 131
BufferedReader br=new BufferedReader(new InputStreamReader(System.in)); String onomapouedwse=br.readline(); System.out.print("Enter surname: "); String epwnymopouedwse=br.readline(); System.out.print("Enter mobile phone: "); String mobilepouedwse=br.readline(); long mobiletelephone=0; long statherotelephone=0; if(mobilepouedwse.length()==10) try mobiletelephone = Long.parseLong(mobilePouEdwse); //ean erthei edw tote simainei oti to String den htan //long catch(numberformatexception nfe) System.out.println("Not an Integer"); //telos if else// den einai 10 xarakthrwn System.out.println("To tilefwno pou dwsate den einai swsto!"); break;//vges apo th while loop System.out.print("If you want to enter stathero thl., enter yes.\nif you don't, enter no: "); String apanthshpouedwse=br.readline(); if(apanthshpouedwse.equals("yes"))//ean eipe yes System.out.print("Enter stathero thl.:"); String statheropouedwse=br.readline(); if(statheropouedwse.length()==10) //ean oi xarakthres einai 10 try mobiletelephone = Long.parseLong(mobilePouEdwse); catch(numberformatexception nfe) System.out.println("Not an Integer"); 132
//telos if else// den einai 10 xarakthrwn System.out.println("To tilefwno pou dwsate den einai swsto!"); break;//vges apo th while loop PrintWriter pw=new PrintWriter(new FileWriter("contacts.doc",true),false); pw.println(onomapouedwse+" "+epwnymopouedwse+" "+mobilepouedwse+" "+statheropouedwse); pw.close(); else if(apanthshpouedwse.equals("no"))//ean eipe no PrintWriter pw=new PrintWriter(new FileWriter("contacts.doc",true),false); pw.println(onomapouedwse+" "+epwnymopouedwse+" "+mobilepouedwse); pw.close(); else//den edwse oute yes oute no System.out.println("Bad input!"); break;//vges apo th while System.out.print("If you want to enter another contact enter yes.\nif you don't, enter no: "); String apanthshpouedwse2=br.readline(); if(apanthshpouedwse2.equals("yes")) continue; else break;//telos ths loop //telos while //telos main //telos klashs 6.11. Ασκήσεις 1. Γράψτε µία εφαρµογή Java που ανοίγει, διαβάζει και παρουσιάζει τα περιεχόµενα ενός αρχείου από το οποίο µπορείτε να διαβάσετε. 2. Βεβαιωθείτε ότι η εφαρµογή σας, σας δηλώνει γιατί δεν µπορούσε να παρουσιάσει ένα αρχείο, συµπεριλαµβάνοντας τις κατάλληλες εξαιρέσεις. 3. ηµιουργήστε µια κλάση για τον χειρισµό αρχείων χρησιµοποιώντας τριαδικούς τελεστές. 133
4. Να γραφεί ένα πρόγραµµα το οποίο θα ζητάει από το χρήστη να εισάγει ένα προϊόν, τον αριθµό αυτού, το κόστος αυτού και έπειτα θα υπολογίζει: το συνολικό κόστος έπειτα από έκπτωση 10%. Επίσης, ζητάει από το χρήστη να δώσει τον αριθµό των ατόµων που θα επιµεριστούν το κόστος και έπειτα µας δίνει το κόστος ανά άτοµο. 5. Να γραφεί ένα πρόγραµµα το οποίο θα δέχεται δύο παραµέτρους, που είναι τα ονόµατα από δύο αρχεία ή από δύο καταλόγους. Αν πρόκειται για αρχεία, θα αντιγράφει το πρώτο στο δεύτερο. Αν πρόκειται για καταλόγους, θα αντιγράφει όλα τα αρχεία του πρώτου στον δεύτερο, καθώς επίσης και όλους τους υποκαταλόγους (αν υπάρχουν). Το πρόγραµµα πρέπει να περιέχει όλους τους απαραίτητους ελέγχους, που απαιτούνται για τη σωστή αντιγραφή. 6. ηµιουργήστε µία εφαρµογή που εξοµοιώνει ένα µικρό πρόγραµµα βάσης δεδοµένων και αποθηκεύει και ανακτά εγγραφές που σχετίζονται µε προϊόντα. Χρησιµοποιήστε την κλάση randomaccessfile και ένα απλό αρχείο. 7. Οι εγγραφές στη βάση δεδοµένων αποτελούνται από ένα όνοµα τύπου String και έναν ακέραιο. 8. Το πρόγραµµά σας θα πρέπει να επιτρέπει στο χρήστη να παρουσιάζει µία εγγραφή, να ενηµερώνει την εγγραφή και να προσθέτει µία νέα εγγραφή. 134
7. Νήµατα (threads) 7.1. Νήµατα και χρήση νηµάτων στη Java 7.1.1. Τι είναι τα νήµατα; Μία απλή, αλλά χρήσιµη, άποψη ενός υπολογιστή είναι ότι έχει µία CPU, που πραγµατοποιεί κάποιους υπολογισµούς, κάποια ROM που περιέχει το πρόγραµµα που εκτελεί η CPU και κάποια RAM που περιέχει τα δεδοµένα µε βάση τα οποία λειτουργεί το πρόγραµµα. Σε αυτή την απλή άποψη υπάρχει µόνο µία εργασία που πραγµατοποιείται σε κάθε χρονική στιγµή. Μία πιο πλήρης άποψη των περισσοτέρων σύγχρονων υπολογιστικών συστηµάτων δίνει τη δυνατότητα να πραγµατοποιούνται περισσότερες από µία εργασίες την ίδια χρονική στιγµή ή τουλάχιστον να έχουµε αυτή την αίσθηση. Σε αυτή τη φάση της συζήτησης δε µας ενδιαφέρει πώς επιτυγχάνεται αυτό το γεγονός, αλλά µόνο να λάβουµε υπόψη µας τα παρελκόµενα από προγραµµατιστικής άποψης. Αν πραγµατοποιούνται περισσότερες από µία εργασίες, είναι αντίστοιχο µε το να έχουµε περισσοτέρους του ενός υπολογιστές. Για το κεφάλαιο αυτό θα θεωρήσουµε ότι ένα νήµα (thread), ή πλαίσιο εκτέλεσης ή συµφραζόµενα εκτέλεσης (execution context), είναι η οριοθέτηση µίας ιδεατής CPU µε το δικό της κώδικα προγράµµατος και τα δικά της δεδοµένα. Η κλάση java.lang.thread στις βασικές βιβλιοθήκες της Java, µας επιτρέπει να δηµιουργούµε και να ελέγχουµε τα νήµατά µας. Στη συνέχεια θα γράφουµε Thread όταν αναφερόµαστε στην κλάση java.lang.thread και νήµα όταν αναφερόµαστε στην ιδέα συµφραζοµένων εκτέλεσης. 7.1.2. Τρία µέρη ενός νήµατος Ένα νήµα αποτελείται από τρία µέρη. Πρώτα υπάρχει η ίδια η ιδεατή CPU. εύτερον υπάρχει ο κώδικας που εκτελεί η CPU. Τρίτον υπάρχουν τα δεδοµένα στα οποία εφαρµόζεται ο κώδικας. 135
Στη Java στην κλάση Thread εµπεριέχεται µία ιδεατή CPU. Όταν δηµιουργείται ένα νήµα µεταβιβάζεται σε αυτό, µέσω των ορισµάτων της συνάρτησης δηµιουργίας του, τα δεδοµένα στα οποία θα πρέπει να λειτουργήσει. Είναι σηµαντικό να τονίσουµε ότι αυτές οι τρεις απόψεις είναι ουσιαστικά ανεξάρτητες. Ένα νήµα µπορεί να εκτελεί τον ίδιο ή διαφορετικό κώδικα από ένα άλλο νήµα. Ένα νήµα µπορεί να έχει πρόσβαση στα ίδια ή σε διαφορετικά δεδοµένα από ό,τι ένα άλλο νήµα. 7.1.3. ηµιουργία ενός νήµατος Ας δούµε τον τρόπο µε τον οποίο δηµιουργείται ένα νήµα και να συζητήσουµε πώς χρησιµοποιούνται τα ορίσµατα των συναρτήσεων δηµιουργίας του για την παροχή του κώδικα και των δεδοµένων για το νήµα κατά την εκτέλεσή του. Μία συνάρτηση δηµιουργίας Thread δέχεται ένα όρισµα που είναι στιγµιότυπο ενός Runnable. ηλαδή πρέπει να ορίσουµε µία κλάση η οποία θα υλοποιεί τη διεπαφή Runnable και στη συνέχεια να δηµιουργήσουµε ένα στιγµιότυπο αυτής της κλάσης. Η αναφορά που προκύπτει είναι κατάλληλο όρισµα για τη συνάρτηση δηµιουργίας που θέλουµε. Για παράδειγµα: public class Xyz implements Runnable int i; public void run() while (true) System.out.println( Hello + i++); Μας επιτρέπει να δηµιουργήσουµε ένα νήµα ως εξής: Runnable r = new Xyz(); Thread t = new Thread(r); Με δεδοµένο αυτό έχουµε ένα νέο νήµα το οποίο εµπεριέχεται στην αναφορά του Thread t. Αυτό ετοιµάζεται για να εκτελέσει τον κώδικα που ξεκινά µε τη µέθοδο run() της κλάσης Xyz. (Η διεπαφή Runnable απαιτεί την ύπαρξη µίας public void µεθόδου µε το όνοµα run().) Τα δεδοµένα που χρησιµοποιεί το νήµα αυτό παρέχονται από το στιγµιότυπο της Xyz στο οποίο αναφερόµαστε ως r. 136
Το νέο νήµα Συνεπώς, εν περιλήψει, αναφερόµαστε σε ένα νήµα µέσω ενός στιγµιοτύπου του αντικειµένου Thread. Ο κώδικας που θα εκτελέσει αυτό το νήµα θα ληφθεί από την κλάση του αντικειµένου που µεταβιβάζεται στη συνάρτηση δηµιουργίας του Thread. Η κλάση αυτή πρέπει να υλοποιεί τη διεπαφή Runnable. Τα δεδοµένα στα οποία λειτουργεί το νήµα αυτό λαµβάνονται από το συγκεκριµένο στιγµιότυπο του Runnable που µεταβιβάζεται στη συνάρτηση δηµιουργίας του Thread. 7.1.4. Εκκίνηση ενός Thread Αν και έχουµε δηµιουργήσει ένα νήµα αυτό δεν ξεκινά να εκτελείται άµεσα. Για να το ξεκινήσουµε χρησιµοποιούµε τη µέθοδο start(). Η µέθοδος αυτή είναι στην κλάση Thread συνεπώς, µε βάση τα πιο πάνω, λέµε απλά: t.start() Στο σηµείο αυτό η ιδεατή CPU που βρίσκεται µέσα στο νήµα γίνεται εκτελέσιµη. Μπορείτε να το φανταστείτε σαν να δίνετε ρεύµα στην ιδεατή CPU. 7.1.5. Χρονοπρογραµµατισµός του νήµατος Αν και το νήµα γίνεται εκτελέσιµο, αυτό δε σηµαίνει απαραίτητα ότι ξεκινά άµεσα. Σε έναν υπολογιστή µε µία µόνο CPU (πραγµατική CPU), σαφώς µπορούµε να κάνουµε µόνο ένα πράγµα κάθε φορά. Ας δούµε τώρα τον τρόπο µε τον οποίο κατανέµεται η CPU όταν θα µπορούσαν να εκτελούνται περισσότερα από ένα νήµατα. Στη Java τα νήµατα είναι προ-εκχωρητικά (pre-emptive), αλλά δεν εκτελούνται κατ ανάγκη µε βάση χρονοµερίδια (time-sliced). (Είναι ένα κοινό λάθος να πιστεύουµε ότι η προ-εκχώρηση ταυτίζεται µε το χρονοπρογραµµατισµό µε βάση τα χρονοµερίδια.) Το µοντέλο του προ-εκχωρητικού χρονοπρογραµµατιστή είναι ότι πολλά νήµατα µπορεί να είναι έτοιµα προς εκτέλεση, αλλά µόνο ένα εκτελείται στην πράξη. Η διεργασία αυτή θα συνεχίσει να εκτελείται µέχρις ότου είτε πάψει να είναι εκτελέσιµη είτε µία άλλη διεργασία υψηλότερης προτεραιότητας γίνει εκτελέσιµη. Στην περίπτωση αυτή λέµε ότι το νήµα χαµηλότερης προτεραιότητας προ-εκχωρείται από το νήµα υψηλότερης προτεραιότητας. Ένα νήµα µπορεί να πάψει να είναι διαθέσιµο για πλήθος λόγων. Μπορεί να έχει εκτελέσει µία κλήση Thread.sleep(), επί τούτου για να ζητήσει να παύσει για µία 137
χρονική περίοδο. Μπορεί να χρειάζεται να περιµένει για κάποια αργή εξωτερική συσκευή, ίσως κάποιον δίσκο ή το χρήστη. Όλα τα νήµατα που είναι εκτελέσιµα αλλά δεν εκτελούνται διατηρούνται σε ουρές ανάλογα µε την προτεραιότητά τους. Το πρώτο νήµα στη µη-κενή ουρά µε την υψηλότερη προτεραιότητα θα εκτελεστεί. Όταν ένα νήµα πάψει να εκτελείται εξ αιτίας της προ-εκχώρησης, φεύγει από την εκτελούµενη κατάσταση και τοποθετείται στο τέλος της εκτελέσιµης ουράς. Με παρόµοιο τρόπο ένα νήµα που γίνεται εκτελέσιµο αφού έχει απενεργοποιηθεί (blocked) (απλή απενεργοποίηση ή πιθανή αναµονή για είσοδο/έξοδο) πάει πάντα στο τέλος της ουράς. εδοµένου του ότι τα νήµατα στη Java δεν χρησιµοποιούν κατ ανάγκη χρονοµερίδια, και µε την έλλειψη περισσότερο προηγµένης κατανόησης, και της κατάλληλης σχεδίασης του προγράµµατος, πρέπει να είστε βέβαιοι ότι ο κώδικας για τα νήµατά σας δίνει στα άλλα νήµατα την ευκαιρία να εκτελεσθούν από καιρού εις καιρόν. Αυτό µπορεί να επιτευχθεί δίνοντας περιοδικά µία κλήση sleep(), όπως στον κεντρικό βρόχο. public class Xyz implements Runnable public void run() while (true) // διάφορα ενδιαφέροντα πράγµατα... // Ας δουλέψουν και οι άλλοι try Thread.sleep(10); catch (InterruptedException e) // το νήµα διακόπηκε από άλλο Παρατηρήστε τη χρήση των try και catch. Επίσης σηµειώστε ότι η κλήση sleep() είναι µία static µέθοδος της κλάσης Thread και συνεπώς αναφερόµαστε σε αυτή ως Thread.sleep(x). Το όρισµα καθορίζει το ελάχιστο αριθµό milliseconds κατά τα οποία το νήµα θα παραµείνει ανενεργό. Η εκτέλεση του νήµατος δε θα συνεχισθεί παρά µόνο όταν περάσει αυτή η χρονική περίοδος συν κάποια χρόνο στον οποίο τα υπόλοιπα νήµατα θα απενεργοποιηθούν, επιτρέποντας στο νήµα να ξεκινήσει στην πράξη. Μία άλλη µέθοδος στην κλάση Thread, η yield() µπορεί να χρησιµοποιηθεί για να δώσει στα άλλα νήµατα την ευκαιρία να εκτελεστούν χωρίς να σταµατήσει στην πράξη το τρέχον νήµα, παρά µόνο αν αυτό είναι απαραίτητο. Αν είναι εκτελέσιµα άλλα νήµατα της ίδιας προτεραιότητας η yield() τοποθετεί το νήµα που την κάλεσε στο τέλος της εκτελέσιµης ουράς και επιτρέπει να ξεκινήσει ένα άλλο νήµα. Αν δεν 138
υπάρχουν εκτελέσιµα νήµατα στην ίδια προτεραιότητα η yield() δεν κάνει τίποτα. Σηµειώστε ότι µία κλήση στη sleep() µπορεί να επιτρέψει την εκτέλεση νηµάτων σε χαµηλότερη προτεραιότητα. Η µέθοδος yield() δίνει την ευκαιρία για εκτέλεση µόνο στα νήµατα ίδιας προτεραιότητας. 7.2. Βασικός έλεγχος νηµάτων 7.2.1. Τερµατισµός ενός νήµατος Deprecated Όταν επιστρέφει ένα νήµα από το τέλος µίας µεθόδου run(), αυτό πεθαίνει. Μετά από αυτό δεν µπορεί να ξανά-εκτελεστεί. Ένα νήµα µπορεί να σταµατήσει µε τη βία δίνοντας την µέθοδο stop(). Αυτή πρέπει να χρησιµοποιηθεί σε συγκεκριµένο στιγµιότυπο της κλάσης Thread για παράδειγµα: public class Xyz implements Runnable // ιάφορα πράγµατα που πρέπει να γίνονται σε ένα νήµα public class ttest public static void main (String args[]) Runnable r = new Xyz(); Thread t = new Thread(r); t.start(); // άλλα πράγµατα if (time_to_kill) t.stop(); Μέσα ένα συγκεκριµένο τµήµα κώδικα είναι δυνατό να αποκτήσουµε αναφορά στο τρέχον νήµα χρησιµοποιώντας τη στατική µέθοδο της Thread currentthread(), για παράδειγµα: public class Xyz implements Runnable public void run() while (true) // διάφορα ενδιαφέροντα if (time_to_die) Thread.currentThread().stop(); 139
Σηµειώστε ότι στην περίπτωση αυτή η εκτέλεση της stop() θα καταστρέψει το τρέχον πλαίσιο εκτέλεσης και συνεπώς δε θα εκτελεστεί η συνέχεια του βρόχου run() σε αυτό το πλαίσιο. Η χρήση της Thread.currentThread() δεν περιορίζεται στο παράδειγµα της stop()! 7.2.2. Έλεγχος ενός νήµατος Ορισµένες φορές είναι δυνατό ένα νήµα να είναι σε άγνωστη κατάσταση (αυτό µπορεί να συµβεί αν ο κώδικάς σας δεν ελέγχει απ ευθείας ένα συγκεκριµένο νήµα). Είναι δυνατό να ρωτήσουµε αν ένα νήµα είναι ακόµα «ζωντανό» χρησιµοποιώντας τη µέθοδο isalive(). Το να είναι ένα νήµα «ζωντανό» δε σηµαίνει απαραίτητα ότι εκτελείται, µόνο ότι έχει ξεκινήσει και δεν έχει κληθεί για αυτό ποτέ η µέθοδος stop() ούτε έχει φτάσει στο τέλος της µεθόδου run(). 7.2.3. Βάζοντας τα νήµατα σε αναµονή Υπάρχουν αρκετοί µηχανισµοί για να σταµατήσουµε προσωρινά την εκτέλεση ενός νήµατος. Ακολουθώντας αυτόν τον τύπο απενεργοποίησης η εκτέλεση µπορεί να ξαναξεκινήσει σαν να µην είχε συµβεί τίποτα, απλά το νήµα φαίνεται να είχε εκτελέσει µία εντολή πολύ αργά. sleep() Η µέθοδος sleep() παρουσιάστηκε νωρίτερα και χρησιµοποιείται για να σταµατήσουµε ένα νήµα για µία χρονική περίοδο. Θυµηθείτε ότι συνήθως το νήµα δε θα ξαναξεκινήσει αµέσως µόλις τελειώσει η περίοδος απενεργοποίησης. Αυτό ισχύει γιατί µπορεί ένα άλλο νήµα να εκτελείται τη χρονική αυτή στιγµή και να µην µπορεί να φύγει από το χρονοπρογραµµατισµό εκτός και αν (α) το νήµα που αφυπνίζεται να είναι υψηλότερης προτεραιότητας, (β) το εκτελούµενο νήµα να απενεργοποιηθεί για κάποιον λόγο ή (γ) να είναι ενεργοποιηµένος ο χρονοπρογραµµατισµός µε βάση χρονοµερίδια. Στις περισσότερες περιπτώσεις καµία από τις δύο τελευταίες περιπτώσεις δε θα συµβεί άµεσα. suspend() και resume() Deprecated Ορισµένες φορές είναι κατάλληλο να απενεργοποιήσετε την εκτέλεση ενός νήµατος επ αόριστον, οπότε κάποιο άλλο νήµα θα πρέπει να είναι υπεύθυνο για να ξεκινήσει ξανά την εκτέλεση. Για το λόγο αυτό χρειαζόµαστε ένα ζεύγος µεθόδων Thread. Ονοµάζονται suspend() και resume(). Για παράδειγµα: public class Xyz implements Runnable public void run() // διάφορα ενδιαφέροντα // τώρα περίµενε µέχρι να σου πουν να συνεχίσεις Thread.currentThread().suspend(); // κάνε άλλα πράγµατα... 140
... runnable r = new Xyz(); thread t = new Thread(r); t.start(); // παύση για λίγο για να εκτελεστεί το Xyz // υπέθεσε ότι έχει φτάσει στο suspend() thread.sleep(1000); // ξανακάνει το Xyz εκτελέσιµο t.resume(); // άφησέ το να εκτελεσθεί thread.yields(); Σηµειώστε ότι εν γένει ένα Thread µπορεί να απενεργοποιηθεί από οποιοδήποτε τµήµα κώδικα που µπορεί να το χειριστεί δηλαδή έχει µία µεταβλητή που αναφέρεται σε αυτό. Σαφώς όµως ένα Thread µπορεί να ξανά-ξεκινήσει µόνο από ένα Thread άλλο από το ίδιο, καθώς το απενεργοποιηµένο Thread δεν εκτελεί κανένα κώδικα! join() Η µέθοδος join() προκαλεί το τρέχον νήµα να περιµένει µέχρι να τερµατίσει το νήµα στο οποίο κλήθηκε η µέθοδος join. Για παράδειγµα: TimerThread tt = new TimerThread(100); tt.start();... public void timeout() // Περίµενε να τελειώσει το TimerThread tt.join(); // Τώρα συνέχισε σε αυτό το νήµα... Η join µπορεί να κληθεί επίσης µε µία τιµή timeout σε milliseconds: void join(long timeout); όπου η µέθοδος join είτε θα απενεργοποιήσει το τρέχον νήµα για τα timeout milliseconds, είτε θα περιµένει µέχρι να τερµατιστεί το νήµα στο οποίο κλήθηκε. 7.3. Άλλοι τρόποι δηµιουργίας νηµάτων Περιγράψαµε ήδη τη δηµιουργία των νηµάτων µε χρήση µίας ξεχωριστής κλάσης η οποία υλοποιεί το Runnable. Στην πράξη δεν είναι η µόνη πιθανή προσέγγιση. Ο ορισµός της κλάσης Thread δηλώνει ότι στην πράξη υλοποιεί τη διεπαφή Runnable. Είναι δυνατό να δηµιουργήσετε ένα Thread δηµιουργώντας µία κλάση η οποία κάνει extends την κλάση Thread, αντί να υλοποιήσετε τη Runnable. public class MyThread extends Thread public void run() 141
while (running) // do lots of interesting stuff sleep(100); public static void main(string args[]) Thread t = new MyThread(); t.start(); Εδώ υπάρχει µόνο µία κλάση η MyThread. Όταν δηµιουργείται ένα αντικείµενο Thread δεν παρέχονται ορίσµατα. Αυτή η µορφή συνάρτησης δηµιουργίας δηµιουργεί ένα Thread που χρησιµοποιεί τη δική του ενσωµατωµένη µέθοδο run(). 7.3.1. Ποιον τρόπο να χρησιµοποιήσουµε; εδοµένης της επιλογής ανάµεσα στις δύο προσεγγίσεις πώς θα επιλέξετε ανάµεσά τους; Υπάρχουν σηµεία υπέρ κάθε µίας προσέγγισης: Υπέρ της υλοποίησης της Runnable Από απόψεως αντικειµενοστρεφούς σχεδίασης η κλάση Thread είναι αυστηρά µία οριοθέτηση µίας ιδεατής CPU και ως τέτοια θα πρέπει να επεκτείνεται µόνο όταν η συµπεριφορά του µοντέλου της CPU αλλάζει ή επεκτείνεται µε κάποιον τρόπο. Συνήθως δεν είναι αυτό εκείνο που θέλουµε να κάνουµε. Εξ αιτίας αυτού, και για να επιτύχουµε τη διάκριση ανάµεσα σε CPU, Κώδικα και εδοµένα, στο παρόν κεφάλαιο επιλέξαµε την πρώτη προσέγγιση. Εφόσον η Java επιτρέπει µόνο απλή κληρονοµικότητα δεν είναι δυνατό να επεκτείνουµε οποιαδήποτε άλλη κλάση, όπως για παράδειγµα την Applet, αν έχουµε ήδη επεκτείνει τη Thread. Σε ορισµένες περιπτώσεις, ο περιορισµός αυτός θα σας αναγκάσει να επιλέξετε την προσέγγιση υλοποίησης της Runnable. Εφόσον ορισµένες φορές αναγκάζεστε να χρησιµοποιήσετε την υλοποίηση της Runnable µπορεί να θέλετε να είστε συνεπής και να το κάνετε πάντα έτσι. Υπέρ της επέκτασης της Thread Όταν η µέθοδος run έχει ενσωµατωθεί σε µία κλάση που κάνει extends την κλάση Thread, τότε η αναφορά this στην πράξη αναφέρεται στο πραγµατικό στιγµιότυπο της Thread και όχι στον έλεγχο της εκτέλεσης. Εξ αιτίας αυτού δε χρειάζεται να γράφετε πλέον µεγάλες εντολές ελέγχου όπως: Thread.currentThread().start() αλλά αντ αυτής µπορείτε να δώσετε απλά: start(); 142
Επειδή ο κώδικας που προκύπτει είναι λίγο απλούστερος, πολλοί προγραµµατιστές Java χρησιµοποιούν το µηχανισµό της επέκτασης της Thread. Προσέξτε όµως ότι ο περιορισµός της απλής κληρονοµικότητας στη συνέχεια µπορεί να οδηγήσει στη συνέχεια του κύκλου ζωής του λογισµικού σας σε δυσκολίες. 7.4. Χρήση της synchronized στη Java 7.4.1. Εισαγωγή Στο τµήµα αυτό θα συζητήσουµε τη χρήση της δεσµευµένης λέξης synchronized. Αυτή παρέχει στη Java ένα µηχανισµό για να επιτρέπει στους προγραµµατιστές να ελέγχουν τα νήµατα τα οποία διαµοιράζονται δεδοµένα. 7.4.2. Το πρόβληµα Έστω µία κλάση που αναπαριστά µία στοίβα. Μία πρώτη έκδοσή της µπορεί να είναι η ακόλουθη: class stack int idx = 0; char [] data = new char[6]; public void push (char c) data[idx] = c; idx++; public char pop() idx--; return data[idx]; Σηµειώστε ότι η κλάση δεν κάνει καµία προσπάθεια να αντιµετωπίσει υπερχείλιση ή υποχείλιση και ότι η χωρητικότητά της είναι περιορισµένη. Τα ζητήµατα αυτά δε θα µας απασχολήσουν στη συζήτησή µας. Παρατηρήστε ότι η συµπεριφορά του µοντέλου αυτού απαιτεί η τιµή του δείκτη να µας δίνει το δείκτη του πίνακα για το επόµενο άδειο κελί στη στοίβα και για το λόγο αυτό υιοθετεί την προσέγγιση «πρώτα µείωση, µετά αύξηση» (predecrement, postincrement). Φανταστείτε τώρα ότι δύο νήµατα έχουν αµφότερα µία αναφορά στο ίδιο στιγµιότυπο της κλάσης. Το ένα νήµα τοποθετεί δεδοµένα (push) στη στοίβα και το άλλο, λίγο-πολύ ανεξάρτητα, βγάζει (pop) δεδοµένα από τη στοίβα. Στη γενική περίπτωση θα φαίνεται ότι τα δεδοµένα θα τοποθετούνται και θα αφαιρούνται επιτυχώς. Υπάρχει όµως ένα εν δυνάµει πρόβληµα. Έστω ότι το νήµα «a» προσθέτει και το νήµα «b» αφαιρεί χαρακτήρες. Το νήµα «a» µόλις έχει αφήσει ένα χαρακτήρα, αλλά δεν έχει αυξήσει ακόµα το µετρητή του δείκτη. Για κάποιο λόγο το νήµα προ-εκχωρείται. Στο σηµείο αυτό, το µοντέλο δεδοµένων που αναπαρίσταται από το αντικείµενό µας είναι ασυνεπές. buffer = p q r 143
idx = 2 ^ Συγκεκριµένα η συνέπεια απαιτεί είτε idx = 3 είτε να µην είχε προστεθεί ο χαρακτήρας. Το νήµα «a» συνεχίζει την εκτέλεσή του και µπορεί να µην υπήρξε πρόβληµα, αλλά έστω ότι και το νήµα «b» περίµενε για να αφαιρέσει ένα χαρακτήρα. Όσο το «a» περίµενε τη σειρά του να εκτελεσθεί, το βήµα «b» έχει την ευκαιρία του να εκτελεσθεί. Μπαίνοντας στη µέθοδο pop() έχουµε µία κατάσταση ασυνεπών δεδοµένων. Η µέθοδος pop συνεχίζει και µειώνει την τιµή του δείκτη: buffer = p q r idx = 1 ^ Με αυτό ουσιαστικά αγνοεί το χαρακτήρα r. Μετά από αυτό επιστρέφει το χαρακτήρα q. Μέχρι τώρα η συµπεριφορά είναι σα να µην τοποθετήθηκε ποτέ ο χαρακτήρας r, συνεπώς δεν µπορούµε να πούµε ότι υπάρχει κάποιο πρόβληµα. Αλλά ας δούµε τι θα συµβεί όταν επιστρέψει το αρχικό νήµα «a». Το νήµα «a» συνεχίζει από εκεί που έµεινε µέσα στη µέθοδο push(). Συνεχίζει και αυξάνει κατά ένα την τιµή του δείκτη. Τώρα έχουµε buffer = p q r idx = 2 ^ Παρατηρήστε ότι αυτή η διαµόρφωση υπονοεί ότι το q είναι επιτρεπτό και ότι το επόµενο κενό κελί είναι το κελί που περιέχει το r. Με άλλα λόγια, θα διαβάσουµε το q σα να είχε τοποθετηθεί δύο φορές στη στοίβα, αλλά δε θα δούµε ποτέ το r. Αυτό είναι ένα απλό παράδειγµα ενός γενικότερου προβλήµατος που µπορεί να προκύψει όταν πολλά νήµατα προσπελαύνουν διαµοιραζόµενα δεδοµένα. Χρειαζόµαστε ένα µηχανισµό για να βεβαιωθούµε ότι τα δεδοµένα µας προστατεύονται από τέτοιου είδους µη ελεγχόµενες προσπελάσεις. Μία προσέγγιση είναι να αποτρέψουµε το νήµα «a» από το να προ-εκχωρηθεί µέχρι να τελειώσει µε το κρίσιµο τµήµα του κώδικα. Η προσέγγιση αυτή είναι κοινή στον προγραµµατισµό υπολογιστών σε χαµηλό επίπεδο, αλλά εν γένει είναι µη κατάλληλη σε συστήµατα µε πολλούς χρήστες. Μία άλλη προσέγγιση, αυτή µε την οποία λειτουργεί η Java, είναι να παρέχουµε ένα µηχανισµό µε βάση τον οποίο τα δεδοµένα θα αντιµετωπίζονται ως ευαίσθητα. 7.4.3. Η ένδειξη κλειδώµατος Object Στη Java κάθε στιγµιότυπο οποιουδήποτε αντικειµένου έχει συσχετισµένο µε αυτό µία ένδειξη (flag). Η ένδειξη αυτή µπορεί να θεωρηθεί ως µία «ένδειξη κλειδώµατος». Για αλληλεπίδραση µε την ένδειξη αυτή παρέχεται η δεσµευµένη λέξη synchronized. είτε το πιο κάτω τροποποιηµένο τµήµα κώδικα: public void push (char c) synchronized(this) data[idx] = c; idx++; 144
Όταν το νήµα φτάσει στην εντολή synchronized εξετάζει το αντικείµενο που µεταβιβάζεται ως όρισµα και προσπαθεί να «πάρει» την ένδειξη κλειδώµατός του. Είναι σηµαντικό να κατανοήσουµε ότι από µόνη της η λύση αυτή δεν προστατεύει τα δεδοµένα στο αντικείµενο this. Αν κληθεί από άλλο νήµα η µέθοδος pop() χωρίς να τροποποιηθεί συνεχίζει να υπάρχει κίνδυνος να καταστραφεί η συνέπεια του αντικειµένου this. Αυτό που πρέπει να κάνουµε είναι να τροποποιήσουµε και την pop µε τον ίδιο τρόπο. Προσθέτουµε µία κλήση στη synchronized(this) γύρω από τα ευαίσθητα τµήµατα της κλήσης της pop() ακριβώς παράλληλα µε τον τρόπο που το κάναµε για τη µέθοδο push(). Το επόµενο σχήµα δείχνει τι θα συµβεί αν ένα άλλο νήµα προσπαθήσει να εκτελέσει τη µέθοδο ενώ το αρχικό νήµα µας διατηρεί την ένδειξη κλειδώµατος. 145
Όταν το νήµα προσπαθήσει να εκτελέσει την εντολή synchronized(this) προσπαθεί να πάρει την ένδειξη κλειδώµατος του αντικειµένου this. Εφόσον η ένδειξη δεν είναι παρούσα, το νήµα δεν µπορεί να συνεχίσει την εκτέλεσή του. Στην πράξη πηγαίνει σε µία ουρά µε νήµατα που αναµένουν. Η ουρά αυτή σχετίζεται µε την ένδειξη κλειδώµατος του αντικειµένου έτσι ώστε όταν η ένδειξη επιστραφεί στο αντικείµενο, το πρώτο νήµα που την ανέµενε να την λάβει και να συνεχίσει να εκτελείται. 7.4.4. Ελευθέρωση της ένδειξης κλειδώµατος Καθώς το νήµα που αναµένει την ένδειξη κλειδώµατος ενός αντικειµένου δε θα συνεχίσει να εκτελείται παρά µόνο όταν αυτή επιστραφεί από το νήµα που την κρατά, είναι σαφώς σηµαντικό να επιστραφεί η ένδειξη όταν αυτή δε χρειάζεται πλέον. Η ένδειξη κλειδώµατος δίνεται στην πράξη πίσω στο αντικείµενο αυτόµατα όταν το νήµα που την κρατά περάσει το τέλος του µπλοκ που σχετίζεται µε την κλήση synchronized() µέσω της οποία έλαβε την ένδειξη. Η Java δίνει µεγάλη προσοχή ώστε η ένδειξη να επιστρέφει πάντα σωστά, συνεπώς αν ένα συγχρονισµένο µπλοκ παράγει µία εξαίρεση ή αν υπάρχει ένα break σε βρόχο που θα βγει έξω από το µπλοκ, η ένδειξη θα επιστραφεί σωστά. Επίσης, αν ένα νήµα κάνει δύο φορές κλήση synchronized στο ίδιο αντικείµενο, η ένδειξη θα απελευθερωθεί σωστά στην έξοδο από το εξώτερο µπλοκ ενώ το ενδότερο ουσιαστικά αγνοείται. Οι κανόνες αυτοί κάνουν τα συγχρονισµένα µπλοκ πιο απλά στο χειρισµό από ισοδύναµες διευκολύνσεις σε άλλα συστήµατα όπως οι δυαδικοί σηµαφόροι. 7.4.5. Βάζοντάς τα όλα µαζί Όπως αναφέραµε και νωρίτερα ο µηχανισµός synchronized() λειτουργεί µόνο αν ο προγραµµατιστής τοποθετήσει τις κλήσεις στις σωστές θέσεις. Τώρα θα δούµε πώς να δηµιουργήσουµε µία καλά προστατευόµενη κλάση. Θεωρήστε την προσβασιµότητα των στοιχείων δεδοµένων που αποτελούν τα ευαίσθητα µέρη του αντικειµένου. Αν αυτά δεν έχουν δηλωθεί ως ιδιωτικά, τότε µπορεί να τα προσπελάσει οποιοσδήποτε εξωτερικός κώδικας. Στην περίπτωση αυτή, βασιζόµαστε ότι ποτέ κανένας προγραµµατιστής δε θα παραλείψει τις προστασίες που απαιτούνται. Σαφώς αυτή δεν είναι µία ιδιαίτερα ασφαλής στρατηγική. Για το λόγο αυτό τα δεδοµένα θα πρέπει να δηλώνονται πάντα ως ιδιωτικά. 146
Εφόσον έχουµε δείξει ότι τα δεδοµένα πρέπει, στην πράξη, να είναι σηµειωµένα ως ιδιωτικά, το όρισµα στην κλήση synchronized() θα πρέπει να είναι το this. Εξ αιτίας αυτής της γενίκευσης η Java επιτρέπει µία συντοµογραφία ώστε αντί να γράφουµε public void push (char c) synchronized(this) : : να µπορούµε να γράψουµε: public synchronized void push (char c) : : µε το ίδιο αποτέλεσµα. Γιατί θα θέλαµε να επιλέξουµε τη µία αντί της άλλης; Αν χρησιµοποιούµε το synchronized ως τροποποιητή µεθόδου τότε ολόκληρη η µέθοδος γίνεται ένα συγχρονισµένο µπλοκ, πράγµα που µπορεί να έχει ως αποτέλεσµα ότι η ένδειξη κλειδώµατος θα κρατηθεί περισσότερο απ όσο αυστηρά χρειάζεται. Αυτό µπορεί να είναι µη αποδοτικό. Από την άλλη, σηµειώνοντας έτσι τη µέθοδο επιτρέπουµε στους χρήστες της να γνωρίζουν ότι µέσα σε αυτή υπάρχει συγχρονισµός, κάτι που µπορεί να είναι σηµαντικό όταν σχεδιάζουµε ενάντια σε ένα αδιέξοδο, όπως θα δούµε στη συνέχεια. Σηµειώστε ότι η γεννήτρια τεκµηρίωσης javadoc θα προωθήσει την ύπαρξη του τροποποιητή synchronized στα αρχεία τεκµηρίωσης, αλλά δε θα τεκµηριώσει τη χρήση του synchronized(this). 7.4.6. Αδιέξοδο Σε προγράµµατα στα οποία πολλά νήµατα ανταγωνίζονται για πρόσβαση σε πολλούς πόρους υπάρχει η πιθανότητα µίας συνθήκης η οποία είναι γνωστή ως αδιέξοδο (deadlock). Αυτό συµβαίνει όταν ένα νήµα αναµένει για µία ένδειξη κλειδώµατος που κατέχει ένα άλλο νήµα, αλλά το άλλο νήµα αναµένει για µία ένδειξη κλειδώµατος που κατέχει ήδη το πρώτο νήµα. Στην περίπτωση αυτή κανένα από τα δύο δεν µπορεί να συνεχίσει µέχρι το άλλο να έχει περάσει το τέρµα του συγχρονισµένου µπλοκ. Και καθώς κανένα από τα δύο δεν µπορεί να προχωρήσει, κανένα από τα δύο δε θα φτάσει στο τέρµα του µπλοκ. Η Java ούτε ανιχνεύει ούτε προσπαθεί να αποφύγει αυτή τη συνθήκη, είναι συνεπώς ευθύνη του προγραµµατιστή να βεβαιωθεί ότι δεν πρόκειταινα συµβεί. Ένας πρακτικός κανόνας για αποφυγή αδιεξόδου είναι ο ακόλουθος. Αν υπάρχουν πολλά αντικείµενα στα οποία θέλετε να αποκτήσετε συγχρονισµένη πρόσβαση, πάρτε µία συνολική απόφαση σχετικά µε τη σειρά µε την οποία θα 147
αποκτάτε τις ενδείξεις κλειδώµατος και ακολουθήστε την σε ολόκληρο το πρόγραµµα. Μία πιο λεπτοµερής συζήτηση του προβλήµατος αυτού είναι πέρα από τους σκοπούς του µαθήµατος. 7.5. Αλληλεπίδραση νηµάτων wait() και notify() 7.5.1. Εισαγωγή Συχνά, δηµιουργούνται διαφορετικά νήµατα ειδικά για να πραγµατοποιήσουν εργασίες που δε σχετίζονται µεταξύ τους. Ορισµένες φορές όµως οι εργασίες που πραγµατοποιούν τελικά σχετίζονται µε κάποιον τρόπο. Όταν συµβαίνει αυτό είναι απαραίτητο να προγραµµατίσουµε κάποια αλληλεπίδραση ανάµεσα στα νήµατα. Υπάρχουν πρότυποι τρόποι για να το κάνουµε αυτό και δεδοµένου ενός µηχανισµού οι υπόλοιποι µπορούν να υλοποιηθούν χρησιµοποιώντας τον. Το τµήµα αυτό εξετάζει το µηχανισµό που παρέχει η Java και περιγράφει τη χρήση του. 7.5.2. Το πρόβληµα Γιατί λοιπόν να θέλουν δύο νήµατα να αλληλεπιδράσουν; Ως απλό παράδειγµα θεωρήστε δύο ανθρώπους, ο ένας πλένει και ο άλλος σκουπίζει πιάτα. Αυτοί οι άνθρωποι αναπαριστούν τα νήµατά µας. Ανάµεσά τους βρίσκεται ένα διαµοιραζόµενο αντικείµενο, η στεγνώστρα. Και οι δύο άνθρωποι είναι λίγο τεµπέληδες και θα προτιµούσαν να κάθονται αν δεν έχουν να κάνουν κάτι. Σαφώς αυτός που σκουπίζει δεν µπορεί να ξεκινήσει να σκουπίσει αν δεν υπάρχει τουλάχιστον ένα αντικείµενο στη στεγνώστρα. Επίσης, αν η στεγνώστρα γεµίσει ο πλύστης δεν µπορεί να συνεχίσει µέχρι να υπάρξει χώρος. Θα εισαγάγουµε τώρα το µηχανισµό που παρέχει η Java για την αποδοτική αντιµετώπιση αυτού του προβλήµατος. 7.5.3. Η λύση Θα ήταν δυνατό να προγραµµατίσουµε µία λύση σε αυτό το σενάριο χρησιµοποιώντας τις µεθόδους suspend() και resume(). Όµως, µία τέτοια λύση θα απαιτούσε τα δύο νήµατα να δηµιουργηθούν σε συνεργασία, καθώς κάθε ένα απαιτεί ένα χειριστήριο προς το άλλο. Εξ αιτίας αυτού η Java παρέχει ένα µηχανισµό επικοινωνίας που βασίζεται στα στιγµιότυπα των αντικειµένων. Κάθε στιγµιότυπο αντικειµένων στη Java έχει συσχετισµένες µαζί του δύο ουρές νηµάτων. Η πρώτη χρησιµοποιείται από τα νήµατα που θέλουν να αποκτήσουν την ένδειξη κλειδώµατος και την παρουσιάσαµε νωρίτερα. Η δεύτερη ουρά χρησιµοποιείται για να υλοποιηθούν οι µηχανισµοί επικοινωνίας wait() και notify(). Στη βασική κλάση java.lang.object ορίζονται τρεις µέθοδοι. Αυτές είναι οι wait(), notify() και notifyall(). Θα ασχοληθούµε µε τις δύο πρώτες. Ας επιστρέψουµε στο παράδειγµα του πλυσίµατος πιάτων. Το νήµα a πλένει και το νήµα b σκουπίζει. Και τα δύο έχουν πρόσβαση στο αντικείµενο της στεγνώστρας. Έστω ότι το νήµα b το νήµα που σκουπίζει θέλει να σκουπίσει ένα αντικείµενο, αλλά η στεγνώστρα είναι άδεια. Αυτό µπορεί να γραφεί ως εξής: if (drainingboard.isempty()) drainingboard.wait(); 148
Τώρα όταν το νήµα b κάνει µία κλήση wait(), παύει να είναι εκτελέσιµο, και πηγαίνει στην ουρά αναµονής για το αντικείµενο της στεγνώστρας. ε θα ξαναεκτελεστεί µέχρι κάτι να το βγάλει από αυτή την ουρά. Πώς λοιπόν θα ξαναξεκινήσει το στέγνωµα. Είναι ευθύνη του νήµατος που πλένει να το ενηµερώσει ότι υπάρχει κάτι χρήσιµο για να κάνει. Αυτό επιτυγχάνεται καλώντας τη notify() στη στεγνώστρα ως εξής: drainingboard.additem(plate); drainingboard.notify(); Στο σηµείο αυτό το πρώτο νήµα που είχε απενεργοποιηθεί στην ουρά αναµονής της στεγνώστρας αφαιρείται από αυτή την ουρά και µπορεί να ανταγωνιστεί για εκτέλεση. Σηµειώστε ότι η κλήση notify() γίνεται στην περίπτωση αυτή χωρίς να ελέγξει αν υπάρχουν νήµατα που αναµένουν ή όχι. Η προσέγγιση αυτή δηµιουργεί σαφώς άχρηστες κλήσεις. Είναι πιθανό να κάνουµε την κλήση µόνο αν το πιάτο που τοποθετήσαµε κάνει τη στεγνώστρα να πάψει να είναι άδεια. Αλλά αυτό είναι µία λεπτοµέρεια και µάς αποµακρύνει από την ουσία της συζήτησης αυτής. Επιπρόσθετα είναι σηµαντικό να κατανοήσουµε ότι αν κληθεί η notify() χωρίς να υπάρχουν απενεργοποιηµένα νήµατα στην ουρά αναµονής, τότε η κλήση δεν έχει καµία επίπτωση. Οι κλήσεις στη notify() δεν αποθηκεύονται. Επιπρόσθετα θα πρέπει να γνωρίζετε ότι η notify() απελευθερώνει το πολύ το πρώτο νήµα στην ουρά αναµονής. Αν υπήρχαν περισσότερα από ένα µηνύµατα που περίµεναν, τα υπόλοιπα συνεχίζουν να παραµένουν στην ουρά. Μπορεί να χρησιµοποιηθεί η µέθοδος notifyall() για να απελευθερωθούν όλα τα νήµατα που περιµένουν, εφόσον αυτή είναι η συµπεριφορά που απαιτεί η σχεδίαση του συστήµατος. Χρησιµοποιώντας το µηχανισµό αυτό µπορούµε να συντονίσουµε τα νήµατα πλυσίµατος και στεγνώµατος αρκετά απλά, και χωρίς να χρειάζεται να γνωρίζουµε την ταυτότητά τους. Κάθε φορά που πραγµατοποιούµε µία πράξη η οποία µάς εγγυάται ότι το άλλο νήµα µπορεί να κάνει κάποια σηµαντική δουλειά, κάνουµε µία notify() στο αντικείµενο της στεγνώστρας. Κάθε φορά που προσπαθούµε να κάνουµε µια δουλειά αλλά δεν µπορούµε να συνεχίσουµε γιατί η στεγνώστρα είναι είτε γεµάτη είτε άδεια, κάνουµε wait() στο αντικείµενο της στεγνώστρας. 7.5.4. Η αλήθεια! Οι µηχανισµοί που περιγράψαµε µέχρι τώρα είναι πολλοί ωραίοι στις αρχές τους, αλλά στη Java η υλοποίηση δεν είναι πάντα τόσο απλή όσο υποθέτουµε. Συγκεκριµένα, η ίδια η ουρά αναµονής είναι µία ευαίσθητη δοµή δεδοµένων και συνεπώς χρειάζεται να την προστατεύουµε χρησιµοποιώντας το µηχανισµός συγχρονισµού. ε χρειάζεται να ασχοληθούµε µε τη λεπτοµέρεια του γιατί αυτό συµβαίνει, αλλά είναι σηµαντικό να γνωρίζουµε ότι πριν από κάθε κλήση wait(), notify() και notifyall() σε ένα αντικείµενο, θα πρέπει να κρατάµε την ένδειξη κλειδώµατος για το αντικείµενο αυτό. Συγκεκριµένα πρέπει να καλούµε αυτές τις µεθόδους µέσα από ένα µπλοκ synchronized. Συνεπώς πρέπει ο κώδικάς µας να αλλάξει ως εξής: synchronized(drainingboard) 149
if (drainingboard.isempty()) drainingboard.wait(); και µε παρόµοιο τρόπο synchronized(drainingboard) drainingboard.additem(plate); drainingboard.notify(); Μπορείτε αυτό να το εκλάβετε ως έναν απλό κανόνα της γλώσσας, αλλά δηµιουργεί µία ενδιαφέρουσα παρατήρηση. Εφόσον γνωρίζουµε ότι η εντολή synchronized απαιτεί από το νήµα να λάβει την ένδειξη κλειδώµατος προτού συνεχίσει, θα µπορούσαµε να φανταστούµε ότι µπορεί το νήµα πλυσίµατος να µην φτάσει ποτέ την εντολή notify() αν το νήµα στεγνώµατος έχει απενεργοποιηθεί σε κατάσταση wait(). Στην πράξη η υλοποίηση είναι τέτοια που αυτό δε συµβαίνει. Συγκεκριµένα, όταν καλούµε τη wait() αυτή πρώτα επιστρέφει στο αντικείµενο την ένδειξη κλειδώµατος του αντικειµένου. Όµως για να αποφύγουµε την πιθανότητα κάποιας καταστροφής, όταν ένα νήµα δεχτεί τη notify() δε γίνεται αµέσως εκτελέσιµο, αλλά τοποθετείτε στην ουρά της ένδειξης κλειδώµατος. Έτσι δεν µπορεί να συνεχίσει στην πράξη, αν δεν ξαναπάρει την ένδειξη κλειδώµατος. Μία άλλη άποψη της πραγµατικής υλοποίησης είναι ότι η µέθοδος wait() µπορεί να τερµατιστεί είτε από µία notify() είτε καλώντας τη µέθοδο interrupt() της Thread. Στην περίπτωση αυτή, η wait() «πετάει» µία InterruptedException, η οποία συνήθως απαιτεί να τοποθετηθεί η wait() µέσα σε µία δοµή try/catch. 7.6. Βάζοντάς τα όλα µαζί Ας προσπαθήσουµε τώρα να φτιάξουµε ένα πραγµατικό παράδειγµα από όλα αυτά. Θα δουλέψουµε µε τη βασική αρχή του πλυσίµατος και του στεγνώµατος, αλλά αντί για πιάτα στη στεγνώστρα θα µεταβιβάζουµε χαρακτήρες µέσω ενός αντικειµένου στοίβας. Στην επιστήµη των υπολογιστών αυτό είναι ένα κλασικό παράδειγµα του προβλήµατος παραγωγών-καταναλωτών (producer-consumer problem). Θα ξεκινήσουµε βλέποντας τη µορφή του αντικειµένου της στοίβας, στη συνέχεια θα εξετάσουµε τις λεπτοµέρειες των νηµάτων που θα παράγουν και θα καταναλώνουν. Τέλος, θα δούµε τις λεπτοµέρειες της στοίβας και τους µηχανισµούς που χρησιµοποιούνται για να την προστατέψουµε και να υλοποιήσουµε την επικοινωνία ανάµεσα στα νήµατα. Η κλάση στοίβας, που θα ονοµάζεται SyncStack για να τη διακρίνουµε από τη βασική κλάση java.util.stack, θα παρέχει την πιο κάτω δηµόσια διεπαφή API: public void push(char c); public char pop(); 150
7.6.1. Παραγωγός Το νήµα του παραγωγού θα εκτελεί την πιο κάτω µέθοδο public void run() char c; for (int i = 0; i < 20; i++) c = (char) (Math.random() * 26 + A ); thestack.push ; System.out.println( Produced: + c); try Thread.sleep((int)(Math.random() * 100)); catch (InterruptedException e) // αγνοήστε την Αυτή θα παράγει 20 τυχαία κεφαλαία λατινικά γράµµατα και θα τα κάνει push σε µία στοίβα µε τυχαία καθυστέρηση ανάµεσα σε κάθε τοποθέτηση. Η καθυστέρηση θα είναι µεταξύ 0 και 100 milliseconds. Κάθε χαρακτήρας που θα τοποθετείτε θα αναφέρεται στην κονσόλα. 7.6.2. Καταναλωτής Το νήµα του καταναλωτή θα εκτελεί την πιο κάτω µέθοδο: public void run() char c; for (int i = 0; i < 20; i++) c = thestack.pop(); System.out.println( Consumed: + c); try Thread.sleep((int) (Math.random() * 1000)); catch (InterruptedException e) // αγνοήστε την Αυτή θα συλλέξει 20 χαρακτήρες από τη στοίβα, µε καθυστέρηση ανάµεσα σε κάθε προσπάθεια. Η καθυστέρηση θα είναι µεταξύ 0 και 2 δευτερολέπτων. Αυτό 151
σηµαίνει ότι η στοίβα θα αδειάζει πιο γρήγορα απ ό,τι θα γεµίζει και συνεπώς θα γεµίσει πλήρως πολύ σύντοµα. Ας δούµε τώρα τη δόµηση της κλάσης της στοίβας. Χρειαζόµαστε ένα δείκτη (index) και ένα πίνακα ως ενδιάµεση µνήµη (buffer array). Η ενδιάµεση µνήµη δε θα πρέπει να είναι πολύ µεγάλη, καθώς σκοπός της άσκησης είναι να δείξουµε ότι υπάρχει ορθή λειτουργία και συγχρονισµός όταν γεµίζει. Στην περίπτωση αυτή ο πίνακας θα είναι 6 χαρακτήρων. 7.6.3. Η κλάση SyncStack Μία στιγµιότυπο της SyncStack που µόλις έχει φτιαχτεί θα πρέπει να είναι κενό, κάτι που µπορεί να επιτευχθεί εύκολα χρησιµοποιώντας την εξ ορισµού αρχικοποίηση των τιµών, αλλά για λόγους σωστής τακτικής θα παρουσιάσουµε τη διαδικασία αυτή ρητά. Συνεπώς ξεκινάµε να φτιάχνουµε την κλάση µας. class SyncStack private int index = 0; private char [] buffer = new char[6]; public synchronized char pop() public synchronized void push(char c) Παρατηρήστε την έλλειψη συναρτήσεων δηµιουργίας. Θα ήταν ίσως καλύτερο για λόγους στυλ να τις παρουσιάσουµε, αλλά για λόγους συντοµίας τις παραλείψαµε. Ας δούµε τώρα τις µεθόδους push και pop. Θα χρειαστεί να τις κάνουµε synchronized, για να προστατέψουµε τα ευαίσθητα στοιχεία δεδοµένων index και buffer. Επιπρόσθετα, χρειάζεται να φροντίσουµε να γίνεται wait() αν η µέθοδος δεν µπορεί να συνεχίσει, και notify() όταν κάνουµε τη δουλειά µας. Η µέθοδος pop() είναι έτσι: public synchronized void push(char c) while (index == buffer.length) try this.wait(); catch (InterruptedException e) // αγνόησε το this.notify(); buffer[index] = c; index++; 152
Παρατηρήστε ότι η κλήση στη wait() έγινε ρητά από ως this.wait(). Η χρήση του this είναι πλεονάζουσα, αλλά έχει συµπληρωθεί για να δοθεί έµφαση στο ότι το ραντεβού (rendezvous) γίνεται στο συγκεκριµένο (this) αντικείµενο στοίβας. Η κλήση wait() τοποθετείται σε µία δοµή try/catch. Εξ αιτίας της δυνατότητας η wait() να βγει εξ αιτίας µίας interrupt() πρέπει να έχουµε επανάληψη στον έλεγχο για την περίπτωση που το Thread «ξύπνησε» από τη wait() πρόωρα. Έστω η κλήση στη notify(). Και πάλι έχουµε µία ρητή this.notify(), που είναι πλεονάζουσα, αλλά περιγραφική. Τι γίνεται µε το σηµείο που καλείται η notify(). Γίνεται προτού γίνει η αλλαγή, και γιατί δεν είναι λάθος; Η απάντηση είναι ότι οποιοδήποτε νήµα είχε σταµατήσει εξ αιτίας του wait() δεν µπορεί να συνεχίσει προτού να βγούµε από το συγχρονισµένο µπλοκ, συνεπώς µπορούµε να δώσουµε τη notify() όποτε θέλουµε, αφού γνωρίζουµε ότι πρόκειται να συνεχίσουµε µε τις αναγκαίες αλλαγές. Το τελικό σηµείο είναι το ζήτηµα του ελέγχου για λάθη. Μπορεί να παρατηρήσετε ότι δεν υπάρχει ρητός κώδικας για να αποτραπεί η κατάσταση υπερχείλισης. Αυτή δεν είναι απαραίτητη καθώς ο µόνος τρόπος για να προσθέσουµε κάτι στη στοίβα είναι µέσω αυτής της µεθόδου, και η µέθοδος αυτή θα µπει µέσα στο βρόχο του wait() αν κληθεί σε τέτοιες συνθήκες ώστε να προκαλέσει υπερχείλιση. Συνεπώς ο έλεγχος για λάθη δεν είναι απαραίτητος. Μπορούµε να είµαστε σίγουροι για αυτό σε ένα εκτελούµενο σύστηµα για περισσότερους του ενός λόγους. Αν η λογική µας προκύψει ότι είναι προβληµατική, θα έχουµε κάνει µία προσπέλαση πέρα από τα όρια του πίνακα, πράγµα που θα προκαλέσει άµεσα µία Exception, συνεπώς δεν υπάρχει πιθανότητα να µην παρατηρήσετε αυτό το λάθος. Σε άλλες περιπτώσεις µπορείτε να χρησιµοποιήσετε εξαιρέσεις κατά το χρόνο εκτέλεσης για να τοποθετήσετε τους ελέγχους σας. public synchronized char pop() while (index == 0) try this.wait(); catch (InterruptedException e) // αγνοήστε την this.notify(); index--; return buffer[index]; Το µόνο που αποµένει είναι να τοποθετήσουµε τα κοµµάτια αυτά σε πλήρεις κλάσεις και να δώσουµε και τα στοιχεία για να τα βάλουµε να τρέξουν όλα µαζί. Ακολουθεί ο τελικός κώδικας. 153
SyncTest.java package mod13; public class SyncTest public static void main (String args[]) SyncStack stack = new SyncStack(); Runnable source = new Producer(stack); Runnable sink = new Consumer(stack); Thread t1 = new Thread(source); Thread t2 = new Thread(sink); t1.start(); t2.start(); Producer.java package mod13; public class Producer implements Runnable SyncStack thestack; public Producer(SyncStack a) thestack = a; public void run() char c; for (int i = 0; i < 20; i++) c = (char) (Math.random() * 26 + A ); thestack.push ; System.out.println( Produced: + c); try Thread.sleep((int)(Math.random() * 100)); catch (InterruptedException e) // αγνοήστε την Consumer.java package mod13; public class Consumer implements Runnable SyncStack thestack; 154
public Consumer (SyncStack a) thestack = a; public void run() char c; for (int i = 0; i < 20; i++) c = thestack.pop(); System.out.println( Consumed: + c); try Thread.sleep((int) (Math.random() * 1000)); catch (InterruptedException e) // αγνοήστε την SyncStack.java package mod13; class SyncStack private int index = 0; private char [] buffer = new char[6]; public synchronized char pop() while (index == 0) try this.wait(); catch (InterruptedException e) // αγνοήστε την this.notify(); index--; return buffer[index]; public synchronized void push(char c) while (index == buffer.length) 155
try this.wait(); catch (InterruptedException e) // αγνόησε το this.notify(); buffer[index] = c; index++; 156
7.7. Λυµένες Ασκήσεις 7.7.1. Να γραφεί πρόγραµµα το οποίο θα εκυπώνει αριθµούς από το 1 έως και το 10 µε καθυστέρηση 1 δευτερολέπτου. Λύση: public class UsingThread public static void main(string[] args) for (int i = 1; i < 11; i++) //εκτυπώνουµε από το 1 ως και το 10 System.out.println(i); try //καθυστερούµε την επανάληψη του βρόγχου κατά 1000 miliseconds= 1 second Thread.sleep(1000); catch (InterruptedException ex) //τέλος for //τέλος main //τέλος class 7.8. Ασκήσεις Τρία νήµατα 1. Γράψτε ένα απλό πρόγραµµα που δηµιουργεί τρία νήµατα: κάθε ένα θα πρέπει να παρουσιάζει την ώρα που εκτελέστηκε (µπορείτε να χρησιµοποιήσετε την κλάση Date()). 157
158
8. Ανάπτυξη GUI σε Java 8.1. Το πακέτο java.awt Το πακέτο java.awt περιέχει κλάσεις για την παραγωγή widgets και συνιστωσών GUI. Μία βασική επισκόπηση του πακέτου παρουσιάζεται στο πιο κάτω διάγραµµα. Οι κλάσεις που παρουσιάζονται µε έντονα γράµµατα τονίζουν τα ζητήµατα που θα µας απασχολήσουν σε αυτό το κεφάλαιο. 159
8.2. Ανάπτυξη γραφικών διεπαφών µε το χρήστη 8.2.1. Containers και Components Βασικά στοιχεία του AWT της Java είναι τα components (συνιστώσες) και τα containers (περιέκτες), τα οποία είναι και αυτά components. Components είναι τα γενικά ορατά στοιχεία ενός GUI, όπως ένα Button ή ένα Label (ετικέτα). Τα components τοποθετούνται σε µία «οθόνη» «προσθέτοντάς» τα σε ένα container. Γενικά ένα container περιέχει ένα ή περισσότερα components και, αν το επιθυµούµε, µπορεί να περιέχει άλλα containers. Σηµείωση Το γεγονός ότι ένα container µπορεί να διατηρεί όχι απλά components, αλλά και containers είναι κρίσιµο και βασικό στη δηµιουργία layouts που είναι εαλιστικής πολυπλοκότητας. 8.2.2. Τοποθέτηση components Η θέση ενός component σε ένα container καθορίζεται από το layout manager. Ένα container διατηρεί µία αναφορά σε ένα συγκεκριµένο στιγµιότυπο ενός LayoutManager. Όταν ένα container χρειάζεται να τοποθετήσει ένα component καλεί το layout manager για να το κάνει. Η ίδια εξουσιοδότηση (delegation) συµβαίνει και όταν θέλει να αποφασίσει το µέγεθος του component. 8.2.3. Μέγεθος component Επειδή ο layout manager γενικά είναι υπεύθυνος για το µέγεθος και τη θέση των components στον container του, δε θα πρέπει κανονικά να προσπαθήσετε να θέσετε το µέγεθος ή τη θέση των components µόνοι σας. Αν προσπαθήσετε να κάνετε κάτι τέτοιο (χρησιµοποιώντας οποιαδήποτε από τις µεθόδους setlocation(), setsize() ή setbounds()), ο layout manager υπερβαίνει την απόφασή σας. Αν πρέπει να ελέγχετε το µέγεθος ή τη θέση των components µε τρόπο που δεν µπορεί να γίνει µε κάποιον από τους συνήθεις layout managers είναι δυνατό να απενεργοποιήσετε το layout manager δίνοντας την ακόλουθη κλήση µεθόδου στον container σας: setlayout(null); Μετά αυτό το βήµα θα πρέπει να χρησιµοποιήσετε τα setlocation(), setsize() ή setbounds() σε component για να τα τοποθετήσετε σε container. Προσέξτε ότι η προσέγγιση αυτή οδηγεί σε layouts που εξαρτώνται από την πλατφόρµα εξαιτίας των διαφορών ανάµεσα στο παραθυρικό σύστηµα και των µεγεθών των γραµµατοσειρών. Μία καλύτερη προσέγγιση είναι να δηµιουργήσετε µία νέα κλάση του LayoutManager. 8.3. Frames Ένα Frame είναι µία υποκλάση του Window. Είναι ένα Window µε τίτλο και γωνίες από τις οποίες µπορούµε να αλλάξουµε το µέγεθός του frame. 8.3.1. ηµιουργία ενός απλού Frame Η µέθοδος δηµιουργίας Frame(String) στην κλάση Frame δηµιουργεί ένα νέο, αόρατο αντικείµενο Frame µε τον τίτλο του να καθορίζεται από το String. Ένα Frame 160
µπορεί να αλλάξει µέγεθος χρησιµοποιώντας τη µέθοδο setsize(), η οποία του κληροδοτείται από την κλάση Component. Σηµείωση Για να γίνει το Frame ορατό θα πρέπει να κληθούν οι µέθοδοι setvisible() και setsize(). Το πιο κάτω πρόγραµµα δηµιουργεί ένα απλό πλαίσιο (frame) µε συγκεκριµένο τίτλο, µέγεθος και χρώµα φόντου (background color). import java.awt.*; public class MyFrame extends Frame public static void main(string args[]) MyFrame fr = new MyFrame( Hello Out There! ); // Component method setsize() fr.setsize(500, 500); fr.setbackground(color.blue); fr.setvisible(true); // Component method show() public MyFrame(String str) super(str);... 8.3.2. Εκτέλεση προγράµµατος $ javac MyFrame.java $ java MyFrame 8.4. Panels Τα Panels, όπως τα frames, σας παρέχουν χώρο για να προσκολλήσετε ένα GUI component, συµπεριλαµβανοµένων άλλων panels. 161
8.4.1. ηµιουργία panels Τα Panels δηµιουργούνται χρησιµοποιώντας τη µέθοδος δηµιουργίας Panel(). Από τη στιγµή που δηµιουργείται ένα αντικείµενο Panel, πρέπει να προστεθεί σε ένα αντικείµενο Window ή Frame για να µπορεί να γίνει ορατό. Αυτό γίνεται µε τη µέθοδο add() στην κλάση Container. Το πιο κάτω πρόγραµµα δηµιουργεί ένα µικρό κίτρινο panel, και το προσθέτει στο αντικείµενο Frame. import java.awt.*; public class FrameWithPanel extends Frame // Constructor public FrameWithPanel (String str) super(str); public static void main (String args[]) FrameWithPanel fr = new FrameWithPanel( Frame with Panel ); Panel pan = new Panel(); fr.setsize(200,200); fr.setbackground(color.blue); fr.setlayout(null); // override default layout mgr pan.setsize(100,100); pan.setbackground(color.yellow); fr.add(pan); fr.setvisible(true);... Ιδού το αποτέλεσµα: 8.5. Container Layouts Το layout των components σε ένα container µπορεί να καθορίζεται από έναν layout manager. Κάθε container, όπως ένα panel ή ένα frame έχει έναν εξ ορισµού 162
layout manager που σχετίζεται µαζί του, το οποίο µπορεί να αλλάξει από τον προγραµµατιστή Java όταν δηµιουργείται ένα στιγµιότυπο αυτού του container. 8.5.1. Layout Managers Στη γλώσσα Java υπάρχουν layout managers: FlowLayout Ο εξ ορισµού layout manager των Panels και Applets. BorderLayout Ο εξ ορισµού layout manager των Windows, Dialogs και Frames. GridLayout CardLayout GridBagLayout 8.6. Ένα απλό παράδειγµα Ο πιο κάτω απλός κώδικας παρουσιάζει διάφορα ενδιαφέροντα σηµεία και θα τον αναλύσουµε στη συνέχεια. import java.awt.*; public class ExGui private Frame f; private Button b1; private Button b2; public static void main(string args[]) ExGui that = new ExGui(); that.go(); public void go() f = new Frame( GUI Example ); f.setlayout(new FlowLayout()); b1 = new Button( Press Me ); b2 = new Button( Don t press Me ); f.add(b1); f.add(b2); f.pack(); f.setvisible(true); 8.6.1. Η µέθοδος main() Η µέθοδος main() στο παράδειγµα αυτό κάνει δύο πράγµατα, πρώτα δηµιουργεί ένα στιγµιότυπο του αντικειµένου ExGui. Θυµηθείτε ότι µέχρι να υπάρξει ένα στιγµιότυπο, δεν υπάρχουν πραγµατικά στοιχεία δεδοµένων µε τα ονόµατα f, b1, και b2 προς χρήση. Όταν έχει δηµιουργηθεί ο χώρος δεδοµένων, η main() καλεί τη µέθοδο στιγµιοτύπου go() στα συµφραζόµενα αυτού του στιγµιοτύπου. Στην go() λαµβάνει χώρα η πραγµατική δράση. 163
8.6.2. new Frame( GUI Example ) Αυτό δηµιουργεί ένα στιγµιότυπο της κλάσης java.awt.frame. Ένα frame στη Java είναι ένα παράθυρο πρώτου επιπέδου, µε τη µπάρα τίτλου η οποία ορίζεται από το όρισµα της µεθόδου δηµιουργίας GUI Example στην περίπτωση αυτή και χειριστήρια αλλαγής µεγέθους και άλλα σχέδια (decoration) σύµφωνα µε τις τοπικές συµβάσεις. Το frame έχει µηδενικό µέγεθος και δεν είναι προς το παρόν ορατό. 8.6.3. f.setlayout(new FlowLayout()) Αυτό δηµιουργεί ένα στιγµιότυπο του flow layout manager, και το εγκαθιστά στο frame. Υπάρχει ένας εξ ορισµού layout manager για κάθε frame, αλλά δεν είναι κατάλληλος για τις ανάγκες µας στο παρόν παράδειγµα. Ο flow layout manager είναι ο απλούστερος στο AWT και τοποθετεί τα components περίπου όπως τις λέξεις σε µία σελίδα, γραµµή µε γραµµή. Σηµειώστε ότι ο flow layout στοιχίζει εξ ορισµού κάθε γραµµή στο κέντρο. 8.6.4. new Button( Press me ) Αυτό δηµιουργεί ένα στιγµιότυπο της κλάσης java.awt.button. Ένα button είναι το σύνηθες push button που προέρχεται από την τοπική παραθυρική εργαλειοθήκη. Η ετικέτα του button ορίζεται από ένα όρισµα συµβολοσειράς στη µέθοδο δηµιουργίας. 8.6.5. f.add(b1) Αυτό λέει στο frame f που είναι ένα container ότι πρόκειται να περιέχει το component b1. Τα buttons είναι παραδείγµατα Java components. Το µέγεθος και η θέση του b1 είναι υπό τον έλεγχο του layout manager του frame από το σηµείο αυτό και µετά. 8.6.6. f.pack() Η µέθοδος αυτή λέει στο frame να θέσει ένα µέγεθος που να «συµπεριλαµβάνει µε ωραίο τρόπο» τα components που περιέχει. Για να πάρει τη σχετική απόφαση ρωτά το layout manager που είναι υπεύθυνος για το µέγεθος και τη θέση όλων όσων περιέχει το frame. 8.6.7. f.setvisible(true) Η µέθοδος αυτή προκαλεί το πλαίσιο (frame) και τα components του να γίνουν ορατά στο χρήστη. Το τελικό αποτέλεσµα του κώδικα αυτού σε περιβάλλον Windows δείχνει ως εξής: 164
8.7. Layout Managers 8.7.1. Flow Layout Managers Το flow layout που χρησιµοποιήθηκε στο πρώτο παράδειγµα τοποθετεί τα components µε βάση γραµµή-γραµµή. Κάθε φορά που γεµίζει µία γραµµή ξεκινά µία άλλη. Σε αντίθεση µε άλλους layout managers, ο flow layout δεν περιορίζει το µέγεθος του component που χειρίζεται, αλλά αντίθετα τους επιτρέπει να έχουν ένα προτιµώµενο µέγεθος. Σηµείωση Όλα τα components έχουν µία µέθοδο που ονοµάζεται setpreferredsize() η οποία χρησιµοποιείται από τους layout managers για να ρωτήσουν το component σχετικά µε το µέγεθος που προτιµά να έχει. Οι επιλογές στο flow layout σας επιτρέπουν να στοιχίζετε τα components στα δεξιά ή τα αριστερά, αν προτιµάτε κάτι τέτοιο σε σχέση µε την εξ ορισµού συµπεριφορά που τα στοιχίζει στο κέντρο. Αν θέλετε να δηµιουργήσετε µεγαλύτερη περιοχή ορίωνανάµεσα σε κάθε component µπορείτε να καθορίσετε insets. Όταν αλλάξετε το µέγεθος µίας περιοχής που χειρίζεται ένας flow layout manager, τότε µπορεί να αλλάξει το layout. Τα ακόλουθα είναι παραδείγµατα του πώς µπορείτε να υλοποιήσετε το FlowLayout χρησιµοποιώντας τη µέθοδο setlayout() η οποία κληροδοτείται από το Component. setlayout(new FlowLayout(FlowLayout.RIGHT, 20, 40)); setlayout(new FlowLayout(FlowLayout.LEFT)); setlayout(new FlowLayout()); Το ακόλουθο παράδειγµα προσθέτει διάφορα buttons σε ένα flow layout σε ένα frame: import java.awt.*; public class MyFlow private Frame f; private Button button1, button2, button3; public static void main(string args[]) MyFlow mflow = new MyFlow(); mflow.go(); 165
public void go() f = new Frame( Flow Layout ); f.setlayout(new FlowLayout()); button1 = new Button( Ok ); button2 = new Button( Open ); button3 = new Button( Close ); f.add(button1); f.add(button2); f.add(button3); f.setsize(100, 100); f.setvisible(true); 8.7.2. Border Layout Manager Ο BorderLayout manager παρέχει ένα πιο σύνθετο σχήµα για την τοποθέτηση των components σας µέσα σε ένα panel ή window. Το BorderLayout περιέχει πέντε διακριτές περιοχές. North, South, East, West και Center. Η North καταλαµβάνει την κορυφή του panel, η East καταλαµβάνει τη δεξιά πλευρά κ.ο.κ. Η περιοχή Center αναπαριστά ό,τι έχει αποµείνει από τη στιγµή που έχουν γεµίσει οι North, South, East, και West. Ο BorderLayout manager είναι ο εξ ορισµού layout manager για Dialogs και Frames. Στη συνέχεια παρουσιάζεται ο κώδικας που δίνει το ακόλουθο αποτέλεσµα. µετά την αλλαγή µεγέθους. Σηµείωση Οι σχετικές θέσεις των buttons δεν αλλάζουν καθώς αλλάζει το µέγεθος του παραθύρου, αλλά αλλάζουν τα µεγέθη των buttons. Ο ακόλουθος κώδικας είναι τροποποίηση του προηγούµενου παραδείγµατος και παρουσιάζει τη συµπεριφορά του border layout manager. import java.awt.*; public class ExGui2 166
private Frame f; private Button bn, bs, bw, be, bc; public static void main(string args[]) ExGui2 that = new ExGui2(); that.go(); public void go() f = new Frame( Border Layout ); bn = new Button( B1 ); bs = new Button( B2 ); bw = new Button( B3 ); be = new Button( B4 ); bc = new Button( B5 ); f.add(bn, North ); f.add(bs, South ); f.add(bw, West ); f.add(be, East ); f.add(bc, Center ); f.setsize(200, 200); f.setvisible(true); Τα components πρέπει να προστίθενται στις ονοµατισµένες περιοχές στο border layout manager, διαφορετικά δεν είναι ορατά. Προσέξτε να είναι ορθή η ορθογραφία των ονοµάτων καθώς και τα κεφαλαία-πεζά. Μπορείτε να χρησιµοποιήσετε το border layout manager για να παράγετε layouts µε στοιχεία που πρέπει να κάνουν stretch προς µία κατεύθυνση ή την άλλη, ή και τις δύο, όταν γίνεται αλλαγή µεγέθους. Αν αφήσετε µία περιοχή ενός border layout αχρησιµοποίητη συµπεριφέρεται σαν το προτιµητέο µέγεθος να ήταν µηδέν επί µηδέν. Συνεπώς η κεντρική περιοχή συνεχίζει να εµφανίζεται ως φόντο ακόµα και αν δεν περιέχει components, αλλά οι τέσσερις περιφερειακές περιοχές θα µειωθούν σε µία γραµµή µηδενικού πάχους και θα εξαφανιστούν. Πρέπει να προσθέτετε ένα µόνο component σε κάθε µία των πέντε περιοχών του border layout manager. Αν προσπαθήσετε να προσθέσετε περισσότερα από ένα, µόνο ένα από αυτά θα είναι ορατό. Θα δούµε στη συνέχεια πώς µπορείτε να χρησιµοποιήσετε ενδιάµεσους containers για να µπορέσετε να έχετε περισσότερα από ένα component στο χώρο µίας µόνο περιοχής ενός border layout manager. 8.7.3. Grid Layout Manager Ο grid layout manager παρέχει ευελιξία στην τοποθέτηση components. ηµιουργείτε το manager δίνοντας το πλήθος των γραµµών και των στηλών. Τα components θα γεµίσουν τα κελιά που ορίζει ο manager. Για παράδειγµα ένα grid layout µε τρεις γραµµές και δύο στήλες, που δηµιουργείται µε την εντολή GridLayout(3,2) θα δηµιουργήσει έξι κελιά όπως φαίνεται πιο κάτω: 167
Όπως και µε το BorderLayout manager, η σχετική θέση των components δεν αλλάζει όταν η περιοχή αλλάζει µέγεθος. Αλλάζουν µόνο τα µεγέθη των components. Παρατηρήστε ότι το πλάτος όλων των κελιών είναι το ίδιο και καθορίζεται ως η απλή διαίρεση του διαθέσιµου πλάτους δια το πλήθος των κελιών. Με παρόµοιο τρόπο το ύψος όλων των κελιών καθορίζεται απλά από το διαθέσιµο ύψος διαιρεµένο δια του πλήθους των γραµµών. Η σειρά µε την οποία τοποθετούνται τα components στο grid καθορίζει το κελί που θα καταλάβουν. Οι γραµµές των κελιών γεµίζονται από τα αριστερά στα δεξιά, όπως το κείµενο, και η «σελίδα» γεµίζει µε γραµµές από πάνω προς τα κάτω. Στη συνέχεια παρουσιάζεται ο κώδικας για το πιο πάνω παράδειγµα: import java.awt.*; public class GridEx private Frame f; private Button b1, b2, b3, b4, b5, b6; public static void main(string args[]) GridEx grid = new GridEx(); grid.go(); public void go() f = new Frame( Grid Example ); f.setlayout(new GridLayout(3, 2)); b1 = new Button( 1 ); b2 = new Button( 2 ); b3 = new Button( 3 ); b4 = new Button( 4 ); b5 = new Button( 5 ); b6 = new Button( 6 ); f.add(b1); f.add(b2); f.add(b3); f.add(b4); f.add(b5); f.add(b6); f.pack(); f.setvisible(true); 168
8.7.4. CardLayout Ο CardLayout manager σας επιτρέπει να αντιµετωπίζετε τη διεπαφή σαν µία σειρά από κάρτες, µόνο κάθε µία από τις οποίες είναι ορατή σε κάθε χρονική στιγµή. Για παράδειγµα, παρουσιάζεται ένα Frame µε 5 κάρτες. Ένα κλικ µε το ποντίκι στο Panel του Frame αλλάζει στην επόµενη κάρτα. Τµήµα του απαιτούµενου κώδικα παρουσιάζεται στη συνέχεια: import java.awt.*; import java.awt.event.*; public class CardTest implements MouseListener Panel p1, p2, p3, p4, p5; Label l1, l2, l3, l4, l5; // Declare a CardLayout object // so I can call its methods CardLayout mycard; Frame f; public static void main(string args[]) CardTest ct = new CardTest(); ct.init(); public void init() f = new Frame( Card Test ); mycard = new CardLayout(); f.setlayout(mycard); // create the panels that I want // to use as cards p1 = new Panel(); p2 = new Panel(); p3 = new Panel(); p4 = new Panel(); p5 = new Panel(); 169
// create a label to attach to each panel, and // change the color of each panel, so they are // easily distinguishable l1 = new Label( This is the first Panel ); p1.setbackground(color.yellow); l2 = new Label( This is the second Panel ); p2.setbackground(color.green); l3 = new Label( This is the third Panel ); p3.setbackground(color.magenta); l4 = new Label( This is the fourth Panel ); p4.setbackground(color.white); l5 = new Label( This is the fifth Panel ); p5.setbackground(color.cyan); // Set up the event handling here... // It is missing we will see it later // add each panel to my applet s CardLayout f.add(p1, First ); f.add(p2, Second ); f.add(p3, Third ); f.add(p4, Fourth ); f.add(p5, Fifth ); // display the first panel mycard.show(f, First ); f.setsize(200, 200); f.show(); 8.7.5. Άλλοι Layout Managers Εκτός από τους flow, border, grid και card layout managers, το βασικό java awt παρέχει επίσης το GridBagLayout. Ο grid bag layout manager παρέχει σύνθετες διευκολύνσεις για το layout, το οποίο βασίζεται σε ένα grid (πλέγµα), αλλά επιτρέπει σε ένα component να λάβει το µέγεθος που προτιµά µέσα στο κελί, αντί να γεµίζει ολόκληρο το κελί. Επίσης στο grid bag layout επιτρέπεται σε ένα component να καταλαµβάνει περισσότερα από ένα κελιά. 8.8. Containers To AWT παρέχει αρκετά containers. Το κεφάλαιο αυτό παρουσιάζει τα δύο πιο σηµαντικά. 8.8.1. Frames Έχουµε ήδη δει το frame σε χρήση στα προηγούµενα παραδείγµατα. Αναπαριστά ένα παράθυρο στο «υψηλότερο επίπεδο» µε τίτλο, όρια και γωνίες από τις οποίες µπορεί να αλλάξει µέγεθος σύµφωνα µε τις συµβάσεις της τοπικής πλατφόρµας. Αν δε χρησιµοποιήσετε ρητά µία κλήση στη µέθοδο setlayout(), ένα πλαίσιο έχει ως εξ ορισµό layout manager, τον border. 170
Οι περισσότερες εφαρµογές θα χρησιµοποιήσουν τουλάχιστον ένα frame ως το βασικό σηµείο των περισσότερων GUI τους, αλλά είναι απόλυτα δυνατό να χρησιµοποιηθούν πολλαπλά frames σε ένα µόνο κοµµάτι κώδικα. 8.8.2. Panels Τα panels είναι contains και σχεδόν τίποτε άλλο. εν έχουν δική τους παρουσίαση και δεν µπορούν να χρησιµοποιηθούν ως αυτοδύναµα παράθυρα. Εξ ορισµού ένα panel σχετίζεται µε το flow layout manager αλλά αυτό µπορεί να αλλάξει µε χρήση και πάλι της µεθόδου setlayout(). Τα panels δηµιουργούνται και προστίθενται σε άλλους containers µε τον ίδιο τρόπο όπως τα components σαν τα buttons. Όµως, όπου προστίθεται ένα panel σε ένα container, υπάρχουν δύο σηµαντικά πράγµατα που µπορείτε να κάνετε στο προκύπτον panel και αυτά είναι: Να του δώσετε ένα δικό του layout manager, επιβάλλοντας διαφορετική προσέγγιση layout στην περιοχή που παρουσιάζεται. Να προσθέσετε components στο panel, ακόµα και αν, για παράδειγµα, το ίδιο το panel αποτελεί το µοναδικό component που µπορεί να προστεθεί σωστά σε µία περιοχή border layout. 8.8.3. ηµιουργία panels και σύνθετων layouts Τα panels δηµιουργούνται µε χρήση της µεθόδου δηµιουργίας Panel(). Από τη στιγµή που έχει δηµιουργηθεί ένα αντικείµενο Panel πρέπει να προστεθεί σε έναν άλλο container. Αυτό γίνεται όπως και πριν µε χρήση της µεθόδου add() του container. Το πιο κάτω πρόγραµµα χρησιµοποιεί ένα panel για να επιτρέψει σε δύο buttons να τοποθετηθούν στην περιοχή North ενός border layout. Αυτού του είδους η ένθεση είναι ουσιώδης σε σύνθετα layouts. Σηµειώστε ότι το panel αντιµετωπίζεται όπως οποιοδήποτε άλλο component σε ό,τι αφορά το ίδιο το frame. import java.awt.*; public class ExGui3 private Frame f; private Panel p; private Button bw, bc; private Button bfile, bhelp; public static void main(string args[]) ExGui3 gui = new ExGui3(); gui.go(); public void go() f = new Frame( Gui Example 3 ); bw = new Button( West ); bc = new Button( Work space region ); f.add(bw, West ); f.add(bc, Center ); p = new Panel(); 171
f.add(p, North ); bfile = new Button( File ); bhelp = new Button( Help ); p.add(bfile); p.add(bhelp); f.pack(); f.setvisible(true); Μετά την εκτέλεση του παραδείγµατος, το αποτέλεσµα στην οθόνη είναι κάπως έτσι: Αν το παράθυρο αλλάξει µέγεθος θα φαίνεται κάπως έτσι: Παρατηρήστε ότι η περιοχή North του border layout στην πράξη πλέον περιέχει δύο buttons. Στην πράξη περιέχει απλά µόνο ένα panel, αλλά το panel περιέχει δύο buttons. Το µέγεθος και η θέση του Panel καθορίζεται από το border layout, το επιθυµητό µέγεθος του pane καθορίζεται από το επιθυµητό µέγεθος των components που βρίσκονται µέσα στο panel. Το µέγεθος και η θέση των buttons στο panel καθορίζονται από το flow layout που σχετίζεται εξ ορισµού µε το panel. 172
8.9. Λυµένες Ασκήσεις. 8.9.1 Γράψτε ένα πρόγραµµα το οποίο να δηµιουργεί ένα πλαίσιο µε τίτλο New Frame και διαστάσεις 400x300 pixels. Λύση: import javax.swing.*; public class NewFrame1 extends JFrame public NewFrame1() super("new Frame1"); setsize(400,300); setdefaultcloseoperation(jframe.exit_on_close); setvisible(true); public static void main(string[] arguments) NewFrame1 a = new NewFrame1(); Η εκτέλεση του παραπάνω προγράµµατος (javac NewFrame.java και java NewFrame) εµφανίζει το ακόλουθο πλαίσιο: 173
8.9.2 Γράψτε ένα πρόγραµµα το οποίο να δηµιουργεί ένα πλαίσιο µε τίτλο New Frame και διαστάσεις 400x300 pixels. Τοποθετήστε µέσα δύο κουµπιά µε ετικέτες One και Two. Λύση: public class NewFrame1 extends JFrame public NewFrame1() super("new Frame"); setsize(400,300); setdefaultcloseoperation(jframe.exit_on_close); setvisible(true); JButton one = new JButton("One"); JButton two = new JButton("Two"); Container pane = getcontentpane(); FlowLayout flo = new FlowLayout(); pane.setlayout(flo); pane.add(one); pane.add(two); setcontentpane(pane); public static void main(string[] arguments) NewFrame1 a = new NewFrame1(); Η εκτέλεση του παραπάνω προγράµµατος (javac NewFrame1.java και java NewFrame1) εµφανίζει το ακόλουθο πλαίσιο: 174
8.9.3 Να γράψετε ένα πρόγραµµα το οποίο να δηµιουργεί ένα κατάλληλο πλαίσιο µέσα στο οποίο να εµφανίζεται η φράση JAVA IS FUN τέσσερις φορές. Την πρώτη φορά µε χαρακτήρες PLAIN των 12 points, την δεύτερη µε χαρακτήρες ITALIC των 20 points, την τρίτη µε χαρακτήρες BOLD των 30 points και την τέταρτη φορά µε χαρακτήρες BOLD+ITALIC των 35 points. Την τελευταία φράση τυπώστε την σε χρώµα κόκκινο. Λύση: import javax.swing.*; import java.awt.*; public class PrintJava extends JFrame public PrintJava() super("print Java is FUN!!!"); setsize(400,300); setdefaultcloseoperation(jframe.exit_on_close); PrintMyJava myjava = new PrintMyJava(); Container pane = getcontentpane(); pane.add(myjava); setcontentpane(pane); setvisible(true); class PrintMyJava extends JPanel public void paintcomponent(graphics comp) super.paintcomponent(comp); Graphics2D comp2d = (Graphics2D) comp; Font myfont = new Font("Dialog", Font.PLAIN,12); comp2d.setfont(myfont); comp2d.drawstring("java IS FUN",100,50); myfont = new Font("Dialog", Font.ITALIC,20); comp2d.setfont(myfont); comp2d.drawstring("java IS FUN",100,100); myfont = new Font("Dialog", Font.BOLD,30); comp2d.setfont(myfont); comp2d.drawstring("java IS FUN",100,150); myfont = new Font("Dialog", Font.BOLD+Font.ITALIC,35); comp2d.setfont(myfont); comp2d.setcolor(color.red); 175
comp2d.drawstring("java IS FUN",100,200); public static void main(string[] args) PrintJava a = new PrintJava(); Η εκτέλεση του παραπάνω προγράµµατος (javac PrintJava.java και java PrintJava) εµφανίζει το επόµενο πλαίσιο: 8.9.4 Να γράψετε ένα πρόγραµµα το οποίο να δηµιουργεί ένα κατάλληλο πλαίσιο µέσα στο οποίο να εµφανίζονται τρία ορθογώνια γεµισµένα µε τα τρία βασικά χρώµατα κόκκινο, πράσινο και µπλε. Για την κατασκευή των ορθογωνίων χρησιµοποιείστε την µέθοδο fillrect(x,y, width, height), όπου x,y οι συντεταγµένες της πάνω αριστερής γωνίας και width, height το πλάτος και το ύψος του ορθογωνίου. Λύση: import javax.swing.*; import java.awt.*; public class PrintColors extends JFrame public PrintColors() super("print Colors"); setsize(500,300); setdefaultcloseoperation(jframe.exit_on_close); PrintMyColors mycolors = new PrintMyColors(); Container pane = getcontentpane(); 176
pane.add(mycolors); setcontentpane(pane); setvisible(true); class PrintMyColors extends JPanel public void paintcomponent(graphics comp) super.paintcomponent(comp); Graphics2D comp2d = (Graphics2D) comp; comp2d.setcolor(color.red); comp2d.fillrect(50,50,100,100); comp2d.setcolor(color.green); comp2d.fillrect(200,50,100,100); comp2d.setcolor(color.blue); comp2d.fillrect(350,50,100,100); public static void main(string[] args) PrintColors a = new PrintColors(); Η εκτέλεση του παραπάνω προγράµµατος (javac PrintColors.java και java PrintColors) εµφανίζει το επόµενο πλαίσιο: 177
8.9.5 Να γράψετε ένα πρόγραµµα το οποίο να δηµιουργεί ένα κατάλληλο πλαίσιο µέσα στο οποίο να χαράζει µία ευθεία µε συντεταγµένες (50,50,350,250). Λύση: import javax.swing.*; import java.awt.*; import java.awt.geom.*; public class TestLine extends JFrame public TestLine() super("drow a Line"); setsize(400,300); setdefaultcloseoperation(jframe.exit_on_close); DrawLine testline = new DrawLine(); Container pane = getcontentpane(); pane.add(testline); setcontentpane(pane); setvisible(true); class DrawLine extends JPanel public void paintcomponent(graphics comp) Graphics2D comp2d = (Graphics2D) comp; Line2D.Float line = new Line2D.Float(50F,50F,350F,250F); comp2d.draw(line); public static void main(string[] args) TestLine a = new TestLine(); 178
Η εκτέλεση του παραπάνω προγράµµατος (javac TestLine.java και java TestLine) εµφανίζει το επόµενο πλαίσιο: 8.9.6. Να γράψετε ένα πρόγραµµα το οποίο να δηµιουργεί ένα κατάλληλο πλαίσιο µέσα στο οποίο να χαράζει έναν κύκλο και να τον γεµίζει µε κόκκινο χρώµα. Λύση: import javax.swing.*; import java.awt.*; import java.awt.geom.*; public class TestCircle extends JFrame public TestCircle() super("drow a Line"); setsize(400,400); setdefaultcloseoperation(jframe.exit_on_close); DrawCircle testcircle = new DrawCircle(); Container pane = getcontentpane(); pane.add(testcircle); setcontentpane(pane); setvisible(true); class DrawCircle extends JPanel public void paintcomponent(graphics comp) super.paintcomponents(comp); Graphics2D comp2d = (Graphics2D) comp; 179
Ellipse2D.Float circle = new Ellipse2D.Float(100F,100F,200F,300F); comp2d.draw(circle); comp2d.setcolor(color.red); comp2d.fill(circle); public static void main(string[] args) TestCircle a = new TestCircle(); Η εκτέλεση του παραπάνω προγράµµατος (javac TestCircle.java και java TestCircle) εµφανίζει το επόµενο πλαίσιο: 8.9.7. Να γράψετε ένα πρόγραµµα το οποίο να δηµιουργεί ένα κατάλληλο πλαίσιο µέσα στο οποίο να χαράζει δύο κύκλους που να τέµνονται και να τους γεµίζει µε κόκκινο τον έναν και µε µπλε τον άλλον χρώµα. Λύση: import javax.swing.*; import java.awt.*; import java.awt.geom.*; public class TestCircle2 extends JFrame public TestCircle2() 180
super("draw two Color-filled Circles"); setsize(600,400); setdefaultcloseoperation(jframe.exit_on_close); DrawCircles testcircle = new DrawCircles(); Container pane = getcontentpane(); pane.add(testcircle); setcontentpane(pane); setvisible(true); class DrawCircles extends JPanel public void paintcomponent(graphics comp) super.paintcomponents(comp); Graphics2D comp2d = (Graphics2D) comp; Ellipse2D.Float circle1 = new Ellipse2D.Float(100F,100F,200F,200F); comp2d.draw(circle1); comp2d.setcolor(color.red); comp2d.fill(circle1); comp2d.setcolor(color.black); Ellipse2D.Float circle2 = new Ellipse2D.Float(250F,100F,200F,200F); comp2d.draw(circle2); comp2d.setcolor(color.blue); comp2d.fill(circle2); public static void main(string[] args) TestCircle2 a = new TestCircle2(); 181
Η εκτέλεση του παραπάνω προγράµµατος (javac TestCircle2.java και java TestCircle2) εµφανίζει το επόµενο πλαίσιο: 8.9.8. Να γράψετε ένα πρόγραµµα το οποίο να δηµιουργεί ένα κατάλληλο πλαίσιο µέσα στο οποίο να χαράζει ένα τρίγωνο µε γραµµή κόκκινου χρώµατος πάχους 5 points. Λύση: import javax.swing.*; import java.awt.*; import java.awt.geom.*; public class TestTriangle extends JFrame public TestTriangle() super("draw two Colored Triangle"); setsize(600,400); setdefaultcloseoperation(jframe.exit_on_close); DrawTriangle triangle = new DrawTriangle(); Container pane = getcontentpane(); pane.add(triangle); setcontentpane(pane); setvisible(true); 182
class DrawTriangle extends JPanel public void paintcomponent(graphics comp) super.paintcomponents(comp); Graphics2D comp2d = (Graphics2D) comp; GeneralPath triang = new GeneralPath(); triang.moveto(300f,100f); triang.lineto(150f,300f); triang.lineto(450f,300f); triang.closepath(); BasicStroke pena = new BasicStroke(5); comp2d.setstroke(pena); comp2d.setcolor(color.red); comp2d.draw(triang); public static void main(string[] args) TestTriangle a = new TestTriangle(); Η εκτέλεση του παραπάνω προγράµµατος (javac TestTriangle.java και java TestTriangle) εµφανίζει το επόµενο πλαίσιο: 8.10 Ασκήσεις 1. ηµιουργείστε ένα απλό πλαίσιο (frame). 2. Να γραφεί ένα πρόγραµµα που θα κάνει χρήση του gui και θα έχει τα εξής 3 συστατικά: 1) κουµπί 2) περιοχή κειµένου 3) πεδίο κειµένου. 3. ηµιουργείστε ένα πρόγραµµα που θα δηµιουργεί ένα απλό πλαίσιο (frame) µε δύο κουµπιά (jbutton), ένα πεδίο κειµένου (jtextfield), µια περιοχή κειµένου και δύο ετικέτες (jlabel) µε τη χρήση διαχειριστή διάταξης (layout manager) 183
4. Να γραφεί ένα πρόγραµµα το οποίο θα κάνει χρήση ενός gui και θα ζητάει από το χρήστη να πληκτρολογήσει τα έσοδα και έξοδά του, ενώ αυτόµατα θα εκτυπώνεται από το πρόγραµµα τα συνολικά κέρδη του µε βάση τα έσοδαέξοδά του. 5. Να γραφεί ένα πρόγραµµα που θα κάνει χρήση του gui και θα έχει τις εξής 3 λειτουργίες: 1) θα διαβάζει από ένα αρχείο το πρόγραµµα του πρωταθλήµατος και θα το εµφανίζει στην οθόνη 2) θα διαβάζει από ένα αρχείο τη βαθµολογία του πρωταθλήµατος και θα την εµφανίζει στην οθόνη 3) θα κάνει έξοδο προγράµµατος. 6. ηµιουργείστε ένα gui για µία απλή αριθµοµηχανή, περίπου όπως η επόµενη: 7. ηµιουργείστε ένα GUI που θα παρέχει την διεπαφή µε τον τελικό χρήστη για την υπηρεσία ανοίγµατος λογαριασµού που περιγράψαµε στο κεφάλαιο 6. Μπορεί να χρειαστείτε ορισµένα components που δεν έχουµε περιγράψει ακόµα (θα περιγραφούν στη συνέχεια). 184
9. Το µοντέλο συµβάντων του AWT 9.1. Τι είναι ένα συµβάν Όταν ένας χρήστης πραγµατοποιεί µια πράξη στη διεπαφή µε το χρήστη, αυτό προκαλεί τη δηµιουργία ενός συµβάντος (event). Τα συµβάντα είναι αντικείµενα που περιγράφουν τι συνέβη. Υπάρχει ένα πλήθος διαφορετικών κλάσεων συµβάντων που περιγράφουν διαφορετικές γενικές κατηγορίες πράξεων του χρήστη. 9.1.1. Πηγές συµβάντων Μία πηγή συµβάντος (στο επίπεδο της διεπαφής µε το χρήστη) είναι το αποτέλεσµα κάποιας πράξης του χρήστη σε ένα AWT component. Για παράδειγµα, ένα κλικ µε το ποντίκι σε ένα button component θα παράγει (θα είναι η πηγή) ενός ActionEvent. Το ActionEvent είναι ένα αντικείµενο (κλάση) που περιέχει πληροφορία σχετικά µε την κατάσταση του συµβάντος: ActionCommand το όνοµα της εντολής που σχετίζεται µε την πράξη Τροποποιητές οποιοιδήποτε τροποποιητές (modifiers) που αποθηκεύθηκαν κατά την πράξη. 9.1.2. Χειριστές συµβάντων Όταν λαµβάνει χώρα ένα συµβάν, το συµβάν λαµβάνεται από ένα component µε το οποίο ο χρήστης είχε αλληλεπίδραση. Για παράδειγµα, το button, slider, text field κλπ. Ένας χειριστής συµβάντων (event handler) είναι η µέθοδος που λαµβάνει το αντικείµενο Event έτσι ώστε το πρόγραµµα να µπορεί να επεξεργαστεί την αλληλεπίδραση µε το χρήστη. 9.1.3. Πώς γίνεται η επεξεργασία των συµβάντων Μεταξύ των JDK 1.0 και JDK 1.1 υπάρχουν σηµαντικές αλλαγές σχετικά µε τον τρόπο που τα συµβάντα λαµβάνονται και επεξεργάζονται. Στο κεφάλαιο αυτό θα συγκρίνουµε το παλιό µοντέλο συµβάντων (JDK 1.0) και το τρέχον µοντέλο συµβάντων (JDK 1.1 και εξής). 9.2 Μοντέλο συµβάντων JDK 1.0 εναντίον JDK 1.1 To JDK 1.0 ακολουθεί ένα ιεραρχικό µοντέλο συµβάντων και το JDK 1.1 χρησιµοποιεί ένα µοντέλο συµβάντων µε χρήση εξουσιοδότησης (delegation event model). Είναι σηµαντικό να γνωρίζουµε πώς συγκρίνονται τα δύο µοντέλα συµβάντων. 9.2.1. Ιεραρχικό µοντέλο (JDK 1.0) Το ιεραρχικό µοντέλο βασίζεται στο «περιέχειν». Τα συµβάντα αποστέλλονται πρώτα στο component και στη συνέχεια προωθούνται προς τα πάνω στην ιεραρχία που δηµιουργείται από τη σχέση «περιέχειν». Τα συµβάντα που δεν αντιµετωπίζονται από το component θα συνεχίσουν αυτόµατα να προωθούνται στο container του component. 185
Για παράδειγµα, στο πιο πάνω σχήµα, ένα κλικ µε το ποντίκι στο αντικείµενο Button (που περιέχεται σε ένα Panel µέσα σε ένα Frame) στέλνει ένα action event πρώτα στο ίδιο το Button. Αν το Button δεν το χειριστεί, το συµβάν θα αποσταλεί στη συνέχεια στο Panel και αν και αυτό δεν το χειριστεί το συµβάν αποστέλλεται στο Frame. Υπάρχει ένα προφανές πλεονέκτηµα σε αυτό το µοντέλο: Είναι απλό και ταιριάζει σε ένα περιβάλλον αντικειµενοστρεφούς προγραµµατισµού άλλωστε τα components της Java κληρονοµούν από την κλάση java.awt.component, όπου και βρίσκεται η µέθοδος handleevent. Υπάρχουν όµως και ορισµένα µειονεκτήµατα: εν υπάρχει απλώς τρόπος για να «φιλτράρουµε» τα συµβάντα. Για να χειριστούµε ένα συµβάν πρέπει είτε να δηµιουργήσουµε υποκλάσεις του component που λαµβάνει το συµβάν είτε να δηµιουργήσουµε µία µεγάλη µέθοδο handleevent στο βασικό container. 9.2.2. Μοντέλο εξουσιοδότησης (JDK 1.1) Το JDK 1.1 εισήγαγε ένα νέο µοντέλο συµβάντων που ονοµάζεται το µοντέλο εξουσιοδότησης συµβάντων (delegation event model). Στο µοντέλο εξουσιοδότησης συµβάντων τα συµβάντα αποστέλλονται σε ένα component, αλλά εξαρτάται από το ίδιο το component να καταχωρήσει (register) µία ρουτίνα χειρισµού συµβάντων (που ονοµάζεται listener) για να λαµβάνει το συµβάν. Με τον τρόπο αυτό ο χειριστής συµβάντων µπορεί να βρίσκεται σε µία κλάση έξω από το component. Ο χειρισµός του συµβάντος εξουσιοδοτείται στη συνέχεια στην ξεχωριστή κλάση. 186
Τα συµβάντα είναι αντικείµενα που αναφέρονται µόνο στους καταχωρηµένους listeners. Κάθε συµβάν έχει µία αντίστοιχη κλάση listener (διεπαφή). Η κλάση που υλοποιεί µία διεπαφή listener ορίζει κάθε µέθοδο που µπορεί να λάβει ένα αντικείµενο Event. Για παράδειγµα, στη συνέχεια είναι ένα απλό Frame µε ένα µόνο Button πάνω του: import java.awt.*; import ButtonHandler; public class TestButton public static void main(string args[]) Frame f = new Frame( Test ); Button b = new Button( Press Me! ); b.addactionlistener(new ButtonHandler()); f.add( Center, b); f.pack(); f.setvisible(); Η κλάση ButtonHandler είναι η κλάση χειρισµού στην οποία θα εξουσιοδοτηθεί το συµβάν: import java.awt.event.*; public class ButtonHandler implements ActionListener public void actionperformed(actionevent e) System.out.println( Action occurred ); Η κλάση Button έχει µία µέθοδο addactionlistener(actionlistener) 187
Η διεπαφή ActionListener ορίζει µία µόνο µέθοδο, την actionperformed, η οποία θα λάβει ένα ActionEvent Όταν δηµιουργείται το αντικείµενο Button, το αντικείµενο µπορεί να καταχωρήσει ένα listener για ActionEvents µέσω της µεθόδου addactionlistener, καθορίζοντας την κλάση αντικειµένων που υλοποιεί τη διεπαφή ActionListener. Όταν κάνουµε κλικ µε το ποντίκι στο αντικείµενο Button, αποστέλλεται µία ActionEvent σε οποιονδήποτε ActionListener είναι καταχωρηµένος µέσω της µεθόδου actionperformed(actionevent) Υπάρχουν αρκετά πλεονεκτήµατα για την προσέγγιση αυτή: Τα συµβάντα δεν αντιµετωπίζονται τυχαία στο ιεραρχικό µοντέλο είναι πιθανό ένα συµβάν να προωθηθεί σε ένα container και να χειριστεί σε ένα επίπεδο όπου δεν αναµενόταν. Είναι δυνατό να δηµιουργήσουµε κλάσεις φίλτρα (adapter) για να κατηγοριοποιούµε τις πράξεις συµβάντων Το µοντέλο εξουσιοδότησης είναι πολύ καλύτερο για την κατανοµή της εργασίας ανάµεσα στις κλάσεις Το νέο µοντέλο συµβάντων παρέχει υποστήριξη για τα Java BeansTM Υπάρχουν επίσης ορισµένα ζητήµατα/µειονεκτήµατα στο µοντέλο τα οποία πρέπει να αναφέρουµε: Είναι περισσότερο σύνθετο, τουλάχιστον στην αρχή, στην κατανόησή του. Η µεταφορά κώδικα από JDK 1.0 σε JDK 1.1 δεν είναι εύκολη Αν και το τρέχον JDK υποστηρίζει το µοντέλο συµβάντων του JDK 1.0 επιπρόσθετα στο µοντέλο εξουσιοδότησης, τα δύο µοντέλα αντικειµένων (JDK 1.0 και JDK 1.1) δεν µπορούν να αναµιχθούν. 9.3. Συµπεριφορά των GUI στη Java 9.3.1. Κατηγορίες συµβάντων Ο γενικός µηχανισµός για τη λήψη συµβάντων από components έχει περιγραφεί για τα συµφραζόµενα ενός απλού τύπου συµβάντος. Στο πακέτο java.awt.event ορίζεται ένα πλήθος συµβάντων και components τρίτων κατασκευαστών προσθέτουν σε αυτή τη λίστα. Για κάθε κατηγορία συµβάντων υπάρχει µία διεπαφή η οποία πρέπει να οριστεί για κάθε κλάση η οποία επιθυµεί να λαµβάνει τα συµβάντα. Η διεπαφή αυτή απαιτεί τον ορισµό µίας ή περισσοτέρων µεθόδων. Οι µέθοδοι αυτοί θα καλούνται όταν προκύψουν συγκεκριµένα συµβάντα. Ο επόµενος πίνακας παρουσιάζει τις κατηγορίες δίνοντας το όνοµα της διεπαφής για κάθε µια από τις κατηγορίες, καθώς και τις απαιτούµενες µεθόδους. Τα ονόµατα των µεθόδων είναι µνηµονικά, δηλώνοντας τις συνθήκες που θα προκαλέσουν την κλήση της µεθόδου. 188
Κατηγορία Όνοµα διεπαφής Μέθοδοι Πράξη (action) ActionListener actionperformed(actionevent) Στοιχείο (item) ItemListener itemstatechanged(itemevent) Κίνηση ποντικιού MouseMotionListener mousedragged(mouseevent) mousemoved(mouseevent) Πλήκτρο ποντικιού MouseListener mousepressed(mouseevent) mousereleased(mouseevent) mouseentered(mouseevent) mouseexited(mouseevent) mouseclicked(mouseevent) Πλήκτρα (key) KeyListener keypressed(keyevent) keyreleased(keyevent) keytyped(keyevent) Εστίαση (Focus) FocusListener focusgained(focusevent) focuslost(focusevent) Adjustment AdjustmentListener adjustmentvaluechanged(adjustmente vent) Component ComponentListener componentmoved(componentevent) componenthidden(componentevent) componentresized(componentevent) componentshown(componentevent) Window WindowListener windowclosing(windowevent) windowopened(windowevent) windowiconified(windowevent) windowdeiconified(windowevent) windowclosed(windowevent) windowactivated(windowevent) windowdeactivated(windowevent) Container ContainerListener componentadded(containerevent) componentremoved(containerevent) Text TextListener textvaluechanged(textevent) 9.3.2. Ένα περισσότερο σύνθετο παράδειγµα Στη συνέχεια θα δούµε ένα περισσότερο σύνθετο παράδειγµα. Θα παρακολουθεί την κίνηση του ποντικιού όταν είναι πατηµένο το πλήκτρο το ποντικιού mouse dragging και επιπρόσθετα θα ανιχνεύει την κίνηση του ποντικιού. Τα συµβάντα που προκαλούνται από την κίνηση του ποντικιού όσο είναι πατηµένο το πλήκτρο µπορούν να λαµβάνονται από µία κλάση που υλοποιεί τη διεπαφή MouseMotionListener. Η διεπαφή απαιτεί να παρέχουµε δύο µεθόδους που είναι η mousedragged() και η mousemoved(). Ενδιαφερόµαστε µόνο για την κίνηση drag, αλλά παρ όλα αυτά πρέπει να παρέχουµε και τις δύο µεθόδους. Όµως το σώµα της mousemoved() µπορεί να είναι κενό. Για να λαµβάνουµε συµβάντα του ποντικιού, συµπεριλαµβανοµένου του κλικ του ποντικιού, χρειάζεται να υλοποιήσουµε τη διεπαφή MouseListener. Η διεπαφή αυτή περιλαµβάνει αρκετά συµβάντα, συµπεριλαµβανοµένων των mouseentered, mouseexited, mousepressed, mousereleased και mouseclicked. 189
Όταν λαµβάνει χώρα ένα από τα συµβάντα drag ή κίνηση του ποντικιού, θα αναφέρουµε τη σχετική πληροφορία σε ένα text field. Ο κώδικας είναι ο ακόλουθος: import java.awt.*; import java.awt.event.*; public class TwoListen implements MouseMotionListener, MouseListener private Frame f; private TextField tf; public static void main(string args[]) TwoListen two = new TwoListen(); two.go(); public void go() f = new Frame( Two listeners example ); f.add(new Label( Click and drag the mouse ), North ); tf = new TextField(30); f.add(tf, South ); f.addmousemotionlistener(this); f.addmouselistener(this); f.setsize(300,200); f.setvisible(true); // f.requestfocus(); // These are MouseMotionListener events public void mousedragged(mouseevent e) String s = Mouse dragging: X = + e.getx()+ Y = + e.gety(); tf.settext(s); public void mousemoved(mouseevent e) // These are MouseListener events public void mouseclicked(mouseevent e) public void mouseentered(mouseevent e) String s = The mouse entered ; tf.settext(s); public void mouseexited(mouseevent e) String s = The mouse has left the building ; tf.settext(s); public void mousepressed(mouseevent e) 190
public void mousereleased(mouseevent e) Θα πρέπει να τονίσουµε ορισµένα στοιχεία σχετικά µε το παράδειγµα. ήλωση πολλών διεπαφών Η κλάση δηλώνεται µε χρήση του ακόλουθου: implements MouseMotionListener, MouseListener Παρατηρήστε ότι µπορούµε να ορίσουµε πολλές διεπαφές χρησιµοποιώντας απλά το κόµµα ως διαχωριστή. Παρακολουθώντας πολλές πηγές Καλώντας τις επόµενες µεθόδους: f.addmousemotionlistener(this); f.addmouselistener(this); δύο είδη συµβάντων προκαλούν την κλήση µεθόδων στην κλάση TwoListen. Μπορείτε να παρακολουθείτε (listen) όσες πηγές θέλετε, τόσο από διαφορετικές κατηγορίες συµβάντων όσο και από διαφορετικές πηγές, όπως διαφορετικά frames ή διαφορετικά buttons. Λήψη λεπτοµερειών σχετικά µε το συµβάν Όταν καλούνται οι µέθοδοι χειρισµού, όπως η mousedragged(), λαµβάνουν ένα όρισµα που εν δυνάµει περιέχει σηµαντική πληροφορία σχετικά µε το αρχικό συµβάν. Για να καθορίσετε τις λεπτοµέρειες του ποια πληροφορία είναι διαθέσιµη για κάθε κατηγορία συµβάντων θα πρέπει να ελέγξετε την τεκµηρίωση της κατάλληλης κλάσης από το πακέτο java.awt.event. 9.3.3. Πολλοί listeners Το πλαίσιο λειτουργίας της παρακολούθησης (listening) των AWT συµβάντων επιτρέπει να προσκολληθούν πολλοί listeners στο ίδιο component. Γενικά, αν θέλετε να γράψετε ένα πρόγραµµα που πραγµατοποιεί πολλές πράξεις βασιζόµενο σε ένα µόνο συµβάν, πιθανότατα θα πρέπει να κωδικοποιήσετε αυτή τη συµπεριφορά στη µέθοδο χειρισµού. Όµως, ορισµένες φορές η σχεδίαση του προγράµµατος απαιτείπολλά µη σχετιζόµενα µεταξύ τους τµήµατα του ίδιου προγράµµατος να αντιδρούν στο ίδιο συµβάν. Αυτό µπορεί να συµβεί αν, για παράδειγµα, προσθέτουµε βοήθεια που εξαρτάται από τα συµφραζόµενα (context-sensitive help) σε ένα υπάρχον πρόγραµµα. Ο µηχανισµός του listener σάς επιτρέπει να καλείτε µία µέθοδο addlistener όσες φορές απαιτείται, καθορίζοντας όσους διαφορετικούς listener απαιτεί η σχεδίασή σας. Θα καλούνται οι µέθοδοι χειρισµού όλων των καταχωρηµένων listeners όταν λάβει χώρα το συµβάν. Σηµείωση Η σειρά µε την οποία θα καλούνται οι µέθοδοι χειρισµού δεν είναι καθορισµένη. Γενικά, αν έχει σηµασία η σειρά κλήσης, τότε οι χειριστές δεν είναι άσχετοι µεταξύ τους, και δε θα έπρεπε να χρησιµοποιείτε αυτή τη διευκόλυνση για την 191
κλήση τους. Αντ αυτού, καταχωρήστε µόνο τον πρώτο listener και βάλτε τον να καλεί άµεσα τους άλλους. 9.4. Προσαρµοστές συµβάντων Προφανώς απαιτείται πολλή δουλειά για να υλοποιήσουµε όλες τις µεθόδους κάθε µιας από τις διεπαφές Listener, συγκεκριµένα των MouseListener και ComponentListener. Για παράδειγµα η διεπαφή MouseListener ορίζει τις επόµενες µεθόδους: mouseclicked(mouseevent) mouseentered(mouseevent) mouseexited(mouseevent) mousepressed(mouseevent) mousereleased(mouseevent) Ως βοήθεια η Java προσφέρει µία κλάση προσαρµοστή (adapter class) για κάθε διεπαφή Listener, η οποία υλοποιεί την αντίστοιχη διεπαφή Listener, αλλά αφήνει κενές τις µεθόδους που υλοποιήθηκαν. Με τον τρόπο αυτό η ρουτίνα Listener που ορίζεται µπορεί να κάνει extend την κλάση Adapter και να υπερβαίνει µόνο τις µεθόδους που χρειαζόσαστε. Για παράδειγµα: import java.awt.*; import java.awt.event.*; public class MouseClickHandler extends MouseAdapter // We just need the mouseclick handler, so we use // the Adapter to avoid having to write all the // event handler methods public void mouseclicked (MouseEvent e) // Do stuff with the mouse click... 192
9.5 Ασκήσεις GUI Αριθµοµηχανής, Μέρος ΙΙ 1. Χρησιµοποιήστε τον κώδικα GUI που γράψατε στο προηγούµενο κεφάλαιο. 2. Γράψτε τον κώδικα συµβάντων που θα συνδέσει τη διεπαφή χρήστη της αριθµοµηχανής µε τους κατάλληλους χειριστές συµβάντων για τις πράξεις στην αριθµοµηχανή. GUI Λογαριασµών, Μέρος ΙΙ 1. Χρησιµοποιήστε το GUI για τους λογαριασµούς που γράψατε στο προηγούµενο Κεφάλαιο. 2. Τροποποιήστε τον κώδικα ώστε να αποστέλλετε ένα αντικείµενο AccountEvent, ως αποτέλεσµα µίας αλλαγής στο λογαριασµό, στο αντικείµενο BankManager, που είναι ένας καταχωρηµένος listener της διεπαφής AccountListener. 193
194
10. Η βιβλιοθήκη Components του AWT 10.1. ιευκολύνσεις του AWT Το AWT παρέχει ένα πλήθος προτύπων διευκολύνσεων (standard facilities). Το κεφάλαιο αυτό θα παρουσιάσει τα διαθέσιµα components και θα αναφέρει πιθανά προβλήµατα τα οποία θα πρέπει να γνωρίζετε. Αρχικά περιγράφονται τα components. Αυτά χρησιµοποιούνται για τη δηµιουργία διεπαφών χρήστη. Σαφώς πρέπει να γνωρίζετε όλο το σύνολο των UI components ώστε να µπορείτε να επιλέξετε τα κατάλληλα όταν φτιάχνετε τις δικές σας διεπαφές. Τα AWT components παρέχουν µηχανισµούς για έλεγχο της εµφάνισής τους, συγκεκριµένα σε ό,τι αφορά το χρώµα και τη γραµµατοσειρά που χρησιµοποιείται για την εµφάνιση κειµένου. Επιπρόσθετα το AWT υποστηρίζει εκτύπωση. Είναι µία διευκόλυνση που προστέθηκε µε τη µετάβαση στο JDK 1.1. 10.2. Buttons Έχουµε ήδη συναντήσει το component Button. Παρέχει ένα βασικό component τύπου «πίεσε για να ενεργοποιηθεί» για τη διεπαφή χρήστη. Μπορεί να δηµιουργηθεί µε µία ετικέτα κειµένου που λειτουργεί ως οδηγία για το χρήστη σχετικά µε τη χρήση του. Button b = new Button( Sample ); add(b); Για αντίδραση κατά την πίεση του button χρησιµοποιείται η διεπαφή ActionListener. Η µέθοδος getactioncommand() του ActionEvent που προκύπτει όταν πιέσουµε το button, επιστρέφει εξ ορισµού τη συµβολοσειρά της ετικέτας. Αυτό µπορεί να αλλάξει µε χρήση της εντολής setactioncommand() του Button. 10.3. Checkboxes Το checkbox παρέχει µία απλή συσκευή «on/off» µε µία ετικέτα κειµένου δίπλα της. Checkbox one = new Checkbox( One, false); Checkbox two = new Checkbox( Two, false); Checkbox three = new Checkbox( Three, true); add(one); add(two); 195
add(three); Η επιλογή ή απο-επιλογή ενός checkox γνωστοποιείται στη διεπαφή ItemInterface. Το ItemEvent που µεταβιβάζεται δηλώνει αν η πράξη ήταν επιλογή ή απο-επιλογή από ένα checkbox µε χρήση της µεθόδου getstatechange(). Η µέθοδος επιστρέφει µία από τις σταθερές ItemEvent.DESELECTED και ItemEvent.SELECTED, ανάλογα µε το τι είναι κατάλληλο. Η µέθοδος getitem() επιστρέφει ένα αντικείµενο String που αναπαριστά τη συµβολοσειρά ετικέτα του checkbox που επηρεάζεται. class Handler implements ItemListener public void itemstatechanged(itemevent ev) String state = deselected ; if (ev.getstatechange() == ItemEvent.SELECTED) state = selected ; System.out.println(ev.getItem() + + state); 10.4. CheckboxGroups Radio buttons Μπορείτε να δηµιουργείτε checkboxes µε µία ειδική συνάρτηση δηµιουργίας η οποία δέχεται ένα επιπλέον όρισµα, ένα CheckboxGroup. Αν το κάνετε αυτό, τότε αλλάζει η εµφάνιση των check boxes και όλα τα checkboxes που σχετίζονται µε το ίδιο checkbox group θα παρουσιάζουν συµπεριφορά radio button. CheckboxGroup cbg = new CheckboxGroup(); Checkbox one = new Checkbox( One, cbg, false); Checkbox two = new Checkbox( Two, cbg, false); Checkbox three = new Checkbox( Three, cbg, true); add(one); add(two); add(three); 196
10.5. Choice Η choice παρέχει µία απλή είσοδο τύπου «επιλογή ενός από µία λίστα». Choice c = new Choice(); c.additem( First ); c.additem( Second ); c.additem( Third ); add(c); Όταν κάνουµε κλικ πάνω στο choice, παρουσιάζει µία λίστα των στοιχείων που έχουν προστεθεί σε αυτό. Παρατηρήστε ότι τα στοιχεία προστίθενται ως αντικείµενα String. Η διεπαφή ItemListener χρησιµοποιείται για να παρατηρούµε τις αλλαγές στο choice. Οι λεπτοµέρειες είναι οι ίδιες όπως µε τα checkbox. 10.6. Canvas Ένα Canvas παρέχει ένα κενό (µε το χρώµα του φόντου) χώρο. Έχει επιθυµητό µέγεθος µηδέν επί µηδέν, συνεπώς στη γενική περίπτωση θα πρέπει να βεβαιωθείτε ότι ο layout manager θα του δώσει µη µηδενικό µέγεθος. Ο χώρος µπορεί να χρησιµοποιηθεί για ζωγραφική, ή για να γράψετε κείµενο, ή για να λαµβάνετε είσοδο από το πληκτρολόγιο ή το ποντίκι. Στη συνέχεια θα δούµε πώς µπορείτε να σχεδιάζετε αποτελεσµατικά στο AWT. Γενικά ο Canvas χρησιµοποιείται είτε ως έχει για να παρέχει ένα γενικευµένο χώρο σχεδίασης ή χρησιµοποιείται ως η βάση ανάπτυξης για να παρέχουµε µία περιοχή εργασίας για ένα custom component. Ο Canvas µπορεί να «ακούει» για όλα τα συµβάντα που εφαρµόζονται σε ένα γενικευµένο component. Συγκεκριµένα µπορεί να θέλετε να προσθέσετε αντικείµενα KeyListener, MouseMotionListener ή MouseListener για να του επιτρέψετε να αντιδρά µε συγκεκριµένο τρόπο στην είσοδο από το χρήστη. Οι µέθοδοι στις διεπαφές αυτές λαµβάνουν αντίστοιχα KeyEvent και MouseEvent. 197
Σηµείωση Για να λάβουµε συµβάντα πληκτρολογίου σε ένα canvas είναι απαραίτητο να καλέσουµε τη µέθοδο requestfocus() του canvas. Αν δεν γίνει αυτό, γενικά δε θα είναι δυνατό να «κατευθύνουµε» την πληκτρολόγηση στο Canvas. Αντίθετα η πληκτρολόγηση θα πηγαίνει σε άλλο component ή ακόµα µπορεί να χάνεται. Ο κώδικας που ακολουθεί παρουσιάζει ακριβώς αυτό. Ακολουθεί ένα παράδειγµα ενός Canvas: import java.awt.*; import java.awt.event.*; import java.util.*; public class MyCanvas implements KeyListener, MouseListener Canvas c; String s = ; public static void main(string args[]) Frame f = new Frame( Canvas ); MyCanvas mc = new MyCanvas(); mc.c = new Canvas(); f.add( Center, mc.c); f.setsize(150, 150); mc.c.addmouselistener(mc); mc.c.addkeylistener(mc); f.setvisible(true); public void keytyped (KeyEvent ev) System.out.println( keytyped ); s += ev.getkeychar(); // Not a good drawing technique!! c.getgraphics().drawstring(s, 0, 20); public void mouseclicked(mouseevent ev) 198
System.out.println( mouseclicked ); // force the focus onto the canvas c.requestfocus(); public void keypressed(keyevent ev) System.out.println( keypressed ); public void keyreleased(keyevent ev) System.out.println( keyreleased); public void mousepressed(mouseevent ev) System.out.println( mousepressed ); public void mousereleased(mouseevent ev) System.out.println( mousereleased); public void mouseentered(mouseevent ev) System.out.println( mouseentered ); public void mouseexited(mouseevent ev) System.out.println( mouseexited ); 10.7. Label Ένα αντικείµενο Label απλά παρουσιάζει µία γραµµή κειµένου. Το πρόγραµµα µπορεί να αλλάξει το κείµενο, αλλά όχι ο χρήστης. εν υπάρχουν ειδικά όρια ή άλλα είδη decoration για το διαχωρισµό ενός label. Label l = new Label( Hello ); add(l); Τα Labels συνήθως δεν αναµένεται ότι θα χειρίζονται συµβάντα, αλλά το κάνουν όπως και ο canvas. ηλαδή οι πληκτρολογήσεις µπορούν να «συλληφθούν» καλώντας τη requestfocus(). 199
10.8. TextField Το TextField είναι µία συσκευή εισόδου κειµένου σε µία γραµµή. TextField f = new TextField( Single line, 30); add(f); Επειδή µόνο µία γραµµή είναι δυνατή, µπορεί να ενηµερωθεί ένας ActionListener µέσω της actionperformed() σχετικά µε το πότε πατήθηκετο πλήκτρο Enter ή Return. Επίσης µπορούν να προστεθούν εφόσον είναι επιθυµητό και άλλο component listeners. Όπως και η text area, µπορεί να τεθεί για ανάγνωση µόνο. εν παρουσιάζει scrollbars σε οποιαδήποτε κατεύθυνση, αλλά µπορεί να κάνει scroll για µεγάλα κείµενα δεξιά ή αριστερά αν είναι απαραίτητο. 10.9. TextArea Η TextArea είναι µία συσκευή εισόδου κειµένου µε πολλές γραµµές και πολλές στήλες. Μπορείτε να την κάνετε µη editable µε χρήση της µεθόδου seteditable(boolean), οπότε γίνεται κατάλληλη για browsing κειµένου. Παρουσιάζει οριζόντιες και κάθετες scrollbars. TextArea t = new TextArea( Initial text, 4, 30); add(t); Μπορείτε να προσθέσετε γενικούς component listeners σε µία text area, αλλά επειδή το κείµενο είναι πολλών γραµµών, πιέζοντας το πλήκτρο Enter απλά τοποθετούµε ακόµα ένα χαρακτήρα στην ενδιάµεση µνήµη (buffer). Αν χρειάζεται να αναγνωρίζετε «ολοκλήρωση εισόδου» µπορείτε να τοποθετήσετε ένα button που δηλώνει Apply ή Commit δίπλα στην text area ώστε να επιτρέψετε στο χρήστη να το δηλώσει. 10.9.1. TextComponent Τόσο η text area όσο και το text field τεκµηριώνονται σε δύο σηµεία. Αν αναζητήσετε µία κλάση µε το όνοµα TextComponent θα βρείτε ότι πολλές από τις ουσιώδεις µεθόδους τεκµηριώνονται σε αυτή. Αυτό ισχύει γιατί τόσο η TextArea όσο 200
και η TextField είναι υποκλάσεις της TextComponent. Η ιδέα αυτή θα παρουσιαστεί στη συνέχεια. Έχετε δει ότι οι συναρτήσεις δηµιουργίας για αµφότερες τις κλάσεις TextArea και TextField σάς επιτρέπουν να καθορίζεται το πλήθος των στηλών για παρουσίαση. Να θυµάστε ότι το µέγεθος του παρουσιαζόµενου component είναι ευθύνη του layout manager, συνεπώς οι προτιµήσεις αυτές µπορεί να αγνοηθούν. Επιπρόσθετα, το πλήθος των στηλών διερµηνεύεται µε όρους του µέσου πλάτους των χαρακτήρων της γραµµατοσειράς που χρησιµοποιείται. Το πλήθος των χαρακτήρων που θα παρουσιαστούν τελικά µπορεί να διαφέρει σηµαντικά αν χρησιµοποιηθεί αναλογική γραµµατοσειρά (proportional spaced font). 10.10. List Μία List σάς επιτρέπει να παρουσιάζετε επιλογές ως κείµενο στο χρήστη, οι οποίες παρουσιάζονται σε µία περιοχή η οποία επιτρέπει την εµφάνιση πολλών στοιχείων ταυτόχρονα. Η List είναι scrollable και µπορεί να υποστηρίξει είτε απλή είτε πολλαπλή κατάσταση επιλογής. List l = new List(4, true); Το αριθµητικό όρισµα στη συνάρτηση δηµιουργίας δηλώνει το προτιµούµενο ύψος της λίστας των στοιχείων µε βάση το πλήθος των ορατών γραµµών. Όπως πάντα, να θυµάστε ότι αυτό µπορεί να αλλάξει από τον layout manager. Το boolean όρισµα δηλώνει αν η λίστα θα πρέπει να επιτρέπει ή όχι στο χρήστη να κάνει πολλαπλές επιλογές. Σε αµφότερες τις καταστάσεις επιλογής, απλή και πολλαπλή, παράγεται ένα ActionEvent, το οποίο λαµβάνει η διεπαφή ActionListener. Τα στοιχεία επιλέγονται από τη λίστα σύµφωνα µε τις συµβάσεις της πλατφόρµας. Για περιβάλλον Unix/Motif σηµαίνει ότι ένα απλό κλικ θα επιλέξει µία καταχώρηση στη λίστα, αλλά χρειάζεται διπλό κλικ για να ενεργοποιηθεί η πράξη στη λίστα. 201
10.11. Frame Πρόκειται για το γενικού σκοπού παράθυρο «πρώτου επιπέδου» (top-level). Έχει decorations τύπου window manager, όπως µπάρα τίτλου και χειριστήρια για αλλαγή µεγέθους. Frame f = new Frame( Frame ); Το µέγεθος ενός frame µπορεί να τεθεί µε χρήση της µεθόδου setsize(). Όµως, στη γενική περίπτωση θα πρέπει να τίθεται µε χρήση της µεθόδου pack(). Αυτή προκαλεί το layout manager να υπολογίσει ένα µέγεθος που θα περικλείει µε «ωραίο» τρόπο όλα τα components του frame. Το µέγεθος του frame τίθεται στη συνέχεια ανάλογα. Τα συµβάντα ενός frame µπορούν να παρακολουθούνται µε χρήση όλων των listeners που εφασµόζονται σε γενικά components. Μπορούµε να χρησιµοποιήσουµε το WindowListener, µέσω της µεθόδου windowclosing(), ώστε να αναγνωρίσουµε ότι έχει πιεστεί το quit button στο window manager menu. ε θα πρέπει να προσπαθείτε να «ακούσετε» τα συµβάντα του πληκτρολογίου, απ ευθείας από το frame. Αν και ορισµένες φορές λειτουργεί η τεχνική που αναφέρθηκε στα components canvas και label, µε την κλήση της requestfocus() δηλαδή, δεν είναι αξιόπιστη. Αν χρειάζεται να παρακολουθείτε τα συµβάντα πληκτρολόγησης θα πρέπει να προσθέσετε ένα canvas, ένα panel ή κάτι αντίστοιχο, στο frame και να προσθέσετε το listener στο εσωτερικό component. 10.12. Panel Πρόκειται για container γενικού σκοπού. εν µπορεί να χρησιµοποιηθεί αυτόνοµα, σε αντίθεση µε τα frames, τα windows και τα dialogs. Panel p = new Panel(); Από τη στιγµή που ένα Panel δεν µπορεί να παρουσιαστεί, δεν υπάρχει διαθέσιµο και screen shot του. Τα Panels µπορούν να χειρίζονται συµβάντα, µε την προϋπόθεση ότι πρέπει να αιτηθούν ρητά την εστίαση του πληκτρολογίου, όπως και µε το παράδειγµα του canvas που ήδη αναφέραµε. 10.13. Dialog Ένα Dialog είναι παρόµοιο µε ένα Frame, από την άποψη ότι πρόκειται για αυτόνοµο παράθυρο µε ορισµένα decorations. ιαφέρει από ένα frame από την 202
άποψη ότι έχει λιγότερα decorations και ότι µπορείτε να αιτηθείτε ένα modal dialog, οπότε αποτρέπει όλες τις µορφές εισόδου µέχρι να το κλείσετε. Dialog d = new Dialog(f, Dialog, false); d.add( Center, new Label( Hello, I m a Dialog )); d.pack(); Τα Dialogs στη γενική περίπτωση δεν είναι άµεσα ορατά στο χρήστη µόλις δηµιουργηθούν. Συνήθως «παρουσιάζονται» ως αντίδραση σε κάποια άλλη πράξη της διεπαφής χρήστη, όπως πίεση ενός button. public void actionperformed(actionevent ev) d.setvisible(true); Σηµείωση Θα πρέπει να χρησιµοποιείτε ένα Dialog ως επαναχρησιµοποιήσιµη συσκευή. ηλαδή, δε θα πρέπεινα καταστρέφετε ένα αντικείµενο όταν αυτό φεύγει από την οθόνη, αλλά να το κρατάτε για µεταγενέστερη χρήση. Η ύπαρξη του συλλογέα απορριµµάτων βοηθάει στο να «ξοδεύουµε» µνήµη, αλλά να θυµάστε ότι η δηµιουργία και η αρχικοποίηση αντικειµένων χρειάζεται χρόνο και δε θα πρέπει να γίνεται χωρίς να υπάρχει κάποιος λόγος. Για να κρύψετε ένα Dialog, πρέπει να καλέσετε πάνω του την setvisible(false). Αυτό συνήθως γίνεται από το WindowListener που είναι προσκολληµένος πάνω του και περιµένει την κλήση στη µέθοδο windowclosing() του συγκεκριµένου listener. Η διαδικασία αυτή είναι παράλληλη µε αυτή του κλεισίµατος ενός frame. 10.14. FileDialog Είναι µία υλοποίηση µίας συσκευής επιλογής αρχείων. Έχει το δικό του αυτόνοµο παράθυρο, µε decorations και επιτρέπει στο χρήστη να κάνει browse στο σύστηµα αρχείων και να επιλέξει ένα συγκεκριµένο αρχείο για περαιτέρω πράξεις. FileDialog d = new FileDialog(f, FileDialog ); d.setvisible(true); String fname = d.getfile(); 203
Εν γένει, δεν είναι απαραίτητο να χειρίζεστε συµβάντα από ένα FileDialog. Η κλήση στη setvisible(true) απενεργοποιείται µέχρι ο χρήστης να επιλέξει OK, οπότε και εσείς απλά ζητάτε το όνοµα του αρχείου που επιλέχθηκε. Αυτό επιστρέφεται ως String. 10.15. ScrollPane Αυτό παρέχει ένα γενικευµένο container που δεν µπορεί να χρησιµοποιηθεί αυτόνοµα. Παρέχει ένα viewport σε µία µεγαλύτερη περιοχή και scrollbars για τη διαχείριση αυτού του viewport. Frame f = new Frame( ScrollPane ); Panel p = new Panel(); ScrollPane sp = new ScrollPane(); p.setlayout(new GridLayout(3,4)); sp.add(p); f.add(sp); f.setsize(200, 200); f.setvisible(true); Το ScrollPane δηµιουργεί και χειρίζεται τα scroll bars όπως είναι απαραίτητο. «Κρατά» ένα µόνο component και δεν µπορείτε να επιλέξετε το layout manager που 204
χρησιµοποιεί. Αντ αυτού θα πρέπει να προσθέσετε στο ScrollPane ένα Panel, να διαχειριστείτε το layout manager του Panel και να τοποθετήσετε τα components µέσα στο συγκεκριµένο panel. Γενικά δε θα χειριστείτε συµβάντα σε ένα Scroll Pane, αλλά θα το κάνετε στα components που περιέχει. 10.16. Menus Τα menus είναι διαφορετικά από τα υπόλοιπα components σε σηµαντικό βαθµό. Γενικά δεν µπορείτε να προσθέτετε menus σε συνήθεις containers και να τα βάζετε να τα τοποθετεί ένας layout manager. Μπορείτε να προσθέσετε menus µόνο σε συγκεκριµένα πράγµατα που ονοµάζονται menus containers. Γενικά µπορείτε να ξεκινήσετε ένα «δένδρο» από menu µόνο τοποθετώντας σε ένα frame µία menu bar, µε χρήση της µεθόδου setmenubar(). Από το σηµείο αυτό, µπορείτε να προσθέτετε menus στη menu bar και στη συνέχεια να προσθέτετε menus ή menu items στα menus. Η µόνη εξαίρεση σε αυτό είναι ένα popup menu που µπορεί να προστεθεί σε οποιοδήποτε component αλλά βέβαια δεν υπάρχει στην περίπτωση αυτή ζήτηµα layout. 10.16.1. Το help menu Ένα συγκεκριµένο χαρακτηριστικό µίας menu bar είναι ότι µπορείτε να ορίσετε ένα συγκεκριµένο menu να είναι το Help menu. Αυτό γίνεται µε τη µέθοδο sethelpmenu(menu). Για να αντιµετωπισθεί ένα menu ως help menu πρέπει να έχει προστεθεί ως τέτοιο στη menu bar και στη συνέχεια θα αντιµετωπίζεται µε τον κατάλληλο τρόπο για ένα µενού βοηθείας στη συγκεκριµένη πλατφόρµα. Για παράδειγµα σε συστήµατα X/Motif, θα τοποθετηθεί στο δεξί άκρο της menu bar. 10.17. MenuBar Πρόκειται για ένα οριζόντιο µενού. Μπορεί να προστεθεί µόνο σε ένα αντικείµενο Frame και αποτελεί τη ρίζα για όλα τα δένδρα µενού. Frame f = new Frame( MenuBar ); MenuBar mb = new MenuBar(); f.setmenubar(mb); Η MenuBar δεν υποστηρίζει listeners. Αυτό ισχύει γιατί αναµένεται ότι όλα τα συµβάντα που λαµβάνουν χώρα µέσα στην περιοχή ενός menubar θα επεξεργάζονται αυτόµατα ως µέρος της συνήθους συµπεριφοράς του menu. 205
10.18. Menu Η κλάση Menu παρέχει το βασικό pull-down menu. Μπορεί να προστεθεί είτε σε µία MenuBar είτε σε ένα άλλο Menu. MenuBar mb = new MenuBar(); Menu m1 = new Menu( File ); Menu m2 = new Menu( Edit ); Menu m3 = new Menu( Help ); mb.add(m1); mb.add(m2); mb.add(m3); mb.sethelpmenu(m3); Σηµείωση Τα µενού που παρουσιάζονται εδώ είναι κενά, που εξηγεί την εµφάνιση του µενού File. Μπορείτε να προσθέσετε έναν ActionListener σε ένα αντικείµενο Menu, αλλά δε συνηθίζεται. Κανονικά, τα menu χρησιµοποιούνται µόνο για να παρουσιάζουν και να ελέγχουν τα menu items που θα δούµε στη συνέχεια. 10.19. MenuItem Τα MenuItems είναι κόµβοι-φύλλα ενός δένδρου menu, υπό µορφή κειµένου. Γενικά τα προσθέτουµε στο menu για ν συµπληρώσουµε την εικόνα. Menu m1 = new Menu( File ); MenuItem mi1 = new MenuItem( Save ); MenuItem mi2 = new MenuItem( Load ); MenuItem mi3 = new MenuItem( Quit ); m1.add(mi1); m1.add(mi2); m1.addseparator(); m1.add(mi3); 206
Συνήθως προσθέτετε ένα ActionListener σε ένα αντικείµενο MenuItem για να παρέχετε συµπεριφορά στα menus σας. 10.20. CheckboxMenuItem Είναι ένα clickable menu item, ώστε να µπορείτε να έχετε επιλογές on ή off µέσα στα menus. Menu m1 = new Menu( File ); MenuItem mi1 = new MenuItem( Save ); CheckboxMenuItem mi2 = new CheckboxMenuItem( Persistent ); m1.add(mi1); m1.add(mi2); Το CheckboxMenuItem πρέπει να παρακολουθείτε µέσω µίας διεπαφής ItemListener. Συνεπώς όταν αλλάζει η κατάσταση του checkbox θα καλείται η µέθοδος itemstatechanged(). 10.21. PopupMenu Παρέχει ένα αυτόνοµο µενού που µπορεί να γίνει pop-up σε οποιοδήποτε component. Σε ένα popup menu µπορείτε να προσθέσετε menu items ή menus. Frame f = new Frame( PopupMenu ); Button b = new Button( Press Me ); PopupMenu p = new PopupMenu( Popup ); MenuItem s = new MenuItem( Save ); MenuItem l = new MenuItem( Load ); b.addactionlistener(this); f.add( Center, b); p.add(s); p.add(l); f.add(p); 207
Σηµείωση Το PopumMenu πρέπει να προστίθεται σε ένα «γονικό» component. Αυτό δεν είναι ίδιο µε την πρόσθεση συνήθως components σε containers. Στο παράδειγµα αυτό το popup προστέθηκε στο frame που το περιέχει. Για να προκαλέσετε ένα PopupMenu να εµφανιστεί πρέπει να καλέσετε τη µέθοδο show. Η παρουσίαση απαιτεί µία αναφορά σε component να λειτουργήσει ως βάση για τις συντεταγµένες x και y. Συνήθως θα χρησιµοποιείτε για το λόγο αυτό το component που προκάλεσε την παρουσίαση. Στην περίπτωσή µας το button b. public void actionperformed(actionevent ev) p.show(b, 10, 10); Σηµείωση Το αρχικό component πρέπει να βρίσκεται κάτω από ή να περιέχεται σε το γονικό component στην ιεραρχία του περιέρχεσθαι. 10.22. Έλεγχος των οπτικών απόψεων Μπορείτε να ελέγξετε την εµφάνιση των AWT components από άποψης του χρώµατος που χρησιµοποιείται σε foreground και background και της γραµµατοσειράς που χρησιµοποιείται για το κείµενο. 10.22.1. Χρώµατα Υπάρχουν δύο µέθοδοι για να θέσετε το χρώµα σε ένα component, και είναι: setforeground() setbackground() Αµφότερες οι µέθοδοι λαµβάνουν ένα όρισµα που είναι στιγµιότυπο της κλάσης java.awt.color. Μπορείτε να χρησιµοποιήσετε σταθερές χρωµάτων οι οποίες αναφέρονται ως Color.red, Color.blue κλπ. Το πλήρες διαθέσιµο εύρος των προκαθορισµένων χρωµάτων βρίσκεται στη σελίδα τεκµηρίωσης για την κλάση Color. Επιπρόσθετα µπορείτε να δηµιουργήσετε ένα συγκεκριµένο χρώµα ως εξής: int r = 255, g = 255, b = 0; Color c = new Color(r, g, b); Μία τέτοια συνάρτηση δηµιουργίας δηµιουργεί ένα χρώµα µε βάση τις καθορισµένες εντάσεις των χρωµάτων κόκκινο (r), πράσινο (g) και µπλε (b), µε βάση ένα εύρος από 0 έως 255 για το κάθε ένα. 10.22.2. Γραµµατοσειρές Η γραµµατοσειρά που χρησιµοποιείται για την παρουσίαση του κειµένου σε ένα component καθορίζεται µε χρήση της µεθόδου setfont(). Το όρισµα της µεθόδου αυτής είναι ένα στιγµιότυπο της κλάσης java.awt.font. εν υπάρχουν ορισµένες σταθερές για τις γραµµατοσειρές, αλλά µπορείτε να δηµιουργήσετε µία γραµµατοσειρά καθορίζοντας το όνοµα της γραµµατοσειράς, το στυλ και το µέγεθος σε στιγµές. Font f = new Font( TimesRoman, Font.PLAIN, 14); 208
Τα επιτρεπτά ονόµατα γραµµατοσειρών περιλαµβάνουν: Dialog Helvetica TimesRoman προσοχή χωρίς κενό ενδιάµεσα Courier Μία πλήρης λίστα µπορεί να καθοριστεί καλώντας τη µέθοδο getfontlist() σε ένα αντικείµενο Toolkit. Το toolkit µπορεί να ληφθεί από το component, αφού αυτό έχει παρουσιαστεί, µε χρήση της µεθόδου gettoolkit(). Εναλλακτικά, µπορείτε να χρησιµοποιήσετε το εξ ορισµού toolkit, το οποίο µπορείτε να βρείτε µε κλήση της µεθόδου Toolkit.getDefaultToolkit(). Οι σταθερές για τα στυλ των γραµµατοσειρών, που στην πράξη είναι τιµές τύπου int είναι µία από: Font.BOLD Fond.ITALIC Font.PLAIN Font.BOLD + Font.ITALIC Τα µεγέθη σε στιγµές πρέπει να καθορίζονται µε χρήση µίας τιµής int. 10.23. Εκτύπωση Η εκτύπωση αντιµετωπίζεται στη Java 1.1 µε τρόπο στενά συνδεδεµένο µε την παρουσίαση στην οθόνη. Λαµβάνεται ένα ειδικό είδος αντικειµένου java.awt.graphics ώστε όλες οι εντολές σχεδίασης (draw) που αποστέλλονται σε αυτό το graphics να αποστέλλονται στην πράξη στον εκτυπωτή. Το σύστηµα εκτύπωσης της Java 1.1 επιτρέπει τη χρήση των τοπικών συνθηκών ελέγχου του εκτυπωτή, ώστε ο χρήστης να βλέπει ένα dialog box επιλογής εκτυπωτή όταν ξεκινάτε µία πράξη εκτύπωσης. Ο χρήστης στη συνέχεια µπορεί να διαλέξει επιλογές όπως µέγεθος χαρτιού, ποιότητα εκτύπωσηςκαι εκτυπωτή προς χρήση: Frame f = new Frame( Print test ); Toolkit t = f.gettoolkit(); PrintJob job = t.getprintjob(f, MyPrintJob, null); Graphics g = job.getgraphics(); Οι γραµµές αυτές δηµιουργούν ένα Graphics που «συνδέεται» µε τον εκτυπωτή που θα επιλέξει ο χρήστης. Λαµβάνει ένα καινούριο graphics για κάθε σελίδα. f.printall(g); Για να γράψετε στον εκτυπωτή µπορείτε να χρησιµοποιήσετε οποιαδήποτε από τις µεθόδους σχεδίασης της κλάσης graphics. Εναλλακτικά, όπως παρουσιάστηκε εδώ, µπορείτε απλά να ζητήσετε από ένα component να «σχεδιάσει τον εαυτό του» στα graphics. Η µέθοδος print() ζητά από ένα component να σχεδιάσει τον εαυτό του µε τον τρόπο αυτό, αλλά σχετίζεται µόνο µε το component για το οποίο κλήθηκε. Στην περίπτωση ενός container, όπως εδώ, µπορούµε να χρησιµοποιήσουµε τη µέθοδο printall() για να αναγκάσουµε τον container και όλα τα components που περιέχει να σχεδιαστούν στον εκτυπωτή. 209
g.dispose(); job.end(); Αφού έχετε δηµιουργήσει µία σελίδα εξόδου όπως θέλετε, χρησιµοποιείτε τη µέθοδο dispose() για να αναγκάσετε τη σελίδα να σταλεί στον εκτυπωτή. Όταν έχετε τελειώσει την εργασία εκτύπωσης (job), καλείται τη µέθοδο end() στο αντικείµενο PrintJob. Αυτό δηλώνει ότι η εργασία εκτύπωσης (print job) έχε ολοκληρωθεί και επιτρέπει στο σύστηµα ετεροχρονισµού εκτύπωσης (print spooling) να εκτελέσει την εργασία και στη συνέχεια να απελευθερώσει τον εκτυπωτή για άλλες εργασίες εκτύπωσης. 210
10.24. Λυµένες Ασκήσεις 10.24.1. Γράψτε ένα πρόγραµµα το οποίο να δηµιουργεί ένα πλαίσιο µε τίτλο Eisagogi keimenou. Τοποθετήστε µέσα την ετικέτα Enter your Name και δίπλα ένα πεδίο κειµένου µήκους 40 χαρακτήρων. Εισάγεται το όνοµά σας. Λύση: import javax.swing.*; import java.awt.*; public class Keimeno extends JFrame public Keimeno() super("eisagwgi Keimenou"); setdefaultcloseoperation(jframe.exit_on_close); setvisible(true); JLabel etiketa = new JLabel("Enter your Name: ",JLabel.RIGHT); JTextField onoma = new JTextField(40); Container pane = getcontentpane(); FlowLayout flo = new FlowLayout(); pane.setlayout(flo); pane.add(etiketa); pane.add(onoma); setcontentpane(pane); pack(); public static void main(string[] args) Keimeno a = new Keimeno(); Η εκτέλεση του παραπάνω προγράµµατος (javac Keimeno.java και java Keimeno) εµφανίζει το ακόλουθο πλαίσιο: 211
Και µε εισαγωγή του ονόµατος: 10.24.2. Γράψτε ένα πρόγραµµα το οποίο να δηµιουργεί ένα πλαίσιο µε τίτλο Lista gia psonia. Τοποθετήστε µέσα πέντε στοιχεία JCheckBox µε ετικέτες Psomi, Gala, Tyri, Laxanika και Kreas. Εκτελέστε το πρόγραµµα και χρησιµοποιώντας το ποντίκι δοκιµάστε να µαρκάρετε τα διάφορα είδη. Λύση: import javax.swing.*; import java.awt.*; public class Psonia extends JFrame public Psonia() super("lista gia Psonia"); setdefaultcloseoperation(jframe.exit_on_close); setvisible(true); JCheckBox psomi = new JCheckBox("Psomi"); JCheckBox gala = new JCheckBox("Gala"); JCheckBox turi = new JCheckBox("Turi"); JCheckBox lahanika = new JCheckBox("Laxanika"); JCheckBox kreas = new JCheckBox("Kreas"); Container pane = getcontentpane(); FlowLayout flo = new FlowLayout(); pane.setlayout(flo); pane.add(psomi); pane.add(gala); pane.add(turi); pane.add(lahanika); pane.add(kreas); setcontentpane(pane); pack(); public static void main(string[] args) Psonia a = new Psonia(); 212
Η εκτέλεση του παραπάνω προγράµµατος (javac Psonia.java και java Psonia) εµφανίζει το ακόλουθο πλαίσιο: 10.24.3. Γράψτε ένα πρόγραµµα το οποίο όπως και στην προηγούµενη άσκηση να δηµιουργεί ένα πλαίσιο µε τίτλο Lista gia psonia. Τα πέντε στοιχεία της λίστας να εµφανίζονται ως αναδυόµενη λίστα επιλογών. Χρησιµοποιείστε το στοιχείο JComboBox. Λύση: import javax.swing.*; import java.awt.*; public class ListaPsonia extends JFrame public ListaPsonia() super("lista gia Psonia"); setdefaultcloseoperation(jframe.exit_on_close); setvisible(true); JComboBox lista = new JComboBox(); lista.additem("psomi"); lista.additem("gala"); lista.additem("turi"); lista.additem("laxanika"); lista.additem("kreas"); Container pane = getcontentpane(); FlowLayout flo = new FlowLayout(); pane.setlayout(flo); pane.add(lista); setcontentpane(pane); pack(); public static void main(string[] args) ListaPsonia a = new ListaPsonia(); 213
Η εκτέλεση του παραπάνω προγράµµατος (javac ListaPsonia.java και java ListaPsonia) εµφανίζει το ακόλουθο πλαίσιο: 10.24.4. Γράψτε ένα πρόγραµµα το οποίο να εµφανίζει το επόµενο πλαίσιο: Λύση: import javax.swing.*; import java.awt.*; public class PanelCtoF extends JFrame public PanelCtoF() super("celsius to Fahreneit"); setdefaultcloseoperation(jframe.exit_on_close); setvisible(true); JPanel row1 = new JPanel(); JLabel tempc_label = new JLabel("Enter Temperature in Celsius: ", JLabel.RIGHT); JTextField tempc = new JTextField(15); JPanel row2 = new JPanel(); JButton calculate = new JButton("Calculate"); JButton reset = new JButton("Reset Values"); JPanel row3 = new JPanel(); JLabel tempf_label = new JLabel("Temperature in Farehneit: ", JLabel.RIGHT); JTextField tempf = new JTextField(15); Container pane = getcontentpane(); 214
GridLayout layout = new GridLayout(); pane.setlayout(layout); FlowLayout layout1 = new FlowLayout(); row1.setlayout(layout1); row1.add(tempc_label); row1.add(tempc); pane.add(row1); FlowLayout layout2 = new FlowLayout(); row2.setlayout(layout2); row2.add(calculate); row2.add(reset); pane.add(row2); FlowLayout layout3 = new FlowLayout(); row3.setlayout(layout3); row3.add(tempf_label); row3.add(tempf); pane.add(row3); setcontentpane(pane); pack(); public static void main(string[] args) PanelCtoF a = new PanelCtoF(); 10.24.5. Γράψτε ένα πρόγραµµα το οποίο να δηµιουργεί το επόµενο πλαίσιο: Στο πάνω πεδίο κειµένου να εισάγεται µια θερµοκρασία σε βαθµούς Celsius. Στη συνέχεια πατώντας το κουµπί Calculate να υπολογίζετε και να τυπώνετε στο κάτω πεδίο κειµένου η θερµοκρασία σε βαθµούς Fahrenheit. Το κουµπί Reset Values να σβήνει τις τρέχουσες τιµές στα δύο πεδία κειµένου. Λύση: import javax.swing.*; import java.awt.*; import java.awt.event.*; 215
public class CtoF extends JFrame implements ActionListener JPanel row1 = new JPanel(); JLabel tempc_label = new JLabel("Enter Temperature in Celsius: ", JLabel.RIGHT); JTextField tempc = new JTextField(15); JPanel row2 = new JPanel(); JButton calculate = new JButton("Calculate"); JButton reset = new JButton("Reset Values"); JPanel row3 = new JPanel(); JLabel tempf_label = new JLabel("Temperature in Farehneit: ", JLabel.RIGHT); JTextField tempf = new JTextField(15); public CtoF() super("celsius to Fahreneit"); setdefaultcloseoperation(jframe.exit_on_close); setvisible(true); Container pane = getcontentpane(); GridLayout layout = new GridLayout(); pane.setlayout(layout); FlowLayout layout1 = new FlowLayout(); row1.setlayout(layout1); row1.add(tempc_label); row1.add(tempc); pane.add(row1); FlowLayout layout2 = new FlowLayout(); row2.setlayout(layout2); row2.add(calculate); row2.add(reset); pane.add(row2); FlowLayout layout3 = new FlowLayout(); row3.setlayout(layout3); row3.add(tempf_label); row3.add(tempf); pane.add(row3); setcontentpane(pane); pack(); calculate.addactionlistener(this); reset.addactionlistener(this); 216
public void actionperformed(actionevent evt) Object source = evt.getsource(); if(source==calculate) Object celsius = tempc.gettext(); String celsius_value = celsius.tostring(); double cel = Double.parseDouble(celsius_value); double far = (1.8*cel+32.); tempf.settext("" + far); if(source==reset) tempc.settext(null); tempf.settext(null); public static void main(string[] args) CtoF a = new CtoF(); Η εκτέλεση του παραπάνω προγράµµατος (javac CtoF.java και java CtoF) εµφανίζει το ακόλουθο πλαίσιο. Στο παρακάτω πλαίσιο εµφανίζεται επίσης ένας υπολογισµός. 10.24.6. Γράψτε ένα πρόγραµµα το οποίο να δηµιουργεί το επόµενο πλαίσιο: 217
Στο πάνω πεδίο κειµένου να εισάγεται ένας αριθµός. Στη συνέχεια πατώντας το κουµπί Calculate να υπολογίζονται και να τυπώνονται στα αντίστοιχα πεδία κειµένου η τετραγωνική ρίζα του αριθµού καθώς και η δεύτερη και τρίτη δύναµη του. Το κουµπί Reset Values να σβήνει τις τρέχουσες τιµές στα όλα τα πεδία κειµένου. Λύση: import javax.swing.*; import java.awt.*; import java.awt.event.*; public class Dunameis extends JFrame implements ActionListener JPanel row1 = new JPanel(); JLabel xlabel = new JLabel("Enter Numper x: ", JLabel.RIGHT); JTextField x = new JTextField(15); JPanel row2 = new JPanel(); JButton calculate = new JButton("Calculate"); JButton reset = new JButton("Reset Values"); JPanel row3 = new JPanel(); JLabel sqrtlabel = new JLabel("Sqrt(x): ", JLabel.RIGHT); JTextField sqrt = new JTextField(15); JPanel row4 = new JPanel(); JLabel x2label = new JLabel("x^2: ", JLabel.RIGHT); JTextField x2 = new JTextField(15); JPanel row5 = new JPanel(); JLabel x3label = new JLabel("x^3: ", JLabel.RIGHT); JTextField x3 = new JTextField(15); public Dunameis() super("power of x"); setdefaultcloseoperation(jframe.exit_on_close); setvisible(true); Container pane = getcontentpane(); GridLayout layout = new GridLayout(5,1); pane.setlayout(layout); FlowLayout layout1 = new FlowLayout(); row1.setlayout(layout1); row1.add(xlabel); row1.add(x); pane.add(row1); FlowLayout layout2 = new FlowLayout(); row2.setlayout(layout2); 218
row2.add(calculate); row2.add(reset); pane.add(row2); FlowLayout layout3 = new FlowLayout(); row3.setlayout(layout3); row3.add(sqrtlabel); row3.add(sqrt); pane.add(row3); FlowLayout layout4 = new FlowLayout(); row4.setlayout(layout4); row4.add(x2label); row4.add(x2); pane.add(row4); FlowLayout layout5 = new FlowLayout(); row5.setlayout(layout5); row5.add(x3label); row5.add(x3); pane.add(row5); setcontentpane(pane); pack(); calculate.addactionlistener(this); reset.addactionlistener(this); public void actionperformed(actionevent evt) Object source = evt.getsource(); if(source==calculate) Object xvalue = x.gettext(); String value = xvalue.tostring(); double x = Double.parseDouble(value); if(x>=0) sqrt.settext("" + Math.sqrt(x)); else sqrt.settext("(" + Math.sqrt(-x) + ")i"); x2.settext("" + x * x); x3.settext("" + x * x * x); if(source==reset) x.settext(null); sqrt.settext(null); x2.settext(null); x3.settext(null); public static void main(string[] args) 219
Dunameis a = new Dunameis(); Η εκτέλεση του παραπάνω προγράµµατος (javac Dinameis.java και java Dinameis) εµφανίζει το ζητούµενο πλαίσιο. Εκτελέστε µόνοι σας µερικά παραδείγµατα. 10.24.7. Γράψτε ένα πρόγραµµα το οποίο να δηµιουργεί το επόµενο πλαίσιο ενός απλού calculator. Στο πεδίο κειµένου να εισάγεται ένας αριθµός. Στη συνέχεια επιλέγουµε την πράξη πατώντας ένα από τα κουµπιά στη δεύτερη γραµµή. Κατόπιν εισάγουµε τον δεύτερο αριθµό στο πεδίο κειµένου και πατώντας το Enter εµφανίζεται το αποτέλεσµα. Το κουµπί Clear να σβήνει την τρέχουσα τιµή στο πεδίο κειµένου. Λύση: import javax.swing.*; import java.awt.*; import java.awt.event.*; public class Calculator extends JFrame implements ActionListener JPanel row1 = new JPanel(); JLabel displaylabel = new JLabel("Display: ", JLabel.RIGHT); JTextField display = new JTextField(20); JPanel row2 = new JPanel(); JButton add = new JButton("+"); JButton substract = new JButton("-"); JButton multiply = new JButton("*"); JButton divide = new JButton("/"); JPanel row3 = new JPanel(); JButton enter = new JButton("Enter"); JButton clear = new JButton("Clear"); double x=0; double y=0; int select=0; 220
public Calculator() super("calculator"); setdefaultcloseoperation(jframe.exit_on_close); setvisible(true); Container pane = getcontentpane(); GridLayout layout = new GridLayout(3,1); pane.setlayout(layout); FlowLayout layout1 = new FlowLayout(); row1.setlayout(layout1); row1.add(displaylabel); row1.add(display); pane.add(row1); FlowLayout layout2 = new FlowLayout(); row2.setlayout(layout2); row2.add(add); row2.add(substract); row2.add(multiply); row2.add(divide); pane.add(row2); FlowLayout layout3 = new FlowLayout(); row3.setlayout(layout3); row3.add(enter); row3.add(clear); pane.add(row3); setcontentpane(pane); pack(); add.addactionlistener(this); substract.addactionlistener(this); multiply.addactionlistener(this); divide.addactionlistener(this); enter.addactionlistener(this); clear.addactionlistener(this); public void actionperformed(actionevent evt) Object source = evt.getsource(); if(source==add source==substract source==multiply source==divide) Object xobject = display.gettext(); String xvalue = xobject.tostring(); x = Double.parseDouble(value); display.settext(null); if(source==add) select = 1; 221
if(source==substract) select = 2; if(source==multiply) select = 3; if(source==divide) select = 4; if(source==enter) Object yobject = display.gettreelock(); String yvalue = yobject.tostring(); y = Double.parseDouble(yvalue); switch(select) case1: display.settext("" + (x+y)); break; case2: display.settext("" + (x-y)); break; case3: display.settext("" + (x*y)); break; case4: display.settext("" + (x/y)); break; if(source==clear) display.settext(null); public static void main(string[] args) Calculator a = new Calculator(); Η εκτέλεση του παραπάνω προγράµµατος (javac Calculator.java και java Calculator) εµφανίζει το ζητούµενο πλαίσιο. Εκτελέστε µόνοι σας µερικές απλές πράξεις. 10.25. Ασκήσεις ηµιουργήστε το ακόλουθο layout 1. Με χρήση του πακέτου java.awt δηµιουργήστε µία εφαρµογή Java που θα παρουσιάζει ένα απλό frame. 2. Επαναλάβεται την άσκηση 1 προσθέτοντας στο frame σας µια λίστα η οποία θα περιέχει τις εξής επιλογές: Άννα, Μαρία, Θεοδοσία. 3. ηµιουργείστε ένα πλαίσιο το οποίο θα έχει µια λίστα από µάρκες αυτοκινήτων. 222
4. Επαναλάβετε την ποιο πάνο άσκηση εµ τη διαφορά ότι στο frame θα εµφανίζεται η µάρκα αυτοκινήτου που διάλεξε ο χρήστης σε µία ετικέτα. 5. Με χρήση του πακέτου java.awt δηµιουργήστε µία εφαρµογή Java που θα παρουσιάζει τα ακόλουθα components. 6. Χρησιµοποιήστε για το Help menu ένα dialog. Πρόγραµµα ζωγραφικής 7. ηµιουργήστε το layout που φαίνεται πιο πάνω και γράψτε ένα απλό πρόγραµµα ζωγραφικής που θα χρησιµοποιεί το GUI. 8. Χρησιµοποιήστε το java.awt.printjob για να εκτυπώσετε το γραφικό που θα σχεδιαστεί όταν επιλεγεί το µενού Print. 223
224
11. Εισαγωγή στα Java Applets 11.1. Τι είναι ένα applet Ένα applet είναι ένα κοµµάτι κώδικα Java που εκτελείται σε ένα περιβάλλον πλοηγού (browser). ιαφέρει από µία εφαρµογή στον τρόπο µε τον οποίο εκτελείται. Μία εφαρµογή ξεκινά όταν καλείται η µέθοδος main(). Αντίθετα ο κύκλος ζωής ενός applet είναι µάλλον περισσότερο πολύπλοκος. Το κεφάλαιο αυτό θα ασχοληθεί µε το πώς εκτελείται ένα applet, πώς να το φορτώσετε στο browser και πώς να γράψετε κώδικα για ένα applet. 11.1.1. Φόρτωµα ενός applet Επειδή ένα applet εκτελείται σε ένα Web browser δεν ξεκινά άµεσα πληκτρολογώντας µία εντολή. Αντ αυτού πρέπει να δηµιουργήσετε ένα αρχείο HTML που θα λέει στον browser τι να φορτώσει και πώς να το εκτελέσει. Στη συνέχεια «δείχνετε» στο browser στο URL που καθορίζει το αρχείο HTML. Θα δούµε σε λίγο πώς πρέπει να είναι η µορφή ενός αρχείου HTML. 11.1.2. Περιορισµοί ασφαλείας σε ένα applet Επειδή τα applets είναι κοµµάτια κώδικα που φορτώνεται πάνω από το δίκτυο, αναπαριστούν µία εγγενώς επικίνδυνη κατάσταση. Τι θα συµβεί αν κάποιος γράψει ένα «κακό» πρόγραµµα που διαβάζει το αρχείο µε τους κωδικούς σας και το αποστέλλει µέσω internet; Ο τρόπος µε τον οποίο η Java αποτρέπει κάτι τέτοιο είναι παρέχοντας µία κλάση SecurityManager που ελέγχει την πρόσβαση σε σχεδόν κάθε κλήση επιπέδου συστήµατος στη Java Virtual Machine. Το µοντέλο αυτό ονοµάζεται µοντέλο ασφαλείας sandbox η JVM παρέχει ένα sandbox που επιτρέπει στα applets να «παίζουν» όµορφα, αλλά απαγορεύεται να βγουν έξω από το sandbox τους. Το βάθος στο οποίο ελέγχεται η ασφάλεια υλοποιείται σε επίπεδο browser. Οι περισσότεροι browsers (συµπεριλαµβανοµένου του Netscape) αποτρέπουν τα ακόλουθα: Εκτέλεση άλλου προγράµµατος κατά το χρόνο εκτέλεσης Οποιαδήποτε είσοδο/έξοδο σε αρχείο Κλήσεις σε οποιαδήποτε ιθαγενή µέθοδο (native method) Προσπάθειες να ανοίξει ένα socket σε οποιαδήποτε άλλη διεργασία που δε βρίσκεται στον υπολογιστή που παρείχε το applet. 11.1.3. Παράδειγµα Hello World! Ο πηγαίος κώδικας για το applet HelloWorld (στο πηγαίο αρχείο HelloWorld.java) παρουσιάζεται στη συνέχεια: // Sample HelloWorld applet import java.awt.graphics; import java.applet.applet; public class HelloWorld extends Applet 225
String hw_text; public void init() hw_text = Hello World ; public void paint(graphics g) g.drawstring(hw_text, 25, 25); 11.2. Συγγραφή ενός applet Για να γράψετε ένα applet θα πρέπει να δηµιουργήσετε µία κλάση που χρησιµοποιεί αυτή τη µορφή: import java.applet.*; public class HelloWorld extends Applet Για να είναι µία κλάση applet, θα πρέπει να είναι δηµόσια, και έτσι το όνοµα του αρχείου θα πρέπει να ταιριάζει µε το όνοµα της κλάσης του applet στην περίπτωση αυτή HelloWorld.java. Επιπρόσθετα, η κλάση θα πρέπει να είναι υποκλάση της κλάσης java.applet.applet. 11.2.1. Ιεραρχία κλάσης Applet Στην πράξη η κλάση java.applet.applet είναι υποκλάση της java.awt.container. Είναι χρήσιµο να έχετε µία συνολική αίσθηση της ιεραρχίας των κλάσεων Applet και AWT. Η ιεραρχία δείχνει ότι ένα applet µπορεί να χρησιµοποιηθεί άµεσα ως αρχικό σηµείο για ένα AWT layout. Επειδή το applet είναι panel, έχει εξ ορισµού flow layout manager. 226
11.2.2. Βασικές µέθοδοι Applet Σε κάθε εφαρµογή το πρόγραµµα ξεκινά από τη µέθοδο main(). Σε ένα applet όµως δεν ισχύει το ίδιο. Ο πρώτος κώδικας που εκτελεί ένα applet είναι αυτός που έχει οριστεί για την αρχικοποίησή του και η συνάρτηση δηµιουργίας του. Αφού ολοκληρωθεί η συνάρτηση δηµιουργίας, ο browser καλεί µία µέθοδο του applet που ονοµάζεται init(). Η µέθοδος init() θα πρέπει να κάνει βασική αρχικοποίηση του applet και στη συνέχεια να ολοκληρωθεί. Αφού ολοκληρωθεί η init() ο browser καλεί µία άλλη µέθοδο που ονοµάζεται start(). Θα δούµε τη start() πιο αναλυτικά στη συνέχεια. Αµφότερες οι µέθοδο init() και start() εκτελούνται µέχρι να ολοκληρωθούν πριν «ζωντανέψει» το applet και εξ αιτίας αυτού δεν µπορούν να χρησιµοποιηθούν για να προγραµµατίσουµε συµπεριφορά που θα εµφανιστεί στη συνέχεια στο applet. Στην πράξη, σε αντίθεση µε τη main() σε µία απλή εφαρµογή, δεν υπάρχει µία µέθοδος που να εκτελείται συνεχώς σε ολόκληρη τη ζωή του applet. Θα δούµε στη συνέχεια πώς αυτό επιτυγχάνεται µε χρήση νηµάτων, αλλά προς το παρόν, δεν µπορούµε να το κάνουµε. 11.2.3. Εµφάνιση ενός Applet Ουσιαστικά τα applets είναι γραφικά από τη φύση τους, συνεπώς αν και µπορείτε να κάνετε κλήσεις στην System.out.println(), κανονικά δε θα έπρεπε. Αντ αυτού θέλετε να δηµιουργήσετε µία παρουσίαση (display) σε ένα γραφικό περιβάλλον. Μπορείτε να σχεδιάζεται (draw) στην παρουσίαση ενός applet δηµιουργώντας µία µέθοδο paint(). Η µέθοδος paint() καλείται από το περιβάλλον του browser όποτε υπάρχει ανάγκη ανανέωσης της παρουσίασης του applet. Αυτό για παράδειγµα συµβαίνει όταν το παράθυρο του browser αποκτά ξανά το αρχικό του µέγεθος, µετά από κάποια ελαχιστοποίησή του. Η µέθοδος paint() θα πρέπει να γραφεί έτσι ώστε να λειτουργεί σωστά οποτεδήποτε µπορεί να κληθεί. Αυτό ισχύει γιατί η έκθεση εξαρτάται από το περιβάλλον, και όχι από το πρόγραµµα και συνεπώς συµβαίνει ασύγχρονα. 11.2.4. Η µέθοδος paint() και τα Graphics Η µέθοδος paint() λαµβάνει ένα όρισµα, το οποίο είναι στιγµιότυπο της κλάσης java.awt.graphics. Μπορείτε να το χρησιµοποιήσετε για να σχεδιάσετε ή να γράψετε κείµενο µέσα στο applet σας. Αυτό που ακολουθεί είναι ένα ελάχιστο παράδειγµα ενός applet, η οποία χρησιµοποιεί τη µέθοδο paint() για να γράψει κείµενο. import java.awt.*; import java.applet.applet; public class HelloWorld extends Applet public void paint(graphics g) g.drawstring( Hello World!, 25, 25); Σηµείωση Τα αριθµητικά ορίσµατα στη µέθοδο drawstring είναι οι συντεταγµένες x και y σε pixel για την αρχή του κειµένου. Οι συντεταγµένες αυτές αναφέρονται στη 227
γραµµή βάσης (baseline) της γραµµατοσειράς, συνεπώς αν γράψετε στην y συντεταγµένη µηδέν το αποτέλεσµα θα είναι το µεγαλύτερο µέρος των γραµµάτων σας να βρίσκονται έξω (πάνω) από το χώρο παρουσίασης µόνο ότι γράφεται κάτω από τη γραµµή, π.χ. η «ουρά» στο «µ» θα είναι ορατό. 11.3. Μέθοδοι applet και κύκλος ζωής ενός applet Ο κύκλος ζωής ενός applet είναι λίγο πιο πολύπλοκη απ ό,τι την περιγράψαµε µέχρι τώρα. Υπάρχουν τρεις βασικές µέθοδοι που σχετίζονται µε αυτόν τον κύκλο ζωής. Αυτές είναι οι init(), start() και stop(). 11.3.1. init() Αυτή η συνάρτηση µέλος καλείται όταν δηµιουργείται το applet και φορτώνεται για πρώτη φορά σε ένα browser που υποστηρίζει Java (όπως ο Applet Viewer). Το applet µπορεί να χρησιµοποιήσει τη µέθοδο αυτή για να αρχικοποιήσει τιµές δεδοµένων. Η µέθοδος αυτή δεν καλείται κάθε φορά που ο browser ανοίγει τη σελίδα που περιέχει το applet, αλλά µόνο την πρώτη φορά. 11.3.2. start() Η µέθοδος start() καλείται για να δηλώσει στο applet ότι θα πρέπει να «ζωντανέψει». Αυτό συµβαίνει όταν το applet ξεκινά για πρώτη φορά, αφού ολοκληρωθεί η µέθοδος init(). Επίσης συµβαίνει οποτεδήποτε ο browser επανέρχεται στην κατάστασή του µετά από µία κατάσταση όπου είχε γίνει εικονίδιο ή όταν ο browser επιστρέφει στη σελίδα που περιέχει το applet αφού έχουµε µεταφερθεί σε ένα άλλο URL. Αυτό σηµαίνει ότι το applet µπορεί να χρησιµοποιήσει τη µέθοδο αυτή για να πραγµατοποιήσει εργασίες όπως να ξεκινά ένα animation ή να παίζουν ήχοι. public void start() musicclip.play(); 11.3.3. stop() Η µέθοδος stop() καλείται όταν το applet παύει να είναι «ζωντανό». Αυτό συµβαίνει όταν ο browser γίνεται εικονίδιο (iconized) ή όταν ακολουθεί ένα σύνδεσµο σε ένα άλλο URL. Το applet µπορεί να χρησιµοποιήσει τη µέθοδο αυτή για να πραγµατοποιήσει εργασίες όπως να σταµατήσει το animation. public void stop() musicclip.stop(); Ουσιαστικά οι µέθοδοι start() και stop() δηµιουργούν ένα ζευγάρι, έτσι ώστε η start() να µπορεί να χρησιµοποιηθεί για να προκαλέσει συµπεριφορά σε ένα applet, ενώ η stop() χρησιµοποιείται για να σταµατήσει αυτή τη συµπεριφορά. 228
11.4. Ζωγραφική AWT Επιπρόσθετα στις βασικές µεθόδους του κύκλου ζωής ένα applet έχει σηµαντικές µεθόδους που σχετίζονται µε την παρουσίασή του. Οι µέθοδοι αυτοί στην πράξη δηλώνονται και τεκµηριώνονται για την κλάση AWT Component και είναι σηµαντικό να συµµορφωνόµαστε στο σωστό µοντέλο για την παρουσίαση και χειρισµό µε χρήση του AWT. Η ενηµέρωση της παρουσίασης γίνεται από διακριτό νήµα στο οποίο θα αναφερόµαστε ως νήµα AWT. Το νήµα αυτό µπορεί να κληθεί για να χειριστεί δύο καταστάσεις οι οποίες σχετίζονται µε την ενηµέρωση της παρουσίασης. Η πρώτη από τις δύο καταστάσεις είναι η έκθεση (exposure), όπου µέρος της παρουσίασης έχει καταστραφεί και πρέπει να αντικατασταθεί. Αυτό µπορεί να συµβεί οποτεδήποτε και το πρόγραµµά σας θα πρέπει να µπορεί να το αντιµετωπίζει. Η δεύτερη κατάσταση είναι όταν το πρόγραµµα αποφασίζει να ξανασχεδιάσει την παρουσίαση µε νέα περιεχόµενα. Αυτό µπορεί να απαιτεί την αφαίρεση πρώτα της παλιάς εικόνας. 11.4.1. Η µέθοδος paint(graphics g) Ο χειρισµός έκθεσης (exposure handling) γίνεται αυτόµατα και έχει ως αποτέλεσµα µία κλήση στη µέθοδο paint(). Χρησιµοποιείται µία διευκόλυνση της κλάσης Graphics, η οποία ονοµάζεται clip rectangle, ώστε να βελτιστοποιήσει τη µέθοδο paint ώστε οι ενηµερώσεις να µη γίνονται σε ολόκληρη την περιοχή των γραφικών, αλλά µόνο στην περιοχή που καταστράφηκε. 11.4.2. Η µέθοδος repaint() Από την άλλη, µπορείτε να ενηµερώσετε το σύστηµα ότι θέλετε να αλλάξετε την παρουσίαση καλώντας τη repaint(). 11.4.3. Η µέθοδος update(graphics g) H repaint προκαλεί ένα AWT νήµα να καλέσει µία άλλη µέθοδο που ονοµάζεται update(). Η µέθοδος update συνήθως συµπεριφέρεται καθαρίζοντας την τρέχουσα παρουσίαση και καλώντας στη συνέχεια την paint(). Το µοντέλο αυτό απαιτεί να έχετε υιοθετήσει µία συγκεκριµένη στρατηγική για τη συντήρηση της παρουσίασής σας: Συντήρηση ενός µοντέλου της παρουσίασης. Αυτό µπορεί να γίνει χρησιµοποιώντας πολλές διαφορετικές προσεγγίσεις. Θα µπορούσατε να χρησιµοποιείται µία απεικόνιση τιµών pixels ή µία τεχνική υψηλότερου επιπέδου, όπως ένα πίνακα σχηµάτων. Η µέθοδος paint() σας θα πρέπει να κάνει render την παρουσίαση µε βάση µόνο τα περιεχόµενα του µοντέλου. Αυτό τη επαναδηµιουργία της παρουσίασης µε συνεπή τρόπο όποτε καλείται, και συνεπώς υπάρχει ορθός χειρισµός της έκθεσης. Όταν το πρόγραµµα θέλει να αλλάξει την παρουσίαση θα πρέπει να ενηµερώνει το µοντέλο και στη συνέχεια να καλεί τη µέθοδο repaint(), ώστε να καλείται από το νήµα AWT η µέθοδος update(). Σηµείωση Μόνο ένα νήµα AWT χειρίζεται όλη τη ζωγραφική των components και επίσης την κατανοµή των συµβάντων εισόδου. Εξ αιτίας αυτού, θα πρέπει να αποφεύγετε να σπαταλάτε πολύ χρόνο είτε στην paint() είτε στην update(). Σε εξαιρετικές 229
περιπτώσεις θα χρειαστείτε τη βοήθεια και άλλων νηµάτων για να το επιτύχετε. Ο προγραµµατισµός νηµάτων αποτελεί θέµα άλλου κεφαλαίου. Το διάγραµµα που ακολουθεί παρουσιάζει τον τρόπο µε τον οποίο σχετίζονται οι µέθοδοι paint(), update() και repaint(). 11.5. O appletviewer Ο appletviewer είναι µία εφαρµογή Java η οποία σάς επιτρέπει να εκτελείτε applets χωρίς να χρησιµοποιείτε ένα web browser. Σηµείωση Οποιοσδήποτε browser µε υποστήριξη Java πραγµατοποιεί τις ίδιες βασικές λειτουργίες όπως αυτές που θα περιγράψουµε εδώ για το appletviewer. 11.5.1. Τι είναι ο appletviewer; Συνήθως ένα applet εκτελείται µέσα από ένα Web browser που υποστηρίζει Java, όπως ο HotJava ή ο Netscape Navigator. Όµως, για να απλοποιήσουµε και να επιταχύνουµε τη διαδικασία ανάπτυξης, το JDK περιέχει ένα απλό εργαλείο το οποίο έχει σχεδιαστεί µόνο για να βλέπουµε applets και όχι σελίδες HTML. Το εργαλείο αυτό είναι ο appletviewer. Ο appletviewer διαβάζει HTML από ένα URL που του παρέχεται στη γραµµή εντολής. Σε αυτή την HTML αναµένει να βρει εντολές για να φορτώσει και να εκτελέσει ένα ή περισσότερα applets. Θα αγνοήσει οποιαδήποτε άλλη HTML εντολή και εξ αιτίας αυτού ο appletviewer δεν παρουσιάζει απλή HTML και δεν ενσωµατώνει τα applets µέσα σε µία σελίδα κειµένου. 230
11.5.2. Κλήση applets µε τον appletviewer Ο appletviewer µοιάζει µε ένα µικρό browser. Αναµένει ως όρισµα το όνοµα ενός αρχείου HTML για να το φορτώσει. Το αρχείο αυτό περιέχει µία tag HTML η οποία καθορίζει τον κώδικα που θα φορτώσει ο appletviewer. <html> <applet code=helloworld.class width=100 height=100> </applet> </html> Ο appletviewer δηµιουργεί το χώρο ενός browser, συµπεριλαµβανοµένης µίας περιοχής γραφικών, στην οποία θα εκτελεστεί το applet και στη συνέχεια καλεί την κατάλληλη κλάση applet. Στο παράδειγµα αυτό, ο appletviewer φορτώνει µία κλάση µε το όνοµα HelloWorld και της επιτρέπει να λειτουργεί µέσα στο γραφικό χώρο. 11.5.3. appletviewer και ιεραρχία Applet Το ακόλουθο διάγραµµα παρουσιάζει την ιεραρχία κληρονοµικότητας για τον appletviewer και applets. 11.6. Χρήση appletviewer 11.6.1. Σύνοψη appletviewer [-debug] urls... O appletviewer δέχεται είτε το όνοµα ενός αρχείου HTML που περιέχει το tag <applet> ή το όνοµα ενός URL σε ένα αρχείου HTML. Αν το HTML αρχείο που θα µεταβιβαστεί στον appletviewer δεν περιέχει ένα έγκυρο <applet> tag, ο appletviewer δε θα κάνει τίποτα. Ο appletviewer δεν παρουσιάζει άλλες HTML tags. Η µόνη έγκυρη επιλογή στο appletviewer είναι η ένδειξη debug, η οποία ξεκινά το applet στο Java debugger, jdb. Για να µπορέσετε να δείτε τον Java πηγαίο κώδικα στο debugger θα πρέπει να µεταγλωττίσετε το Java κώδικά σας µε την επιλογή g. 231
11.6.2. Παράδειγµα Η εντολή appletviewer που φαίνεται στη συνέχεια ξεκινά το appletviewer $ appletviewer example1.html Αυτό προκαλεί και παρουσιάζει το πιο κάτω µικρό παράδειγµα. 11.7. Η tag applet 11.7.1. Σύνταξη Στη συνέχεια παρουσιάζεται η πλήρης σύνταξη της tag applet. <applet archive=archivelist code=appletfile.class width=pixels height=pixels [codebase=codebaseurl] [alt=alternatetext] [name=appletinstancename] [align=alignment] [vspace=pixels] [hspace=pixels] > [<param name=appletattribute1 value=value>] [<param name=appletattribute2 value=value>]... [alternatehtml] </applet> 11.7.2. Περιγραφή archive=archivelist Η προαιρετική αυτή attribute περιγράφει ένα ή περισσότερα αρχεία (archives) που περιέχουν κλάσεις ή άλλους πόρους που θα «προ-φορτωθούν». Οι κλάσεις φορτώνονται χρησιµοποιώντας ένα στιγµιότυπο 232
ενός AppletClassLoader µαζί µε το δεδοµένο CODEBASE. Τα αρχεία στην archivelist χωρίζονται µε «,». code=appletfile.class Η υποχρεωτική αυτή attribute δίνει το όνοµα του αρχείου που περιέχει τη µεταγλωττισµένη υποκλάση της Applet που αντιστοιχεί στο συγκεκριµένο applet. Θα µπορούσε να είναι επίσης της µορφής apackage.appletfile.class. Σηµείωση Το αρχείο αυτό είναι σχετικό µε βάση το URL του HTML αρχείου από το οποίο το φορτώνετε. εν µπορεί να περιλαµβάνει ένα όνοµα διαδροµής. Για να αλλάξετε το URL βάσης του applet, δείτε την tag codebase πιο κάτω. width=pixels height=pixels Οι υποχρεωτικές αυτές attributes δίνουν το αρχικό πλάτος και ύψος (σε pixels) της περιοχής παρουσίασης του applet, χωρίς να υπολογίζονται οποιαδήποτε παράθυρα ή dialogs εµφανίζει το applet. codebase=codebaseurl Η προαιρετική αυτή attribute καθορίζει το URL βάσης για το applet τον κατάλογο που περιέχει τον κώδικα του applet. Αν δεν έχει καθοριστεί αυτή η attribute χρησιµοποιείται το URL του κειµένου. alt=alternatetext Η προαιρετική αυτή attribute καθορίζει οποιοδήποτε κείµενο θα πρέπει να παρουσιάζεται αν ο browser κατανοεί το tag applet αλλά δεν µπορεί να εκτελέσει Java applets. name=appletinstancename Η προαιρετική αυτή attribute καθορίζει ένα όνοµα για το στιγµιότυπο του applet, ώστε τα applets που βρίσκονται στην ίδια σελίδα να µπορούν να βρουν το ένα το άλλο (και να επικοινωνούν µεταξύ τους). align=alignment Η προαιρετική αυτή attribute καθορίζει τη στοίχιση του applet. Οι δυνατές τιµές είναι οι ίδιες µε αυτές της tag IMG στη βασική HTML: left, right, top, texttop, middle, absmiddle, baseline, bottom και absbottom. vspace=pixels hspace=pixels Οι προαιρετικές αυτές attributes καθορίζουν το πλήθος των pixels πάνω και κάτω από το applet (vspace) και σε κάθε πλευρά του applet (hspace). Αντιµετωπίζονται µε τον ίδιο τρόπο όπως και οι attributes vspace και hspace της tag IMG. <param name=appletattribute1 value=value> Αυτή η tag είναι ο µόνος διαθέσιµος τρόπος για να ορίσουµε attributes συγκεκριµένες στο applet. Τα applets έχουν πρόσβαση στα attributes τους µε τη µέθοδο getparameter(). Οι browsers που δεν υποστηρίζουν Java θα παρουσιάζουν όλη την κανονική HTML που παρουσιάζεται ανάµεσα στα tags <applet> και </applet>. Οι browsers που υποστηρίζουν Java αγνοούν την κανονική HTML που βρίσκεται ανάµεσα σε αυτά τα tags. To URL που καθορίζετε ως όρισµα στον appletviewer θα πρέπει να περιέχει εντολές για τη φόρτωση ενός applet. Οι εντολές αυτές έχουν τη µορφή ενός applet tag το οποίο στην πιο απλή µορφή του φαίνεται ως εξής: <applet code=helloworld.class width=100 height=100> </applet> Σηµειώστε ότι η γενική µορφή αυτού του tag είναι η ίδια όπως και όλων των άλλων HTML, µε χρήση των συµβόλων < και > για να διαχωριστούν οι εντολές. Όλα τα τµήµατα που παρουσιάζονται εδώ είναι απαραίτητα, πρέπει να έχετε τόσο το 233
<applet > όσο και το </applet>, και στο τµήµα <applet > πρέπει να καθορίζετε την καταχώρηση για κώδικα και ύψος και πλάτος. Σηµείωση Γενικά, θα πρέπει να αντιµετωπίζετε τα applets σα να είναι σταθερού µεγέθους, λαµβάνοντας το µέγεθος όπως καθορίζεται στο applet tag. Θα βρείτε στη συνέχεια ότι είναι πιθανό υπό ορισµένες συνθήκες να αλλάζετε το µέγεθος ενός applet, αλλά τα αποτελέσµατα δεν είναι πάντα καλά. 11.8. Επιπρόσθετες διευκολύνσεις Applet Υπάρχουν διαθέσιµες αρκετές επιπλέον διευκολύνσεις σε ένα applet και θα τα παρουσιάσουµε εδώ. Όλα τα προγράµµατα Java έχουν πρόσβαση σε δικτυακές διευκολύνσεις, χρησιµοποιώντας τις κλάσεις του πακέτου java.net. Τα applets έχουν επιπρόσθετες µεθόδους που τους επιτρέπουν να καθορίζουν πληροφορία σχετικά µε το περιβάλλον του browser στο οποίο έχουν ξεκινήσει. Η κλάση java.net.url περιγράφει URLs και µπορεί να χρησιµοποιηθεί για να συνδεθεί σε αυτά. ύο µέθοδοι στην κλάση Applet µπορούν να καθορίσουν την τιµή σηµαντικών URLs. getdocumentbase() επιστρέφει ένα αντικείµενο URL που περιγράφει την τρέχουσα σελίδα του browser. getcodebase() επιστρέφει ένα αντικείµενο URL που περιγράφει την πηγή του κώδικα του ίδιου του applet. Συχνά αυτό είναι το ίδιο µε την τρέχουσα σελίδα, αλλά αυτό δεν είναι απαραίτητο. Χρησιµοποιώντας το αντικείµενο URL ως αρχικό σηµείο µπορείτε να φέρετε στο applet σας εικόνες και ήχους. getimage(url base, String target) φέρνει µία εικόνα από το αρχείο µε όνοµα target το οποίο βρίσκεται στο URL που καθορίζει η base. Η επιστρεφόµενη τιµή είναι ένα στιγµιότυπο της κλάσης Image. getaudioclip(url base, String target) φέρνει έναν ήχο από το αρχείο µε όνοµα target το οποίο βρίσκεται στο URL που καθορίζει η base. Η επιστρεφόµενη τιµή είναι ένα στιγµιότυπο της κλάσης AudioClip. 11.9. Μία απλή δοκιµή Image Το επόµενο applet θα φέρει το αρχείο εικόνας graphics/joe.surf.yellow.small.gif σχετικό ως προς τη διαδροµή καταλόγου που επιστρέφει η µέθοδος getdocumentbase και θα το εµφανίσει στον appletviewer. // HelloWorld extended to draw an image // Assumes existence of // graphics/joe.surf.yellow.small.gif import java.awt.*; import java.applet.applet; public class HwImage extends Applet Image duke; public void init() duke = getimage(getdocumentbase(), 234
graphics/joe.surf.yellow.small.gif ); public void paint(graphics g) g.drawimage(duke, 25, 25, this); Τα ορίσµατα της µεθόδου drawimage() είναι: 1. Το αντικείµενο Image που πρόκειται να σχεδιαστεί 2. Η συντεταγµένη X για τη σχεδίαση 3. Η συντεταγµένη Y για τη σχεδίαση 4. Ο παρατηρητής εικόνας (image observer). Ένας παρατηρητής εικόνας είναι κάτι που πρέπει να γνωρίζει αν άλλαξε η εικόνα. Μία εικόνα που φορτώνεταιµε το getimage() θα αλλάξει µε το χρόνο αφού γίνει πρώτη φορά η κλήση. Αυτό συµβαίνει γιατί η φόρτωση γίνεται στο παρασκήνιο (background). Κάθε φορά που φορτώνεται µεγαλύτερο µέρος της εικόνας καλείται ξανά η µέθοδος paint(). Αυτό γίνεται γιατί το applet έχει δηλωθεί ως παρατηρητής µεταβιβαζόµενος ως το τέταρτο όρισµα της κλήσης drawimage(). 11.10. AudioClips Η γλώσσα Java έχει επίσης µεθόδους για να παίζει audio clips. Οι µέθοδοι αυτές βρίσκονται στην κλάση java.applet.audioclip. Προφανώς, για να παίξετε ήχο θα πρέπει να έχετε το κατάλληλο υλικό στον υπολογιστή σας. 11.10.1. Παίζοντας ένα clip Ο πιο απλός τρόπος για να ακούσετε ένα audio clip είναι µέσω της µεθόδου play της Applet: play(url sounddirectory, String soundfile); ή πιο απλά play(url soundurl); Ένα σύνηθες URL για να χρησιµοποιήσετε µε τη µέθοδο play() είναι ο κατάλογος όπου βρίσκονται τα αρχεία HTML. Η πρόσβαση στη θέση αυτή γίνεται µέσω της µεθόδου getdocumentbase(): play(getdocumentbase(), bark.au ); Για να δουλέψει αυτό το αρχείο HTML και το αρχείο bark.au πρέπει να βρίσκονται στον ίδιο κατάλογο. 11.11. Μία απλή δοκιµή ήχου Το επόµενο applet εκτυπώνει το µήνυµα Audio Test στον appletviewer και στη συνέχεια παίζει ένα αρχείο ήχου sounds/cuckoo.au. // HelloWorld extended to play an Audio sound 235
// Assume existence of sounds/cuckoo.au file import java.awt.graphics; import java.applet.applet; public class HwAudio extends Applet public void paint(graphics g) g.drawstring( Audio Test, 25, 25); play(getdocumentbase(), sounds/cuckoo.au ); 11.12. Επανάληψη µε ένα audio clip Μπορείτε επίσης να αντιµετωπίσετε ένα audio clip ως εικόνα. Μπορείτε να τα φορτώσετε και να τα παίξετε αργότερα. 11.12.1. Φόρτωση ενός Audio Clip Για να φορτώσετε ένα audio clip, χρησιµοποιήστε τη µέθοδο getaudioclip από την κλάση java.applet.applet: AudioClip sound; sound = getaudioclip(getdocumentbase(), bark.au ); Από τη στιγµή που ένα clip έχει φορτωθεί, µπορείτε να χρησιµοποιήσετε µία από τις τρεις µεθόδους που σχετίζονται µαζί τους: play, loop ή stop. 11.12.2. Παίξιµο ενός Audio Clip Για να παίξουµε ένα φορτωµένο audio clip χρησιµοποιούµε στη µέθοδο play της κλάσης java.applet.audioclip sound.play(); Για να ξεκινήσουµε ένα clip να παίζει και να το αναγκάσουµε να επαναλαµβάνεται (αυτόµατα) χρησιµοποιούµε τη µέθοδο loop της κλάσης java.applet.audioclip sound.loop(); 11.12.3. Σταµάτηµα ενός Audio Clip Για να σταµατήσουµε ένα clip που παίζεται χρησιµοποιούµε τη µέθοδο stop στην κλάση java.applet.audioclip sound.stop(); 11.13. Μία απλή δοκιµή επανάληψης Ιδού ένα παράδειγµα που κάνει αυτόµατη επανάληψη σε ένα φορτωµένο audio clip. // HelloWorld extended to loop an audio track 236
// Assumes existence of sounds/cuckoo.au import java.awt.graphics; import java.applet.*; public class HwLoop extends Applet AudioClip sound; public void init() sound = getaudioclip(getdocumentbase(), sounds/cuckoo.au ); public void paint(graphics g) g.drawstring( Audio test, 25, 25); public void start() sound.loop(); public void stop() sound.stop(); 11.14. Είσοδος από το ποντίκι Ένα από τα πιο χρήσιµα χαρακτηριστικά που υποστηρίζει η γλώσσα Java είναι η άµεση αλληλεπίδραση. Μία εφαρµογή µπορεί να δίνει σηµασία στο ποντίκι και να αντιδρά σε αλλαγές στο ποντίκι. Το µοντέλο συµβάντων στο JDK 1.1 υποστηρίζει έναν τύπο συµβάντος για κάθε αλληλεπίδρασης. Τα συµβάντα ποντικιού λαµβάνονται από κλάσεις που υλοποιούν τη διεπαφή MouseListener, η οποία λαµβάνει συµβάντα για: mouseclicked όταν έχει γίνει κλικ στο ποντίκι (το πλήκτρο του ποντικιού έχει πιεστεί και έχει αφεθεί σε µία κίνηση). mouseentered όταν το ποντίκι έχει µπει σε ένα component mouseexited όταν το ποντίκι αφήνει ένα component mousepressed όταν το πλήκτρο του ποντικιού είναι πατηµένο mousereleased όταν το πλήκτρο του ποντικιού έχει εν συνεχεία αφεθεί. 11.15. Μία απλή δοκιµή µε το ποντίκι Το επόµενο πρόγραµµα παρουσιάζει τη θέση ενός κλικ µε το ποντίκι µέσα σε ένα applet. // HelloWorld extended to watch for mouse input // HelloWorld! is reprinted at the location of // the mouse click. import java.awt.*; import java.awt.event.*; 237
import java.applet.applet; public class HwMouse extends Applet implements MouseListener int mousex = 25; int mousey = 25; // Register the mousepressed event public void init () addmouselistener(this); public void paint(graphics g) g.drawstring( Hello world!, mousex, mousey); public void mousepressed(mouseevent evt) mousex = evt.getx(); mousey = evt.gety(); repaint(); // We are not using the other mouse events public void mouseclicked(mouseevent evt) public void mouseentered(mouseevent evt) public void mouseexited(mouseevent evt) public void mousereleased(mouseevent evt) 11.16. Ανάγνωση παραµέτρων Στο αρχείο HTML η tag applet µπορεί να µεταβιβάσει πληροφορία διαµόρφωσης (configuration) στο applet. Αυτό γίνεται χρησιµοποιώντας την tag <param>. Για παράδειµα: <applet code=configureme.class width=100 heigh=100> <param name=image value=duke.gif> </applet> Μέσα στο applet µπορείτε να χρησιµοποιήσετε τη µέθοδο getparameter() για να διαβάσετε τις τιµές αυτές import java.awt.*; import java.applet.*; public class DrawAny extends Applet Image im; public void init() URL mypage = getdocumentbase(); String imagename = getparameter( image ); 238
im = getimage(mypage, imagename); public void paint(graphics g) g.drawimage(im, 0, 0, this); Αν το όνοµα της παραµέτρου δεν µπορεί να βρεθεί σε κάποια tag <param> µέσα στο ζεύγος <applet> </applet>, τότε η getparameter() επιστρέφει null. Ένα πρόγραµµα παραγωγής θα πρέπει να το αντιµετωπίσει µε καλό τρόπο (gracefully). Ο τύπος παραµέτρου είναι πάντα String. Αν τη χρειάζεστε µε άλλον τύπο πρέπει να τη µετατρέψετε. Για παράδειγµα για να διαβάσετε µία int παράµετρο: int speed = Integer.parseInt(getParameter( speed )); Οι παράµετροι, εξ αιτίας της φύσης της HTML δεν είναι case sensitive. Είναι όµως καλό στυλ να αποφασίσετε και να χρησιµοποιείτε είτε µόνο κεφαλαία είτε µόνο πεζά, όπως παρουσιάζονται. 11.17. Κώδικας που λειτουργεί µε δύο τρόπους Είναι δυνατό να δηµιουργήσουµε κώδικα στη Java µέσα στο ίδιο αρχείο που να µπορεί να λειτουργεί είτε ως εφαρµογή Java είτε ως Java applet. Υπάρχει λίγη παραπάνω δουλειά στην κατανόηση του τι απαιτεί η εφαρµογή, αλλά από τη στιγµή που έχει δηµιουργηθεί ο κώδικας applet/εφαρµογής µπορεί να χρησιµοποιηθεί ως πρότυπο για πιο πολύπλοκα προγράµµατα. 11.17.1. Applet/Εφαρµογή Στη συνέχεια δίνεται το πρόγραµµα Hello World ως applet/εφαρµογή // Example of Hello World that is both // an applet and an application import java.applet.applet; import java.awt.*; import java.awt.event.*; public class AppletApp extends Applet // An application will require a main() public static void main (String args[]) // Create a Frame to house the applet Frame frame = new Frame( Application ); // Create an instance of the class (applet) AppletApp app = new AppletApp(); // Add it to the center of the frame frame.add( Center, app); frame.setsize(200, 200); frame.validate(); frame.setvisible(true); 239
// Register the AppletApp class as the // listener for a Window Destroy event frame.addwindowlistener(new WindowControl(app)); // Call the applet methods app.init(); app.start(); // end main public void paint(graphics g) g.drawstring( Hello World, 25, 25); public void destroy () System.exit(0); // Class used to control the applet window class WindowControl extends WindowAdapter Applet c; public WindowControl (Applet c) this.c = c; public void windowclosing(windowevent e) c.destroy(); 240
11.18. Λυµένες Ασκήσεις 11.18.1 Να γράψετε ένα applet το οποίο να εµφανίζει η φράση JAVA IS FUN!! δύο φορές µε χρώµατα κόκκινο και πράσινο και µε χαρακτήρες BOLD+ITALIC των 35 points. Στη συνέχεια γράψτε ένα απλό αρχείο HTML το οποίο να καλεί το applet. Χρησιµοποιείστε έναν Web Browser για να εκτελέσετε την εφαρµογή. Λύση: import java.awt.*; public class JavaIsFun extends javax.swing.japplet String phrase; public void init() phrase = "JAVA IS FUN!!!"; public void paint(graphics screen) Graphics2D screen2d = (Graphics2D)screen; Font myfont = new Font("Dialog", Font.PLAIN,12); myfont = new Font("Dialog", Font.BOLD+Font.ITALIC,35); screen2d.setfont(myfont); screen2d.setcolor(color.red); screen2d.drawstring(phrase,50,50); screen2d.setcolor(color.green); screen2d.drawstring(phrase,50,100); Αφού µεταγλωττίσουµε το παραπάνω πρόγραµµα (JavaIsFun.java) συντάσσουµε το παρακάτω αρχείο JavaIsFun.html. <applet code="javaisfun.class" height=250 width=400> </applet> Εάν ανοίξουµε το αρχείο JavaIsFun.html µε τον Konqueror (ή οποίον άλλον Web Browser) έχουµε το παρακάτω αποτέλεσµα: 241
11.18.2 Να γράψετε ένα applet το οποίο να εµφανίζει πέντε κουµπιά στη σειρά µε ονόµατα one έως five. Στη συνέχεια γράψτε ένα απλό αρχείο HTML το οποίο να καλεί το applet. Χρησιµοποιείστε έναν Web Browser για να εκτελέσετε την εφαρµογή. Λύση: import java.awt.*; import javax.swing.*; public class Kkoumpia extends javax.swing.japplet public void init() JButton one = new JButton("One"); JButton two = new JButton("Two"); JButton three = new JButton("Three"); JButton four = new JButton("Four"); JButton five = new JButton("Five"); Container pane = getcontentpane(); FlowLayout flo = new FlowLayout(); pane.setlayout(flo); pane.add(one); pane.add(two); pane.add(three); pane.add(four); pane.add(five); setcontentpane(pane); Αφού µεταγλωττίσουµε το παραπάνω πρόγραµµα (javac Koumpia.java) συντάσσουµε το παρακάτω αρχείο Koumpia.html. <applet code="kkoumpia.class" height=250 width=400> </applet> Εάν ανοίξουµε το αρχείο Koumpia.html µε τον Konqueror (ή οποίον άλλον Web Browser) έχουµε το παρακάτω αποτέλεσµα: 242
11.18.3. Να γραφεί ένα πρόγραµµα το οποίο θα εµφανίζει ένα µενού (Choice) µε διάφορα χρώµατα και δύο κουµπιά µε ετικέτες Print και Clear. Πατώντας το Print, να εµφανίζεται ένας κύκλος µε χρώµα αυτό που είναι επιλεγµένο στο µενού. Πατώντας το Clear, ο κύκλος να εξαφανίζεται. Από το µενού να επιλέγουµε το χρώµα του κύκλου. Λύση: import java.awt.*; import java.applet.*; import java.awt.event.*; public class AppletMenu extends Applet implements ActionListener, ItemListener Choice ch=new Choice(); Button b1=new Button("Print"); Button b2=new Button("Clear"); Color c=color.black; boolean paint=false; public void init() ch.setfont(new Font ("Serif", Font.BOLD, 16)); ch.additem("magenta"); ch.additem("red"); ch.additem("yellow"); ch.additem("green"); ch.additem("cyan"); ch.additem("blue"); add(ch); ch.additemlistener(this); 243
b1.setfont(new Font ("Serif", Font.BOLD, 16)); b2.setfont(new Font ("Serif", Font.BOLD, 16)); add(b1); b1.addactionlistener(this); add(b2); b2.addactionlistener(this); public void paint (Graphics g) g.setcolor(c); g.filloval(150,180,100,100); public void actionperformed(actionevent e) if(e.getsource()==b1) itemstatechanged(null); paint=true; if(e.getsource()==b2) c=color.white; paint=false; repaint(); public void itemstatechanged(itemevent e) switch(ch.getselectedindex()) case 0: c=color.magenta; break; case 1: c=color.red; break; case 2: c=color.yellow; break; case 3: c=color.green; break; case 4: c=color.cyan; break; case 5: c=color.blue; break; if(paint) repaint(); Αφού µεταγλωττίσουµε το παραπάνω πρόγραµµα (javac Koumpia.java) συντάσσουµε το παρακάτω αρχείο Koumpia.html. <applet code="appletmenu.class" height=250 width=400> </applet> 11.18.4. Να γραφεί ένα πρόγραµµα το οποίο θα εµφανίζει τρία πεδία κειµένου και έξι κουµπιά που αντιστοιχούν σε αριθµητικές πράξεις: Πρόσθεση, Αφαίρεση, Πολλαπλασιασµός, ιαίρεση, Υπόλοιπο διαίρεσης, Υψωση σε δύναµη. Πατώντας καθένα από τα κουµπιά, να εκτελεί την αντίστοιχη πράξη µεταξύ των δύο αρχικών πεδίων κειµένου και να εµφανίζει το αποτέλεσµα στο τρίτο. 244
Λύση: import java.awt.*; import java.applet.*; import java.awt.event.*; public class askhsh_ariumo extends Applet implements ActionListener TextField t1=new TextField(""); TextField t2=new TextField(""); TextField t3=new TextField(""); Button b1=new Button("+"); Button b2=new Button("-"); Button b3=new Button("*"); Button b4=new Button("/"); Button b5=new Button("%"); Button b6=new Button("^"); public void init() GridBagLayout lo=new GridBagLayout(); setlayout(lo); GridBagConstraints gbc=new GridBagConstraints(); gbc.weightx=0; gbc.weighty=0; // Για γέµισµα περιθωριακού περισεύµατος, =1 gbc.fill=gridbagconstraints.both; // Γέµισµα περιοχής gbc.insets=new Insets(3,3,0,0); // Περιθώρια πάνω, αριστερά,κάτω, δεξιά gbc.gridx=0; gbc.gridwidth=6; gbc.gridheight=1; gbc.gridy=0; lo.setconstraints(t1,gbc); add(t1); gbc.gridy=1; lo.setconstraints(t2,gbc); add(t2); gbc.gridy=2; lo.setconstraints(t3,gbc); add(t3); gbc.gridy=3; gbc.gridwidth=1; gbc.gridheight=1; gbc.gridx=0; lo.setconstraints(b1,gbc); add(b1); gbc.gridx=1; lo.setconstraints(b2,gbc); add(b2); gbc.gridx=2; lo.setconstraints(b3,gbc); add(b3); 245
gbc.gridx=3; lo.setconstraints(b4,gbc); add(b4); gbc.gridx=4; lo.setconstraints(b5,gbc); add(b5); gbc.gridx=5; lo.setconstraints(b6,gbc); add(b6); b1.addactionlistener(this); b2.addactionlistener(this); b3.addactionlistener(this); b4.addactionlistener(this); b5.addactionlistener(this); b6.addactionlistener(this); public void actionperformed(actionevent e) double v1=double.parsedouble(t1.gettext().trim()); double v2=double.parsedouble(t2.gettext().trim()); double v3=0; if(e.getsource()==b1)v3=v1+v2; if(e.getsource()==b2)v3=v1-v2; if(e.getsource()==b3)v3=v1*v2; if(e.getsource()==b4)v3=v1/v2; if(e.getsource()==b5)v3=v1%v2; if(e.getsource()==b6)v3=math.pow(v1,v2); t3.settext(double.tostring(v3)); Αφού µεταγλωττίσουµε το παραπάνω πρόγραµµα (javac Koumpia.java) συντάσσουµε το παρακάτω αρχείο Koumpia.html. <applet code="askhsh_ariumo.class" height=250 width=400> </applet> 11.18.5. Να γραφεί ένα πρόγραµµα applet το οποίο θα κατασκευάζει διάφορα components ενός παραθύρου (π.χ. µενού, κουµπιά κ.τ.λ). 246
Λύση: import java.awt.*; import java.applet.*; public class showbutton extends Applet public void init() Button b1=new Button("1ο κουµπί διαταγής"); add(b1); Button b2=new Button("2ο κουµπί διαταγής"); add(b2); // Label // Label x=new Label(String, στοίχιση) // Label.LEFT // Label.CENTER // Label.RIGHT // settext (String text) Ορίζει το κείµενο // setalignment (int alignment) Ορίζει τη στοίχιση // setfont (Font font) Ορίζει τη γραµµατοσειρά Label l1=new Label ("Αριστερή Στοίχιση", Label.LEFT); l1.setfont(new Font ("Dialog", Font.BOLD, 18)); add (l1); Label l2=new Label ("Κεντρική Στοίχιση", Label.CENTER); l2.setfont(new Font ("Monospaced", Font.BOLD, 18)); add (l2); Label l3=new Label (" εξιά Στοίχιση", Label.RIGHT); l3.setfont (new Font ("Serif", Font.BOLD, 18)); add (l3); Label l4=new Label (); add(l4); l4.settext("ακόµα µια ετικέτα"); l4.setfont(new Font("SansSerif",Font.ITALIC,14)); 247