Mòdul 5

Pràctica 2: Herència. La biblioteca escolar amb herència
Tornar presentació tema
    Pràctica 1 Pràctica 2 Pràctica 3 Pràctica 4 Pràctica 5  
     
  Els problemes del programa de biblioteca sense herència  
 


Repassa una mica el codi de la classe Biblioteca de la pràctica anterior. El programa funciona, per tant és util, però ... no està massa ben planificat!.

Ets capaç de veure quins defectes de disseny té?

 
     
 

La primera sensació que tens al revisar la classe és la presència de massa codi duplicat. Tot i l'escassa capacitat operativa del programa, trobem més d'una situació de duplicació de codi:

  • Tens dos ArrayList, un per a cada tipus d'objecte que necessites desar a la base de dades.

        private ArrayList llibres;
        private ArrayList revistes;

  • El mètode imprimir() fa dos bucles amb dos objecte Iterator, un per als llibres i l'altre pera les revistes.

        public void imprimir() {
            for(Iterator iterador = llibres.iterator();
             iterador.hasNext();) {
                Llibre llibre = (Llibre)iterador.next();
                llibre.imprimir();
                System.out.println("--------------------");
            }
            for(Iterator iterador = revistes.iterator();
             iterador.hasNext();)     {
                Revista revista = (Revista)iterador.next();
                revista.imprimir();
                System.out.println("------------------");
            }
        }

  • Són necessaris dos mètodes d'entrada de dades; afegeixLlibre() i afegeixRevista().
  • Finalment, els objectes Llibre i Revista tenen més camps en comú que camps exclusius, però La reescriptura de camps idèntics en diferents classes fa força "pesant" l'aplicació.
 
     
 

El programa tampoc resulta fàcil de mantenir. Imagina que et demanen afegir a la base de dades els DVDs, Videos o qualsevol altre recurs. L'impacte sobre el programa serà enorme. A més de crear les noves classes, hauràs de modificar completament la classe Biblioteca: et serà necessari sumar un tercer o quart mètode d'entrade de dades (afegeixDVD(), afegeixVideo(), etc), hauras de preveure nous ArrayList i a la impressió hauràs d'afegir nous Iterator. Si els efectes de la modificació són sensibles en un programa com el que has fet, mínimament funcional, imagina com podria seria en un programa complet posat en producció.

 
     
  Ara veuràs com les capacitats d'herència de Java t'ajudaran a resoldre amb elegància i simplicitat tots aquest problemes.  
     
  El concepte d'herència  
     
 

La idea d'herència és força senzilla: intenta pensar en els objectes de Java en termes d'organismes biològics. Imagina que un objecte de Java pot tenir fills i que aquests fills hereten l'aspecte físic i les habilitats de la mare. Així és com funciona l'herència en els llenguatges de programació moderns: una classe mare pot tenir una classe filla i aquesta classe filla heretarà els camps i mètodes de la classe mare. Tots els fills d'una classe mare compartiran, d'entrada, aquest fons comú de camps i mètodes. Tot allò que sap fer la mare ho saben fer les classes filles. Qualsevol canvi en un mètode de la classe mare s'incorpora automàticament a les classes filles.

 
     
 

En termes del projecte de biblioteca, oi que els llibres i les revistes de la biblioteca tenen moltes coses en comú? Tantes que podriem dir que són filles de la mateixa mare? Ambdues són recursos o Items de la biblioteca i tenen un codi d'inventari, un codi de classificació decimal, un número de pàgines, una ubicació a la biblioteca i tants altres elements d'nformació compartits.

 
     
 

Java ens permet expressar amb facilitat aquesta relació de parentiu entre els objectes. Observa i estudia el diagrama següent. Podem crear un objecte Item que és la classe mare a la que pertany qualsevol recurs de la biblioteca, ja sigui un llibre, una revista, un video, un CDRom, un DVD o qualsevol altre format de recurs que pugui aparéixer d'aquí un temps.

Després, les classes concretes ( Llibre, Revista, DVD, etc,) les podem crear com a descendents de la classe Item:

 
     
 
 
 
Nova jerarquia d'objectes de l'aplicació de biblioteca
 
     
 


Observa bé el diagrama UML de la reestructuració de les classes:

1) Hem creat una classe nova que es diu Item. Aquesta classe conté els camps i mètodes que comparteixen tots els recursos de la biblioteca. Tots els items reben una classificació decimal determinada, poden ser comentats, es van crear en una data determinada per una editorial determinada, tenen un títol i pertanyen a un gènere concret. Fixa't en que els camps de la classe Item són el que poden compartir tots els recursos. Els camps específics d'un llibre o una revista, no apareixen aquí.

També afegirem a aquesta classe els mètodes corresponents a accions que afecten a tots els tipus de recursos. En algun moment o altre haurem de llistar els objectes de la biblioteca, per tant, tots els recursos comparteixen el mètode imprimir(). També el crearem a la classe Item.

2) Hem creat dues classes descendents d'Item. Són les classes Llibre i Revista. Com són filles d'Item, hereten els camps i mètodes de la classe mare. Només ens caldrà afegir els camps i mètodes particulars (en el cas dels llibres afegim el camp autor, en el cas de les revistes el camp numero, per a recollir el número de la revista).

 
     
  Superclasse i subclasse  
     
 

Quan una classe té classes filles que extenen la seva funcionalitat es diu que aquesta classe és una superclasse.

Quan una classe és filla d'una altra classe, es diu que és subclasse de la classe mare.

La classe Item és superclasse de les classes Llibre i Revista. Les classes Llibre i Revista són subclasses de la classe Item.

 
     
La profunditat de l'herència entre classes pot ser tan complexa com t' interessi, Java soportarà tantes subclasses com siguis capaç de crear. A l'hora de pensar la jerarquia d'herència de la teva aplicació, però, has d'intentar sempre crear una estructura que sigui comprensible. Això vol dir que has de procurar no crear jerarquies massa profundes que facin il·legible el programa. Pensa que les classes del paquet estandar de Java ràrament tenen una profunditat superior a quatre esglaons d'herència (besavia - avia - mare - filla ).  
     

Abans de revisar el codi de l'aplicació de biblioteca, jugaràs una mica a crear jerarquies d'herència.

Observa la següent jerarquia de classes:

 
 
 
  Es tracta d'una petita classificació dels vehicles: Tots els vehicles amb motor d'explosió comparteixen uns elements amb els vehicles elèctrics. Automòbils i Motocicletes comparteixen característiques com a vehicles impulsats per motors d'explosió i tant les motos de motocros com les de carretera són motocicletes.  
 


Crea, amb llapis i paper, els teus propis diagrames de jerarquia de classes per a representar els següents conjunts d'objectes:

1) De l'institut: Professor, alumne, delegat de classe, cap d'estudis, director, bibliotecari, administratiu.

2) De la informàtica: Portàtil, sobretaula, impressora, escàner.

3) De la natura: Gat, gos, cèrvol, siamès, lluç, sardina, tonyina.

 
     
 

Et sents ja còmode amb les jerarquies? Doncs revisa ara com es gestiona l'herència des del llenguatge Java.

 
     

Crea un nou projecte BlueJ a la teva carpeta de projectes. Dóna-li el nom de "bibliotecaherencia".

Escriu en primer lloc la classe "mare" de tots els recursos, els items de la biblioteca:

 
     
/**
* @author Angel Solans
* @version 24/04/2005
*
*/

public class Item {

    protected String classificacioDecimal;
    protected String comentaris;
    protected String titol;
    protected String editorial;
    protected String data;
    protected int pagines;
    protected String genere;

    public Item(String titol, String data) {
        this.titol=titol;
        this.data=data;
    }

    public void imprimir() {
        System.out.println("mètode imprimir() de la classe Item");
    }

}

 
     
 

Observa la simplicitat de la classe:

En primer lloc recollim tots els camps comuns a qualsevol recurs de la biblioteca. Seguidament creem un constructor mínim que inicialitza el títol del recurs i la data d'edició i finalment afegim un mètode void imprimir() que no fa absolutament res.

Segurament et cridaran l'atenció dos aspectes del codi:

  • L'etiqueta protected dels camps de la classe i,
  • L'existència d'un mètode void que, aparentment, no fa res, el mètode imprimir().

Ara et satisfarem la primera curiositat, la segona te l'explicarem una mica més avall.

 
     
  Visibilitat entre classes amb relació d' herència (camps i mètodes protected)  
     
 

Com ja saps, els camps i mètodes etiquetats com a public són visibles dins la classe i des de l'exterior de la classe. Els camps i mètodes private, en canvi, només són visibles des de l'interior de la pròpia classe i mai des de fora.

 
     
 

Com encaixa aquest model amb l'herència? Les classes filles poden veure els camps i mètodes privats de les classes mare? La resposta és NO. Si tenim una classe mare que té camps marcats com a privats, aquests camps no són visibles a les classes filles. Per a obtenir i modificar els seus valors necessitarem utilitzar mètodes accessors, com qualsevol altra classe. Si tenim, en canvi, camps públics a la classe mare, les classes filles els poden utilitzar com a camps propis.

 
     
  L'etiqueta protected s'ha creat per a fer possible que les classes lligades per l'herència puguin accedir als camps i mètodes de la classe mare.  
     
  Marcant un camp d'una classe com a protected aconseguim que sigui visible a totes les classes descendents però completament invisible a les classes no vinculades per herència.  
     
  Observa la classe Item, al declarar el camp titol com a protected, aconseguim que les classes Llibre o Revista el vegin i puguin utilitzar com a propi mentres que queda ocult a la resta de classes.  
     
Escriu ara les dues classes filles d'Item, les classes Llibre i Revista:  
     
/**
* @author Angel Solans
* @version 14/04/2005
*/

public class Llibre extends Item {
    private String autor;


    public Llibre(String autor, String titol,
         String editorial, String data) {
        super(titol,data);
        this.autor=autor;
        this.editorial=editorial;
    }

    public void imprimir() {
        System.out.println("(Llibre) "+titol+
         ", "+autor+", ed. "+editorial);
        System.out.println(" -> "+comentaris);
    }

}

 
     

/**
* @author Angel Solans
* @version 14-04-2005
*/

public class Revista extends Item {

    private String numero;

    public Revista(String titol, String numero, String data ) {
        super(titol,data);
        this.numero=numero;
    }

    public void imprimir() {
        System.out.println("(Revista) "+titol+
         ", nº "+numero+", "+data);
        System.out.println(" -> "+comentaris);
    }

}

 
     
  Compara la classe que acabes d'escriure amb l'equivalent de la primera versió del programa:  
     
  /**
* @author Angel Solans
* @version 13/04/2005
*/
public class Revista {

    private String numero;
    private String titol;
    private String editorial;
    private String data;
    private int pagines;
    private String classificacioDecimal;
    private String comentaris;
    private String departament;
    private String genere;

    public Revista(String titol, String numero, String data ) {
        this.titol=titol;
        this.numero=numero;
        this.data=data;
    }

    public void imprimir() {
        System.out.println("(Revista) "+titol+
         ", nº "+numero+", "+data);
        System.out.println(" -> "+comentaris);
    }

}

 
     
 

El codi s'ha simplificat considerablement. Han desaparegut la major part dels camps perquè ambdues classes prendran com a seus els camps comuns continguts a la classe mare Item.

Repassa ara les novetats del codi:

1) Declaració de l'herència (extends) . Observa el capçal de les classes:

public class Llibre extends Item {

amb l'expressió extends Item estem dient a java que la classe Llibre és filla de la classe Item. A partir d'aquí, les classes i mètodes public o protected de la classe mare Item poden ser utilitzades com a pròpies per la classe filla Revista.

- Busca en el constructor de la classe Llibre, un exemple d'utilització a la classe filla d'un camp de la classe mare. Efectivament, en el constructor de Llibre:

    public Llibre(String autor, String titol,
         String editorial, String data) {
        super(titol,data);
        this.autor=autor;
        this.editorial=editorial;
    }

utilitzem el camp editorial, que no està definit a la classe filla Llibre sinó a la classe mare Item.

2) Inicialitzacions d'objectes i herència (super) . Observa la següent expressió en el constructor de la classe Llibre:

    public Llibre(String autor, String titol,
         String editorial, String data) {
        super(titol,data);
        this.autor=autor;
        this.editorial=editorial;
    }

aquesta crida a super(titol,data) és una crida al constructor de la classe Item. Quan creem una classe que és subclasse d'una altra i aquesta superclasse té un constructor parametritzat o té més d'un constructor, hem de cridar explícitament el constructor que ens interessa des del constructor de la subclasse . Si no ho fem així, Java intentarà fer una inicialització automàtica.

Tot i que no és imprescindible cridar el constructor de la superclasse si aquest no està parametritzat, és un bon costum posar com a primera expressió del constructor d'una subclasse la crida al constructor de la superclasse.

Crea una nova versió de la classe Biblioteca. Ha de tenir el següent aspecte:

 
import java.util.Iterator;
import java.util.ArrayList;

/**
*
* @author Angel Solans
* @version 14-04-2005
*/


public class Biblioteca {
    private ArrayList items;

    public Biblioteca() {
        items = new ArrayList();
    }

    public void afegeixItem(Item item) {
        items.add(item);
    }

    public void imprimir() {
        for (Iterator iterador = items.iterator();
         iterador.hasNext();) {
            Item item = (Item)iterador.next();
            item.imprimir();
        }
    }

}
 
 


Observa l'enorme simplificació de codi que suposa la utilització de l'herència a la classe Biblioteca:

1) Reduïm la base de dades de dos ArrayList a un de sol. Situació que no ens cal modificar tot i que afegim més subclasses a la classe Item.

2) Reduïm els mètodes d'inserció de dades a un de sol. En lloc de necessitar els mètodes afegeixLlibre(), afegeixRevista(), afegeixDVD()... només necessitem un sol mètode, el mètode afegeixItem(). Com tant les revistes com els llibres són Items, aquest mètode accepta per igual la inserció d'instàncies de la classe Llibre com instàncies de la classe Revista o qualsevol subclasse d'Item.

3) Reduïm els bucles d'impressió a un de sol. Ja no és necessari fer un bucle per a cada tipus de recurs sinó que, com tots els objectes desats a l'ArrayList són Items, es poden recórrer junts.

 
     
  Sobreescriptura de mètodes  
     
 

Aquesta simplificació l'hem obtingut sense afegir gairebé cap novetat significativa en el codi del programa. Només és ressenyable, segurament, la utilització del mètode imprimir(). Has observat que la classe Item té un mètode imprimir() i cadascuna de les seves subclasses també? Quin sentit té aquesta duplicitat de defincions (sobreescriptura en termes de Java)?

Per a comprovar-ho executa el programa. Crea instàncies de llibres i revistes i afegeix-los a la base de dades. Finalment executa el mètode imprimir() de la classe Biblioteca. Revisa la sortida a la cònsola, té el següent aspecte, idèntic al programa de la primera versió:

 
 
 
 


Quan es fa la impressió, el programa va recorrent l'ArrayList d'objectes. Tot i que, genèricament, tots són del tipus Item Java no oblida la subclasse a la que pertany cadascun dels Items. Si es troba amb un Llibre, utilitza el seu mètode imprimir() per a fer la sortida a cònsola.

Si es dona el cas que una subclasse d'Item no defineix cap mètode imprimir(), el programa farà la impressió a través del mètode imprimir() de la classe mare. L'estratègia és senzilla i eficaç: java intentarà executar sempre el mètode de la subclasse, si no el troba, executarà el mètode de la superclasse.

Com pots observar, la sobreescriptura de mètodes dóna una gran versatilitat a la programació.

 
     
Verifica que passa si la classe Revista no sobreescriu el mètode imprimeix(). Esborra aquest mètode de la classe Revista, compil·la i executa el programa afegint uns quants objectes dels dos tipus. Quin mètode imprimir() s'executa ara quan es llista un objecte de tipus Revista?  
     
  Polimorfisme  
     
 

En aquest mètode de la classe Biblioteca

    public void imprimir() {
        for (Iterator iterador = items.iterator();
         iterador.hasNext();) {
            Item item = (Item)iterador.next();
            item.imprimir();
        }
    }


es diu que
la variable item és una variable polimòrfica. Això vol dir que es tracta d'una variable que pot contenir objectes de diferents subclasses (totes les subclasses de la superclasse Item). Aquesta potent característica, com has vist, fa possible una gran simplificació de codi.

 
     
  Assignació de variables i casting utilitzant l'herència  
     
 

Per defecte, sempre que assignem un objecte a una variable, creem l'objecte del mateix tipus que la variable. Per exemple, si creem un objecte Persona procedim així:

Persona unaPersona = new Persona("Carles");

Això et sóna evident, oi? En situacions d'herència, en canvi, podem fer alguna variació: podem fer que un objecte s'inicialitzi des d'una subclasse .

En aquesta jerarquia d'herència:

 
     
 
 
     
 

És possible fer inicialitzacions convencionals:

Persona unaPersona = new Persona();

però també ens pot interessar inicialitzar des de la superclasse cap a una subclasse:

Persona unaPersona = new Home();
Persona unaSegonaPersona = new Dona();

En aquesta situació, unaPersona i unaSegonaPersona són objectes de la mateixa classe, del tipus Persona. Podem manipular-los junts en molts contextes de programació. Però, de fet, un d'aquests objectes contindrà un Home i l'altre una Dona.

El camí contrari NO és posible. No es pot inicialitat des de la subclasse cap a la superclasse:

Home unaPersona = new Persona(); // codi incorrecte!

De forma similar, i seguint el mateix criteri d'inicialitzar només cap a les subclasses, has de vigilar a l'hora de fer casting entre superclasses i subclasses. Només pots fer conversions d'una superclasse a una subclasse. Mai podràs fer una conversió d'una subclasse a un superclasse ni d'una subclasse a una altra subclasse.

Tot plegat és una mica complicat. Repassa-ho a través d'aquest petit repte. Observa les següents declaracions i assignacions. Et proposem dues assignacions correctes i dues d'incorrectes. Intenta identificar-les abans de continuar llegint:

Persona unaPersona;
Home home=new Home();
Dona dona=new Dona();

// Assignacions
unaPersona=home;
home = (Home)unaPersona;
dona = (Dona)home;
dona = (Dona)unaPersona ;



Per si no la trobes, aquesta és la solució:

  • unaPersona = home és correcte perquè és una inicialització cap avall, de superclasse a subclasse (superclasse Persona = Subclasse Home).
  • home = (Home)unaPersona és correcte gràcies al casting perquè és una inicialització horitzontal (subclasse Home = subclasse Home).
  • dona = (Dona)home dóna error en temps de compil·lació perquè no són dues classes que tinguin cap relació Mare/Filla.
  • dona = (Dona)unaPersona dóna error en temps d'execució. Potencialment es tracta d'una assignació horitzontal, per això no hi ha cap error en temps de compil·lació. Però com anteriorment hem assignat a unaPersona un subtipus Home, la conversió es farà impossible en temps d'execució i saltarà una RuntimeException.

No desesperis si et costa una mica sentir-te segur amb les conversions. Continua practicant:

 
     

1) Comprova la certesa d'aquestes assignacions i conversions a partir de les classes del teu projecte: Item unItem = new Item();

a) Item unItem = new Revista();

b) Revista unItem = new Item();

Considerant

     Llibre llibre = new Llibre();
   Revista revista = new Revista();


és correcte el següent?

c) Item item = llibre;

d) llibre = (Llibre) item;

e) llibre = (Llibre) revista;

 

2) Afegeix el mètode públic getter getTitol() a la classe mare Item. Comprova si s'incorpora a les classes filles tot creant un objecte Llibre i executant en ell dit mètode getTitol().

3) Revisa alguns dels conceptes que has après en aquesta pràctica. Intenta definir que vol dir: superclasse, subclasse, protected, super(), sobreescriptura de mètodes, polimorfisme.

4) Per a concloure amb el projecte, intenta modificar-lo per a què funcioni tot afegint una nova subclasse a la superclasse Item. Crea una subclasse que es digui DVD am