CodeGym /Java blog /Tilfældig /Java Generics: hvordan man bruger vinklede beslag i praks...
John Squirrels
Niveau
San Francisco

Java Generics: hvordan man bruger vinklede beslag i praksis

Udgivet i gruppen

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. Generisk i Java: hvordan man bruger vinklede beslag i praksis - 1Lad 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: Generisk i Java: hvordan man bruger vinklede beslag i praksis - 2Efter 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: Generisk i Java: hvordan man bruger vinklede beslag i praksis - 3Parametriserede typer omfatter på hånden en "afklaring": Generisk i Java: hvordan man bruger vinklede beslag i praksis - 4Som 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: Generisk i Java: hvordan man bruger vinklede beslag i praksis - 5Og 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: Generisk i Java: hvordan man bruger vinklede beslag i praksis - 6Og 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:
Kommentarer
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION