CodeGym /Blog Java /Aleatoriu /Java Generics: cum să utilizați parantezele unghiulare în...
John Squirrels
Nivel
San Francisco

Java Generics: cum să utilizați parantezele unghiulare în practică

Publicat în grup

Introducere

Începând cu JSE 5.0, genericele au fost adăugate la arsenalul limbajului Java.

Ce sunt genericele în java?

Genericurile sunt mecanismul special Java pentru implementarea programării generice — o modalitate de a descrie datele și algoritmii care vă permite să lucrați cu diferite tipuri de date fără a modifica descrierea algoritmilor. Site-ul Oracle are un tutorial separat dedicat genericelor: „ Lecția ”. Pentru a înțelege genericele, trebuie mai întâi să vă dați seama de ce sunt necesare și ce oferă. Secțiunea „ De ce să folosiți generice? ” a tutorialului spune că câteva scopuri sunt verificarea tipului mai puternică în timpul compilării și eliminarea necesității de modelări explicite. Generic în Java: cum să utilizați parantezele unghiulare în practică - 1Să ne pregătim pentru câteva teste în iubitul nostru compilator java online Tutorialspoint . Să presupunem că aveți următorul cod:

import java.util.*;
public class HelloWorld {
	public static void main(String []args) {
		List list = new ArrayList();
		list.add("Hello");
		String text = list.get(0) + ", world!";
		System.out.print(text);
	}
}
Acest cod va rula perfect. Dar dacă șeful vine la noi și ne spune „Bună, lume!” este o frază suprautilizată și că trebuie să returnezi doar „Bună ziua”? Vom elimina codul care concatenează „, lume!” Asta pare destul de inofensiv, nu? Dar primim de fapt o eroare LA TIMPUL COMPILĂRII:

error: incompatible types: Object cannot be converted to String
Problema este că în Lista noastră se stochează obiecte. String este un descendent al Object (deoarece toate clasele Java moștenesc implicit Object ), ceea ce înseamnă că avem nevoie de o distribuție explicită, dar nu am adăugat una. În timpul operației de concatenare, metoda statică String.valueOf(obj) va fi apelată folosind obiectul. În cele din urmă, va apela metoda toString a clasei Object . Cu alte cuvinte, Lista noastră conține un Object . Aceasta înseamnă că oriunde avem nevoie de un anumit tip (nu Object ), va trebui să facem singuri conversia tipului:

import java.util.*;
public class HelloWorld {
	public static void main(String []args) {
		List list = new ArrayList();
		list.add("Hello!");
		list.add(123);
		for (Object str : list) {
		    System.out.println("-" + (String)str);
		}
	}
}
Totuși, în acest caz, deoarece List preia obiecte, poate stoca nu numai șiruri de caractere , ci și numere întregi . Dar cel mai rău lucru este că compilatorul nu vede nimic în neregulă aici. Și acum vom primi o eroare AT RUN TIME (cunoscută ca „eroare de rulare”). Eroarea va fi:

java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String
Trebuie să fiți de acord că acest lucru nu este foarte bun. Și toate acestea pentru că compilatorul nu este o inteligență artificială capabilă să ghicească întotdeauna corect intenția programatorului. Java SE 5 a introdus generice pentru a ne permite să spunem compilatorului despre intențiile noastre - despre ce tipuri vom folosi. Reparăm codul spunându-i compilatorului ce vrem:

import java.util.*;
public class HelloWorld {
	public static void main(String []args) {
		List<String> list = new ArrayList<>();
		list.add("Hello!");
		list.add(123);
		for (Object str : list) {
		    System.out.println("-" + str);
		}
	}
}
După cum puteți vedea, nu mai avem nevoie de o distribuție a unui String . În plus, avem paranteze unghiulare în jurul argumentului tip. Acum compilatorul nu ne va lăsa să compilam clasa până când nu eliminăm linia care adaugă 123 la listă, deoarece acesta este un Integer . Și așa ne va spune. Mulți oameni numesc generice „zahăr sintactic”. Și au dreptate, deoarece după compilarea genericelor, acestea chiar devin conversii de același tip. Să ne uităm la bytecode-ul claselor compilate: una care folosește o distribuție explicită și una care utilizează generice: Generic în Java: cum să utilizați parantezele unghiulare în practică - 2După compilare, toate genericele sunt șterse. Aceasta se numește „ ștergere de tip ”.„. Ștergerea tipurilor și genericele sunt concepute pentru a fi compatibile cu versiunile mai vechi ale JDK, permițând simultan compilatorului să ajute cu definițiile de tip în noile versiuni de Java.

Tipuri brute

Vorbind de generice, avem întotdeauna două categorii: tipuri parametrizate și tipuri brute. Tipurile brute sunt tipuri care omit „clarificarea tipului” în paranteze unghiulare: Generic în Java: cum să utilizați parantezele unghiulare în practică - 3Tipurile parametrizate, pe de altă parte, includ o „clarificare”: Generic în Java: cum să utilizați parantezele unghiulare în practică - 4După cum puteți vedea, am folosit o construcție neobișnuită, marcată de o săgeată în captură de ecran. Aceasta este o sintaxă specială care a fost adăugată la Java SE 7. Se numește „ diamond ”. De ce? Parantezele unghiulare formează un romb: <> . De asemenea, ar trebui să știți că sintaxa diamant este asociată cu conceptul de „ inferență de tip ”. La urma urmei, compilatorul, văzând <>în dreapta, se uită în partea stângă a operatorului de atribuire, unde găsește tipul de variabilă a cărei valoare este atribuită. Pe baza a ceea ce găsește în această parte, înțelege tipul valorii din dreapta. De fapt, dacă un tip generic este dat în stânga, dar nu în dreapta, compilatorul poate deduce tipul:

import java.util.*;
public class HelloWorld {
	public static void main(String []args) {
		List<String> list = new ArrayList();
		list.add("Hello, World");
		String data = list.get(0);
		System.out.println(data);
	}
}
Dar acest lucru amestecă stilul nou cu generice și stilul vechi fără ele. Și acest lucru este extrem de nedorit. La compilarea codului de mai sus, primim următorul mesaj:

Note: HelloWorld.java uses unchecked or unsafe operations
De fapt, motivul pentru care trebuie chiar să adăugați un diamant aici pare de neînțeles. Dar iată un exemplu:

import java.util.*;
public class HelloWorld {
	public static void main(String []args) {
		List<String> list = Arrays.asList("Hello", "World");
		List<Integer> data = new ArrayList(list);
		Integer intNumber = data.get(0);
		System.out.println(data);
	}
}
Vă veți aminti că ArrayList are un al doilea constructor care ia o colecție ca argument. Și aici stă ascuns ceva sinistru. Fără sintaxa diamant, compilatorul nu înțelege că este înșelat. Cu sintaxa de diamant, se întâmplă. Deci, regula #1 este: folosiți întotdeauna sintaxa romboială cu tipuri parametrizate. În caz contrar, riscăm să pierdem locul în care folosim tipuri brute. Pentru a elimina avertismentele „folosește operațiuni necontrolate sau nesigure”, putem folosi adnotarea @SuppressWarnings(„necontrolate”) pentru o metodă sau clasă. Dar gândește-te de ce te-ai hotărât să-l folosești. Amintiți-vă regula numărul unu. Poate că trebuie să adăugați un argument de tip.

Metode Java generice

Genericurile vă permit să creați metode ale căror tipuri de parametri și tip de returnare sunt parametrizate. O secțiune separată este dedicată acestei capacități în tutorialul Oracle: „ Metode generice ”. Este important să vă amintiți sintaxa predată în acest tutorial:
  • include o listă de parametri de tip în paranteze unghiulare;
  • lista parametrilor de tip trece înaintea tipului de returnare al metodei.
Să ne uităm la un exemplu:

import java.util.*;
public class HelloWorld {
	
    public static class Util {
        public static <T> T getValue(Object obj, Class<T> clazz) {
            return (T) obj;
        }
        public static <T> T getValue(Object obj) {
            return (T) obj;
        }
    }

    public static void main(String []args) {
		List list = Arrays.asList("Author", "Book");
		for (Object element : list) {
		    String data = Util.getValue(element, String.class);
		    System.out.println(data);
		    System.out.println(Util.<String>getValue(element));
		}
    }
}
Dacă te uiți la clasa Util , vei vedea că are două metode generice. Datorită posibilității de inferență de tip, putem fie indica tipul direct compilatorului, fie îl putem specifica noi înșine. Ambele opțiuni sunt prezentate în exemplu. Apropo, sintaxa are mult sens dacă te gândești la asta. Când declarăm o metodă generică, specificăm parametrul de tip ÎNAINTE de metodă, deoarece dacă declarăm parametrul de tip după metodă, JVM-ul nu ar putea să-și dea seama ce tip să folosească. În consecință, declarăm mai întâi că vom folosi parametrul de tip T și apoi spunem că vom returna acest tip. Desigur, Util.<Integer>getValue(element, String.class) va eșua cu o eroare:tipuri incompatibile: Class<String> nu poate fi convertită în Class<Integer> . Când utilizați metode generice, ar trebui să vă amintiți întotdeauna ștergerea tipului. Să ne uităm la un exemplu:

import java.util.*;
public class HelloWorld {
	
    public static class Util {
        public static <T> T getValue(Object obj) {
            return (T) obj;
        }
    }

    public static void main(String []args) {
		List list = Arrays.asList(2, 3);
		for (Object element : list) {
		    System.out.println(Util.<Integer>getValue(element) + 1);
		}
    }
}
Acest lucru va funcționa bine. Dar numai atâta timp cât compilatorul înțelege că tipul de returnare al metodei apelate este Integer . Înlocuiți instrucțiunea de ieșire a consolei cu următoarea linie:

System.out.println(Util.getValue(element) + 1);
Primim o eroare:

bad operand types for binary operator '+', first type: Object, second type: int.
Cu alte cuvinte, a avut loc ștergerea tipului. Compilatorul vede că nimeni nu a specificat tipul, așa că tipul este indicat ca Object și metoda eșuează cu o eroare.

Clasele generice

Nu numai metodele pot fi parametrizate. Clasele pot, de asemenea. Secțiunea „Tipuri generice” a tutorialului Oracle este dedicată acestui lucru. Să luăm în considerare un exemplu:

public static class SomeType<T> {
	public <E> void test(Collection<E> collection) {
		for (E element : collection) {
			System.out.println(element);
		}
	}
	public void test(List<Integer> collection) {
		for (Integer element : collection) {
			System.out.println(element);
		}
	}
}
Totul este simplu aici. Dacă folosim clasa generică, parametrul tip este indicat după numele clasei. Acum să creăm o instanță a acestei clase în metoda principală :

public static void main(String []args) {
	SomeType<String> st = new SomeType<>();
	List<String> list = Arrays.asList("test");
	st.test(list);
}
Acest cod va rula bine. Compilatorul vede că există o listă de numere și o colecție de șiruri . Dar ce se întâmplă dacă eliminăm parametrul tip și facem asta:

SomeType st = new SomeType();
List<String> list = Arrays.asList("test");
st.test(list);
Primim o eroare:

java.lang.ClassCastException: java.lang.String cannot be cast to java.lang.Integer
Din nou, aceasta este ștergerea tipului. Deoarece clasa nu mai folosește un parametru de tip, compilatorul decide că, din moment ce am trecut un List , metoda cu List<Integer> este cea mai potrivită. Și eșuăm cu o eroare. Prin urmare, avem Regula #2: Dacă aveți o clasă generică, specificați întotdeauna parametrii de tip.

Restricții

Putem restricționa tipurile specificate în metode generice și clase. De exemplu, să presupunem că vrem ca un container să accepte doar un număr ca argument de tip. Această caracteristică este descrisă în secțiunea Parametrii de tip delimitat din tutorialul Oracle. Să ne uităm la un exemplu:

import java.util.*;
public class HelloWorld {
	
    public static class NumberContainer<T extends Number> {
        private T number;
    
        public NumberContainer(T number) { this.number = number; }
    
        public void print() {
            System.out.println(number);
        }
    }

    public static void main(String []args) {
		NumberContainer number1 = new NumberContainer(2L);
		NumberContainer number2 = new NumberContainer(1);
		NumberContainer number3 = new NumberContainer("f");
    }
}
După cum puteți vedea, am restricționat parametrul tip la clasa/interfața Number sau la descendenții acesteia. Rețineți că puteți specifica nu numai o clasă, ci și interfețe. De exemplu:

public static class NumberContainer<T extends Number & Comparable> {
Genericurile acceptă, de asemenea, metacaracterele. Acestea sunt împărțite în trei tipuri: Utilizarea de către dvs. a caracterelor joker ar trebui să respecte principiul Get-Put . Poate fi exprimat astfel:
  • Utilizați un wildcard extins atunci când obțineți doar valori dintr-o structură.
  • Utilizați un super wildcard atunci când puneți doar valori într-o structură.
  • Și nu folosiți un wildcard atunci când amândoi doriți să obțineți și să puneți de la/la o structură.
Acest principiu mai este numit și principiul Producer Extends Consumer Super (PECS). Iată un mic exemplu din codul sursă pentru metoda Java Collections.copy : Generic în Java: cum să utilizați parantezele unghiulare în practică - 5Și iată un mic exemplu despre ceea ce NU VA funcționa:

public static class TestClass {
	public static void print(List<? extends String> list) {
		list.add("Hello, World!");
		System.out.println(list.get(0));
	}
}

public static void main(String []args) {
	List<String> list = new ArrayList<>();
	TestClass.print(list);
}
Dar dacă înlocuiți extinde cu super , atunci totul este în regulă. Deoarece populăm lista cu o valoare înainte de a-i afișa conținutul, este un consumator . În consecință, folosim super.

Moştenire

Genericurile au o altă caracteristică interesantă: moștenirea. Modul în care funcționează moștenirea pentru generice este descris în „ Generice, moștenire și subtipuri ” din tutorialul Oracle. Important este să vă amintiți și să recunoașteți următoarele. Nu putem face asta:

List<CharSequence> list1 = new ArrayList<String>();
Deoarece moștenirea funcționează diferit cu genericele: Generic în Java: cum să utilizați parantezele unghiulare în practică - 6Și iată un alt exemplu bun care va eșua cu o eroare:

List<String> list1 = new ArrayList<>();
List<Object> list2 = list1;
Din nou, totul este simplu aici. List<String> nu este un descendent al List<Object> , chiar dacă String este un descendent al Object . Pentru a consolida ceea ce ați învățat, vă sugerăm să urmăriți o lecție video de la Cursul nostru Java

Concluzie

Așa că ne-am împrospătat memoria cu privire la generice. Dacă profitați rar de capacitățile lor, unele detalii devin neclare. Sper că această scurtă recenzie v-a ajutat să vă strângeți memoria. Pentru rezultate și mai bune, vă recomand cu tărie să vă familiarizați cu următorul material:
Comentarii
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION