Hei! Vi fortsetter vår serie med leksjoner om generiske legemidler. Vi har tidligere fått en generell idé om hva de er og hvorfor de trengs. I dag skal vi lære mer om noen av funksjonene til generiske legemidler og om å jobbe med dem. La oss gå! I forrige leksjon snakket vi om forskjellen mellom generiske typer og råtyper . En råtype er en generisk klasse hvis type er fjernet.
Dokumentasjonen sier: "T - typen klasse som er modellert av dette klasseobjektet." Når vi oversetter dette fra dokumentasjonsspråket til vanlig tale, forstår vi at klassen til objektet
List list = new ArrayList();
Her er et eksempel. Her angir vi ikke hvilken type gjenstander som skal plasseres i vår List
. Hvis vi prøver å lage en slik List
og legge til noen objekter til den, vil vi se en advarsel i IDEA:
"Unchecked call to add(E) as a member of raw type of java.util.List".
Men vi snakket også om det faktum at generikk bare dukket opp i Java 5. Da denne versjonen ble utgitt, hadde programmerere allerede skrevet en haug med kode ved å bruke råtyper, så denne funksjonen i språket kunne ikke slutte å fungere, og muligheten til å lage råtyper i Java ble bevart. Problemet viste seg imidlertid å være mer utbredt. Som du vet, konverteres Java-kode til et spesielt kompilert format kalt bytecode, som deretter kjøres av den virtuelle Java-maskinen. Men hvis vi legger informasjon om typeparametere i bytekoden under konverteringsprosessen, ville den bryte all den tidligere skrevne koden, fordi det ikke var noen typeparametere før Java 5! Når du arbeider med generiske medisiner, er det ett veldig viktig konsept du må huske. Det kalles type sletting. Det betyr at en klasse ikke inneholder informasjon om en typeparameter. Denne informasjonen er kun tilgjengelig under kompilering og slettes (blir utilgjengelig) før kjøretid. Hvis du prøver å sette feil type objekt i List<String>
, vil kompilatoren generere en feil. Dette er nøyaktig hva språkets skapere ønsker å oppnå når de opprettet generiske artikler: kompileringstidssjekker. Men når all Java-koden din blir til bytekode, inneholder den ikke lenger informasjon om typeparametere. I bytecode List<Cat>
er kattelisten din ikke annerledes enn List<String>
strenger. I bytekode er det ingenting som sier at det cats
er en liste over Cat
objekter. Slik informasjon slettes under kompilering — bare det at du har en List<Object> cats
liste vil havne i programmets bytekode. La oss se hvordan dette fungerer:
public class TestClass<T> {
private T value1;
private T value2;
public void printValues() {
System.out.println(value1);
System.out.println(value2);
}
public static <T> TestClass<T> createAndAdd2Values(Object o1, Object o2) {
TestClass<T> result = new TestClass<>();
result.value1 = (T) o1;
result.value2 = (T) o2;
return result;
}
public static void main(String[] args) {
Double d = 22.111;
String s = "Test String";
TestClass<Integer> test = createAndAdd2Values(d, s);
test.printValues();
}
}
Vi opprettet vår egen generiske TestClass
klasse. Det er ganske enkelt: det er faktisk en liten "samling" av 2 objekter, som lagres umiddelbart når objektet er opprettet. Den har 2 T
felt. Når createAndAdd2Values()
metoden er utført, må de to passerte objektene ( Object a
og Object b
castes til T
typen og deretter legges til objektet TestClass
. I main()
metoden lager vi en TestClass<Integer>
, dvs. Integer
typeargumentet erstatter Integer
typeparameteren. Vi sender også a Double
og a String
til metoden createAndAdd2Values()
. Tror du programmet vårt vil fungere? Vi spesifiserte tross alt Integer
som typeargumentet, men en String
kan definitivt ikke castes til en Integer
! La oss kjøremain()
metode og sjekk. Konsoll utgang:
22.111
Test String
Det var uventet! Hvorfor skjedde dette? Det er et resultat av type sletting. Informasjon om Integer
typeargumentet som ble brukt til å instansiere objektet vårt TestClass<Integer> test
ble slettet da koden ble kompilert. Feltet blir TestClass<Object> test
. Våre Double
og String
argumenter ble enkelt konvertert til Object
objekter (de konverteres ikke til Integer
objekter slik vi forventet!) og stille lagt til TestClass
. Her er et annet enkelt, men veldig avslørende eksempel på type sletting:
import java.util.ArrayList;
import java.util.List;
public class Main {
private class Cat {
}
public static void main(String[] args) {
List<String> strings = new ArrayList<>();
List<Integer> numbers = new ArrayList<>();
List<Cat> cats = new ArrayList<>();
System.out.println(strings.getClass() == numbers.getClass());
System.out.println(numbers.getClass() == cats.getClass());
}
}
Konsoll utgang:
true
true
Det ser ut til at vi har laget samlinger med tre forskjellige typer argumenter - String
, Integer
, og vår egen Cat
klasse. Men under konverteringen til bytekode blir alle tre listene List<Object>
, så når programmet kjører forteller det oss at vi bruker samme klasse i alle tre tilfellene.
Skriv sletting når du arbeider med matriser og generiske artikler
Det er et veldig viktig poeng som må forstås tydelig når du arbeider med matriser og generiske klasser (som f.eks.List
). Du bør også ta det i betraktning når du velger datastrukturer for programmet ditt. Generiske er gjenstand for typesletting. Informasjon om typeparametere er ikke tilgjengelig under kjøring. Derimot vet arrays om og kan bruke informasjon om deres datatype når programmet kjører. Forsøk på å sette en ugyldig type inn i en matrise vil føre til at et unntak blir kastet:
public class Main2 {
public static void main(String[] args) {
Object x[] = new String[3];
x[0] = new Integer(222);
}
}
Konsoll utgang:
Exception in thread "main" java.lang.ArrayStoreException: java.lang.Integer
Fordi det er så stor forskjell mellom arrays og generiske, kan de ha kompatibilitetsproblemer. Fremfor alt kan du ikke lage en rekke generiske objekter eller bare en parameterisert matrise. Høres det litt forvirrende ut? La oss ta en titt. Du kan for eksempel ikke gjøre noe av dette i Java:
new List<T>[]
new List<String>[]
new T[]
Hvis vi prøver å lage en rekke List<String>
objekter, får vi en kompileringsfeil som klager over generisk matriseoppretting:
import java.util.List;
public class Main2 {
public static void main(String[] args) {
// Compilation error! Generic array creation
List<String>[] stringLists = new List<String>[1];
}
}
Men hvorfor gjøres dette? Hvorfor er det ikke tillatt å lage slike arrays? Dette er alt for å gi typesikkerhet. Hvis kompilatoren lar oss lage slike arrays av generiske objekter, kan vi lage massevis av problemer for oss selv. Her er et enkelt eksempel fra Joshua Blochs bok "Effective Java":
public static void main(String[] args) {
List<String>[] stringLists = new List<String>[1]; // (1)
List<Integer> intList = Arrays.asList(42, 65, 44); // (2)
Object[] objects = stringLists; // (3)
objects[0] = intList; // (4)
String s = stringLists[0].get(0); // (5)
}
La oss forestille oss at List<String>[] stringLists
det er tillatt å lage en array like og ikke vil generere en kompileringsfeil. Hvis dette var sant, her er noen ting vi kan gjøre: I linje 1 lager vi en rekke lister: List<String>[] stringLists
. Arrayet vårt inneholder en List<String>
. På linje 2 lager vi en liste med tall: List<Integer>
. I linje 3 tilordner vi vår List<String>[]
til en Object[] objects
variabel. Java-språket tillater dette: en rekke X
objekter kan lagre X
objekter og objekter av alle underklasser X
. Følgelig kan du sette hva som helst i en Object
matrise. I linje 4 erstatter vi det eneste elementet i matrisen objects()
(a List<String>
) med en List<Integer>
. Dermed la vi a List<Integer>
i en matrise som kun var ment å lagreList<String>
gjenstander! Vi vil støte på en feil bare når vi kjører linje 5. A ClassCastException
vil bli kastet under kjøring. Følgelig ble et forbud mot å lage slike arrays lagt til Java. Dette lar oss unngå slike situasjoner.
Hvordan kan jeg komme meg rundt type sletting?
Vel, vi lærte om type sletting. La oss prøve å lure systemet! :) Oppgave: Vi har en generiskTestClass<T>
klasse. Vi ønsker å skrive en createNewT()
metode for denne klassen som vil opprette og returnere et nytt T
objekt. Men dette er umulig, ikke sant? All informasjon om T
typen slettes under kompilering, og under kjøring kan vi ikke bestemme hvilken type objekt vi må lage. Det er faktisk én vanskelig måte å gjøre dette på. Du husker sikkert at Java har en Class
klasse. Vi kan bruke den til å bestemme klassen til noen av objektene våre:
public class Main2 {
public static void main(String[] args) {
Class classInt = Integer.class;
Class classString = String.class;
System.out.println(classInt);
System.out.println(classString);
}
}
Konsoll utgang:
class java.lang.Integer
class java.lang.String
Men her er ett aspekt vi ikke har snakket om. I Oracle-dokumentasjonen vil du se at klasseklassen er generisk!
https://docs.oracle.com/javase/8/docs/api/java/lang/Class.html
Integer.class
ikke bare er Class
, men snarere Class<Integer>
. Objekttypen String.class
er ikke bare Class
, men heller Class<String>
, osv. Hvis det fortsatt ikke er klart, kan du prøve å legge til en type-parameter i forrige eksempel:
public class Main2 {
public static void main(String[] args) {
Class<Integer> classInt = Integer.class;
// Compilation error!
Class<String> classInt2 = Integer.class;
Class<String> classString = String.class;
// Compilation error!
Class<Double> classString2 = String.class;
}
}
Og nå, ved å bruke denne kunnskapen, kan vi omgå type sletting og utføre oppgaven vår! La oss prøve å få informasjon om en typeparameter. Typeargumentet vårt vil være MySecretClass
:
public class MySecretClass {
public MySecretClass() {
System.out.println("A MySecretClass object was created successfully!");
}
}
Og her er hvordan vi bruker løsningen vår i praksis:
public class TestClass<T> {
Class<T> typeParameterClass;
public TestClass(Class<T> typeParameterClass) {
this.typeParameterClass = typeParameterClass;
}
public T createNewT() throws IllegalAccessException, InstantiationException {
T t = typeParameterClass.newInstance();
return t;
}
public static void main(String[] args) throws InstantiationException, IllegalAccessException {
TestClass<MySecretClass> testString = new TestClass<>(MySecretClass.class);
MySecretClass secret = testString.createNewT();
}
}
Konsoll utgang:
A MySecretClass object was created successfully!
Vi ga nettopp det nødvendige klasseargumentet til konstruktøren av vår generiske klasse:
TestClass<MySecretClass> testString = new TestClass<>(MySecretClass.class);
Dette tillot oss å lagre informasjonen om typeargumentet, og forhindret at den ble fullstendig slettet. Som et resultat klarte vi å lage enT
gjenstand! :) Med det går dagens leksjon mot slutten. Du må alltid huske type sletting når du arbeider med generiske medisiner. Denne løsningen ser ikke veldig praktisk ut, men du bør forstå at generiske medisiner ikke var en del av Java-språket da det ble opprettet. Denne funksjonen, som hjelper oss med å lage parameteriserte samlinger og fange opp feil under kompilering, ble tatt på senere. På noen andre språk som inkluderte generikk fra den første versjonen, er det ingen typesletting (for eksempel i C#). Forresten, vi er ikke ferdige med å studere generikk! I neste leksjon vil du bli kjent med noen flere funksjoner ved generiske legemidler. Foreløpig ville det vært greit å løse et par oppgaver! :)