Цель этой статьи состоит в том, чтобы представить дженерики Java, чтобы каждый мог иметь окончательное, четкое и точное понимание всех аспектов дженериков Java, а также заложить основу для следующей статьи «Повторное понимание размышлений о Java».
Введение
Дженерики являются очень важной точкой знания в Java. Дженерики широко используются в фреймворках Java Collection. В этой статье мы рассмотрим дизайн дженериков Java с нуля, который будет включать обработку подстановочных знаков и стирание печати.
Общие основы
Общие классы
Давайте сначала определим простой класс коробки:
Public Class Box {Private String Object; public void set (string object) {this.object = object; } public String get () {return Object; }}Это самая распространенная практика. Одним из недостатков этого является то, что в коробке могут быть загружены только элементы строкового типа. В будущем, если нам нужно загружать другие типы элементов, таких как целое число, мы также должны переписать другую коробку. Код не может быть использован повторно, и использование Generics может хорошо решить эту проблему.
открытый класс Box <t> {// t означает «тип» частного T T; public void set (t t) {this.t = t; } public t get () {return t; }}Таким образом, наш класс коробки может быть использован повторно, и мы можем заменить T на любой тип, который мы хотим:
Box <integer> integerbox = new Box <integer> (); Box <way> doublebox = new Box <way> (); Box <string> stringbox = new Box <string> ();
Общие методы
Прочитав общий класс, давайте узнаем о общих методах. Объявление общего метода прост, просто добавьте форму, похожую на <k, v> к типу возврата:
открытый класс util {public static <K, v> boolean compare (pair <k, v> p1, pair <k, v> p2) {return p1.getkey (). equals (p2.getkey ()) && p1.getValue (). equals (p2.getValue ()); }} public Class Pare <K, V> {private K Key; частное значение V; public pair (k key, v value) {this.key = key; this.value = значение; } public void setKey (k key) {this.key = key; } public void setValue (v value) {this.value = value; } public k getKey () {return key; } public v getValue () {return Value; }}Мы можем назвать общие методы, подобные этим:
Pair <Integer, String> p1 = новая пара <> (1, "Apple"); pair <integer, string> p2 = new pair <> (2, "pear"); boolean some = util. <Integer, string> compare (p1, p2);
Или используйте вывод типа в Java 1.7/1.8, чтобы дать Java автоматически вывести соответствующие параметры типа:
Pair <Integer, String> p1 = новая пара <> (1, "Apple"); pair <integer, string> p2 = новая пара <> (2, "Pear"); Boolean Some = util.compare (p1, p2);
Граничный символ
Теперь мы хотим реализовать такую функцию, чтобы найти количество элементов в общем массиве, которые больше, чем конкретный элемент. Мы можем реализовать это так:
public static <t> int countgreaterthan (t [] anarray, t elem) {int count = 0; для (t e: anarray) if (e> elem) // ошибка компилятора ++ count; возврат count;}Но это, очевидно, неправильно, потому что, за исключением примитивных типов, таких как короткие, int, двойной, длинный, плавающий, байт, чар и т. Д., Другие классы не обязательно могут использовать операторы>, поэтому компилятор сообщает об ошибке. Как решить эту проблему? Ответ - использовать граничный символ.
Общедоступный интерфейс сопоставимо <t> {public int compareto (t o);}Сделайте объявление, аналогичное следующему, что эквивалентно сообщению компилятору, что параметр типа T представляет классы, которые реализуют сопоставимый интерфейс, что эквивалентно сообщению компилятору, что все они реализуют, по крайней мере, метод сравнения.
public static <T расширяется сопоставимо <t>> int countgreaterthan (t [] anarray, t elem) {int count = 0; для (t e: anarray) if (e.compareto (elem)> 0) ++ count; возврат count;}Подстановочный знак
Прежде чем понимать подстановочные знаки, мы должны сначала прояснить концепцию или одолжить класс коробки, которые мы определили выше, предположим, что мы добавляем подобный метод:
public void boxtest (box <число> n) { / * ... * /}Итак, какой тип параметров позволяет принять Box <cumber> n? Можем ли мы пройти в Box <Integer> или Box <wo Double>? Ответ нет. Хотя Integer и Double являются подклассами числа, между Box <Integer> или Box <Double> и Box <Номер> нет отношения. Это очень важно, и мы будем использовать полный пример, чтобы углубить наше понимание.
Во -первых, мы определяем несколько простых классов, и мы будем использовать их ниже:
Класс Fruit {} класс Apple расширяет Fruit {} Class Orange Extens Fruit {}В следующем примере мы создаем общего считывателя класса, а затем в f1 (), когда мы пробуем Fruit f = FruitReader.readExact (яблоки); Компилятор сообщит об ошибке, потому что между списком <Fruit> и списком <Apple> нет никаких отношений.
открытый класс GenericReading {Статический список <pplef> apples = arrays.aslist (new Apple ()); Статический список <fruit> fruit = arrays.aslist (new Fruit ()); Статический класс Reader <t> {t readexact (list <t> list) {return list.get (0); }} static void f1 () {reader <Fruit> fruitReader = new Reader <Fruit> (); // Ошибки: список <Fruit> не может быть применен к списку <pple>. // Fruit F = FruitReader.readExact (яблоки); } public static void main (string [] args) {f1 (); }}Но, согласно нашим обычным привычкам мышления, между яблоком и фруктами должна быть связь, но компилятор не может его распознать. Итак, как я могу решить эту проблему в общем коде? Мы можем решить эту проблему с помощью подстановочных знаков:
Статический класс CoviarionTreader <t> {t redtcovariant (list <? Extends t> list) {return list.get (0); }} static void f2 () {coviarionTreader <Fruit> FruitReader = new CoviarionTreader <Fruit> (); Fruit F = FruitReader.ReadCovariant (фрукты); Fruit A = FruitReader.readCoviariant (яблоки);} public static void main (string [] args) {f2 ();}Это очень похоже на то, чтобы сообщить компилятору, что параметры, принятые методом чтения FruitReader, столько же, сколько подкласс, который удовлетворяет плодам (включая сам фрукты), так что связь между подклассом и родительским классом также связана.
Принципы PECS
Мы видели использование, похожее на <? расширяет T> выше. Используя его, мы можем получить элементы из списка, так что можем ли мы добавить элементы в список? Попробуем:
открытый класс genericsandcovariance {public static void main (string [] args) {// подстановочные знаки разрешают ковариацию: список <? Extens Fruit> Flist = New ArrayList <pple> (); // Скомпилируйте ошибку: не удается добавить любой тип объекта: // flist.add (new Apple ()) // flist.add (new Orange ()) // flist.add (new Fruit ()) // flist.add (new Object ()) flist.add (null); // законные, но неинтересные // Мы знаем, что он возвращает хотя бы фрукты: фрукты f = flist.get (0); }}Ответ нет, компилятор Java не позволяет нам сделать это, почему? Мы могли бы также рассмотреть эту проблему с точки зрения компилятора. Потому что список <? Распространяет фрукты> Flist может иметь много значений:
Список <? Extens Fruit> Flist = new ArrayList <fruit> (); список <? Extens Fruit> Flist = New ArrayList <pple> (); список <? расширяет фрукты> flist = new ArrayList <Orange> ();
Поэтому для классов сбора, которые реализуют <? Extends T>, их можно рассматривать только как производитель, предоставляющий (получить) элемент снаружи, и не может использоваться в качестве потребителя для получения (добавления) элементов снаружи.
Что мы должны делать, если хотим добавить элемент? Вы можете использовать <? Super t>:
открытый класс genericWriting {static list <pplef> apples = new ArrayList <pple> (); Статический список <Fruit> Fruit = новый ArrayList <Fruit> (); static <t> void writexact (list <t> list, t item) {list.add (item); } static void f1 () {writeExact (Apples, new Apple ()); WriteExact (Fruit, New Apple ()); } static <t> void writewithWildCard (list <? Super t> list, t item) {list.add (item)} static void f2 () {writeWithWildCard (Apples, new Apple ()); writewithwildcard (Fruit, New Apple ()); } public static void main (string [] args) {f1 (); f2 (); }}Таким образом, мы можем добавить элементы в контейнер, но недостаток использования Super заключается в том, что мы не можем получить элементы в контейнере в будущем. Причина очень проста. Мы продолжаем рассматривать этот вопрос с точки зрения компилятора. Для списка <? Super Apple> Список, он может иметь следующие значения:
Список <? Super Apple> List = new ArrayList <pple> (); List <? Super Apple> List = new ArrayList <fruit> (); List <? Super Apple> List = new ArrayList <Object> ();
Когда мы пытаемся получить яблоко через список, мы можем получить фрукты, которые могут быть другими видами фруктов, таких как апельсин.
Основываясь на примере выше, мы можем суммировать правило: «Производитель Extens, Consumer Super»:
После прочтения некоторого исходного кода Java Collections, мы можем обнаружить, что мы обычно используем их вместе, например, следующее:
открытые коллекции классов {public static <t> void copy (list <? Super t> dest, list <? Extends t> src) {for (int i = 0; i <src.size (); i ++) dest.set (i, src.get (i)); }}Тип стирания
Возможно, самая печальная вещь в Java Generics - это стирание типа, особенно для программистов с опытом C ++. Стирание типа означает, что дженерики Java могут использоваться только для проверки статического типа во время компиляции, а затем код, сгенерированный компилятором, будет стереть соответствующую информацию типа. Таким образом, во время пробега JVM на самом деле знает конкретный тип, представленное универсальным. Цель этого в том, что дженерики Java были введены после 1,5. Чтобы поддерживать нисходящую совместимость, вы можете выполнять только стирание типа, чтобы быть совместимым с предыдущим негенерическим кодом. В этом моменте, если вы прочитаете исходный код фреймворка коллекции Java, вы можете обнаружить, что некоторые классы на самом деле не поддерживают дженерики.
Сказав так много, что означает общее стирание? Давайте сначала посмотрим на следующий простой пример:
Общедоступный узел класса <t> {private t data; Частный узел <T> следующий; public Node (t Data, Node <t> Далее)} this.data = data; this.next = Далее; } public t getData () {return data; } // ...}После того, как компилятор завершит соответствующую проверку типа, приведенный выше код фактически будет преобразован в:
Общедоступный узел класса {данные частного объекта; частный узел следующий; public node (данные объекта, Node next) {this.data = data; this.next = Далее; } public Object getData () {return Data; } // ...}Это означает, что независимо от того, объявляем ли мы Node <string> или Node <Integer>, JVM считается Node <Object> во время выполнения. Есть ли способ решить эту проблему? Это требует от нас сбросить границы сами и изменить приведенный выше код на следующее:
Общедоступный узел класса <T Extens сопоставимо <t>> {private t data; Частный узел <T> следующий; public Node (t Data, Node <t> Далее) {this.data = data; this.next = Далее; } public t getData () {return data; } // ...}Таким образом, компилятор заменит место, где T появится с сопоставимым вместо объекта по умолчанию:
Общедоступный узел класса {частные сопоставимые данные; частный узел следующий; public node (сопоставимые данные, Node Next) {this.data = data; this.next = Далее; } public complable getData () {return data; } // ...}Приведенная выше концепция может быть легче понять, но на самом деле общее стирание вызывает гораздо больше проблем. Затем давайте систематически рассмотрим некоторые проблемы, вызванные стиранием типа. Некоторые проблемы могут не столкнуться в C ++ Generics, но вы должны быть очень осторожны в Java.
Вопрос 1
Общие массивы не допускаются на Java. Если компилятор делает что -то вроде следующего, он сообщит об ошибке:
Список <integer> [] arrayoflists = новый список <Integer> [2]; // ошибка времени компиляции
Почему компилятор не поддерживает вышеуказанную практику? Продолжайте использовать обратное мышление, мы рассматриваем эту проблему с точки зрения компилятора.
Давайте сначала посмотрим на следующий пример:
Object [] strings = new String [2]; Strings [0] = "hi"; // OkStrings [1] = 100; // ArrayStoreException брошен.
Приведенный выше код легко понять. Строковые массивы не могут хранить целочисленные элементы, и такие ошибки часто должны быть обнаружены до тех пор, пока код не будет запущен, и компилятор не может их распознать. Далее, давайте посмотрим, что произойдет, если Java поддержит создание общих массивов:
Object [] stringlists = новый список <string> []; // ошибка компилятора, но притворяется, что это разрешено строковые списки [0] = new ArrayList <string> (); // OK // Следует бросить ArrayStoreException, но время выполнения не может обнаружить его.
Предположим, мы поддерживаем создание общих массивов. Поскольку информация о типе во время выполнения была стерта, JVM фактически не знает разницы между New ArrayList <string> () и New ArrayList <Integer> () вообще. Если такие ошибки возникают в практических сценариях применения, их будет очень трудно обнаружить.
Если вы все еще скептически относитесь к этому, вы можете попробовать запустить следующий код:
открытый класс ErassTyPeeQuivalence {public static void main (String [] args) {class C1 = новый ArrayList <string> (). getClass (); Класс C2 = новый ArrayList <Integer> (). GetClass (); System.out.println (c1 == c2); // истинный }}Вопрос 2
Продолжайте повторно использовать наш класс узлов выше. Для общего кода компилятор Java фактически тайно поможет нам реализовать метод моста.
Общедоступный узел класса <t> {public t data; public node (t data) {this.data = data; } public void setData (t data) {System.out.println ("node.setData"); this.data = data; }} открытый класс myNode extends node <Integer> {public myNode (Integer Data) {super (data); } public void setData (Integer Data) {System.out.println ("mynode.setData"); Super.SetData (данные); }}После прочтения приведенного выше анализа вы можете подумать, что после стирания типа компилятор превратит узел и MyNode в следующее:
Общедоступный узел класса {данные открытого объекта; public node (объект Data) {this.data = data; } public void setData (data Object) {System.out.println ("node.setData"); this.data = data; }} открытый класс myNode extends node {public myNode (Integer Data) {super (data); } public void setData (Integer Data) {System.out.println ("mynode.setData"); Super.SetData (данные); }}На самом деле, это не так. Давайте сначала посмотрим на следующий код. Когда этот код будет запущен, будет выброшено класссена
Mynode mn = new mynode (5); node n = mn; // необработанный тип - компилятор бросает неконтролируемый warningn.setdata ("hello"); // приводит к брошению ClassCastException.// Integer x = mn.data;Если мы следуем по коду, который мы сгенерировали выше, мы не должны сообщать об ошибке при запуске в строке 3 (обратите внимание, что я прокомментировал строку 4), потому что метод SetData (строки данных) не существует в MyNode, поэтому мы можем только вызвать метод SetData (Data Data) в узле родительского класса. Поскольку в этом способе код строки 3 не должен сообщать об ошибке, потому что, конечно, строка может быть преобразована в объект, так как же классное застройка?
Фактически, компилятор Java автоматически обрабатывает приведенный выше код:
класс mynode extends node {// bridge Метод, сгенерированный компилятором public void setData (data Object) {setData ((Integer) Data); } public void setData (Integer Data) {System.out.println ("mynode.setData"); Super.SetData (данные); } // ...}Вот почему приведенная выше ошибка сообщается. Когда setData (((целое число) данные); Строка не может быть преобразована в целое число. Следовательно, когда компилятор предлагает неконтролируемое предупреждение в строке 2 выше, мы не можем игнорировать его, иначе нам придется подождать, пока время выполнения, чтобы найти исключение. Было бы здорово, если бы мы добавили узел <Integer> n = Mn в начале, чтобы компилятор мог помочь нам заранее найти ошибки.
Вопрос 3
Как мы упоминали выше, Java Generics может предоставить только проверку статического типа в значительной степени, а затем информация о типе будет стерта, поэтому компилятор не будет передавать следующий метод использования параметров типа для создания экземпляров:
public static <e> void append (list <e> list) {e elem = new e (); // Список ошибок времени компиляции.add (elem);}Но что мы должны делать, если мы хотим создавать экземпляры, используя параметры типа в определенных сценариях? Отражение может быть использовано для решения этой проблемы:
public static <e> void append (список <e> list, class <e> cls) бросает исключение {e elem = cls.newinstance (); // ok list.add (elem);}Мы можем назвать это так:
List <string> ls = new ArrayList <> (); Append (ls, String.class);
Фактически, для вышеуказанной проблемы вы также можете использовать заводские и шаблоны проектирования для решения. Заинтересованные друзья могут пожелать взглянуть на объяснение создания экземпляра типов в главе 15 в «Мышлении на Яве». Мы не пойдем в это здесь.
Вопрос 4
Мы не можем использовать ключевое слово EncementOF непосредственно для общего кода, потому что компилятор Java будет стереть всю соответствующую информацию об общем типе при генерации кода, так же, как JVM, который мы проверили выше, не может распознать разницу между ArrayList <Integer> и ArrayList <string> во время выполнения: Время выполнения: Время выполнения: Время выполнения: Время выполнения: Время выполнения:
public static <e> void rtti (list <e> list) {if (instanceof ancessionof arraylist <integer>) {// ошибка компиляции времени // ...}} => {arraylist <Integer>, arraylist <string>, linkedlist <символ>, ...}Как указано выше, мы можем использовать подстановочные знаки для сброса границ для решения этой проблемы:
public static void rtti (list <?> list) {if (instanceof ancessionof arraylist <?>) {// ok; экземпляр требует обстоятельного типа // ...}}Суммировать
Вышеуказанное посвящено повторному пониманию дженериков Java в этой статье, я надеюсь, что это будет полезно для всех. Заинтересованные друзья могут продолжать ссылаться на этот сайт:
Подробное объяснение оснований массива Java
Основы программирования Java: подражание обмену кодом входа пользователя
Основы программирования сети Java: одностороннее общение
Если есть какие -либо недостатки, пожалуйста, оставьте сообщение, чтобы указать это. Спасибо, друзья, за вашу поддержку на этом сайте!