здрасти Продължаваме нашата серия от уроци по генерични лекарства. Преди това получихме обща представа Howво представляват и защо са необходими. Днес ще научим повече за някои от характеристиките на генеричните лекарства и за работата с тях. Да тръгваме! Тип изтриване - 1В последния урок говорихме за разликата между генеричните типове и необработените типове . Суровият тип е общ клас, чийто тип е премахнат.

List list = new ArrayList();
Ето един пример. Тук не посочваме Howъв тип обекти ще бъдат поставени в нашия List. Ако се опитаме да създадем такъв Listи добавим някои обекти към него, ще видим предупреждение в IDEA:

"Unchecked call to add(E) as a member of raw type of java.util.List".
Но също така говорихме за факта, че генериците се появиха само в Java 5. По времето, когато тази version беше пусната, програмистите вече бяха написали куп code, използвайки необработени типове, така че тази функция на езика не можеше да спре да работи и възможността да създаване на необработени типове в Java беше запазено. Проблемът обаче се оказа по-масов. Както знаете, codeът на Java се преобразува в специален компorран формат, наречен bytecode, който след това се изпълнява от виртуалната машина на Java. Но ако поставим информация за параметрите на типа в byte codeа по време на процеса на преобразуване, това ще наруши целия написан преди това code, защото не е имало параметри на типа преди Java 5! Когато работите с генерични лекарства, има една много важна концепция, която трябва да запомните. Нарича се изтриване на типа. Това означава, че класът не съдържа информация за параметър на типа. Тази информация е достъпна само по време на компилация и се изтрива (става недостъпна) преди време на изпълнение. Ако се опитате да поставите грешен тип обект във вашия List<String>, компилаторът ще генерира грешка. Точно това искат да постигнат създателите на езика, когато създадоха генерични codeове: проверки по време на компилация. Но когато целият ви Java code се превърне в byte code, той вече не съдържа информация за параметрите на типа. В byte code вашият List<Cat>списък с котки не се различава от List<String>низове. В byte codeа нищо не казва, че catsе списък от Catобекти. Такава информация се изтрива по време на компилация — само фактът, че имате списък, List<Object> catsще се окаже в byte codeа на програмата. Да видим How работи това:

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();
   }
}
Създадохме наш собствен общ TestClassклас. Много е просто: това всъщност е малка "колекция" от 2 обекта, които се съхраняват веднага след създаването на обекта. Има 2 Tполета. Когато createAndAdd2Values()методът се изпълни, двата предадени обекта ( Object aи Object bтрябва да бъдат прехвърлени към Tтипа и след това добавени към TestClassобекта. В main()метода създаваме TestClass<Integer>, т.е. Integerаргументът на типа замества Integerпараметъра на типа. Ние също предаваме a Doubleи a Stringна метода createAndAdd2Values(). Мислите ли, че нашата програма ще работи? В края на краищата ние посочихме Integerкато аргумент тип, но a Stringопределено не може да се преобразува в Integer! Нека изпълнимmain()метод и проверка. Конзолен изход:

22.111 
Test String
Това беше неочаквано! Защо се случи това? Това е резултат от изтриване на типа. Информацията за Integerаргумента тип, използван за инстанциране на нашия TestClass<Integer> testобект, беше изтрита, когато codeът беше компorран. Полето става TestClass<Object> test. Нашите Doubleи Stringаргументи бяха лесно преобразувани в Objectобекти (те не се преобразуват в Integerобекти, Howто очаквахме!) и тихо добавени към TestClass. Ето още един прост, но много показателен пример за изтриване на типа:

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

   }
}
Конзолен изход:

true 
true
Изглежда сме създали колекции с три различни типа аргументи — String, Integerи нашия собствен Catклас. Но по време на преобразуването в byte code и трите списъка стават List<Object>, така че когато програмата се изпълнява, тя ни казва, че използваме един и същ клас и в трите случая.

Изтриване на тип при работа с масиви и генерични файлове

Има един много важен момент, който трябва да бъде ясно разбран, когато работите с масиви и общи класове (като List). Трябва също да го вземете предвид, когато избирате структури от данни за вашата програма. Генеричните продукти подлежат на изтриване на типа. Информацията за параметрите на типа не е налична по време на изпълнение. Обратно, масивите знаят и могат да използват информация за своя тип данни, когато програмата работи. Опитът за поставяне на невалиден тип в масив ще доведе до хвърляне на изключение:

public class Main2 {

   public static void main(String[] args) {

       Object x[] = new String[3];
       x[0] = new Integer(222);
   }
}
Конзолен изход:

Exception in thread "main" java.lang.ArrayStoreException: java.lang.Integer
Тъй като има толкова голяма разлика между масивите и генеричните, те може да имат проблеми със съвместимостта. Преди всичко не можете да създадете масив от общи обекти or дори само параметризиран масив. Звучи ли малко объркващо? Нека да разгледаме. Например, не можете да направите нищо от това в Java:

new List<T>[]
new List<String>[]
new T[]
Ако се опитаме да създадем масив от List<String>обекти, получаваме грешка при компилация, която се оплаква от създаването на общ масив:

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];
   }
}
Но защо се прави това? Защо не е разрешено създаването на такива масиви? Това е всичко, за да се осигури безопасност на типа. Ако компилаторът ни позволи да създадем такива масиви от генерични обекти, бихме могли да си създадем много проблеми. Ето един прост пример от книгата на Джошуа Блок "Ефективна 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)
}
Нека си представим, че създаването на масив като List<String>[] stringListsе разрешено и няма да генерира грешка при компorране. Ако това беше вярно, ето някои неща, които бихме могли да направим: В ред 1 създаваме масив от списъци: List<String>[] stringLists. Нашият масив съдържа един List<String>. В ред 2 създаваме списък с числа: List<Integer>. В ред 3 присвояваме нашето List<String>[]на Object[] objectsпроменлива. Езикът Java позволява това: масив от Xобекти може да съхранява Xобекти и обекти от всички подкласове X. Съответно можете да поставите всичко в Objectмасив. В ред 4 заместваме единствения елемент от objects()масива (a List<String>) с a List<Integer>. По този начин поставяме a List<Integer>в масив, който е предназначен само за съхранениеList<String>обекти! Ще срещнем грешка само когато изпълним ред 5. A ClassCastExceptionще бъде хвърлен по време на изпълнение. Съответно в Java беше добавена забрана за създаване на такива масиви. Това ни позволява да избягваме подобни ситуации.

Как мога да заобиколя изтриването на типа?

Е, научихме за изтриването на типове. Нека се опитаме да излъжем системата! :) Задача: Имаме генеричен TestClass<T>клас. Искаме да напишем createNewT()метод за този клас, който ще създаде и върне нов Tобект. Но това е невъзможно, нали? Цялата информация за Tтипа се изтрива по време на компилация и по време на изпълнение не можем да определим Howъв тип обект трябва да създадем. Всъщност има един труден начин да направите това. Вероятно си спомняте, че Java има Classклас. Можем да го използваме, за да определим класа на всеки от нашите обекти:

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);
   }
}
Конзолен изход:

class java.lang.Integer 
class java.lang.String
Но ето един аспект, за който не сме говорor. В documentацията на Oracle ще видите, че класът Class е общ! Тип изтриване - 3

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

Документацията казва: "T - типът на класа, моделиран от този обект на клас." Превеждайки това от езика на documentацията на обикновена реч, разбираме, че класът на Integer.classобекта не е просто Class, а по-скоро Class<Integer>. Типът на String.classобекта не е просто Class, а по-скоро Class<String>и т.н. Ако все още не е ясно, опитайте да добавите тип параметър към предишния пример:

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;
   }
}
И сега, използвайки това знание, можем да заобиколим изтриването на типове и да изпълним задачата си! Нека се опитаме да получим информация за параметър тип. Нашият аргумент тип ще бъде MySecretClass:

public class MySecretClass {

   public MySecretClass() {

       System.out.println("A MySecretClass object was created successfully!");
   }
}
И ето How използваме нашето решение на практика:

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

   }
}
Конзолен изход:

A MySecretClass object was created successfully!
Току-що предадохме необходимия аргумент на класа на конструктора на нашия общ клас:

TestClass<MySecretClass> testString = new TestClass<>(MySecretClass.class);
Това ни позволи да запазим информацията за аргумента тип, предотвратявайки пълното му изтриване. В резултат на това успяхме да създадем aTобект! :) С това днешният урок приключва. Винаги трябва да помните изтриването на типа, когато работите с генерични продукти. Това решение не изглежда много удобно, но трябва да разберете, че генериците не са бor част от езика Java, когато е бил създаден. Тази функция, която ни помага да създаваме параметризирани колекции и да улавяме грешки по време на компилация, беше добавена по-късно. В някои други езици, които включват генерични codeове от първата version, няма изтриване на типове (например в C#). Между другото, не сме приключor с изучаването на генеричните лекарства! В следващия урок ще се запознаете с още няколко характеристики на генериците. Засега би било добре да решите няколко задачи! :)