JavaRush /Blog Java /Random-PL /Usuwanie typów

Usuwanie typów

Opublikowano w grupie Random-PL
Cześć! Kontynuujemy cykl wykładów na temat leków generycznych. Wcześniej ustaliliśmy ogólnie, co to jest i dlaczego jest potrzebne. Dzisiaj porozmawiamy o niektórych cechach leków generycznych i przyjrzymy się pewnym pułapkom podczas pracy z nimi. Iść! Usuń typy - 1W ostatnim wykładzie mówiliśmy o różnicy pomiędzy typami ogólnymi i typami surowymi . Jeśli zapomniałeś, Raw Type jest klasą ogólną, z której usunięto jej typ.
List list = new ArrayList();
Oto przykład. Nie określamy tutaj, jaki rodzaj obiektów będzie umieszczany w naszym pliku List. Jeśli spróbujemy go utworzyć Listi dodać do niego jakieś obiekty, w IDEa pojawi się ostrzeżenie:

“Unchecked call to add(E) as a member of raw type of java.util.List”.
Ale rozmawialiśmy też o tym, że typy generyczne pojawiły się tylko w wersji języka Java 5. Do czasu jej wydania programiści napisali dużo kodu przy użyciu typów surowych, aby nie przestał działać, możliwość tworzenie i praca z typami surowymi w Javie została zachowana. Problem ten okazał się jednak znacznie szerszy. Jak wiadomo, kod Java jest konwertowany na specjalny kod bajtowy, który jest następnie wykonywany przez wirtualną maszynę Java. A jeśli podczas procesu tłumaczenia umieścimy w kodzie bajtowym informacje o typach parametrów, zepsuje to cały wcześniej napisany kod, ponieważ przed Javą 5 nie istniały żadne typy parametrów! Pracując z lekami generycznymi, należy pamiętać o jednej bardzo ważnej funkcji. Nazywa się to wymazywaniem typu. Jej istota polega na tym, że wewnątrz klasy nie jest przechowywana żadna informacja o typie jej parametru. Informacje te są dostępne jedynie na etapie kompilacji i są usuwane (stają się niedostępne) w czasie wykonywania. Jeśli spróbujesz umieścić obiekt niewłaściwego typu w swoim List<String>kompilator zgłosi błąd. To właśnie osiągnęli twórcy języka, tworząc generyki – sprawdzenia na etapie kompilacji. Ale kiedy cały napisany kod Java zamieni się w kod bajtowy, nie będzie żadnych informacji o typach parametrów. Wewnątrz kodu bajtowego lista List<Cat>kotów nie będzie się różnić od List<String>ciągów znaków. Nic w kodzie bajtowym nie będzie mówiło, że catsjest to lista obiektów Cat. Informacje na ten temat zostaną usunięte podczas kompilacji, a do kodu bajtowego trafi jedynie informacja, że ​​masz w programie określoną listę List<Object> cats. 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ą klasę generyczną TestClass. Jest to dość proste: w zasadzie jest to mały „zbiór” 2 obiektów, które są tam umieszczane natychmiast po utworzeniu obiektu. Ma 2 obiekty jako pola T. Po wykonaniu metody createAndAdd2Values()dwa przekazane obiekty należy rzucić Object ana Object bnasz typ T, po czym zostaną dodane do obiektu TestClass. W metodzie , main()którą tworzymy TestClass<Integer>, to znaczy w jakości, Tktórą będziemy mieli Integer. Ale jednocześnie createAndAdd2Values()przekazujemy do metody liczbę Doublei obiekt String. Czy sądzisz, że nasz program się sprawdzi? W końcu określiliśmy jako typ parametru Integer, ale Stringz pewnością nie można go rzutować na Integer! Uruchommy metodę main()i sprawdźmy. Dane wyjściowe konsoli: 22.111 Ciąg testowy Nieoczekiwany wynik! Dlaczego się to stało? Właśnie z powodu usunięcia typu. Podczas kompilacji kodu została usunięta informacja o typie parametru Integernaszego obiektu . TestClass<Integer> testZamienił się w TestClass<Object> test. Nasze parametry zostały bez problemu przekształcone w Double( a nie w , jak się spodziewaliśmy!) i po cichu dodane do . Oto kolejny prosty, ale bardzo ilustrujący przykład usuwania typów: StringObjectIntegerTestClass
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());

   }
}
Dane wyjściowe konsoli: true true Wygląda na to, że stworzyliśmy kolekcje z trzema różnymi typami parametrów - String, Integeri utworzoną przez nas klasą Cat. Ale podczas konwersji na kod bajtowy wszystkie trzy listy zmieniły się w List<Object>, więc po uruchomieniu program informuje nas, że we wszystkich trzech przypadkach używamy tej samej klasy.

Wpisz erasure podczas pracy z tablicami i typami rodzajowymi

Jest jeden bardzo ważny punkt, który należy jasno zrozumieć podczas pracy z tablicami i typami rodzajowymi (na przykład List). Warto to również rozważyć przy wyborze struktury danych dla swojego programu. Typy generyczne podlegają wymazaniu. Informacja o typie parametru nie jest dostępna w trakcie wykonywania programu. Natomiast tablice znają i mogą wykorzystywać informacje o swoim typie danych podczas wykonywania programu. Próba umieszczenia wartości niewłaściwego typu w tablicy spowoduje wygenerowanie 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 tablicami rodzajowymi, mogą wystąpić problemy ze zgodnością. Po pierwsze, nie można utworzyć tablicy obiektów ogólnych ani nawet tablicy z określonym typem. Brzmi trochę mylące? Przyjrzyjmy się bliżej. 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 List<String>, otrzymamy ogólny błąd kompilacji tworzenia tablicy:
import java.util.List;

public class Main2 {

   public static void main(String[] args) {

       //ошибка компиляции! Generic array creation
       List<String>[] stringLists = new List<String>[1];
   }
}
Ale dlaczego to zrobiono? Dlaczego tworzenie takich tablic jest zabronione? Wszystko po to, aby zapewnić bezpieczeństwo typu. Gdyby kompilator pozwolił nam na utworzenie takich tablic z obiektów ogólnych, moglibyśmy mieć mnóstwo kłopotów. Oto prosty przykład z książki Joshuy Blocha „Efektywna 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 tworzenie tablic List<String>[] stringListsbyłoby dozwolone, a kompilator nie narzekałby. Oto, co moglibyśmy zrobić w tym przypadku: W linii 1 tworzymy tablicę arkuszy List<String>[] stringLists. Nasza tablica zawiera jeden List<String>. W linii 2 tworzymy listę liczb List<Integer>. W linii 3 przypisujemy naszą tablicę List<String>[]do zmiennej Object[] objects. Język Java pozwala na to: Xmożesz umieścić zarówno obiekty X, jak i obiekty wszystkich klas podrzędnych w tablicy obiektów Х. W związku z tym Objectsmożesz umieścić w tablicy wszystko. W linii 4. zastępujemy pojedynczy element tablicy objects (List<String>)listą List<Integer>. W rezultacie umieściliśmy List<Integer>w naszej tablicy, która była przeznaczona wyłącznie do przechowywania List<String>! Błąd napotkamy dopiero, gdy kod dotrze do linii 5. Podczas wykonywania programu zostanie zgłoszony wyjątek ClassCastException. Dlatego do języka Java wprowadzono zakaz tworzenia takich tablic – pozwala to uniknąć takich sytuacji.

Jak mogę ominąć usuwanie typu?

Cóż, dowiedzieliśmy się o wymazywaniu typów. Spróbujmy oszukać system! :) Zadanie: Mamy klasę ogólną TestClass<T>. Musimy stworzyć w nim metodę createNewT(), która utworzy i zwróci nowy obiekt typu Т. Ale tego nie da się zrobić, prawda? Wszystkie informacje o typie Тzostaną usunięte podczas kompilacji, a podczas działania programu nie będziemy w stanie dowiedzieć się, jakiego rodzaju obiekt musimy utworzyć. W rzeczywistości jest jeden trudny sposób. Prawdopodobnie pamiętasz, że w Javie istnieje klasa Class. Za jego pomocą możemy uzyskać klasę 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 jedna funkcja, o której nie rozmawialiśmy. W dokumentacji Oracle zobaczysz, że Klasa jest klasą ogólną! Usuń typy - 3Dokumentacja mówi: „T to typ klasy modelowany przez ten obiekt klasy”. Jeśli przetłumaczymy to z języka dokumentacji na język ludzki, oznacza to, że klasa obiektu Integer.classto nie tylko Class, ale Class<Integer>. Typ obiektu string.classto nie tylko Class, Class<String>, itp. Jeśli nadal nie jest jasne, spróbuj dodać parametr typu do poprzedniego przykładu:
public class Main2 {

   public static void main(String[] args) {

       Class<Integer> classInt = Integer.class;
       //ошибка компиляции!
       Class<String> classInt2 = Integer.class;


       Class<String> classString = String.class;
       //ошибка компиляции!
       Class<Double> classString2 = String.class;
   }
}
A teraz korzystając z tej wiedzy możemy ominąć kasowanie typów i rozwiązać nasz problem! Spróbujmy uzyskać informację o typie parametru. Jej rolę pełnić będzie klasa MySecretClass:
public class MySecretClass {

   public MySecretClass() {

       System.out.println("Объект секретного класса успешно создан!");
   }
}
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:

Объект секретного класса успешно создан!
Po prostu przekazaliśmy wymagany parametr klasy do konstruktora naszej klasy ogólnej:
TestClass<MySecretClass> testString = new TestClass<>(MySecretClass.class);
Dzięki temu zapisaliśmy informację o typie parametru i zabezpieczyliśmy ją przed usunięciem. W rezultacie udało nam się stworzyć obiekt T! :) Na tym zakończymy dzisiejszy wykład. Podczas pracy z typami generycznymi zawsze należy pamiętać o wymazywaniu typów. Nie wygląda to zbyt wygodnie, ale musisz zrozumieć, że typy generyczne nie były częścią języka Java w momencie jego tworzenia. Jest to później dodana funkcja, która pomaga nam tworzyć kolekcje z typem i wychwytywać błędy na etapie kompilacji. Niektóre inne języki, w których języki generyczne istnieją od wersji 1, nie mają możliwości wymazywania typów (na przykład C#). Jednak nie skończyliśmy studiować leków generycznych! W następnym wykładzie poznasz jeszcze kilka możliwości pracy z nimi. W międzyczasie miło byłoby rozwiązać kilka problemów! :)
Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION