CodeGym /Java blogg /Slumpmässig /Java Generics: hur man använder vinklade parenteser i pra...
John Squirrels
Nivå
San Francisco

Java Generics: hur man använder vinklade parenteser i praktiken

Publicerad i gruppen

Introduktion

Från och med JSE 5.0 lades generika till Java-språkets arsenal.

Vad är generika i java?

Generika är Javas speciella mekanism för att implementera generisk programmering — ett sätt att beskriva data och algoritmer som låter dig arbeta med olika datatyper utan att ändra beskrivningen av algoritmerna. Oracle-webbplatsen har en separat handledning dedikerad till generika: " Lektion ". För att förstå generika måste du först ta reda på varför de behövs och vad de ger. Avsnittet " Varför använda generika? " i handledningen säger att ett par syften är starkare typkontroll vid kompilering och eliminering av behovet av explicita casts. Generics i Java: hur man använder vinklade konsoler i praktiken - 1Låt oss förbereda oss för några tester i vår älskade Tutorialspoint online java-kompilator. Anta att du har följande kod:

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);
	}
}
Den här koden kommer att fungera utmärkt. Men tänk om chefen kommer till oss och säger att "Hej världen!" är en överanvänd fras och att du bara måste returnera "Hej"? Vi tar bort koden som sammanfogar "värld!" Detta verkar harmlöst nog, eller hur? Men vi får faktiskt ett fel VID KOMPILERINGSTID:

error: incompatible types: Object cannot be converted to String
Problemet är att i vår lista lagrar objekt. String är en avkomling av Object (eftersom alla Java-klasser implicit ärver Object ), vilket betyder att vi behöver en explicit cast, men vi har inte lagt till någon. Under sammankopplingsoperationen kommer den statiska String.valueOf(obj) -metoden att anropas med hjälp av objektet. Så småningom kommer den att anropa Object -klassens toString- metod. Med andra ord innehåller vår lista ett objekt . Detta innebär att varhelst vi behöver en specifik typ (inte Object ), måste vi göra typkonverteringen själva:

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 det här fallet, eftersom List tar objekt, kan den lagra inte bara String s, utan även Integer s. Men det värsta är att kompilatorn inte ser något fel här. Och nu får vi ett fel VID KÖRNINGSTID (känd som ett "runtime error"). Felet blir:

java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String
Du måste hålla med om att det här inte är särskilt bra. Och allt detta eftersom kompilatorn inte är en artificiell intelligens som alltid kan gissa programmerarens avsikt korrekt. Java SE 5 introducerade generika för att låta oss berätta för kompilatorn om våra avsikter - om vilka typer vi kommer att använda. Vi fixar vår kod genom att tala om för kompilatorn vad vi vill ha:

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 behöver vi inte längre en cast till en sträng . Dessutom har vi vinkelparenteser kring typargumentet. Nu låter kompilatorn oss inte kompilera klassen förrän vi tar bort raden som lägger till 123 till listan, eftersom detta är ett heltal . Och det kommer att berätta det för oss. Många människor kallar generika "syntaktisk socker". Och de har rätt, eftersom efter att generika har kompilerats blir de verkligen samma typ av konverteringar. Låt oss titta på bytekoden för de kompilerade klasserna: en som använder en explicit cast och en som använder generika: Generics i Java: hur man använder vinklade konsoler i praktiken - 2Efter kompilering raderas alla generika. Detta kallas " typ radering". Typradering och generika är designade för att vara bakåtkompatibla med äldre versioner av JDK samtidigt som kompilatorn kan hjälpa till med typdefinitioner i nya versioner av Java.

Råa typer

På tal om generika har vi alltid två kategorier: parametriserade typer och råtyper. Råtyper är typer som utelämnar "typförtydligandet" inom vinkelparenteser: Generics i Java: hur man använder vinklade konsoler i praktiken - 3Parameteriserade typer inkluderar å andra sidan ett "förtydligande": Generics i Java: hur man använder vinklade konsoler i praktiken - 4Som du kan se använde vi en ovanlig konstruktion, markerad med en pil i skärmdumpen. Detta är en speciell syntax som lades till i Java SE 7. Den kallas " diamanten ". Varför? Vinkelparenteserna bildar en diamant: <> . Du bör också veta att diamantsyntaxen är associerad med begreppet " typinferens ". När allt kommer omkring, kompilatorn, ser <>till höger, tittar på den vänstra sidan av tilldelningsoperatorn, där den hittar typen av variabel vars värde tilldelas. Baserat på vad den hittar i den här delen förstår den typen av värde till höger. Faktum är att om en generisk typ anges till vänster, men inte till höger, kan kompilatorn sluta sig till 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 detta blandar den nya stilen med generika och den gamla stilen utan dem. Och detta är högst oönskat. När vi kompilerar koden ovan får vi följande meddelande:

Note: HelloWorld.java uses unchecked or unsafe operations
Faktum är att anledningen till att du ens behöver lägga till en diamant här verkar obegriplig. Men här är ett exempel:

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 kommer ihåg att ArrayList har en andra konstruktor som tar en samling som ett argument. Och det är här något ondskefullt ligger gömt. Utan diamantsyntaxen förstår inte kompilatorn att den blir lurad. Med diamantsyntaxen gör det det. Så, regel #1 är: använd alltid diamantsyntaxen med parametriserade typer. Annars riskerar vi att missa var vi använder råtyper. För att eliminera "använder okontrollerade eller osäkra operationer"-varningar kan vi använda @SuppressWarnings("okontrollerad") annotering på en metod eller klass. Men tänk på varför du har bestämt dig för att använda den. Kom ihåg regel nummer ett. Du kanske behöver lägga till ett typargument.

Java Generiska metoder

Generics låter dig skapa metoder vars parametertyper och returtyp parametreras. Ett separat avsnitt ägnas åt denna funktion i Oracle-handledningen: " Generic Methods ". Det är viktigt att komma ihåg syntaxen som lärs ut i denna handledning:
  • den innehåller en lista över typparametrar inom vinkelparenteser;
  • listan med typparametrar går före metodens returtyp.
Låt oss titta på ett exempel:

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));
		}
    }
}
Om du tittar på Util -klassen ser du att den har två generiska metoder. Tack vare möjligheten till typinferens kan vi antingen ange typen direkt till kompilatorn, eller så kan vi specificera den själva. Båda alternativen presenteras i exemplet. Syntaxen är förresten väldigt vettig om man tänker efter. När vi deklarerar en generisk metod anger vi typparametern FÖRE metoden, för om vi deklarerar typparametern efter metoden, skulle JVM inte kunna ta reda på vilken typ som ska användas. Följaktligen förklarar vi först att vi kommer att använda parametern T- typ, och sedan säger vi att vi kommer att returnera denna typ. Naturligtvis kommer Util.<Integer>getValue(element, String.class) att misslyckas med ett fel:inkompatibla typer: Klass<String> kan inte konverteras till Klass<Heltal> . När du använder generiska metoder bör du alltid komma ihåg typradering. Låt oss titta på ett exempel:

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);
		}
    }
}
Det här kommer att gå bra. Men bara så länge som kompilatorn förstår att returtypen för metoden som anropas är Integer . Ersätt konsolutmatningen med följande rad:

System.out.println(Util.getValue(element) + 1);
Vi får ett felmeddelande:

bad operand types for binary operator '+', first type: Object, second type: int.
Med andra ord har typradering skett. Kompilatorn ser att ingen har specificerat typen, så typen indikeras som Objekt och metoden misslyckas med ett fel.

Generiska klasser

Inte bara metoder kan parametriseras. Klasser kan också. Avsnittet "Generiska typer" i Oracles handledning ägnas åt detta. Låt oss överväga ett exempel:

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);
		}
	}
}
Allt är enkelt här. Om vi ​​använder den generiska klassen indikeras typparametern efter klassnamnet. Låt oss nu skapa en instans av denna klass i huvudmetoden :

public static void main(String []args) {
	SomeType<String> st = new SomeType<>();
	List<String> list = Arrays.asList("test");
	st.test(list);
}
Den här koden kommer att fungera bra. Kompilatorn ser att det finns en lista med nummer och en samling strängar . Men vad händer om vi tar bort typparametern och gör så här:

SomeType st = new SomeType();
List<String> list = Arrays.asList("test");
st.test(list);
Vi får ett felmeddelande:

java.lang.ClassCastException: java.lang.String cannot be cast to java.lang.Integer
Återigen, detta är typradering. Eftersom klassen inte längre använder en typparameter, bestämmer kompilatorn att, eftersom vi skickade en List , är metoden med List<Integer> mest lämplig. Och vi misslyckas med ett fel. Därför har vi regel #2: Om du har en generisk klass, ange alltid typparametrarna.

Restriktioner

Vi kan begränsa de typer som anges i generiska metoder och klasser. Anta till exempel att vi vill att en behållare endast ska acceptera ett nummer som typargument. Den här funktionen beskrivs i avsnittet Bounded Type Parameters i Oracles handledning. Låt oss titta på ett exempel:

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änsat typparametern till nummerklassen /gränssnittet eller dess avkomlingar. Observera att du inte bara kan ange en klass utan även gränssnitt. Till exempel:

public static class NumberContainer<T extends Number & Comparable> {
Generika stöder också jokertecken. De är indelade i tre typer: Din användning av jokertecken bör följa Get-Put-principen . Det kan uttryckas på följande sätt:
  • Använd ett utökat jokertecken när du bara får ut värden ur en struktur.
  • Använd ett superjokertecken när du bara lägger in värden i en struktur.
  • Och använd inte ett jokertecken när du både vill komma och sätta från/till en struktur.
Denna princip kallas även PECS-principen (Producer Extends Consumer Super). Här är ett litet exempel från källkoden för Javas Collections.copy -metod: Generics i Java: hur man använder vinklade konsoler i praktiken - 5Och här är ett litet exempel på vad som INTE fungerar:

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 om du byter ut extends med super , så är allt bra. Eftersom vi fyller i listan med ett värde innan vi visar dess innehåll, är det en konsument . Därför använder vi super.

Arv

Generika har en annan intressant egenskap: arv. Hur arv fungerar för generika beskrivs under " Generics, Arv och undertyper " i Oracles handledning. Det viktiga är att komma ihåg och känna igen följande. Vi kan inte göra detta:

List<CharSequence> list1 = new ArrayList<String>();
Eftersom arv fungerar annorlunda med generika: Generics i Java: hur man använder vinklade konsoler i praktiken - 6Och här är ett annat bra exempel som kommer att misslyckas med ett fel:

List<String> list1 = new ArrayList<>();
List<Object> list2 = list1;
Återigen, allt är enkelt här. List<String> är inte en avkomling av List<Object> , även om String är en avkomling av Object . För att förstärka det du lärde dig föreslår vi att du tittar på en videolektion från vår Java-kurs

Slutsats

Så vi har fräschat upp vårt minne angående generika. Om du sällan drar full nytta av deras möjligheter, blir några av detaljerna suddiga. Jag hoppas att denna korta recension har hjälpt till att öka ditt minne. För ännu bättre resultat rekommenderar jag starkt att du bekantar dig med följande material:
Kommentarer
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION