CodeGym /Java-blogg /Tilfeldig /Java Generics: hvordan bruke vinklede parenteser i praksi...
John Squirrels
Nivå
San Francisco

Java Generics: hvordan bruke vinklede parenteser i praksis

Publisert i gruppen

Introduksjon

Fra og med JSE 5.0 ble generikk lagt til Java-språkets arsenal.

Hva er generika i java?

Generiske er Javas spesielle mekanisme for å implementere generisk programmering - en måte å beskrive data og algoritmer som lar deg jobbe med forskjellige datatyper uten å endre beskrivelsen av algoritmene. Oracle-nettstedet har en egen opplæring dedikert til generiske medisiner: " Leksjon ". For å forstå generiske medisiner, må du først finne ut hvorfor de trengs og hva de gir. " Hvorfor bruke generikk? "-delen av opplæringen sier at et par formål er sterkere typekontroll ved kompilering og eliminering av behovet for eksplisitte casts. Generikk i Java: hvordan bruke vinklede parenteser i praksis - 1La oss forberede oss på noen tester i vår elskede Tutorialspoint online java-kompiler. Anta 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 koden vil fungere utmerket. Men hva om sjefen kommer til oss og sier at "Hei, verden!" er en overbrukt setning og at du bare må returnere "Hei"? Vi fjerner koden som setter sammen ", verden!" Dette virker harmløst nok, ikke sant? Men vi får faktisk en feil PÅ KOMPILERINGSTID:

error: incompatible types: Object cannot be converted to String
Problemet er at i vår liste lagrer objekter. String er en etterkommer av Object (siden alle Java-klasser implisitt arver Object ), noe som betyr at vi trenger en eksplisitt rollebesetning, men vi la ikke til en. Under sammenkoblingsoperasjonen vil den statiske String.valueOf(obj) -metoden bli kalt ved bruk av objektet. Til slutt vil den kalle Object- klassens toString -metode. Med andre ord inneholder listen vår et objekt . Dette betyr at uansett hvor vi trenger en spesifikk type (ikke Object ), må vi gjøre typekonverteringen selv:

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 tilfellet, fordi List tar objekter, kan den lagre ikke bare String s, men også Integer s. Men det verste er at kompilatoren ikke ser noe galt her. Og nå får vi en feil AT RUN TIME (kjent som en "runtime error"). Feilen vil være:

java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String
Du må være enig i at dette ikke er veldig bra. Og alt dette fordi kompilatoren ikke er en kunstig intelligens som alltid er i stand til å gjette programmererens hensikt riktig. Java SE 5 introduserte generikk for å la oss fortelle kompilatoren om intensjonene våre - om hvilke typer vi skal bruke. Vi fikser koden vår ved å fortelle kompilatoren hva vi vil 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, trenger vi ikke lenger en rollebesetning til en streng . I tillegg har vi vinkelparenteser rundt typeargumentet. Nå vil ikke kompilatoren la oss kompilere klassen før vi fjerner linjen som legger til 123 til listen, siden dette er et heltall . Og det vil fortelle oss det. Mange kaller generika "syntaktisk sukker". Og de har rett, siden etter at generiske medisiner er kompilert, blir de virkelig samme type konverteringer. La oss se på bytekoden til de kompilerte klassene: en som bruker en eksplisitt cast og en som bruker generikk: Generikk i Java: hvordan bruke vinklede parenteser i praksis - 2Etter kompilering slettes alle generiske. Dette kalles " type sletting".". Typesletting og generikk er designet for å være bakoverkompatible med eldre versjoner av JDK, samtidig som kompilatoren kan hjelpe med typedefinisjoner i nye versjoner av Java.

Rå typer

Når vi snakker om generika, har vi alltid to kategorier: parameteriserte typer og råtyper. Råtyper er typer som utelater "typeavklaring" i vinkelparentes: Generikk i Java: hvordan bruke vinklede parenteser i praksis - 3Parameteriserte typer inkluderer på siden en "avklaring": Generikk i Java: hvordan bruke vinklede parenteser i praksis - 4Som du kan se brukte vi en uvanlig konstruksjon, markert med en pil i skjermbildet. Dette er en spesiell syntaks som ble lagt til Java SE 7. Den kalles " diamanten ". Hvorfor? Vinkelparentesene danner en diamant: <> . Du bør også vite at diamantsyntaksen er assosiert med konseptet " typeinferens ". Tross alt, kompilatoren, ser <>til høyre, ser på venstre side av tilordningsoperatoren, der den finner typen variabel hvis verdi blir tildelt. Basert på hva den finner i denne delen, forstår den typen verdi til høyre. Faktisk, hvis en generisk type er gitt til venstre, men ikke til høyre, kan kompilatoren utlede 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 stilen med generiske og den gamle stilen uten dem. Og dette er høyst uønsket. Når vi kompilerer koden ovenfor, får vi følgende melding:

Note: HelloWorld.java uses unchecked or unsafe operations
Grunnen til at du til og med trenger å legge til en diamant her virker faktisk 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 andre konstruktør som tar en samling som et argument. Og det er her noe uhyggelig ligger gjemt. Uten diamantsyntaksen forstår ikke kompilatoren at den blir lurt. Med diamantsyntaksen gjør det det. Så, regel #1 er: bruk alltid diamantsyntaksen med parameteriserte typer. Ellers risikerer vi å gå glipp av hvor vi bruker råtyper. For å eliminere "bruker ukontrollerte eller usikre operasjoner"-advarsler, kan vi bruke @SuppressWarnings("ukontrollerte") merknaden på en metode eller klasse. Men tenk på hvorfor du har bestemt deg for å bruke den. Husk regel nummer én. Kanskje du må legge til et typeargument.

Java Generiske metoder

Generiske lar deg lage metoder hvis parametertyper og returtype er parameterisert. En egen del er viet til denne muligheten i Oracle-opplæringen: " Generiske metoder ". Det er viktig å huske syntaksen som er undervist i denne opplæringen:
  • den inkluderer en liste over typeparametere innenfor vinkelparenteser;
  • listen over typeparametere går foran metodens returtype.
La oss 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 muligheten for typeslutning kan vi enten indikere typen direkte til kompilatoren, eller vi kan spesifisere den selv. Begge alternativene er presentert i eksemplet. Syntaksen gir forresten mye mening hvis du tenker deg om. Når vi erklærer en generisk metode, spesifiserer vi typeparameteren FØR metoden, fordi hvis vi erklærer typeparameteren etter metoden, ville ikke JVM kunne finne ut hvilken type som skal brukes. Følgelig erklærer vi først at vi vil bruke T -type-parameteren, og så sier vi at vi skal returnere denne typen. Naturligvis vil Util.<Integer>getValue(element, String.class) mislykkes med en feil:inkompatible typer: Klasse<String> kan ikke konverteres til Klasse<Heltall> . Når du bruker generiske metoder, bør du alltid huske type sletting. La oss 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 gå helt fint. Men bare så lenge kompilatoren forstår at returtypen til metoden som kalles er Integer . Erstatt konsollutdatasetningen med følgende linje:

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

bad operand types for binary operator '+', first type: Object, second type: int.
Med andre ord har typesletting skjedd. Kompilatoren ser at ingen har spesifisert typen, så typen er indikert som Objekt og metoden mislykkes med en feil.

Generiske klasser

Ikke bare metoder kan parameteriseres. Klasser kan også. "Generiske typer" -delen av Oracles opplæring er viet til dette. La oss vurdere 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 bruker den generiske klassen, er typeparameteren angitt etter klassenavnet. La oss nå lage en forekomst av denne klassen i hovedmetoden :

public static void main(String []args) {
	SomeType<String> st = new SomeType<>();
	List<String> list = Arrays.asList("test");
	st.test(list);
}
Denne koden vil fungere bra. Kompilatoren ser at det er en liste over tall og en samling av strenger . Men hva om vi eliminerer typeparameteren og gjør dette:

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

java.lang.ClassCastException: java.lang.String cannot be cast to java.lang.Integer
Igjen, dette er type sletting. Siden klassen ikke lenger bruker en type-parameter, bestemmer kompilatoren at siden vi passerte en List , er metoden med List<Heltall> mest passende. Og vi mislykkes med en feil. Derfor har vi regel #2: Hvis du har en generisk klasse, spesifiser alltid typeparameterne.

Begrensninger

Vi kan begrense typene som er spesifisert i generiske metoder og klasser. Anta for eksempel at vi vil at en beholder bare skal akseptere et tall som typeargument. Denne funksjonen er beskrevet i avsnittet Bounded Type Parameters i Oracles veiledning. La oss 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 begrenset typeparameteren til nummerklassen /grensesnittet eller dens etterkommere. Merk at du kan spesifisere ikke bare en klasse, men også grensesnitt. For eksempel:

public static class NumberContainer<T extends Number & Comparable> {
Generika støtter også jokertegn. De er delt inn i tre typer: Din bruk av jokertegn bør følge Get-Put-prinsippet . Det kan uttrykkes som følger:
  • Bruk et utvidet jokertegn når du bare får verdier ut av en struktur.
  • Bruk et superjokertegn når du bare legger verdier inn i en struktur.
  • Og ikke bruk jokertegn når du både vil komme og sette fra/til en struktur.
Dette prinsippet kalles også PECS-prinsippet (Producer Extends Consumer Super). Her er et lite eksempel fra kildekoden for Javas Collections.copy -metode: Generikk i Java: hvordan bruke vinklede parenteser i praksis - 5Og her er et lite eksempel på hva som IKKE VIL fungere:

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 extends med super , så er alt i orden. Fordi vi fyller listen med en verdi før vi viser innholdet, er den en forbruker . Følgelig bruker vi super.

Arv

Generika har en annen interessant funksjon: arv. Måten arv fungerer for generiske medisiner er beskrevet under " Generiske, arv og undertyper " i Oracles veiledning. Det viktige er å huske og gjenkjenne følgende. Vi kan ikke gjøre dette:

List<CharSequence> list1 = new ArrayList<String>();
Fordi arv fungerer annerledes med generiske medisiner: Generikk i Java: hvordan bruke vinklede parenteser i praksis - 6Og her er et annet godt eksempel som vil mislykkes med en feil:

List<String> list1 = new ArrayList<>();
List<Object> list2 = list1;
Igjen, alt er enkelt her. List<String> er ikke en etterkommer av List<Object> , selv om String er en etterkommer av Object . For å forsterke det du lærte, foreslår vi at du ser en videoleksjon fra vårt Java-kurs

Konklusjon

Så vi har frisket opp hukommelsen angående generiske legemidler. Hvis du sjelden drar full nytte av mulighetene deres, blir noen av detaljene uklare. Jeg håper denne korte anmeldelsen har hjulpet med å øke hukommelsen din. For enda bedre resultater anbefaler jeg på det sterkeste at du gjør deg kjent med følgende materiale:
Kommentarer
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION