Invoering
Vanaf JSE 5.0 werden generieke geneesmiddelen toegevoegd aan het arsenaal van de Java-taal.
Wat zijn generieke geneesmiddelen in Java?
Generics zijn het speciale mechanisme van Java voor het implementeren van generieke programmering — een manier om gegevens en algoritmen te beschrijven waarmee u met verschillende datatypes kunt werken zonder de beschrijving van de algoritmen te wijzigen. De Oracle-website heeft een aparte tutorial gewijd aan generieke geneesmiddelen: "
Les ". Om generieke geneesmiddelen te begrijpen, moet u eerst uitzoeken waarom ze nodig zijn en wat ze geven. De sectie "
Waarom Generics gebruiken? " van de tutorial zegt dat een aantal doelen een sterkere typecontrole tijdens het compileren zijn en het elimineren van de behoefte aan expliciete casts.
Laten we ons voorbereiden op enkele tests in onze geliefde
Tutorialspoint online java-compiler. Stel dat je de volgende code hebt:
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);
}
}
Deze code zal perfect werken. Maar wat als de baas naar ons toe komt en zegt: "Hallo wereld!" is een veelgebruikte uitdrukking en dat u alleen "Hallo" moet retourneren? We zullen de code verwijderen die
", wereld!" Dit lijkt onschuldig genoeg, toch? Maar we krijgen eigenlijk een foutmelding BIJ COMPILE TIJD:
error: incompatible types: Object cannot be converted to String
Het probleem is dat in onze Lijst Objecten worden opgeslagen.
String is een afstammeling van
Object (aangezien alle Java-klassen
Object impliciet erven ), wat betekent dat we een expliciete cast nodig hebben, maar die hebben we niet toegevoegd. Tijdens de aaneenschakelingsbewerking wordt de statische methode
String.valueOf(obj) aangeroepen met behulp van het object. Uiteindelijk zal het de methode
toString van de klasse
Object aanroepen. Met andere woorden, onze
Lijst bevat een
Object . Dit betekent dat waar we een specifiek type nodig hebben (niet
Object ), we de typeconversie zelf moeten doen:
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);
}
}
}
In dit geval echter, omdat
List objecten neemt, kan het niet alleen
String s opslaan, maar ook
Integer s. Maar het ergste is dat de compiler hier niets verkeerds ziet. En nu krijgen we een foutmelding AT RUN TIME (bekend als een "runtime error"). De fout zal zijn:
java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String
Je moet het ermee eens zijn dat dit niet erg goed is. En dit alles omdat de compiler geen kunstmatige intelligentie is die in staat is om de bedoeling van de programmeur altijd correct te raden. Java SE 5 introduceerde generieke codes om ons de compiler te laten vertellen over onze bedoelingen - over welke typen we gaan gebruiken. We repareren onze code door de compiler te vertellen wat we willen:
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);
}
}
}
Zoals je kunt zien, hebben we niet langer een cast naar een
String nodig . Daarnaast hebben we punthaken rond het argument type. Nu laat de compiler ons de klasse niet compileren totdat we de regel verwijderen die 123 aan de lijst toevoegt, aangezien dit een geheel
getal is . En dat zal het ons vertellen. Veel mensen noemen generieke geneesmiddelen "syntactische suiker". En ze hebben gelijk, want nadat generieke geneesmiddelen zijn samengesteld, worden het echt conversies van hetzelfde type. Laten we eens kijken naar de bytecode van de gecompileerde klassen: een die een expliciete cast gebruikt en een die generieke klassen gebruikt:
na compilatie worden alle generieke klassen gewist. Dit wordt "
typeverwijdering" genoemd". Typeverwijdering en generieke gegevens zijn ontworpen om achterwaarts compatibel te zijn met oudere versies van de JDK, terwijl de compiler tegelijkertijd kan helpen met typedefinities in nieuwe versies van Java.
Rauwe soorten
Over generieke geneesmiddelen gesproken, we hebben altijd twee categorieën: geparametriseerde typen en onbewerkte typen. Onbewerkte typen zijn typen die de "typeverduidelijking" tussen punthaken weglaten:
geparametriseerde typen, aan de kant, bevatten een "verduidelijking":
zoals u kunt zien, gebruikten we een ongebruikelijke constructie, gemarkeerd door een pijl in de schermafbeelding. Dit is een speciale syntaxis die is toegevoegd aan Java SE 7. Het wordt de "
diamant " genoemd. Waarom? De punthaken vormen een ruit:
<> . U moet ook weten dat de diamantsyntaxis wordt geassocieerd met het concept van "
type-inferentie ". Immers, de compiler, gezien
<>aan de rechterkant, kijkt naar de linkerkant van de toewijzingsoperator, waar het type variabele wordt gevonden waarvan de waarde wordt toegewezen. Op basis van wat het in dit deel vindt, begrijpt het het type waarde aan de rechterkant. Als een generiek type aan de linkerkant wordt gegeven, maar niet aan de rechterkant, kan de compiler het type afleiden:
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);
}
}
Maar dit combineert de nieuwe stijl met generieke geneesmiddelen en de oude stijl zonder deze. En dit is hoogst ongewenst. Bij het compileren van bovenstaande code krijgen we de volgende melding:
Note: HelloWorld.java uses unchecked or unsafe operations
De reden waarom je hier zelfs een diamant moet toevoegen, lijkt zelfs onbegrijpelijk. Maar hier is een voorbeeld:
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);
}
}
U zult zich herinneren dat
ArrayList een tweede constructor heeft die een verzameling als argument neemt. En hier ligt iets sinister verborgen. Zonder de diamant-syntaxis begrijpt de compiler niet dat hij wordt misleid. Met de diamant-syntaxis wel. Regel #1 is dus: gebruik altijd de diamant-syntaxis met geparametriseerde typen. Anders lopen we het risico te missen waar we onbewerkte typen gebruiken.
Om waarschuwingen "gebruikt ongecontroleerde of onveilige bewerkingen" te elimineren, kunnen we de annotatie @SuppressWarnings("niet aangevinkt") gebruiken voor een methode of klasse. Maar bedenk waarom je hebt besloten het te gebruiken. Onthoud regel nummer één. Misschien moet u een type-argument toevoegen.
Java generieke methoden
Met generieke methoden kunt u methoden maken waarvan de parametertypen en het retourtype zijn geparametriseerd. Een aparte sectie is aan deze mogelijkheid gewijd in de Oracle-tutorial: "
Generic Methods ". Het is belangrijk om de syntaxis te onthouden die in deze zelfstudie wordt geleerd:
- het bevat een lijst met typeparameters tussen punthaken;
- de lijst met typeparameters gaat vóór het retourtype van de methode.
Laten we naar een voorbeeld kijken:
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));
}
}
}
Als u naar de klasse
Util kijkt , ziet u dat deze twee generieke methoden heeft. Dankzij de mogelijkheid van type-inferentie kunnen we het type rechtstreeks aan de compiler aangeven, of we kunnen het zelf specificeren. Beide opties worden in het voorbeeld gepresenteerd. Trouwens, de syntaxis is logisch als je erover nadenkt. Bij het declareren van een generieke methode specificeren we de typeparameter VOOR de methode, want als we de typeparameter na de methode declareren, zou de JVM niet kunnen achterhalen welk type moet worden gebruikt.
Dienovereenkomstig verklaren we eerst dat we de parameter T- type zullen gebruiken en vervolgens zeggen we dat we dit type gaan retourneren. Uiteraard mislukt
Util.<Integer>getValue(element, String.class) met een fout:incompatibele typen: Class<String> kan niet worden geconverteerd naar Class<Integer> . Wanneer u generieke methoden gebruikt, moet u altijd het wissen van typen onthouden. Laten we naar een voorbeeld kijken:
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);
}
}
}
Dit gaat prima lopen. Maar alleen zolang de compiler begrijpt dat het retourtype van de aangeroepen methode
Integer is . Vervang de console-uitvoerinstructie door de volgende regel:
System.out.println(Util.getValue(element) + 1);
We krijgen een foutmelding:
bad operand types for binary operator '+', first type: Object, second type: int.
Met andere woorden, er is typeverwijdering opgetreden. De compiler ziet dat niemand het type heeft gespecificeerd, dus het type wordt aangegeven als
Object en de methode mislukt met een fout.
Generieke lessen
Niet alleen methoden kunnen worden geparametriseerd. Klassen kunnen ook. Het gedeelte
"Generieke typen" van Oracle's zelfstudie is hieraan gewijd. Laten we een voorbeeld bekijken:
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);
}
}
}
Alles is hier eenvoudig. Als we de generieke klasse gebruiken, wordt de parameter type aangegeven na de klassenaam. Laten we nu een instantie van deze klasse maken in de
hoofdmethode :
public static void main(String []args) {
SomeType<String> st = new SomeType<>();
List<String> list = Arrays.asList("test");
st.test(list);
}
Deze code zal goed werken. De compiler ziet dat er een
lijst met getallen en een
verzameling strings is . Maar wat als we de parameter type elimineren en dit doen:
SomeType st = new SomeType();
List<String> list = Arrays.asList("test");
st.test(list);
We krijgen een foutmelding:
java.lang.ClassCastException: java.lang.String cannot be cast to java.lang.Integer
Nogmaals, dit is typeverwijdering. Omdat de klasse geen parameter type meer gebruikt, besluit de compiler dat, aangezien we een
List hebben doorgegeven , de methode met
List<Integer> het meest geschikt is. En we falen met een fout. Daarom hebben we Regel #2: Als je een generieke klasse hebt, geef dan altijd de typeparameters op.
Beperkingen
We kunnen de typen beperken die zijn gespecificeerd in generieke methoden en klassen. Stel dat we willen dat een container alleen een
nummer accepteert als typeargument. Deze functie wordt beschreven in de sectie
Bounded Type Parameters van de Oracle-tutorial. Laten we naar een voorbeeld kijken:
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");
}
}
Zoals u kunt zien, hebben we de parameter type beperkt tot de klasse/interface
Number of de afstammelingen ervan. Merk op dat u niet alleen een klasse kunt specificeren, maar ook interfaces. Bijvoorbeeld:
public static class NumberContainer<T extends Number & Comparable> {
Generics ondersteunen ook
jokertekens. Ze zijn onderverdeeld in drie typen:
Uw gebruik van jokertekens moet voldoen aan het
Get-Put-principe . Het kan als volgt worden uitgedrukt:
- Gebruik een uitbreidingsjokerteken wanneer u alleen waarden uit een structuur haalt.
- Gebruik een superjokerteken wanneer u alleen waarden in een structuur invoert.
- En gebruik geen wildcard als jullie allebei van/naar een structuur willen halen en plaatsen.
Dit principe wordt ook wel het Producer Extends Consumer Super (PECS) principe genoemd. Hier is een klein voorbeeld uit de broncode voor de methode
Collections.copy van Java :
En hier is een klein voorbeeld van wat NIET ZAL werken:
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);
}
Maar als je extends vervangt door
super , dan is alles in orde. Omdat we de
lijst vullen met een waarde voordat de inhoud wordt weergegeven, is het een
consument . Daarom gebruiken we super.
Erfenis
Generieke geneesmiddelen hebben nog een ander interessant kenmerk: overerving. De manier waarop overerving werkt voor generieke geneesmiddelen wordt beschreven onder "
Generics, Inheritance, and Subtypes " in de handleiding van Oracle. Het belangrijkste is om het volgende te onthouden en te herkennen. Wij kunnen dit niet:
List<CharSequence> list1 = new ArrayList<String>();
Omdat overerving anders werkt met generieke geneesmiddelen:
En hier is nog een goed voorbeeld dat zal mislukken met een fout:
List<String> list1 = new ArrayList<>();
List<Object> list2 = list1;
Nogmaals, alles is hier eenvoudig.
List<String> is geen afstammeling van
List<Object> , ook al is
String een afstammeling van
Object .
Om te versterken wat je hebt geleerd, raden we je aan een videoles van onze Java-cursus te bekijken
Conclusie
Dus we hebben ons geheugen opgefrist met betrekking tot generieke geneesmiddelen. Als u zelden ten volle profiteert van hun mogelijkheden, worden sommige details wazig. Ik hoop dat deze korte recensie heeft geholpen om je geheugen op te frissen. Voor nog betere resultaten raad ik u ten zeerste aan om vertrouwd te raken met het volgende materiaal:
GO TO FULL VERSION