Skip to the content.

4.1 Εξαιρέσεις (Exceptions)

© Γιάννης Κωστάρας


Δ ->

Όπως και στην πραγματική ζωή, τα πράγματα δεν έρχονται πάντα όπως τα περιμένουμε. Μια εξαίρεση είναι ένα μη αναμενώμενο γεγονός, μια ανώμαλη συνθήκη, που διαταράσει την κανονική ροή του προγράμματος. Είναι ένας τρόπος να επικοινωνήσουμε ένα λάθος. Π.χ. ανάγνωση ενός αρχείου που δεν υπάρχει, επικοινωνία με κάποιον διακομιστή (server) ενώ δεν υπάρχει δικτυακή επικοινωνία, γέμισμα της μνήμης ή του δίσκου, διαίρεση με το μηδέν κλπ.

Όσοι έχουν/είχαν την “τύχη” να δουλεύουν με γλώσσες προγραμματισμού που δεν διαχειρίζονται εξαιρέσεις, βλέπουν ότι στις άτυχες αυτές περιπτώσεις το πρόγραμμα “κρασάρει” και θα πρέπει να ελέγξουν τα υπολείμματα του σωρού (heap dump) ή του πυρήνα (core dump) για να δουν για ποιο λόγο “κράσαρε” το πρόγραμμα.

Είναι πολύ σημαντικό να μην αγνοείτε τις εξαιρέσεις αλλά να τις αντιμετωπίζετε ώστε τα προγράμματά σας να μην “κρασάρουν” αλλά να τερματίζουν ομαλά. Όταν λέμε ότι πρέπει να διαχειριζόμαστε τις εξαιρέσεις, δεν εννοούμε να διορθώνουμε το πρόβλημα σώνει και καλά, απλά ότι πρέπει να βρίσκουμε εναλλακτικούς τρόπους ώστε να μπορεί το πρόγραμμα να συνεχίσει την εκτέλεσή του αν γίνεται. Π.χ. ας υποθέσουμε ότι το πρόγραμμά μας θα πρέπει να ανακτήσει ένα αρχείο από το διαδίκτυο. Εκείνη τη στιγμή όμως, ο διακομιστής (server) που διαθέτει το αρχείο δεν είναι διαθέσιμος (offline). Θα πρέπει να διαχειριστούμε αυτή την εξαίρεση της εκτέλεσης του προγράμματός μας, διαθέτοντας εναλλακτικά ένα τοπικό αρχείο.

try {
 // διάβασε το αρχείο από το διαδίκτυο
} catch (FileNotFoundException e) {
 // διάβασε το αρχείο τοπικά
}

ή

void readFile(String filename) throws IOException {
	//...
}

Λέξεις κλειδιά:

Ιεραρχία κλάσεων εξαιρέσεων

Εικόνα 4.1.1 Ιεραρχία κλάσεων εξαιρέσεων

Όπως βλέπουμε στην παραπάνω εικόνα, όλες οι εξαιρέσεις κληρονομούν από την κλάση Throwable:

public class Throwable {
	// Constructors
	Throwable();
	Throwable(String message);
	Throwable(String message, Throwable cause);
	Throwable(Throwable cause);
	
	// Methods
	String getMessage();
	void printStackTrace();
	Throwable getCause();
	StackTraceElement[] getStackTrace();
	Throwable[] getSuppressed();
}

Εξαιρέσεις της κλάσης Error και των υποκλάσεών της εμφανίζονται στην περίπτωση που συμβεί λάθος στην εικονική μηχανή της Java (JVM), π.χ. δεν υπάρχει άλλη διαθέσιμη μνήμη. Σε αυτήν την περίπτωση δεν μπορούμε να κάνουμε τίποτα. Η ΕΜ “κράσαρε” με core dump. Συνήθως δημιουργείται ένα αρχείο καταγραφής της μορφής hs_er_pid<process_id>.log.

Ο προγραμματιστής ασχολείται μόνο με εξαιρέσεις τύπου Exception. Αυτές διακρίνονται σε δυο κατηγορίες:

Έγερση Εξαιρέσεων

Ο παρακάτω κώδικας εγείρει (raises) μια εξαίρεση

jshell> double arcsine(double sine)  {
	if (sine < -1.0 || sine > 1.0) 
		throw new IllegalArgumentException("sine must be in [-1.0, 1.0]");
	// υπολόγισε την γωνία
	return 0.0;
}

jshell> arcsine(2.0)
|  Exception java.lang.IllegalArgumentException: sine must be in [-1.0, 1.0]
|        at arcsine (#3:3)
|        at (#4:1)

Όταν συμβεί μια εξαίρεση, τότε οι γραμμές που την ακολουθούν στο ίδιο μπλοκ, δεν εκτελούνται. Αν δεν έχουμε μεριμνήσει να διαχειριστούμε την εξαίρεση, τότε το πρόγραμμά μας τερματίζεται, όπως στο παραπάνω παράδειγμα.

Διαχείριση Εξαιρέσεων

Η Java επιτρέπει τη διαχείριση εξαιρέσεων στο ίδιο ή σε κάποιο άλλο μέρος (βλ. κλάση) του προγράμματος. Διαχειριζόμαστε μια εξαίρεση μέσα σ’ ένα μπλοκ try-catch. Ας δούμε πώς μπορούμε να διαχειριστούμε την παραπάνω εξαίρεση:

jshell> double arcsine(double sine)  {
	try {
		if (sine < -1.0 || sine > 1.0) 
			throw new IllegalArgumentException("sine must be in [-1.0, 1.0]");
		// υπολόγισε την γωνία
	} catch (IllegalArgumentException iae) {
		System.err.println(iae.getMessage());
	}
	return 0.0;
}
|  created method arcsine(double)

jshell> arcsine(2.0)
sine must be in [-1.0, 1.0]
$6 ==> 0.0

Στο παραπάνω παράδειγμα, διαχειριζόμαστε την εκτέλεση που εγείρεται στη μέθοδό μας, με αποτέλεσμα το πρόγραμμα να μην κρασάρει. Ας δούμε άλλο ένα παράδειγμα:

jshell> double arcsine(double x) {
	try {
		double angle = arcsine(x);
		return angle;	// αν συμβεί εξαίρεση στην προηγούμενη γραμμή, αυτή η γραμμή δεν εκτελείται και ο έλεγχος περνάει στην catch
	} catch (IllegalArgumentException e) {
		e.printStackTrace();
		return 0.0;
	}
}
|  created method arcsine(double)

jshell> arcsine(2.0)
|  Exception java.lang.StackOverflowError

Όταν συμβεί μια εξαίρεση, τότε οι γραμμές που την ακολουθούν στο ίδιο μπλοκ, δεν εκτελούνται και ο έλεγχος περνάει στο μπλοκ catch που διαχειρίζεται την εξαίρεση. Στο ίδιο try-catch μπλοκ μπορούμε να διαχειριστούμε πολλές εξαιρέσεις. Αν δεν διαχειριστούμε μιαν εξαίρεση στο try-catch μπλοκ που την καλύπτει, τότε η διαδικασία περιγράφεται ακολούθως.

Στην παρακάτω εικόνα βλέπουμε μια κανονική εκτέλεση ροής χωρίς έγερση εξαιρέσεων. Η μέθοδος main() καλεί μια άλλη μέθοδο methodA() η οποία με τη σειρά της καλεί μια μέθοδο methodB.

Εικόνα 4.1.2 Κανονική ροή προγράμματος χωρίς έγερση εξαιρέσεων

Στη Εικόνα 4.1.3 βλέπουμε πώς γίνεται η διαχείριση μιας εξαίρεσης τοπικά. Μια εξαίρεση εγείρεται στη μέθοδο methodB() η οποία όμως διαχειρίζεται από ένα catch μπλοκ αυτής της μεθόδου.

Εικόνα 4.1.3 Τοπική διαχείριση εξαιρέσεων

Στη Εικόνα 4.1.4 βλέπουμε πώς γίνεται η διαχείριση μιας εξαίρεσης απομακρυμένα. Μια εξαίρεση εγείρεται στη μέθοδο methodB() η οποία όμως δε διαχειρίζεται από κανένα catch μπλοκ αυτής της μεθόδου με αποτέλεσμα η διαχείρισής της να μεταφέρεται στη καλούσα μέθοδο. Καθώς δε γίνεται διαχείριση εξαιρέσεων από τη μέθοδο methodA() η ροή μεταφέρεται στα catch μπλοκ της main() όπου εκεί, ευτυχώς, υπάρχει catch μπλοκ που διαχειρίζεται την εξαίρεση τύπου ExceptionC.

Εικόνα 4.1.4 Απομακρυσμένη διαχείριση εξαιρέσεων

Στην παρακάτω εικόνα βλέπουμε ότι δεν υπάρχει catch μπλοκ που να μπορεί να διαχειριστεί την εξαίρεση που εγείρεται στη μέθοδο methodB() με αποτέλεσμα το πρόγραμμα να τερματίζεται.

Εικόνα 4.1.5 Χωρίς διαχείριση εξαιρέσεων

Συνίσταται να διαχειρίζεστε πάντα τις ελεγχόμενες (checked) εξαιρέσεις (βλ. και τις συμβουλές παρακάτω). Ας δούμε ένα παράδειγμα:

System.out.println("Γειά σας");
System.out.println(10/0);
System.out.println("Αντίο");

και το αποτέλεσμα:

Γειά σας
Exception in thread "main" java.lang.ArithmeticException: / by zero
	at Main.main(Main.java:5)
	Java returned: 1

Αντιθέτως, αν διαχειριστούμε την εξαίρεση:

try {
    System.out.println("Γειά σας");
    System.out.println(10 / 0);
    System.out.println("Αντίο");
} catch (ArithmeticException e) {
    System.err.println("Διαίρεση με το μηδέν");
}

τότε το πρόγραμμά μας τερματίζει κανονικά:

Γειά σας
Διαίρεση με το μηδέν

Μια μέθοδος μπορεί να δηλώσει πολλές εξαιρέσεις. Όταν εμφανίζεται μια εξαίρεση, διάφοροι πόροι μπορεί να μην έχουν προλάβει να καθαριστούν (π.χ. ανοικτά αρχεία, ανοικτές συνδέσεις με βάσεις δεδομένων κλπ.). Χρησιμοποιήστε το μπλοκ finally για να τους καθαρίσετε:

try {
	readFile(filename);
} catch (FileNotFoundException fe) {
	System.err.print("File not found " + fe.getMessage());
	fe.printStackTrace();
} catch (IOException ioe) {
	ioe.printStackTrace();
} catch (Exception e) {
	e.printStackTrace();
} finally {
	try {
		file.close();            // μπορεί να υπάρξει λάθος κατά το κλείσιμο του αρχείου
	} catch (Exception ex) {
		ex.printStackTrace();
	}		
}

Η σειρά διαχείρισης των εξαιρέσεων έχει σημασία. Πάντα να ξεκινάτε με την πιο ειδικευμένη εξαίρεση και μετά να ακολουθούν οι πιο γενικευμένες (δηλ. από υποκλάση προς υπερκλάση). Π.χ. στο παραπάνω παράδειγμα, FileNotFoundException extends IOException extends Exception. Διαφορετικά θα λάβετε το λάθος exception XXX has already been caught.

try {
	System.out.println(10/0);
} catch (Exception e) {
	e.printStackTrace();
} catch (ArithmeticException ae) {
	ae.printStackTrace();
}
|  Error:
|  exception java.lang.ArithmeticException has already been caught
|  } catch (ArithmeticException ae) {
|    ^-------------------------------...

Η έκδοση 7 παρέχει βελτιωμένη διαχείριση εξαιρέσεων. Η εντολή try-with-resources κλείνει αυτόματα τους πόρους που ανοίξαμε για επεξεργασία (π.χ. αρχείο, socket κλπ). Προηγούμενα, όταν χρησιμοποιούσαμε στον κώδικά μας εξωτερικούς πόρους, έπρεπε να φροντίσουμε να τους κλείσουμε ώστε να μπορούν να ξαναχρησιμοποιηθούν. Πλέον, αυτό γίνεται αυτόματα μ’ αυτή την εντολή την οποία θα εκτιμήσουν ιδιαίτερα ξεχασιάρηδες προγραμματιστές κι όχι μόνο.

Ας δούμε τον παρακάτω κώδικα σε Java 6 που δημιουργεί ένα αντίγραφο ενός αρχείου test.txt (θα μάθουμε περισσότερα για διαχείριση αρχείων στα μαθήματα της επόμενης εβδομάδας):

InputStream in = null;
try {
  final File input = 
       new File("test.txt");
  in = new FileInputStream(input);
  final File output = 
       new File("backup.txt");
  final OutputStream out = 
       new FileOutputStream(output);
  try {
   byte[] buf = new byte[4096];
   int len;
   while ((len = in.read(buf)) >= 0)
    out.write(buf, 0, len);
  } catch (IOException ioEx) {
   // Handle this exception 
  } finally  {
     try {
       out.close();
     } catch (
         IOException closeOutEx) {
     // Suppress this exception
     }
  }
} catch (FileNotFoundException fnfEx) {
     // Handle this exception
} catch (IOException openEx) {
     // Handle this exception
} finally  {
    try {
      if (in != null) in.close();
    } catch (IOException closeInx) {
     // Suppress this exception
    }
}

Σύμφωνα με το Νόμο του Μέρφυ, μπορεί να συμβεί κάτι απ’ τα ακόλουθα:

α. Ν’ αποτύχει το InputStream:

β. Ν’ αποτύχει το OutputStream:

γ. Κάποιος συνδυασμός των παραπάνω.

Ας δούμε πώς μεταφράζεται ο παραπάνω κώδικας σε Java 7 ή νεώτερη έκδοση:

final File input = new File("test.txt");
final File output = new File("backup.txt");
try (
  FileOutputStream out = 
     new FileOutputStream(output);
  InputStream in = 
     new FileInputStream(input);) {
     byte[] buf = new byte[4096];
     int len;
     while ((len=in.read(buf))>0) {
       out.write(buf, 0, len);
   }
} catch (IOException e) {
   // If file is not found
}

Όπως βλέπετε στον παραπάνω κώδικα, η εντολή

try (εντολές) { } 

δεν απαιτεί μπλοκ finally { } για να κλείσετε τους πόρους που ανοίξατε. Ο κώδικας αυτός κλείνει τον πόρο ακόμα και στην περίπτωση που συνέβη κάποια εξαίρεση.

Οι εντολές πρέπει να υλοποιούν τη διεπαφή AutoCloseable η οποία επεκτείνει τη διεπαφή Closeable και περιέχει μια μέθοδο close() η οποία αναλαμβάνει να κλείσει τους ανοικτούς πόρους.

Ας δούμε και το παρακάτω παράδειγμα:

try (
 BufferedReader br = 
  new BufferedReader(
   new InputStreamReader(
    new FileInputStream(
               "nofile.txt")))) {
  String strLine;
  while ((strLine = br.readLine()) != null) {
    System.out.println(strLine);
   }
} catch (IOException e) { 
   // If file is not found
   e.printStackTrace();
}

Αν το αρχείο nofile.txt δεν υπάρχει, τότε ο παραπάνω κώδικας θα εγείρει μια εξαίρεση java.io.FileNotFoundException και θα κλείσει αυτόματα τον πόρο που αναφέρεται από τη μεταβλητή br. Τι γίνεται όμως με τους πόρους που άνοιξαν οι InputStreamReader και FileInputStream; Καθώς δεν αποθηκεύτηκαν σε κάποια μεταβλητή θα παραμείνουν ανοικτοί! Γι’ αυτό το λόγο, ο παραπάνω κώδικας θα πρέπει να γραφτεί ως εξής για να κλείσουν σωστά οι πόροι σε περίπτωση εξαίρεσης:

try (
 FileInputStream fis = 
   new FileInputStream(
               "nofile.txt");
 InputStreamReader isr = 
   new InputStreamReader(fis);
 BufferedReader br = 
  new BufferedReader(isr);) {
  String strLine;
  while ((strLine = br.readLine()) != null) {
    System.out.println(strLine);
   }
} catch (IOException e) { 
   // If file is not found
   e.printStackTrace();
}

Η Java επιτρέπει επίσης την ομαδοποίηση διαφορετικών τύπων εξαιρέσεων ενός μπλοκ κώδικα, όπως π.χ.

try {
//...
} catch (FileNotFoundException | IOException e) {
//...
}

Συμβουλές χρήσης εξαιρέσεων

try {
... 
} catch (LowerLevelException e) {
	throw new HigherLevelException(...);
}
try {
//...
} catch (IOException ioe) {
}

Αν το κάνετε, όταν το πρόγραμμά σας “κρασάρει” δε θα γνωρίζετε σε ποια γραμμή του κώδικά σας οφείλεται.

Π.χ.

Boolean flag = false;
try {
	flag = null; // μετά από κάποιον υπολογισμό
	// άλλες εντολές που μπορεί να εμφανίσουν NullPointerException
	// με αποτέλεσμα ο έλεγχος να μεταφέρεται στο catch
	// και ο παρακάτω έλεγχος να μην εκτελείται
	if (!flag) {
		throw new SecurityException("Invalid Credentials");
	}
} catch(NullPointerException npe) {
	// καταγραφή της εξαίρεσης και συνέχιση της εκτέλεσης
}
// κώδικας που εκτελείται ενώ δε θα 'πρεπε να εκτελεστεί
// στην περίπτωση που flag == false

Καλύτερα να ελέγχετε για null.

Π.χ. αντί για

private void myMethod() throws Exception {
}

προτιμήστε:

private void myMethod() throws IOException {
}

ή όποια άλλη πιο ειδικευμένη εξαίρεση μπορεί να εγείρει η μέθοδός σας.

Επίσης, μη διαχειρίζεστε κάποια από αυτές τις εξαιρέσεις καθώς υπάρχει ο κίνδυνος να διαχειριστείτε κάποια εξαίρεση που είναι προς διαχείριση σε άλλο επίπεδο του προγράμματος:

double avg(int sum, int num) throws ArithmeticException, IOException {
	double av = sum/num;
	// εντολές που μπορεί να εγείρουν IOException ή άλλου είδους εξαιρέσεις
	return av;
}

try {
	avg(s,n);
} catch (Exception e) {
	// διαχειρίζεται πέραν των ArithmeticException, 
	// IOException και όλες τις πιθανές άλλες εξαιρέσεις που 
	// ενδέχεται να πρέπει να διαχειριστούν σε άλλο σημείο
}

Συνήθεις Εξαιρέσεις

Συνήθεις Μη Ελεγχόμενες Εξαιρέσεις

Συνήθεις Ελεγχόμενες Εξαιρέσεις

Συνήθη Λάθη

Τεκμηρίωση Εξαιρέσεων

Για την τεκμηρίωση των εξαιρέσεων, η γλώσσα διαθέτει την ετικέττα javadoc @throws, π.χ.

/**
  * Sets the user's age.
  * @param age the age of the user
  * @throws IllegalArgumentException if age is not in [18-65]
 */
public void setAge(short age) {
  if (age < 18 || age > 65)
   	  throw new IllegalArgumentException("A valid user of the service must be [18-65].");
  this.age = age;
} 

Καλό είναι να τεκμηριώνετε και τις ελεγχόμενες (checked) και τις μη ελεγχόμενες (unchecked) εξαιρέσεις. Οι τελευταίες δηλώνουν τις προσυνθήκες που πρέπει να είναι αναγκαίες για την επιτυχημένη εκτέλεση της μεθόδου. Η αναφορά [2] υπογραμμίζει ότι πρέπει οπωσδήποτε να τεκμηριώνονται οι ελεγχόμενες εξαιρέσεις, αλλά όχι οι μη ελεγχόμενες.

Δημιουργία Νέων Εξαιρέσεων

Καλό είναι ν’ αποφεύγετε να δημιουργείτε νέες εξαιρέσεις και να προσπαθήτε να χρησιμοποιείτε εκείνες που παρέχονται από τη γλώσσα. Στην περίπτωση που δεν σας καλύπτουν, τότε μπορείτε να δημιουργήσετε τις δικές σας, π.χ.

public class ArgumentOutOfRangeException extends IllegalArgumentException {
	
	public ArgumentOutOfRangeException(int lower, int upper, int val) {
		super("Out of range exception: " + val + " must be in the range [" + lower + ", " + upper + "]");
	}
}

Στην περίπτωση αυτή, συνίσταται να δημιουργείτε RuntimeExceptions (κι όχι checked exceptions).

Εξαιρέσεις και κληρονομικότητα

Τα πράγματα περιπλέκονται όταν έχουμε να κάνουμε με εξαιρέσεις που εγείρονται στις γονικές κλάσεις. Ας δούμε τι κανόνες ισχύουν:

Μια μέθοδος σε μια υποκλάση (subclass) δεν μπορεί να δηλώσει ότι εγείρει μια ελεγχόμενη (checked) εξαίρεση αν αυτή δεν ορίζεται στην υπερσκελισμένη μέθοδο της υπερκλάσης (superclass). Π.χ.

jshell> class A { 
   ...>    void m() throws IOException{ } 
   ...> } 
|  created class A

jshell> class B extends A { 
   ...>    void m() throws Exception; 
   ...> }
|  Error:
|  m() in B cannot override m() in A
|    overridden method does not throw java.lang.Exception
|     void m() throws Exception; 
|     ^------------------------^

Η μέθοδος m() δηλώνει ότι εγείρει IOException, αλλά όχι την Exception που δηλώνει η υπερσκελισμένη μέθοδος της υποκλάσης Β. Αυτό δεν επιτρέπεται με αποτέλεσμα το λάθος μεταγλώττισης που φαίνεται. Μπορείτε να σκεφτείτε για ποιο λόγο υπάρχει αυτός ο κανόνας; Φανταστείτε ότι η μέθοδος a.m() καλείται από κώδικα όπως ο παρακάτω (όπου a είναι ένα αντικείμενο τύπου Α):

A a = new A();
try {
   a.m();
} catch (IOException ex) {

}

Όπως ίσως γνωρίζετε από τα μαθήματα της 2ης εβδομάδας, μπορείτε να γράψετε και τον παρακάτω κώδικα:

A b = new B();
try {
   b.m();
} catch (IOException ex) {

}

Η μέθοδος b.m() όμως, μπορεί να εγείρει και Exception η οποία δεν διαχειρίζεται από τον παραπάνω κώδικα, και καθώς πρέπει να διαχειριστεί επειδή η Exception είναι ελεγχόμενη εξαίρεση, προκαλείται λάθος μεταγλώττισης. Καθώς ο συγγραφέας της κλάσης Α δεν μπορεί να γνωρίζει πως μπορούν να υπερσκελιστούν οι μέθοδοι της κλάσης Α από άλλες υποκλάσεις, και για να μην δημιουργούνται τέτοιου είδους προβλήματα, οι σχεδιαστές της γλώσσας πρόσθεσαν αυτόν τον κανόνα, ο οποίος γενικεύεται στον παρακάτω.

Μια υπερσκελισμένη (overriden) μέθοδος δεν μπορεί να δηλώσει ότι εγείρει μια νέα ή πιο γενικευμένη ελεγχόμενη εξαίρεση απ’ ότι η μέθοδος της υπερκλάσης την οποία υπερσκελίζει. Π.χ. αν η superMethod() throws IOException, η υπερσκελισμένη (overriding) μέθοδος δεν μπορεί να δηλώσει ότι εγείρει Exception ή Throwable καθώς αυτές είναι πιο γενικές εξαιρέσεις. Μπορεί να εγείρει όμως άλλες εξαιρέσεις, π.χ. RuntimeException ή πιο ειδικευμένες ελεγχόμενες εξαιρέσεις όπως FileNotFoundException ή να μην εγείρει καμία νέα εξαίρεση.

Μια υπερσκελισμένη μέθοδος μπορεί να μην εγείρει καμία εξαίρεση, να εγείρει νέες μη ελεγχόμενες εξαιρέσεις, ή να εγείρει την ίδια εξαίρεση με την υπερσκελισμένη μέθοδο της υπερκλάσης, ή υποεξαιρέσεις αυτής, αλλά όχι πιο γενικευμένες ελεγχόμενες εξαιρέσεις από την μέθοδο της υπερκλάσης. Ο κανόνας αυτός δεν ισχύει φυσικά για τις μη ελεγχόμενες εξαιρέσεις όπου δεν υπάρχει κανένας περιορισμός.

Ο κανόνας αντιστρέφεται στην περίπτωση των μεθόδων κατασκευής (constructors). Μια υπερσκελισμένη μέθοδος δεν μπορεί να εγείρει μια πιο γενικευμένη ελεγχόμενη εξαίρεση, ενώ μια μέθοδος κατασκευής (constructor) μιας υποκλάσης δεν μπορεί να εγείρει μια πιο ειδικευμένη ελεγχόμενη εξαίρεση. Π.χ.

jshell> class Super { 
   public Super() throws IOException{ } 
}  
|  created class Super

jshell> class Sub extends Super { 
     public Sub() { }  
}
|  Error:
|  unreported exception java.io.IOException; must be caught or declared to be thrown
|     public Sub() { } 

Μπορείτε να σκεφτείτε γιατί;

Αν καλέσουμε τον constructor της Sub, π.χ.

new Sub();

αυτός θα καλέσει τον constructor της Super ο οποίος μπορεί να εγείρει μια εξαίρεση η οποία δεν διαχειρίζεται από τον κώδικα που την καλεί. Ίσως να σκεφτείτε να γράψετε το εξής:

class Sub extends Super { 
     public Sub() { 
	 	try {
			super();
		} catch (IOException ex) {}
	 }  
}
|  Error:
|  call to super must be first statement in constructor
|  			super();
|     ^-----^
|  Error:
|  unreported exception java.io.IOException; must be caught or declared to be thrown
|       public Sub() { 
|          

Επομένως, η μέθοδος κατασκευής της Sub θα πρέπει να δηλώσει τουλάχιστον ότι μπορεί να εγείρει την εξαίρεση που δηλώνει ο constructor της Super. Μπορεί να δηλώσει μια πιο γενικευμένη εξαίρεση καθώς έτσι μπορεί να διαχειριστεί και η εξαίρεση του super contructor.

class Sub extends Super { 
   // IOException είναι ΟΚ, όχι όμως FileNotFoundException. Πρέπει επίσης να δηλώσει ότι εγείρει IOException (ή κάποια υποκλάση της) 
   public Sub() throws IOException, Exception{ }  
}

Π.χ. ο παρακάτω κώδικας μπορεί να διαχειριστεί και IOException που πιθανόν να προκληθεί από τον constructor της Super.

try {
   new Sub();
} catch (Exception e) {}

Ισχυρισμοί (Assertions)

Οι Ισχυρισμοί (Assertions) βοηθούν στην ανίχνευση λαθών. Αν εμφανιστεί κάποιο λάθος που ελέγχει ένας ισχυρισμός, τότε εμφανίζεται AssertionError (AssertionError extends Error).

Οι ισχυρισμοί μπορούν να (απ)ενεργοποιηθούν σε επίπεδο κλάσης, πακέτου, ή ακόμα και ΕΜ (JVM).

java -ea:<package> ...		// ενεργοποίηση ισχυρισμών για το πακέτο
java -da:<package> ...    	// απενεργοποίηση ισχυρισμών για το πακέτο
java -esa ...		// ενεργοποίηση ισχυρισμών συστήματος 
java -dsa ...		// απενεργοποίηση ισχυρισμών συστήματος

Σύνταξη:

assert Expression

assert Expression : ErrorMessage 

Π.χ.

assert var1 > 0;

Μπορείτε να χρησιμοποιήσετε τους ισχυρισμούς σε μεθόδους για να ελέγξετε αν ικανοποιούνται οι προσυνθήκες (pre-conditions) ή οι μετασυνθήκες (post-conditions) καθώς και αξιώματα (invariants):

Οι ισχυρισμοί δεν μπορούν ν’ αντικαταστήσουν τις συνθήκες. Να έχετε πάντοτε υπόψιν σας ότι οι ισχυρισμοί μπορούν να απενεργοποιούνται. Καλό είναι να ενεργοποιούνται όταν ακόμα τεστάρετε τον κώδικα, και να απενεργοποιούνται όταν το πρόγραμμα είναι στην παραγωγή.

Προαιρετικό (Optional)

Ο Sir Tony Hoare, ο δημιουργός του null, θεωρεί αυτή του τη δημιουργία ως το μεγαλύτερο λάθος του (βλ. εδώ και εδώ).

Η κλάση Optional<T> εισήχθηκε στην έκδοση 8 με σκοπό να αντιμετωπίσει μια από τις πιο συνηθισμένες εξαιρέσεις, την NullPointerException. Η κλάση Optional<T> αναπαριστά μια αμετάβλητη αποθήκη μνήμης που μπορεί να φυλάσσει ένα όχι null στοιχείο T ή τίποτα (άδεια). Μια προαιρετική (optional) μεταβλητή είναι στην ουσία μια αμετάβλητη (immutable) συλλογή (αν και δεν κληρονομεί από την διεπαφή Collection<T>) που μπορεί να αποθηκεύσει το πολύ ένα στοιχείο.

jshell> String s1 = null
s1 ==> null

jshell> String s2 = "Hello"
s2 ==> "Hello" 

jshell> Optional<String> o1 = Optional.ofNullable(s1)
o1 ==> Optional.empty

jshell> Optional<String> o2 = Optional.of(s2)
o2 ==> Optional[Hello]

jshell> o1.isPresent()
$1 ==> false

jshell> o2.isPresent()
$2 ==> true

jshell> o1.orElse("") + o2.get()    
$3 ==> "Hello"					// αντί για NullPointerException

Είναι καλή ιδέα οι μεθόδοι σας να επιστρέφουν Optional<T> αντί για Τ αν νομίζετε ότι η Τ μπορεί να είναι null δηλ. όχι τιμή και οι χρήστες αυτής της μεθόδου πρέπει να ελέγξουν αυτήν την περίπτωση. Π.χ. η παρακάτω μέθοδος αναζητά σειριακά ένα στοιχείο σε μια συλλογή, κι αν δεν το βρει επιστρέφει null:

public static <E extends Comparable<E>> E find(E el, Collection<E> c) {
	for (E e : c) {
		if (e.equals(el)) {
			return el;
		}
	}
	return null;
}
//...
if (find("Ελένη", names) != null) {
//...
}

Θα ήταν καλύτερα αν επέστρεφε Optional<E>:

public static <E extends Comparable<E>> Optional<E> find(E el, Collection<E> c) {
	for (E e : c) {
		if (e.equals(el)) {
			return Optional.of(el);
		}
	}
	return Optional.empty();
}
//...
String foundName = find("Ελένη", names).orElse("Not found...");

Χρήσιμη είναι και η μέθοδος isPresent() που επιστρέφει boolean αν υπάρχει τιμή στο προαιρετικό.

Αποφύγετε να χρησιμοποιείτε Optional στις ακόλουθες περιπτώσεις:


Δ ->