CodeGym /Java Blog /Willekeurig /Java Generics: het gebruik van punthaken in de praktijk
John Squirrels
Niveau 41
San Francisco

Java Generics: het gebruik van punthaken in de praktijk

Gepubliceerd in de groep Willekeurig

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. Generics in Java: hoe punthaken in de praktijk te gebruiken - 1Laten 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: Generics in Java: hoe punthaken in de praktijk te gebruiken - 2na 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: Generics in Java: gebruik van punthaken in de praktijk - 3geparametriseerde typen, aan de kant, bevatten een "verduidelijking": Generics in Java: het gebruik van punthaken in de praktijk - 4zoals 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 : Generics in Java: gebruik van punthaken in de praktijk - 5En 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: Generics in Java: het gebruik van punthaken in de praktijk - 6En 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:
Opmerkingen
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION