Τετάρτη 28 Νοεμβρίου 2012

Exceptions στη Java.


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


    Οι εξαιρέσεις στη Java είναι αντικείμενα τα οποία ανήκουν στην ακόλουθη ιεραρχία των τάξεων (του πακέτου java.lang):
    Όπως φαίνεται και από την ιεραρχία των τάξεων αυτών, τα αντικείμενα τύπου Exception κληρονομούν από την τάξη Throwable την ικανότητα να "πεταχτούν" από την εντολή throw. Όλες οι εξαιρέσεις στη Java θα πρέπει να είναι αντικείμενα υποτάξεων της τάξης Exception. Άρα και οι δικές μας εξαιρέσεις θα πρέπει να επεκτείνουν την τάξη Excpetion ή κάποια από τις υποτάξεις της.Στην ιεραρχία φαίνονται ακόμα κάποιες άλλες τάξεις:
    • Η τάξη Error επεκτείνει κι αυτή την τάξη Throwable. Συνήθως αυτά τα λάθη προκαλούνται από την εικονική μηχανή της Java, και είναι κάποια "βαριά" λάθη με τα οποία ένα συνηθισμένο πρόγραμμα δεν έχει νόημα να ασχοληθεί. Ένα παράδειγμα είναι τα αντικείμενα της τάξηςNoClassDefFoundError που επεκτείνει την τάξη LinkageError που με τη σειρά της επεκτείνει την τάξη Error και που προκαλείται όταν μία μέθοδος κάλεσε μία άλλη μέθοδος σε ένα αντικείμενο, ή δοκίμασε να δημιουργήσει ένα νέο αντικείμενο με την new, η τάξη του οποίου (το αρχείο με επέκταση .class) δεν μπορεί να βρεθεί για να φορτωθεί. Φυσικά η τάξη αυτή υπήρχε όταν μεταφράστηκε το .java αρχείο της τάξης που έκανε την κλήση αλλά δεν μπορεί να βρεθεί πια.
    • Η τάξη RuntimeException από την άλλη μεριά είναι υποτάξη της τάξης Exception. Οι εξαιρέσεις που είναι αντικείμενα υποτάξεων της τάξης αυτής, δεν είναι υποχρεωτικό να πεταχτούν από μία μέθοδο, ούτε είναι υποχρεωτικό η μέθοδος να τις χειριστεί. Αυτές οι εξαιρέσεις είναι εξαιρέσεις που είναι αποτέλεσμα της κανονικής λειτουργίας της Εικονικής Μηχανής Java και θεωρήθηκε από τους σχεδιαστές της γλώσσας ότι θα ήταν υπερβολικά επιβαρυντικό να απαιτήσουν τον χειρισμό ή την δήλωση αυτών των εξαιρέσεων. Ένα παράδειγμα υποτάξης της τάξης RuntimeException είναι η εξαίρεση ArrayIndexOutOfBoundsException η οποία προκαλείται όταν μία μέθοδος αναφερθεί σε μια θέση ενός πίνακα που δεν υπάρχει. Θα θυμάστε ίσως ότι στο μάθημα "Τα δεδομένα μιας τάξης" είδαμε την μέθοδο setter της τάξης Person:

    •  
            public void setter(int index, Account a)
                             throws ArrayIndexOutOfBoundsException
            { ...
      την οποία η συνάρτηση main χρησιμοποιήσε χωρίς να δώσει καμία σημασία στο γεγονός ότι μπορεί να προκαλέσει την εξαίρεση ArrayIndexOutOfBoundsException. Αυτό δεν θα μπορούσε να το κάνει η main αν η ArrayIndexOutOfBoundsException ήταν υποτάξη της τάξηςException αντί της τάξης RuntimeException. Τότε η main θα έπρεπε να χειριστεί την εξαίρεση.
Example HRΠρόκληση εξαιρέσεων - Η εντολή throw
Μία μέθοδος μπορεί να προκαλέσει μία νέα εξαίρεση, χρησιμοποιώντας την εντολή throw. Η εντολή αυτή έχει την ακόλουθη σύνταξη:
throw αντικείμενο_εξαίρεσης;
Επειδή η throw πρέπει να "πετάξει" ένα αντικείμενο εξαίρεσης είναι πολύ συνηθισμένο στα προγράμματα να τη συναντάμε με την ακόλουθη μορφή:
throw new κατασκευαστής_τάξης_εξαίρεσης();
όπως στην ακόλουθη εντολή:
throw new ArrayIndexOutOfBoundsException();
Όταν μία μέθοδος έχει στο σώμα της μία εντολή throw και η τάξη της εξαίρεσης που προκαλείται από αυτή δεν είναι υποτάξη της τάξης RuntimeException, θα πρέπει να δηλώσει ότι μπορεί να προκαλέσει αυτή την εξαίρεση.Δηλαδή, για παράδειγμα στη τάξη Person που είδαμε στο μάθημα "Τα δεδομένα μιας τάξης" είχε τον ακόλουθο κώδικα:
public void setter(int index, Account a)
    throws ArrayIndexOutOfBoundsException
{
    //an o index einai metaxy 0 kai MAX_ACCOUNTS
    //vale sti thesi index toy accounts ton a
    if (index>=0 && index <MAX_ACCOUNTS)
        accounts[index] = a;
    //diaforetika prokalese mia exairesi deikti ektos oriwn
    else
        throw new ArrayIndexOutOfBoundsException();
}
Σ' αυτή τη περίπτωση έχουμε την εντολή throw να δημιουργεί (προκαλεί) μία εξαίρεση ArrayIndexOutOfBoundsException. Αυτή είναι μία RuntimeException και επομένως δεν υπάρχει από τη πλευρά της setter η υποχρέωση να δηλώσει ότι την προκαλεί. Παρόλα αυτά η setter την δηλώνει με την φράση throws ArrayIndexOutOfBoundsException μετά τη λίστα των παραμέτρων της. Έτσι αυτός που καλεί τη μέθοδο setter γνωρίζει ότι μπορεί να προκληθεί αυτή η εξαίρεση και μπορεί να λάβει τα μέτρα του, να χειριστεί δηλαδή την εξαίρεση σε περίπτωση που αυτή προκληθεί.Επισημαίνουμε και πάλι ότι η δήλωση με τη φράση throws δεν θα ήταν προαιρετική στη περίπτωση που η εξαίρεση δεν ήταν υποτάξη της RuntimeException, αλλά υποχρεωτική.
Example HRΧειρισμός των εξαιρέσεων: Η ομάδα εντολών try - catch
    Όπως είδαμε αν μια εξαίρεση προκληθεί σε μία μέθοδο, τότε αυτή θα παραδοθεί από το σύστημα εκτέλεσης (Runtime System) στη μέθοδο που έκανε τη κλήση. Αυτή η μέθοδος θα πρέπει να χειριστεί την εξαίρεση ή να δηλώσει με τη σειρά της ότι την προκαλεί με τη χρήση της φράσηςthrows στη δήλωση της. Εξαίρεση σ' αυτό το κανόνα αποτελούν μόνο οι εξαιρέσεις που είναι υποτάξεις της τάξης RuntimeException, τις οποίες οι μέθοδοι μπορούν να αγνοήσουν (αλλά όχι με ασφάλεια!). Οι εξαιρέσεις που δεν είναι υποτάξεις της τάξης RuntimeException επειδή υποχρεωτικά θα πρέπει να τις χειριστούν οι μέθοδοι που θα γίνουν αποδέκτες τους ονομάζονται ελεγχόμενες εξαιρέσεις (checked exceptions).Μία μέθοδος που καλεί μία άλλη μέθοδος η οποία προκαλεί μία ελεγχόμενη εξαίρεση, οφείλει να χειριστεί αυτή την εξαίρεση. Υπάρχουν δύο τρόποι χειρισμού:

    1. Η δήλωση throws μετά τη λίστα παραμέτρων της μεθόδου-αποδέκτης. Αυτό σημαίνει ότι η εξαίρεση θα παραδοθεί ποιο πάνω στη στοίβα των κλήσεων, στη μέθοδο που κάλεσε τη μέθοδο αποδέκτης. Αυτή με τη σειρά της ή θα πιάσει (catch) την εξαίρεση ή θα δηλώσει ότι την προκαλεί πάλι με τη φράση throws. Αν η εξαίρεση δεν "πιαστεί" ούτε από την τελευταία μέθοδο στη στοίβα των κλήσεων, το πρόγραμμα θα διακοπεί.
    2. Η ομάδα εντολών try-catch: Η κλήση της μεθόδου που μπορεί να προκαλέσει την εξαίρεση περικλείεται σε ένα block try το οποίο ακολουθείται από ένα block catch ως εξής:

    3. try
      {
          κλήση_μεθόδου //και πιθανώς και άλλες εντολές
      }
      catch (τύπος_εξαίρεσης όνομα_αντικειμένου_εξαίρεσης)
      {
          εντολές χειρισμού της εξαίρεσης
      }
Example HRΔημιουργία των δικών μας τάξεων εξαιρέσεων
    Πέρα από τις εξαιρέσεις που παρέχει η Java μπορούμε να δηλώσουμε τις δικές μας τάξεις εξαιρέσεων.
    Ουσιαστικά η πιο σημαντική απόφαση που πρέπει να πάρουμε είναι από το ποια τάξη εξαίρεσης θα κληρονομήσει η τάξη μας. Θα είναι υποτάξη της τάξης RuntimeException ή της τάξης Exception; Αν ισχύει το πρώτο τότε η εξαίρεση που δημιουργούμε δεν θα είναι ελεγχόμενη ενώ αν ισχύει το δεύτερο τότε θα είναι..Επειδή έχουμε ήδη δει ένα παράδειγμα μη ελεγχόμενης εξαίρεσης (της εξαίρεσης ArrayIndexOutOfBoundsException στο μάθημα "Τα δεδομένα μιας τάξης"), ας δούμε ένα παράδειγμα ελεγχόμενης εξαίρεσης. Στη τάξη Account που είχαμε δηλώσει στο ίδιο μάθημα, έχουμε τις μεθόδουςwithdraw και deposit. Οι δύο αυτές μέθοδοι ήταν boolean και επέστρεφαν false αν δεν μπορούσαν να κάνουν την κατάθεση ή την ανάληψη λόγω του ότι το ποσό δεν ήταν κατάλληλο (δεν καλύπτονταν από το υπόλοιπο στη περίπτωση της withdraw ή ήταν αρνητικό στην περίπτωση τηςdeposit). Σύμφωνα με τη φιλοσοφία της Java μια ποιο κατάλληλη τεχνική θα ήταν να δημιουργήσουμε μία τάξη εξαίρεσης που επεκτείνει την Exception. Βεβαίως στο πακέτο java.lang υπάρχει ήδη η μη ελεγχόμενη εξαίρεση IllegalArgumentException που είναι υποτάξη τηςRuntimeException και που θα μπορούσαμε να χρησιμοποιήσουμε και θα ήταν κατάλληλη για δύο λόγους:

    • Το πολύ κατάλληλο όνομά της για την περίπτωση (μια και η παράμετρος money δεν έχει την κατάλληλη τιμή)
    • Το γεγονός ότι η μέθοδος που έκανε τη κλήση στη μέθοδό μας θα έπρεπε να έχει ελέγξει την παράμετρο και επομένως δεν είναι δικό μας το λάθος για να έχουμε την υποχρέωση να "σώσουμε" την κατάσταση, υποχρεώνοντάς τη "απρόσεκτη" μέθοδο να χειριστεί την εξαίρεση.

    Παρόλα αυτά θα δημιουργήσουμε (χάριν παραδείγματος) μία δική μας τάξη εξαίρεσης, την InvalidMoneyException που επεκτείνει την τάξη Exception.
    Ο κώδικας της τάξης αυτής, που βρίσκεται στο αρχείο InvalidMoneyException.java,  είναι ο ακόλουθος:

      public class InvalidMoneyException extends Exception
      {
          public InvalidMoneyException()
          {
              super();
          }    public InvalidMoneyException(String m)
          {
              super(m);
          }
      }

    Για τους σκοπούς μας εδώ, θα χρησιμοποιήσουμε μια απλή τάξη Account χωρίς τον πίνακα των δικαιούχων του λογαριασμού, που βρίσκεται στο αρχείο Account.java, και ο κώδικάς της δίνεται στη συνέχεια:

      public class Account
      {
          private double balance;    //constructor
          public Account()
          {
              balance = 0.0;
          }
          //methodos getBalance
          public double getBalance()
          {
              return balance;
          }
          //methodos withdraw
          //An i analipsi den borei na ginei petaei tin exairesi InvalidMoneyException
          public void withdraw(double money)
              throws InvalidMoneyException
          {
              if (money <= balance )
              {
                  balance -= money;
                  return;
              }
              throw new InvalidMoneyException();
          }
          //methodos deposit
          //An i katathesi den borei na ginei petaei tin exairesi InvalidMoneyException
          public void deposit(double money)
              throws InvalidMoneyException
          {
              if (money >= 0)
              {
                  balance += money;
                  return;
              }
              throw new InvalidMoneyException();
           }
      }
    Παρατηρείστε ότι οι μέθοδοι deposit και withdraw πετάνε την εξαίρεση InvalidMoneyException και υποχρεωτικά θα πρέπει να δηλώσουν ότι την "πετάνε" με τη φράση throws μετά τη λίστα παραμέτρων. Αν δεν τη δήλωναν ο Java compiler (javac) δεν θα μετέφραζε την τάξη σε bytecodes.Στη συνέχεια δίνουμε μία τάξη Main που βρίσκεται στο αρχείο Main.javaκαι η οποία περιέχει την main που θα πρέπει να χειριστεί την εξαίρεση InvalidMoneyException μια και καλεί την deposit. Το κάνει αυτό με ένα block try-catch που πιάνει την εξαίρεση που θα προκληθεί (μια και προσπαθούμε να καταθέσουμε αρνητικό ποσό)

      public class Main
      {
          public static void main(String[] args)
          {
              Account a = new Account();        try
              {
                  a.deposit(-1000);
              }
              catch (InvalidMoneyException e)
              {
                  e.printStackTrace();
              }
          }
      }

    Όπως παρατηρείτε το πρόγραμμα θα χειριστεί την εξαίρεση, καλώντας τη μέθοδο printStackTrace που θα εμφανίσει το όνομα της εξαίρεσης και τη στοίβα των κλήσεων που οδήγησε σ' αυτήν. Διευκρινίζεται ότι η μέθοδος printStackTrace κληρονομείται από την τάξη Throwable.
Example HRΠολλαπλές ομάδες εντολών catch και η εντολή finally
    Πολλές φορές είναι δυνατόν μια ομάδα εντολών να προκαλέσει περισσότερες από μία εξαιρέσεις. Σ' αυτή τη περίπτωση θα πρέπει για να χειριστούμε τις εξαιρέσεις επιτυχώς, να περικλείσουμε τις εντολές σε ένα try block και μετά το try block να δώσουμε τόσα catch blocks όσες και οι εξαιρέσεις που μπορούν να προκληθούν.
    Στην περίπτωση των πολλαπλών catch blocks που χειρίζονται εξαιρέσεις στην ίδια ιεραρχία τάξεων θα πρέπει να βάζουμε υψηλότερα τις ειδικότερες εξαιρέσεις (αυτές που βρίσκονται πιο χαμηλά στην ιεραρχία) και χαμηλότερα τις γενικότερες.Για παράδειγμα δεν θα ήταν σωστό να γράψουμε κώδικα σαν τον ακόλουθο:

      try
      {
          ...
      }
      catch (java.io.ΙΟException e)
      {
          //χειρισμός της εξαίρεση Excpetion
      }
      catch (java.io.FileNotFoundException e)
      {
          //χειρισμός της εξαίρεσης FileNotFoundException
      }

    διότι η εξαίρεση java.io.FileNotFoundException είναι υποτάξη της τάξης java.io.IOException και επομένως αν προκληθεί θα γίνει ο χειρισμός της στο πρώτο catch block, αφού οι υποτάξεις μιας τάξης ανήκουν και στον τύπο της υπερτάξης τους. Η σωστή σειρά λοιπόν, θα ήταν η αντίστροφη.
    Τέλος, μετά τα catch blocks μπορούμε να βάλουμε την ομάδα εντολών finally η οποία εκτελείται είτε συμβεί κάποια εξαίρεση είτε όχι, ακόμα και αν γίνει ο χειρισμός της εξαίρεσης. Μία πολύ κοινή χρήση της finally είναι η εκτέλεση κάποιων εντολών που είναι κοινές για όλα τα σενάρια εκτέλεσης ενός try block:

      try
      {
          ...
      }
      catch (java.io.ΙΟException e)
      {
          //χειρισμός της εξαίρεση Excpetion
      }
      catch (java.io.FileNotFoundException e)
      {
          //χειρισμός της εξαίρεσης FileNotFoundException
      }
      finally {
          //εντολές που θα εκτελεστούν σε κάθε περίπτωση
          //φυσιολογικής εξόδου ή εξόδου μετά από πρόκληση εξαίρεσης
      }
ΠΗΓΗ: http://users.cs.teilar.gr/gkakaron/java/Day2/6.html

2 σχόλια: