Добавил:
ivanov666
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз:
Предмет:
Файл:Технологии обработки информации на Java. Учебное пособие
.pdf
ГраницыМаскиТипа:
extends СсылочныйТип
super СсылочныйТип
Пример 2.5.1-1. Маски типов
import java.util.Collection;
import java.util.ArrayList;
class Test {
static void printCollection(Collection<?> c) {
// коллекция масок типов
for (Object o : c) {
System.out.println(o);
}
}
public static void main(String[] args) {
Collection<String> cs = new ArrayList<String>();
cs.add("hello");
cs.add("world");
printCollection(cs);
}
}
Заметим, что использование Collection<Object> для параметра С не было
бы даже ориентировочно так полезно. В этом случае единственная ситуация,
в которой данный метод мог бы быть использован – это случай передачи в качестве аргументов параметра тип Collection<Object>, что происходит не так уж часто. В противоположность этому использование маски типа позволит передавать
в качестве параметра любой вид коллекций.
Далее приведен пример, где элементы массива параметризованы маской
типа:
public Method getMethod(Class<?>[] parameterTypes) { ... }
Для масок типа могут быть явно указаны границы так же, как и при
объявлении обычных типовых переменных. Для объявления верхней границы B используется следующий синтаксис:
? extends B
В отличие от обычных типовых перменных, объявленных в сигнатуре метода, при использовании масок типов не требуется использование механизма вывода типов. Следовательно, допустимо объявление нижней границы маски типа с использованием следующего синтак
сиса (здесь B – гра-
ница типа).
? super B
Пример 2.5.1-2. Ограниченные Маски типов
boolean addAll(Collection<? extends E> c)
31

В этом примере метод объявлен с интерфейсом Collection<E> и предназначен для
того, чтобы добавлять все элементы его входного аргумента в коллекцию, для которой происходит вызов. Естественным желанием было бы использовать Collection<E> как тип для аргумента C, однако это ограничение совершенно необязательно. В качестве альтернативы можно предложить изменить метод так, что бы
он стал генериком:
<T> boolean addAll(Collection<T> c)
Эта версия менее гибка, однако можно отметить, что типовый параметр использован только один раз в его сигнатуре. Это отражает тот факт, что типовый параметр
не используется для выражения какой-либо внутренней зависимости между типом(ами) аргумента(ов), возвращаемым типом и\или выбрасываемыми типами.
При отсутствии такой внутренней зависимости генерик методы считаются плохим
стилем кодирования, и более предпочтительно использование масок типов.
Reference(T referent, ReferenceQueue<? super T> queue);
В этом примере аргумент referent может быть вставлен в любой объект queue (очередь), чей элементный тип – это супертип типа T аргумента referent; T является
нижней границей для маски типа.
Два типовых аргумента явно различаются, если верно одно из следующих утверждений:
аргумент является или типовой переменной, или маской типа,
и эти два аргумента не являются одинаковыми типами;
один типовый аргумент – это типовая переменная или маска типа
с верхней границей (полученной, если необходимо, в результате capture
conversion) типа S; другой типовый аргумент T – это не типовая переменная или маск
а типа; кроме того, не выполняется ни |S| <: |T| ни |T| <: |S|.
каждый типовый аргумент – это типовая переменная или маска
типа с верхней границей (полученной, если необходимо, в результате
capture conversion) типов S и T и не выполняется ни |S| <: |T| ни |T| <: |S|.
Говорят, что типовый аргумент T1 содержит другой типовый аргумент T2, что записывает
ся как T2 <= T1, если можно доказать, что набор
типов, определённый в T2 – это поднабор того набора, который определен
типовым аргументом T1 с учетом рефлексивной и транзитивной замкнутости следующих правил (здесь <: определяет отношения между типом
и подтипом (§4.10) ):
? extends T <= ? extends S если T <: S
? super T <= ? super S если S <: T
32
T <= T
T <= ? extends T
T <= ? super T

Отношения между масками типов и существующей теорией типов
является интересным вопросом, который мы кратко упомянем здесь. Маски
типов – это ограниченная форма существующих(?) типов. Объявление генерик типа G<T extends B>, G<?> – это примерный аналог для записи Some
X<: B. G<X>.
Исторически, маски типов берут свои корни в работе Ацуши Игараши и Мирко Вироли. Заинтересованный в более всестороннем обсуждении
читатель может обратиться к работе On Variance-Based Subtyping for
Parametric Types (Atsushi Igarashi, Mirko Viroli), опубликованной
в Proceedings of the 16th European Conference on Object Oriented
Programming (ECOOP 2002). Эта работа, в свою очередь, основывается на
работе Кристен Торуп и Мэдса Торгерсена (Kresten Thorup, Mads Torgersen,
Unifying Genericity, ECOOP 99), так же как и традиционные работы, посвященные вариациям, описаным при объявлении, которые являются отсылкой
к Pierre America's work on POOL (OOPSLA 89).
Маски типов в конкретных деталях отличаются от конструкций, описанных в упомянутых статьях, в частности, в отношении использования capture conversion вместо использования операции close, описанной Игараши
и Вироли. Для получения формального описания масок типов можно обратиться к статье Wild FJ, написанной Mads Torgersen, Erik Ernst и Christian
Plesner Hansen, в рамках 12th workshop on Foundations of Object Oriented Programming (FOOL 2005).
2.5.2. Члены и Конструкторы Параметризованных Типов
Пусть С – это генерик класс или интерфейс с типовыми параметрами
A
,…,An и пусть C<T1,…,Tn > будет конкретным вызовом для C, где 1 ≤ i ≤
1
n, а T
– это тип (а не маска типа). Тогда:
i
пусть m будет объявлением члена или конструктора в классе C,
чей тип объявлен в T.
Тип для m в C<T
,…,Tn > будет следующий T[A1:=T1,...,An:=Tn];
1
пусть m будет объявлением члена или конструктора в D, где D –
это класс, от которого наследуется класс C или интерфейс, который расширяет класс C. Пусть D<U
,…,Uk > – это супертип для C<T1,…,Tn >, ко-
1
торый соответствует D.
,…,Tn > – это одновременно и тип для m
1
в D<U
Тип для m в C<T
,…,Uk >.
1
Если любой из типовых аргументов – это вызов класса C с использо-
ванием маски типа, тогда:
типы полей, методов и конструкторов в C<T1,…,Tn > – это тип
поля, метода и конструктора, получаемого в результате capture conversion
для C<T1,…,Tn>;
пусть D – это объявление класса или интерфейса (возможно, ге-
нерик объявление) в типе С. Тогда тип для D в C<T1,…,Tn > – это D, в котором, если D – это генерик, все аргументы – это неограниченные маски
типов.
33

В реальности указанные правила не имеют никакого значения, т.к. попросту невозможно получить доступ к члену параметризованного типа без выполнения
capture conversion, и невозможно использовать тип с маской типа в качестве аргумента после ключевого слова new в выражении создания объекта класса.
Единственным исключением к предыдущему параграфу является ситуация, когда параметризованый нестед тип используется как выражение для операции
instanceof, где capture conversion неприменимо.
2.6. Стирание Типов
Стирание типов – это процесс маппинга одного типа (возможно,
включающего параметризованный тип и типовые переменные) на другой
(который никогда не является параметризованным и не может иметь типовых переменных). Мы используем запись |T| для обозначения операции
стирания у типа |T|.
Стирание при маппинге определено следующим образом:
стирание типа у параметризованного типа G<T1,…,Tn > – это |G
|;
стирание типа у нестед типа T.C – это |T|.C;
стирание типа у массива T[]– это |T|[];
стирание типа у типовой переменной – это стирание до его самой
левой границы.
стирание типа у любого другого типа – это стирание самого типа.
Стирание типов – это также и маппинг сигнатуры одного конструктора или метода на другую сигнатуру, которая не имеет параметризованного типа или типовой переменной. Стирание сигнатуры метода или конструктора S представля
ет собой сигнатуру, состоящую из того же имени,
что и S, но с удалением из неё всех формальных типовых параметров, приведенных у S.
Типовый параметр конструктора или метода и возвращаемый тип
метода так
же подвергается стиранию, если была стерта сигнатура кон-
структора или метода.
Затертая сигнатура генерик метода не имеет типовых параметров.
2.7. Материализованные Типы
Поскольку информация о типах стирается в процессе компиляции, то
не вся она может быть доступна во время исполнения программы. Типы,
полностью доступные во время исполнения, известны как рафинированные
типы.
Тип считается рафинированным, если выполняется одно из следующего:
34
это объявление не-генерик класса или интерфейса;

это параметризованный тип, в котором все типовые аргументы
являются неограниченными масками типов;
это необработанный тип;
это примитивный тип;
это массив, чьи элементы рафинированы;
это нестед тип, в котором для каждого T, отделенного символом
“.”, T в свою очередь является рафинированным.
К примеру, если генерик класс X<T> имеет членский генерик класс Y<U>,
тогда тип X<?>.Y<?> является рафинированным типом, поскольку тип X<?> является рафинированным и Y<?> тоже рафинированный тип. Тип X<?>.Y<Object>
не рафинированный, поскольку Y<Object> не рафинированный.
Типовые пересечения не являются рафинированными.
Решение не делать все генерик типы рафинированными является одним из наиболее важных и спорных решений в отношении дизайна языка
в вопросах, касающихся системы типов языка программирования Java.
В конечном счете основной причиной для принятия такого решения
стала необходимость поддерживать обратную совместимость кода. Проще
говоря, принятое решение позволяет добавить в язык программирования новые конструкции, такие, как генерики, не повлияв при этом на тот код, который существовал ранее. Язык программирования Java сам по себе совместим
с предыдущими версиями до тех пор, пока каждая программа, написанная
в предыдущей версии, сохраняет свое значение в новой версии. Однако, это
замечание, которое можно назвать уточнением терминологии, представляет
собой чисто теоретический интерес. Реальные программы (даже такие тривиальные, как «Hello wolrd») состоят из нескольких юнитов компляции, часть
из которых предоставляется платформой Java SE (такие, как элементы пакетов java.lang или java.util). На практике минимальное требование к совместимости программ – чтобы программа, написанная для предыдущей версии, работала и в следующей версии платформы Java SE без внесения в эту программу изменений.
2.8. Необработанные Типы
С целью облегчения взаимодействия с унаследованным не-генерик
кодом в языке Java в качестве типа может быть использован результат выполнения операции стирания параметризованных типов или массивов,
чьими элементами являются параметризованные типы. Тип, полученный
в результате операции стирания, называется необработанным.
Говоря более точно, тип называется необработанным в том случае,
если он:
ссылочны
типа, но без указания списка типовых аргументов;
й тип, созданный путем использования имени генерик
35

массив, чьими элементами являются необработанные типы;
нестатический членский тип необработанного типа R, который не
унаследован от суперкласса или суперинтерфейса R.
Не-генерик тип не может являться необработанным типом.
Решение не делать все генерик типы рафинированными является одним
из наиболее важных и спорных Чтобы увидеть, почему не-static членский тип
необработанного типа также является необработанным, рассмотрим следующий пример:
class Outer <T> {
T t;
class Inner{
T setOuterT(T t1) {t=t1; return t;}
}
}
Тип членов класса Inner зависит от типового параметра класса Outer.
Если класс Outer является необработанным типом, Inner также должен учитываться как необработанный, так как в нем нет валидной привязки к переменной T.
Это правило применимо только к тем членам, которые не были унаследованы. Унаследованные члены, зависящие от типовых переменных, будут унаследованы уже необработанными типами как следствие того правила, что супертипы необработанных типов также являются необработанными (более подробно описывается ниже).
Другим последствием этого правила является то, что внутренний генерик класс необработанного типа, в свою очередь, может использоваться только
как необработанный тип:
class Outer <T> {
class Inneеr<S>{
S s;
}
}
Невозможно получить доступ к классу Inner как к частично необработанному типу («rare» тип):
Outer.Inner <Double> x=null; //неверно
Double d=x.s;
так как класс Outer является необработанным, то и все его внутренние классы,
включая Inner, также необработанные, а значит, и невозможно получить доступ к его типовым аргументам.
Суперклассы (и, соответсвенно, суперинтерфейсы) необработанных
типов, в свою очередь, являются стертыми типами их суперклассов (или
суперинтерфейсов) для любых их параметризованных вызовов.
36

Для необработанного типа С, который не унаследован от суперкласса или суперинтерфейса, тип его конструктора, нестатического метода
или нестатического поля M – это необработанный тип, который соответствует результату стирания его типа в генерик объявлении соответсвуюещего С.
Тип статического метода или статического поля необработанного
типа |С| – это тот же тип, что и в его генерик объявлении соответствующего С.
Во время компиляции про
исходит ошибка, если производится
попытка доступа к типовым аргументам не-static члена типа необработанного типа, который не унаследован от суперкласса или суперинтерфейса.
Во время компиляции происходит ошибка, если производится попытка использования членского типа параметризованного типа как необработанного типа.
Это означает, что запрет на использование «rare» типов распространяется и на те случаи, когда указываемый тип параметризован, но мы производим попытку использовать внутренний тип как необработанный тип:
Outer<Integer>.Inner x=null; //неверно
Этот случай – противоположность тому, что был осбужден ранее. Не
существует какого-либо практического оправдания такого половинно параметризованного типа. В унаследованном коде не может быть никаких типовых
аргументов. В неунаследованном коде мы должны использовать генерик типы
корректно и передавать все требуемые типовые аргументы.
Супертипом класса может быть необработанный тип. Доступ к членам этого класса считается нормальным, а доступ к членам супертипа рассматривается как доступ к необработанному типу. В конструкторе класса
вызов super считается вызовом метода у необработанного типа.
Использование необработанных типов допустимо только как компромисс обеспечения совместимости с унаследованным кодом. Использование нео
бработанных типов в коде, написанном после введения генериков
в язык программирования Java строго не рекомендуется. Вполне возможно, что в будущих версиях языка Java использование необработанных типов будет запрещено.
Для того чтобы убедиться, что потенциальные угрозы правилам работы с типами всегда обнаруживаются, некоторые категории обращений
к членам необработанных типов будут результироваться в виде анчекед
ворнингов во время компиляции.
Правила для анч
екед ворнингов при доступе к конструкторам или
членам необработанных типов следующие:
37

При доступе к полям: если тип леовостороннего операнда – это
необработанный тип, тогда, если стирание типа изменит тип поля, то возникает анчекед ворнинг времени компиляции.
При вызове метода или конструктора: если тип искомого класса
или интерфейса – это необработанный тип, тогда, если стирание типа изменяет любой из типов параметров метода или конструктора, то п
роисхо-
дит анчекед ворнинг.
Никаких анчекед ворнингов во время компиляции не происхо-
дит в тех случаях, когда при вызове метода никакие формальные параметры не изменятся во время стирания (даже если изменится возвращаемый тип и\или типы выражения throws), когда производится чтение из
поля и когда созд
ается экземпляр класса, относящегося к необработан-
ным типам.
Заметим, что описанные ранее анчекед ворнингс отличаются от тех анчекед
ворнингов, которые возникают для анчекед конверсий, кастов, объявлений методов и вызовов методов с аргументами переменной длины.
Описанные здесь ворнинги покрывают те случаи, когда унаследованный потребитель использует библиотеку, которая была переписана под генерики. К примеру, библиотека объявляет генрик класс Foo<T extends String>, который имеет
поле f типа Vector<T>, но потребитель назначает вектор целых чисел в e.f, где e
относится к необработанному типу Foo. Унаследованный потребитель получает
ворнинг, потому что он может вызвать засорение кучи для тех потребителей
и библиотек, которые написаны с использованием генериков.
(Заметим, что унаследованный потребитель может назначить Vector<String> из
библиотеки в его собственную переменную типа Vector, и не получить при этом
ворнинг. Возможность назначить переменной необработанного типа значение
любого его параметризованного варианта существует благодаря правилам
о подтипах языка программирования Java.
Ворнинги от анчекед конверсий покрывают и обратный случай, когда написанный на генериках потребитель использует унаследованную библиотеку. К примеру, метод библиотеки в качестве возвращаемого типа имеет необработанный тип Vector, а потребитель назначает результат вызова метода переменной типа Vector<String>. Это
небезопасно, так как необработанный вектор может иметь элементы
другого типа, чем String, но все же разрешено с использованием анчекед конверсий, с целью позволить вза
ным кодом. Ворнинги от анчекед конверсий показывают, что у потребителя написанного на генериках могут возникнуть проблемы от
засорения кучи в других точках программы.
имодействие с унаследован-
38

Пример 2.8-1. Необработанные Типы
class Cell<E> {
E value;
Cell(E v) { value = v; }
E get() { return value; }
void set(E v) { value = v; }
public static void main(String[] args) {
Cell x = new Cell<String>("abc");
System.out.println(x.value); // OK, имеет тип Object
System.out.println(x.get()); // OK, имеет тип Object
x.set("def"); // unchecked warning
}
}
Пример 2.8-2. Необработанные Типы и Наследование
import java.util.*;
class NonGeneric {
Collection<Number> myNumbers() { return null; }
}
abstract class RawMembers<T> extends NonGeneric
implements Collection<String> {
static Collection<NonGeneric> cng =
new ArrayList<NonGeneric>();
public static void main(String[] args) {
RawMembers rw = null;
Collection<Number> cn = rw.myNumbers(); // OK
Iterator<String> is = rw.iterator(); // Unchecked warning
Collection<NonGeneric> cnn = rw.cng; // OK,
статический член
}
}
В этой программе класс RawMembers<T> наследует метод:
Iterator<String> iterator()
из суперинтерфейса Collection<String>. Однако, тип RawMembers наследует iterator() от затертого, Collection<String> что означает, что
возвращаемый тип метода iterator() – это результат стирания Itera-
tor<String>, т.е. просто Iterator.
В результате попытки присвоения результата метода rw.iterator() потребуется выполнение анчекед конверсии из Iterator в Iterator<String>,
что вызовет анчекед ворнинг.
39

В противоположность этому, статический член cng сохраняет его полный параметризованный тип даже тогда, когда к нему обращаются через объект необработанного типа. (Заметим, что доступ к статическому члену через экземпляр
объекта считается плохим тилем программирования и путает читающего программу). Член myNumbers наследуется из класса NonGeneric (результат стирания которого это также NonGeneric), и, таким образом, полностью сохраняет
свой параметризованный тип.
Необработанные типы тесно связаны с масками типов. И то и другое
основывается на экзистенциальных типах. Необработанные типы могут рассматриваться как маски типов, чьи правила на типы заведово необоснованы,
для обеспечения взаимодействия с унаследованным кодом. Исторически
необработанные типы предшествовали маскам типов; впервые они были
представлены в GJ и описаны в статье Making the future safe for the past:
Adding Genericity to the Java Programming Language, за авторством Gilad
Bracha, Martin Odersky, David Stoutamire, и Philip Wadler, в сборнике ACM
Conference on Object-Oriented Programming, Systems, Languages and
Applications (OOPSLA 98), Октябрь 1998.
2.9. Типовое Пересечение
Типовое пересечение принимает форму T
& ... & Tn (n > 0), где Ti
1
(1 ≤ i ≤ n) – это выражение типа.
Типовое пересечение возникает в процессе выполнения capture
conversion и type inference. Невозможно написать типовое пересечение
непосредственно как часть программы, так как нет синтаксиса, который бы
это поддерживал.
Значение типового пересечения – это те объекты, которые являются
значениями всех типов T
Члены типового пересечения T
, где 1 ≤ i ≤ n.
i
& ... & Tn определяются следующим
1
образом:
Для каждого T
классом или массивом, таким, что T
кое T
<: Ck что Ck <: Ci для любого i (1 ≤ i ≤ n), иначе происходит ошибка
k
(1 ≤ i ≤ n), пусть Ci будет наиболее специфичным
i
<: Ci. Тогда должно существовать та-
i
компиляции.
Для 1 ≤ j ≤ n, если T
– это типовая переменная, тогда пусть Tj' бу-
j
дет интерфейсом, чьи члены – те же самые, что и публичные члены у T
иначе, если T
– это интрефейс, тогда пусть Tj' будет Tj.
j
Тогда типовое пересечение имеет те же члены, что и класс с пу-
стым телом, непосредственный суперкласс C
перинтрефейс T
', ..., Tn', объявленные в том же пакете, в котором появля-
1
и непосредственный су-
k
ется типовое пересечение.
Имеет смысл затронуть вопрос различия между типовыми пересечениями
и границами типовых переменных. Каждая граница типовой переменной приводит к появлению типового пересечения. Эти типовые пересечения зачастую
40
;
j
Соседние файлы в предмете [НЕСОРТИРОВАННОЕ]
