MySQL Java JavaScript PHP Python HTML-CSS C-sharp

Generice (Generics)

Genericele sau tipurile și metodele generice permit utilizarea unor tipuri de date flexibile, evitând definirea strictă a tipurilor utilizate. Să analizăm o problemă în care genericele ne pot fi utile.

Să presupunem că definim o clasă pentru reprezentarea unui cont bancar. De exemplu, aceasta ar putea arăta astfel:

class Account {
   private int id;
   private int sum;

   Account(int id, int sum) {
       this.id = id;
       this.sum = sum;
   }

   public int getId() { return id; }
   public int getSum() { return sum; }
   public void setSum(int sum) { this.sum = sum; }
}

Clasa Account are două câmpuri: id - identificatorul unic al contului și sum - suma din cont.

În acest caz, identificatorul este setat ca o valoare numerică întreagă, de exemplu, 1, 2, 3, 4 și așa mai departe. Totuși, destul de frecvent se folosesc și valori de tip șir de caractere pentru identificator. Atât valorile numerice, cât și cele de tip șir au avantajele și dezavantajele lor.

În momentul scrierii clasei, este posibil să nu știm exact ce este mai bine să alegem pentru stocarea identificatorului - șiruri de caractere sau numere. De asemenea, este posibil ca această clasă să fie utilizată de alți dezvoltatori, care ar putea avea propria opinie cu privire la această problemă. De exemplu, aceștia ar putea dori să folosească propriul lor tip de clasă pentru identificator.

Și la prima vedere putem rezolva această problemă în felul următor: să atribuim id-ului tipul Object, care este un supertip universal și de bază pentru toate celelalte tipuri:

public class Program{
     
   public static void main(String[] args) {
         
       Account acc1 = new Account(2334, 5000); // id - număr
       int acc1Id = (int)acc1.getId();
       System.out.println(acc1Id);
       
       Account acc2 = new Account("sid5523", 5000);    // id - șir
       System.out.println(acc2.getId());
   }
}
class Account{
   
   private Object id;
   private int sum;
   
   Account(Object id, int sum){
       this.id = id;
       this.sum = sum;
   }
   
   public Object getId() { return id; }
   public int getSum() { return sum; }
   public void setSum(int sum) { this.sum = sum; }
}

În acest caz, totul funcționează perfect. Totuși, ne confruntăm cu o problemă legată de securitatea tipurilor. De exemplu, în următorul caz vom primi o eroare:

Account acc1 = new Account("2345", 5000);
int acc1Id = (int)acc1.getId(); // java.lang.ClassCastException
System.out.println(acc1Id);

Problema poate părea artificială, deoarece în acest caz vedem că în constructor se transmite un șir de caractere, așa că este puțin probabil să încercăm să-l convertim în tipul int.

Totuși, în timpul dezvoltării, este posibil să nu știm exact ce tip reprezintă valoarea din id și, atunci când încercăm să obținem un număr, ne vom confrunta cu o excepție java.lang.ClassCastException.

Să scriem câte o versiune separată a clasei Account pentru fiecare tip nu este o soluție bună, deoarece ar duce la redundanță.

Aceste probleme au fost soluționate cu ajutorul generalizărilor sau generics. Generalizările permit utilizarea unor tipuri nespecificate. Așadar, să definim clasa Account ca o clasă generică:

class Account<T>{
   
   private T id;
   private int sum;
   
   Account(T id, int sum){
       this.id = id;
       this.sum = sum;
   }
   
   public T getId() { return id; }
   public int getSum() { return sum; }
   public void setSum(int sum) { this.sum = sum; }
}

Cu ajutorul literei T în definiția clasei class Account<T>, indicăm faptul că tipul T va fi folosit de această clasă. Parametrul T din parantezele unghiulare se numește parametru universal, deoarece în locul lui se poate introduce orice tip. Momentan nu știm ce tip va fi: String, int sau altul. Litera T este aleasă în mod convențional, dar poate fi orice altă literă sau set de caractere.

După ce am declarat clasa, putem aplica parametrul universal T: astfel, în clasă se declară o variabilă de acest tip, căreia i se atribuie o valoare în constructor.

Metoda getId() returnează valoarea variabilei id, iar deoarece această variabilă reprezintă tipul T, metoda va returna un obiect de tipul T: public T getId().

Să utilizăm această clasă:

public class Program{
     
   public static void main(String[] args) {
         
       Account<String> acc1 = new Account<String>("2345", 5000);
       String acc1Id = acc1.getId();
       System.out.println(acc1Id);
       
       Account<Integer> acc2 = new Account<Integer>(2345, 5000);
       Integer acc2Id = acc2.getId();
       System.out.println(acc2Id);
   }
}
class Account<T>{
   
   private T id;
   private int sum;
   
   Account(T id, int sum){
       this.id = id;
       this.sum = sum;
   }
   
   public T getId() { return id; }
   public int getSum() { return sum; }
   public void setSum(int sum) { this.sum = sum; }
}

La definirea variabilei acestui tip de clasă și la crearea obiectului, după numele clasei, în parantezele unghiulare trebuie să specificăm ce tip va fi folosit în locul parametrului universal. Trebuie să luăm în considerare că acestea funcționează doar cu obiecte, nu și cu tipuri primitive.

De exemplu, putem scrie Account<Integer>, dar nu putem folosi tipul int sau double, de exemplu Account<int>. În locul tipurilor primitive trebuie să folosim clasele-înveliș: Integer în loc de int, Double în loc de double, etc.

De exemplu, primul obiect va folosi tipul String, așa că în loc de T va fi utilizat String:

Account<String> acc1 = new Account<String>("2345", 5000);

În acest caz, primul parametru transmis în constructor va fi un șir de caractere.

Al doilea obiect folosește tipul int (Integer):

Account<Integer> acc2 = new Account<Integer>(2345, 5000);

Interfețele generice

Interfețele, la fel ca și clasele, pot fi generice. Să creăm o interfață generică Accountable și să o folosim în program:

public class Program{
     
   public static void main(String[] args) {
         
       Accountable<String> acc1 = new Account("1235rwr", 5000);
       Account acc2 = new Account("2373", 4300);
       System.out.println(acc1.getId());
       System.out.println(acc2.getId());
   }
}
interface Accountable<T>{
   T getId();
   int getSum();
   void setSum(int sum);
}
class Account implements Accountable<String>{
   
   private String id;
   private int sum;
   
   Account(String id, int sum){
       this.id = id;
       this.sum = sum;
   }
   
   public String getId() { return id; }
   public int getSum() { return sum; }
   public void setSum(int sum) { this.sum = sum; }
}

La implementarea unei astfel de interfețe există două strategii. În acest caz, am implementat prima strategie, unde pentru parametrul universal al interfeței s-a stabilit un tip concret, ca de exemplu String. Astfel, clasa care implementează interfața este fixată pe acest tip.

A doua strategie constă în definirea unei clase generice, care folosește același parametru universal:

public class Program{
     
   public static void main(String[] args) {
         
       Account<String> acc1 = new Account<String>("1235rwr", 5000);
       Account<String> acc2 = new Account<String>("2373", 4300);
       System.out.println(acc1.getId());
       System.out.println(acc2.getId());
   }
}
interface Accountable<T>{
   T getId();
   int getSum();
   void setSum(int sum);
}
class Account<T> implements Accountable<T>{
   
   private T id;
   private int sum;
   
   Account(T id, int sum){
       this.id = id;
       this.sum = sum;
   }
   
   public T getId() { return id; }
   public int getSum() { return sum; }
   public void setSum(int sum) { this.sum = sum; }
}

Metode generice

Pe lângă tipurile generice, putem crea și metode generice, care vor folosi parametri universali. De exemplu:

public class Program{
     
   public static void main(String[] args) {
         
       Printer printer = new Printer();
       String[] people = {"Tom", "Alice", "Sam", "Kate", "Bob", "Helen"};
       Integer[] numbers = {23, 4, 5, 2, 13, 456, 4};
       printer.<String>print(people);
       printer.<Integer>print(numbers);
   }
}

class Printer{
   
   public <T> void print(T[] items){
       for(T item: items){
           System.out.println(item);
       }
   }
}

O particularitate a metodei generice este utilizarea parametrului universal în definiția metodei, după toți modificatorii și înainte de tipul de returnare.

public <T> void print(T[] items)

Apoi, în interiorul metodei, toate valorile de tipul T vor reprezenta acest parametru universal.

La apelarea metodei, înainte de numele ei, în parantezele unghiulare indicăm ce tip va fi transmis în locul parametrului universal:

printer.<String>print(people);
printer.<Integer>print(numbers);

Utilizarea mai multor parametri universali

Putem defini mai mulți parametri universali simultan:

public class Program{
     
   public static void main(String[] args) {
         
       Account<String, Double> acc1 = new Account<String, Double>("354", 5000.87);
       String id = acc1.getId();
       Double sum = acc1.getSum();
       System.out.printf("Id: %s  Sum: %f \n", id, sum);
   }
}
class Account<T, S>{
   
   private T id;
   private S sum;
   
   Account(T id, S sum){
       this.id = id;
       this.sum = sum;
   }
   
   public T getId() { return id; }
   public S getSum() { return sum; }
   public void setSum(S sum) { this.sum = sum; }
}

În acest caz, tipul String va fi transmis în locul parametrului T, iar tipul Double - în locul parametrului S.

Constructori generici

Constructorii, la fel ca metodele, pot fi și ei generici. În acest caz, înainte de constructor se indică în parantezele unghiulare parametrii universali:

public class Program{
     
   public static void main(String[] args) {
         
       Account acc1 = new Account("cid2373", 5000);
       Account acc2 = new Account(53757, 4000);
       System.out.println(acc1.getId());
       System.out.println(acc2.getId());
   }
}

class Account{
   
   private String id;
   private int sum;
   
   <T>Account(T id, int sum){
       this.id = id.toString();
       this.sum = sum;
   }
   
   public String getId() { return id; }
   public int getSum() { return sum; }
   public void setSum(int sum) { this.sum = sum; }
}

În acest caz, constructorul acceptă parametrul id, care reprezintă tipul T. În constructor, valoarea lui este convertită în șir de caractere și stocată într-o variabilă locală.

← Lecția anterioară Lecția următoare →