Χειρισμός λαθών και εξαιρέσεις
- Μια εξαίρεση (exception) είναι ένα σήμα πως
έχει συμβεί κάποιο μη κανονικό συμβάν (π.χ. λάθος).
- Η μη κανονική κατάσταση προκαλεί (throws) μια
εξαίρεση.
- Στο πρόγραμμά μας μπορούμε να ορίσουμε κώδικα που θα
συλλάβει (catch) την εξαίρεση.
- Με τη βοήθεια των εξαιρέσεων μπορούμε να ξεχωρίσουμε στο πρόγραμμά μας
το χειρισμό των λαθών από τον υπόλοιπο κώδικα.
- Οι εξαιρέσεις υποστηρίζονται από πολλές γλώσσες όπως η
Ada, C++, Java, C#, και Visual Basic.
Παράσταση εξαιρέσεων
- Στη Java οι εξαιρέσεις παριστάνονται ως υποκλάσεις της κλάσης
Throwable:
- Error
- Λάθη που δεν επιτρέπουν την επανάκτηση της λειτουργίας
- Exception
- Λάθη που μπορεί να επιτρέπουν την επανάκτηση της λειτουργίας
- Εφαρμογές μπορούν να ορίζουν νέες υποκλάσεις με τις δικές τους εξαιρέσεις.
Κάθε κλάση εξαίρεσης χρειάζεται να έχει μόνο ως μέλη δύο μεθόδους κατασκευαστές:
- μία χωρίς όρισμα
- μία με όρισμα συμβολοσειρά
Παράδειγμα παράστασης εξαιρέσεων
import java.io.*;
public class NonGreekCharacterException extends IOException {
public NonGreekCharacterException() {}
public NonGreekCharacterException(String msg) {
super(msg);
}
}
Δημιουργία εξαιρέσεων
Στη γλώσσα Java μια εξαίρεση μπορεί να δημιουργηθεί:
- Ρητά με την εντολή throw
if (ammount < 0)
throw new NegativeAmmountException();
- Με την κλήση μιας μεθόδου που προκαλεί την εξαίρεση.
Για παράδειγμα η μέθοδος print μπορεί να προκαλέσει την
NullPointerException.
- Με την εκτέλεση κώδικα που δημιουργεί εξαίρεση.
Για παράδειγμα ο παρακάτω κώδικας θα δημιουργήσει μια
εξαίρεση ArrayIndexOutOfBoundsException.
int a[] = new int[10];
a[15] = 42;
Χειρισμός εξαιρέσεων
Σε μια μέθοδο της Java μια εξαίρεση μπορούμε:
- να τη χειριστούμε τοπικά
- να δηλώσουμε πως η δική μας μέθοδος δημιουργεί την συγκεκριμένη
εξαίρεση
- να την αγνοήσουμε αν είναι τύπου Error ή RuntimeException,
δηλαδή
μη ελεγχόμενη εξαίρεση (unchecked exception)
Τοπικός χειρισμός
Ο τοπικός χειρισμός γίνεται με μπλοκ try/catch/finally:
try {
// Κώδικας που μπορεί να δημιουργήσει την εξαίρεση
} catch (ExceptionClass_1 e) {
// Κώδικας που χειρίζεται την ExceptionClass_1
} catch (ExceptionClass_2 e) {
// Κώδικας που χειρίζεται την ExceptionClass_2
} finally {
// Κώδικας που εκτελείται πάντα στο τέλος
}
Παράδειγμα από τη μέθοδο readInt της βιβλιοθήκης BIO:
public static int readInt() {
int i = 0;
try {
i = Integer.parseInt(readString());
} catch (NumberFormatException e) {
System.err.println("Error reading Integer: " + e);
System.exit(1);
}
return (i);
}
Δήλωση εξαιρέσεων
Αν μια δική μας μέθοδος
- δε χειρίζεται μια εξαίρεση που μπορεί να δημιουργηθεί από μια μέθοδο που εμείς καλούμε ή
- προκαλεί ρητά μια εξαίρεση με τη χρήση της throw
τότε πρέπει να το δηλώσουμε με τη σύνταξη
myMethodName( /* ... */) throws Exception_1, Exception_2 { }
Παράδειγμα:
public static int parsePositiveInt(String s) throws NumberFormatException {
int i = parseInt(s);
if (i < 0)
throw new NumberFormatException();
}
Ολοκληρωμένο παράδειγμα εξαιρέσεων
class BadArgException extends Throwable {
public BadArgException() {}
public BadArgException(String msg) {
super(msg);
}
}
class Test {
/** Verify that the args table is not empty.
* @throws BadArgException if the table is empty.
*/
static void verifyArgs(String args[]) throws BadArgException {
if (args.length == 0)
throw new BadArgException("Empty table");
}
static public void main(String args[]) {
int exitCode = 0;
try {
int i;
verifyArgs(args);
for (i = 0; i < args.length; i++)
System.out.print(args[i]);
System.out.println();
} catch (BadArgException e) {
System.err.println("Bad argument " + e);
exitCode = 1;
} finally {
System.out.println("Argument processing done");
}
System.out.println("Program termination");
System.exit(exitCode);
}
}
Σχεδιασμός με εξαιρέσεις
- Στον κώδικά μας προκαλούμε μια εξαίρεση όταν παραβιάζεται μια
συνθήκη λειτουργίας του.
- Χειριζόμαστε εξαιρέσεις τύπου Error τοπικά.
- Χειριζόμαστε εξαιρέσεις τύπου Exception μέσα στα όρια του
αρθρώματος που υλοποιούμε
(έστω και με δημιουργία νέας εξαίρεσης, για να διασφαλίζουμε
το χειρισμό τους.
- Δεν επιτρέπουμε σε εξαιρέσεις τύπου Error να βγουν έξω από τα όρια του αρθρώματός μας· τις κλιμακώνουμε σε Exception.
Ισχυρισμοί
- Ένας ισχυρισμός (assertion) μας επιτρέπει να
τεκμηριώσουμε στον κώδικα του προγράμματος την άποψή μας για
τον τρόπο λειτουργίας του.
- Ο ισχυρισμός έχει τυπικά τη μορφή ενός κατηγορήματος που θεωρούμε
πως στη συγκεκριμένη στιγμή θα είναι αληθές.
Παράδειγμα:
b = a / 2;
assert (a >= 0 && b <= a) || (a < 0 && b > a);
- Ο ισχυρισμός επιτρέπει σε κάποιον που διαβάζει τον κώδικά μας να
καταλάβει καλύτερα πως λειτουργεί ο κώδικας.
- Επιπλέον ο μεταγλωττιστής μπορεί να δημιουργήσει κώδικα που ελέγχει
τους ισχυρισμούς κατά την εκτέλεση του προγράμματος.
Ισχυρισμοί στην πράξη
- Όσο ο κώδικας βρίσκεται στο στάδιο των δοκιμών ενεργοποιούμε τον έλεγχο
των ισχυρισμών για να εντοπίσουμε λάθη.
- Όταν ο κώδικας εκτελείται σε περιβάλλον παραγωγής απενεργοποιούμε τον
έλεγχο των ισχυρισμών για να μην έχουμε αρνητικές επιπτώσεις στην ταχύτητα
εκτέλεσης του προγράμματος.
- Με τη χρήση ισχυρισμών μπορούμε να ορίσουμε
- Προϋποθέσεις (preconditions)
- Συνθήκες που πρέπει να ισχύουν πριν την εκτέλεση
κάποιου τμήματος κώδικα (π.χ. μεθόδου).
- Μετασυνθήκες (postcondition)
- Συνθήκες που ξέρουμε πως θα ισχύουν μετά
την εκτέλεση κάποιου τμήματος κώδικα.
Ισχυρισμοί στη Java
- Στη Java ο ισχυρισμός δηλώνεται με τη δεσμευμένη λέξη
assert την οποία ακολουθεί με λογική τιμή που κανονικά πρέπει να
είναι αληθής.
- Μετά τη λογική τιμή μπορούμε να προσθέσουμε και μια
συμβολοσειρά που θα τυπωθεί στην οθόνη αν ο ισχυρισμός αποτύχει
(η συνθήκη βρεθεί ψευδής)
- Αν η συνθήκη κατά την εκτέλεση του προγράμματος είνα ψευδής,
τότε δημιουργείται μια εξαίρεση τύπου AssertionError
- Όταν χρησιμοποιούμε ισχυρισμούς
τους ενεργοποιούμε κατά την εκτέλεση να το εκτελούμε με την
εντολή:
Παράδειγμα χρήσης ισχυρισμών
/*
* Run With java -ea FindMax
*/
class FindMax {
/** Return the maximum number in non-empty array v */
public static int findMax(int v[]) {
int max = Integer.MIN_VALUE;
// Precondition: v[] is not empty
assert v.length > 0 : "v[] is empty";
// Precondition: max <= v[i] for every i
for (int i = 0; i < v.length; i++)
assert max <= v[i] : "Found value < MIN_VALUE";
// Locate the real maximum value
for (int i = 0; i < v.length; i++)
if (v[i] > max)
max = v[i];
// Postcondition: max >= v[i] for every i
for (int i = 0; i < v.length; i++)
assert max >= v[i] : "Found value > max";
return max;
}
// Test harness
public static void main(String argv[]) {
int t[] = new int[5];
t[0] = 4;
t[1] = -4;
t[2] = 145;
t[3] = 0;
t[4] = Integer.MIN_VALUE;
System.out.println("Max value is " + findMax(t));
}
}
Διεπαφές
- Μια διεπαφή (interface)
(ή διασύνδεση σε άλλα ελληνικά βιβλία)
είναι μια μονάδα σχεδιασμού.
- Η διεπαφή ορίζει μεθόδους που πρέπει να υλοποιήσει μια κλάση.
- Ο ορισμός μιας διεπαφής είναι παρόμοιος με τον ορισμό μιας κλάσης.
Καμιά όμως από τις μεθόδους δεν έχει σώμα.
Παράδειγμα, κινητήρας εσωτερικής καύσης:
interface InternalCombustionEngine {
void start();
void stop();
void setThrottle(int throttleLevel);
int getRPM();
}
Διεπαφές: κανόνες
- Όλες οι μέθοδοι χωρίς σώμα υπονοούνται ως
abstract
.
- Μέθοδοι με σώμα πρέπει να δηλώνονται ως
default
.
- Επιτρέπονται μέθοδοι static (από τη Java 8 και μετά)
- Τα πεδία πρέπει να είναι static και final.
- Τα πεδία αυτά χρησιμοποιούνται για τον ορισμό σταθερών των κλήσεων των
μεθόδων.
Υλοποίηση διεπαφών
Διάγραμμα υλοποίησης διεπαφών
Υλοποίηση πολλαπλών διεπαφών
Μια κλάση μπορεί να υλοποιήσει πολλές διεπαφές.
class Car implements
VehicleBody,
InternalCombustionEngine,
TransmissionSystem,
BreakSystem,
Console
{
}
class Truck implements
VehicleBody,
InternalCombustionEngine,
TransmissionSystem,
BreakSystem,
Console,
Container
{
}
Διάγραμμα υλοποίησης πολλαπλών διεπαφών
- Με την υποστήριξη πολλαπλών διεπαφών από μια κλάση,
μπορούμε στη Java να υλοποιήσουμε σχέδια που βασίζονται σε
πολλαπλή κληρονομικότητα (multiple inheritance)
Επέκταση διεπαφών
- Οι διεπαφές μπορούν να επεκταθούν,
ακριβώς όπως και οι κλάσεις.
interface Engine {}
interface InternalCombustionEngine extends Engine {}
interface ElectricEngine extends Engine {}
interface SteamEngine extends Engine {}
Διαφορά αφηρημένης κλάσης από μια διεπαφή
- Μια κλάση μπορεί να επεκτείνει μόνο μία (αφηρημένη) κλάση.
(Αλλιώς θα έπρεπε να καθορίζεται ποια από όμοιες μεθόδους που κληρονομεί
θα ισχύει.)
- Μια κλάση μπορεί να υλοποιήσει πολλαπλές διεπαφές.
(Διότι μια μέθοδος που υλοποιεί μπορεί να απαιτείται από πολλές διεπαφές.)
- Μια διεπαφή μπορεί να ορίσει συμπεριφορά σε πολλά σημεία της ιεραρχίας των
κλάσεων (π.χ. Comparable, Cloneable).
Αφηρημένες κλάσεις ή διεπαφές;
- Οι διεπαφές προδιαγράφουν συμπεριφορά.
- Οι (αφηρημένες) κλάσεις προδιαγράφουν (και υποστηρίζουν) υλοποίηση.
- Οι επιστρεφόμενοι αφηρηρημένοι τύποι καλό είναι να είναι διεπαφές.
- Οι αφηρημένοι τύποι ορισμάτων καλό είναι να είναι αφηρημένες κλάσεις.
Η αρχή της στιβαρότητας
H χρήση ορισμάτων αφηρημένων κλάσεων αλλά η επιστροφή διεπαφών
αιτιολογείται από την περίφημη
αρχή της στιβαρότητας του Jon Postel (
http://en.wikipedia.org/wiki/Robustness_principle):
Να είσαι συντηρητικός σ' αυτά που κάνεις εσύ και ανεκτικός σ' αυτά που δέχεσαι από τους άλλους (Be conservative in what you do, be liberal in what you accept from others).
Η παραπάνω αρχή θεωρείται από πολλούς ο θεμέλιος λίθος λειτουργίας του διαδικτύου.
Αιτιολόγηση χρήσης διεπαφών
Με βάση την αρχή της στιβαρότητας σχεδιάζουμε ως εξής.
- Όταν ένα άρθρωμα επιστρέφει αντικείμενα κάποιου αφηρημένου τύπου
που δεν είναι γνωστά στα αρθρώματα που το καλούν, συνιστάται αυτά να
είναι τύπου διεπαφής.
-
Έτσι η υλοποίηση των αρθρωμάτων που το καλούν δεν επηρεάζεται από
αλλαγές στο εσωτερικό του αρθρώματος που καλείται.
- Όταν ένα άρθρωμα δέχεται αντικείμενα κάποιου αφηρημένου τύπου
του οποίου την υλοποίηση ελέγχει
συνιστάται αυτά να είναι τύπου αφηρημένης κλάσης.
-
Έτσι αν το καλούμενο άρθρωμα επεκταθεί στο μέλλον, αυτό θα μπορεί να παράσχει
συμβατότητα στα αρθρώματα που το καλούν με την υλοποίηση στην κλάση
μεθόδων συμβατότητας.
Παράδειγμα στιβαρού σχεδιασμού
interface InternalCombustionEngine { /* ... */ }
abstract class FourStrokeEngine implements InternalCombustionEngine { /* ... */ }
class GeneralMotorsLS3 extends FourStrokeEngine { /* ... */ }
class ChevroletCorvette implements InternalCombustionEngine {
FourStrokeEngine engine;
/*
* Receive abstract class so that changes in it can transparently
* update all classes that extend the abstract class.
*/
void setEngine(FourStrokeEngine e) { engine = e; }
// Return interface rather than concrete class to allow widest possible use
InternalCombustionEngine getEngine() { return engine; }
public static int main(String[] args) {
var corvette = new ChevroletCorvette();
corvette.setEngine(new GeneralMotorsLS3());
return 0;
}
}
Πακέτα
- Σε μεγάλα προγράμματα η πληθώρα των κλάσεων που ορίζονται
οδηγεί συχνά σε ρύπανση του χώρου ονοματοδοσίας (namespace polution)
- Αυτό συμβαίνει όταν το ίδιο όνομα χρησιμοποιείται με διαφορετικούς
τρόπους σε δύο διαφορετικά τμήματα του προγράμματος.
- Τοποθετόντας την υλοποίηση σε ένα πακέτο (package)
μπορούμε
- να περιορίσουμε το χώρο στον οποίο είναι ορατά τα ονόματα
της υλοποίησής μας
- να ομαδοποιήσουμε σχετικές κλάσεις
- να επιτρέψουμε σε τρίτους να χρησιμοποιήσουν τις δικές μας
κλάσεις ακόμα και αν το όνομά τους ταυτίζεται με άλλες
Ορισμός κλάσεων σε πακέτα
- Ο ορισμός μιας κλάσης σε ένα πακέτο γίνεται αν στο αντίστοιχο αρχείο
προσθέσουμε τη λέξη package και το όνομα του πακέτου
package gr.aueb.dmst.dds;
class Shape {}
- Έχει επικρατήσει η ονομασία πακέτων να βασίζεται στο διαδικτυακό
όνομα του οργανισμού.
- Με τον τρόπο αυτό ελαχιστοποιούμε την πιθάνοτητα
σύγκρουσης ονομάτων (name collision).
Χρήση κλάσεων σε πακέτα
- Για να χρησιμοποιήσουμε μια κλάση που βρίσκεται μέσα σε ένα πακέτο
μπορούμε
- Να προτάξουμε το όνομα του πακέτου πριν την κλάση
class DrawingEditor {
gr.aueb.dmst.dds.Shape s;
}
- Να δηλώσουμε στην αρχή του αρχείου μας πως θέλουμε να
κάνουμε ορατές όλες τις κλάσεις του πακέτου με το ονομά τους,
χωρίς πρόθεμα.
Αυτό γίνεται με την εντολή import.
import gr.aueb.dmst.dds.Shape; // or .*
class DrawingEditor {
Shape s;
}
Κανόνες χρήσης πακέτων
- Κλάσεις ή πεδία (μέθοδοι και ιδιότητες) που δεν έχουν οριστεί public
φαίνονται μόνο μέσα στο πακέτο στο οποίο ορίζονται.
- Με τον τρόπο αυτό μπορούμε να προστατεύσουμε τμήματα της υλοποίησής μας
από εξωτερικές παρεμβάσεις.
- Κώδικας που δε βρίσκεται σε ένα συγκεκριμένο πακέτο θεωρείται πως βρίσκεται
στο ανώνυμο πακέτο.
Απλοποίηση χρήσης στατικών πεδίων
Με την εντολή import static
μπορούμε να εισάγουμε
στατικές μεθόδους και πεδία ώστε να χρησιμοποιηθούν χωρίς το πρόθεμα της
κλάσης.
import static java.lang.Math.*;
class Sqrt {
public static void main(String args[]) {
System.out.println(sqrt(2));
}
}
Άσκηση: παραγωγή και έλεγχος εξαιρέσεων
Άσκηση 6 και 16
Μπορείτε να κατεβάσετε το αντίστοιχο αρχείο και να στείλετε τους
βαθμούς σας από τους δεσμούς που βρίσκονται στη
σελίδα των ασκήσεων.
Βιβλιογραφία
- Rogers CadenheadΠλήρες εγχειρίδιο της Java 12Όγδοη έκδοση. Εκδόσεις Μ. Γκιούρδας, Αθήνα 2023. Κεφ. 6, 7.
- Joyce Farrell. Java: Εκμάθηση με πρακτικά παραδείγματα. 2η έκδοση. Εκδόσεις Κριτική, Αθήνα 2024. Κεφ. 11.
- Herbert Schildt. Οδηγός της Java 7. 5η έκδοση. Εκδόσεις Γκιούρδας Μ., Αθήνα 2012. Κεφ. 7, 8, 9.
- Harvey M. Deitel και Paul J. Deitel. Java Προγραμματισμός, 6η έκδοση. Εκδόσεις Μ. Γκιούρδας, Αθήνα 2005. Κεφάλαια 10, 13.
- Else Lervik και Vegard B. Havdal Java με UML. Εκδόσεις Κλειδάριθμος 2005. Κεφάλαιο 8.
- Γιώργος Λιακέας
Εισαγωγή στην Java. σ. 153, 172-173 371-412,
Εκδόσεις Κλειδάριθμος 2001.
- Γιάννη Κάβουρα. Προγραμματισμός με Java. Εκδόσεις Κλειθάριθμος, Αθήνα 2003. σ. 328, 355-366, 485-492
- Rogers Cadenhead και Laura Lemay Πλήρες εγχειρίδιο της Java 2 Εκδόσεις Μ. Γκιούρδας, Αθήνα 2003. σ. 141-167, 179-208.
- K. N. King.
Java Programming: from the Beginning, pages 61–62, 300–305,
418–420, 465–466, 493–494.
W. W. Norton & Company, New York, NY, USA, 2000.
- Diomidis
Spinellis.
An exception handling
revelation.
Available online https://www.spinellis.gr/blog/20240205/, 2024.
Blog entry.
- Tetsuo Tamai.
Social impact of information system failures.
Computer, 42(6):58–65, 2009.
(doi:10.1109/MC.2009.199 (http://dx.doi.org/10.1109/MC.2009.199))
Περιεχόμενα