Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Технологии обработки информации на Java. Учебное пособие

.pdf
Скачиваний:
0
Добавлен:
08.09.2026
Размер:
2 Мб
Скачать
ГраницыМаскиТипа:
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> и предназначен для того, чтобы добавлять все элементы его входного аргумента в коллекцию, для ко­торой происходит вызов. Естественным желанием было бы использовать Collec­tion<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).
Маски типов в конкретных деталях отличаются от конструкций, опи­санных в упомянутых статьях, в частности, в отношении использования cap­ture conversion вместо использования операции close, описанной Игараши и Вироли. Для получения формального описания масок типов можно обра­титься к статье Wild FJ, написанной Mads Torgersen, Erik Ernst и Christian
Plesner Hansen, в рамках 12th workshop on Foundations of Object Oriented Pro­gramming (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(); // Un­checked 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
Соседние файлы в предмете [НЕСОРТИРОВАННОЕ]