Глава 4. Типы, значения и переменные
Оглавление
- 4.1. Виды типов и значений
- 4.2. Примитивные типы и значения
- 4.3. Ссылочные типы и значения
- 4.4. Переменные типов
- 4.5. Параметризованные типы
- 4.6. Стирание типов
- 4.7. Реализуемые типы
- 4.8. Необработанные типы
- 4.9. Пересекающиеся типы
- 4.10. Подтипизация
- 4.11. Места использования типов
- 4.12. Переменные
Язык программирования 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, представляющую его значение в десятичном формате, а затем создает новую строку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, если требуется преобразование разворачивания (§5.1.8) нулевой ссылки. -
Оператор целочисленного деления
/(§15.17.2) и оператор целочисленного остатка%(§15.17.3) могут генерировать исключениеArithmeticException, если правой операнд равен нулю. -
Операторы инкремента и декремента
++(§15.14.2, §15.15.1) и--(§15.14.3, §15.15.2) могут генерировать исключениеOutOfMemoryError, если требуется преобразование упаковки (§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, которые концептуально связаны со значениями и операциями формата IEEE 754 с одинарной точностью (32 бита) и двойной точностью (64 бита), как указано в Стандарте IEEE для бинарной арифметики с плавающей запятой, ANSI/IEEE Стандарт 754-1985 (IEEE, Нью-Йорк).
Стандарт IEEE 754 включает не только положительные и отрицательные числа, состоящие из знака и модуля, но также положительные и отрицательные нули, положительные и отрицательные бесконечности и специальные значения Не Число (далее сокращенно 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 на 2 и увеличить e на 1, чтобы получить второе представление для того же значения v. Представление в этой форме называется нормализованным, если m ≥ 2N-1; в противном случае представление называется денормализованным. Если значение в наборе значений нельзя представить таким образом, что m ≥ 2N-1, то значение называется денормализованным значением, потому что у него нет нормализованного представления.
Ограничения параметров N и K (и производных параметров Emin и Emax) для двух необходимых и двух необязательных наборов значений с плавающей точкой приведены в таблице 4.2.3-A.
Таблица 4.2.3-A. Параметры наборов значений с плавающей точкой
| Параметр | float | float-extended-exponent | double | double-extended-exponent |
|---|---|---|---|---|
| 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-extended-exponent, double и double-extended-exponent. Аналогично, каждый элемент набора значений double также является элементом набора значений double-extended-exponent. Каждый расширенный набор значений имеет больший диапазон значений показателя, чем соответствующий стандартный набор значений, но не имеет большей точности.
Элементы набора значений float — это точно те значения, которые могут быть представлены с помощью формата с плавающей точкой одинарной точности, определённого в стандарте IEEE 754. Элементы набора значений double — это точно те значения, которые могут быть представлены с помощью формата с плавающей точкой двойной точности, определенного в стандарте IEEE 754. Однако элементы наборов значений float-extended-exponent и double-extended-exponent, определённых здесь, не соответствуют значениям, которые могут быть представлены с использованием форматов IEEE 754 single extended и double extended соответственно.
Наборы значений float, float-extended-exponent, double и double-extended-exponent не являются типами. Для реализации языка программирования Java всегда верно использование элемента набора значений float для представления значения типа float; однако в определенных частях кода может быть допустимо использование элемента набора значений float-extended-exponent вместо этого. Аналогично, для реализации всегда верно использование элемента набора значений double для представления значения типа double; однако в определенных частях кода может быть допустимо использование элемента набора значений double-extended-exponent вместо этого.
За исключением 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 — false. Но другие операции могут отличать положительный и отрицательный ноль; например, 1.0/0.0 имеет значение плюс бесконечность, а значение 1.0/-0.0 — минус бесконечность.
NaN неупорядочен, поэтому:
-
Операторы числового сравнения
<,<=,>и>=возвращаютfalse, если один или оба операнда являются NaN (§15.20.1). -
Оператор равенства
==возвращаетfalse, если любой из операндов является NaN.В частности,
(x<y) == !(x>=y)будетfalse, еслиxилиyравно 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.0e+308*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)
-
Операторы условного AND и условного OR && (§15.23) и || (§15.24)
-
Условный оператор ?: (§15.25)
-
Оператор конкатенации строк + (§15.18.1), который, при применении к операнду boolean и операнду String, преобразует операнд boolean в строку (либо "true", либо "false"), а затем создаёт новую строку, являющуюся конкатенацией двух строк
Выражения boolean определяют поток управления в различных типах операторов:
Выражение boolean также определяет, какое подвыражение вычисляется в условном операторе ?: (§15.25).
Только выражения boolean и int могут использоваться в операторах управления потоком и в качестве первого операнда условного оператора ?.
Выражение целого или вещественного типа может быть преобразовано в значение boolean, в соответствии с соглашениями языка C, где любое ненулевое значение — это true, с помощью выражения x!=0.
Ссылка на объект может быть преобразована в значение boolean, в соответствии с соглашениями языка C, где любая ссылка, отличная от null, — это true, с помощью выражения obj!=null.
Значение boolean может быть преобразовано в строку с помощью преобразования в строку (§5.4).
Значение boolean может быть приведено к типу int, float или long (§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).
Новый экземпляр класса неявно создаётся, когда используется оператор конкатенации строк + (§15.18.1) в неконстантном выражении (§15.28), что приводит к созданию нового объекта типа String (§4.3.3).
Новый объект массива неявно создаётся, когда вычисляется выражение инициализатора массива (§10.6); это может произойти при инициализации класса или интерфейса (§12.4), при создании нового экземпляра класса (§15.9) или при выполнении оператора объявления локальной переменной (§14.4).
Новые объекты типов Boolean, Byte, Short, Character, Integer, Long, Float и Double могут быть неявно созданы при преобразовании в ящике (§5.1.7).
Пример 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).
Два типа ссылок являются одинаковыми типами на этапе компиляции, если у них одинаковое двоичное имя (§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>.
Исторически джойлд-кары являются прямыми потомками работ Ацуши Игараши и Мирко Вироли. Читатели, заинтересованные в более подробном обсуждении, должны обратиться к статье О подтипировании на основе вариативности для параметризованных типов Ацуши Игараши и Мирко Вироли в Трудах 16-й европейской конференции по объектно-ориентированному программированию (ECOOP 2002). Эта работа сама по себе основана на более ранних работах Крёстэна Торупа и Мадса Торгэрсена (Объединение обобщений, 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.9), приведениях типов (§5.5.2), объявлениях методов (§8.4.1, §8.4.8.3, §8.4.8.4, §9.4.1.2) и вызовах методов с переменным числом аргументов (§15.12.4.2).
Данные предупреждения охватывают случай, когда устаревший клиент использует дженеризованную библиотеку. Например, библиотека объявляет дженеризованный класс 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' — интерфейс, члены которого совпадают с
publicчленами Tj; в противном случае, если 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, и тип null. Мы пишем 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 являются все следующие:
Учитывая объявление generic типа C<F1,...,Fn> (n > 0), прямыми супертипами raw типа C (§4.8) являются все следующие:
-
Прямой суперкласс raw типа C.
-
Прямые суперинтерфейсы raw типа C.
-
Тип
Object, если C<F1,...,Fn>является generic типом интерфейса без прямых суперинтерфейсов (§9.1.2).
Учитывая объявление generic типа C<F1,...,Fn> (n > 0), прямыми супертипами generic типа C<F1,...,Fn> являются все следующие:
-
Прямой суперкласс C
<F1,...,Fn>. -
Прямые суперинтерфейсы C
<F1,...,Fn>. -
Тип
Object, если C<F1,...,Fn>является generic типом интерфейса без прямых суперинтерфейсов. -
Raw тип C.
Учитывая объявление generic типа C<F1,...,Fn> (n > 0), прямыми супертипами параметризованного типа C<T1,...,Tn>, где Ti (1 ≤ i ≤ n) является типом, являются все следующие:
-
D
<U1 θ,...,Uk θ>, где D<U1,...,Uk>является generic типом, который является прямым супертипом generic типа C<T1,...,Tn>и θ является заменой[F1:=T1,...,Fn:=Tn]. -
C
<S1,...,Sn>, где Si содержит Ti (1 ≤ i ≤ n) (§4.5.1). -
Тип
Object, если C<F1,...,Fn> является generic типом интерфейса без прямых суперинтерфейсов. -
Raw тип C.
Учитывая объявление generic типа 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).
Прямые супертипы переменной типа - это типы, перечисленные в ее границе.
Переменная типа является прямым супертипом своей нижней границы.
Прямые супертипы null типа - все ссылочные типы, кроме самого null типа.
Следующие правила определяют отношение прямого супертипа среди типов массивов:
-
Если S и T являются типами ссылок, то S
[]>1T[]iff 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 если U = V, в противном случае
?extendslub(U, V) - ...
-
- ...
Строго говоря, эта функция lub() лишь приближает наименьшую верхнюю границу. Формально, может существовать другой тип T, такой что все U1 ... Uk являются подтипами T, и T является подтипом lub(U1, ..., Uk). Однако, компилятор языка программирования Java должен реализовывать lub() как указано выше.
Возможна ситуация, когда функция lub() возвращает бесконечный тип. Это допустимо, и компилятор языка программирования Java должен распознавать такие ситуации и представлять их соответствующим образом, используя циклические структуры данных.
Возможность бесконечного типа обусловлена рекурсивными вызовами lub(). Читателям, знакомым с рекурсивными типами, следует отметить, что бесконечный тип отличается от рекурсивного типа.
Типы используются в большинстве видов деклараций и в определённых видах выражений. Конкретно, существуют 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.1)
-
Тип в декларации локальной переменной (§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.9, §5.5.2, §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 переменная примитивного типа или типа String, которая инициализируется константным выражением (§15.28). То, является ли переменная постоянной, может иметь последствия для инициализации классов (§12.4.1), бинарной совместимости (§13.1, §13.4.9) и определённой инициализации (§16 (Определённая инициализация)).
Три вида переменных неявно объявляются final: поле интерфейса (§9.3), локальная переменная, которая является ресурсом оператора try-with-resources (§14.20.3), и параметр исключения в много-catch операторе (§14.20). Параметр исключения в одно-catch операторе никогда не неявно объявляется 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 8 для получения более подробной информации.
-
Переменные типов (§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.