Interfețe

Conceptul de interfață

Interfețe

Conform principiului încapsulării, fiecare clasă are "interfața" sa intrinsecă, prin care poate fi accesata din exterior. Aceasta "interfață" conține toate datele și metodele publice ale clasei respective.

Pentru a compensa lipsa moștenirii multiple, în limbajul Java s-a admis că o clasă poate avea mai multe interfețe și că mai multe clase pot implementa aceeași interfață.  S-a introdus astfel o nouă categorie de componente, numite interfețe, care se declară în mod asemănător cu niște clase abstracte, dar nu sunt înglobate, așa cum sunt clasele abstracte, în ierarhia unică de clase .
 
Interfața este o specificație care descrie metodele publice și variabilele finale publice pe care trebuie sa le aibă o clasă care implementeaza interfața respectivă. Dacă o clasă implementează mai multe interfețe, ea conține toate metodele publice și variabilele finale publice ale acestora.

Interfața nu este o clasă, dar poate fi utilizată de programator ca și când ar fi o clasă abstractă. Se pot declara variabile referință la o interfață în mod asemănător cu declararea variabilelor referința la obiecte aparținând unei clase, adică sub forma:

    interfața nume_variabila1[=initializare1], ..., nume_variabilaN[=initializareN];

în care  interfața este numele unei interfețe, iar celelalte elemente ale declarației sunt aceleași ca în cazul declarării de referințe la obiecte. Interfețele pot fi și ele organizate ierarhic, aplicându-se principiul moștenirii. În schimb, pentru interfețe, ierarhia nu mai este unică, așa cum este în cazul claselor, și se admite moștenirea multiplă.

Remarcam că, la fel ca și clasele abstracte, interfețele nu pot fi instanțiate. În schimb, unei variabile referință la o interfață i se pot da ca valori referințe către obiecte din orice clasă care implementează acea interfață sau este descendentă a unei astfel de clase.

Pentru a le distinge mai ușor, în documentația Java API numele interfețelor sunt scrise cu litere cursive (stil italic), în timp ce numele claselor sunt scrise cu litere normale (drepte). De exemplu, în pachetul java.lang există interfața Cloneable care este implementată de toate clasele din SDK care implementează metoda clone(), deci ale căror obiecte pot fi clonate.

Sa considerăm acum exemplul din Figura 1, în care clasele B și C implementeaza interfețele I1 și I2, iar clasele C si D implementează interfețele I2 și I3.

- Figura 1 -

În acest exemplu, interfetele I1 și I2 sunt derivate din interfata I0 deci moștenesc variabilele finale și metodele acesteia, putând avea fiecare, de asemenea, date finale, câmpuri și metode proprii. Clasa B implementează interfața I1, deci conține variabilele finale și metodele interfeței I1 și  pe cele mostenite de aceasta de la interfața I0. Clasa C implementează interfețele I1 și I2, deci conține variabilele finale și metodele interfețelor I1 și I2, inclusiv pe cele moștenite de acestea de la interfața I0. Totodată, clasele A, B și C moștenesc datele și metodele clasei A. Faptul că variabilele finale și metodele interfeței I0 se propagă la clasa C atât pe traseul I0-I1-C, cât și pe traseul I0-I2-C nu mai produce dificultăți, ca în cazul moștenirii multiple a claselor, deoarece implementarea efectivă a acestor variabile și metode se face în clasa C și nu in interfețe.

Pentru exemplul din Figura 1, să considerăm acum următoarele declarații și instrucțiuni de atribuire valabile, în care A(), B(),C() si D() sunt constructori ai claselor respective:

    A v1=new A(), v2;
    B v3=new B(), v4;
    C v5=new C(), v6;
    I0 w1=new B(),w2;
    I1 w3=new C(), w4;
    I2 w5, w6;
    v2=v5;
    w2=new D();
    v4=v3;
    w4=v3;
    v6=new C();
    w5=v6;
    w2=w4;

Constatăm că variabilele referință la interfețe (w1, w2, w3 etc.) sunt utilizate la fel ca și variabilele referinta la obiecte ale claselor. Dacă, insă, știind că variabila w1 are ca valoare o referință la un obiect din clasa B, vom dori sa folosim instrucțiunea
    v4=w1;
vom constata că apare o eroare de compilare, deoarece v4 este referință la un obiect al unei clase, în timp ce w1 este o referință la interfață. În această situație este necesară o conversie explicită (cast), astfel că vom folosi instrucțiunea
    v4=(B)w1;

Declararea interfețelor

O declarație de interfață introduce un nou tip referință, ai cărui membri sunt câmpuri statice finale și metode abstracte. În consecință, interfața se aseamănă cu o clasa abstractă pură, care nu conține decât metode abstracte și câmpuri statice finale și nu se încadrează în ierarhia unică de clase descendente din Object (amintim ca o clasa abstractă, în afară de una sau mai multe metode abstracte, poate conține și câmpuri de date și metode concrete și este descendentă a clasei Object).

Deși nu se încadrează în ierarhia claselor, interfețele se pot constitui in diverse ierarhii de interfețe, aplicându-li-se principiul mostenirii. O clasă poate implementa mai multe interfețe.

Cel mai important avantaj al folosirii interfețelor este că mai multe clase, de pe diferite ramuri ale arborelui ierarhic al claselor, pot fi "văzute" printr-o singură interfață. Se pot declara variabile referință la interfață la fel cum se pot declara variabile referință la clasă. Interfața este abstractă și deci nu poate fi instanțiată. În schimb, unei variabile referință la interfață i se pot atribui ca valori referințe la obiecte din orice clasă care implementează interfața respectivă.

Declarația de interfață are forma generală următoare:

 [public] interface NumeInterfata [extends lista_superinterfete] {
           declaratii_de_membri_ai_interfetei
          }

În afară de modificatorul  public, se poate folosi și modificatorul  abstract, numai că acesta este implicit, deci folosirea lui este de prisos.

Spre deosebire de clase, la care moștenirea multiplă este interzisă (o clasă poate avea numai o singura superclasă), în cazul interfețelor moștenirea multiplă este permisă. În consecință, clauza  extends poate conține o listă de nume de interfețe separate prin virgule.

Corpul interfeței conține una sau mai multe declarații de membru al interfeței.

Declarațiile de membri ai interfeței pot fi:

    a/ Declarația de câmpuri statice finale, numită și declarație de constante, sub forma:

  tip NUME_CAMP1=valoare1,NUME_CAMP2=valoare2,... , NUME_CAMP_N=valoareN;

în care tip este un tip primitiv. Modificatorii  public, static și final pentru câmpurile interfeței sunt impliciți, deci folosirea lor este de prisos.   Aceasta înseamnă că toate câmpurile unei interfețe sunt publice, statice și finale, deci ele trebuie să fie inițializate la declarare și nu mai pot fi modificate ulterior. Se obisnuiește ca numele de câmp ale interfețelor să fie scrise numai cu majuscule.
 
Exemplu:
    interface CuloareDeBaza {
        int ROSU=1, VERDE=2, ALBASTRU=4;
    }
    interface Culoare extends CuloareDeBaza {
        int GALBEN=3, ORANGE=5, INDIGO=6, VIOLET=7;
    }

        b/ Declarația de metodă abstractă, care constă din antetul metodei urmat de simbolul ';' (punct și virgulă). Ea este deci la fel ca o declarație de metodă abstractă din corpul unei clase, cu următoarele observații:
    - modificatorii  public și abstract sunt impliciti, deci folosirea lor este de prisos (dar este permisă);
    - modificatorul  final nu poate fi folosit, deoarece se declară o metodă abstractă;
    - corpul metodei este înlocuit prin simbolul punct și virgulă, ca la orice metodă abstractă.
 
Exemplu:
    interface Interf1 {
        double metoda1();
        int metoda2(int a, int b, double c);
    }

Ambele metode din exemplul de mai sus sunt implicit publice și abstracte. Orice clasă, care implementează aceasta interfață, trebuie sa conțină declarații de metode cu aceeași semnătură cu cele din interfață. La definirea acestor metode îm cadrul claselor, folosirea identificatorului public este obligatorie.


 

Exemplu de declarare, implementare și utilizare a unor interfețe

Considerăm că dorim să realizăm diferite clase de integratoare numerice, care calculează prin diferite metode valoarea integralei pe intervalul [inf, sup] a funcției reale f(x). Dorim ca, în oricare din aceste integratoare, să existe metoda de calculare a integralei:
    double integrala(double inf, double sup, FunctieReala func)

în care inf și sup sunt, respectiv, marginea inferioara și cea superioara a intervalului de integrare, iar func este o referință la un obiect care contine funcția de integrat f(x). Având în vedere că nu avem, deocamdată, în vedere o metoda de integrare numerică particulară, vom declara interfața
 
    interface Integrator {
       double integrala(double inf, double sup, FunctieReala func);
    }

în care metoda de integrare apare ca o metoda abstractă.

Cu aceasta ocazie remarcam că, spre deosebire de limbajele de programare tradiționale, cum sunt Pascal sau C/C++, în limbajul Java nu există posibilitatea ca o funcție (metodă) să aibă ca argument o referința la altă funcție (metodă). În schimb, putem să-i oferim ca argument o referința la un obiect care conține funcția respectivă. Astfel, în cazul nostru, func este referință la un obiect care conține funcția de integrat f(x). Având în vedere că dorim ca integratoarele noastre să fie valabile pentru orice funcție reală f(x), vom declara o interfață:
 
    interface FunctieReala {
        double f(double x);
    }

care conține metoda abstractă f(x) pe care trebuie să o conțină orice obiect pe care îl oferim ca argument metodei de integrare din interfața Integrator.

Să realizăm acum un integrator concret, pentru o anumita metodă particulară de integrare. Pentru simplitate, vom implementa integrarea prin metoda trapezelor. Știind că integrala poate fi interpretată geometric ca aria suprafeței cuprinse între graficul funcției respective și axa ox, putem împărți intervalul de integrare [inf, sup] în n subintervale de lungime egala  h=(sup-inf)/n, ca în Figura 1.

- Figura 1 -

Înlocuind pe fiecare din aceste subintervale curba f(x) prin coarda ei, obtinem n trapeze. Considerand că ariile cuprinse între curbă și coarda curbei în fiecare din subintervale sunt mici, aproximăm aria de sub curbă prin suma ariilor celor n trapeze. Se obține astfel pentru calcularea integralei I cunoscuta formulă a integrării numerice prin metoda trapezelor dată în Figura 2.

- Figura 2 -

Pentru calcularea integralei este necesar să se impună și numărul de pași n, pe care îl vom nota în continuare cu  nrPasi. Putem astfel declara clasa  IntegrareTrapeze, care implementeaza interfața  Integrator,  în modul următor:
 
    class IntegrareTrapeze implements Integrator {
       int nrPasi; // Numarul de pasi de integrare
       /* Constructorul initializeaza numarul de pasi de integrare */
       public IntegrareTrapeze(int nrPasi) throws Exception {
          if(nrPasi<=0) throw new Exception("IntegrareTrapeze: nrPasi<=0");
          this.nrPasi=nrPasi;
       }
       /* Metoda de modificare a numarului de pasi de integrare */
       public void puneNrPasi(int nrPasi) throws Exception {
          if(nrPasi<=0) throw new Exception("IntegrareTrapeze: nrPasi<=0");
          this.nrPasi=nrPasi;
       }
       /* Implementarea metodei abstracte din interfata Integrator */
       public double integrala(double inf, double sup, FunctieReala func) {
          double h, s;
          h=(sup-inf)/nrPasi; // h este pasul de integrare
          /* Calcularea sumei ordonatelor s */
          s=0;
          for(int i=1; i<nrPasi; i++) s+=func.f(inf+i*h);
          /* Calcularea valorii integralei si intoarcerea rezultatului */
          return ((func.f(inf)+func.f(sup))/2+s)*h;
       }
    }

La calcularea integralei s-a aplicat formula din Figura 2, luând în considerație că   xi=inf+i*h. Remarcăm că funcția f(x) nu a fost încă precizată. S-a indicat numai sub forma func.f(inf+i*h) că se folosește funcția (metoda) f(x) a obiectului func, care implementează interfața FunctieReala. Integratorul nostru are deci o utilizare foarte largă, putând integra funcția f(x) din orice obiect care implementează această interfață. Remarcăm, de asemenea, că putem aplica și alte metode de integrare numerică, dacă creem pentru fiecare din aceste metode o clasa care implementează interfața Integrator.

Să considerăm acum că dorim să integrăm funcția
            f(x)=a*sin(b*x+c)+d*cos(b*x)
în careparametrii  a, b, c și d sunt  numere reale. Pentru aceasta vom creea o clasă care implementează interfața FunctieReala și, totodată, conține drept câmpuri parametrii funcției:
 
    class Functie1 implements FunctieReala {
       private double a, b, c, d; // parametrii functiei
       /* Constructorul clasei introduce valorile parametrilor functiei */
       public Functie1(double a, double b, double c, double d) {
          this.a=a; this.b=b; this.c=c; this.d=d;
       }
       /* implementarea metodei abstracte din interfata */
       public double f(double x) {
          return a*Math.sin(b*x+c)+d*Math.cos(b*x);
       }
    }

Putem  acum să scriem o aplicație în care se calculează integrala acestei funcții. În fișierul Integrare.javasunt date atât declarațiile de interfețe și clase prezentate mai sus, cât și cea a clasei-aplicație în care se folosesc acestea.



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