CodeGym/Blog Java/Random-PL/Wymazanie typu
Autor
Andrey Gorkovenko
Frontend Engineer at NFON AG

Wymazanie typu

Opublikowano w grupie Random-PL
Cześć! Kontynuujemy naszą serię lekcji na temat leków generycznych. Wcześniej mieliśmy ogólne pojęcie o tym, czym one są i dlaczego są potrzebne. Dzisiaj dowiemy się więcej o niektórych funkcjach typów generycznych io pracy z nimi. Chodźmy! Wymazywanie typu - 1W ostatniej lekcji mówiliśmy o różnicy między typami ogólnymi a typami surowymi . Typ surowy to klasa ogólna, której typ został usunięty.
List list = new ArrayList();
Oto przykład. Tutaj nie wskazujemy jakiego typu obiekty będą umieszczane w naszym List. Jeśli spróbujemy stworzyć taki Listi dodać do niego jakieś obiekty, w IDEA zobaczymy ostrzeżenie:
"Unchecked call to add(E) as a member of raw type of java.util.List".
Ale rozmawialiśmy też o tym, że generyki pojawiły się dopiero w Javie 5. Do czasu wydania tej wersji programiści napisali już sporo kodu przy użyciu surowych typów, więc ta funkcja języka nie mogła przestać działać, a możliwość tworzenie surowych typów w Javie zostało zachowane. Problem okazał się jednak bardziej powszechny. Jak wiesz, kod Java jest konwertowany do specjalnego skompilowanego formatu zwanego kodem bajtowym, który jest następnie wykonywany przez wirtualną maszynę Java. Ale gdybyśmy umieścili informacje o parametrach typu w kodzie bajtowym podczas procesu konwersji, zepsułoby to cały wcześniej napisany kod, ponieważ przed Javą 5 nie było parametrów typu! Podczas pracy z lekami generycznymi należy pamiętać o jednej bardzo ważnej koncepcji. Nazywa się to wymazywaniem czcionek. Oznacza to, że klasa nie zawiera informacji o parametrze typu. Informacje te są dostępne tylko podczas kompilacji i są usuwane (stają się niedostępne) przed uruchomieniem. Jeśli spróbujesz umieścić niewłaściwy typ obiektu w swoim pliku List<String>, kompilator wygeneruje błąd. To jest dokładnie to, co twórcy języka chcą osiągnąć, tworząc generyczne: kontrole w czasie kompilacji. Ale kiedy cały twój kod Java zamienia się w kod bajtowy, nie zawiera już informacji o parametrach typu. W kodzie bajtowym twoja List<Cat>lista kotów nie różni się od List<String>łańcuchów. W kodzie bajtowym nic nie mówi, że catsjest to lista Catobiektów. Takie informacje są usuwane podczas kompilacji — tylko fakt, że masz listę, List<Object> catsznajdzie się w kodzie bajtowym programu. Zobaczmy, jak to działa:
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();
   }
}
Stworzyliśmy własną TestClassklasę generyczną. To całkiem proste: w rzeczywistości jest to mała „kolekcja” 2 obiektów, które są przechowywane natychmiast po utworzeniu obiektu. Posiada 2 Tpola. Kiedy createAndAdd2Values()metoda jest wykonywana, dwa przekazane obiekty ( Object ai Object bmuszą być rzutowane na Ttyp, a następnie dodane do TestClassobiektu. W main()metodzie tworzymy TestClass<Integer>, tj. Integerargument typu zastępuje Integerparametr typu. Przekazujemy również a Doublei a Stringdo metoda createAndAdd2Values(). Myślisz, że nasz program zadziała? W końcu podaliśmy Integerjako argument typ, ale Stringzdecydowanie nie można rzutować na an Integer! Uruchommymain()sposób i sprawdź. Wyjście konsoli:
22.111
Test String
To było niespodziewane! Dlaczego się to stało? To wynik wymazywania czcionek. Informacje o Integerargumencie typu użytym do utworzenia instancji naszego TestClass<Integer> testobiektu zostały usunięte podczas kompilacji kodu. Pole staje się TestClass<Object> test. Argumenty nasze Doublei Stringzostały łatwo przekonwertowane na Objectobiekty (nie są konwertowane na Integerobiekty, jak się spodziewaliśmy!) i po cichu dodane do TestClass. Oto kolejny prosty, ale bardzo wymowny przykład wymazywania czcionek:
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());

   }
}
Wyjście konsoli:
true
true
Wygląda na to, że stworzyliśmy kolekcje z argumentami trzech różnych typów — String, Integer, i naszą własną Catklasą. Ale podczas konwersji na kod bajtowy wszystkie trzy listy stają się List<Object>, więc po uruchomieniu programu informuje nas, że używamy tej samej klasy we wszystkich trzech przypadkach.

Wymazywanie tekstu podczas pracy z tablicami i rodzajami

Istnieje bardzo ważny punkt, który należy jasno zrozumieć podczas pracy z tablicami i klasami ogólnymi (takimi jak List). Powinieneś również wziąć to pod uwagę przy wyborze struktur danych dla swojego programu. Generyki podlegają wymazywaniu czcionek. Informacje o parametrach typu nie są dostępne w czasie wykonywania. Natomiast tablice wiedzą o swoim typie danych i mogą z niego korzystać, gdy program jest uruchomiony. Próba umieszczenia nieprawidłowego typu w tablicy spowoduje zgłoszenie wyjątku:
public class Main2 {

   public static void main(String[] args) {

       Object x[] = new String[3];
       x[0] = new Integer(222);
   }
}
Wyjście konsoli:
Exception in thread "main" java.lang.ArrayStoreException: java.lang.Integer
Ponieważ istnieje tak duża różnica między tablicami a rodzajami, mogą one mieć problemy ze zgodnością. Przede wszystkim nie można utworzyć tablicy obiektów ogólnych ani nawet tablicy sparametryzowanej. Czy to brzmi trochę myląco? Spójrzmy. Na przykład nie możesz tego zrobić w Javie:
new List<T>[]
new List<String>[]
new T[]
Jeśli spróbujemy utworzyć tablicę List<String>obiektów, otrzymamy błąd kompilacji, który narzeka na ogólne tworzenie tablicy:
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];
   }
}
Ale dlaczego to się robi? Dlaczego tworzenie takich tablic jest niedozwolone? Wszystko to ma na celu zapewnienie bezpieczeństwa typu. Gdyby kompilator pozwolił nam tworzyć takie tablice ogólnych obiektów, moglibyśmy zrobić sobie mnóstwo problemów. Oto prosty przykład z książki Joshua Blocha „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)
}
Wyobraźmy sobie, że utworzenie tablicy typu like List<String>[] stringListsjest dozwolone i nie spowoduje błędu kompilacji. Gdyby to była prawda, oto kilka rzeczy, które moglibyśmy zrobić: W linii 1 tworzymy tablicę list: List<String>[] stringLists. Nasza tablica zawiera jeden List<String>. W linii 2 tworzymy listę liczb: List<Integer>. W linii 3 przypisujemy nasze List<String>[]do Object[] objectszmiennej. Język Java na to pozwala: tablica Xobiektów może przechowywać Xobiekty i obiekty wszystkich podklas X. W związku z tym w tablicy można umieścić wszystko Object. W linii 4 zamieniamy jedyny element tablicy objects()(a List<String>) na List<Integer>. W ten sposób umieściliśmy a List<Integer>w tablicy, która była przeznaczona tylko do przechowywaniaList<String>obiekty! Napotkamy błąd tylko wtedy, gdy wykonamy linię 5. A ClassCastExceptionzostanie wyrzucone w czasie wykonywania. W związku z tym do Javy dodano zakaz tworzenia takich tablic. Dzięki temu unikniemy takich sytuacji.

Jak obejść wymazywanie czcionek?

Cóż, dowiedzieliśmy się o wymazywaniu czcionek. Spróbujmy oszukać system! :) Zadanie: Mamy TestClass<T>klasę ogólną. Chcemy napisać createNewT()metodę dla tej klasy, która utworzy i zwróci nowy Tobiekt. Ale to jest niemożliwe, prawda? Wszystkie informacje o Ttypie są usuwane podczas kompilacji, aw czasie wykonywania nie możemy określić, jaki typ obiektu musimy utworzyć. Właściwie jest na to jeden trudny sposób. Zapewne pamiętasz, że Java ma Classklasę. Możemy go użyć do określenia klasy dowolnego z naszych obiektów:
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);
   }
}
Wyjście konsoli:
class java.lang.Integer
class java.lang.String
Ale jest jeden aspekt, o którym nie mówiliśmy. W dokumentacji Oracle zobaczysz, że klasa Class jest ogólna! Wymazywanie czcionek — 3

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

Dokumentacja mówi: „T - typ klasy modelowanej przez ten obiekt klasy”. Przekładając to z języka dokumentacji na zwykłą mowę, rozumiemy, że klasa obiektu Integer.classto nie tylko Class, ale raczej Class<Integer>. Typ obiektu String.classto nie tylko Class, ale raczej Class<String>, itp. Jeśli nadal nie jest to jasne, spróbuj dodać parametr typu do poprzedniego przykładu:
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;
   }
}
A teraz, korzystając z tej wiedzy, możemy ominąć wymazywanie czcionek i wykonać nasze zadanie! Spróbujmy uzyskać informacje o parametrze typu. Naszym argumentem typu będzie MySecretClass:
public class MySecretClass {

   public MySecretClass() {

       System.out.println("A MySecretClass object was created successfully!");
   }
}
A oto jak wykorzystujemy nasze rozwiązanie w praktyce:
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();

   }
}
Wyjście konsoli:
A MySecretClass object was created successfully!
Właśnie przekazaliśmy wymagany argument klasy do konstruktora naszej klasy ogólnej:
TestClass<MySecretClass> testString = new TestClass<>(MySecretClass.class);
Pozwoliło nam to zachować informacje o argumencie typu, zapobiegając jego całkowitemu wymazaniu. Dzięki temu udało nam się stworzyć tzwTobiekt! :) Na tym dzisiejsza lekcja dobiega końca. Podczas pracy z rodzajami należy zawsze pamiętać o wymazywaniu czcionek. To obejście nie wygląda na zbyt wygodne, ale powinieneś zrozumieć, że generyczne nie były częścią języka Java, kiedy został stworzony. Ta funkcja, która pomaga nam tworzyć sparametryzowane kolekcje i wychwytywać błędy podczas kompilacji, została dołączona później. W niektórych innych językach, które zawierały generyczne z pierwszej wersji, nie ma wymazywania typu (na przykład w języku C#). Nawiasem mówiąc, nie skończyliśmy studiować generyków! W następnej lekcji zapoznasz się z kilkoma dodatkowymi cechami generyków. Na razie dobrze byłoby rozwiązać kilka zadań! :)
Komentarze
  • Popularne
  • Najnowsze
  • Najstarsze
Musisz się zalogować, aby dodać komentarz
Ta strona nie ma jeszcze żadnych komentarzy