Hej! Vi fortsætter vores serie af lektioner om generiske lægemidler. Vi fik tidligere en generel idé om, hvad de er, og hvorfor de er nødvendige. I dag lærer vi mere om nogle af funktionerne ved generiske lægemidler og om at arbejde med dem. Lad os gå!
I sidste lektion talte vi om forskellen mellem generiske typer og råtyper . En rå type er en generisk klasse, hvis type er blevet fjernet.
Dokumentationen siger, "T - typen af klassen, der er modelleret af dette klasseobjekt." Når vi oversætter dette fra dokumentationssproget til almindelig tale, forstår vi, at objektets klasse

List list = new ArrayList();
Her er et eksempel. Her angiver vi ikke, hvilken type objekter der vil blive placeret i vores List
. Hvis vi forsøger at oprette sådan en List
og tilføje nogle 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 talte også om det faktum, at generika kun dukkede op i Java 5. Da denne version blev frigivet, havde programmører allerede skrevet en masse kode ved hjælp af råtyper, så denne funktion i sproget kunne ikke stoppe med at fungere, og evnen til at oprette råtyper i Java blev bevaret. Problemet viste sig dog at være mere udbredt. Som du ved, konverteres Java-kode til et særligt kompileret format kaldet bytecode, som derefter udføres af den virtuelle Java-maskine. Men hvis vi lægger information om typeparametre i bytekoden under konverteringsprocessen, ville det bryde al den tidligere skrevne kode, fordi der ikke var nogen typeparametre før Java 5! Når du arbejder med generiske lægemidler, er der et meget vigtigt koncept, som du skal huske. Det kaldes type sletning. Det betyder, at en klasse ikke indeholder information om en typeparameter. Disse oplysninger er kun tilgængelige under kompilering og slettes (bliver utilgængelige) før runtime. Hvis du forsøger at sætte den forkerte type objekt i din List<String>
, vil compileren generere en fejl. Det er præcis, hvad sprogets skabere ønsker at opnå, når de oprettede generiske artikler: kompileringstidstjek. Men når al din Java-kode bliver til bytekode, indeholder den ikke længere information om typeparametre. I bytecode List<Cat>
er din katteliste ikke anderledes end List<String>
strenge. I bytecode siger intet, at det cats
er en liste over Cat
objekter. Sådanne oplysninger slettes under kompileringen — kun det faktum, at du har en List<Object> cats
liste, ender i programmets bytekode. Lad os se, hvordan dette virker:
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 skabte vores egen generiske TestClass
klasse. Det er ganske enkelt: det er faktisk en lille "samling" af 2 objekter, som gemmes med det samme, når objektet er oprettet. Den har 2 T
felter. Når createAndAdd2Values()
metoden udføres, skal de to beståede objekter ( Object a
og Object b
castes til T
typen og derefter tilføjes til TestClass
objektet. I main()
metoden opretter vi et TestClass<Integer>
, dvs. Integer
typeargumentet erstatter Integer
typeparameteren. Vi sender også a Double
og a String
til metoden createAndAdd2Values()
. Tror du, vores program vil fungere? Vi har jo angivet Integer
som typeargumentet, men en String
kan absolut ikke castes til en Integer
! Lad os køremain()
metode og check. Konsoludgang:
22.111
Test String
Det var uventet! Hvorfor skete dette? Det er resultatet af type sletning. Oplysninger om Integer
typeargumentet, der blev brugt til at instantiere vores TestClass<Integer> test
objekt, blev slettet, da koden blev kompileret. Feltet bliver TestClass<Object> test
. Vores Double
og String
argumenter blev nemt konverteret til Object
objekter (de konverteres ikke til Integer
objekter, som vi forventede!) og stille og roligt tilføjet til TestClass
. Her er et andet simpelt, men meget afslørende eksempel på typesletning:
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());
}
}
Konsoludgang:
true
true
Det ser ud til, at vi har lavet samlinger med tre forskellige typer argumenter - String
, Integer
, og vores helt egen Cat
klasse. Men under konverteringen til bytecode bliver alle tre lister List<Object>
, så når programmet kører fortæller det os, at vi bruger den samme klasse i alle tre tilfælde.
Skriv sletning, når du arbejder med arrays og generiske artikler
Der er en meget vigtig pointe, som skal forstås klart, når man arbejder med arrays og generiske klasser (såsomList
). Du bør også tage det i betragtning, når du vælger datastrukturer til dit program. Generiske stoffer er underlagt typesletning. Oplysninger om typeparametre er ikke tilgængelige under kørsel. Derimod kender arrays til og kan bruge information om deres datatype, når programmet kører. Forsøg på at indsætte en ugyldig type i et array vil medføre, at en undtagelse bliver kastet:
public class Main2 {
public static void main(String[] args) {
Object x[] = new String[3];
x[0] = new Integer(222);
}
}
Konsoludgang:
Exception in thread "main" java.lang.ArrayStoreException: java.lang.Integer
Fordi der er så stor forskel mellem arrays og generiske stoffer, kan de have kompatibilitetsproblemer. Frem for alt kan du ikke oprette et array af generiske objekter eller bare en parameteriseret array. Lyder det lidt forvirrende? Lad os se. For eksempel kan du ikke gøre noget af dette i Java:
new List<T>[]
new List<String>[]
new T[]
Hvis vi forsøger at oprette et array af List<String>
objekter, får vi en kompileringsfejl, der klager over generisk array-oprettelse:
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 gøres dette? Hvorfor er oprettelse af sådanne arrays ikke tilladt? Dette er alt sammen for at give typesikkerhed. Hvis compileren lader os skabe sådanne arrays af generiske objekter, kunne vi lave et væld af problemer for os selv. Her er et simpelt eksempel fra Joshua Blochs bog "Effektiv 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)
}
Lad os forestille os, at oprettelse af et array-like List<String>[] stringLists
er tilladt og ikke vil generere en kompileringsfejl. Hvis dette var sandt, er her nogle ting, vi kunne gøre: I linje 1 opretter vi en række lister: List<String>[] stringLists
. Vores array indeholder en List<String>
. I linje 2 opretter vi en liste med tal: List<Integer>
. I linje 3 tildeler vi vores List<String>[]
til en Object[] objects
variabel. Java-sproget tillader dette: en række X
objekter kan gemme X
objekter og objekter af alle underklasser X
. Derfor kan du overhovedet sætte hvad som helst i et Object
array. I linje 4 erstatter vi det eneste element i objects()
arrayet (a List<String>
) med en List<Integer>
. Således satte vi a List<Integer>
i et array, der kun var beregnet til at gemmeList<String>
genstande! Vi vil kun støde på en fejl, når vi udfører linje 5. A ClassCastException
vil blive kastet under kørsel. Derfor blev et forbud mod oprettelse af sådanne arrays tilføjet til Java. Dette lader os undgå sådanne situationer.
Hvordan kommer jeg uden om typesletning?
Nå, vi lærte om tekstsletning. Lad os prøve at narre systemet! :) Opgave: Vi har en generiskTestClass<T>
klasse. Vi ønsker at skrive en createNewT()
metode til denne klasse, der vil skabe og returnere et nyt T
objekt. Men det er umuligt, ikke? Al information om T
typen slettes under kompileringen, og ved kørsel kan vi ikke bestemme, hvilken type objekt vi skal oprette. Der er faktisk én vanskelig måde at gøre dette på. Du husker sikkert, at Java har en Class
klasse. Vi kan bruge det til at bestemme klassen for ethvert af vores objekter:
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);
}
}
Konsoludgang:
class java.lang.Integer
class java.lang.String
Men her er et aspekt, som vi ikke har talt om. I Oracle-dokumentationen kan 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>
. Objektets type String.class
er ikke kun Class
, men snarere Class<String>
osv. Hvis det stadig ikke er klart, kan du prøve at tilføje en type-parameter til det 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 nu, ved at bruge denne viden, kan vi omgå typesletning og udføre vores opgave! Lad os prøve at få information om en typeparameter. Vores type argument vil være MySecretClass
:
public class MySecretClass {
public MySecretClass() {
System.out.println("A MySecretClass object was created successfully!");
}
}
Og her er hvordan vi bruger vores løsning 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();
}
}
Konsoludgang:
A MySecretClass object was created successfully!
Vi har lige givet det påkrævede klasseargument til konstruktøren af vores generiske klasse:
TestClass<MySecretClass> testString = new TestClass<>(MySecretClass.class);
Dette gjorde det muligt for os at gemme informationen om typeargumentet, hvilket forhindrede det i at blive helt slettet. Som et resultat var vi i stand til at skabe enT
objekt! :) Dermed slutter dagens lektion. Du skal altid huske typesletning, når du arbejder med generiske lægemidler. Denne løsning ser ikke særlig praktisk ud, men du bør forstå, at generiske lægemidler ikke var en del af Java-sproget, da det blev oprettet. Denne funktion, som hjælper os med at skabe parametriserede samlinger og fange fejl under kompilering, blev taget på senere. På nogle andre sprog, der inkluderede generiske artikler fra den første version, er der ingen type sletning (f.eks. i C#). I øvrigt er vi ikke færdige med at studere generika! I den næste lektion vil du stifte bekendtskab med et par flere funktioner i generiske lægemidler. Indtil videre ville det være godt at løse et par opgaver! :)
GO TO FULL VERSION