gb-java-devel/jtd3-08-abstract.tex

912 lines
72 KiB
TeX
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

\documentclass[j-spec.tex]{subfiles}
\begin{document} \sloppy
\setcounter{section}{7}
\setlength{\columnsep}{22pt}
\pagestyle{plain}
\tableofcontents
\section{Инструментарий: Обобщения}
\subsection*{В предыдущем разделе}
\begin{itemize}
\item программные интерфейсы — понятие и принцип работы;
\item ключевое слово implements;
\item Наследование и множественное наследование интерфейсов;
\item реализация, реализации по-умолчанию;
\item частичная реализация интерфейсов, адаптеры;
\item анонимные классы.
\end{itemize}
\subsection*{В этом разделе}
Смысл обобщений в программировании примерно такой же, как и в обычной разговорной речи -- отсутствие деталей реализации. С точки зрения проектирования обобщения -- это более сложный полиморфизм. Будет рассмотрены diamond operator, обобщённые методы и подстановочные символы (Wildcards). Применение обобщений для ограничений типов сверху и снизу. Выведение и стирание типов, целевые типы и загрязнение кучи.
\begin{itemize}
\item \nom{Обобщения}{особый подход к описанию данных и алгоритмов, позволяющий работать с различными типами данных без изменения внешнего описания.};
\item \nom{Wildcard}{в обобщённом коде знак вопроса, называемый подстановочным символом, означает неизвестный тип. Подстановочный символ может использоваться в разных ситуациях: как параметр типа, поля, локальной переменной, иногда в качестве возвращаемого типа.};
\item \nom{Diamond operation}{автоматизация подстановки типа, пустые угловые скобки, вынуждающие компилятор вывести тип из контекста.};
\item \nom{Стирание типа}{подготовка написанного обобщённого кода к компиляции таким образом, чтобы в байт-коде не было обобщённых конструкций.};
\item \nom{Выведение типа}{это возможность компилятора автоматически определять аргументы типа на основе контекста};
\item \nom{Сырой (raw) тип}{это имя обобщённого класса или интерфейса без аргументов типа, то есть это, фактически, написание идентификатора и вызов конструктора обобщённого класса как обычного, без треугольных скобок.};
\item \nom{Целевой тип}{это тип данных, который компилятор Java ожидает в зависимости от того, в каком месте находится выражение.}.
\end{itemize}
\subsection{Понятие обобщения}
\subsubsection{Работа без обобщений}
Обобщения -- это механизм создания общего подхода к реализации одинаковых алгоритмов, но с разными данными. Обобщения -- это некоторые конструкции, позволяющие работать с данными не задумываясь о том, какие именно данные лежат внутри структуры или метода (строки, числа или коты). Обобщения позволяют единообразно работать с разными типами данных.
\begin{frm} \info
Обобщённое программирование в Java позволяет создавать классы, интерфейсы и методы, которые могут работать с различными типами данных. В результате, код становится более универсальным и повторно используемым.
\end{frm}
Для приведения примера поведения контейнера, который может хранить данные любого типа необходимо создать некоторый класс, который будет хранить внутри \code{Object} (любые данные Java). На примере чисел, в основном методе приложения созданы два экземпляра коробок с числами. Числа, которые сохранены в коробке желательно иметь возможность не только хранить, но и использовать, например, производить операцию сложения.
\begin{lstlisting}[language=Java,style=JCodeStyle,caption={Контейнер, хранящий любые данные}]
private static class Box {
private Object obj;
public Box(Object obj) {
this.obj = obj;
}
public Object getObj() {
return obj;
}
public void setObj(Object obj) {
this.obj = obj;
}
public void printInfo() {
System.out.printf("Box (%s): %s\n",
obj.getClass().getSimpleName(),
obj.toString());
}
}
public static void main(String[] args) {
Box b1 = new Box(20);
Box b2 = new Box(30);
System.out.println(b1.getObj() + b2.getObj());
}
\end{lstlisting}
Поскольку в коробке, с точки зрения языка, хранятся объекты, а оператор сложения для объектов не определён, следует применить операцию приведения типов. Средства языка не запрещают создать больше коробок, например, со строками, и будет производиться уже не складывание чисел, а конкатенация строк.
Проблема такого способа в том, что при каждом получении данных из коробки необходимо делать приведение типов, чтобы указать какие именно в контейнере лежат данные. Данную проблему для программы возможно решить организационно, если, например, называть переменные в венгерской нотации\footnote{Венгерская нотация в программировании -- соглашение об именовании переменных, констант и прочих идентификаторов в коде программ. Тип переменной указывается строчной буквой перед именем переменной, например для типа \code{int} переменная может называться \code{iMyVariable}.}.
\begin{lstlisting}[language=Java,style=JCodeStyle,caption={Согласование типов}]
public static void main(String[] args) {
Box b1 = new Box(20);
Box b2 = new Box(30);
System.out.println((Integer) b1.getObj() + (Integer) b2.getObj());
Box b3 = new Box("Hello, ");
Box b4 = new Box("World!");
System.out.println((String) b3.getObj() + (String) b4.getObj());
}
\end{lstlisting}
Вторая проблема в том, что данные в контейнере ничем не защищены. Java не запрещает менять данные внутри контейнера, поскольку для платформы это объект. Проблему возможно решить сделав проверку \code{instanceof} перед приведением типов.
Третья проблема в том, что все проблемы подобного рода проявляют себя только во время исполнения приложения, то есть у конечного пользователя перед глазами, когда разработчик ничего исправить не может.
\begin{lstlisting}[language=Java,style=JCodeStyle,caption={Изменение типа хранения во время исполнения}]
public static void main(String[] args) {
Box iBox1 = new Box(20);
Box iBox2 = new Box(30);
if (iBox1.getObj() instanceof Integer && iBox2.getObj() instanceof Integer) {
int sum = (Integer) iBox1.getObj() + (Integer) iBox2.getObj();
System.out.println("sum = " + sum);
} else {
System.out.println("The contents of the boxes differ by type");
}
iBox1.setObj("sdf"); // Java: "What can go wrong here? You can do it!"
}
\end{lstlisting}
Таким образом, в языке Java возможно создавать классы, которые могут работать с любыми типами данных, но при любом обращении к таким классам и данным необходимо делать достаточно сложные проверки.
\subsubsection{Создание обобщения}
\begin{itemize}
\item Java generics -- это механизм языка, который позволяет создавать обобщенные (шаблонизированные) типы и методы в Java (особый подход к описанию данных и алгоритмов, позволяющий работать с различными типами данных без изменения внешнего описания);
\item Java generics были добавлены в Java 1.5 и стали одной из наиболее важных новых функций в языке;
\item Java generics работают только со ссылочными типами данных;
\item Java generics предоставляют безопасность типов во время компиляции, что означает, что ошибки связанные с типами данных могут быть обнаружены на этапе компиляции, а не во время выполнения программы.
\end{itemize}
Для описания обобщения в треугольных скобках пишется буква \code{<T>}, чтобы обозначить Type, Тип. На этапе описания класса невозможно сказать, какого типа данные будут лежать в переменной во время исполнения (число, строка или кот).
\begin{frm} \excl
Если написать \code{T} не в треугольных скобках при описании класса, то Java будет искать реально существующий класс, который она не видит.
\end{frm}
Таким образом указывается, что это обобщение и тип будет задаваться при создании объекта. Естественно поменять его будет нельзя, потому что Java -- это язык сильной статической типизации.
\begin{lstlisting}[language=Java,style=JCodeStyle,caption={Создание обобщённого контейнера}]
private static class GBox<T> {
private T value;
public GBox(T value) {
this.value = value;
}
public T getValue() {
return value;
}
public void setValue(T value) {
this.value = value;
}
public void showType() {
System.out.printf("Type is %s, with value %s\\n",
value.getClass().getName(), getValue());
}
}
public static void main(String[] args) {
GBox<String> stringBox = new GBox<>("Hello!");
stringBox.showType();
GBox<Integer> integerBox = new GBox<>(12);
integerBox.showType();
}
\end{lstlisting}
При вызове конструктора такого объекта, будет указано, что конструктор ожидает указанный ранее тип, а не \code{T}. При указании типа в левой части, этот тип подставляется во все места класса, где компилятор обнаружит \code{T}. При получении значений из \code{integerBox} и \code{stringBox} не требуется преобразование типов, \code{integerBox.getValue()} сразу возвращает \code{Integer}, а \code{stringBox.getValue()} -- \code{String}.
Если объект создан как \code{Integer}, то становится невозможно записать в него строку. При попытке написать такую строку кода, получится ошибка на этапе компиляции, то есть обобщения отслеживают корректность используемых типов данных.
По соглашению, переменные типа именуются одной буквой в верхнем регистре. Если обобщённых переменных более одной -- они пишутся через запятую.
\begin{frm} \info
\begin{itemize}
\item [] \code{E} -- элемент (Element, Entity обширно используется Java Collections);
\item [] \code{K} -- Ключ;
\item [] \code{N} -- Число;
\item [] \code{T} -- Тип;
\item [] \code{V} -- Значение;
\item [] \code{S}, \code{U}, и т. п. — 2-й, 3-й, 4-й типы.
\end{itemize}
\end{frm}
\subsubsection{Ограничения обобщений}
Обобщения накладывают на работу с собой некоторые ограничения.
\begin{enumerate}
\item Невозможно внутри метода обобщённого класса создать экземпляр параметризующего класса \code{T}, потому что на этапе компиляции об этом классе ничего не известно. Это ограничение возможно обойти, используя паттерн проектирования \textit{абстрактная фабрика}.
\item Нельзя создавать внутри обобщения массив из обобщённого типа данных. Но всегда можно подать такой массив снаружи.
\item По причине отсутствия информации о параметризируещем классе, невозможно создать статическое поле типа. Конкретный тип для параметра \code{T} становится известен только при создании \textbf{объекта} обобщённого класса.
\item Нельзя создать исключение обобщённого типа.
\end{enumerate}
\subsubsection{Работа с обобщёнными объектами}
При обращении к обобщённому классу необходимо заменить параметры типа на конкретные классы или интерфейсы, например строку, целое число или кота.
\begin{frm} \excl
«Параметр типа» и «аргумент типа» -- это два разных понятия. Когда объявляется обобщённый тип \code{GBox<T>}, то \code{T} является параметром типа, а когда происходит обращение к обобщённому типу, передается аргумент типа, например \code{Integer}.
\end{frm}
Как и любое другое объявление переменной запись вида \code{GBox<Integer> integerBox} сам по себе не создаёт экземпляр класса \code{GBox}. Такой код объявляет идентифиактор типа \code{GBox}, но сразу уточняет, что это будет коробка с целыми числами. Такой идентификатор обычно называется \textbf{параметризованным типом}.
\begin{lstlisting}[language=Java,style=JCodeStyle,caption={Способы создания обобщённых идентификаторов и объектов}]
GBox<Integer> integerBox0;
GBox<Integer> integerBox1 = new GBox<Integer>(1);
GBox<Integer> integerBox2 = new GBox<>(1);
\end{lstlisting}
Чтобы создать экземпляр класса, используется ключевое слово \code{new} и, в дополнение, указывается, что создаётся не просто \code{GBox}, а обобщённый, поэтому пишется \code{<Integer>}. Компиляторы, начиная с Java 1.7, научились самостоятельно подставлять в треугольные скобки нужный тип (\textbf{выведение типа из контекста}).
Если тип совпадает с аргументом типа в идентификаторе, в скобках экземпляра его можно не писать. Это называется \textbf{бриллиантовый оператор}.
\subsection{Варианты обобщений}
\subsubsection{Множество параметризированных типов}
Ограничений на количество параметризированных типов не накладывается. Часто можно встретить обобщения с двумя типами, например, в коллекциях, хранящих пары ключ-значение. Также, нет ограничений на использование типов внутри угловых скобок.
\begin{lstlisting}[language=Java,style=JCodeStyle,caption={Множество параметризированных типов}]
private static class KVBox<K, V> {
private K key;
private V value;
public KVBox(K key, V value) {
this.key = key;
this.value = value;
}
public V getValue() {
return value;
}
public K getKey() {
return key;
}
public void showType() {
System.out.printf("Type of key is %s, key = %s, " +
"type of value is %s, value = %s\\n",
key.getClass().getName(), getKey(),
value.getClass().getName(), getValue());
}
}
public static void main(String[] args) {
KVBox<Integer, String> kvb0 = new KVBox<>(1, "Hello");
KVBox<String, GBox<String>> kvb1 = new KVBox<>("World", new GBox<>("Java"));
}
\end{lstlisting}
\subsubsection{Raw type (сырой тип)}
Сырой тип -- это имя обобщённого класса или интерфейса без аргументов типа, то есть это, фактически, написание идентификатора и вызов конструктора обобщённого класса как обычного, без треугольных скобок. При использовании сырых типов, программируется поведение, которое существовало до введения обобщений в Java.
Геттеры сырых типов возвращают объекты. Это логично, потому что ни на одном из этапов не указан аргумент типа.
\begin{frm} \excl
\code{GBox} -- это сырой тип обобщённого типа \code{GBox<T>}. Однако необобщённый класс или интерфейс не являются сырыми типами.
\end{frm}
Для совместимости со старым кодом допустимо присваивать параметризованный тип своему собственному сырому типу.
\begin{lstlisting}[language=Java,style=JCodeStyle,caption={Использование сырых типов}]
GBox<Integer> intBox = new GBox<>(1);
GBox box = intBox;
GBox box = new GBox(1);
GBox<Integer> intBox = box;
GBox<Integer> intBox0 = new GBox<>(1);
GBox box0 = intBox0;
box.setValue(4);
\end{lstlisting}
Также, если присвоить параметризованному типу сырой тип, или если попытаться вызвать обобщённый метод в сыром типе, то буквально каждое слово в программе будет с предупреждением среды разработки. Предупреждения показывают, что сырой тип обходит проверку обобщённого типа, что откладывает обнаружение потенциальной ошибки на время выполнения программы.
Предупреждения среды, а на самом деле, предупреждения компилятора, обычно имеют вид, представленный на рис \hrf{pic:ide-unchecked}.
\begin{figure}[H]
\centering
\includegraphics[width=15cm]{jd-03-ide-unchecked.jpeg}
\caption{Предупреждение среды о непроверенном типе}
\label{pic:ide-unchecked}
\end{figure}
Термин «unchecked» означает непроверенные, то есть компилятор не имеет достаточного количества информации для обеспечения безопасности типов. По умолчанию этот вид предупреждений выключен, поэтому компилятор в терминале на самом деле даёт подсказку.
\begin{verbatim}
Note: <ClassName>.java uses unchecked or unsafe operations.
Note: Recompile with -Xlint:unchecked for details.
\end{verbatim}
Чтобы увидеть все «unchecked» предупреждения нужно перекомпилировать код с опцией \code{-Xlint:unchecked}.
\begin{figure}[H]
\centering
\includegraphics[width=15cm]{jd-03-lint-unchecked.jpeg}
\caption{Предупреждение компилятора о непроверенном типе}
% \label{pic:}
\end{figure}
К предупреждениям среды в написанном коде желательно относиться внимательно, и придирчиво перепроверять код на наличие ненадёжных конструкций.
\subsubsection{Вопросы для самопроверки}
\begin{enumerate}
\item Обобщения это способ создания общих
\begin{enumerate}
\item классов для разных пакетов;
\item алгоритмов для разных типов данных;
\item библиотек для разных приложений.
\end{enumerate}
\item Что именно значит буква в угловых скобках?
\begin{enumerate}
\item Название обобщённого класса;
\item имя класса, с которым будет работать обобщение;
\item название параметра, используемого в обобщении.
\end{enumerate}
\item Возможно ли передать обобщённым аргументом примитивный тип?
\begin{enumerate}
\item Да;
\item нет;
\item только строку.
\end{enumerate}
\end{enumerate}
\subsubsection{Обобщённые методы}
Обобщённые методы синтаксически схожи с обобщёнными классами, но параметры типа относятся к методу, а не к классу. Допустимо делать обобщёнными статические и не статические методы, а также конструкторы.
Синтаксис обобщённого метода включает параметры типа внутри угловых скобок, которые указываются перед возвращаемым типом.
\begin{lstlisting}[language=Java,style=JCodeStyle,caption={Обобщённый метод}]
private static <T> void setIfNull(GBox<T> box, T t) {
if (box.getValue() == null) {
box.setValue(t);
}
}
public static void main(String[] args) {
GBox<Integer> box = new GBox<>(null);
setIfNull(box, 13);
System.out.println(box.getValue());
GBox<Integer> box0 = new GBox<>(1);
setIfNull(box0, 13);
System.out.println(box0.getValue());
\end{lstlisting}
\subsection{Ограниченные параметры типа}
Bounded type parameters позволяют ограничить типы данных, которые могут быть использованы в качестве параметров.
Использование bounded type parameters в Java является хорошей практикой, которая позволяет более точно определить используемые типы данных и обеспечивает более безопасный и читаемый код.
\subsubsection{Ограничение «сверху»}
В качестве примера необходимости ограничений будет использоваться уже написанная коробка. В коробке описывается операция сложения. Если сложение чисел и конкатенация строк -- это понятные операции, то сложение котиков или потоков ввода-вывода не определены. Применение \textbf{bounded type parameters} позволяет более точно задавать ограничения на используемые типы данных, что помогает писать более безопасный и читаемый код. То есть, в обобщениях существует возможность ограничиться только теми типами, которые соответствуют определенным требованиям
Например, коробку предлагается сделать так, чтобы в ней хранились только числа -- наследники класса \code{Number}. Подобное ограничение делается с помощью ограниченного параметра типа (bounded type parameter). Чтобы объявить «ограниченный сверху» параметр типа необходимо после имени параметра указать ключевое слово \code{extends}, а затем указать верхнюю границу (upper bound), которой в данном примере является класс \code{Number}.
\begin{lstlisting}[language=Java,style=JCodeStyle,caption={Ограничение параметра типа «сверху»}]
public class Box<V extends Number> {
public V getValue() {
return value;
}
public void setValue(V value) {
this.value = value;
}
private V value;
}
private static <T extends Number> void setIfNull(BBox<T> box, T t) {
if (box.getValue() == null) {
box.setValue(t);
}
}
public static void main(String[] args) {
BBox<Integer> integerBBox = new BBox<>();
BBox<String> stringBBox = new BBox<>();
setIfNull(integerBBox, 4);
setIfNull(stringBBox, "hello");
}
\end{lstlisting}
В рассматриваемом примере типы, которые можно использовать в параметризованных классах \code{Box}, ограничены наследниками класса \code{Number}. Если попытаться создать переменную с типом, например, \code{Box<String>}, то возникнет ошибка компиляции. Аналогичным образом создаются обобщённые методы с ограничением.
\begin{lstlisting}[language=Java,style=JCodeStyle,caption={Множественные ограничения}]
class Bird{}
interface Animal{}
interface Man{}
class CBox<T extends Bird & Animal & Man> {
// ...
}
\end{lstlisting}
Также возможно задать несколько границ. При этом, важно помнить, что также, как и в объявлении классов, возможен только один класс-наследник и он указывается первым. Все остальные границы могут быть только интерфейсами и указываются через знак амерсанда.
\begin{frm} \info
В языке Java, несмотря на строгость типизации, возможно присвоить идентификатору одного типа объект другого типа, если эти типы \textbf{совместимы}. Например, можно присвоить объект типа \code{Integer} переменной типа \code{Object}, так как \code{Object} является одним из супертипов \code{Integer}. В объектно-ориентированной терминологии это называется связью «является» («is a»).
\end{frm}
Предположим, что существует метод, описанный с generic параметром.
\begin{lstlisting}[language=Java,style=JCodeStyle,caption={Generic параметр метода}]
private static void boxTest(GBox<Number> n) { /* ... */ }
public static void main(String[] args) {
boxTest(new GBox<Number>(10));
boxTest(new GBox<Integer>(1)); // compile error
boxTest(new GBox<Float>(1.0f)); // compile error
\end{lstlisting}
На первый взгляд кажется, что в метод возможно передать \code{Box<Integer>} или \code{Box<Double>}, но нельзя, так как \code{Box<Integer>} и \code{Box<Double>} не являются потомками Box<Number>.
\begin{frm} \excl
Это частое недопонимание принципов работы обобщений, и это важно знать. Наследование не работает в Java generics так, как оно работает в обычной Java.
\end{frm}
Идентификатор коробки с \code{Number} не может в себе хранить коробку с \code{Integer}. Обобщение защищает от попыток положить в коробку, например, строк -- не строку. То есть, предположим, в коробку кладётся \code{Integer}, как наследник \code{Number}, а затем, например \code{Float}, получится путаница.
Из приведённого примера возможно сделать вывод о том, что если методу с таким параметром передать коробку с \code{Integer} -- он не будет работать. Чтобы допустить передачу таких контейнеров, в аргументе следует указать что в параметре возможен любой тип, являющийся наследником \code{Number}, то есть использовать маску \code{<? extends Number>}. Ограничивать такую маску возможно как сверху так и снизу. Таким образом, обобщения защищают самих себя от путаницы и не дают складывать в одни и те же контейнеры разные типы данных.
Поскольку коробку с чем то ещё, кроме \code{Number} и его наследников создавать нельзя, маск\'{и}рование при вызове метода будет избыточно
На самом деле обобщения -- это так называемый синтаксический сахар. То есть, когда в коде используется обобщение, во время компиляции произойдёт так называемое «стирание», и все обобщённые типы данных преобразуются в \code{Object}, соответствующие проверки и приведения типов.
\subsubsection{Ограничение «снизу»}
Ограничение типов возможно вводить как сверху, так и снизу. На примере обобщённых методов. Такие методы необходимы, когда требуется объединить несколько похожих, но всё же разных типов данных. Если подать на вход обобщённого метода два типа -- \code{Integer} и \code{Float} в итоге для работы будет выбран ближайший старший для них обоих -- \code{Number}.
\begin{lstlisting}[language=Java,style=JCodeStyle,caption={Пример обобщённого метода}]
private static <T extends Number> boolean compare(T src, T dst) {
return src.equals(dst);
}
public static void main(String[] args) {
System.out.println(compare(1, 1.0f));
System.out.println(compare(1.0f, 1.0f));
System.out.println(compare(1, 1));
\end{lstlisting}
Например, даны два списка -- один с \code{Integer} другой с \code{Number}, и требуется написать метод, который будет перекидывать числа из одного списка в другой. Для совершения этого действия описан метод, при работе которого возможны два сценария -- копировать элементы \code{Number} в список \code{Integer} или элементы \code{Integer} в список \code{Number}.
\begin{lstlisting}[language=Java,style=JCodeStyle,caption={Копирование чисел в списки}]
public static void copyTo(ArrayList src, ArrayList dst) {
for (Object o : src) dst.add(o);
}
public static void main(String[] args) {
ArrayList<Integer> ial = new ArrayList<>(Arrays.asList(1, 2, 3));
ArrayList<Number> nal = new ArrayList<>(Arrays.asList(1f, 2, 3.0));
System.out.println(ial);
System.out.println(nal);
copyTo(ial, nal);
System.out.println(nal);
copyTo(nal, ial);
System.out.println(ial);
\end{lstlisting}
Правильным рабочим сценарием будет только второй -- копирование более точного типа в более общий список. Java при компиляции пропустит в работу оба сценария, но действительно работающим всё равно остаётся только один.
Для примера, описаны два класса: «животное» и наследник животного -- «кот». На их основе создаются обобщённые списки и вызывается обобщённый метод копирования списков.
\begin{lstlisting}[language=Java,style=JCodeStyle,caption={Копирование списков с «животными» и «котами»}]
public static void copyTo(ArrayList src, ArrayList dst) {
for (Object o : src) dst.add(o);
}
private static class Animal {
protected String name;
protected Animal() { this.name = "Animal"; }
@Override public String toString() { return name; }
}
private static class Cat extends Animal {
protected Cat() { this.name = "Cat"; }
}
public static void main(String[] args) {
ArrayList<Cat> cats = new ArrayList<>(Arrays.asList(new Cat()));
ArrayList<Animal> animals = new ArrayList<>(Arrays.asList(new Animal()));
copyTo(animals, cats);
System.out.println(cats);
\end{lstlisting}
При компиляции и исполнении ошибок не возникло. К классу кота добавляется метод голос и совершается попытка вызвать голос у объекта из списка.
\begin{figure}[H]
\centering
\includegraphics[width=12cm]{jd-03-lists-merged-problem.jpeg}
\caption{Проблема объединения списков}
\end{figure}
При попытке вызвать у более общего «животного» метод более частного «кота» выбрасывается исключение о невозможности приведения типов.
Для того, чтобы избежать подобных проблем, необходимо описать метод таким образом, чтобы он работал только с каким-то одним типом \code{<T>} и списки будут \code{<T>} и в цикле тоже будут перебираться элементы типа \code{T}. Если более не уточнять, получится, что в список котов возможно класть только котов, а в список животных класть только животных. Но кот -- это наследник животного, его присутствие в списке животных уместно. Получается, что источником может быть список из заданного типа или его наследников, а приёмником -- тип или его родители, и далее метод, виртуальная машина и другие механизмы сами разбираются, кто подходит под эти параметры.
При таком описании метода, неверные варианты будут отсекаться на этапе компиляции.
\begin{lstlisting}[language=Java,style=JCodeStyle,caption={}]
public static <T> void copyTo (
ArrayList<? extends T> src, ArrayList<? super T> dst) {
for (T o : src) {
dst.add(o);
}
}
\end{lstlisting}
\subsection{Выведение типов}
Алгоритм выведения типов определяет типы аргументов, а также, если это применимо, тип, в который присваивается результат или в котором возвращается результат.
\begin{frm} \info
Выведение типов -- это возможность компилятора автоматически определять аргументы типа на основе контекста.
\end{frm}
Алгоритм работает от наиболее общего типа (\code{Object}) к наиболее точному, подходящему к данной ситуации. Например, добавив к коту реализацию интерфейса \code{Serializable} и написав метод, работающий с одним и только одним типом данных \code{T} будет создана ситуация, в которой никакие аргументы не связаны наследованием.
\begin{lstlisting}[language=Java,style=JCodeStyle,caption={Ситуация, в которой сработает выведение типов}]
private static class Cat extends Animal implements Serializable {
protected Cat() { this.name = "Cat"; }
public void voice(){ System.out.println("meow"); }
}
private static <T> T pick(T first, T second) { return second; }
public static void main(String[] args) {
Serializable se1 = pick("d", new Cat());
Serializable se2 = pick("d", new ArrayList<String>());
\end{lstlisting}
В таком методе выведение типов определяет, что вторые аргументы метода \code{pick}, а именно \code{Cat} и \code{ArrayList}, передаваемые в метод имеют тип \code{Serializable}, но этого недостаточно, потому что первый аргумент тоже должен быть того же типа. Удачно, что строка -- это тоже \code{Serializable}.
В описании обобщённых методов, выведение типа делает возможным вызов обобщённого метода так, будто это обычный метод, без указания типа в угловых скобках.
\begin{lstlisting}[language=Java,style=JCodeStyle,caption={Выведение типов в обобщённых методах},label={lst:generic-type-resolve}]
public class App {
public static <U> void addBox(U u, List<Box<U>> boxes) {
Box<U> box = new Box<>();
box.setValue(u);
boxes.add(box);
}
public static void main( String[] args ) {
ArrayList<Box<Cat>> catsInBoxes = new ArrayList<>();
App.<Cat>addBox(new Cat("Kusya"), catsInBoxes);<@\label{lst-line:with-type}@>
addBox(new Cat("Kusya"), catsInBoxes);<@\label{lst-line:no-type}@>
addBox(new Cat("Murka"), catsInBoxes);
printBoxes(catsInBoxes);
}
\end{lstlisting}
Очевидно, что в листинге \hrf{lst:generic-type-resolve} обобщённый метод \code{addBox()} объявляет один параметр типа \code{U}. В большинстве случаев компилятор Java может вывести параметры типа вызова обобщённого метода, в результате чаще всего вовсе не обязательно их указывать.
Чтобы вызвать обобщённый метод \code{addBox()}, возможно указать параметры типа (строка \hrf{lst-line:with-type}) либо опустить их, тогда компилятор языка автоматически выведет тип Cat из аргументов метода при вызове (строка \hrf{lst-line:no-type}).
Выведение типа при создании экземпляра обобщённого класса позволяет заменить аргументы типа, необходимые для вызова конструктора обобщённого класса пустым множеством параметров типа (пустые треугольные скобки, бриллиантовая операция), так как компилятор может вывести аргументы типа из контекста.
Очевидно, что конструкторы могут быть обобщёнными как в обобщённых, так и в необобщённых классах.
\begin{lstlisting}[language=Java,style=JCodeStyle,caption={Обобщённый конструктор}]
public class Box<T> {
<U> Box(U u){
// ...
}
public T getValue() {
return value;
}
public void setValue(T value) {
this.value = value;
}
private T value;
}
public static void main(String[] args) {
Box<Cat> box = new Box<Cat>("Some message");
\end{lstlisting}
Например, возможно явно указать, что у коробки будет обобщённый аргумент «кот», а у конструктора -- какой-то другой аргумент, например, строка или число. Компилятор выведет тип String для формального параметра U, так как фактически переданный аргумент является экземпляром класса String.
\begin{frm} \excl
Алгоритм выведения типа использует только аргументы вызова, целевые типы и возможно очевидный ожидаемый возвращаемый тип для выведения типов. Алгоритм выведения не использует последующий код программы.
\end{frm}
\subsection{Целевые типы}
Компилятор Java пользуется целевыми типами для вывода параметров типа вызова обобщённого метода.
\begin{frm} \info
Целевой тип выражения -- это тип данных, который компилятор Java ожидает в зависимости от того, в каком месте находится выражение.
\end{frm}
Например, как в методе \code{emptyBox()} (листинг \hrf{lst:target-types}, строка \hrf{lst-line:empty-box}). В основном методе инициализация ожидает экземпляр \code{Box<String>}. Этот тип данных является целевым типом. Поскольку метод \code{emptyBox()} возвращает значение обобщённого типа \code{Box<T>}.
\begin{lstlisting}[language=Java,style=JCodeStyle,caption={Целевые типы},label={lst:target-types}]
public class TBox<T> {
public static final TBox EMPTY_BOX = new TBox<>();
public T getValue() { return value; }
public void setValue(T value) {
this.value = value;
}
private T value;
static <T> TBox<T> emptyBox(){<@\label{lst-line:empty-box}@>
return (TBox<T>) EMPTY_BOX;
}
}
public static void main( String[] args ) {
TBox<String> box = TBox.emptyBox();
}
\end{lstlisting}
\subsection{Вопросы для самопроверки}
\begin{enumerate}
\item Что из следующего является недопустимым?
\begin{enumerate}
\item \code{ArrayList<? extends Number> al1 = new ArrayList<Number>();}
\item \code{ArrayList<? extends Number> al2 = new ArrayList<Integer>();}
\item \code{ArrayList<? extends Number> al3 = new ArrayList<String>();}
\item Всё допустимо.
\end{enumerate}
\item параметры метода \code{ArrayList<? extends T> src, ArrayList<? super T> dst} вызов метода \code{copyTo(cats, animals);} Какой тип данных будет взят в качестве \code{Т}?
\begin{enumerate}
\item Animal;
\item Cat;
\item Object.
\end{enumerate}
\end{enumerate}
\subsection{Подстановочный символ \code{<?>} (wildcard)}
В обобщённом коде знак вопроса, называемый подстановочным символом, означает неизвестный тип. Подстановочный символ может использоваться в разных ситуациях: как параметр типа, поля, локальной переменной, иногда в качестве возвращаемого типа. Подстановочный символ никогда не используется (рис \hrf{pic:wildcard-errors}) в качестве аргумента типа для вызова обобщённого метода, создания экземпляра обобщённого класса или супертипа.
\begin{figure}[H]
\centering
\includegraphics[width=16cm]{jd-03-wildcard-error.jpeg}
\caption{Ошибки при работе с подстановочным символом}
\label{pic:wildcard-errors}
\end{figure}
Передаваемые типы для уточнения, возможно \textbf{ограничивать} сверху и снизу. Ограниченный сверху подстановочный символ применяется, чтобы \textbf{ослабить ограничения} переменной.
\begin{figure}[H]
\centering
\fontsize{12}{1}\selectfont
\includesvg[scale=1.01]{pics/jd-03-wildext.svg}
\end{figure}
Чтобы написать метод, который работает с коробками, в которых содержится \code{Number} и дочерние от \code{Number} типы, например \code{Integer}, \code{Double} и другие, необходимо указать \code{Box<? extends Number>}.
\begin{figure}[H]
\centering
\fontsize{12}{1}\selectfont
\includesvg[scale=1.01]{pics/jd-03-nowild.svg}
\end{figure}
Пример кода будет приводиться с классами животного и наследников.
\begin{lstlisting}[language=Java,style=JCodeStyle,caption={Подготовка классов для демнстрации},label={}]
private static class Animal {
protected String name;
protected Animal(String name) { this.name = name; }
@Override public String toString() {
return this.getClass().getSimpleName() + " with name " + name;
}
}
private static class Cat extends Animal {
protected Cat(String name) { super(name); }
}
private static class Dog extends Animal {
protected Dog(String name) { super(name); }
}
\end{lstlisting}
Внутри метода, выводящего информацию о содержимом коробки присутствует ограниченный сверху подстановочный символ \code{<? extends Animal>}, где вместо \code{Animal} может быть любой тип.
\begin{lstlisting}[language=Java,style=JCodeStyle,caption={Использование ограниченного подстановочного символа},label={lst:wildcard-extended}]
public static class TBox<T> {
public static final TBox EMPTY_BOX = new TBox<>();
private T value;
public T getValue() { return value; }
public void setValue(T value) { this.value = value; }
static <T> TBox<T> emptyBox() {
return (TBox<T>) EMPTY_BOX;
}
@Override public String toString() {
return value.toString();
}
}
static void printInfo(TBox<? extends Animal> animalInBox) {<@\label{lst-line:printinfo}@>
System.out.println("Information about animal: " + animalInBox);
}
\end{lstlisting}
Создав соответствующие объекты, положив их в коробки и запустив код возможно наблюдать, что коробка вмещает как животных, так и наследников животного, чего не произошло бы при использовании в параметре типа животного без подстановочного символа.
\begin{frm} \info
Если просто использовать подстановочный символ \code{<?>}, то получится подстановочный символ без ограничений. \code{Box<?>} означает коробку с неизвестным содержимым (неизвестным типом).
\end{frm}
Такой синтаксис существует для того, чтобы продолжать использовать обобщённый тип без уточнения типа, как следствие, без использования обобщённой функциональности. Неограниченный подстановочный символ полезен, если нужен метод, который может быть реализован с помощью функциональности класса \code{Object}. Когда код использует методы обобщённого класса, которые не зависят от параметра типа.
\begin{frm} \info
В программах, использующих Reflection API конструкция \code{Class<?>} используется чаще других конструкций, потому что большинство методов объекта Класс не зависят от расположенного внутри типа.
\end{frm}
Например, метод \code{printInfo()} (строка \hrf{lst-line:printinfo}) из листинга \hrf{lst:wildcard-extended}, не использует никаких методов животного, цель метода -- вывод в консоль информации об объекте в коробке любого типа, поэтому в параметре метода можно заменить «коробку с наследниками животного» на «коробку с чем угодно», ведь в методе будет использоваться только метод коробки \code{toString()}.
\begin{lstlisting}[language=Java,style=JCodeStyle,caption={Исправление метода вывода информации на экран},label={}]
static void printInfo(TBox<?> animalInBox) {
System.out.println("Information about animal: " + animalInBox);
}
\end{lstlisting}
\begin{frm} \excl
Важно запомнить, что \code{Box<Object>} и \code{Box<?>} -- это не одно и то же.
\end{frm}
\begin{figure}[H]
\centering
\includegraphics[width=12cm]{jd-03-wildcard-compile-error.jpeg}
\caption{Ошибка компиляции при использовании параметра типа \code{Object}}
\end{figure}
Ограниченный снизу подстановочный символ ограничивает неизвестный тип так, чтобы он был либо указанным типом, либо одним из его предков.
В обобщённых конструкциях возможно указать либо только верхнюю границу для подстановочного символа, либо только нижнюю.
\begin{figure}[H]
\centering
\includegraphics[width=12cm]{jd-03-no-both-borders.jpeg}
\end{figure}
То есть, если написать метод \code{printInfo()}, с параметром коробки и обобщённым аргументом не \code{<? extends Animal>}, а \code{<? super Animal>}, токод также не будет работать, потому что метод будет ожидать не «животное и наследников», а «животное и родителей», то есть \code{Object}.
\begin{figure}[H]
\centering
\fontsize{12}{1}\selectfont
\includesvg[scale=1.01]{pics/jd-03-wildsup.svg}
\end{figure}
Обобщённые классы или интерфейсы связаны не только из-за связи между их типами. С обычными, необобщёнными классами, наследование работает по правилу подчинённых типов: класс \code{Cat} является подклассом класса \code{Animal}, и расширяет его.
\begin{lstlisting}[language=Java,style=JCodeStyle,caption={Наследование необобщённых классов},label={}]
private static class Animal {
protected String name;
protected Animal(String name) { this.name = name; }
@Override public String toString() {
return this.getClass().getSimpleName() + " with name " + name;
}
}
private static class Cat extends Animal {
protected Cat(String name) { super(name); }
}
public static void main(String[] args) {
Cat cat = new Cat("Vasya");
Animal animal = cat;
TBox<Cat> catInBox = new TBox<>();
TBox<Animal> animalInBox = catInBox; // Incompatible types
\end{lstlisting}
Данное правило не работает для обобщённых типов. Несмотря на то, что \code{Cat} является подтипом \code{Animal}, \code{Box<Cat>} не является подтипом \code{Box<Animal>}. Общим предком для \code{Box<Animal>} и \code{Box<Cat>} является \code{Box<?>}.
\begin{figure}[H]
\centering
\fontsize{12}{1}\selectfont
\includesvg[scale=1.01]{pics/jd-03-wildparent.svg}
\end{figure}
\begin{lstlisting}[language=Java,style=JCodeStyle,caption={Наследование в параметрах типа через подстановочнвй символ},label={}]
TBox<? extends Cat> catInBox = new TBox<>();
TBox<? extends Animal> animalInBox = catInBox; // OK
\end{lstlisting}
В некоторых случаях компилятор может вывести тип подстановочного символа. Коробка может быть определена как \code{Box<?>}, но при вычислении выражения компилятор выведет конкретный тип из кода, такой сценарий называется \textbf{захватом подстановочного символа}. В большинстве случаев нет нужды беспокоиться о захвате подстановочного символа, кроме случаев, когда в сообщении об ошибке появляется фраза \code{capture of}. В примере, приведённом в листинге \hrf{lst:capture-error} компилятор обрабатывает параметр коробки как тип \code{Object}. Когда вызывается метод внутри \code{testError()}, компилятор не может подтвердить тип объекта, который будет присутствовать внутри коробки и генерирует ошибку. Обобщения были добавлены в Java именно для этого -- чтобы усилить безопасность типов на этапе компиляции.
\begin{lstlisting}[language=Java,style=JCodeStyle,caption={Ошибка захвата подстановочного символа},label={lst:capture-error}]
static void testError(Box<?> box){
box.setValue(box.getValue()); // capture of ?
}
\end{lstlisting}
Когда есть уверенность в том, что операция безопасна, ошибку возможно исправить написав приватный вспомогательный метод (в англоязычной литературе private helper method), захватывающий подстановочный символ.
\begin{lstlisting}[language=Java,style=JCodeStyle,caption={Использование вспомогательного метода},label={}]
private static <T> void testErrorHelper(TBox<T> box) {
box.setValue(box.getValue());
}
static void testError(TBox<?> box) {
testErrorHelper(box);
}
\end{lstlisting}
\subsubsection{Краткое руководство по использованию подстановочных символов}
\textbf{Входная переменная.} Предоставляет данные для кода. Для метода \code{copy(src, dst)} параметр \code{src} предоставляет данные для копирования, поэтому он считается входной переменной.
\textbf{Выходная переменная.} Содержит данные для использования в другом месте. В примере \code{copy(src, dst)} параметр \code{dst} принимает данные и будет считаться выходной переменной.
\begin{enumerate}
\item Входная переменная определяется с ограничением сверху.
\item Выходная переменная определяется с ограничением снизу.
\item Если ко входной переменной можно обращаться только как к \code{Object} -- неограниченный подстановочный символ.
\item Если переменная должна использоваться как входная и как выходная одновременно, НЕ использовать подстановочный символ.
\item Не использовать подстановочные символы в возвращаемых типах.
\end{enumerate}
\subsubsection{Вопросы для самопроверки}
\begin{enumerate}
\item Ограниченный снизу подстановочный символ позволяет передать в качестве аргумента типа
\begin{enumerate}
\item только родителей типа;
\item только наследников типа;
\item сам тип и его родителей;
\item сам тип и его наследников.
\end{enumerate}
\item Конструкция <? super Object>
\begin{enumerate}
\item не скомпилируется;
\item не имеет смысла;
\item не позволит ничего передать в аргумент типа;
\item не является чем-то необычным.
\end{enumerate}
\end{enumerate}
\subsection{Стирание типа и загрязнение кучи}
Обобщения были введены в язык программирования Java для обеспечения более жёсткого контроля типов во время компиляции и для поддержки обобщённого программирования. Для реализации обобщения компилятор Java применяет стирание типа.
\subsubsection{Стирание типа}
\begin{itemize}
\item Механизм стирания типа фактически заменяет все параметры типа в обобщённых типах их границами или \code{Object}, если параметры типа не ограничены.
\item Сгенерированный байткод содержит только обычные классы, интерфейсы и методы.
\item Вставляет явное приведение типов где необходимо.
\item Генерирует связующие методы, чтобы сохранить полиморфизм в расширенных обобщённых типах.
\item Гарантирует, что никакие новые классы не будут созданы для параметризованных типов, следовательно обобщения не приводят к накладным расходам во время исполнения.
\end{itemize}
Компилятор Java также стирает параметры типа обобщённых методов. Обобщённый метод в котором используется неограниченный тип будет заменён компилятором на \code{Object}.
Аналогично классам для методов происходит стирание типа при расширении ключевым словом \code{extends} -- параметр в угловых скобках заменяется на максимально возможного родителя.
\begin{lstlisting}[language=Java,style=JCodeStyle,caption={},label={}]
private static <T> void setIfNull(TBox<T> box, T t) {
if (box.getValue() == null) {
box.setValue(t);
}
}
// ... both methods have same erasure
private static void setIfNull(TBox<Object> box, Object t) {
if (box.getValue() == null) {
box.setValue(t);
}
}
\end{lstlisting}
Стирание типа имеет последствия, связанные с произвольным количеством параметров (varargs).
\textbf{Материализуемые типы} -- это типы, информация о которых полностью доступна во время выполнения, такие как примитивы, необобщённые типы, сырые типы, обращения к неограниченным подстановочным символам.
\textbf{Нематериализуемые типы} -- это типы, информация о которых удаляется во время компиляции стиранием типов, например, обращения к обобщённым типам, которые не объявлены с помощью неограниченных подстановочных символов. Во время выполнения о нематериализуемых типах нет всей информации. Виртуальная машина Java не может узнать разницу между нематериализуемыми типами во время выполнения.
\subsubsection{Загрязнение кучи}
Загрязнение кучи (heap pollution) возникает, когда переменная параметризованного типа ссылается на объект, который не является параметризованным типом. Такая ситуация возникает, если программа выполнила операцию, генерирующую предупреждение \code{unchecked warning} во время компиляции. Предупреждение \code{unchecked warning} генерируется, если правильность операции, в которую вовлечён параметризованный тип (например, приведение типа или вызов метода) не может быть проверена.
Если компилируются различные части кода отдельно, то становится трудно определить потенциальную угрозу загрязнения кучи.
\subsection{Ограничения обобщений (резюме)}
В этом разделе приводится некоторое резюме вышесказанного в части наиболее частых ошибок при работе с обобщениями.
Нельзя использовать при создании экземпляра примитивы. Выход из ситуации -- использование классов-обёрток.
\begin{figure}[H]
\centering
\includegraphics[width=12cm]{jd-03-38-01.jpeg}
\end{figure}
Нельзя создавать экземпляры параметров типа. В качестве выхода из ситуации -- передавать вновь созданный объект в обобщённые методы в качестве параметра.
\begin{figure}[H]
\centering
\includegraphics[width=12cm]{jd-03-38-02.jpeg}
\end{figure}
Статические поля класса являются общими для всех объектов этого класса, поэтому статические поля с типом параметра типа запрещены. Так как статическое поле является общим для коробки с животным, коробки с котом и коробки с собакой, то какого типа это поле? Также запрещено использовать приведение типа к параметризованному типу, если он не использует неограниченный подстановочный символ.
\begin{figure}[H]
\centering
\includegraphics[width=12cm]{jd-03-38-03.jpeg}
\end{figure}
Запрещено использовать приведения типов для обобщённых объектов. Так как компилятор стирает все параметры типа из обобщённого кода, то нельзя проверить во время выполнения, какой параметризованный тип используется для обобщённого типа.
\begin{figure}[H]
\centering
\includegraphics[width=12cm]{jd-03-38-04.jpeg}
\end{figure}
Запрещено создавать массивы параметризованных типов.
\begin{figure}[H]
\centering
\includegraphics[width=12cm]{jd-03-38-05.jpeg}
\end{figure}
Обобщённый класс не может расширять класс \code{Throwable} напрямую или опосредовано. Метод не может ловить (\code{catch}) экземпляр параметра типа. Однако можно использовать параметр типа в \code{throws}.
\begin{figure}[H]
\centering
\includegraphics[width=12cm]{jd-03-38-06.jpeg}
\end{figure}
Класс не может иметь два перегруженных метода, которые будут иметь одинаковую сигнатуру после стирания типов. Такой код не скомпилируется.
\begin{figure}[H]
\centering
\includegraphics[width=12cm]{jd-03-38-08.jpeg}
\end{figure}
\subsection*{Практическое задание}
\begin{enumerate}
\item Написать метод, который меняет два элемента массива местами (массив может быть любого ссылочного типа);
\item Большая задача:
\begin{itemize}
\item Есть классы \code{Fruit} -> \code{Apple}, \code{Orange}; (больше не надо);
\item Класс \code{Box} в который можно складывать фрукты, коробки условно сортируются по типу фрукта, поэтому в одну коробку нельзя сложить и яблоки, и апельсины; Для хранения фруктов внутри коробки можете использовать \code{ArrayList};
\item Сделать метод \code{getWeight()} который высчитывает вес коробки, зная количество фруктов и вес одного фрукта(вес яблока -- \code{1.0f}, апельсина -- \code{1.5f}, не важно в каких единицах);
\item Внутри класса коробки сделать метод \code{compare()}, который позволяет сравнить текущую коробку с той, которую подадут в \code{compare()} в качестве параметра, \code{true} -- если их веса равны, \code{false} в противном случае (коробки с яблоками возможно сравнивать с коробками с апельсинами);
\item Написать метод, который позволяет пересыпать фрукты из текущей коробки в другую коробку (при этом, нельзя яблоки высыпать в коробку с апельсинами), соответственно, в текущей коробке фруктов не остается, а в другую перекидываются объекты, которые были в этой коробке.
\end{itemize}
\end{enumerate}
\newpage
\printnomenclature[40mm]
\end{document}