John Squirrels
Nivå
San Francisco

Typ radering

Publicerad i gruppen
Hej! Vi fortsätter vår serie lektioner om generika. Vi har tidigare fått en allmän uppfattning om vad de är och varför de behövs. Idag kommer vi att lära oss mer om några av funktionerna i generika och om att arbeta med dem. Nu går vi! Typ radering - 1I förra lektionen pratade vi om skillnaden mellan generiska typer och råtyper . En råtyp är en generisk klass vars typ har tagits bort.

List list = new ArrayList();
Här är ett exempel. Här anger vi inte vilken typ av föremål som kommer att placeras i vår List. Om vi ​​försöker skapa ett sådant Listoch lägga till några objekt till det, ser vi en varning i IDEA:

"Unchecked call to add(E) as a member of raw type of java.util.List".
Men vi pratade också om det faktum att generika endast dök upp i Java 5. När den här versionen släpptes hade programmerare redan skrivit en massa kod med råtyper, så denna funktion i språket kunde inte sluta fungera, och förmågan att skapa råtyper i Java bevarades. Problemet visade sig dock vara mer utbrett. Som ni vet konverteras Java-kod till ett speciellt kompilerat format som kallas bytecode, som sedan exekveras av den virtuella Java-maskinen. Men om vi lägger in information om typparametrar i bytekoden under omvandlingsprocessen, skulle den bryta all tidigare skriven kod, eftersom det inte fanns några typparametrar före Java 5! När du arbetar med generika finns det ett mycket viktigt koncept som du måste komma ihåg. Det kallas typradering. Det betyder att en klass inte innehåller någon information om en typparameter. Denna information är endast tillgänglig under kompilering och raderas (blir otillgänglig) före körning. Om du försöker lägga in fel typ av objekt i din , List<String>kommer kompilatorn att generera ett fel. Detta är precis vad språkets skapare vill uppnå när de skapade generika: kompileringstidskontroller. Men när all din Java-kod förvandlas till bytekod innehåller den inte längre information om typparametrar. I bytecode List<Cat>är din kattlista inte annorlunda än List<String>strängar. I bytecode står det ingenting att det catsär en lista över Catobjekt. Sådan information raderas under kompileringen — bara det faktum att du har en List<Object> catslista kommer att hamna i programmets bytekod. Låt oss se hur det här fungerar:

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 skapade vår egen generiska TestClassklass. Det är ganska enkelt: det är faktiskt en liten "samling" av 2 objekt, som lagras direkt när objektet skapas. Den har 2 Tfält. När createAndAdd2Values()metoden exekveras måste de två skickade objekten ( Object aoch Object bcastas till Ttypen och sedan läggas till TestClassobjektet. I main()metoden skapar vi en , TestClass<Integer>dvs. Integertypargumentet ersätter Integertypparametern. Vi skickar även a Doubleoch a Stringtill metoden createAndAdd2Values(). Tror du att vårt program kommer att fungera? Vi har trots allt angett Integersom typargument, men en Stringkan definitivt inte gjutas till en Integer! Låt oss köramain()metod och kontroll. Konsolutgång:

22.111 
Test String
Det var oväntat! Varför hände det här? Det är resultatet av typradering. Information om Integertypargumentet som användes för att instansiera vårt TestClass<Integer> testobjekt raderades när koden kompilerades. Fältet blir TestClass<Object> test. Våra Doubleoch Stringargument konverterades lätt till Objectobjekt (de konverteras inte till Integerobjekt som vi förväntade oss!) och lades tyst till TestClass. Här är ett annat enkelt men mycket avslöjande exempel på typradering:

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());

   }
}
Konsolutgång:

true 
true
Det verkar som om vi skapade samlingar med tre olika typer av argument — , , Stringoch Integervår alldeles egna Catklass. Men under konverteringen till bytecode blir alla tre listorna , List<Object>så när programmet körs säger det till oss att vi använder samma klass i alla tre fallen.

Skriv radering när du arbetar med arrayer och generika

Det finns en mycket viktig punkt som måste förstås tydligt när man arbetar med arrayer och generiska klasser (som ) List. Du bör också ta hänsyn till det när du väljer datastrukturer för ditt program. Generika är föremål för typradering. Information om typparametrar är inte tillgänglig under körning. Däremot känner arrayer till och kan använda information om sin datatyp när programmet körs. Om du försöker lägga in en ogiltig typ i en array kommer ett undantag att kastas:

public class Main2 {

   public static void main(String[] args) {

       Object x[] = new String[3];
       x[0] = new Integer(222);
   }
}
Konsolutgång:

Exception in thread "main" java.lang.ArrayStoreException: java.lang.Integer
Eftersom det är så stor skillnad mellan arrayer och generika, kan de ha kompatibilitetsproblem. Framför allt kan du inte skapa en array av generiska objekt eller ens bara en parameteriserad array. Låter det lite förvirrande? Låt oss ta en titt. Du kan till exempel inte göra något av detta i Java:

new List<T>[]
new List<String>[]
new T[]
Om vi ​​försöker skapa en array av List<String>objekt får vi ett kompileringsfel som klagar på generisk array-skapande:

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 varför görs detta? Varför är det inte tillåtet att skapa sådana arrayer? Allt detta för att ge typsäkerhet. Om kompilatorn låter oss skapa sådana arrayer av generiska objekt, skulle vi kunna skapa massor av problem för oss själva. Här är ett enkelt exempel från 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)
}
Låt oss föreställa oss att det List<String>[] stringListsär tillåtet att skapa en array-like och att det inte kommer att generera ett kompileringsfel. Om detta vore sant, här är några saker vi skulle kunna göra: På rad 1 skapar vi en rad listor: List<String>[] stringLists. Vår array innehåller en List<String>. På rad 2 skapar vi en lista med nummer: List<Integer>. På rad 3 tilldelar vi vår List<String>[]till en Object[] objectsvariabel. Java-språket tillåter detta: en array av Xobjekt kan lagra Xobjekt och objekt av alla underklasser X. Följaktligen kan du lägga vad som helst i en Objectarray. På rad 4 ersätter vi det enda elementet i objects()arrayen (a List<String>) med en List<Integer>. Således lägger vi en List<Integer>i en array som bara var avsedd att lagraList<String>föremål! Vi kommer att stöta på ett fel endast när vi kör rad 5. A ClassCastExceptionkommer att kastas under körning. Följaktligen lades ett förbud mot att skapa sådana arrayer till Java. Detta låter oss undvika sådana situationer.

Hur kan jag komma runt typradering?

Tja, vi lärde oss om typradering. Låt oss försöka lura systemet! :) Uppgift: Vi har en generisk TestClass<T>klass. Vi vill skriva en createNewT()metod för den här klassen som kommer att skapa och returnera ett nytt Tobjekt. Men detta är omöjligt, eller hur? All information om Ttypen raderas under kompileringen och vid körning kan vi inte avgöra vilken typ av objekt vi behöver skapa. Det finns faktiskt ett knepigt sätt att göra detta. Du kommer säkert ihåg att Java har en Classklass. Vi kan använda den för att bestämma klassen för något av våra objekt:

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);
   }
}
Konsolutgång:

class java.lang.Integer 
class java.lang.String
Men här är en aspekt som vi inte har pratat om. I Oracle-dokumentationen ser du att klassen Class är generisk! Typ radering - 3

https://docs.oracle.com/javase/8/docs/api/java/lang/Class.html

Dokumentationen säger, "T - typen av klass som modelleras av detta Class-objekt." När vi översätter detta från dokumentationsspråket till rent tal förstår vi att objektets klass Integer.classinte bara är , Classutan snarare Class<Integer>. Objektets typ String.classär inte bara Class, utan snarare , Class<String>etc. Om det fortfarande inte är tydligt, försök att lägga till en typparameter i föregående exempel:

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;
   }
}
Och nu, med hjälp av denna kunskap, kan vi kringgå typradering och utföra vår uppgift! Låt oss försöka få information om en typparameter. Vårt typargument kommer att vara MySecretClass:

public class MySecretClass {

   public MySecretClass() {

       System.out.println("A MySecretClass object was created successfully!");
   }
}
Och så här använder vi vår lösning i praktiken:

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();

   }
}
Konsolutgång:

A MySecretClass object was created successfully!
Vi skickade precis det obligatoriska klassargumentet till konstruktören av vår generiska klass:

TestClass<MySecretClass> testString = new TestClass<>(MySecretClass.class);
Detta gjorde det möjligt för oss att spara informationen om typargumentet, vilket förhindrade att den raderades helt. Som ett resultat kunde vi skapa enTobjekt! :) Med det tar dagens lektion sitt slut. Du måste alltid komma ihåg typradering när du arbetar med generika. Den här lösningen ser inte särskilt bekväm ut, men du bör förstå att generika inte var en del av Java-språket när det skapades. Denna funktion, som hjälper oss att skapa parametriserade samlingar och fånga upp fel under kompileringen, togs på senare. På vissa andra språk som inkluderade generika från den första versionen finns det ingen typradering (till exempel i C#). Förresten, vi är inte klara med att studera generika! I nästa lektion kommer du att bekanta dig med några fler funktioner i generika. För nu skulle det vara bra att lösa ett par uppgifter! :)
Kommentarer
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION