Introduktion
Fra og med JSE 5.0 blev generika tilføjet til Java-sprogets arsenal.
Hvad er generiske lægemidler i java?
Generics er Javas specielle mekanisme til implementering af generisk programmering — en måde at beskrive data og algoritmer på, der lader dig arbejde med forskellige datatyper uden at ændre beskrivelsen af algoritmerne. Oracle-webstedet har en separat tutorial dedikeret til generiske lægemidler: "
Lektion ". For at forstå generika skal du først finde ud af, hvorfor de er nødvendige, og hvad de giver. Afsnittet "
Hvorfor bruge generiske lægemidler? " i selvstudiet siger, at et par formål er stærkere typekontrol på kompileringstidspunktet og eliminering af behovet for eksplicitte casts.
Lad os forberede os på nogle tests i vores elskede
Tutorialspoint online java compiler. Antag at du har følgende kode:
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);
}
}
Denne kode vil køre perfekt. Men hvad nu hvis chefen kommer til os og siger at "Hej verden!" er en overbrugt sætning, og at du kun skal returnere "Hej"? Vi fjerner koden, der sammenkæder
"verden!" Det virker harmløst nok, ikke? Men vi får faktisk en fejl PÅ KOMPILERINGSTID:
error: incompatible types: Object cannot be converted to String
Problemet er, at i vores liste gemmer objekter.
String er en efterkommer af
Object (da alle Java-klasser implicit arver
Object ), hvilket betyder, at vi har brug for en eksplicit cast, men vi har ikke tilføjet en. Under sammenkædningsoperationen vil den statiske
String.valueOf(obj) metode blive kaldt ved hjælp af objektet. Til sidst vil den kalde
Object- klassens
toString -metode. Med andre ord indeholder vores
liste et objekt . Det betyder, at uanset hvor vi har brug for en specifik type (ikke
Object ), bliver vi nødt til selv at udføre typekonverteringen:
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);
}
}
}
Men i dette tilfælde, fordi
List tager objekter, kan den ikke kun gemme
String s, men også
Integer s. Men det værste er, at compileren ikke ser noget galt her. Og nu får vi en fejl PÅ RUN TIME (kendt som en "runtime error"). Fejlen vil være:
java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String
Du må være enig i, at det ikke er særlig godt. Og alt dette fordi compileren ikke er en kunstig intelligens, der altid er i stand til at gætte programmørens hensigt korrekt. Java SE 5 introducerede generika for at lade os fortælle compileren om vores intentioner - om hvilke typer vi skal bruge. Vi fikser vores kode ved at fortælle compileren, hvad vi vil have:
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);
}
}
}
Som du kan se, har vi ikke længere brug for en cast til en
streng . Derudover har vi vinkelparenteser omkring typeargumentet. Nu vil compileren ikke lade os kompilere klassen, før vi fjerner linjen, der tilføjer 123 til listen, da dette er et
heltal . Og det vil det fortælle os. Mange mennesker kalder generika "syntaktisk sukker". Og de har ret, da efter generiske stoffer er kompileret, bliver de virkelig den samme type konverteringer. Lad os se på bytekoden for de kompilerede klasser: en, der bruger en eksplicit cast og en, der bruger generics:
Efter kompilering slettes alle generics. Dette kaldes "
type sletning". Typesletning og generiske artikler er designet til at være bagudkompatible med ældre versioner af JDK, samtidig med at compileren kan hjælpe med typedefinitioner i nye versioner af Java.
Rå typer
Når vi taler om generika, har vi altid to kategorier: parameteriserede typer og råtyper. Råtyper er typer, der udelader "typeafklaring" i vinkelparenteser:
Parametriserede typer omfatter på hånden en "afklaring":
Som du kan se, brugte vi en usædvanlig konstruktion, markeret med en pil i skærmbilledet. Dette er en speciel syntaks, der blev tilføjet til Java SE 7. Den kaldes "
diamanten ". Hvorfor? Vinkelbeslagene danner en diamant:
<> . Du skal også vide, at diamantsyntaksen er forbundet med begrebet "
typeinferens ". Efter alt, compileren, ser
<>til højre, ser på venstre side af tildelingsoperatoren, hvor den finder typen af den variabel, hvis værdi bliver tildelt. Baseret på hvad den finder i denne del, forstår den typen af værdien til højre. Faktisk, hvis en generisk type er angivet til venstre, men ikke til højre, kan compileren udlede typen:
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);
}
}
Men dette blander den nye stil med generiske og den gamle stil uden dem. Og dette er meget uønsket. Når vi kompilerer koden ovenfor, får vi følgende besked:
Note: HelloWorld.java uses unchecked or unsafe operations
Faktisk virker grunden til, at du overhovedet skal tilføje en diamant her, uforståelig. Men her er et eksempel:
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);
}
}
Du vil huske, at
ArrayList har en anden konstruktør, der tager en samling som et argument. Og det er her, der ligger noget uhyggeligt gemt. Uden diamantsyntaksen forstår compileren ikke, at den bliver bedraget. Med diamantsyntaksen gør den det. Så regel #1 er: brug altid diamantsyntaksen med parameteriserede typer. Ellers risikerer vi at gå glip af, hvor vi bruger råtyper. For at eliminere "bruger ukontrollerede eller usikre operationer"-advarsler, kan vi bruge
@SuppressWarnings("unchecked") annotationen på en metode eller klasse. Men tænk over, hvorfor du har valgt at bruge det. Husk regel nummer et. Måske skal du tilføje et typeargument.
Java Generiske metoder
Generisk giver dig mulighed for at oprette metoder, hvis parametertyper og returtype er parametriseret. Et separat afsnit er afsat til denne funktion i Oracle-tutorialen: "
Generiske metoder ". Det er vigtigt at huske syntaksen, der er undervist i denne øvelse:
- den inkluderer en liste over typeparametre inden for vinkelparenteser;
- listen over typeparametre går før metodens returtype.
Lad os se på et eksempel:
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));
}
}
}
Hvis du ser på
Util- klassen, vil du se, at den har to generiske metoder. Takket være muligheden for typeinferens kan vi enten angive typen direkte til compileren, eller vi kan selv specificere den. Begge muligheder er præsenteret i eksemplet. Syntaksen giver i øvrigt rigtig god mening, hvis man tænker over det. Når vi erklærer en generisk metode, specificerer vi typeparameteren FØR metoden, for hvis vi erklærer typeparameteren efter metoden, ville JVM ikke være i stand til at finde ud af, hvilken type der skal bruges. Derfor erklærer vi først, at vi vil bruge
T -type-parameteren, og derefter siger vi, at vi vil returnere denne type. Naturligvis vil
Util.<Integer>getValue(element, String.class) fejle med en fejl:
inkompatible typer: Klasse<String> kan ikke konverteres til Klasse<Heltal> . Når du bruger generiske metoder, skal du altid huske type sletning. Lad os se på et eksempel:
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);
}
}
}
Dette vil køre fint. Men kun så længe compileren forstår, at returtypen for den metode, der kaldes, er
Integer . Erstat konsoloutputsætningen med følgende linje:
System.out.println(Util.getValue(element) + 1);
Vi får en fejl:
bad operand types for binary operator '+', first type: Object, second type: int.
Der er med andre ord forekommet typesletning. Compileren ser, at ingen har specificeret typen, så typen er angivet som
Objekt , og metoden fejler med en fejl.
Generiske klasser
Ikke kun metoder kan parametreres. Klasser kan også. Sektionen
"Generiske typer" i Oracles tutorial er viet til dette. Lad os overveje et eksempel:
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);
}
}
}
Alt er enkelt her. Hvis vi bruger den generiske klasse, er typeparameteren angivet efter klassenavnet. Lad os nu oprette en forekomst af denne klasse i
hovedmetoden :
public static void main(String []args) {
SomeType<String> st = new SomeType<>();
List<String> list = Arrays.asList("test");
st.test(list);
}
Denne kode vil køre godt. Compileren ser, at der er en
liste over tal og en
samling af
strenge . Men hvad hvis vi fjerner typeparameteren og gør dette:
SomeType st = new SomeType();
List<String> list = Arrays.asList("test");
st.test(list);
Vi får en fejl:
java.lang.ClassCastException: java.lang.String cannot be cast to java.lang.Integer
Igen, dette er type sletning. Da klassen ikke længere bruger en type-parameter, beslutter compileren, at siden vi har bestået en
List , er metoden med
List<Integer> mest passende. Og vi fejler med en fejl. Derfor har vi Regel #2: Hvis du har en generisk klasse, skal du altid angive typeparametrene.
Begrænsninger
Vi kan begrænse de typer, der er angivet i generiske metoder og klasser. Antag for eksempel, at vi ønsker, at en container kun skal acceptere et
tal som typeargument. Denne funktion er beskrevet i afsnittet
Bounded Type Parameters i Oracles selvstudie. Lad os se på et eksempel:
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");
}
}
Som du kan se, har vi begrænset typeparameteren til
nummerklassen /grænsefladen eller dens efterkommere. Bemærk, at du ikke kun kan angive en klasse, men også grænseflader. For eksempel:
public static class NumberContainer<T extends Number & Comparable> {
Generika understøtter også
jokertegn. De er opdelt i tre typer:
Din brug af jokertegn skal overholde
Get-Put-princippet . Det kan udtrykkes som følger:
- Brug et udvidet jokertegn, når du kun får værdier ud af en struktur.
- Brug et super jokertegn, når du kun sætter værdier ind i en struktur.
- Og lad være med at bruge et jokertegn, når du både vil komme og sætte fra/til en struktur.
Dette princip kaldes også PECS-princippet (Producer Extends Consumer Super). Her er et lille eksempel fra kildekoden til Javas
Collections.copy- metode:
Og her er et lille eksempel på, hvad der IKKE VIL virke:
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);
}
Men hvis du erstatter
forlænger med
super , så er alt fint. Fordi vi udfylder
listen med en værdi, før dens indhold vises, er den en
forbruger . Derfor bruger vi super.
Arv
Generika har en anden interessant funktion: arv. Den måde, nedarvning fungerer på for generiske lægemidler, er beskrevet under "
Generiske, arv og undertyper " i Oracles selvstudie. Det vigtige er at huske og genkende følgende. Vi kan ikke gøre dette:
List<CharSequence> list1 = new ArrayList<String>();
Fordi arv fungerer anderledes med generiske lægemidler:
Og her er et andet godt eksempel, der vil mislykkes med en fejl:
List<String> list1 = new ArrayList<>();
List<Object> list2 = list1;
Igen, alt er enkelt her.
List<String> er ikke en efterkommer af
List<Object> , selvom
String er en efterkommer af
Object .
For at styrke det, du har lært, foreslår vi, at du ser en videolektion fra vores Java-kursus
Konklusion
Så vi har genopfrisket vores hukommelse vedrørende generiske lægemidler. Hvis du sjældent udnytter deres muligheder fuldt ud, bliver nogle af detaljerne uklare. Jeg håber, at denne korte anmeldelse har hjulpet med at øge din hukommelse. For endnu bedre resultater anbefaler jeg kraftigt, at du gør dig bekendt med følgende materiale:
GO TO FULL VERSION