Дженерики (Generics) в Java


Сегодня мы узнаем как устранить другой фундаментальный дефект реализаций, которые мы рассматривали до сих пор, состоящий в том, что эти реализации хороши только для строк. Что если нам нужна очередь или стек элементов другого типа данных? И это подводит нас к теме Generics.

Мы реализовали стек строк, но в приложениях возможны различные типы данных, которые нам, возможно, понадобится реализовать: стек чисел, URL-ссылок, машин, грузовиков и любых данных, с которыми мы работаем. Как мы реализуем стек или очередь для таких данных?

Первое решение, которое мы можем рассмотреть и в действительности обязаны рассмотреть во многих программных средах, это реализация отдельного класса стека для каждого из типов данных, который мы используем. Это кажется неудовлетворительным решением. У нас есть аккуратно написанный код, который поддерживает изменение размера массива и прочее, и мы будем копировать этот код и менять везде тип данных со строки на число или объект грузовик. Что если мы работаем с сотнями типов данных? У нас будут сотни разных реализаций?

К сожалению, на заре Java у нас не было других вариантов, и существует достаточно языков программирования, где у нас нет других вариантов. Мы хотим рассмотреть современный подход к избеганию повторяющихся реализаций для каждого типа данных. Быстрым, широко распространённым решением является приведение типов. Так, мы делаем нашу реализацию с типом Object, а все в Java является подтипом Object. А затем, когда клиент будет использовать её, он будет просто приводить результат к соответствующему типу.

StackOfObjects s = new StackOfObjects();
Apple a = new Apple();
Orange b = new Orange();
s.push(a);
s.push(b);
a = (Apple) (s.pop());
Я не хочу тратить много времени на это, потому что я думаю, что это также неудовлетворительное решение. Так, в этом примере у нас два разных типа с двумя стеками - один для яблок, другой для апельсинов. И затем, заботой клиента является при снятии элемента с яблочного стека, привести полученный объект к яблокам, чтобы удовлетворить требования системы проверки типов. Проблема с этим состоит в том, что клиентский код обязан делать такое приведение. И это оказывается сортом коварной ошибки, когда такое приведение не удается выполнить.

Stack<Apple> s = new Stack<Apple>();
Apple a = new Apple();
Orange b = new Orange();
s.push(a);
s.push(b);
a = s.pop();
А третий подход, о котором мы сейчас поговорим, использует дженерики (обобщения). И на этом пути клиентскому коду не требуется выполнять приведения типов. Мы можем обнаружить ошибки несоответствия типов во время компиляции кода, а не во время его выполнения. Таким образом, в этом случае при задействовании механизма обобщений, у нас может быть параметр типа в нашем обобщенном классе. И он располагается внутри угловых скобок в коде.

И затем, если у нас стек яблок, и мы попытаемся поместить в этот стек яблок апельсин, то мы получим ошибку времени компиляции. Потому что этот стек был объявлен как содержащий только яблоки. Руководящим принципом хорошего модульного программирования является то, что мы приветствуем ошибки времени компиляции, и стараемся избегать ошибок времени выполнения.

Потому что, если мы можем обнаруживать ошибки во время компиляции, то мы можем поставлять и развертывать реализации нашего API, и иметь некоторую уверенность в том, что они будут работать с любым клиентским кодом. Тогда как ошибка, которую нельзя обнаружить до времени выполнения, может проявиться в какой-то клиентской разработке даже годы спустя развертывания нашего программного обеспечения, и будет исключительно сложной для всех.

Итак, на самом деле, в хорошей обобщенной реализации нетрудно заменить тип String на обобщенный тип везде, где он используется. Как в этом коде вот здесь.



Слева показана наша реализация стека строк при помощи связного списка. Справа расположена обобщенная реализация. Таким образом, везде, где мы слева используем тип String, мы используем слово Item с правой стороны. А сверху, в объявлении класса, мы объявляем в угловых скобках, что Item - это обобщенный тип, который мы будем использовать.

Реализация вряд ли может быть более прямолинейной, и это великолепный путь решения задачи работы с множеством типов данных с помощью одной реализации. С массивами это не совсем работает. И, еще раз, все языки программирования...

Знаете, у многих языков программирования есть трудности с этим, и у языка программирования Java здесь специфическое затруднение. Так, здесь мы просто хотим объявить новый массив, используя наше обобщенное имя Item, как показано вот в этой подсвеченной строке. В остальном же всё то же самое. К сожалению, Java не разрешает создание обобщенных массивов. Для этого есть различные технические причины. И вы можете почитать обширные дебаты на эту тему в интернете. Они выходят за рамки нашего внимания.

А пока нам нужно использовать приведение типов, чтобы все заработало. Мы создаем массив с элементами типа Object, и затем мы приводим его к массиву с элементами типа Item. Далее, на мой взгляд, хороший код не должен содержать приведения типов. Так что мы стремимся избегать приведения типов настолько, насколько это возможно. Потому что, на самом деле, приведение типов является признанием слабости в том, что мы делаем. Но в данном случае нам придется поместить это единственное приведение типов. Так что мы осведомлены о том, что это приведение ужасно, знаем, что этот код не может быть отнесен к хорошему.

public class FixedCapacityStack
{
private Item[] s;
private int N = 0;
public FixedCapacityStack(int capacity)
{ s = (Item[]) new Object[capacity]; }
public boolean isEmpty()
{ return N == 0; }
public void push(Item item)
{ s[N++] = item; }
public Item pop()
{ return s[--N]; }
}
И это не то решение, к которому мы бы пришли по собственной воле. И, я думаю, это нежелательная деталь для настолько простого кода. Но, к счастью, мы можем справиться почти со всем, что мы собираемся сделать, просто зная об этом единственном ужасном приведении типов. Так, теперь, когда мы скомпилируем эту программу, мы получим предупреждение от Java.

% javac FixedCapacityStack.java
Note: FixedCapacityStack.java uses unchecked or unsafe operations.
Note: Recompile with -Xlint:unchecked for details.
% javac -Xlint:unchecked FixedCapacityStack.java
FixedCapacityStack.java:26: warning: [unchecked] unchecked cast
found : java.lang.Object[]
required: Item[]
a = (Item[]) new Object[capacity];
^
1 warning
Оно скажет, что мы используем непроверенные или небезопасные операции, и нам стоит перекомпилировать с опцией -Xlint:unchecked, чтобы увидеть детали. Так что мы так и поступим. И нам сообщат, что мы поместили непроверяемое приведение типов в код, и нас об этом предупреждают, потому что не следует ставить в код непроверяемое приведение типов. Ну ладно, вы будете получать такое сообщение всегда при компиляции подобного кода

Я думаю, может быть, им следовало бы добавить к этому сообщению такой текст: "Мы приносим свои извинения за то, что заставляем вас так делать". Не наша вина в том, что нам пришлось так сделать. Мы вынуждены сделать это из-за ваших требований, не разрешающих нам объявлять обобщенные массивы. Таким образом, вооружившись этим замечанием, пожалуйста, не думайте, что с вашим кодом что-то не так, если вы следуете нашим предписаниям и получаете такое предупреждающее сообщение.

Далее, есть одна деталь, о которой заботится Java, и она касается примитивных типов. Используемый нами обобщенный тип предназначен для использования с объектными типами. Мы приводим к целевому типу массив объектов. Поэтому, чтобы с помощью обобщенных типов управиться с примитивными типами, мы должны использовать их объектные оболочки.

Так, Integer с большой буквы для int, и так далее. Многие из вас, вероятно, знакомы с этим. И есть в Java процесс, называемый автоупаковкой, который автоматически проводит преобразование между примитивными типами и их объектными оболочками. Так что все это обслуживает задачу работы с примитивными типами как бы за сценой. И, в качестве итога, мы можем сформулировать API для обобщенных стеков, которое работает с любыми типами данных. И мы получили две реализации, с помощью связных списков и массивов, которые работают очень хорошо для любого типа данных, используя изменение размера массивов или связные списки, как мы описывали.
Автор этого материала - я - Пахолков Юрий. Я оказываю услуги по написанию программ на языках Java, C++, C# (а также консультирую по ним) и созданию сайтов. Работаю с сайтами на CMS OpenCart, WordPress, ModX и самописными. Кроме этого, работаю напрямую с JavaScript, PHP, CSS, HTML - то есть могу доработать ваш сайт или помочь с веб-программированием. Пишите сюда.

тегистатьи IT, алгоритмы, теория программирования, java




Отправляя сообщение я подтверждаю, что ознакомлен и согласен с политикой конфиденциальности данного сайта.




Поля и методы в Java: сцепление экземпляров методов цепочкой
Урок 20. Абстрактные классы C#
9 советов для разработчиков JavaScript