Εισαγωγή στη Java - κεφ. 10

...ασύγχρονα μαθήματα γλώσσας Java

Εισαγωγή στη Java - κεφ. 10

Δημοσίευσηαπό alkismavridis » 19 Νοέμ 2013, 19:34

Προηγούμενο: Τρεις χρήσιμες κλάσεις
Επόμενο: Κείμενα


Exceptions


Σκοπός αυτού του κεφαλαίου είναι να μάθουμε πως να κάνουμε το πρόγραμμά μας να αντιδρά «ψύχραιμα» σε περιπτώσεις που τα πράγματα δεν πάνε όπως θα τα ήθελε.


1. Εξαιρέσεις - Exceptions
Στη ζωή μας τα πράγματα δεν έρχονται πάντα όπως θα περιμέναμε. Όσο κι αν σας φαίνεται περίεργο,το ίδιο συμβαίνει και στη «ζωή» ενός προγράμματος. Ας πάρουμε σαν παράδειγμα τον παρακάτω κώδικα:
Μορφοποιημένος Κώδικας: Επιλογή όλων
int[] array = new int[] {2,3,5,7,11,13,17,19};
Scanner sc = new Scanner(System.in);
int i = sc.nextInt();
System.out.print("Το στοιχείο "+i+"του πίνακα array είναι "+array[i]+"\n");


Ο κώδικας αυτός διαβάζει έναν ακέραιο από το χρήστη, τυπώνει το αντίστοιχο στοιχείο πίνακα στην οθόνη. Το πρόγραμμά μας λοιπόν περιμένει δύο πράγματα: πρώτον ότι ο χρήστης θα πληκτρολογήσει έναν ακέραιο αριθμό και όχι κάτι άλλο, και δεύτερον ότι ο ακέραιος αυτός θα είναι μεταξύ 0 και 7, γιατί αλλιώς βγαίνουμε εκτός των ορίων του πίνακα. Στην πραγματικότητα δεν υπάρχει εγγύηση για κανένα από τα δύο! Γιατί ο χρήστης μπορεί να δώσει ένα κείμενο που δεν είναι ακέραιος, και γιατί ακόμα και αν είναι, μπορεί κάλλιστα να μην βρίσκεται στην περιοχή [0,7].

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

Θα μπορούσαμε για να αποφύγουμε δυσάρεστες καταστάσεις βάζοντας όλη την ώρα δομές if. Για παράδειγμα πριν από κάθε πρόσβαση στοιχείου πίνακα να γράφαμε κάτι σαν
Μορφοποιημένος Κώδικας: Επιλογή όλων
if (index>=0 && index<array.length) //δώσε το στοιχείο
else //κάνε κάτι άλλο
Ομοίως για μετατροπές κειμένου σε αριθμό θα μπορούσαμε να χρησιμοποιήσουμε τις μεθόδους της κλάσης Scanner hasNextInt() κτλ.

Υπάρχει όμως και ένα άλλο εργαλείο, συνήθως καλύτερο, και σίγουρα πιο βολικό για εμάς: οι εξαιρέσεις ή Exceptions.



2. Η δομή try - catch
Ετοιμαστείτε λοιπόν να μάθουμε μία καινούρια δομή! Αυτή θα αποτελείται από δύο μέρη: το block try και το block catch. Η δομή αυτή μοιάζει λίγο με την if-else if και συντάσσεται κάπως έτσι:
Μορφοποιημένος Κώδικας: Επιλογή όλων
try {
//...
//εντολές
//...
} //try

catch (Throwable th) {
//...
//εντολές
//...
} //catch


Να πως δουλεύει: όταν ένα «σφάλμα» λαμβάνει χώρα (πχ διαίρεση με το μηδέν ή άλλα παρόμοια) το jvm δημιουργεί και «ρίχνει»ένα αντικείμενο της κλάσης Throwable που περιέχει διάφορες χρήσιμες πληροφορίες για το σφάλμα. Από τη στιγμή που το «ρίχνει», περιμένουμε κάποιος να το «πιάσει». Αυτή τη δουλειά ακριβώς κάνει η δομή try - catch.

Ορίστε πως:
  • Αν το σφάλμα συμβεί έξω από οποιοδήποτε try block, το πρόγραμμα κάνει το γνωστό: τυπώνει το μήνυμα λάθους και τερματίζει.
  • Αν το σφάλμα συμβεί εντός κάποιου try block, το try «πιάνει» το σφάλμα που έριξε το jvm, και αν τα καταφέρει, η εκτέλεση του προγράμματος συνεχίζει στο catch block. Έπειτα κανονικά έξω από αυτό.
  • Αν το try block τελειώσει χωρίς να συμβεί κανένα σφάλμα, τότε το catch block δεν εκτελείται ποτέ και το πρόγραμμα συνεχίζει μετά το catch.

Άρα εμείς:
  • Στο try block γράφουμε τις εντολές που θέλουμε, ελπίζοντας ότι δε θα συμβεί κάτι «κακό».
  • Στο catch block γράφουμε τις εντολές που θέλουμε να εκτελεστούν σε περίπτωση που γίνει το «κακό».
  • Γίνει-δε γίνει το «κακό», το πρόγραμμα θα συνεχίσει την πορεία του μετά το τέλος της δομής try-catch.

Ας δούμε ένα παράδειγμα:
Μορφοποιημένος Κώδικας: Επιλογή όλων
Object ob=null;
String str;

try {
str = ob.toString(); //η εντολή αυτή θα προκαλέσει σφάλμα, γιατί το ob είναι null και άρα δεν μπορεί να καλεί μεθόδους!!
System.out.print("Αυτή η εντολή δε θα εκτελεστεί ποτέ!\n");
} //try
catch (Throwable thr) { System.out.print("Ανίχνευσα ένα σφάλμα! Όμως δεν πτοούμαι και συνεχίζω κανονικά...\n"); }

System.out.print("Εγώ είμαι η πρώτη εντολή που θα εκτελεστεί μετά τη δομή try-catch, ανεξάρτητα από το αν προκύψει σφάλμα ή όχι!\n");



Και μία παραλλαγή του:
Μορφοποιημένος Κώδικας: Επιλογή όλων
int ar[] = new int[] {1,2,3}, i=0;

try {
i = ar[2]; //η εντολή αυτή εκτελείται κανονικά, το 2 είναι εντός ορίων του πίνακα.
System.out.print("Αυτή η εντολή θα εκτελεστεί επίσης κανονικά.\n");
} //try
catch (Throwable thr) { System.out.print("Αυτή η εντολή δε θα εκτελεστεί ποτέ, γιατί κανένα σφάλμα δεν προκύπτει στο try block.\n"); }

System.out.print("Εγώ είμαι η πρώτη εντολή που θα εκτελεστεί μετά τη δομή try-catch, ανεξάρτητα από το αν προκύψει σφάλμα ή όχι!\n");


Στα try και catch block, η χρήση αγκύλων {} είναι υποχρεωτική, ακόμα και αν αυτά περιέχουν μία εντολή!


Όπως μπορούμε να έχουμε δομές for μέσα σε άλλες δομές for, δομές if μέσα σε άλλες if κτλ, έτσι μπορούμε να έχουμε δομές try-catch μέσα σε try block, μέσα σε catch block και γενικά... όπου θέλουμε! Νοιώστε ελεύθεροι να χρησιμοποιείτε τη δομή try-catch όπου νομίζετε αναγκαίο.



3. Η κλάση Throwable και οι υπο-κλάσεις της
Περιγραφή της κλάσης θα βρείτε εδώ. Όπως είπαμε, σκοπός της είναι να μας παρέχει πληροφορίες για τη φύση του σφάλματος. Ίσως η πιο χρήσιμη μέθοδος που υπάρχει εδώ είναι η printStackTrace(). Αυτή η μέθοδος τυπώνει στο System.err ένα εκτεταμένο μήνυμα για το τι συνέβη. Το System.err είναι αντικείμενο της κλάσης PrintStream (σαν το System.out), και αν δεν το ορίσουμε διαφορετικά, ταυτίζεται με το System.out.
Μία άλλη χρήσιμη μέθοδος είναι η getMessage() που επιστρέφει ένα σύντομο String που περιγράφει το τι έγινε. Η printStackTrace() όμως τυπώνει περισσότερες πληροφορίες.

Η κλάση Throwable έχει δύο βασικές υπο-κλάσεις: την Error και την Exception. Η κλάση Exception έχει πολλές υπο-κλάσεις εκ των οποίων μία σημαντική είναι η RuntimeException. Οι κλάσεις Error και RuntimeException έχουν τις δικές τους υπο-κλάσεις. Όπως βλέπετε μιλάμε για ένα ολόκληρο δέντρο από αντικείμενα!
Από όλα τα παραπάνω, η Java μεταχειρίζεται με ιδιαίτερο τρόπο τα Error και RuntimeException, με τρόπο που θα δούμε στην πορεία.

Οι υπο-κλάσεις της Error εκφράζουν τόσο σοβαρά σφάλματα που καλό είναι να μην τα μεταχειριζόμαστε. Πιο φρόνιμο είναι να αφήνουμε το πρόγραμμα να τερματίσει. Η κλάση Exception όμως, και οι πολλές υπο-κλάσεις της εκφράζουν πιο ήπια σφάλματα που μπορούμε και καλό είναι να μεταχειριζόμαστε.

Μερικές από τις υπο-κλάσεις της RuntimeException που θα συναντάμε συχνά είναι οι:
  • ArrayIndexOutOfBoundsException: δημιουργείται από το jvm όταν παραβιάζουμε τα όρια ενός πίνακα.
  • NullPointerException: όταν προσπαθούμε να προσπελάσουμε πεδίο ή να καλέσουμε μέθοδο αντικειμένου που είναι null.
  • ClassCastException: όταν προσπαθούμε να κάνουμε ένα type casting που δεν γίνεται

Στην πορεία θα συναντούμε πολλές άλλες Exception. Τα τρία παραπάνω, καθώς και οι κλάσεις Throwable, Exception και Error βρίσκονται στο πακέτο java.lang το οποίο όπως έχουμε πει γίνεται αυτόματα import, άρα δε χρειάζεται να γράψουμε κανένα import για να χρησιμοποιήσουμε τα παραπάνω αντικείμενα.

Ας δούμε ένα παράδειγμα:
Μορφοποιημένος Κώδικας: Επιλογή όλων
//αρχείο PrintErrorMessage.java
import java.util.Scanner;

public class PrintErrorMessage {

public static void main(String arg[]) {
Scanner sc=null;
System.out.print("\n\n");

try { int i = sc.nextInt(); }
catch (Throwable thr) { thr.printStackTrace(); }

System.out.print("\n\n");
}//main
}


Το αποτέλεσμα του προγράμματος θα είναι:
Κώδικας: Επιλογή όλων
alkis@Alkis:~/Programs/java/Tutorial$ java PrintErrorMessage


java.lang.NullPointerException
   at PrintErrorMessage.main(PrintErrorMessage.java:10)

Το πρόγαμμα συνεχίζει!
alkis@Alkis:~/Programs/java/Tutorial$
Όπως βλέπετε η μέθοδος printStackTrace μας λέει τον τύπο του σφάλματος (NullPointerException), σε ποια μέθοδο συνέβη (στην PrintErrorMessage.main), και σε ποια γραμμή του κώδικα, ποιου αρχείου (PrintErrorMessage.java:10)!!


4. Πολλαπλά catch
Όπως είπαμε, το όρισμα του catch block είναι ένα Throwable, όπως της δομής if και η while είναι μία boolean. Όμως η αρχή του αντικειμενοστραφούς προγραμματισμού λέει ότι ένα αντικείμενο Exception είναι και αυτό Throwable! Με την ίδια λογική και ένα αντικείμενο NullPointerException είναι και αυτό ένα Throwable και γενικά, κάθε υπο-κλάση («παιδί», «εγγόνι», «δισέγγονο» κτλ) της κλάσης Throwable είναι και αυτό ένα Throwable.
Είναι λοιπόν απόλυτα σωστό να γράψουμε:
Μορφοποιημένος Κώδικας: Επιλογή όλων
try { /*...*/ }
catch (Exception ex) { /*...*/ }

//ή και
try { /*...*/ }
catch (ArrayIndexOutOfBoundsException ex) { /*...*/ }


Επειδή όπως είπαμε πριν, καλό είναι να ασχολούμαστε μόνο με τις Exceptions, και να μην χειριζόμαστε τα Errors, ο πιο γενικός κώδικας που θα χρησιμοποιούμε θα είναι ο:
Μορφοποιημένος Κώδικας: Επιλογή όλων
catch (Exception ex) { /*...*/ }


Ακριβώς επειδή το catch block πιάνει μόνο «το είδος του», και επειδή υπάρχουν «πολλά είδη» σφαλμάτων, η Java μας δίνει τη δυνατότητα πολλών catch σε ένα try όπως ακριβώς έχουμε πολλά else if σε μία δομή if. Εδώ υπάρχει όμως ένα λεπτό σημείο που θέσει πολύ προσοχή. Μία δομή try μπορεί να «πιάσει» μόνο Throwable του τύπου που αναφέρεται στα catch block της. Αν ο αντίστοιχος τύπος δεν υπάρχει, η δομή try δε θα καταφέρει να πιάσει το σφάλμα.

Ας δούμε ένα παράδειγμα:
Μορφοποιημένος Κώδικας: Επιλογή όλων
int array[] = new int[] {3,5,7};
Object ob = null;
try {
array[7]++; //θα προκαλέσει ArrayIndexOutOfBoundsException
ob.toString(); //θα προκαλέσει NullPointerException

ob = new java.util.Scanner(System.in);
String str = (String)ob; //θα προκαλέσει ClassCastException, το Scanner δεν μετατρέπεται σε String!

System.out.print("Δε θα εκτελεστώ ποτέ!\n");
} //try

catch (ArrayIndexOutOfBoundsException ex_array) {
System.out.print("Παραβίαση ορίων πίνακα! Κάνω την πράξη στο τελευταίο στοιχείο!\n");
array[2]++;
}

catch (NullPointerException ex_null) {
System.out.print("Σφάλμα! Κενά αντικείμενα δεν μπορούν να καλούν μεθόδους!\n");
}

Εδώ έχουμε μία δομή try - catch που «πιάνει» ArrayIndexOutOfBoundsException και NullPointerException. Και όντως, η εντολή array[7]++ δε θα αρέσει στο jvm, και αυτό θα «ρίξει» μία ArrayIndexOutOfBoundsException. Είναι όμως η δομή try που φτιάξαμε ικανή να «πιάσει» αυτό το σφάλμα; Κοιτάξτε για ένα λεπτό τον κώδικα. Θα δείτε πως είναι, αφού υπάρχει ένα catch block που «πιάνει» ArrayIndexOutOfBoundsException. Μόλις το σφάλμα αυτό συμβεί, θα εκτελεστεί αυτό το block.

Αλλάξτε τώρα την εντολή array[7]++ σε array[0]++ για να αποφύγουμε το ArrayIndexOutOfBoundsException. Τι θα συμβεί; Το πρόγραμμα θα προσπαθήσει να εκτελέσει και την επόμενη εντολή: ob.toString() η οποίο θα προκαλέσει το jvm να «ρίξει» μία NullPointerException. Ας αναρωτηθούμε το ίδιο: Είναι η δομή try ικανή να «πιάσει» το NullPointerException; Θα δείτε και πάλι πως είναι! Και θα εκτελέσει το αντίστοιχο catch block.

Ας αποφύγουμε τώρα και το NullPointerException. Αλλάξτε την εντολή ob.toString() σε if (ob!=null) ob.toString(). Τότε το πρόγραμμα θα προσπαθήσει να εκτελέσει τις δύο επόμενες εντολές, προκαλώντας το jvm να μας «ρίξει» ένα ClassCastException. Είναι σε θέση η δομή μας να «πιάσει» ένα τέτοιο αντικείμενο; Αν κοιτάξουμε τα catch blocks θα δούμε πως όχι. Η δομή try θα αποτύχει γιατί δεν θα έχει το κατάλληλο catch, και το πρόγραμμα θα τερματίσει με το γνωστό μήνυμα λάθους. Με αυτό τον τρόπο μπορούμε να επιλέξουμε ποια είδη Exceptions θα «πιάνουμε» και ποια όχι.


5. Το finally block
Εδώ θα γενικεύσουμε λίγο την προηγούμενη δομή. Ό,τι είπαμε πριν ισχύει και εδώ, με τη διαφορά ότι προσθέτουμε και ένα τρίτο block, το finally. Η όλη δομή θα μοιάζει κάπως έτσι:
Μορφοποιημένος Κώδικας: Επιλογή όλων
try {
//...
//εντολές
//...
} //try

catch (Throwable th) {
//...
//εντολές
//...
} //catch

//ίσως και άλλα catch...

finally {
//...
//εντολές
//...
}
Στην πραγματικότητα μπορούμε να έχουμε try - finally χωρίς καθόλου catch!

Το finally block παίζει το ρόλο του κλεισίματος μίας δομής try - catch.
Συνοψίζοντας το ρόλο του finally block σε μία φράση: «για όποιο λόγο και αν τελειώνει μία δομή try, το finally block θα εκτελεστεί πριν το τέλος της».

Μία δομή try-catch μπορεί να τελειώσει με τους εξής τρόπους:
  • Η ροή του προγράμματος να φτάσει στο τέλος της try, χωρίς να συμβεί κανένα σφάλμα. Σε αυτή την περίπτωση θα εκτελεστεί το finally block, και το πρόγραμμα θα συνεχίσει.
  • Κάποιο σφάλμα συμβαίνει και πιάνεται από ένα catch block, το οποίο ολοκληρώνεται φυσιολογικά. Σε αυτή την περίπτωση μετά το catch θα εκτελεστεί το finally block. Έπειτα το πρόγραμμα θα συνεχίσει κανονικά.
  • Κάποιο σφάλμα συμβαίνει το οποίο δεν πιάνεται από κανένα catch block. Σε αυτή την περίπτωση κανονικά το πρόγραμμα θα τερμάτιζε. Πριν τερματίσει λοιπόν, θα εκτελέσει το finally block (εκτός αν το σφάλμα ήταν τόσο σοβαρό που η συνέχιση του προγράμματος είναι αδύνατη).
  • Συναντάμε κάποια εντολή που μας πετάει έξω από τη δομή try. Εντολές όπως return, break, continue. Σε τέτοιες περιπτώσεις, πριν εκτελεστούν οι εντολές αυτές, θα εκτελεστεί το finally block. Εξαίρεση αποτελεί η εντολή System.exit(int i) όπου θα τερματίσει αμέσως το πρόγραμμα χωρίς να ενδιαφερθεί για το finally block.

Να σημειωθεί ότι στην δεύτερη περίπτωση εντάσσονται σφάλματα που συμβαίνουν και μέσα στα catch block χωρίς να πιάνονται από άλλο (εσωτερικό) catch.

Ας δούμε ένα παράδειγμα:
Μορφοποιημένος Κώδικας: Επιλογή όλων
//αρχείο FinallyTest.java
import java.util.Scanner;

public class FinallyTest {

public static void main(String arg[]) {
Scanner sc=null;
System.out.print("\n\n");

for (;;) {
try { break; }
//εδώ θα μπορούσαν να μπουν διάφορα catch
finally { System.out.print("Για μια στιγμή... Πήγες να εκτελέσεις την break; Πριν την break θα εκτελεστώ εγώ!!\n"); }
} //for

System.out.print("\nΒγήκα από το βρόγχο for!\n");
}//main
}



6. Δομή try μέσα σε άλλη δομή try
Όπως ήδη αναφέραμε, μία δομή try μπορεί να βρίσκεται μέσα σε μία άλλη δομή try. Σε μία τέτοια περίπτωση δεν έχουμε πολλά καινούρια να μάθουμε, εκτός από την περίπτωση όπου ένα σφάλμα προκύπτει και δεν πιάνεται από κανένα catch block. Κανονικά το πρόγραμμα θα κράσαρε (αφού εκτελούσε το finally block, αν υπήρχε).
Αν όμως η δομή try μας βρισκόταν μέσα σε ένα άλλο try block, η εξωτερική δομή try θα προσπαθούσε εκείνη να «πιάσει» το σφάλμα που δεν έπιασε η εσωτερική. Αν ούτε αυτή έχει το κατάλληλο catch για να το πιάσει, θα προσπαθήσει να το πιάσει η ακόμα πιο εξωτερική δομή try (αν υπάρχει) κ.ο.κ.

Τελικά το πρόγραμμα κρασάρει αν καμία δομή try δεν πιάσει το σφάλμα.

Για παράδειγμα:
Μορφοποιημένος Κώδικας: Επιλογή όλων
try {
try {
Object ob = null;
ob.hashCode();
} //εσωτερική try

catch (ArrayIndexOutOfBoundsException ex) {
System.out.print("Δε θα εκτελεστώ ποτέ, γιατί το try block δεν παράγει σφάλμα του είδους μου.\n");
} // εσωτερικό catch

finally { System.out.print("Είμαι το finally block της εσωτερικής try. Θα εκτελεστώ πριν το σφάλμα «περάσει» στην εξωτερική.\n"); }

} //εξωτερική try
catch (NullPointerException ex) { System.out.print("Είμαι η εξωτερική δομή try. Έπιασα το σφάλμα που δεν κατάφερε να πιάσει η εσωτερική.\n"); }



7. Δομή try μέσα από μεθόδους
Υπάρχει και η περίπτωση, να βρισκόμαστε μέσα σε μία δομή try χωρίς να το ξέρουμε, και χωρίς να μπορούμε να το προβλέψουμε. Αυτό γίνεται στην περίπτωση που γράφουμε μία μέθοδο ( έστω methodos1() ) που θα καλείται από άλλες μεθόδους. Η μέθοδος που κάλεσε την methodos1(), μπορεί να το έκανε μέσα από μία δομή try, μπορεί και όχι:
Μορφοποιημένος Κώδικας: Επιλογή όλων
//χωρίς try:
void methodos2() {
methodos1();
} //methodos2

//ή με try
void methodos2() {
try { methodos1(); }
catch (Exception ex) { /*...*/ }
} //methodos2


Αν συμβεί ένα σφάλμα που δεν «πιάνεται» από κάποια catch στη methodos1(), η methodos1() θα τερματίσει και το σφάλμα θα προσπαθήσει να το πιάσει η μέθοδος που κάλεσε τη methodos1() (στο παράδειγμά μας η methodos2() ). Η διαδικασία από εκεί και πέρα συνεχίζει κατά τα γνωστά.

Αν τώρα η methodos1() δεν ήταν void, αλλά επέστρεφε κάτι (πχ. int), και προκύψει κάποιο σφάλμα τι γίνεται; Για παράδειγμα δείτε τον παρακάτω κώδικα:
Μορφοποιημένος Κώδικας: Επιλογή όλων
//αρχείο ErrorFromMethod.java
public class ErrorFromMethod {

static int[] array;


public static void main(String arg[]) {
System.out.print("\n\n");

int i=1357;
try { i = methodos1(); }
catch (Exception ex) { ex.printStackTrace(); }

System.out.print("\ni= "+i+"\n");
}//main



static int methodos1() { array[2]++; return 80; }
}
Το i είναι 1357. Η μέθοδος methodos1 επιστρέφει 80, άρα μετά την εντολή i = methodos1() το i πρέπει να έχει γίει 80.
Παρ όλα αυτά η methodos1() τερματίζει πριν προλάβει να επιστρέψει το 80 γιατί παράγει ένα NullPointerException. Τι θα επιστρέψει λοιπόν; Τι θα γίνει το i;

Η απάντηση είναι τίποτα. ολόκληρη η εντολή i = methodos1() θα ακυρωθεί, και το i θα παραμείνει 1357 όπως ήταν. Δοκιμάστε να τρέξετε το πρόγραμμα και θα το διαπιστώσετε. Αν θέλαμε η methodos1() να επιστρέφει οπωσδήποτε κάποια τιμή, ακόμα και αν συμβεί κάποιο σφάλμα, θα έπρεπε να βάλουμε και εκεί μία δομή try, κάπως έτσι:
Μορφοποιημένος Κώδικας: Επιλογή όλων
static int methodos1() {
try {
array[2]++;
return 80;
}
finally { return 0; }
} //methodos1
Με αυτό τον τρόπο η methodos1 επιστρέφει 80, αλλά αν προκύψει κάποιο σφάλμα εκτελεί το finally block και επιστρέφει 0. Δοκιμάστε να αντικαταστήσετε την methodos1 με τον παραπάνω κώδικα και να τρέξετε το πρόγραμμα και θα δείτε ότι το i γίνεται 0.



8. Η εντολή throw και η λέξη-κλειδί throws
Ας συνοψίσουμε!
Όταν κάτι που συνέβη μέσα σε ένα try block δεν αρέσει στο jvm, αυτό δημιουργεί και «ρίχνει» ένα αντικείμενο Throwable. Αν αυτό το «πιάσει» κάποιο από τα catch block, εκτελείται ο κώδικάς του, μετά (αν υπάρχει) το finally block και το πρόγραμμα συνεχίζει.
Αν δεν το πιάσει κάποιο catch block, το try block εκτελεί το finally block (αν υπάρχει), και «ρίχνει» το Throwable στο επόμενο (πιο εξωτερικό) try block, το οποίο με τη σειρά του κάνει την ίδια διαδικασία κτλ. μέχρι κάποιο try block να «πιάσει» το Throwable, αλλιώς το πρόγραμμα τερματίζει με ένα μήνυμα λάθους.

Όμως... μία στιγμή! Πως το jvm αποφασίζει «τι του αρέσει» και τι όχι;
Ή για να το πούμε αλλιώς, μπορούμε κι εμείς να αποφασίσουμε αν κάτι δε μας αρέσει, και να «ρίξουμε» ένα δικό μας Throwable;
Νομίζω ότι μυρίζεστε την απάντηση. Μπορούμε. Αρκεί να δημιουργήσουμε (με το γνωστό τρόπο-constructors) ένα αντικείμενο Throwable (ή οποιαδήποτε υπο-κλάση της Throwable), και να χρησιμοποιήσουμε την εντολή throw.

Ας πούμε ότι για τους λόγους μας αποφασίζουμε να «ρίξουμε» μία ArrayIndexOutOfBoundsException. Να πως θα το κάναμε:
Μορφοποιημένος Κώδικας: Επιλογή όλων
//αρχείο ThrowTest.java
public class ThrowTest {

public static void main(String args[]) {

try { myMethode(); }
catch (Exception ex) { System.out.print (ex.getMessage()+"\n"); }

} //main

static void myMethode() {

int[] ar=new int[] {1,2,3};
ar[7]++;

} //myMethode
}



Με τη γνωστή μας λέξη-κλειδί extends μπορούμε να επεκτείνουμε την κλάση Exception, γράφοντας τις δικές μας Ecxeptions, και τις δικές μας μεθόδους που... όποτε νομίζουν θα «ρίχνουν» μέσω της εντολής throw τις Exceptions που φτιάξαμε.

Ίσως κάποιος να αναρωτηθεί γιατί να το κάνουμε αυτό! Γιατί να δημιουργούμε και να ρίχνουμε... αντικείμενα λάθους!
Η απάντηση είναι ότι αυτός είναι ένας τρόπος να επικοινωνήσουμε με αυτόν που θα «πιάσει» το λάθος αυτό (και θα κάνει τα ανάλογα βήματα). Ας μην ξεχνάμε ότι γράφουμε κλάσεις και μεθόδους που μπορεί να χρησιμοποιηθούν από άλλα προγράμματα και από άλλους προγραμματιστές. Το να ενημερώσουμε τον «χρήστη» της κλάσης μας ότι «το τάδε πράγμα πήγε στραβά» είναι ζωτικής σημασίας.

Φτάνουμε λοιπόν στο εξής συμπέρασμα: άλλες φορές είναι χρήσιμο να κανονίζουμε επί τόπου το σφάλμα (μέσα από μία δομή try-catch), και άλλες φορές να το «ρίχνουμε» με την εντολή throw, για να το πιάσει κάποιος άλλος. Στην πραγματικότητα μπορούμε να κάνουμε και τα δύο! Πρώτα κάνουμε τα βήματα που νομίζουμε για να «σώσουμε την κατάσταση», και μετά χρησιμοποιούμε την εντολή throw για να πούμε σε αυτόν που θα «πιάσει» το σφάλμα: «συνέβη το τάδε σφάλμα, κάντε τώρα εσείς τα βήματα που νομίζετε». Έτσι εκείνος, στο δικό του try-catch μπορεί να χειριστεί με τον τρόπο που θέλει το σφάλμα. Να σημειώσουμε βεβαίως ότι ο «εκείνος» μπορεί να είμαστε και εμείς που χρησιμοποιούμε τη μέθοδο κάποιου άλλου, ή ακόμα και τη δική μας μέθοδο που φτιάξαμε νωρίτερα.


Υπάρχει τέλος ένας τρόπος να σημαδέψουμε μία μέθοδο, γράφοντας πάνω της την ταμπέλα: «προσοχή! αυτή η μέθοδος ρίχνει κάποια Exception». Αυτό το κάνουμε κατά τη δήλωση της μεθόδου χρησιμοποιώντας τη λέξη-κλειδί throws, κάπως έτσι:
Μορφοποιημένος Κώδικας: Επιλογή όλων
static int twoTimes(int i) throws Exception {
if (i >= 0x40000000) throw new Exception();
return 2*i;
} //twoTimes

//Φυσικά μπορούμε να γράψουμε οποιοδήποτε Throwable όπως:
static int twoTimes(int i) throws Error { /*...*/ }
static int twoTimes(int i) throws ArithmeticException { /*...*/ }
static int twoTimes(int i) throws Throwable { /*...*/ }
//κτλ...

//αν θέλουμε να πούμε ότι η μέθοδός μας ρίχνει πολλά είδη Throwable, τα γράφουμε όλα χωρισμένα με κόμμα. Πχ:
static int twoTimes(int i) throws ArithmeticException, NullPointerException { /*...*/ }

Πως όμως αυτή η δήλωση αλλάζει τη συμπεριφορά της μεθόδου μας; Η απάντηση είναι κατά το compile! Κατά το compile μίας οποιασδήποτε μεθόδου που προσπαθεί να καλέσει την μέθοδο που «μαρκάραμε» ως επικίνδυνη.

Να πως δουλεύει: Ας πούμε ότι φτιάξαμε μία μέθοδο με τον εξής τρόπο:
Μορφοποιημένος Κώδικας: Επιλογή όλων
//κλάση MyClass1
void method1() throws DataFormatException { /*...*/ }
//DataFormatException είναι μία από τις υπο-κλάσεις της Exception...


Τώρα μία μέθοδος προσπαθεί να καλέσει την method1 (έστω η method2). Η method2 μπορεί να βρίσκεται στην κλάση μας MyClass1, μπορεί και όχι. Η method2 θα μοιάζει κάπως έτσι:
Μορφοποιημένος Κώδικας: Επιλογή όλων
void method2() {
//...
//...
MyClass1.method1(); //ή σκέτο method1() αν την καλούμε από την ίδια κλάση...
//...
//...
} //method2
Ας πούμε επίσης ότι στις //... δεν υπάρχει κάποια δομή try που να μπορεί να «πιάσει» το DataFormatException που «ρίχνει» η method1. Σε αυτή την περίπτωση ο compiler θα μας δώσει μήνυμα λάθους λέγοντάς μας πως πρέπει να μεριμνήσουμε για την DataFormatException που ρίχνει η method1(). Αν λοιπόν ο χρήστης της μεθόδου μας μεριμνήσει και τροποποιήσει τον κώδικά του κάπως έτσι:
Μορφοποιημένος Κώδικας: Επιλογή όλων
void method2() {
//...
try {
//...
MyClass1.method1(); //ή σκέτο method1() αν την καλούμε από την ίδια κλάση...
//...
catch (DataFormatException ex) { /*...*/ }
//...
} //method2
ο compiler θα ησύχαζε. Ο compiler θα ησυχάσει επίσης αν η μέθοδος method2 δηλώσει και αυτή ότι ρίχνει το συγκεκριμένο είδος Ecxeption. Άρα την «ευθύνη» θα την έχει μία άλλη μέθοδος (έστω η method3) που θα καλέσει την 2, και η ιστορία συνεχίζεται. Για παράδειγμα αν γράψουμε:
Μορφοποιημένος Κώδικας: Επιλογή όλων
void method2() throws DataFormatException {
//...
//...
MyClass1.method1(); //ή σκέτο method1() αν την καλούμε από την ίδια κλάση...
//...
//...
} //method2
ο compiler θα μείνει ικανοποιημένος.

Τι κερδίζουμε λοιπόν δηλώνοντας ότι η μέθοδός μας ρίχνει κάποια Exception; Κερδίζουμε τη σιγουριά ότι όποιος καέσει τη μέθοδό μας, θα έχει φροντίσει να «πιάσει» το σφάλμα που η μέθοδός μας πιθανόν να «ρίξει».

Για να το δείτε και μόνοι σας, προσπαθήστε να κάνετε compile τον εξής κώδικα (αγνοείστε τις λεπτομέρειες της κλάσης File, θα μιλήσουμε για αυτή στο επόμενο κεφάλαιο):
Μορφοποιημένος Κώδικας: Επιλογή όλων
//αρχείο ThrowTest2.java

public class ThrowTest2 {

public static void main(String args[]) {
openFile("myFile");
} //main

static void openFile(String path) throws java.io.FileNotFoundException {
java.io.File f = new java.io.File(path);
if (!f.exists()) throw new java.io.FileNotFoundException("Το αρχείο "+path+" δεν υπάρχει.");
//κτλ κτλ

} //openFile

}

Θα διαπιστώσετε ότι ο compiler θα σας απαιτήσει «ή να πιάσετε την IOException, ή να δηλώσετε ότι η μέθοδος main ρίχνει IOException. Κάντε ένα από τα δύο και ο compiler θα ησυχάσει.

Εξαίρεση σε αυτό το μηχανισμό αποτελούν οι υποκλάσεις των Error, RuntimeException.
Αν η μέθοδός σας δηλώσει ότι ρίχνει ένα από αυτά τα αντικείμενα, ο compiler ΔΕΝ θα απαιτήσει από αυτόν που θα καλέσει τη μέθοδό σας να κάνει ένα από τα παραπάνω βήματα.

Ο λόγος είναι ότι τα μεν Errors δεν θα έπρεπε να τα χειριζόμαστε καθόλου, τις δε RuntimeExceptions δεν μπορεί να τις προβλέψει ο compiler ακριβώς επειδή αντιπροσωπεύουν σφάλματα που συμβαίνουν κατά τη διάρκεια της εκτέλεσης.

Είναι λοιπόν λίγο ανώφελο να δηλώνουμε ότι η συνάρτησή μας «ρίχνει» ένα από τα παραπάνω.


Κλείνοντας αυτό το κεφάλαιο θα γράψουμε ένα πρόγραμμα που μας δείχνει τα οφέλη του να χειριζόμαστε σφάλματα. Η ιδέα είναι ότι θα έχουμε ένα πίνακα από 10 int, και ο χρήστης θα μπορεί να γράφει, και να διαβάζει τα στοιχεία που έγραψε στον πίνακα αυτό. Αν δώσει μη ακέραιες τιμές το πρόγραμμα θα του το πει. Αν δώσει τιμή που είναι εκτός των ορίων του πίνακα, το πρόγραμμα επίσης θα του το πει. Καλή διασκέδαση!
Μορφοποιημένος Κώδικας: Επιλογή όλων
// αρχείο ExceptionTest.java
import java.util.Scanner;

public class ExceptionTest {

static int[] array = new int[10];

public static void main(String args[]) {

Scanner sc = new Scanner(System.in);
String command="", elements[];
int i, j;

System.out.print("Εντολές:\n\tγράψε [θέση] [τιμή]\n\tδιάβασε [θέση]\n\tq (έξοδος)\n\n");
while (!command.equals("q")) {

System.out.print(">");
command = sc.nextLine();
elements = command.split(" ");

if (elements[0].equals("γράψε")) {
try { i = Integer.parseInt(elements[1]); j = Integer.parseInt(elements[2]); array[i]=j; }
catch (NumberFormatException ex) { System.out.print("Παρακαλώ δώστε ακέραιες τιμές.\n"); }
catch (ArrayIndexOutOfBoundsException ex) { System.out.print("Παρακαλώ δώστε θέση πίνακα μεταξύ 0 και 9.\n"); }
} //if "γράψε"

else if (elements[0].equals("διάβασε")) {
try { i = Integer.parseInt(elements[1]); System.out.print("\t"+array[i]+"\n"); }
catch (NumberFormatException ex) { System.out.print("Παρακαλώ δώστε ακέραια τιμή.\n"); }
catch (ArrayIndexOutOfBoundsException ex) { System.out.print("Παρακαλώ δώστε θέση πίνακα μεταξύ 0 και 9.\n"); }
}//if "διάβασε"

} //while

} //main

}



Προηγούμενο: Τρεις χρήσιμες κλάσεις
Επόμενο: Κείμενα

Creative Commons License
Η εργασία υπάγεται στην άδεια Creative Commons Αναφορά-Παρόμοια διανομή 3.0 Ελλάδα
Γνώσεις ⇛ Linux: Μέτριο┃ Προγραμματισμός: Java, Assembly, Fortran, μαθαίνω C/X11┃ Αγγλικά: Μέτρια
Λειτουργικό σε Η/Υ ϰ μοντέλο: Ubuntu 14.04 64-bit ┃ Τρόπος εγκατάστασης: Live USB
Προδιαγραφές ⇛ Desktop: Intel i5 2320 3.00GHz.┃ MotherBoard: Asus p8h61 -m pro
Προδιαγραφές ⇛ RAM: 4GB ┃ Τροφοδοτικό Corsair CX430

GPU: Intel 2nd Generation Core Processor Family Integrated Graphics Controller [8086:0102] {i915}
5 eth0: Realtek RTL8111/8168B PCI Express Gigabit Ethernet controller [10ec:8168] (rev 06) ⋮ wlan0: 0b05:1723 ASUS WL-167G v2 802.11g Adapter [Ralink RT2571W]
Οθόνη Schaub Lorenz (Tv)
alkismavridis
punkTUX
punkTUX
 
Δημοσιεύσεις: 273
Εγγραφή: 18 Μαρ 2009, 18:46
Εκτύπωση

Επιστροφή στο Μαθήματα Java

cron