Moștenirea

Conceptul de moștenire

Moștenirea este o trăsătură fundamentală a programării orientate pe obiecte, conform căreia:
    - dintr-o clasă se pot deriva alte clase. Dacă din clasa A este derivată clasa B, atunci A se numește clasă de bază, sau superclasă, iar B se numește clasă derivată, sau subclasă;
    - subclasa mosteneste câmpurile și metodele superclasei;
    - metodele superclasei pot fi redefinite în subclasă prin alte metode cu aceeași signatură;
    - metodele statice ale superclasei pot fi ascunse în subclasă;
    - câmpurile superclasei pot fi ascunse în subclasă prin câmpuri cu același nume dar care pot avea, eventual, alt tip.
    - câmpurile unei clase pot fi ascunse chiar și în metodele clasei respective, dacă aceste metode au argumente sau variabile locale cu același nume.

Membrii protejați ai clasei

Există trei moduri de acces la membrii claselor (atât la câmpuri, cât și la metode): public, privat si protejat. Până în prezent am folosit numai modificatorii de acces public și private, deoarece nu am declarat clase derivate. Introducem acum și modificatorul de acces protected. Câmpurile sau metodele declarate cu acest modificator de acces sunt vizibile (accesibile) în propria clasă și în clasele derivate din aceasta. Iată, deci, care sunt cei trei modificatori de acces la membrii unei clase folositi în declarațiile de clase:
    - public - pentru câmpuri și metode care sunt accesibile din orice clasă (inclusiv din clase aparținând altor pachete);
    - protected - pentru câmpuri și metode care sunt accesibile în propria clasă și în subclasele acesteia, dar nu sunt vizibile din alte clase;
    - private - pentru câmpurile și metodele care sunt accesibile numai din propria clasă. Acestea nu sunt accesibile din nici o alta clasă (nici chiar din subclasele propriei lor clase).
    Amintim că, în lipsa modificatorului de acces, câmpurile și metodele respective sunt accesibile din clasele aceluiași pachet. Acest mod de acces este cunoscut sub numele de prietenos (engleză: friendly) sau de pachet (engleză: package).
 

Referințele this și super

În orice clasă pot fi utilizate două referințe predefinite:
    this - este o referință la "această" instanță, adică la instanța (obiectul) din care se face referința respectivă;
    super - este o referință la superclasă.
 

Declararea clasei derivate. Clauza extends

În declarația clasei derivate (subclasei), numele clasei care se declară este urmat de clauza extends, în care se indică numele superclasei. În consecință, clasa derivată poate fi declarată astfel:

    class NumeClasa extends NumeSuperclasa {
       declaratii_de_membri
    }

Amintim că în limbajul Java orice clasă are o superclasă și numai una. Excepție face clasa Object, care este rădăcina ierarhiei de clase. Daca lipsește clauza extends, superclasa implicită este Object.

Declararea constructorului clasei derivate

Pentru a se da valori inițiale câmpurilor superclasei, în declarația constructorului subclasei poate fi invocat constructorul superclasei prin instrucțiunea
    super(lista_parametri_efectivi);
unde lista parametrilor efectivi este cea a constructorului superclasei. Aceasta instrucțiune, dacă există, trebuie să fie prima instrucțiune din corpul constructorului clasei derivate. În lipsa ei, va fi invocat constructorul fără parametri al superclasei.

Este posibil, de asemenea, ca într-un constructor să se invoce alt constructor al aceleeași clase, sub forma
    this(lista_parametri_efectivi);
 
 

Redefinirea metodelor

Metodele de instanță (nestatice) ale unei clase pot fi redefinite în subclasele acesteia. Redefinirea unei metode se face declarând în subclasă o metodă având aceeași signatură cu una din superclasă. Atunci când se redefinește o metoda protejată, modificatorul de acces al acesteia poate fi menținut, sau poate fi transformat în public.

În subclasă pot fi folosite, totuși, și metodele superclasei care au fost redefinite, dacă la invocarea acestor metode se foloseste referința super.
 
Exemplu:
În fișierul S1.java este declarata clasa S1, iar in fișierul CD1.java este declarata clasa CD1, care extinde clasa S1 (este, deci, derivată din aceasta). Reproducem aici declarațiile celor două clase:
 
/* Clasa S1 din care vor fi derivate alte clase */

public class S1 {
  public int a;
  protected double b;
  private int c;
  public final static int alpha=12;

  public S1(int a, double b, int c) { // constructorul clasei S1
    this.a=a; this.b=b; this.c=c;
  }

  private double  f1() { // o metoda privata
    return a*c+b;
  }

  protected int f2() { // o metoda protejata
    return a+2*c;
  }

  public double f3() { // o metoda publica in care se folosesc f1 si f2
    return 2*f1()+f2();
  }

  public String toString() { // redefinirea metodei Object.toString()
    return "(S1: "+a+" "+b+" "+c+")";
  }

  public static int f4(int k) {
    return 2*k+alpha;
  }
}

/* O clasa derivata din S1, in care se redefinesc unele metode */

class CD1 extends S1 {
  public CD1(int a, double b, int c) {
    super(a,b,c);
  }

  public int f2() { // redefinirea metodei f2() din superclasa
    return 2*a;
  }
  public double f3() { // redefinirea metodei f3() din superclasa
    return f2()+super.f3();
  }
}

Remarcăm următoarele:
  - în metodele clasei S1 pot fi folosite toate câmpurile declarate în această clasă, fie ele publice, protejate sau private; de asemenea, pot fi invocate toate metodele din această clasă;
  - în clasa S1 a fost redefinită metoda toString din clasa Object, astfel incât să se întoarca un șir care conține valorile câmpurilor a, b și c;
  - metoda statică f4() din clasa S1 utilizează numai câmpul static alpha al acestei clase;
  - în constructorul clasei CD1 a fost invocat constructorul clasei  S1 sub forma super(a,b,c). În acest mod au fost inițializate toate câmpurile din superclasa S1, inclusiv câmpul privat c, care nu este direct accesibil din clasa CD1;
  - în clasa CD1 au fost redefinite metodele f2 și f3 din superclasa S1 și a fost iarăși redefinită metoda toString;
  - în mod normal, în clasa CD1 se folosesc metodele f2 și f3 redefinite în această clasă. Este totuși posibil ca în clasa CD1 să se apeleze metodele superclasei, dacă în expresia de invocare se folosește referinta super. Astfel, în metoda f3 din clasa CD1 există expresia super.f3(), prin care se invoca metoda f3 din superclasa S1;
  - la redefinirea metodei f2, modul de acces a fost modificat din protected in public;
  - în metodele clasei CD1 nu au putut fi folosite câmpurile și metodele private ale superclasei S1.

Testarea claselor S1 si CD1 se face în clasa TestCD1, care se găsește în fișierul TestCD1.java. Iată aceasta clasă:
 
/* Testarea claselor S1 si CD1 */

class TestCD1 {
  public static void main(String args[]) {
    S1 s=new S1(1, 1.1, 2);
    CD1 cd=new CD1(10, 10.2, 20);
    System.out.println("s="+s+" cd="+cd);
    System.out.println("s.f2()="+s.f2()+" s.f3()="+s.f3());
    System.out.println("cd.f2()="+cd.f2()+" cd.f3()="+cd.f3());
    System.out.println("CD2.f4(3)="+CD2.f4(3)+" S1.f4(3)="+S1.f4(3));
    System.out.println("cd.f4(3)="+cd.f4(3)+" s.f4(3)="+s.f4(3));
  }
}

Executând aplicația TestCD1 se obțin urmatoarele rezultate:
 
s=(S1: 1 1.1 2) cd=(S1: 10 10.2 20)
s.f2()=5 s.f3()=11.2
cd.f2()=20 cd.f3()=460.4
CD2.f4(3)=18
cd.f4(3)=18

Remarcăm că:
  - crearea instanțelor claselor S1 și CD1 s-a făcut aplicând operatorul new asupra constructorilor claselor respective;
  - în instrucțiunea 
    System.out.println("s="+s+" cd="+cd);
este de două ori invocată implicit metoda toString, pentru a converti în șiruri afișabile obiectele s și cd;
prima dată este invocată metoda s.toString() din clasa S1. A doua oară ar trebui sa fie  invocată metoda cd.toString() din clasa CD1 dar, intrucât metoda nu a fost redefinită în aceasta clasă, a fost aplicată efectiv metoda toString() din superclasa S1;
  - în expresiile s.f2() si s.f3() sunt invocate metodele din clasa S1, careia îi aparține s, iar in expresiile cd.f2() și cd.f3() sunt invocate metodele corespunzătoare din clasa CD1;
  - metoda statică f4() din clasa S1 a fost invocată în patru moduri: calificând numele metodei f1 cu numele clasei S1 (căreia îi aparține de fapt această metodă), cu numele subclasei CD1 (care moștenește această metodă) și cu numele instanțelor s și cd ale acestor clase. Cum era de așteptat, în toate cele patru cazuri s-a obținut același rezultat.

Puteti compila cele trei fișiere menționate și pune în executie aplicația TestCD1 pentru a verifica respectarea afirmațiilor de mai sus. Puteți, de asemenea, face modificări în metodele celor trei clase menționate, astfel încât să verificați:
  - ce se întâmplă dacă într-o metoda a clasei CD1 se încearcă folosirea câmpurilor sau metodelor private ale superclasei S1;
  - ce se întâmplă dacă în clasa TestDC1 se încearcă folosirea câmpurilor sau metodelor private sau protejate din clasele S1 și CD1.

Ascunderea câmpurilor

Câmpurile declarate într-o clasă pot fi ascunse prin câmpuri cu același nume declarate în subclasă, chiar dacă acestea au tipuri diferite. Aceasta înseamna că, în mod normal, în metodele clasei se folosesc câmpurile declarate în clasa respectivă, și nu cele cu același nume ale superclasei. În subclasă pot fi, totuși, folosite și câmpurile superclasei, dacă sunt calificate cu referința super.

Ascunderea metodelor statice

Metodele statice nu aparțin instanțelor, ci clasei. Din această cauză, dacă într-o subclasă se declara o metoda statică cu aceeași signatură ca o metodă a superclasei, atunci se spune că metoda din subclasă o ascunde pe cea din superclasă (nu o redefinește). Modul de lucru cu metodele ascunse este similar cu cel în care se lucrează cu câmpurile ascunse. Vom înțelege mai bine deosebirea dintre redefinire și ascundere când vom studia polimorfismul.
 
Exemplu
În fișierul CD2.java este declarată clasa CD2, care extinde clasa S1 din exemplul precedent. În clasa CD2 există declarația   public double a, b, m; prin care se creează câmpuri ale instanțelor acestei clase, dintre care a și b ascund câmpurile cu aceleași nume ale superclasei. De asemenea, în clasa CD2 este declarată metoda statică f4(int h), care ascunde metoda statică cu aceeași signatura din superclasa S1.
 
/* O clasa in care se ascund unele din campurile superclasei */

class CD2 extends S1 {
  public double a, b, m; // campurile a, b le ascund pe cele cu
                         // aceleasi nume din superclasa

  public CD2(int a1, double b1, int c1, double a2, 
             double b2, double m2) {
    super(a1,b1,c1); // initializarea campurilor din superclasa
    a=a2; b=b2; m=m2; // initializarea campurilor proprii
  }

  public void afisare() {
    System.out.println("Campuri din CD2: a="+a+" b="+b+" m="+m);
    System.out.println("Campuri din S1: a="+super.a+" b="+super.b);
  }

  public static int f4(int h) { // ascunde metoda statica f4() din
     // superclasa
    return h+alpha;
  }

  public String toString() {
    return "(CD2: "+super.toString()+" "+a+" "+b+" "+m+")";
  }
}

Remarcăm că:
  - în constructorul clasei CD2 este mai întâi invocat constructorul superclasei, prin care se inițializează câmpurile din S1, după care se inițializează și câmpurile din CD2;
  - întrucât metoda afisare() este declarată în clasa CD2, ea operează cu câmpurile a, b și m declarate în CD2. Totuși, în acesastă metodă se poate lucra și cu câmpurile a și b din S1, folosindu-se în mod corespunzător expresiile super.a și super.b;
  - în redefinirea metodei toString() s-a folosit expresia super.toString() pentru a invoca metoda cu aceeasi signatură din superclasa S1. Observăm că pentru a utiliza în aceasta metodă în mod direct câmpurile a și b din S1 (care sunt ascunse în clasa CD2), ar fi trebuit utilizate expresiile super.a și super.b;

Testarea clasei CD2 se face în aplicația din fișierul TestCd2.java, care are conținutul următor:
 
/* Testarea clasei CD2 */

class TestCD2 {
  public static void main(String args[]) {
    CD2 cd=new CD2(1, 2.45, 3, 10.23, 12.5, 17.08);
    System.out.println("Imediat dupa ce s-a creat instanta clasei CD2:");
    cd.afisare();
    System.out.println("cd.toString(): "+cd.toString());
    cd.a=-17.83; cd.b=28.16; cd.m=127.9;
    System.out.println("dupa ce s-au schimbat valorile "+
         "campurilor din CD2:");
    cd.afisare();
    System.out.println("cd.toString(): "+cd.toString());
    System.out.println("S1.f4(3)="+S1.f4(3)+" CD2.f4(3)="+CD2.f4(3)+" CD1.f4(3)=cd1.f4(3));
  }
}

Executând aplicația TestCD2 obținem următoarele rezultate:
 
Imediat dupa ce s-a creat instanta clasei CD2:
Campuri din CD2: a=10.23 b=12.5 m=17.08
Campuri din S1: a=1 b=2.45
cd.toString()=(CD2: (S1: 1 2.45 3) 10.23 12.5 17.08)
dupa ce s-au schimbat valorile campurilor din CD2:
Campuri din CD2: a=-17.83 b=28.16 m=127.9
Campuri din S1: a=1 b=2.45
cd.toString()=(CD2: (S1: 1 2.45 3) -17.83 28.16 127.9)
S1.f4(3)=18 CD2.f4(3)=15 CD1.f4(3)=18

Remarcăm că:
  - în metoda afișare() din clasa CD2 se operează corect atât cu câmpurile a și b din clasa CD2, cât și cu cele ascunse de acestea din clasa S1; 
  - în metoda toString() din CD2 se operează direct cu câmpurile a și b declarate în această clasă și cu metodele declarate în CD2 și se opereaza indirect (folosind referința super) cu câmpurile și metodele din superclasa S1 care sunt ascunse sau redefinite în CD2;
  - în expresiile S1.f4(3) și CD1.f4(3) este invocată metoda statică a clasei S1, în timp ce în expresia CD2.f4(3) este invocată metoda cu aceeași signatură a clasei CD2, care o ascunde pe cea din clasa S1.

Metode finale

Metodele finale sunt metode care nu mai pot fi redefinite în clasele derivate. Astfel de metode se declară cu modificatorul final. De exemplu, în clasa Object, metoda getClass este o metoda finală, fiind declarată sub forma public final Class getClass(). Aceasta inseamnă că în nici o altă clasă nu este posibil ca aceasta metodă să fie redefinită. Când declarăm propriile noastre clase, avem și noi posibilitatea să declarăm că unele din metodele lor sunt finale.

Declararea propriilor clase de excepții

Deși Java API oferă o varietate destul de mare de clase de excepții, uneori programatorul poate simți nevoia să iși creeze propriile sale clase de exceptii. Aceasta se poate realiza derivând clasele respective din clasa Exception, existentă în pachetul java.lang. Corpul declarației clasei de excepții conține numai declarații de constructori, în care se apelează constructorul corespunzător al superclasei.
 
Exemplu: 

În fișierul TestExceptie.java se dă ca exemplu declararea și utilizarea clasei ExceptieDomeniuFactorial, care este generată la calcularea funcției factorial(n),  dacă argumentul acesteia n este negativ. S-au creat doi constructori: unul fără argument, altul având un argument din clasa String. Dintre aceștia a fost folosit unul singur. Puteți modifica programul pentru a-l utiliza și pe al doilea. 

Clase finale

Dacă se dorește ca o clasă să nu poată avea subclase, la declararea acesteia se folosește modificatorul final. De exemplu, dacă se dorea ca clasa CD1, dată ca exemplu mai sus, să fie finala, ea trebuia declarata sub forma
    public final class CD1 extends S1
sau, dacă nu se dorea sa fie publica, sub forma
    final class CD1 extends S1



© Copyright 2000 - Severin BUMBARU, Universitatea "Dunărea de Jos" din Galați