Глава 4. Типы, значения и переменные
Оглавление
Язык программирования Java — это статически типизированный язык, что означает, что каждая переменная и каждое выражение имеет тип, известный на этапе компиляции.
Язык программирования Java также является строго типизированным языком, поскольку типы ограничивают значения, которые может хранить переменная (§4.12), или которые может производить выражение, ограничивают поддерживаемые операции над этими значениями и определяют смысл операций. Сильная статическая типизация помогает обнаруживать ошибки на этапе компиляции.
Типы языка программирования Java делятся на две категории: примитивные типы и ссылочные типы. Примитивные типы (§4.2) — это тип boolean и числовые типы. Числовые типы — это целочисленные типы byte, short, int, long и char, и типы с плавающей точкой float и double. Ссылочные типы (§4.3) — это типы классов, типы интерфейсов и типы массивов. Существует также специальный тип null. Объект (§4.3.1) — это динамически созданный экземпляр типа класса или динамически созданный массив. Значения ссылочного типа — это ссылки на объекты. Все объекты, включая массивы, поддерживают методы класса Object (§4.3.2). Строковые литералы представляются объектами String (§4.3.3).
Существует два вида типов в языке программирования Java: примитивные типы (§4.2) и ссылочные типы (§4.3). Соответственно, существуют два вида значений данных, которые могут храниться в переменных, передаваться в качестве аргументов, возвращаться методами и над которыми выполняются операции: примитивные значения (§4.2) и ссылочные значения (§4.3).
Существует также специальный тип null, тип выражения null (§3.10.7, §15.8.1), который не имеет имени.
Поскольку тип null не имеет имени, невозможно объявить переменную типа null или выполнить приведение к типу null.
Нулевая ссылка является единственно возможным значением выражения типа null.
Нулевая ссылка всегда может быть присвоена или приведена к любому ссылочному типу (§5.2, §5.3, §5.5).
На практике программист может игнорировать тип null и просто предположить, что null — это просто специальный литерал, который может быть любого ссылочного типа.
Примитивный тип предопределяется языком программирования Java и называется его зарезервированным ключевым словом (§3.9):
byte short int long char float double Примитивные значения не разделяют состояние с другими примитивными значениями.
Числовые типы — это целочисленные типы и типы с плавающей точкой.
Целочисленные типы — это byte, short, int и long, значения которых соответственно являются 8-битными, 16-битными, 32-битными и 64-битными знаковыми целыми числами в дополнительном коде, и char, значения которого являются 16-битными беззнаковыми целыми числами, представляющими код UTF-16 (§3.1).
Типы с плавающей точкой — это float, чьи значения включают 32-битные числа с плавающей точкой IEEE 754, и double, чьи значения включают 64-битные числа с плавающей точкой IEEE 754.
Тип boolean имеет ровно два значения: true и false.
Значения целочисленных типов находятся в следующих диапазонах:
-
Для
byte, от -128 до 127 включительно -
Для
short, от -32768 до 32767 включительно -
Для
int, от -2147483648 до 2147483647 включительно -
Для
long, от -9223372036854775808 до 9223372036854775807 включительно -
Для
char, от'\u0000'до'\uffff'включительно, то есть от 0 до 65535
Язык программирования Java предоставляет ряд операторов, которые действуют над целочисленными значениями:
-
Операторы сравнения, которые приводят к значению типа
boolean: -
Числовые операторы, которые приводят к значению типа
intилиlong:-
Мультипликативные операторы
*,/и%(§15.17) -
Аддитивные операторы
+и-(§15.18) -
Оператор инкремента
++, как префиксный (§15.15.1), так и постфиксный (§15.14.2) -
Оператор декремента
--, как префиксный (§15.15.2), так и постфиксный (§15.14.3) -
Операторы сдвига со знаком и без знака
<<,>>и>>>(§15.19) -
Оператор побитового дополнения
~(§15.15.5) -
Битовые операторы для целых чисел
&,^и|(§15.22.1)
-
Условный оператор
? :(§15.25) -
Оператор приведения типа (§15.16), который может преобразовывать целочисленное значение в значение любого заданного числового типа
-
Оператор конкатенации строк
+(§15.18.1), который, когда получает операнд типаStringи целочисленный операнд, преобразует целочисленный операнд в строкуString(десятичная форма операндаbyte,short,intилиlong, или символ операндаchar), а затем создает новую строкуString, которая является конкатенацией двух строк
Другие полезные конструкторы, методы и константы определены в классах Byte, Short, Integer, Long и Character.
Если оператор целых чисел, кроме оператора сдвига, имеет хотя бы один операнд типа long, то операция выполняется с точностью 64 бита, и результат числового оператора имеет тип long. Если другой операнд не long, он сначала расширяется (§5.1.5) до типа long с помощью числового повышения (§5.6).
В противном случае операция выполняется с точностью 32 бита, и результат числового оператора имеет тип int. Если ни один из операндов не является int, он сначала расширяется до типа int с помощью числового повышения.
Любое значение любого целочисленного типа может быть приведено к любому числовому типу или из него. Нет приведений между целочисленными типами и типом boolean.
См. §4.2.5 для образца преобразования целочисленных выражений в boolean.
Операторы целых чисел никак не указывают переполнение или недополнение.
Оператор целых чисел может выбросить исключение (§11 (Исключения)) по следующим причинам:
-
Любой оператор целых чисел может выбросить исключение
NullPointerException, если требуется преобразование unboxing (§5.1.8) нулевого ссылочного значения. -
Операторы целочисленного деления
/(§15.17.2) и целочисленного остатка%(§15.17.3) могут выбросить исключениеArithmeticException, если операнд правой части равен нулю. -
Операторы инкремента и декремента
++(§15.14.2, §15.15.1) и--(§15.14.3, §15.15.2) могут выбросить исключениеOutOfMemoryError, если требуется преобразование boxing (§5.1.7), и нет достаточно памяти для выполнения преобразования.
Пример 4.2.2-1. Операции с целыми числами
class Test {
public static void main(String[] args) {
int i = 1000000;
System.out.println(i * i);
long l = i;
System.out.println(l * l);
System.out.println(20296 / (l - i));
}
}
Эта программа выводит:
-727379968 1000000000000
а затем сталкивается с исключением ArithmeticException при делении на l - i, так как l - i равно нулю. Первое умножение выполняется с точностью 32 бита, в то время как второе умножение является умножением long. Значение -727379968 — это десятичное значение низких 32 битов математического результата, 1000000000000, которое слишком велико для типа int.
Типы с плавающей точкой — это float и double, которые концептуально связаны с однократным 32-битным и двойным 64-битным форматом значениями и операциями IEEE 754, как указано в Стандарте IEEE для арифметики с плавающей запятой, ANSI/IEEE Standard 754-1985 (IEEE, Нью-Йорк).
Стандарт IEEE 754 включает не только положительные и отрицательные числа, состоящие из знака и модуля, но также положительные и отрицательные нули, положительные и отрицательные бесконечности и специальные значения Not-a-Number (далее сокращенно NaN). Значение NaN используется для представления результата некоторых недопустимых операций, таких как деление нуля на ноль. Постоянные NaN обоих типов float и double предварительно определены как Float.NaN и Double.NaN.
Каждая реализация языка программирования Java обязана поддерживать два стандартных набора значений с плавающей точкой, называемые множеством значений с плавающей точкой float и множеством значений с плавающей точкой double. Кроме того, реализация языка программирования Java может поддерживать один или оба расширенных набора значений с плавающей точкой с расширенным порядком, называемые множеством значений с плавающей точкой float с расширенным порядком и множеством значений с плавающей точкой double с расширенным порядком. Эти наборы значений с расширенным порядком могут в определенных обстоятельствах использоваться вместо стандартных наборов значений для представления значений выражений типа float или double (§5.1.13, §15.4).
Все конечные отличные от нуля значения любого набора значений с плавающей точкой могут быть выражены в форме s ⋅ m ⋅ 2(e - N + 1), где s равно +1 или -1, m — положительное целое число, меньшее 2N, а e — целое число, находящееся в пределах от Emin = -(2K-1-2) до Emax = 2K-1-1 включительно, и где N и K — параметры, которые зависят от набора значений. Некоторые значения могут быть представлены в этой форме более чем одним способом; например, предположим, что значение v в наборе значений может быть представлено в этой форме с помощью определенных значений для s, m и e, то если m было бы чётным и e было бы меньше 2K-1, то можно было бы уменьшить m вдвое и увеличить e на 1, чтобы получить второе представление для того же значения v. Представление в этой форме называется нормализованным, если m ≥ 2N-1; в противном случае представление называется денормализованным. Если значение в наборе значений нельзя представить таким образом, что m ≥ 2N-1, то это значение называется денормализованным значением, потому что у него нет нормализованного представления.
Ограничения на параметры N и K (и на выведенные параметры Emin и Emax) для двух требуемых и двух необязательных наборов значений с плавающей точкой суммируются в таблице 4.2.3-A.
Таблица 4.2.3-A. Параметры набора значений с плавающей точкой
| Параметр | float | float с расширенным порядком | double | double с расширенным порядком |
|---|---|---|---|---|
| N | 24 | 24 | 53 | 53 |
| K | 8 | ≥ 11 | 11 | ≥ 15 |
| Emax | +127 | ≥ +1023 | +1023 | ≥ +16383 |
| Emin | -126 | ≤ -1022 | -1022 | ≤ -16382 |
Если реализация поддерживает один или оба расширенных набора значений с расширенным порядком, то для каждого поддерживаемого набора значений с расширенным порядком существует специфическая константа K, значение которой ограничено таблицей 4.2.3-A; это значение K в свою очередь определяет значения для Emin и Emax.
Каждый из четырех наборов значений включает не только конечные значения, отличные от нуля, приписанные ему выше, но также значения NaN и четыре значения: положительный ноль, отрицательный ноль, положительная бесконечность и отрицательная бесконечность.
Обратите внимание, что ограничения в таблице 4.2.3-A разработаны таким образом, чтобы каждый элемент набора значений с плавающей точкой float обязательно также являлся элементом набора значений float с расширенным порядком, набора значений double и набора значений double с расширенным порядком. Точно так же каждый элемент набора значений double обязательно также является элементом набора значений double с расширенным порядком. Каждый набор значений с расширенным порядком имеет больший диапазон значений порядка, чем соответствующий стандартный набор значений, но не имеет большей точности.
Элементы набора значений float — это точно те значения, которые можно представить с помощью однократного формата с плавающей точкой, определенного в стандарте IEEE 754. Элементы набора значений double — это точно те значения, которые можно представить с помощью двойного формата с плавающей точкой, определенного в стандарте IEEE 754. Однако следует отметить, что элементы наборов значений float с расширенным порядком и double с расширенным порядком, определенные здесь, не соответствуют значениям, которые можно представить с помощью расширенных форматов IEEE 754 single и double соответственно.
Наборы значений float, float с расширенным порядком, double и double с расширенным порядком — это не типы. Для реализации языка программирования Java всегда верно использовать элемент набора значений float для представления значения типа float; однако в определенных участках кода может быть допустимо использовать элемент набора значений float с расширенным порядком вместо него. Аналогично, всегда верно использовать элемент набора значений double для представления значения типа double; однако в определенных участках кода может быть допустимо использовать элемент набора значений double с расширенным порядком вместо него.
За исключением NaN, значения с плавающей точкой упорядочены; от меньшего к большему они располагаются следующим образом: отрицательная бесконечность, отрицательные конечные значения, отличные от нуля, положительный и отрицательный ноль, положительные конечные значения, отличные от нуля, и положительная бесконечность.
IEEE 754 допускает несколько различных значений NaN для каждого из его форматов с одинарной и двойной плавающей точкой. Хотя каждая архитектура аппаратного обеспечения возвращает определенную битовую последовательность для NaN при генерации нового NaN, программист также может создавать NaN с различными битовыми последовательностями для кодирования, например, ретроспективной диагностической информации.
По большей части платформа Java SE обрабатывает значения NaN данного типа так, как будто они сводятся к одному каноническому значению, и поэтому это описание обычно относится к произвольному NaN так, как будто к каноническому значению.
Однако в версии 1.3 платформы Java SE были введены методы, позволяющие программисту различать значения NaN: методы Float.floatToRawIntBits и Double.doubleToRawLongBits. Заинтересованный читатель может обратиться к спецификациям классов Float и Double для получения дополнительной информации.
Положительный и отрицательный ноль равны; таким образом, результат выражения 0.0==-0.0 равен true, а результат 0.0>-0.0 — ложно. Но другие операции могут различать положительный и отрицательный ноль; например, 1.0/0.0 имеет значение положительной бесконечности, а значение 1.0/-0.0 — отрицательной бесконечности.
NaN неупорядочен, поэтому:
-
Операторы численного сравнения
<,<=,>и>=возвращаютfalse, если хотя бы один из операндов или оба являются NaN (§15.20.1).В частности,
(x<y) == !(x>=y)будетfalse, еслиxилиyявляется NaN. -
Оператор равенства
==возвращаетfalse, если хотя бы один из операндов является NaN. -
Оператор неравенства
!=возвращаетtrue, если хотя бы один из операндов является NaN (§15.21.1).В частности,
x!=xявляетсяtrueтогда и только тогда, когдаxявляется NaN.
Язык программирования Java предоставляет ряд операторов, действующих над значениями с плавающей точкой:
-
Операторы сравнения, которые возвращают значение типа
boolean: -
Числовые операторы, которые возвращают значение типа
floatилиdouble: -
Условный оператор
? :(§15.25) -
Оператор приведения типа (§15.16), который может преобразовать значение с плавающей точкой в значение любого указанного числового типа
-
Оператор конкатенации строк
+(§15.18.1), который, когда получаетStringоперанд и операнд с плавающей точкой, преобразует операнд с плавающей точкой вString, представляющий его значение в десятичном формате (без потерь информации), а затем создает новуюStringпутем конкатенации двух строк
Другие полезные конструкторы, методы и константы определены в классах Float, Double и Math.
Если хотя бы один из операндов бинарного оператора имеет тип с плавающей точкой, то операция является операцией с плавающей точкой, даже если другой — целочисленный.
Если хотя бы один из операндов числового оператора имеет тип double, то операция выполняется с использованием 64-битной арифметики с плавающей точкой, и результат числового оператора является значением типа double. Если другой операнд не является double, он сначала расширяется (§5.1.5) до типа double с помощью числового продвижения (§5.6).
В противном случае операция выполняется с использованием 32-битной арифметики с плавающей точкой, и результат числового оператора является значением типа float. (Если другой операнд не является float, он сначала расширяется до типа float с помощью числового продвижения.)
Любое значение типа с плавающей точкой может быть приведено к любому числовому типу или из него. Преобразования между типами с плавающей точкой и типом boolean отсутствуют.
См. §4.2.5 для примера преобразования выражений с плавающей точкой в boolean.
Операторы над числами с плавающей точкой ведут себя так, как указано в стандарте IEEE 754 (за исключением оператора остатка (§15.17.3)). В частности, язык программирования Java требует поддержки стандарта IEEE 754 денормализованных чисел с плавающей точкой и постепенного уменьшения, что облегчает доказательство желаемых свойств определенных численных алгоритмов. Операции с плавающей точкой не «сбрасывают к нулю» результат, если полученное значение является денормализованным числом.
Язык программирования Java требует, чтобы арифметика с плавающей точкой вела себя так, как будто каждый оператор с плавающей точкой округлял свой результат до точности результата. Неточные результаты должны быть округлены до ближайшего представимого значения к бесконечно точному результату; если два ближайших представимых значения одинаково близки, выбирается то, у которого младший бит равен нулю. Это стандартный режим округления IEEE 754, известный как округление до ближайшего.
Язык программирования Java использует округление к нулю при преобразовании значения с плавающей точкой в целое число (§5.1.3), который в этом случае действует так, как если бы число было усечено, отбрасывая биты мантиссы. Округление к нулю выбирает в качестве результата значение формата, наиболее близкое к бесконечно точному результату и не большее по величине.
Операция с плавающей точкой, которая переполняется, даёт результат бесконечности соответствующего знака.
Операция с плавающей точкой, которая недополняется, даёт денормализованное значение или нуль соответствующего знака.
Операция с плавающей точкой, у которой нет математически определенного результата, даёт NaN.
Все числовые операции с NaN в качестве операнда дают NaN в качестве результата.
Оператор с плавающей точкой может выбросить исключение (§11 (Исключения)) по следующим причинам:
-
Любой оператор с плавающей точкой может выбросить
NullPointerException, если необходимо выполнить распаковку (§5.1.8) нулевого ссылки. -
Операторы инкремента и декремента
++(§15.14.2, §15.15.1) и--(§15.14.3, §15.15.2) могут выброситьOutOfMemoryError, если требуется упаковка (§5.1.7) и для выполнения преобразования недостаточно памяти.
Пример 4.2.4-1. Операции с плавающей точкой
class Test {
public static void main(String[] args) {
// An example of overflow:
double d = 1e308;
System.out.print("overflow produces infinity: ");
System.out.println(d + "*10==" + d*10);
// An example of gradual underflow:
d = 1e-305 * Math.PI;
System.out.print("gradual underflow: " + d + "\n ");
for (int i = 0; i < 4; i++)
System.out.print(" " + (d /= 100000));
System.out.println();
// An example of NaN:
System.out.print("0.0/0.0 is Not-a-Number: ");
d = 0.0/0.0;
System.out.println(d);
// An example of inexact results and rounding:
System.out.print("inexact results with float:");
for (int i = 0; i < 100; i++) {
float z = 1.0f / i;
if (z * i != 1.0f)
System.out.print(" " + i);
}
System.out.println();
// Another example of inexact results and rounding:
System.out.print("inexact results with double:");
for (int i = 0; i < 100; i++) {
double z = 1.0 / i;
if (z * i != 1.0)
System.out.print(" " + i);
}
System.out.println();
// An example of cast to integer rounding:
System.out.print("cast to int rounds toward 0: ");
d = 12345.6;
System.out.println((int)d + " " + (int)(-d));
}
}
Эта программа выводит:
overflow produces infinity: 1.0E308*10==Infinity
gradual underflow: 3.141592653589793E-305
3.1415926535898E-310 3.141592653E-315 3.142E-320 0.0
0.0/0.0 is Not-a-Number: NaN
inexact results with float: 0 41 47 55 61 82 83 94 97
inexact results with double: 0 49 98
cast to int rounds toward 0: 12345 -12345
Этот пример демонстрирует, среди прочего, что постепенное уменьшение может привести к постепенной потере точности.
Результаты, когда i равняется 0, связаны с делением на ноль, так что z становится положительной бесконечностью, а z * 0 — NaN, которое не равно 1.0.
Тип boolean представляет логическую величину с двумя возможными значениями, обозначаемыми литералами true и false (§3.10.3).
Операторы для boolean:
-
Операторы сравнения
==и!=(§15.21.2) -
Оператор логического отрицания
!(§15.15.6) -
Логические операторы
&,^и|(§15.22.2) -
Операторы условной конъюнкции и условного дизъюнкции
&&(§15.23) и||(§15.24) -
Условный оператор
? :(§15.25) -
Оператор конкатенации строк
+(§15.18.1), который, при применении к операндуStringи операндуboolean, преобразует операндbooleanвString(либо"true", либо"false"), а затем создаёт новую строку, являющуюся конкатенацией двух строк
Булевы выражения определяют поток управления в нескольких типах операторов:
Выражение boolean также определяет, какое подвыражение вычисляется в условном ? : операторе (§15.25).
Только boolean и Boolean выражения могут быть использованы в операторах управления потоком и в качестве первого операнда условного оператора ? :.
Целочисленное или дробное выражение x может быть преобразовано в значение boolean, следуя соглашению языка C, где любое ненулевое значение является true, при помощи выражения x!=0.
Ссылка на объект obj может быть преобразована в значение boolean, следуя соглашению языка C, где любая ссылка, отличная от null, является true, при помощи выражения obj!=null.
Значение boolean может быть преобразовано в String путём преобразования в строку (§5.4).
Значение boolean может быть приведено к типу boolean, Boolean или Object (§5.5). Другие приведения к типу boolean не допускаются.
Существует четыре типа типов ссылок: типы классов (§8.1), типы интерфейсов (§9.1), переменные типов (§4.4) и типы массивов (§10.1).
Пример кода:
class Point { int[] metrics; }
interface Move { void move(int deltax, int deltay); }
объявляет тип класса Point, тип интерфейса Move и использует тип массива int[] (массив int) для объявления поля metrics класса Point.
Тип класса или интерфейса состоит из идентификатора или последовательности идентификаторов, разделённых точками, где каждый идентификатор может быть необязательно последован аргументами типа (§4.5.1). Если аргументы типа встречаются где-либо в типе класса или интерфейса, то это параметризованный тип (§4.5).
Каждый идентификатор в типе класса или интерфейса классифицируется как имя пакета или имя типа (§6.5.1). Идентификаторы, которые классифицируются как имена типов, могут быть аннотированы. Если тип класса или интерфейса имеет вид T.id (необязательно последован аргументами типа), то id должно быть простым именем доступного вложенного типа T (§6.6, §8.5, §9.5), в противном случае произойдет ошибка компиляции. Тип класса или интерфейса обозначает этот вложенный тип.
Объект — это экземпляр класса или массив.
Значения ссылок (часто просто ссылки) — это указатели на эти объекты, и специальная ссылка null, которая не ссылается на какой-либо объект.
Экземпляр класса явно создаётся выражением создания экземпляра класса (§15.9).
Массив явно создаётся выражением создания массива (§15.10.1).
Другие выражения могут неявно создавать экземпляр класса (§12.5) или массив (§10.6).
Пример 4.3.1-1. Создание объекта
class Point {
int x, y;
Point() { System.out.println("default"); }
Point(int x, int y) { this.x = x; this.y = y; }
/* A Point instance is explicitly created at
class initialization time: */
static Point origin = new Point(0,0);
/* A String can be implicitly created
by a + operator: */
public String toString() { return "(" + x + "," + y + ")"; }
}
class Test {
public static void main(String[] args) {
/* A Point is explicitly created
using newInstance: */
Point p = null;
try {
p = (Point)Class.forName("Point").newInstance();
} catch (Exception e) {
System.out.println(e);
}
/* An array is implicitly created
by an array constructor: */
Point a[] = { new Point(0,0), new Point(1,1) };
/* Strings are implicitly created
by + operators: */
System.out.println("p: " + p);
System.out.println("a: { " + a[0] + ", " + a[1] + " }");
/* An array is explicitly created
by an array creation expression: */
String sa[] = new String[2];
sa[0] = "he"; sa[1] = "llo";
System.out.println(sa[0] + sa[1]);
}
}
Эта программа выводит:
default
p: (0,0)
a: { (0,0), (1,1) }
hello
Операции над ссылками на объекты:
-
Доступ к полям, используя либо квалифицированное имя (§6.6), либо выражение доступа к полю (§15.11)
-
Вызов метода (§15.12)
-
Оператор конкатенации строк
+(§15.18.1), который, при наличии операнда ссылки и ссылки, преобразует ссылку в строку, вызвав методtoStringсвязанного объекта (используя"null", если либо ссылка, либо результатtoStringравна null), а затем создаёт новую строку, которая является конкатенацией двух строк. -
Оператор
instanceof(§15.20.2) -
Операторы равенства ссылок
==и!=(§15.21.3) -
Условный оператор
? :(§15.25).
Может быть множество ссылок на один и тот же объект. Большинство объектов имеют состояние, хранящееся в полях объектов, которые являются экземплярами классов, или в переменных, которые являются компонентами объекта массива. Если две переменные содержат ссылки на один и тот же объект, состояние объекта может быть изменено с помощью ссылки одной переменной на объект, а затем изменённое состояние может быть наблюдаемо через ссылку в другой переменной.
Пример 4.3.1-2. Примитивные и ссылочные идентификаторы
class Value { int val; }
class Test {
public static void main(String[] args) {
int i1 = 3;
int i2 = i1;
i2 = 4;
System.out.print("i1==" + i1);
System.out.println(" but i2==" + i2);
Value v1 = new Value();
v1.val = 5;
Value v2 = v1;
v2.val = 6;
System.out.print("v1.val==" + v1.val);
System.out.println(" and v2.val==" + v2.val);
}
}
Эта программа выводит:
i1==3 but i2==4 v1.val==6 and v2.val==6
потому что v1.val и v2.val ссылаются на одну и ту же переменную экземпляра (§4.12.3) в одном объекте Value, созданном единственным выражением new, в то время как i1 и i2 — это разные переменные.
Каждый объект связан с монитором (§17.1), который используется методами synchronized (§8.4.3) и оператором synchronized (§14.19) для управления одновременным доступом к состоянию несколькими потоками (§17 (Потоки и блокировки)).
Класс Object является суперклассом (§8.1.4) всех остальных классов.
Все типы классов и массивов наследуют (§8.4.8) методы класса Object, которые обобщаются следующим образом:
-
Метод
cloneиспользуется для создания дубликата объекта. -
Метод
equalsопределяет понятие равенства объектов, основанное на сравнении значений, а не ссылок. -
Метод
finalizeвыполняется непосредственно перед уничтожением объекта (§12.6). -
Метод
getClassвозвращает объектClass, который представляет класс объекта.Объект
Classсуществует для каждого типа ссылки. Он может быть использован, например, для определения полного квалифицированного имени класса, его членов, непосредственного суперкласса и всех реализуемых интерфейсов.Тип выражения вызова метода
getClass— этоClass<?extends|T|>, где T — это класс или интерфейс, который искалсяgetClass(§15.12.1), а |T| обозначает стирание T (§4.6).Метод класса, объявленный как
synchronized(§8.4.3.6), синхронизируется на мониторе, связанном с объектомClassкласса. -
Метод
hashCodeочень полезен, наряду с методомequals, в хеш-таблицах, таких какjava.util.HashMap. -
Методы
wait,notifyиnotifyAllиспользуются в конкурентном программировании с использованием потоков (§17.2). -
Метод
toStringвозвращаетStringпредставление объекта.
Экземпляры класса String представляют собой последовательности точек кода Юникода.
Объект String имеет постоянное (не изменяемое) значение.
Строковые литералы (§3.10.5) являются ссылками на экземпляры класса String.
Оператор конкатенации строк + (§15.18.1) неявно создаёт новый объект String, когда результат не является константным выражением (§15.28).
Два типа ссылок являются одним типом на этапе компиляции, если они объявлены в модулях компиляции, связанных с одним и тем же модулем (§7.3), имеют одинаковое бинарное имя (§13.1), и их аргументы типа, если таковые имеются, идентичны, применяя это определение рекурсивно.
Когда два типа ссылок совпадают, их иногда называют одним классом или одним интерфейсом.
Во время выполнения несколько типов ссылок с одинаковым бинарным именем могут быть загружены одновременно различными загрузчиками классов. Эти типы могут или не могут представлять один и тот же тип объявления. Даже если два таких типа представляют одно и то же объявление типа, они считаются отличными.
Два типа ссылок являются одним типом на этапе выполнения, если:
-
Они оба являются типами класса или обоих интерфейсов, определены одним и тем же загрузчиком классов и имеют одинаковое бинарное имя (§13.1), в этом случае их иногда называют одним классом на этапе выполнения или одним интерфейсом на этапе выполнения.
-
Они оба являются типами массивов, и их компоненты являются одним типом на этапе выполнения (§10 (Массивы)).
Переменная типа — это неопределённый идентификатор, используемый в качестве типа в телах классов, интерфейсов, методов и конструкторов.
Переменная типа вводится объявлением параметра типа для обобщённого класса, интерфейса, метода или конструктора (§8.1.2, §9.1.2, §8.4.4, §8.8.4).
Область действия переменной типа, объявленной как параметр типа, указана в §6.3.
Каждая переменная типа, объявленная как параметр типа, имеет границу. Если для переменной типа граница не объявлена, предполагается Object. Если граница объявлена, она состоит либо из:
-
единственной переменной типа T, или
-
типа класса или интерфейса T, за которым могут следовать типы интерфейсов I1
&...&In.
Ошибка компиляции, если какой-либо из типов I1 ... In является типом класса или переменной типа.
Стирания (§4.6) всех составляющих типов границы должны быть попарно различны, в противном случае произойдёт ошибка компиляции.
Переменная типа не может одновременно быть подтипом двух типов интерфейсов, которые являются различными параметризациями одного и того же обобщённого интерфейса, в противном случае произойдёт ошибка компиляции.
Порядок типов в границе важен только в том, что стирание переменной типа определяется первым типом в её границе, и что тип класса или переменная типа могут появляться только на первом месте.
Члены переменной типа X с границей T & I1 & ... & In — это члены пересечения типов (§4.9) T & I1 & ... & In, появляющиеся в точке объявления переменной типа.
Пример 4.4-1. Члены переменной типа
package TypeVarMembers;
class C {
public void mCPublic() {}
protected void mCProtected() {}
void mCPackage() {}
private void mCPrivate() {}
}
interface I {
void mI();
}
class CT extends C implements I {
public void mI() {}
}
class Test {
<T extends C & I> void test(T t) {
t.mI(); // OK
t.mCPublic(); // OK
t.mCProtected(); // OK
t.mCPackage(); // OK
t.mCPrivate(); // Compile-time error
}
}
Переменная типа T имеет те же члены, что и пересечение типов C & I, которое, в свою очередь, имеет те же члены, что и пустой класс CT, определённый в той же области видимости с эквивалентными надтипами. Члены интерфейса всегда public, и поэтому всегда наследуются (если не переопределены). Следовательно, mI является членом CT и T. Среди членов C все, кроме mCPrivate, наследуются CT, и поэтому являются членами как CT, так и T.
Если бы C был объявлен в другом пакете, чем T, то вызов mCPackage привёл бы к ошибке компиляции, так как этот член не был бы доступен в точке объявления T.
Объявление класса или интерфейса, являющегося обобщённым (§8.1.2, §9.1.2) определяет набор параметризованных типов.
Параметризованный тип — это тип класса или интерфейса вида C<T1,...,Tn>, где C — имя обобщённого типа, а <T1,...,Tn> — список аргументов типа, обозначающих конкретную параметризацию обобщённого типа.
Обобщённый тип имеет параметры типа F1,...,Fn с соответствующими границами B1,...,Bn. Каждый аргумент типа Ti параметризованного типа принимает все типы, которые являются подтипами всех типов, указанных в соответствующей границе. То есть, для каждого типа границы S в Bi, Ti является подтипом S[F1:=T1,...,Fn:=Tn] (§4.10).
Параметризованный тип C<T1,...,Tn> является корректным, если выполняются все следующие условия:
-
C — имя обобщённого типа.
-
Количество аргументов типа равно количеству параметров типа в обобщённом объявлении C.
-
При применении преобразования захвата (§5.1.10), приводящего к типу C
<X1,...,Xn>, каждый аргумент типа Xi является подтипом S[F1:=X1,...,Fn:=Xn]для каждого типа границы S в Bi.
Ошибка компиляции, если параметризованный тип не корректен.
В данном спецификации, когда мы говорим о типе класса или интерфейса, мы включаем и обобщённую версию, если явно не исключено.
Два параметризованных типа являются доказательно различными, если выполняется одно из следующих условий:
-
Они являются параметризациями разных объявлений обобщённых типов.
-
Любой из их аргументов типа является доказательно различным.
На основании обобщённых типов в примерах §8.1.2, вот некоторые корректные параметризованные типы:
-
Seq<String> -
Seq<Seq<String>> -
Seq<String>.Zipper<Integer> -
Pair<String,Integer>
Вот некоторые неправильные параметризации этих обобщённых типов:
-
Seq<int>недопустимо, так как примитивные типы не могут быть аргументами типа. -
Pair<String>недопустимо, так как недостаточно аргументов типа. -
Pair<String,String,String>недопустимо, так как слишком много аргументов типа.
Параметризованный тип может быть параметризацией обобщённого класса или интерфейса, который является вложенным. Например, если необобщённый класс C имеет обобщённый вложенный класс D<T>, то C.D<Object> — параметризованный тип. И если обобщённый класс C<T> имеет необобщённый вложенный класс D, то тип члена C<String>.D является параметризованным типом, даже если класс D не обобщённый.
Аргументы типа могут быть либо типами ссылок, либо джойлд-символами. Джойлд-символы полезны в ситуациях, когда требуется лишь частичное знание о параметре типа.
Джойлд-символам можно задать явные границы, подобно обычным объявлениям переменных типа. Верхняя граница обозначается следующим синтаксисом, где B — граница:
? extends B
В отличие от обычных переменных типа, объявленных в сигнатуре метода, при использовании джойлд-символа не требуется вывод типа. Поэтому разрешается объявлять нижние границы джойлд-символа, используя следующий синтаксис, где B — нижняя граница:
? super B
Джойлд-символ ? extends Object эквивалентен неограниченному джойлд-символу ?.
Два аргумента типа доказательно различны, если выполняется одно из следующих условий:
-
Ни один из аргументов не является переменной типа или джойлд-символом, и оба аргумента не являются одним и тем же типом.
-
Один аргумент типа является переменной типа или джойлд-символом с верхней границей (из преобразования захвата (§5.1.10), если необходимо) S; другой аргумент типа T не является переменной типа или джойлд-символом; и ни |S|
<:|T| ни |T|<:|S| (§4.8, §4.10). -
Каждый аргумент типа является переменной типа или джойлд-символом с верхними границами (из преобразования захвата, если необходимо) S и T; и ни |S|
<:|T| ни |T|<:|S|.
Говорят, что аргумент типа T1 содержит другой аргумент типа T2, записываемое как T2 <= T1, если множество типов, обозначаемое T2, является доказательно подмножеством множества типов, обозначаемых T1, в соответствии с рефлексивным и транзитивным замыканием следующих правил (где <: обозначает подтипизацию (§4.10)):
-
?extendsT<=?extendsS если T<:S -
?extendsT<=? -
?superT<=?superS если S<:T -
?superT<=? -
?superT<=?extendsObject -
T
<=T -
T
<=?extendsT -
T
<=?superT
Связь джойлд-символов с установленной теорией типов интересная, что мы кратко упомянули здесь. Джойлд-символы — это ограниченная форма экзистенциальных типов. Учитывая общее объявление типа G<T extends B>, G<?> примерно соответствует Some X <: B. G<X>.
Исторически джойлд-символы являются прямым потомком работ Ацуси Игараси и Мирко Вироли. Читатели, заинтересованные в более подробном обсуждении, должны обратиться к работе «On Variance-Based Subtyping for Parametric Types» Ацуси Игараси и Мирко Вироли в «Proceedings of the 16th European Conference on Object Oriented Programming (ECOOP 2002)». Эта работа сама по себе основана на более ранних работах Крстен Торупа и Мадса Торгерсена («Unifying Genericity», ECOOP 99), а также на длительной традиции работ по вариации, основанной на объявлении, которая восходит к работе Пьера Америки над POOL (OOPSLA 89).
Джойлд-символы отличаются в некоторых деталях от конструкций, описанных в упомянутой статье, в частности, в использовании преобразования захвата (§5.1.10) вместо операции close, описанной Игараси и Вироли. Для формального описания джойлд-символов см. «Wild FJ» Мадса Торгерсена, Эрика Эрнста и Кристиана Плеснера Хансена в 12-м семинаре по основам объектно-ориентированного программирования (FOOL 2005).
Пример 4.5.1-1. Неограниченные джойлд-символы
import java.util.Collection;
import java.util.ArrayList;
class Test {
static void printCollection(Collection<?> c) {
// a wildcard collection
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> в качестве типа входного параметра, c, было бы не столь полезным; метод мог бы использоваться только с выражением аргумента, имеющим тип Collection<Object>, что встречается довольно редко. В отличие от этого, использование неограниченного джойлд-символа позволяет передавать в качестве аргумента любые коллекции.
Вот пример, где элемент массива параметризован джойлд-символом:
public Method getMethod(Class<?>[] parameterTypes) { ... }
Пример 4.5.1-2. Ограниченные джойлд-символы
boolean addAll(Collection<? extends E> c)
Здесь метод объявлен внутри интерфейса Collection<E> и предназначен для добавления всех элементов своего входного аргумента в коллекцию, к которой он вызывается. Естественным было бы использовать Collection<E> в качестве типа c, но это излишне ограничивает. Альтернативой было бы объявить сам метод как обобщенный:
<T> boolean addAll(Collection<T> c)
Этот вариант достаточно гибкий, но обратите внимание, что параметр типа используется только один раз в сигнатуре. Это отражает тот факт, что параметр типа не используется для выражения какого-либо рода взаимозависимости между типами аргументов, возвращаемым типом и/или типом исключения. При отсутствии такой зависимости обобщенные методы считаются плохой стилистикой, и предпочтение отдается джойлд-символам.
Reference(T referent, ReferenceQueue<? super T> queue)
Здесь референт может быть вставлен в любую очередь, тип элемента которой является надтипом типа T референта; T — нижняя граница для джойлд-символа.
Пусть C — это объявление обобщённого класса или интерфейса с параметрами типа A1,...,An, и пусть C<T1,...,Tn> — это параметризация C, где для 1 ≤ i ≤ n, Ti — это тип (а не подстановочный знак). Тогда:
-
Пусть
m— это объявление члена или конструктора в C, тип которого, как указано, — T (§8.2, §8.8.6).Тип
mв C<T1,...,Tn>равен T[A1:=T1,...,An:=Tn]. -
Пусть
m— это объявление члена или конструктора в D, где D — класс, расширяющий C, или интерфейс, реализуемый C. Пусть D<U1,...,Uk>— это супертип C<T1,...,Tn>, соответствующий D.Тип
mв C<T1,...,Tn>— это типmв D<U1,...,Uk>.
Если какие-либо аргументы типа в параметризации C являются подстановочными знаками, то:
-
Типы полей, методов и конструкторов в C
<T1,...,Tn>— это типы полей, методов и конструкторов в преобразовании захвата C<T1,...,Tn>(§5.1.10). -
Пусть D — это (возможно, обобщённое) объявление класса или интерфейса в C. Тогда тип D в C
<T1,...,Tn>— это D, где, если D является обобщённым, все аргументы типа являются неограниченными подстановочными знаками.
Это не имеет значения, так как невозможно получить доступ к члену параметризованного типа без выполнения преобразования захвата, и невозможно использовать подстановочный знак после ключевого слова new в выражении создания экземпляра класса (§15.9).
Единственное исключение из предыдущего абзаца — это когда вложенный параметризованный тип используется в качестве выражения в операторе instanceof (§15.20.2), где преобразование захвата не применяется.
Член static, объявленный в обобщённом объявлении типа, должен быть указан с использованием необобщённого типа, который соответствует обобщённому типу (§6.1, §6.5.5.2, §6.5.6.2), иначе произойдёт ошибка компиляции.
Другими словами, недопустимо ссылаться на член static, объявленный в обобщённом объявлении типа, используя параметризованный тип.
Стирание типов — это отображение типов (возможно, включая параметризованные типы и переменные типов) в типы (которые никогда не являются параметризованными типами или переменными типов). Мы пишем |T| для стирания типа T. Отображение стирания определяется следующим образом:
Стирание типов также отображает сигнатуру (§8.4.2) конструктора или метода на сигнатуру без параметризованных типов или переменных типов. Стирание сигнатуры конструктора или метода s — это сигнатура, состоящая из того же имени, что и s, и стираний всех типов формальных параметров, заданных в s.
Тип возвращаемого значения метода (§8.4.5) и параметры типа обобщённого метода или конструктора (§8.4.4, §8.8.4) также подвергаются стиранию, если стирается сигнатура метода или конструктора.
Стирание сигнатуры обобщённого метода не имеет параметров типа.
Поскольку некоторые сведения о типах стираются во время компиляции, не все типы доступны во время выполнения. Типы, которые полностью доступны во время выполнения, называются типами, подлежащими реификации.
Тип подлежит реификации тогда и только тогда, когда выполняется одно из следующих условий:
-
Он ссылается на объявление класса или интерфейса без дженериков.
-
Это параметризованный тип, в котором все аргументы типа являются неограниченными универсальными переменными (§4.5.1).
-
Это необработанный тип (§4.8).
-
Это примитивный тип (§4.2).
-
Это тип массива (§10.1), тип элемента которого подлежит реификации.
-
Это вложенный тип, где для каждого типа T, разделенного "
.", сам T подлежит реификации.Например, если у дженерик-класса X
<T>есть вложенный дженерик-класс Y<U>, то тип X<?>.Y<?>подлежит реификации, потому что X<?>подлежит реификации и Y<?>подлежит реификации. Тип X<?>.Y<Object>не подлежит реификации, потому что Y<Object>не подлежит реификации.
Тип пересечения не подлежит реификации.
Решение о том, чтобы не делать все дженерик-типы подлежащими реификации, является одним из самых важных и спорных решений, связанных с системой типов языка программирования Java.
В конечном счете, наиболее важной мотивацией этого решения является совместимость со существующим кодом. В наивном смысле добавление новых конструкций, таких как дженерики, не имеет последствий для существующего кода. Язык программирования Java, сам по себе, совместим с более ранними версиями, пока каждый программный код, написанный в предыдущих версиях, сохраняет свое значение в новой версии. Однако это понятие, которое можно назвать совместимостью языка, имеет чисто теоретический интерес. Реальные программы (даже тривиальные, такие как «Hello World») состоят из нескольких единиц компиляции, некоторые из которых предоставляются платформой Java SE (такие как элементы java.lang или java.util). На практике минимальное требование — это совместимость платформы — что любая программа, написанная для предыдущей версии платформы Java SE, продолжает работать без изменений в новой версии.
Один из способов обеспечить совместимость платформы — оставить существующую функциональность платформы без изменений, добавив только новую функциональность. Например, вместо изменения существующей иерархии коллекций в java.util можно создать новую библиотеку, использующую дженерики.
Недостатки такой схемы заключаются в том, что существующим клиентам библиотеки коллекций очень сложно мигрировать на новую библиотеку. Коллекции используются для обмена данными между независимо разработанными модулями; если поставщик решает перейти на новую, дженерик-библиотеку, этот поставщик должен также распространить две версии своего кода для совместимости со своими клиентами. Библиотеки, которые зависят от кода других поставщиков, не могут быть изменены для использования дженериков до тех пор, пока библиотека поставщика не будет обновлена. Если два модуля взаимно зависят друг от друга, изменения должны быть внесены одновременно.
Очевидно, что совместимость платформы, как указано выше, не предоставляет реалистичный путь для внедрения такой распространённой новой функции, как дженерики. Поэтому проектирование системы дженерик-типов стремится поддержать совместимость миграции. Совместимость миграции позволяет эволюционировать существующий код для использования дженериков без наложения зависимостей между независимо разработанными программными модулями.
Цена совместимости миграции заключается в том, что полная и надежная реификация системы дженерик-типов невозможна, по крайней мере, пока происходит миграция.
Для облегчения взаимодействия с устаревшим кодом, не использующим дженерики, можно использовать стирание (§4.6) параметризованного типа (§4.5) или стирание типа массива (§10.1), элемент которого является параметризованным типом. Такой тип называется необработанным типом.
Более точно, необработанный тип определяется как один из следующих:
-
Ссылочный тип, образованный из имени объявления дженерик-типа без сопровождающего списка аргументов типа.
-
Тип массива, элемент которого является необработанным типом.
-
Тип-член, не являющийся членом
static, необработанного типа 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 Inner<S> {
S s;
}
}
Доступ к Inner как к частично необработанному типу («редкому» типу) невозможен:
Outer.Inner<Double> x = null; // illegal Double d = x.s;
потому что Outer сам является необработанным, следовательно, все его внутренние классы, включая Inner, также являются необработанными, и поэтому невозможно передавать какие-либо аргументы типа в Inner.
Суперклассы (соответственно, суперинтерфейсы) необработанного типа — это стирания суперклассов (суперинтерфейсов) любого из параметризаций дженеризованного типа.
Тип конструктора (§8.8), метода экземпляра (§8.4, §9.4) или не static поля (§8.3) необработанного типа C, который не унаследован от его суперклассов или суперинтерфейсов, — это необработанный тип, соответствующий стиранию его типа в дженеризованном объявлении, соответствующем C.
Тип static метода или static поля необработанного типа C — это тот же тип, что и в дженеризованном объявлении, соответствующем C.
Передача аргументов типа необработанному члену типа, не являющемуся static, не унаследованному от его суперклассов или суперинтерфейсов, является ошибкой на этапе компиляции.
Попытка использовать член параметризованного типа как необработанный тип является ошибкой на этапе компиляции.
Это означает, что запрет на «редкие» типы распространяется на случай, когда квалифицирующий тип параметризован, но мы пытаемся использовать внутренний класс как необработанный тип:
Outer<Integer>.Inner x = null; // illegal
Это противоположность случаю, обсуждаемому выше. Нет практического оправдания для такого полуготового типа. В устаревшем коде аргументы типа не используются. В современном коде мы должны правильно использовать дженеризованные типы и передавать все необходимые аргументы типа.
Супертип класса может быть необработанным типом. Обращения к членам класса обрабатываются как обычно, а обращения к членам супертипа — как к необработанным типам. В конструкторе класса вызовы super обрабатываются как вызовы методов на необработанном типе.
Использование необработанных типов разрешено только как уступка для совместимости с устаревшим кодом. Использование необработанных типов в коде, написанном после введения дженериков в язык программирования Java, настоятельно не рекомендуется. В будущих версиях языка программирования Java использование необработанных типов, возможно, будет запрещено.
Чтобы убедиться, что потенциальные нарушения правил типизации всегда отмечаются, некоторые обращения к членам необработанного типа приведут к предупреждениям на этапе компиляции. Правила для предупреждений на этапе компиляции при обращении к членам или конструкторам необработанных типов следующие:
-
При присваивании полю: если тип Основного в выражении доступа к полю (§15.11) является необработанным типом, то предупреждение на этапе компиляции возникает, если стирание изменяет тип поля.
-
При вызове метода или конструктора: если тип класса или интерфейса для поиска (§15.12.1) является необработанным типом, то предупреждение на этапе компиляции возникает, если стирание изменяет любой из формальных типов параметров метода или конструктора.
-
Предупреждение на этапе компиляции не возникает при вызове метода, когда формальные типы параметров не изменяются при стирании (даже если тип возвращаемого значения и/или
throwsусловие изменяются), для чтения из поля или для создания экземпляра класса необработанного типа.
Обратите внимание, что вышеуказанные предупреждения об отсутствии проверки типов отличаются от возможных предупреждений об отсутствии проверки типов при сужении преобразования ссылочных типов (§5.1.6), преобразования без проверки типа (§5.1.9), объявления методов (§8.4.1, §8.4.8.3) и некоторых выражений (§15.12.4.2, §15.13.2, §15.27.3).
Эти предупреждения охватывают случай, когда устаревший потребитель использует библиотекку, использующую дженерики. Например, библиотека объявляет дженеризованный класс Foo<T extends String>, у которого есть поле f типа Vector<T>, но потребитель присваивает вектор целых чисел полю e.f, где e имеет необработанный тип Foo. Устаревший потребитель получает предупреждение, потому что он может вызвать загрязнение кучи (§4.12.2) для дженеризованных потребителей дженеризованной библиотеки.
(Обратите внимание, что устаревший потребитель может присвоить Vector<String> из библиотеки своей переменной Vector без получения предупреждения. То есть, правила подтипизации (§4.10.2) языка программирования Java позволяют переменной необработанного типа быть присвоенной значением любого параметризованного экземпляра типа.)
Предупреждения из-за преобразований без проверки типа охватывают двойной случай, когда дженеризованный потребитель использует устаревшую библиотеку. Например, метод библиотеки имеет необработанный тип возврата Vector, но потребитель присваивает результат вызова метода переменной типа Vector<String>. Это небезопасно, так как у необработанного вектора может быть другой тип элемента, чем String, но все еще разрешено с помощью преобразования без проверки типа, чтобы обеспечить взаимодействие со старым кодом. Предупреждение о преобразовании без проверки типа указывает на то, что дженеризованный потребитель может столкнуться с проблемами загрязнения кучи в других частях программы.
Пример 4.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, has type Object
System.out.println(x.get()); // OK, has type Object
x.set("def"); // unchecked warning
}
}
Пример 4.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, static member
}
}
В этой программе (которую не предполагается запускать), RawMembers<T> наследует метод:
Iterator<String> iterator()
от Collection<String> суперинтерфейса. Необработанный тип RawMembers наследует iterator() от Collection, стирания Collection<String>, что означает, что тип возвращаемого значения iterator() в RawMembers равен Iterator. В результате, попытка присвоить rw.iterator() переменной Iterator<String> требует преобразования без проверки типа, поэтому выдаётся предупреждение на этапе компиляции.
В отличие от этого, RawMembers наследует myNumbers() от класса NonGeneric, чьё стирание также равно NonGeneric. Таким образом, тип возвращаемого значения myNumbers() в RawMembers не стирается, и попытка присвоить rw.myNumbers() переменной Collection<Number> не требует преобразования без проверки типа, поэтому предупреждение на этапе компиляции не выдаётся.
Аналогично, член static cng сохраняет свой параметризованный тип даже при доступе через объект необработанного типа. Обратите внимание, что доступ к члену static через экземпляр считается плохим стилем и не рекомендуется.
Этот пример показывает, что некоторые члены необработанного типа не стираются, а именно члены static, типы которых параметризованы, и члены, унаследованные от недженеризованного супертипа.
Необработанные типы тесно связаны с подстановочными знаками. Оба основаны на экзистенциальных типах. Необработанные типы можно рассматривать как подстановочные знаки, правила типов которых преднамеренно некорректны, чтобы обеспечить взаимодействие со старым кодом. Исторически необработанные типы предшествовали подстановочным знакам; они были впервые представлены в GJ и описаны в статье Обеспечение безопасности будущего для прошлого: добавление универсальности в язык программирования Java Гиллада Брахи, Мартина Одерски, Дэвида Стаутамира и Филипа Уолдлера в Труды конференции ACM по объектно-ориентированному программированию, системам, языкам и приложениям (OOPSLA 98), октябрь 1998 года.
Пересекающийся тип имеет вид T1 & ... & Tn (n > 0), где Ti (1 ≤ i ≤ n) — типы.
Пересекающиеся типы могут быть получены из ограничений параметров типа (§4.4) и выражений приведения типов (§15.16); они также возникают в процессах преобразования захвата (§5.1.10) и вычисления наименьшего общего надтипа (§4.10.4).
Значения пересекающегося типа — это те объекты, которые являются значениями всех типов Ti для 1 ≤ i ≤ n.
Каждый пересекающийся тип T1 & ... & Tn индуцирует условный класс или интерфейс для определения членов пересекающегося типа следующим образом:
-
Для каждого Ti (1 ≤ i ≤ n), пусть Ci — наиболее специфичный класс или массив типов такой, что Ti
<:Ci. Тогда должен существовать некоторый Ck, такой что Ck<:Ci для любого i (1 ≤ i ≤ n), иначе произойдёт ошибка времени компиляции. -
Для 1 ≤ j ≤ n, если Tj — переменная типа, пусть Tj' — интерфейс, члены которого такие же, как члены
publicTj; иначе, если Tj — интерфейс, то пусть Tj' будет Tj. -
Если Ck —
Object, индуцируется условный интерфейс; иначе индуцируется условный класс с прямым суперклассом Ck. Этот класс или интерфейс имеет прямые суперинтерфейсы T1', ..., Tn' и объявлен в пакете, в котором появляется пересекающийся тип.
Члены пересекающегося типа — это члены класса или интерфейса, которые он индуцирует.
Стоит остановиться на различии между пересекающимися типами и границами переменных типа. Каждое ограничение переменной типа индуцирует пересекающийся тип. Этот пересекающийся тип часто тривиален, состоящий из одного типа. Форма ограничения ограничена (только первый элемент может быть классом или переменной типа, и только одна переменная типа может появиться в ограничении), чтобы предотвратить возникновение некоторых неудобных ситуаций. Однако преобразование захвата может привести к созданию переменных типов, границы которых более общие, такие как типы массивов).
Отношения подтипа и надтипа являются бинарными отношениями на типах.
Надтипы типа получаются путём рефлексивного и транзитивного замыкания по отношению к прямому надтипу, записанному как S >1 T, которое определяется правилами, приведёнными позже в данном разделе. Мы пишем S :> T, чтобы указать, что отношение надтипа справедливо для S и T.
S является надтипом T, записанным как S > T, если S :> T и S ≠ T.
Подтипы типа T — это все типы U, для которых T является надтипом U, а также нулевой тип. Мы пишем T <: S, чтобы указать, что отношение подтипа справедливо для типов T и S.
T является собственным подтипом S, записанным как T < S, если T <: S и S ≠ T.
T является прямым подтипом S, записанным как T <1 S, если S >1 T.
Подтипизация не распространяется на параметризованные типы: T <: S не подразумевает, что C<T> <: C<S>.
Учитывая объявление не обобщенного типа C, прямыми супертипами типа C являются все следующие:
Учитывая объявление обобщенного типа C<F1,...,Fn> (n > 0), прямыми супертипами необработанного типа C (§4.8) являются все следующие:
-
Прямой суперкласс необработанного типа C.
-
Прямые суперинтерфейсы необработанного типа C.
-
Тип
Object, если C<F1,...,Fn>является обобщенным типом интерфейса без прямых суперинтерфейсов (§9.1.2).
Учитывая объявление обобщенного типа C<F1,...,Fn> (n > 0), прямыми супертипами обобщенного типа C<F1,...,Fn> являются все следующие:
-
Прямой суперкласс C
<F1,...,Fn>. -
Прямые суперинтерфейсы C
<F1,...,Fn>. -
Тип
Object, если C<F1,...,Fn>является обобщенным типом интерфейса без прямых суперинтерфейсов. -
Необработанный тип C.
Учитывая объявление обобщенного типа C<F1,...,Fn> (n > 0), прямыми супертипами параметризованного типа C<T1,...,Tn>, где Ti (1 ≤ i ≤ n) - это тип, являются все следующие:
-
D
<U1 θ,...,Uk θ>, где D<U1,...,Uk>является обобщенным типом, который является прямым супертипом обобщенного типа C<F1,...,Fn>и θ является подстановкой[F1:=T1,...,Fn:=Tn]. -
C
<S1,...,Sn>, где Si содержит Ti (1 ≤ i ≤ n) (§4.5.1). -
Тип
Object, если C<F1,...,Fn> является обобщенным типом интерфейса без прямых суперинтерфейсов. -
Необработанный тип C.
Учитывая объявление обобщенного типа C<F1,...,Fn> (n > 0), прямыми супертипами параметризованного типа C<R1,...,Rn> где по крайней мере один из Ri (1 ≤ i ≤ n) является аргументом типа wildcard, являются прямыми супертипами параметризованного типа C<X1,...,Xn>, который является результатом применения преобразования захвата к C<R1,...,Rn> (§5.1.10).
Прямыми супертипами типа пересечения T1 & ... & Tn являются Ti (1 ≤ i ≤ n).
Прямыми супертипами переменной типа являются типы, перечисленные в ее границе.
Переменная типа является прямым супертипом своей нижней границы.
Прямыми супертипами нулевого типа являются все ссылочные типы, кроме самого нулевого типа.
Следующие правила определяют отношение прямого супертипа среди типов массивов:
-
Если S и T являются типами ссылок, то S
[]>1T[]если и только если S>1T. -
Object>1Object[] -
Cloneable>1Object[] -
java.io.Serializable>1Object[] -
Если P - примитивный тип, то:
-
Object>1P[] -
Cloneable>1P[] -
java.io.Serializable>1P[]
-
Наименьшая верхняя граница, или "lub", набора ссылочных типов — это общий супертип, который является более специфичным, чем любой другой общий супертип (то есть, никакой другой общий супертип не является подтипом наименьшей верхней границы). Этот тип, lub(U1, ..., Uk), определяется следующим образом.
Если k = 1, то lub — это сам тип: lub(U) = U.
В противном случае:
-
Для каждого Ui (1 ≤ i ≤ k):
Пусть ST(Ui) будет набором супертипов Ui.
Пусть EST(Ui), набор стертых супертипов Ui, будет:
EST(Ui) = { |W| | W in ST(Ui) } где |W| — стирание W.
Причина вычисления набора стертых супертипов — это работа с ситуациями, когда набор типов включает в себя несколько различных параметризаций обобщенного типа.
Например, учитывая
Listи<String>List, простое пересечение наборов ST(<Object>List) = {<String>List,<String>Collection,<String>Object} и ST(List) = {<Object>List,<Object>Collection,<Object>Object} приведет к получению набора {Object}, и мы потеряем факт, что верхнюю границу можно безопасно считатьList.В отличие от этого, пересечение EST(
List) = {<String>List,Collection,Object} и EST(List) = {<Object>List,Collection,Object} приводит к {List,Collection,Object}, что в конечном итоге позволит нам создатьList.<?> -
Пусть EC, набор кандидатов со стиранием для U1 ... Uk, будет пересечением всех наборов EST(Ui) (1 ≤ i ≤ k).
-
Пусть MEC, минимальный набор кандидатов со стиранием для U1 ... Uk, будет:
MEC = { V | V in EC, и для всех W ≠ V in EC, неверно, что W
<:V }Поскольку мы стремимся вывести более точные типы, мы хотим отфильтровать любых кандидатов, которые являются супертипами других кандидатов. Именно это и делает вычисление MEC. В нашем рабочем примере у нас было EC = {
List,Collection,Object}, поэтому MEC = {List}. Следующим шагом является восстановление аргументов типа для стертых типов в MEC. -
Для любого элемента G из MEC, который является обобщенным типом:
Пусть "соответствующие" параметризации G, Relevant(G), будут:
Relevant(G) = { V | 1 ≤ i ≤ k: V in ST(Ui) и V = G
<...>}В нашем рабочем примере единственным обобщенным элементом MEC является
List, и Relevant(List) = {List,<String>List}. Сейчас мы попытаемся найти аргумент типа для<Object>List, который содержит (§4.5.1) какString, так иObject.Это делается с помощью операции наименьшей содержащей параметризации (lcp), определенной ниже. Первая строка определяет lcp() на наборе, таком как Relevant(
List), как операцию над списком, состоящим из элементов набора. Следующая строка определяет операцию над таким списком как попарное сокращение элементов списка. Третья строка — определение lcp() для пар параметризованных типов, которое, в свою очередь, опирается на понятие наименьшего содержащего аргумента типа (lcta). lcta() определена для всех возможных случаев.Пусть "кандидатская" параметризация G, Candidate(G), будет самой конкретной параметризацией обобщенного типа G, которая содержит все соответствующие параметризации G:
Candidate(G) = lcp(Relevant(G))
где lcp(), наименьшая содержащая параметризация, является:
-
lcp(S) = lcp(
e1, ...,en) гдеei(1 ≤ i ≤ n) in S -
lcp(
e1, ...,en) = lcp(lcp(e1,e2),e3, ...,en) -
lcp(G
<X1, ..., Xn>, G<Y1, ..., Yn>) = G<lcta(X1, Y1), ..., lcta(Xn, Yn)> -
lcp(G
<X1, ..., Xn>) = G<lcta(X1), ..., lcta(Xn)>
и где lcta(), наименьший содержащий аргумент типа, является: (предполагая, что U и V — это типы)
-
lcta(U, V) = U if U = V, otherwise
?extendslub(U, V) -
lcta(U,
?extendsV) =?extendslub(U, V) -
lcta(U,
?superV) =?superglb(U, V) -
lcta(
?extendsU,?extendsV) =?extendslub(U, V) -
lcta(
?extendsU,?superV) =? -
lcta(
?superU,?superV) =?superglb(U, V) -
lcta(U) =
?if U's upper bound isObject, otherwise?extendslub(U,Object)
и где glb() определена в §5.1.10.
-
-
Пусть lub(U1 ... Uk) будет:
Best(W1)
&...&Best(Wr)где Wi (1 ≤ i ≤ r) — элементы MEC, минимального набора кандидатов со стиранием для U1 ... Uk;
и где, если какие-либо из этих элементов являются обобщенными, мы используем кандидатскую параметризацию (чтобы восстановить аргументы типа):
Best(X) = Candidate(X) if X is generic; X otherwise.
Строго говоря, эта функция lub() только аппроксимирует наименьшую верхнюю границу. Формально может существовать другой тип T, такой что все U1 ... Uk являются подтипами T, а T является подтипом lub(U1, ..., Uk). Однако компилятор для языка программирования Java должен реализовать lub() как указано выше.
Возможно, что функция lub() выдает бесконечный тип. Это допустимо, и компилятор для языка программирования Java должен распознавать такие ситуации и представлять их соответствующим образом, используя циклические структуры данных.
Возможность бесконечного типа проистекает из рекурсивных вызовов lub(). Читатели, знакомые с рекурсивными типами, должны отметить, что бесконечный тип не то же самое, что рекурсивный тип.
Синтетическая переменная типа — это переменная типа, введенная компилятором во время преобразования захвата (§5.1.10) или разрешения переменной вывода (§18.4).
Иногда необходимо найти близкий супертип типа, где этот супертип не упоминает определенные синтетические переменные типа. Это достигается с помощью восходящей проекции, применяемой к типу.
Аналогично, нисходящая проекция может быть применена для нахождения близкого подтипа типа, где этот подтип не упоминает определенные синтетические переменные типа. Поскольку такой тип не всегда существует, нисходящая проекция является частичной функцией.
Эти операции принимают в качестве входных данных набор переменных типа, на которые больше не следует ссылаться, называемых ограниченными переменными типа. Когда операции повторяются, набор ограниченных переменных типа неявно передается в рекурсивное приложение.
Восходящая проекция типа T относительно набора ограниченных переменных типа определяется следующим образом:
-
Если T не упоминает ни одной ограниченной переменной типа, то результатом будет T.
-
Если T является ограниченной переменной типа, то результатом является восходящая проекция верхней границы T.
-
Если T является параметризованным типом класса или параметризованным типом интерфейса, G
<A1,...,An>, то результатом будет G<A1',...,An'>, где для 1 ≤ i ≤ n, Ai' выводится из Ai следующим образом:-
Если Ai не упоминает ни одной ограниченной переменной типа, то Ai' = Ai.
-
Если Ai является типом, который упоминает ограниченную переменную типа, то пусть U будет восходящей проекцией Ai. Ai' является подстановочным знаком, определяемым тремя случаями:
-
Если U не
Object, и если объявленная граница i-го параметра G, Bi, упоминает параметр типа G, или Bi не является подтипом U, то Ai' — это подстановочный знак с верхней границей,?extendsU. -
В противном случае, если нисходящая проекция Ai — это
L, то Ai' — это подстановочный знак с нижней границей,?superL. -
В противном случае нисходящая проекция Ai не определена, и Ai' — это неограниченный подстановочный знак,
?.
-
-
Если Ai является подстановочным знаком с верхней границей, который упоминает ограниченную переменную типа, то пусть U будет восходящей проекцией границы подстановочного знака. Ai' — это подстановочный знак с верхней границей,
?extendsU. -
Если Ai является подстановочным знаком с нижней границей, который упоминает ограниченную переменную типа, то, если нисходящая проекция границы подстановочного знака — это
L, то Ai' — это подстановочный знак с нижней границей,?superL; если нисходящая проекция границы подстановочного знака не определена, то Ai' — это неограниченный подстановочный знак,?.
-
-
Если T является типом массива, S
[], то результатом будет тип массива, компонентом которого является восходящая проекция S. -
Если T является типом пересечения, то результатом будет тип пересечения. Для каждого элемента, S, из T, результат имеет в качестве элемента восходящую проекцию S.
Нисходящая проекция типа T относительно набора ограниченных переменных типа является частичной функцией, определяемой следующим образом:
-
Если T не упоминает ни одной ограниченной переменной типа, то результатом будет T.
-
Если T является ограниченной переменной типа, то, если T имеет нижнюю границу, и если нисходящая проекция этой границы — это
L, результатом будетL; если T не имеет нижней границы или если нисходящая проекция этой границы не определена, то результат не определен. -
Если T является параметризованным типом класса или параметризованным типом интерфейса, G
<A1,...,An>, то результатом будет G<A1',...,An'>, если для 1 ≤ i ≤ n аргумент типа Ai' может быть выведен из Ai следующим образом; в противном случае результат не определен:-
Если Ai не упоминает ограниченную переменную типа, то Ai' = Ai.
-
Если Ai является типом, который упоминает ограниченную переменную типа, то Ai' не определен.
-
Если Ai является подстановочным знаком с верхней границей, который упоминает ограниченную переменную типа, то, если нисходящая проекция границы подстановочного знака — это U, то Ai' является подстановочным знаком с верхней границей,
?extendsU; если нисходящая проекция границы подстановочного знака не определена, то Ai' не определен. -
Если Ai является подстановочным знаком с нижней границей, который упоминает ограниченную переменную типа, то пусть
Lбудет восходящей проекцией границы подстановочного знака. Ai' является подстановочным знаком с нижней границей,?superL.
-
-
Если T является типом массива, S
[], то, если нисходящая проекция S — это S', результатом будет S'[]; если нисходящая проекция S не определена, то результат не определен. -
Если T является типом пересечения, то, если нисходящая проекция определена для каждого элемента T, результатом будет тип пересечения, элементами которого являются нисходящие проекции элементов T; если нисходящая проекция не определена для любого элемента T, то результат не определен.
Как и lub (§4.10.4), восходящая и нисходящая проекция могут создавать бесконечные типы из-за рекурсии по границам переменных типа.
Типы используются в большинстве видов деклараций и в определенных типах выражений. Конкретно, существует 16 контекстов типов, где используются типы:
-
В декларациях:
-
Тип в
extendsилиimplementsфрагменте объявления класса (§8.1.4, §8.1.5, §8.5, §9.5) -
Тип в
extendsфрагменте объявления интерфейса (§9.1.3, §8.5, §9.5) -
Тип возвращаемого значения метода (включая тип элемента типа аннотации) (§8.4.5, §9.4, §9.6.1)
-
Тип в
throwsфрагменте метода или конструктора (§8.4.6, §8.8.5, §9.4) -
Тип в
extendsфрагменте объявления параметра типа обобщенного класса, интерфейса, метода или конструктора (§8.1.2, §9.1.2, §8.4.4, §8.8.4) -
Тип в объявлении поля класса или интерфейса (включая константу перечисления) (§8.3, §9.3, §8.9.1)
-
Тип в объявлении формального параметра метода, конструктора или лямбда-выражения (§8.4.1, §8.8.1, §9.4, §15.27.1)
-
Тип параметра-получателя метода (§8.4)
-
Тип в объявлении локальной переменной (§14.4, §14.14.1, §14.14.2, §14.20.3)
-
Тип в объявлении параметра исключения (§14.20)
-
-
В выражениях:
-
Тип в явном списке аргументов типа для явного вызова конструктора, выражения создания экземпляра класса или выражения вызова метода (§8.8.7.1, §15.9, §15.12)
-
В выражении создания экземпляра класса без указания имени класса, как тип создаваемого экземпляра (§15.9) или как непосредственный суперкласс или непосредственный суперинтерфейс создаваемого анонимного класса (§15.9.5)
-
Тип элемента в выражении создания массива (§15.10.1)
-
Тип в операторе приведения выражения приведения типов (§15.16)
-
Тип, следующий за оператором
instanceof(оператором сравнения типов) (§15.20.2) -
В выражении ссылки на метод (§15.13), как тип ссылки для поиска метода-члена или как тип класса или массива для создания.
-
Также типы используются как:
-
Тип элемента типа массива в любом из вышеперечисленных контекстов; и
-
Аргумент типа без диких карт или граница аргумента типа дикой карты параметризованного типа в любом из вышеперечисленных контекстов.
Наконец, в языке программирования Java существуют три специальных термина, обозначающих использование типа:
Значение типов в контекстах типов определяется:
-
§4.2, для примитивных типов
-
§4.4, для параметров типа
-
§4.5, для типов классов и интерфейсов, которые параметризованы или появляются как аргументы типа в параметризованном типе или как границы аргументов типа диких карт в параметризованном типе
-
§4.8, для типов классов и интерфейсов, которые являются сырыми
-
§4.9, для пересекающихся типов в границах параметров типа
-
§6.5, для типов классов и интерфейсов в контекстах, где обобщенность не важна (§6.1)
-
§10.1, для типов массивов
Некоторые контексты типов ограничивают то, как тип ссылки может быть параметризован:
-
В следующих контекстах типов требуется, чтобы, если тип является параметризованным ссылочным типом, у него не было аргументов типа с подстановкой:
-
В пункте
extendsилиimplementsобъявления класса (§8.1.4, §8.1.5) -
В пункте
extendsобъявления интерфейса (§9.1.3) -
В выражении создания экземпляра класса без квалификатора, в качестве типа класса, подлежащего созданию (§15.9) или в качестве непосредственного надкласса или непосредственного надинтерфейса анонимного класса, подлежащего созданию (§15.9.5)
-
В выражении ссылки на метод (§15.13), как тип ссылки для поиска метода-члена или как тип класса или тип массива для создания.
Кроме того, в явном списке аргументов типа для явного вызова конструктора или выражения создания экземпляра класса или выражения вызова метода или выражения ссылки на метод не допускаются аргументы типа с подстановкой (§8.8.7.1, §15.9, §15.12, §15.13).
-
-
В следующих контекстах типов требуется, чтобы, если тип является параметризованным ссылочным типом, у него были только аргументы типа с неограниченной подстановкой (т. е. это реифицируемый тип):
-
В следующих контекстах типов параметризованный ссылочный тип запрещен, поскольку они связаны с исключениями, а тип исключения не является обобщенным (§6.1):
В любом контексте типа, где используется тип, можно анотировать ключевое слово, обозначающее примитивный тип, или Идентификатор, обозначающий простое имя ссылочного типа. Также можно анотировать тип массива, написав аннотацию слева от [ на желаемом уровне вложенности в типе массива. Аннотации в этих местах называются типовыми аннотациями и определяются в §9.7.4. Вот некоторые примеры:
-
@Foo int[] f;анотирует примитивный типint -
int @Foo [] f;анотирует тип массиваint[] -
int @Foo [][] f;анотирует тип массиваint[][] -
int[] @Foo [] f;анотирует тип массиваint[], который является типом компонента типа массиваint[][]
Пять из контекстов типов, которые появляются в объявлениях, занимают то же синтаксическое место, что и ряд контекстов объявления (§9.6.4.1):
-
Тип возвращаемого значения метода (включая тип элемента типа аннотации)
-
Тип в объявлении поля класса или интерфейса (включая константу перечисления)
-
Тип в объявлении формального параметра метода, конструктора или лямбда-выражения
-
Тип в объявлении локальной переменной
-
Тип в объявлении параметра исключения
Тот факт, что одно и то же синтаксическое место в программе может быть одновременно контекстом типа и контекстом объявления, обусловлен тем, что модификаторы объявления непосредственно предшествуют типу объявленной сущности. §9.7.4 объясняет, как аннотация в таком месте считается появляющейся в контексте типа или контексте объявления или обоих.
Пример 4.11-1. Использование типа
import java.util.Random;
import java.util.Collection;
import java.util.ArrayList;
class MiscMath<T extends Number> {
int divisor;
MiscMath(int divisor) { this.divisor = divisor; }
float ratio(long l) {
try {
l /= divisor;
} catch (Exception e) {
if (e instanceof ArithmeticException)
l = Long.MAX_VALUE;
else
l = 0;
}
return (float)l;
}
double gausser() {
Random r = new Random();
double[] val = new double[2];
val[0] = r.nextGaussian();
val[1] = r.nextGaussian();
return (val[0] + val[1]) / 2;
}
Collection<Number> fromArray(Number[] na) {
Collection<Number> cn = new ArrayList<Number>();
for (Number n : na) cn.add(n);
return cn;
}
<S> void loop(S s) { this.<S>loop(s); }
}
В этом примере типы используются в объявлениях следующего:
-
Импортируемые типы (§7.5); здесь тип
Random, импортированный из типаjava.util.Randomпакетаjava.util, объявлен -
Поля, которые представляют собой переменные класса и экземпляра класса (§8.3), и константы интерфейсов (§9.3); здесь поле
divisorв классеMiscMathобъявлено как типаint -
Параметры метода (§8.4.1); здесь параметр
lметодаratioобъявлен как типаlong -
Результаты метода (§8.4); здесь результат метода
ratioобъявлен как типаfloat, а результат методаgausserобъявлен как типаdouble -
Параметры конструктора (§8.8.1); здесь параметр конструктора для
MiscMathобъявлен как типаint -
Локальные переменные (§14.4, §14.14); локальные переменные
rиvalметодаgausserобъявлены как типыRandomиdouble[](массивdouble) -
Параметры исключений (§14.20); здесь параметр исключения
eблокаcatchобъявлен как типаException -
Параметры типа (§4.4); здесь параметр типа
MiscMath— это переменная типаTс типомNumberкак объявленной границей -
В любом объявлении, использующем параметризованный тип; здесь тип
Numberиспользуется в качестве аргумента типа (§4.5.1) в параметризованном типеCollection<Number>.
и в выражениях следующих видов:
-
Создание экземпляра класса (§15.9); здесь локальная переменная
rметодаgausserинициализируется выражением создания экземпляра класса, которое использует типRandom -
Создание экземпляра обобщенного класса (§8.1.2) (§15.9); здесь
Numberиспользуется как аргумент типа в выраженииnew ArrayList<Number>() -
Создание массива (§15.10.1); здесь локальная переменная
valметодаgausserинициализируется выражением создания массива, которое создает массивdoubleразмером 2 -
Вызовы обобщенных методов (§8.4.4) или конструкторов (§8.8.4) (§15.12); здесь метод
loopвызывает себя с явным аргументом типаS -
Приведения типов (§15.16); здесь оператор
returnметодаratioиспользует типfloatв приведении -
Оператор
instanceof(§15.20.2); здесь операторinstanceofпроверяет, совместим лиeс типомArithmeticException
Переменная — это место хранения, имеющее связанный тип, иногда называемый типом во время компиляции, который является либо примитивным типом (§4.2), либо ссылочным типом (§4.3).
Значение переменной изменяется при присваивании (§15.26) или с помощью префиксного или постфиксного ++ (инкремента) или -- (декремента) оператора (§15.14.2, §15.14.3, §15.15.1, §15.15.2).
Совместимость значения переменной с её типом гарантируется конструкцией языка программирования Java, пока программа не приводит к предупреждениям о не проверенных во время компиляции (§4.12.2). Значения по умолчанию (§4.12.5) совместимы, и все присваивания переменной проверяются на совместимость при присваивании (§5.2), обычно во время компиляции, но в одном случае с массивами, проверка выполняется во время выполнения (§10.5).
Переменная примитивного типа всегда содержит примитивное значение этого же примитивного типа.
Переменная типа класса T может содержать нулевую ссылку или ссылку на экземпляр класса T или любого класса, являющегося подклассом T.
Переменная типа интерфейса может содержать нулевую ссылку или ссылку на любой экземпляр любого класса, реализующего интерфейс.
Обратите внимание, что переменная не гарантирует, что всегда будет ссылаться на подтип своего объявленного типа, а только на подклассы или подинтерфейсы объявленного типа. Это обусловлено возможностью загрязнения кучи, обсуждаемой ниже.
Если T — примитивный тип, то переменная типа "массив T" может содержать нулевую ссылку или ссылку на любой массив типа "массив T".
Если T — ссылочный тип, то переменная типа "массив T" может содержать нулевую ссылку или ссылку на любой массив типа "массив S", где тип S является подклассом или подинтерфейсом типа T.
Переменная типа Object[] может содержать ссылку на массив любого ссылочного типа.
Переменная типа Object может содержать нулевую ссылку или ссылку на любой объект, является ли он экземпляром класса или массивом.
Возможно, что переменная параметризованного типа будет ссылаться на объект, который не является объектом этого параметризованного типа. Эта ситуация известна как загрязнение кучи.
Загрязнение кучи может произойти только в том случае, если программа выполнила некоторую операцию, связанную с сырым типом, которая привела бы к предупреждению о не проверенных во время компиляции (§4.8, §5.1.6, §5.1.9, §8.4.1, §8.4.8.3, §8.4.8.4, §9.4.1.2, §15.12.4.2), или если программа алиасирует переменную массива с нереализуемым типом элемента через переменную массива супертипа, которая является либо сырой, либо не генерической.
Например, код:
List l = new ArrayList<Number>(); List<String> ls = l; // Unchecked warning
вызывает предупреждение о не проверенных во время компиляции, потому что нельзя определить, ни во время компиляции (в рамках правил проверки типа во время компиляции), ни во время выполнения, ссылается ли переменная l на фактический List<String>.
Если код выше выполнен, возникает загрязнение кучи, так как переменная ls, объявленная как List<String>, ссылается на значение, которое фактически не является List<String>.
Проблема не может быть выявлена во время выполнения, потому что переменные типов не реанимируются, и поэтому экземпляры не несут никакой информации во время выполнения относительно используемых аргументов типа для их создания.
В простом примере, как показано выше, может показаться, что определить ситуацию во время компиляции и выдать ошибку должно быть просто. Однако в общем (и типичном) случае значение переменной l может быть результатом вызова отдельно скомпилированного метода, или его значение может зависеть от произвольного потока управления. Поэтому код выше является очень нетипичным и очень плохим стилем.
Кроме того, тот факт, что Object[] является супертипом всех массивов, означает, что могут произойти небезопасные алиасы, что приводит к загрязнению кучи. Например, следующий код компилируется, потому что он статически правильно типизирован:
static void m(List<String>... stringLists) {
Object[] array = stringLists;
List<Integer> tmpList = Arrays.asList(42);
array[0] = tmpList; // (1)
String s = stringLists[0].get(0); // (2)
}
Загрязнение кучи происходит в (1), потому что компонент в массиве stringLists, который должен ссылаться на List<String>, теперь ссылается на List<Integer>. Нет способа обнаружить это загрязнение в присутствии как универсального супертипа (Object[]), так и нереализуемого типа (объявленный тип формального параметра, List<String>[]). Предупреждение о не проверенных во время компиляции не обосновано в (1); тем не менее, во время выполнения произойдёт ClassCastException в (2).
Предупреждение о не проверенных во время компиляции будет выдаваться при любом вызове метода выше, так как вызов рассматривается статической системой типов языка программирования Java как создание массива, тип элемента которого List<String> не реанимируется (§15.12.4.2). Если и только если тело метода было безопасно с точки зрения переменного параметра, то программист мог бы использовать аннотацию SafeVarargs для предотвращения предупреждений при вызовах (§9.6.4.7). Поскольку тело метода, как написано выше, вызывает загрязнение кучи, было бы совершенно неуместно использовать аннотацию для отключения предупреждений для вызывающих сторон.
Наконец, обратите внимание, что массив stringLists может быть алиасирован через переменные других типов, кроме Object[], и загрязнение кучи всё ещё может произойти. Например, тип переменной array мог бы быть java.util.Collection[] — сырой тип элемента — и тело метода выше скомпилировалось бы без предупреждений или ошибок и всё ещё вызвало бы загрязнение кучи. И если платформа Java SE определила бы, скажем, Sequence как негенерический супертип List<T>, то использование Sequence в качестве типа array также вызвало бы загрязнение кучи.
Переменная всегда будет ссылаться на объект, который является экземпляром класса, представляющего параметризованный тип.
Значение ls в приведённом выше примере всегда является экземпляром класса, который предоставляет представление List.
Присваивание из выражения сырого типа переменной параметризованного типа следует использовать только при объединении устаревшего кода, не использующего параметризованные типы, с более современным кодом, который их использует.
Если никакая операция, требующая выдачи предупреждения о не проверенных во время компиляции, не происходит, и не происходит небезопасного алиасирования переменных массивов с нереализуемыми типами элементов, то загрязнения кучи не происходит. Следует отметить, что это не означает, что загрязнение кучи происходит только в том случае, если фактически возникло предупреждение о не проверенных во время компиляции. Возможна работа программы, где некоторые двоичные файлы были сгенерированы компилятором более старой версии языка программирования Java или из источников, которые явно подавили предупреждения о не проверенных типах. Такая практика в лучшем случае неуздоровительна.
Напротив, возможно, что, несмотря на выполнение кода, который мог (и, возможно, делал) вызвать предупреждение о не проверенных во время компиляции, загрязнения кучи не происходит. Действительно, хороший стиль программирования требует, чтобы программист убедилась, что, несмотря на любые предупреждения, код правильный и загрязнения кучи не произойдёт.
Существует восемь видов переменных:
-
Переменная класса — это поле, объявленное с помощью ключевого слова
staticвнутри объявления класса (§8.3.1.1), или с ключевым словомstaticили без него внутри объявления интерфейса (§9.3).Переменная класса создаётся при подготовке её класса или интерфейса (§12.3.2) и инициализируется значением по умолчанию (§4.12.5). Переменная класса фактически перестаёт существовать при разгрузке её класса или интерфейса (§12.7).
-
Переменная экземпляра — это поле, объявленное внутри объявления класса без использования ключевого слова
static(§8.3.1.1).Если у класса T есть поле
a, являющееся переменной экземпляра, то новая переменная экземпляраaсоздаётся и инициализируется значением по умолчанию (§4.12.5) как часть каждого вновь созданного объекта класса T или любого класса, являющегося подклассом T (§8.1.4). Переменная экземпляра фактически перестаёт существовать, когда объект, частью которого она является, больше не ссылается на него, после завершения любых необходимых финализаций объекта (§12.6). -
Элементы массива — это безымянные переменные, которые создаются и инициализируются значениями по умолчанию (§4.12.5) всякий раз, когда создаётся новый массив (§10 (Массивы), §15.10.2). Элементы массива фактически перестают существовать, когда на массив больше нет ссылок.
-
Параметры метода (§8.4.1) — имена значений аргументов, передаваемых методу.
Для каждого параметра, объявленного в объявлении метода, создаётся новая переменная параметра каждый раз, когда вызывается этот метод (§15.12). Новая переменная инициализируется соответствующим значением аргумента из вызова метода. Параметр метода фактически перестаёт существовать, когда завершается выполнение тела метода.
-
Параметры конструктора (§8.8.1) — имена значений аргументов, передаваемых конструктору.
Для каждого параметра, объявленного в объявлении конструктора, создаётся новая переменная параметра каждый раз, когда выражение создания экземпляра класса (§15.9) или явный вызов конструктора (§8.8.7) вызывает этот конструктор. Новая переменная инициализируется соответствующим значением аргумента из выражения создания или вызова конструктора. Параметр конструктора фактически перестаёт существовать, когда завершается выполнение тела конструктора.
-
Параметры лямбда-выражения (§15.27.1) — имена значений аргументов, передаваемых в тело лямбда-выражения (§15.27.2).
Для каждого параметра, объявленного в лямбда-выражении, создаётся новая переменная параметра каждый раз, когда вызывается метод, реализованный телом лямбда-выражения (§15.12). Новая переменная инициализируется соответствующим значением аргумента из вызова метода. Параметр лямбда-выражения фактически перестаёт существовать, когда завершается выполнение тела лямбда-выражения.
-
Переменная исключения создаётся каждый раз, когда исключение перехватывается блоком
catchоператораtry(§14.20).Новая переменная инициализируется фактическим объектом, связанным с исключением (§11.3, §14.18). Переменная исключения фактически перестаёт существовать, когда завершается выполнение блока, связанного с блоком
catch. -
Локальные переменные объявляются операторами объявления локальных переменных (§14.4).
Всякий раз, когда поток управления входит в блок (§14.2) или оператор
for(§14.14), для каждой локальной переменной, объявленной в операторе объявления локальной переменной, непосредственно содержащемся в этом блоке или оператореfor, создаётся новая переменная.Оператор объявления локальной переменной может содержать выражение, которое инициализирует переменную. Однако локальная переменная с инициализирующим выражением не инициализируется до тех пор, пока не выполнится оператор объявления локальной переменной, который её объявляет. (Правила определённой инициализации (§16 (Определённая инициализация)) предотвращают использование значения локальной переменной до её инициализации или присвоения значения.) Локальная переменная фактически перестаёт существовать по завершении выполнения блока или оператора for.
Если бы не одно исключительное положение, локальную переменную можно было бы всегда рассматривать как созданную при выполнении оператора объявления локальной переменной. Исключение связано с оператором
switch(§14.11), где возможно вхождение в блок, но пропуск выполнения оператора объявления локальной переменной. Однако из-за ограничений, накладываемых правилами определённой инициализации (§16 (Определённая инициализация)), локальная переменная, объявленная таким пропущенным оператором объявления локальной переменной, не может быть использована до тех пор, пока ей не будет определённо присвоено значение выражением присваивания (§15.26).
Пример 4.12.3-1. Разные виды переменных
class Point {
static int numPoints; // numPoints is a class variable
int x, y; // x and y are instance variables
int[] w = new int[10]; // w[0] is an array component
int setX(int x) { // x is a method parameter
int oldx = this.x; // oldx is a local variable
this.x = x;
return oldx;
}
}
Переменная может быть объявлена final. Переменная final может быть присвоена только один раз. Это ошибка времени компиляции, если переменной final присваивается значение, если только она не является явно неинициализированной непосредственно перед присвоением (§16 (Определённое присвоение)).
После присвоения значению переменной final она всегда содержит одно и то же значение. Если переменная final ссылается на объект, то состояние объекта может быть изменено операциями над объектом, но переменная всегда будет ссылаться на тот же объект. Это также относится к массивам, поскольку массивы являются объектами; если переменная final ссылается на массив, то компоненты массива могут быть изменены операциями над массивом, но переменная всегда будет ссылаться на тот же массив.
Пустая переменная — это переменная final, объявление которой не содержит инициализатора.
Константная переменная — это переменная final примитивного типа или типа final, которая инициализируется константным выражением (§15.28). Является ли переменная константной или нет, может повлиять на инициализацию класса (§12.4.1), бинарную совместимость (§13.1), достижимость (§14.21) и определённое присвоение (§16.1.1).
Три вида переменных неявно объявляются final: поле интерфейса (§9.3), локальная переменная, объявленная как ресурс оператора try-with-resources (§14.20.3), и параметр исключения в операторе многократной обработки исключений (§14.20). Параметр исключения в операторе обработки одного исключения никогда не объявляется неявно final, но может быть эффективно final.
Пример 4.12.4-1. Переменные final
Объявление переменной final может служить полезной документацией, что её значение не будет изменено, и может помочь избежать ошибок программирования. В этой программе:
class Point {
int x, y;
int useCount;
Point(int x, int y) { this.x = x; this.y = y; }
static final Point origin = new Point(0, 0);
}
класс Point объявляет переменную класса final origin. Переменная origin хранит ссылку на объект, являющийся экземпляром класса Point, координаты которого (0, 0). Значение переменной Point.origin никогда не изменится, поэтому она всегда будет ссылаться на тот же объект Point, созданный своим инициализатором. Однако операция над этим объектом Point может изменить его состояние — например, изменить его useCount или даже, вводя в заблуждение, его x или y координаты.
Некоторые переменные, которые не объявлены final, считаются эффективно final:
-
Локальная переменная, декларатор которой содержит инициализатор (§14.4.2), является эффективно final, если все следующие условия выполняются:
-
Она не объявлена final.
-
Она никогда не используется в левой части выражения присваивания (§15.26). (Обратите внимание, что декларатор локальной переменной, содержащий инициализатор, не является выражением присваивания.)
-
Она никогда не используется в качестве операнда префиксной или постфиксной операции инкремента или декремента (§15.14, §15.15).
-
-
Локальная переменная, декларатор которой не содержит инициализатор, является эффективно final, если выполняются все следующие условия:
-
Она не объявлена final.
-
Всякий раз, когда она используется в левой части выражения присваивания, она явно не инициализирована и не явно присвоена до присвоения; то есть, она явно не инициализирована и не явно присвоена после правой части выражения присваивания (§16 (Определённое присвоение)).
-
Она никогда не используется в качестве операнда префиксной или постфиксной операции инкремента или декремента.
-
-
Параметр метода, конструктора, лямбда-выражения или исключения (§8.4.1, §8.8.1, §9.4, §15.27.1, §14.20) для целей определения, является ли она эффективно final, обрабатывается как локальная переменная, декларатор которой содержит инициализатор.
Если переменная является эффективно final, добавление модификатора final к её объявлению не создаст ошибок компиляции. Обратно, локальная переменная или параметр, объявленная final в корректной программе, становится эффективно final, если модификатор final удалён.
Каждая переменная в программе должна иметь значение до того, как её значение будет использовано:
-
Каждая переменная класса, экземпляра или компонент массива инициализируется значением по умолчанию при создании (§15.9, §15.10.2):
-
Для типа
byteзначением по умолчанию является ноль, то есть значение(byte)0. -
Для типа
shortзначением по умолчанию является ноль, то есть значение(short)0. -
Для типа
intзначением по умолчанию является ноль, то есть0. -
Для типа
longзначением по умолчанию является ноль, то есть0L. -
Для типа
floatзначением по умолчанию является положительный ноль, то есть0.0f. -
Для типа
doubleзначением по умолчанию является положительный ноль, то есть0.0d. -
Для типа
charзначением по умолчанию является нулевой символ, то есть'\u0000'. -
Для типа
booleanзначением по умолчанию являетсяfalse. -
Для всех типов ссылок (§4.3) значением по умолчанию является
null.
-
-
Каждый параметр метода (§8.4.1) инициализируется соответствующим значением аргумента, предоставленным вызывающей стороной метода (§15.12).
-
Каждый параметр конструктора (§8.8.1) инициализируется соответствующим значением аргумента, предоставленным выражением создания экземпляра класса (§15.9) или явным вызовом конструктора (§8.8.7).
-
Параметр исключения (§14.20) инициализируется брошенным объектом, представляющим исключение (§11.3, §14.18).
-
Локальная переменная (§14.4, §14.14) должна быть явно инициализирована до использования, путём инициализации (§14.4) или присваивания (§15.26), способом, который можно проверить с помощью правил определённого присваивания (§16 (Определённое присваивание)).
Пример 4.12.5-1. Начальные значения переменных
class Point {
static int npoints;
int x, y;
Point root;
}
class Test {
public static void main(String[] args) {
System.out.println("npoints=" + Point.npoints);
Point p = new Point();
System.out.println("p.x=" + p.x + ", p.y=" + p.y);
System.out.println("p.root=" + p.root);
}
}
Эта программа выводит:
npoints=0 p.x=0, p.y=0 p.root=null
иллюстрируя начальную инициализацию npoints, которая происходит при подготовке класса Point (§12.3.2), и начальную инициализацию x, y и root, которая происходит при создании нового экземпляра Point. См. §12 (Выполнение) для полного описания всех аспектов загрузки, компоновки и инициализации классов и интерфейсов, а также описания создания экземпляров классов для создания новых экземпляров класса.
В языке программирования Java каждая переменная и каждое выражение имеют тип, который можно определить во время компиляции. Тип может быть примитивным типом или ссылочным типом. Ссылочные типы включают типы классов и типы интерфейсов. Ссылочные типы вводятся с помощью объявлений типов, которые включают объявления классов (§8.1) и объявления интерфейсов (§9.1). Мы часто используем термин тип для обозначения класса или интерфейса.
В виртуальной машине Java каждый объект принадлежит какому-то определённому классу: классу, который был упомянут в выражении создания, которое произвело объект (§15.9), или классу, чей Class объект был использован для вызова рефлексивного метода для создания объекта, или классу String для объектов, неявно созданных оператором конкатенации строк + (§15.18.1). Этот класс называется классом объекта. Говорят, что объект является экземпляром своего класса и всех его суперклассов.
Каждый массив также имеет класс. Метод getClass, когда вызывается для объекта массива, вернёт объект класса Class, который представляет класс массива (§10.8).
Тип переменной во время компиляции всегда объявляется, а тип выражения во время компиляции может быть выведен во время компиляции. Тип переменной во время компиляции ограничивает возможные значения, которые может содержать переменная во время выполнения, или выражение может произвести во время выполнения. Если значение во время выполнения является ссылкой, которая не является null, оно ссылается на объект или массив, у которого есть класс, и этот класс обязательно будет совместим с типом во время компиляции.
Даже если переменная или выражение может иметь тип во время компиляции, который является типом интерфейса, экземпляров интерфейсов не существует. Переменная или выражение, тип которого является типом интерфейса, может ссылаться на любой объект, чья класс реализует (§8.1.5) этот интерфейс.
Иногда говорят, что переменная или выражение имеет "тип во время выполнения". Это относится к классу объекта, на который ссылается значение переменной или выражения во время выполнения, при условии, что это значение не null.
Соответствие между типами во время компиляции и типами во время выполнения неполно по двум причинам:
-
Во время выполнения классы и интерфейсы загружаются виртуальной машиной Java с помощью загрузчиков классов. Каждый загрузчик классов определяет свой собственный набор классов и интерфейсов. В результате возможно, что два загрузчика классов загрузят идентичное определение класса или интерфейса, но создадут разные классы или интерфейсы во время выполнения. Следовательно, код, который компилировался правильно, может завершиться неудачей во время компоновки, если загрузчики классов, которые его загружают, несовместимы.
См. статью Динамическая загрузка классов в виртуальной машине Java, Сянь Лиан и Гилад Брэча, в Труды OOPSLA '98, опубликованные как ACM SIGPLAN Notices, том 33, номер 10, октябрь 1998 г., страницы 36-44, и Спецификация виртуальной машины Java, издание Java SE 11 для получения более подробной информации.
-
Параметрические типы (§4.4) и аргументы типа (§4.5.1) не являются реифицированными во время выполнения. В результате один и тот же класс или интерфейс во время выполнения представляет собой несколько параметризованных типов (§4.5) во время компиляции. В частности, все параметризации во время компиляции данного обобщённого типа (§8.1.2, §9.1.2) разделяют одно представление во время выполнения.
В определённых условиях переменная параметризованного типа может ссылаться на объект, который не является объектом этого параметризованного типа. Такая ситуация известна как загрязнение кучи (§4.12.2). Переменная всегда будет ссылаться на объект, который является экземпляром класса, который представляет собой параметризованный тип.
Пример 4.12.6-1. Тип переменной по сравнению с классом объекта
interface Colorable {
void setColor(byte r, byte g, byte b);
}
class Point { int x, y; }
class ColoredPoint extends Point implements Colorable {
byte r, g, b;
public void setColor(byte rv, byte gv, byte bv) {
r = rv; g = gv; b = bv;
}
}
class Test {
public static void main(String[] args) {
Point p = new Point();
ColoredPoint cp = new ColoredPoint();
p = cp;
Colorable c = cp;
}
}
В этом примере:
-
Локальная переменная
pметодаmainклассаTestимеет типPointи первоначально получает ссылку на новый экземпляр классаPoint. -
Локальная переменная
cpаналогичным образом имеет типColoredPointи первоначально получает ссылку на новый экземпляр классаColoredPoint. -
Присваивание значения
cpпеременнойpприводит к тому, чтоpхранит ссылку на объектColoredPoint. Это разрешено, потому чтоColoredPointявляется подклассомPoint, поэтому классColoredPointсовместим с типомPoint(присваивание). ОбъектColoredPointсодержит поддержку всех методов классаPoint. В дополнение к собственным полямr,gиb, он содержит поля классаPoint, а именноxиy. -
Локальная переменная
cимеет тип интерфейсаColorable, поэтому может содержать ссылку на любой объект, чья класс реализует интерфейсColorable; в частности, может содержать ссылку на объект классаColoredPoint.
Обратите внимание, что выражение, такое как new
Colorable(), не является допустимым, так как невозможно создать экземпляр интерфейса, можно создать только экземпляр класса. Однако выражение new Colorable() { public void
setColor... } является допустимым, так как оно объявляет анонимный класс (§15.9.5), который реализует интерфейс Colorable.
© Oracle and/or its affiliates. All rights reserved.
Licensed under the Oracle Technology Network License Agreement.