Mòdul 5

Pràctica 4: Excepcions   
Tornar presentació tema
    Pràctica 1 Pràctica 2 Pràctica 3 Pràctica 4 Pràctica 3  
 

Si has arribat fins aquí en aquest curs de programació ja deus haver experimentat que si una cosa pot funcionar malament, en algun moment o altre acabarà funcionant malament. No falla. En un entorn informàtic els errors són inevitables. No només perquè els comet el programador quan escriu el seu codi, sinó també per les condicions de l'entorn d'execució: un disc es pot omplir, un servidor pot caure i no contestar-nos, un motor de dades pot fallar...

Per a conviure amb aquests errors i, fins i tot, posar-los a favor del programador, Java ha creat un conjunt de classes especials, les excepcions, que fan possible que el programa intercepti els errors i pugui reaccionar per a solucionar-los.

Una excepció és una instància d'un tipus especial de classe que hereta indirectament de la classe Throwable, una classe prou important com per estar al paquet java.lang, aquell que no cal importar mai. Aquest és l'arbre d'herència de les excepcions. Totes deriven de Throwable. Algunes deriven de la classe Exception i d'altres de la classe RuntimeException.

 
 
 
 

Esquema d'herència de les excepcions

 
     
  Fent saltar una excepció  
 


Abans d'entendre el funcionament de les excepcions, en faràs saltar una. Diem que 'fem saltar una excepció' quan en el programa o en l'entorn d'execució es produeix un error i aquest error és interceptat per una excepció. Està en mans del programador la gestió d'aquesta excepció: pot acabar el programa, reconduir el seu flux, o el que millor convingui segons el cas.

 
     

Inicia un nou projecte i posa-li el nom d'"excepcions". El faràs servir per escriure les diferents classes d'aquesta pràctica.

Escriuràs una classe extremadament simple, la classe DividirPerZero la feina de la qual és equivocar-se i provocar un error: intentar dividir un número per zero. Com ja deus saber, una divisió per zero té un valor infinit. A Java no li senten bé els infinits, no sap que fer-ne i, si es dóna una situació d'aquest tipus, es produirà un error en temps d'execució. Comprova-ho:

 
     
/**
* Excepcions: dividir per zero
*
* @author Angel Solans
* @version 25-02-2005
*/

public class DividirPerZero {

    public int divideixPerZero(int unEnter) {
         return unEnter/0; //Això és un error!
    }


}

 
     
 
Un cop tinguis escrita la classe, compil·la i crea'n un objecte. Intenta executar el mètode divideixPerZero(). En quan has introduït el número enter que ha de fer de numerador, el programa s'interromp i salta una finestra com aquesta, que ens indica que s'ha produït un errord d'aritmètica (ArithmeticException) a l'intentar fer una divisió per zero (/ by zero).

 
 
 
 
El BlueJ fent saltar una excepció
 
     
 

En aquest moment, l'aplicació ha deixat de funcionar. Imagina't quin problema: un programa que es trenca per un error tan petit!

El que faràs ara serà protegir aquesta font d'errors gestionant l'excepció que s'ha produït . Modifica la classe DividirPerZero de la següent forma:

 
     
/**
* Excepcions: dividir per zero
*
* @author Angel Solans
* @version 25-02-2005
*/

public class DividirPerZero {

    public int divideixPerZero(int unEnter) {
        try {
        
// Això s'executa si no hi ha errors

            return unEnter/0;
        } catch(Exception e) {
        // tot el que hi ha a dins del catch s'executa si hi ha error
            System.out.println("Ep! ha saltat l'error: "+
                e.getMessage());
            return -1;
        }

    }
}

 
     
 

Observa les modificacions respecte la classe original: has embolcallat el codi que pot donar error amb una estructura del tipus try {} catch {}, que és una de les formes per a gestionar excepcions de les que disposa Java. En pseudocodi el text que has afegit al programa es pot traduir com:

"intenta fer la divisió d' unEnter per zero . Si no hi ha cap error, retorna el valor de la divisió.
Si es produeix un error,
1) crea un objecte de la classe Exception que es digui e i que capturi la informació sobre l'error que s'ha produit
2) escriu a la cònsola "Ep! ha saltat l'error" + el missatge d'error que porta l'objecte e i, finalment
3) retorna l'enter -1

Compil·la novament la classe i executa-la. Crea una nova instància de la classe DividirPerZero i executa el mètode divideixPerZero().

El programa ja no es trenca! En el moment que es produeix l'error, el fluxe de programa abandona el bloc try {} i salta cap al bloc catch {}. La informació de l'error la porta l'excepció e:

 
     
 
 
     
 
Observa com el programa obre una cònsola i t' informa de l'error:

 
     
 
 
     
  i, finalment, retorna el valor -1 :
 
     
 
 
     
 

La gestió de l'excepció ha fet possible recuperar-te de l'error sense que el programa es trenqui.

 
 
 
  Diferents tipus d'Excepcions  
 


Excepcions predefinides

Ara que ja veus per a què serveixen les excepcions, pots començar a considerar-les amb més detall.

El primer que has de saber és que, d'excepcions, n'hi ha de molts tipus. Moltes estan predefinides pel llenguatge, altres les pots crear tu. El paquet java.lang en defineix un grup d'utilització comuna. Aquestes excepcions salten freqüentment, especialment quan no fem les coses massa bé: NullPointerException, ArrayIndexOutOfBoundsException, etc. són excepcions que t'avisen de que estàs accedint als mètodes d'un objecte null, que t'has passat de rang en la lectura d'una matriu i aquest tipus de repèl habituals en el procés de programació d'un projecte.

Si, per exemple, estàs llegint una matriu i intentes fer una lectura fora de rang, salta una excepció en temps d'execució:

 
 
 
 
El BlueJ ens informa d'una excepció del tipus ArrayIndexOutOfBoundsException
 
     
 

En aquest exemple hem intentat llegir la sisena posició d'una matriu que només té tinc elements. El programa genera una excepció de les que estan 'enllaunades' en el llenguatge, una ArrayIndexOutBoundsException. Ara és responsabilitat del programador decidir com es captura l'excepció i que s'ha de fer per recuperar el normal funcionament del programa.

No t'explicarem aquí tota la jerarquia d'excepcions predefinides. Només et proposem un petit exercici. Intenta endevinar, pel nom, en quin context es produeixen les següents excepcions del paquet java.lang:

StringIndexOutOfBoundsException

NumberFormatException

ClassCastException

OutOfMemoryException

 

Llençar una excepció predefinida a voluntat del programador

Observaràs que aquestes excepcions es produeixen exclusivament quan es dóna una condició predeterminada. Java permet, però, que el programador pugui decidir en quina situació es llançarà una excepció. D'aquesta forma s'incrementa el control del programador sobre el fluxe del programa i es pot escriure un codi més manejable.

Aquesta classe, per exemple, genera una excepció si, al cridar el constructor, li donem un valor null al paràmetre nick:

 
     
/**
*
* @author Angel Solans
* @version 25-02-2005
*/

public class Jugador {
    private String nick;

    public Jugador(String nick) throws Exception {
        if (nick==null) throw
            new Exception("S'ha de donar un nick al jugador")
;
        this.nick=nick;
    }
}
 
     
 

Escriu aquesta nova classe, compil·la i crea'n un objecte. En el moment de crear-lo, el BlueJ et demanarà un valor per al paràmetre nick. Si escrius null, el programa es trencarà tot donant aquest error:

 
     
 
 
 
El BlueJ es trenca per una excepció que hem llençat nosaltres des del programa
 
     
 

Ja has disparat una excepció a la teva voluntat. Aquesta eina et pot ser força útil en temps de disseny i depuració de programes i també per a fer més robust i legible el codi que tinguis en producció.

El mecanisme per a provocar l'excepció ha estat una crida a throw new Exception. En aquesta pràctica t'explicarem millor com l'has d'utilitzar.

 
     
  Gestió completa d'excepcions: els blocs try-catch.  
     
 

Fixa't en la forma de gestionar l'error que et proposem en el text de la primera classe de la pràctica, DividirPerZero, i compara-la amb la forma en què has operat a la classe Jugador. En un cas has utilitzat un bloc try-catch i en el següent has fet anar una sentència throws.

Els blocs try-catch constitueixen el que definim com a mètode de gestió completa d'excepcions en Java. Diem que són el mètode complet perquè quan tanquem un bloc de codi en un try-catch no només protegim el programa en el bloc try sinó que també decidim el mecanisme de recuperació en el bloc catch . Aquesta és la sintaxi d'un bloc try-catch:

    try{
        // <codi a protegir> ;
    } catch (Exception e) {
        // <què fem en cas d'error> ;
    }

La lògica d'aquesta estructura és la següent: si es produeix qualsevol error a l'interior del bloc try{ } , es crea un objecte excepció - Exception e al nostre exemple- i el flux del programa salta dins del bloc catch{ } on hi haurem posat la solució del problema. Per exemple:

    try{
        // <codi a protegir> ;
    } catch (Exception e) {
        // <què fem en cas d'error> ;
    }

Podem posar vàries sentències catch per cada try{. Per exemple aquesta protecció és perfectament correcta:

    try{
        // <codi a protegir> ;
    } catch (NullPointerException npe) {
        // <què fem en cas d'error NullPointerException> ;
    } catch (Exception e) {
        // <què fem en cas de qualsevol altre tipus d'error> ;
    }

El block try pot contenir opcionalment un tercer component. És la cláusula finally. Serveix per a que, passi el que passi amb la gestió de l'excepció, salti o no salti, s'executi un codi. Té el següent aspecte:

    try {
        // Accions protegides
    } catch(Exception e) {
        // Accions a fer en cas d'error
    } finally {
       
//Això s'executa en qualsevol cas
    }

Finally fa possible que un bloc try no contingui cap clàusula catch. Això és legal:

    try {
        // Accions protegides
    } finally {
        //Això s'executa en qualsevol cas
    }


 
     
  Gestió parcial d'excepcions: throw i throws.  
     
 

Si no protegim el nostre codi amb blocs try-catch els errors es propagen en cascada saltant entre mètodes i classes fins arrivar a la màquina virtual que és quan el programa es trenca. Ja ho has experimentat al començar la pràctica.

Imagina un programa hipotètic que tingués el següent aspecte:

 
     
 
 
     
 

La màquina de java crida el mètode main() del programa. Aquest fa una crida a un primer mètode i el primer mètode posa a treballar un segon mètode. Si es produeix un error en el segon mètode i no hi ha cap bloc try-catch protector es genera una excepció que el segon mètode passa al primer. Si aquest primer mètode no està preparat per aturar l'excepció i no sap que fer-ne, li passa la patata calenta al mètode main(). Finalment si aquest mètode tampoc sap que fer, li cedeix l'excepció a la màquina virtual i es trenca el programa.

A vegades no ens interessa tancar completament l'excepció i ens pot resultar útil que l'error es propagui controladament d'un mètode a un altre. Això ho podem aconseguir amb l'especificació d'excepcions throws en combinació amb el generador d'excepcions throw. Entre els dos formen un tàndem que ens permet ordenar perfectament el fluxe del programa en la gestió dels errors.

 
     

Crea una nova classe en el projecte Excepcions que porti de nom "ExpenedorEntrades". Ha de tenir el següent contingut:

 
     
/**
*
* @author Angel Solans
* @version 15-02-2005
*/

public class ExpenedorEntrades {
    public boolean ven(int numentrades) throws Exception {
        if (numentrades<0 || numentrades>10)
            throw new Exception("Número d'entrades no permés");
        return true;
    }

    public void guixeta(int numentrades) {
        System.out.println("Benvinguts al Teatre Principal");
        ven(numentrades); // Fem la venda
    }

}

 
     
 

Es tracta d'una simulació de venda d'entrades. El mètode guixeta() dóna la benvinguda a l'usuari i crida un segon mètode ven() que ens confirma la venda. Observa com planifiquem la gestió d'excepcions: en cas que intentem vendre menys d'una entrada o més de deu, farem saltar una excepció genèrica del tipus Exception.

Com ens interessa avisar a qualsevol altre mètode que pugui utilitzar el mètode ven() que del seu interior pot saltar una excepció del tipus Exception, ho publiquem amb la clàusula throws Exception.

La clàusula throws serveix, doncs, per a avisar a qualsevol mètode client que és força probable que el mètode retorni una excepció del tipus indicat en el throws. I si es fa aquest avís és perquè el mètode en qüestió no pensa fer cap gestió d'aquesta excepció. És el mètode client qui ha de preveure que en farà si es dóna el cas.

Intenta compil·lar la classe. Què passa?

Doncs que falla la compil·lació i et retornen el següent missatge d'error:

 
     
  unreported exception java.lang.Exception; must be caught or declared to be thrown  
     
 

Què és el que ha passat?

Java t'està protegint i està intentant que gestionis correctament l'excepció. Les excepcions del tipus genèric Exception són d'una categoria tal que el Java no et deixarà que es propaguin fins a la màquina virtual de Java. Si en un mètode declares que pot saltar una Exception a través d'un throws a la capçalera, el mètode client ha de contemplar forçosament la gestió de l'excepció, ja sigui a través d'un bloc try-catch o posant un throws a la capçalera del client.

Modifica el programa de la següent forma:

 
     
/**
*
* @author Angel Solans
* @version 15-02-2005
*/
public class ExpenedorEntrades {
    public boolean ven(int numentrades) throws Exception {
        if (numentrades<0 || numentrades>10)
            throw new Exception("Número d'entrades no permés");
        return true;
    }

    public void guixeta(int numentrades) {
        System.out.println("Benvinguts al Teatre Principal");
        try {
            ven(numentrades); // Fem la venda
            System.out.println("Venda correcta");
        } catch(Exception e) {
            System.out.println(e.getMessage());
        }
    }

}

 
     
 

Observa que el que has fet és tancar el mètode potencialment perillós amb un bloc try-catch. Només amb això, el compil·lador ja no es queixarà perquè has completat la protecció davant l'excepció.

Compil·la el programa. Ara no tindràs cap problema. Crea una instància i executa el mètode guixeta(). Intenta la venda de diferents quantitats d'entrades. Observa què passa si vens menys d'una entrada o més de deu.

Quan utilitzis la parella throws-throw observaràs que el compil·lador no sempre falla quan poses un throw en un mètode i no especifiquis el throws corresponent a la capçalera. Amb algunes excepcions protesta i amb altres no. Potser en un principi et semblarà una mica atzarós, però el llenguatge ho té completament determinat: Totes les excepcions derivades d'Exception s'han de gestionar íntegrament. En canvi totes aquelles que hereten de RuntimeException es poden gestionar de forma incompleta. L'esquema és el següent:

 
 

 

 
 
 
     
 

Java ha planificat les excepcions del tipus Exception per aquelles situacions en que l'error és previsible (operacions de lectura/escriptura, connexions a base de dades, etc) i, per tant, el cicle de control ha de ser complet. Les RuntimeException, en canvi, es produeixen en situacions imprevisibles (accés a objectes nuls, lectura de matrius fora de rang, divisions per zero, etc) habitualment producte d'un mal disseny de programa i són de control més relaxat. Java suposa que el programador depurarà el codi d'aquest tipus d'errors en el cicle de programació i no apareixeran quan entregui el programa al client.

Has de tenir molt en consideració aquests dos tipus d'excepcions que t'ofereix el llenguatge, especialment en el moment en que creis les teves pròpies excepcions. Si vols que una excepció teva tingui una gestió estricta, fes-la derivar d'Exception. Si no, en tens prou amb que sigui filla de RuntimeException. Les regles per triar un o altre model no són estrictes, dependran una mica de les teves manies com a programador.

Ara aprendràs a crear excepcions adaptades a les teves necessitats.

 
 


 
  Excepcions a la carta: Creant les nostres pròpies excepcions.  
     
 

Quan no en tinguis prou amb les excepcions convencionals sempre pots recórrer a la creació personalitzada d'excepcions. Si vols fer que es gestioni completament, fes-la derivar d'Exception si no, com has vist, la pots fer derivar de RuntimeException.

Potser et preguntaràs per a què necessites crear excepcions personalitzades quan n'hi ha tantes d'enllaunades amb el llenguatge. La resposta està en l'augmen d'eficàcia en el control de les teves aplicacions.

Imagina, per exemple, que fas un programa per a la gestió d'envasat de pastisseria industrial. No seria pràctic recollir i gestionar excepcions que contenen en el seu propi nom una bona informació sobre els errors que s'estan produïnt? Posa imaginació i dedueix que està passant si salten aquestes excepcions personalitzades en el teu programa d'envasat:

ImpossibleTancarEnvasException

MassaBrioxosALaCapsaException

EnvasBuidException

CruasantTrencatException

Aquest és l'objectiu de les excepcions personalitzades, donar molta informació i permetre una gestió molt fina dels possibles errors que es poden donar en un programa.

La creació d'una excepció personalitzada és una tasca força simple.

En el teu projecte Excepcions crearàs una excepció personalitzada per a utilitzar en la cadena d'envasat de pastisseria industrial. És aquesta:

 
     

/**
*
* @author Angel Solans
* @version 06-03-2005
*/
public class CruasantTrencatException extends Exception {

    // Codi del treballador que ha trencat el cruasant
    private String codiEnvasador;

    public CruasantTrencatException(String codiEnvasador) {
       this.codiEnvasador=codiEnvasador;
    }

    public String getCodiEnvasador() {
       return codiEnvasador;
    }

    public String toString() {
        return "L'envasador amb codi '"+
            codiEnvasador+"' ha trencat el cruasant.";
    }

}

 
     
 

És tracta d'una excepció pensada per a saltar quan un treballador especialment inhàbil de la cadena d'envasat trenca un cruasant. Com el gerent de l'empresa et demana informació sobre el responsable de la malifeta, aprofites l'excepció per a incloure informació sobre l'obrer.

Observa que hem triat Exception com a classe mare. Això vol dir que has de fer una gestió completa de l'excepció o no podràs compil·lar el programa.

L'excepció té un atribut, codiEnvasador, que serveix per a recollir el codi del treballador responsable de l'error, i un mètode getter per accedir a l'atribut. Finalment, creem un mètode toString() amb informació relativa a l'origen de l'excepció.

Escriu aquest programa per a posar en marxa la teva excepció personalitzada. Crea la classe Cadena al teu projecte Excepcions. Ha de ser així:

 
     
/**
*
* @author Angel Solans
* @version 06-03-2005
*/

public class Cadena {
    public void envasa(String envasador)
        throws CruasantTrencatException {
        
        if (envasador.equals("Manscompeus"))
            throw new CruasantTrencatException(envasador);
        System.out.println(envasador+" ha envasat el cruasant");
    }

    public void cadenaEnvasat() {
        // Hem de protegir amb try-catch per força!
        try {
            envasa("Envasador Potent ");
            envasa("Envasador Mitjanet ");
            envasa("Manscompeus");
        }catch(CruasantTrencatException e) {
            System.out.println(
                "Cruasant trencat per "+e.getCodiEnvasador());
        }

    }

}

 
     
 

Crea'n una instància i executa el mètode cadenaEnvasat(). Analitza el funcionament de la teva primera excepció personalitzada. Quina feina ha fet?

Et proposem completar aquesta pràctica creant un programa per a fer divisions que utilitza les excepcions per a fer el control d'entrada de dades. Necessites crear dues excepcions personalitzades i la classe que far les divisions. Afegeix-les totes al teu projecte:

 
     
/**
* Excepció per a números que estan fora del rang.
*
* @author Angel Solans
* @version 25-02-2005
*/

public class ForadeRangException extends RuntimeException {
    public ForadeRangException(String msg) {
        super(msg);
    }
}
 
     
/**
* Excepció per a denominador igual a zero.
*
* @author Angel Solans
* @version 25-02-2005
*/

public class DenominadorZeroException extends RuntimeException {
    public DenominadorZeroException(String msg) {
        super(msg);
    }
}
 
     
/**
*
* @author Angel Solans
* @version 25-02-2005
*/

public class Dividir {
    public void divideix(String num, String den) {
        double numerador=0.0;
        double denominador=0.0;
        String incorrecte="El càlcul no ha estat possible: ";

        try {
            numerador = Double.parseDouble(num);
            denominador = Double.parseDouble(den);
            dinsRang(numerador,denominador);
            System.out.println("El resultat és                         "+Double.toString(numerador/denominador));
        }catch(NumberFormatException e) {
            System.out.println(incorrecte+e.getMessage());
        }catch(ForadeRangException e) {
            System.out.println(incorrecte+e.getMessage());
        }catch(DenominadorZeroException e) {
            System.out.println(incorrecte+e.getMessage());
        }catch(Exception e) {
            System.out.println(incorrecte+e.getMessage());
        }            
    }

    private void dinsRang(double numerador, double denominador)             throws ForadeRangException, DenominadorZeroException {
            if (denominador==0.0) throw new                     DenominadorZeroException(
                         "No és vàlid un denominador igual a 0");
            if (numerador < 1.0 || numerador > 1000.0) throw new                     ForadeRangException(
                         "El numerador només entre 1 i 1000");
            if (denominador < 1.0 || denominador > 100.0) throw new
                    ForadeRangException(
                         "El denominador només entre 1 i 100");
    }
}

 
     
 

Observa les excepcions personalitzades. En aquesta ocasió hem optat per a fer-les el més simples possible. Només contenen un constructor que activa el constructor de la classe mare. És el mecanisme més convencional de creació d'excepcions i suficient per a la major part dels casos.

Repassa la classe Dividir. Veuràs que el sistema d'excepcions s'utilitza per a fer una validació completa del numerador i el denominador. Un mètode dinsRang() fa la verificació de la qualitat de dades. En el cas que les dades no siguin correctes, retorna una excepció del tipus ForadeRangException o DenominadorZeroException.

 
     

1) Observa la següent sequècia de blocs catch. L'ordre en que estan posats els elements és rellevant o indiferent? Raona la teva resposta.

       }catch(NumberFormatException e) {
            System.out.println(incorrecte+e.getMessage());
        }catch(ForadeRangException e) {
            System.out.println(incorrecte+e.getMessage());
        }catch(DenominadorZeroException e) {
            System.out.println(incorrecte+e.getMessage());
        }catch(Exception e) {
            System.out.println(incorrecte+e.getMessage());
        }  

2) Inventa una nova restricció de dades de la teva elecció. Crea una excepció personalitzada que la representi i afegeix-la a l'estructura de control de l'aplicació.