Глава 4. Типы, значения и переменные
Оглавление
Язык программирования Java — это язык статической типизации, что означает, что каждая переменная и каждое выражение имеют тип, известный во время компиляции.
Язык программирования Java также является языком сильной типизации, поскольку типы ограничивают значения, которые может содержать переменная (§4.12), или которые может генерировать выражение, ограничивают поддерживаемые операции над этими значениями и определяют смысл операций. Сильная статическая типизация помогает обнаруживать ошибки на этапе компиляции.
Типы языка программирования Java делятся на два вида: примитивные типы и ссылочные типы. Примитивные типы (§4.2) — это тип boolean и числовые типы. Числовые типы — это целочисленные типы byte, short, int, long и char, а также типы с плавающей точкой float и double. Ссылочные типы (§4.3) — это типы классов, типы интерфейсов и типы массивов. Также существует специальный тип null. Объект (§4.3.1) — это динамически созданный экземпляр типа класса или динамически созданный массив. Значения ссылочного типа — это ссылки на объекты. Все объекты, включая массивы, поддерживают методы класса Object (§4.3.2). Строковые литералы представляются объектами String (§4.3.3).
В языке программирования Java существует два вида типов: примитивные типы (§4.2) и ссылочные типы (§4.3). Соответственно, существуют два вида данных, которые могут храниться в переменных, передаваться в качестве аргументов, возвращаться методами и обрабатываться: примитивные значения (§4.2) и ссылочные значения (§4.3).
Также существует специальный тип null, тип выражения null (§3.10.8, §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 binary32, и double, значения которых точно соответствуют 64-битным числам с плавающей точкой IEEE 754 binary64.
Тип boolean имеет ровно два значения: true и false.
Значения целочисленных типов находятся в следующих диапазонах:
-
Для
byte, от -128 до 127 включительно -
Для
short, от -32768 до 32767 включительно -
Для
int, от -2147483648 до 2147483647 включительно -
Для
long, от -9223372036854775808 до 9223372036854775807 включительно -
Для
char, от'\u0000'до'\uffff'включительно, то есть от 0 до 65535
Язык программирования Java предоставляет ряд операторов, действующих над целочисленными значениями:
-
Операторы сравнения, которые приводят к значению типа
boolean: -
Числовые операторы, которые приводят к значению типа
intилиlong:-
Множительные операторы
*,/и%(§15.17) -
Сложение операторы
+и-(§15.18) -
Оператор инкремента
++, как префиксный (§15.15.1) так и постфиксный (§15.14.2) -
Оператор декремента
--, как префиксный (§15.15.2) так и постфиксный (§15.14.3) -
Операторы сдвига со знаком и без знака
<<,>>и>>>(§15.19) -
Оператор поразрядного дополнения
~(§15.15.5) -
Поразрядные целочисленные операторы
&,^и|(§15.22.1)
-
Условный оператор
? :(§15.25) -
Оператор приведения типа (§15.16), который может преобразовывать целочисленное значение в значение любого заданного числового типа
-
Оператор конкатенации строк
+(§15.18.1), который, когда получает операнд типаStringи целочисленный операнд, преобразует целочисленный операнд вString(десятичная форма операндаbyte,short,intилиlong, или символ операндаchar), а затем создаёт новую строку, являющуюся конкатенацией двух строк
Другие полезные конструкторы, методы и константы предопределены в классах 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, которые концептуально связаны с 32-битным двоичным форматом float и 64-битным двоичным форматом double для значений и операций IEEE 754, как указано в стандарте IEEE 754 (§1.7).
В Java SE 15 и более поздних версиях язык программирования Java использует версию стандарта IEEE 754 2019 года. В версиях Java SE до Java SE 15 язык программирования Java использовал версию стандарта IEEE 754 1985 года, где формат binary32 был известен как формат single, а формат binary64 — как формат double.
Стандарт IEEE 754 включает не только положительные и отрицательные числа, состоящие из знака и модуля, но также положительный и отрицательный ноль, положительную и отрицательную бесконечность и специальные значения Not-a-Number (в дальнейшем сокращённо NaN). Значение NaN используется для представления результата некоторых недопустимых операций, таких как деление нуля на ноль. Константы NaN типов float и double предварительно определены как Float.NaN и Double.NaN.
Конечные ненулевые значения типа с плавающей точкой могут быть выражены в форме s ⋅ m ⋅ 2(e - N + 1), где:
-
s равно +1 или -1,
-
m — положительное целое число, меньшее 2
N, -
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) для float и double приведены в таблице 4.2.3-A.
Таблица 4.2.3-A. Параметры чисел с плавающей точкой
| Параметр | float | double |
|---|---|---|
N | 24 | 53 |
| K | 8 | 11 |
| Emax | +127 | +1023 |
| Emin | -126 | -1022 |
За исключением NaN, значения чисел с плавающей точкой упорядочены. В порядке возрастания они расположены следующим образом: отрицательная бесконечность, отрицательные конечные ненулевые значения, отрицательный и положительный ноль, положительные конечные ненулевые значения и положительная бесконечность.
Стандарт IEEE 754 допускает несколько различных значений NaN для каждого из его форматов чисел с плавающей точкой binary32 и binary64. Однако платформа Java SE обычно рассматривает значения NaN данного типа с плавающей точкой как сворачивающиеся в одно каноническое значение, и поэтому данное описание обычно относится к произвольному NaN как к каноническому значению.
Согласно IEEE 754, операция с плавающей точкой с аргументами, не являющимися NaN, может сгенерировать результат NaN. IEEE 754 определяет набор шаблонов бит NaN, но не предписывает конкретный шаблон NaN для представления результата NaN; это оставляется на усмотрение архитектуры оборудования. Программист может создавать NaN с различными шаблонами бит для кодирования, например, ретроспективной диагностической информации. Эти значения NaN могут быть созданы с помощью методов Float.intBitsToFloat и Double.longBitsToDouble для float и double соответственно. И наоборот, для просмотра шаблонов бит значений 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).В частности,
(x<y) == !(x>=y)будетfalse, еслиxилиyявляется NaN. -
Оператор равенства
==возвращаетfalse, если один из операндов равен NaN. -
Оператор неравенства
!=возвращаетtrue, если один из операндов равен NaN (§15.21.1).В частности,
x!=xбудетtrue, если и только еслиxявляется NaN.
Язык программирования Java предоставляет ряд операторов, которые действуют над значениями с плавающей точкой:
-
Операторы сравнения, которые приводят к значению типа
boolean: -
Числовые операторы, которые приводят к значению типа
floatилиdouble: -
Условный оператор
? :(§15.25) -
Оператор приведения типа (§15.16), который может преобразовать значение с плавающей точкой в значение любого указанного числового типа
-
Оператор конкатенации строк
+(§15.18.1), который, при задании операнда типаStringи операнда с плавающей точкой, преобразует операнд с плавающей точкой в строкуString, представляющую его значение в десятичной форме (без потерь информации), а затем создаёт новую строкуString, конкатенировав две строки
Другие полезные конструкторы, методы и константы определены в классах Float, Double и Math.
Если хотя бы один из операндов бинарного оператора имеет тип с плавающей точкой, то операция является операцией с плавающей точкой, даже если другой операнд является целым.
Если хотя бы один из операндов числового оператора имеет тип double, то операция выполняется с использованием 64-битной арифметики с плавающей точкой, а результат числового оператора — значение типа double. Если другой операнд не является типом double, он сначала расширяется (§5.1.5) до типа double путём числового повышения (§5.6).
В противном случае, по крайней мере, один из операндов имеет тип float; операция выполняется с использованием 32-битной арифметики с плавающей точкой, а результат числового оператора — значение типа float. Если другой операнд не является типом float, он сначала расширяется до типа float путём числового повышения.
Арифметика с плавающей точкой выполняется в соответствии с правилами стандарта IEEE 754, включая переполнение и недополнение (§15.4), за исключением оператора остатка % (§15.17.3).
Любое значение типа с плавающей точкой может быть приведено к любому числовому типу или из него. Нет приведений между типами с плавающей точкой и типом boolean.
См. §4.2.5 для способа преобразования выражений с плавающей точкой в boolean.
Оператор с плавающей точкой может выбросить исключение (§11 (Исключения)) по следующим причинам:
-
Любой оператор с плавающей точкой может выбросить исключение
NullPointerException, если требуется преобразование из неявного типа null. -
Операторы инкремента и декремента
++(§15.14.2, §15.15.1) и--(§15.14.3, §15.15.2) могут выбросить исключениеOutOfMemoryError, если требуется явное преобразование, и нет достаточного объема памяти для выполнения преобразования.
Пример 4.2.4-1. Операции с плавающей точкой
class Test {
public static void main(String[] args) {
// An example of overflow:
double d = 1e308;
System.out.print("overflow produces infinity: ");
System.out.println(d + "*10==" + d*10);
// An example of gradual underflow:
d = 1e-305 * Math.PI;
System.out.print("gradual underflow: " + d + "\n ");
for (int i = 0; i < 4; i++)
System.out.print(" " + (d /= 100000));
System.out.println();
// An example of NaN:
System.out.print("0.0/0.0 is Not-a-Number: ");
d = 0.0/0.0;
System.out.println(d);
// An example of inexact results and rounding:
System.out.print("inexact results with float:");
for (int i = 0; i < 100; i++) {
float z = 1.0f / i;
if (z * i != 1.0f)
System.out.print(" " + i);
}
System.out.println();
// Another example of inexact results and rounding:
System.out.print("inexact results with double:");
for (int i = 0; i < 100; i++) {
double z = 1.0 / i;
if (z * i != 1.0)
System.out.print(" " + i);
}
System.out.println();
// An example of cast to integer rounding:
System.out.print("cast to int rounds toward 0: ");
d = 12345.6;
System.out.println((int)d + " " + (int)(-d));
}
}
Эта программа выводит:
overflow produces infinity: 1.0E308*10==Infinity
gradual underflow: 3.141592653589793E-305
3.1415926535898E-310 3.141592653E-315 3.142E-320 0.0
0.0/0.0 is Not-a-Number: NaN
inexact results with float: 0 41 47 55 61 82 83 94 97
inexact results with double: 0 49 98
cast to int rounds toward 0: 12345 -12345
Этот пример демонстрирует, среди прочего, что постепенное недополнение может привести к постепенной потере точности.
Результаты, когда i равно 0, включают деление на ноль, так что z становится положительной бесконечностью, а z * 0 — NaN, что не равно 1.0.
Тип boolean представляет логическое значение с двумя возможными значениями, обозначаемыми литералами true и false (§3.10.3).
Булевы операторы:
-
Операторы отношения
==и!=(§15.21.2) -
Оператор логического отрицания
!(§15.15.6) -
Логические операторы
&,^и|(§15.22.2) -
Операторы условного И и условного ИЛИ
&&(§15.23) и||(§15.24) -
Условный оператор
? :(§15.25) -
Оператор конкатенации строк
+(§15.18.1), который при применении к операнду типаStringи операнду типаbooleanпреобразует операнд типаbooleanв строку типаString(либо"true", либо"false"), а затем создаёт новую строку, являющуюся конкатенацией двух строк.
Булевы выражения определяют поток управления в различных операторах:
Булевое выражение также определяет, какое подвыражение оценивается в условном операторе ? : (§15.25).
Только boolean и Boolean выражения могут быть использованы в операторах управления потоком и в качестве первого операнда условного оператора ? :.
Целое или дробное выражение x может быть преобразовано в булевое значение boolean, следуя соглашению языка C, что любое ненулевое значение является true, при помощи выражения x!=0.
Ссылка на объект obj может быть преобразована в булевое значение boolean, следуя соглашению языка C, что любая ссылка, отличная от null, является true, при помощи выражения obj!=null.
Булевое значение boolean может быть преобразовано в строковое значение String путём преобразования в строку (§5.4).
Булевое значение boolean может быть приведено к типу boolean, Boolean или Object (§5.5). Другие приведения к типу boolean недопустимы.
Существует четыре вида типов ссылок: типы классов (§8.1), типы интерфейсов (§9.1), переменные типов (§4.4) и типы массивов (§10.1).
Пример кода:
class Point { int[] metrics; }
interface Move { void move(int deltax, int deltay); }
объявляет тип класса Point, тип интерфейса Move и использует тип массива int[] (массив int) для объявления поля metrics класса Point.
Тип класса или интерфейса состоит из идентификатора или последовательности идентификаторов, разделенных точками, где каждый идентификатор может быть необязательно последован аргументами типа (§4.5.1). Если аргументы типа присутствуют в типе класса или интерфейса, то это параметризованный тип (§4.5).
Каждый идентификатор в типе класса или интерфейса классифицируется как имя пакета или имя типа (§6.5.1). Идентификаторы, классифицированные как имена типов, могут быть аннотированы. Если тип класса или интерфейса имеет вид T.id (необязательно последован аргументами типа), то id должно быть простым именем доступного вложенного типа T (§6.6, §8.5, §9.5), иначе возникает ошибка компиляции. Тип класса или интерфейса обозначает этот вложенный тип.
Объект — это экземпляр класса или массив.
Значения ссылок (часто просто ссылки) являются указателями на эти объекты и особой ссылкой null, которая не ссылается ни на какой объект.
Экземпляр класса явным образом создается выражением создания экземпляра класса (§15.9).
Массив явным образом создается выражением создания массива (§15.10.1).
Другие выражения могут неявно создавать экземпляр класса (§12.5) или массив (§10.6).
Пример 4.3.1-1. Создание объекта
class Point {
int x, y;
Point() { System.out.println("default"); }
Point(int x, int y) { this.x = x; this.y = y; }
/* A Point instance is explicitly created at
class initialization time: */
static Point origin = new Point(0,0);
/* A String can be implicitly created
by a + operator: */
public String toString() { return "(" + x + "," + y + ")"; }
}
class Test {
public static void main(String[] args) {
/* A Point is explicitly created
using newInstance: */
Point p = null;
try {
p = (Point)Class.forName("Point").newInstance();
} catch (Exception e) {
System.out.println(e);
}
/* An array is implicitly created
by an array initializer: */
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), который, когда получает операндStringи ссылку, преобразует ссылку вString, вызывая методtoStringсвязанного объекта (используя"null", если ссылка или результатtoString— это ссылка null), а затем создаёт новуюString, которая является конкатенацией двух строк -
Оператор
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) и текстовые блоки (§3.10.6) являются ссылками на экземпляры класса String.
Оператор конкатенации строк + (§15.18.1) неявно создаёт новый объект String, когда результат не является константным выражением (§15.29).
Два типа ссылок являются одинаковым типом на этапе компиляции, если они объявлены в единицах компиляции, связанных с одним модулем (§7.3), имеют одинаковое двоичное имя (§13.1), и их аргументы типа, если таковые имеются, одинаковы, применяя это определение рекурсивно.
Когда два типа ссылок одинаковы, их иногда называют одним классом или одним интерфейсом.
Во время выполнения несколько типов ссылок с одинаковым двоичным именем могут быть загружены одновременно различными загрузчиками классов. Эти типы могут или не могут представлять одну и ту же декларацию типа. Даже если два таких типа представляют одну и ту же декларацию типа, они считаются различными.
Два типа ссылок являются одним типом на этапе выполнения, если:
-
Они оба являются типами классов или интерфейсов, определены одним и тем же загрузчиком классов и имеют одинаковое двоичное имя (§13.1), в этом случае их иногда называют одним классом на этапе выполнения или одним интерфейсом на этапе выполнения.
-
Они оба являются типами массивов, и их компоненты — одинаковым типом на этапе выполнения (§10 (Массивы)).
Переменная типа — это неопределённый идентификатор, используемый в качестве типа в телах классов, интерфейсов, методов и конструкторов.
Переменная типа вводится объявлением параметра типа обобщённого класса, интерфейса, метода или конструктора (§8.1.2, §9.1.2, §8.4.4, §8.8.4).
Область видимости переменной типа, объявленной как параметр типа, указана в §6.3.
Каждая переменная типа, объявленная как параметр типа, имеет границу. Если для переменной типа не объявлена граница, Object предполагается. Если граница объявлена, она состоит либо из:
-
единственной переменной типа T, или
-
типа класса или интерфейса T, за которым могут следовать типы интерфейсов I1
&...&In.
Если какой-либо из типов I1, ..., In является типом класса или переменной типа, возникает ошибка компиляции.
Стирания (§4.6) всех составляющих типов границы должны быть попарно различны, иначе произойдёт ошибка компиляции.
Переменная типа не может одновременно являться подтипом двух типов интерфейсов, которые являются различными параметризациями одного и того же обобщённого интерфейса, иначе произойдёт ошибка компиляции.
Порядок типов в границе существенен только в том, что стирание переменной типа определяется первым типом в её границе, и что тип класса или переменной типа может появляться только на первом месте.
Членами переменной типа X с границей T & I1 & ... & In являются членами пересечения типов (§4.9) T & I1 & ... & In, появляющимися в точке объявления переменной типа.
Пример 4.4-1. Члены переменной типа
package TypeVarMembers;
class C {
public void mCPublic() {}
protected void mCProtected() {}
void mCPackage() {}
private void mCPrivate() {}
}
interface I {
void mI();
}
class CT extends C implements I {
public void mI() {}
}
class Test {
<T extends C & I> void test(T t) {
t.mI(); // OK
t.mCPublic(); // OK
t.mCProtected(); // OK
t.mCPackage(); // OK
t.mCPrivate(); // Compile-time error
}
}
Переменная типа T имеет те же члены, что и пересечение типов C & I, которое, в свою очередь, имеет те же члены, что и пустой класс CT, определённый в той же области видимости с эквивалентными надтипами. Члены интерфейса всегда public, и поэтому всегда наследуются (если не переопределены). Таким образом, mI является членом CT и T. Среди членов C все, кроме mCPrivate, наследуются CT и, следовательно, являются членами как CT, так и T.
Если C был объявлен в другом пакете, чем T, то вызов mCPackage привёл бы к ошибке компиляции, поскольку этот член не был бы доступен в точке объявления T.
Класс или интерфейс, который является обобщённым (§8.1.2, §9.1.2), определяет набор параметризованных типов.
Параметризованный тип — это тип класса или интерфейса вида C<T1,...,Tn>, где C — имя обобщённого класса или интерфейса, а <T1,...,Tn> — список аргументов типа, обозначающий конкретную параметризацию обобщённого класса или интерфейса.
Обобщённый класс или интерфейс имеет параметры типа F1,...,Fn с соответствующими границами B1,...,Bn. Каждый аргумент типа Ti параметризованного типа принимает все типы, которые являются подтипами всех типов, указанных в соответствующей границе. То есть, для каждого типа границы S в Bi, Ti является подтипом S[F1:=T1,...,Fn:=Tn] (§4.10).
Параметризованный тип C<T1,...,Tn> является корректным, если выполняются следующие условия:
-
C — имя обобщённого класса или интерфейса.
-
Количество аргументов типа совпадает с количеством параметров типа в обобщённом объявлении C.
-
При применении преобразования захвата (§5.1.10), приводящего к типу C
<X1,...,Xn>, каждый аргумент типа Xi является подтипом S[F1:=X1,...,Fn:=Xn]для каждого типа границы S в Bi.
Ошибка компиляции возникает, если параметризованный тип некорректен.
В данном спецификации, когда мы говорим о типе класса или интерфейса, мы также включаем параметризованные типы, если не указано обратное.
Два параметризованных типа являются доказательно различными, если выполняется одно из следующих условий:
-
Они являются параметризациями различных объявлений обобщённых типов.
-
Любой из их аргументов типа является доказательно различным.
Взяв обобщённые классы из примеров §8.1.2, вот некоторые корректные параметризованные типы:
-
Seq<String> -
Seq<Seq<String>> -
Seq<String>.Zipper<Integer> -
Pair<String,Integer>
Вот некоторые неправильные параметризации этих обобщённых классов:
-
Seq<int>некорректно, так как примитивные типы не могут быть аргументами типа. -
Pair<String>некорректно, так как недостаточно аргументов типа. -
Pair<String,String,String>некорректно, так как слишком много аргументов типа.
Параметризованный тип может быть параметризацией вложенного обобщённого класса или интерфейса. Например, если необобщённый класс C имеет обобщённый вложенный класс D с одним параметром типа, тогда C.D<Object> является параметризованным типом. В то же время, если обобщённый класс C с одним параметром типа имеет необобщённый вложенный класс D, то тип вложенного класса C<String>.D является параметризованным типом, даже если класс D не является обобщённым.
Аргументы типов могут быть либо типами ссылок, либо универсальными знаками. Универсальные знаки полезны в ситуациях, когда требуется лишь частичное знание о параметре типа.
Универсальным знакам можно задать явные границы, подобно обычным объявлениям переменных типа. Верхняя граница обозначается следующим синтаксисом, где B — граница:
? extends B
В отличие от обычных переменных типа, объявленных в сигнатуре метода, при использовании универсального знака не требуется никакого вывода типа. Поэтому разрешается объявлять нижние границы для универсального знака, используя следующий синтаксис, где B — нижняя граница:
? super B
Универсальный знак ? extends Object эквивалентен неограниченному универсальному знаку ?.
Два аргумента типа доказуемо различны, если выполняется одно из следующих условий:
-
Ни один из аргументов не является переменной типа или универсальным знаком, и два аргумента не являются одним и тем же типом.
-
Один аргумент типа — переменная типа или универсальный знак с границей (если переменная типа) или верхней границей (если универсальный знак, используя преобразование захвата (§5.1.10), если необходимо) типа S; а другой аргумент типа T не является переменной типа или универсальным знаком; и ни |S|
<:|T| ни |T|<:|S| (§4.8, §4.10). -
Каждый аргумент типа — переменная типа или универсальный знак с верхними границами (из преобразования захвата, если необходимо) S и T; и ни |S|
<:|T| ни |T|<:|S|.
Говорят, что аргумент типа T1 содержит другой аргумент типа T2, записанный T2 <= T1, если множество типов, обозначаемых T2, доказуемо является подмножеством множества типов, обозначаемых T1, в соответствии с рефлексивным и транзитивным замыканием следующих правил (где <: обозначает суботипирование (§4.10)):
-
?extendsT<=?extendsS, если T<:S -
?extendsT<=? -
?superT<=?superS, если S<:T -
?superT<=? -
?superT<=?extendsObject -
T
<=T -
T
<=?extendsT -
T
<=?superT
Взаимосвязь универсальных знаков со сложившейся теорией типов интересна, что мы здесь кратко затронем. Универсальные знаки — это ограниченная форма экзистенциальных типов. Учитывая общее объявление типа G<T extends B>, G<?> примерно аналогично Some X <: B. G<X>.
Исторически универсальные знаки являются прямым потомком работ Ацуси Игараси и Мирко Вироли. Читателям, заинтересованным в более подробном обсуждении, следует обратиться к работе On Variance-Based Subtyping for Parametric Types Ацуси Игараси и Мирко Вироли, в Труды 16-й европейской конференции по объектно-ориентированному программированию (ECOOP 2002). Эта работа сама по себе опирается на более ранние работы Крстен Торупа и Мадса Торгенсена (Unifying Genericity, ECOOP 99), а также на богатую традицию работ по объявлению основанной на вариации, восходящей к работе Пьера Америки над POOL (OOPSLA 89).
Универсальные знаки отличаются некоторыми деталями от конструкций, описанных в упомянутой статье, в частности, в использовании преобразования захвата (§5.1.10) вместо операции close, описанной Игараси и Вироли. Более формальное описание универсальных знаков см. в работе Wild FJ Мадса Торгенсена, Эрика Эрнста и Кристиана Плеснера Хансена в 12-м семинаре по основам объектно-ориентированного программирования (FOOL 2005).
Пример 4.5.1-1. Неограниченные универсальные знаки
import java.util.ArrayList;
import java.util.Collection;
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>— надтип (§4.10.2) 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), элемент которого является параметризованным типом. Такой тип называется необработанным типом.
Более точно, необработанный тип определяется как один из:
-
Ссылочный тип, образованный путем взятия имени объявления обобщенного класса или интерфейса без сопровождающего списка аргументов типа.
-
Тип массива, элемент которого является необработанным типом.
-
Имя внутреннего вложенного класса члена необработанного типа R, который не унаследован от суперкласса или суперинтерфейса R.
Тип необобщенного класса или интерфейса не является необработанным типом.
Чтобы понять, почему имя внутреннего вложенного класса члена необработанного типа считается необработанным, рассмотрим следующий пример:
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 необработанного типа C, которое не унаследовано от своих суперклассов или суперинтерфейсов, является стиранием его типа в обобщенном классе или интерфейсе C.
Тип унаследованного метода экземпляра или поля не-static необработанного типа C, где член был объявлен в классе или интерфейсе D, является типом члена в супертипе C, который называет D.
Тип статического метода или статического поля static необработанного типа C совпадает с его типом в обобщенном классе или интерфейсе C.
Это ошибка времени компиляции, если передать аргументы типа в не-static вложенный класс или интерфейс необработанного типа, который не унаследован от своих суперклассов или суперинтерфейсов.
Это ошибка времени компиляции, если попытаться использовать вложенный класс или интерфейс параметризованного типа как необработанный тип.
Это означает, что запрет на «редкие» типы распространяется на случай, когда квалифицирующий тип параметризован, но мы пытаемся использовать внутренний класс как необработанный тип:
Outer<Integer>.Inner x = null; // illegal
Это противоположность случаю, обсуждаемому выше. Для этого полуфабрикатного типа нет практического оправдания. В устаревшем коде не используются аргументы типа. В современном коде мы должны правильно использовать обобщенные типы и передавать все необходимые аргументы типа.
Использование необработанных типов разрешается только для обеспечения совместимости со старым кодом. Использование необработанных типов в коде, написанном после введения обобщений в язык программирования Java, крайне не рекомендуется. Возможно, в будущих версиях языка программирования Java использование необработанных типов будет запрещено.
Для обеспечения того, чтобы все потенциальные нарушения правил типизации всегда отмечались, некоторые обращения к членам необработанного типа приведут к предупреждениям времени компиляции. Правила для предупреждений времени компиляции при доступе к членам или конструкторам необработанных типов следующие:
-
При присваивании полю: если тип Основного в выражении доступа к полю (§15.11) является необработанным типом, то происходит предупреждение времени компиляции, если стирание изменяет тип поля.
-
При вызове метода или конструктора: если тип класса или интерфейса для поиска (§15.12.1) является необработанным типом, то происходит предупреждение времени компиляции, если стирание изменяет любой из типов формальных параметров метода или конструктора.
-
Предупреждение времени компиляции не возникает для вызова метода, когда типы формальных параметров не изменяются при стирании (даже если изменяется тип возвращаемого значения и/или
throws), для чтения из поля или для создания экземпляра класса необработанного типа.
Обратите внимание, что вышеуказанные предупреждения отличаются от предупреждений, возможных при сужении ссылочного преобразования (§5.1.6), неявного преобразования (§5.1.9), объявлений методов (§8.4.1, §8.4.8.3) и некоторых выражений (§15.12.4.2, §15.13.2, §15.27.3).
Здесь рассматривается случай, когда устаревший пользовательский код использует обобщенную библиотеку. Например, библиотека объявляет обобщенный класс Foo<T extends String>, который имеет поле f типа Vector<T>, но пользовательский код присваивает вектор целых чисел полю e.f, где e имеет необработанный тип Foo. Пользовательский код получает предупреждение, так как он может вызвать утечку памяти (§4.12.2) для обобщенных потребителей обобщенной библиотеки.
(Обратите внимание, что устаревший потребитель может присвоить Vector<String> из библиотеки своей переменной Vector без получения предупреждения. То есть, правила подтипизации (§4.10.2) языка программирования Java позволяют присвоить переменной необработанного типа значение любого параметризованного экземпляра типа.)
Предупреждения по неявным преобразованиям охватывают двойной случай, когда обобщенный потребитель использует устаревшую библиотеку. Например, метод библиотеки имеет необработанный возвращаемый тип Vector, но потребитель присваивает результат вызова метода переменной типа Vector<String>. Это небезопасно, так как у необработанного вектора может быть другой тип элемента, чем String, но это все же разрешено с помощью неявного преобразования, чтобы обеспечить взаимодействие со старым кодом. Предупреждение по неявным преобразованиям указывает, что обобщенный потребитель может столкнуться с проблемами утечки памяти в других частях программы.
Пример 4.8-1. Необработанные типы
class Cell<E> {
E value;
Cell(E v) { value = v; }
E get() { return value; }
void set(E v) { value = v; }
public static void main(String[] args) {
Cell x = new Cell<String>("abc");
System.out.println(x.value); // OK, has type Object
System.out.println(x.get()); // OK, has type Object
x.set("def"); // unchecked warning
}
}
Пример 4.8-2. Необработанные типы и наследование
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
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>.
Следующие правила определяют отношение прямого надтипа среди примитивных типов:
-
double>1float -
float>1long -
long>1int -
int>1char -
int>1short -
short>1byte
Для негенерического класса или интерфейса C, прямые надтипы типа C — это все следующее:
Для генерического класса или интерфейса C с параметрами типа F1,...,Fn (n > 0), прямые надтипы исходного типа C (§4.8) — это все следующее:
-
Стирание (§4.6) прямого надкласса C, если C является классом.
-
Стирание прямых надинтерфейсов C.
-
Тип
Object, если C является интерфейсом без прямых надинтерфейсов.
Для генерического класса или интерфейса C с параметрами типа F1,...,Fn (n > 0), прямые надтипы параметризованного типа C<T1,...,Tn>, где каждый из Ti (1 ≤ i ≤ n) является типом, — это все следующее:
-
Подстановка
[F1:=T1,...,Fn:=Tn], применённая к прямому надклассу C, если C является классом. -
Подстановка
[F1:=T1,...,Fn:=Tn], применённая к прямым надинтерфейсам C. -
C
<S1,...,Sn>, где Si содержит Ti (1 ≤ i ≤ n) (§4.5.1). -
Тип
Object, если C является интерфейсом без прямых надинтерфейсов. -
Исходный тип C.
Для генерического класса или интерфейса C с параметрами типа F1,...,Fn (n > 0), прямые надтипы параметризованного типа C<R1,...,Rn>, где по крайней мере один из Ri (1 ≤ i ≤ n) является подставленным типом-аргументом, являются прямыми надтипами параметризованного типа C<X1,...,Xn>, который является результатом применения преобразования захвата к C<R1,...,Rn> (§5.1.10).
Прямые надтипы пересечения типов T1 & ... & Tn — это Ti (1 ≤ i ≤ n).
Прямые надтипы переменной типа — это типы, перечисленные в её ограничении.
Переменная типа является прямым надтипом её нижней границы.
Прямые надтипы типа null — это все ссылочные типы, кроме самого типа null.
Следующие правила определяют отношение прямого наследования среди типов массивов:
-
Если S и T — ссылочные типы, то S
[]>1T[]тогда и только тогда, когда S>1T. -
Object>1Object[] -
Cloneable>1Object[] -
java.io.Serializable>1Object[] -
Если P — примитивный тип, то:
-
Object>1P[] -
Cloneable>1P[] -
java.io.Serializable>1P[]
-
Наименьшая верхняя граница (lub) набора ссылочных типов — это общий супертип, который более специфичен, чем любой другой общий супертип (то есть, никакой другой общий супертип не является подтипом наименьшей верхней границы). Этот тип, lub(U1, ..., Uk), определяется следующим образом.
Если k = 1, то lub — это сам тип: lub(U) = U.
В противном случае:
-
Для каждого Ui (1 ≤ i ≤ k):
Пусть ST(Ui) — множество супертипов Ui.
Пусть EST(Ui), множество стёртых супертипов Ui, равно:
EST(Ui) = { |W| | W в ST(Ui) }, где |W| — стёртый вид W.
Причина вычисления множества стёртых супертипов — в обработке ситуаций, когда множество типов включает несколько различных параметризаций обобщённого типа.
Например, заданы
List<String>иList<Object>, простое пересечение множеств ST(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 в EC и для всех W ≠ V в EC неверно, что W
<:V }Так как мы стремимся вывести более точные типы, мы хотим отфильтровать любые кандидаты, являющиеся супертипами других кандидатов. Именно это достигается вычислением MEC. В нашем примере было EC = {
List,Collection,Object}, поэтому MEC = {List}. Следующим шагом является восстановление аргументов типов для стёртых типов в MEC. -
Для любого элемента G из MEC, являющегося обобщённым типом:
Пусть «релевантные» параметризации G, Relevant(G), равны:
Relevant(G) = { V | 1 ≤ i ≤ k: V в 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) в 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) - ... (остальные пункты переведены аналогичным образом)
и где glb() определяется в §5.1.10.
-
- ... (остальные пункты переведены аналогичным образом)
Строго говоря, эта функция lub() только аппроксимирует наименьшую верхнюю границу. Формально, может существовать некоторый другой тип T такой, что все U1, ..., Uk являются подтипами T, а T является подтипом lub(U1, ..., Uk). Однако компилятор для языка программирования Java должен реализовывать lub() так, как указано выше.
Возможно, что функция lub() выдает бесконечный тип. Это допустимо, и компилятор для языка программирования Java должен распознавать такие ситуации и представлять их соответствующим образом, используя циклические структуры данных.
Возможность бесконечного типа вытекает из рекурсивных вызовов lub(). Читатели, знакомые с рекурсивными типами, должны отметить, что бесконечный тип — это не то же самое, что рекурсивный тип.
Синтетическая переменная типа — это переменная типа, введенная компилятором во время преобразования захвата (§5.1.10) или разрешения переменной вывода (§18.4).
Иногда необходимо найти близкий супертип типа, где этот супертип не упоминает определенные синтетические переменные типа. Это достигается с помощью восходящей проекции, примененной к типу.
Аналогичным образом, нисходящая проекция может быть применена для поиска близкого подтипа типа, где этот подтип не упоминает определенные синтетические переменные типа. Поскольку такой тип существует не всегда, нисходящая проекция является частичной функцией.
Эти операции принимают в качестве входных данных набор переменных типа, на которые больше не следует ссылаться, которые называются ограниченными переменными типа. Когда операции рекурсивно применяются, набор ограниченных переменных типа неявно передается рекурсивному приложению.
Восходящая проекция типа T относительно набора ограниченных переменных типа определяется следующим образом:
-
Если T не упоминает ни одной ограниченной переменной типа, то результатом будет T.
-
Если T — ограниченная переменная типа, то результатом является восходящая проекция верхней границы T.
-
Если T — параметризованный тип класса или параметризованный тип интерфейса, G
<A1,...,An>, то результатом будет G<A1',...,An'>, где для 1 ≤ i ≤ n, Ai' выводится из Ai следующим образом:-
Если Ai не упоминает ни одной ограниченной переменной типа, то Ai' = Ai.
-
Если Ai — тип, упоминающий ограниченную переменную типа, то пусть U будет восходящей проекцией Ai. Ai' — это подстановочный знак, определяемый тремя случаями:
-
Если U не является
Object, и если объявленная граница i-го параметра G, Bi, упоминает параметр типа G или Bi не является подтипом U, то Ai' — это подстановочный знак с верхней границей,?extendsU. -
В противном случае, если нисходящая проекция Ai равна
L, то Ai' — это подстановочный знак с нижней границей,?superL. -
В противном случае нисходящая проекция Ai не определена, и Ai' — это неограниченный подстановочный знак,
?.
-
-
Если Ai — подстановочный знак с верхней границей, который упоминает ограниченную переменную типа, то пусть U будет восходящей проекцией границы подстановочного знака. Ai' — это подстановочный знак с верхней границей,
?extendsU. -
Если Ai — подстановочный знак с нижней границей, который упоминает ограниченную переменную типа, то если нисходящая проекция границы подстановочного знака равна
L, то Ai' — это подстановочный знак с нижней границей,?superL; если нисходящая проекция границы подстановочного знака не определена, то Ai' — это неограниченный подстановочный знак,?.
-
-
Если T — тип массива, S
[], то результатом является тип массива, компонентом которого является восходящая проекция S. -
Если T — тип пересечения, то результатом является тип пересечения. Для каждого элемента, S, из T, результат имеет в качестве элемента восходящую проекцию S.
Нисходящая проекция типа T относительно набора ограниченных переменных типа является частичной функцией, определенной следующим образом:
-
Если T не упоминает ни одной ограниченной переменной типа, то результатом будет T.
-
Если T — ограниченная переменная типа, то если T имеет нижнюю границу, и если нисходящая проекция этой границы равна
L, то результатом будетL; если T не имеет нижней границы, или если нисходящая проекция этой границы не определена, то результат не определен. -
Если T — параметризованный тип класса или параметризованный тип интерфейса, G
<A1,...,An>, то результатом будет G<A1',...,An'>, если для 1 ≤ i ≤ n аргумент типа Ai' может быть выведен из Ai следующим образом; если это не так, то результат не определен:-
Если Ai не упоминает ограниченную переменную типа, то Ai' = Ai.
-
Если Ai — тип, который упоминает ограниченную переменную типа, то Ai' не определена.
-
Если Ai — подстановочный знак с верхней границей, который упоминает ограниченную переменную типа, то если нисходящая проекция границы подстановочного знака равна U, то Ai' — это подстановочный знак с верхней границей,
?extendsU; если нисходящая проекция границы подстановочного знака не определена, то Ai' не определена. -
Если Ai — подстановочный знак с нижней границей, который упоминает ограниченную переменную типа, то пусть
Lбудет восходящей проекцией границы подстановочного знака. Ai' — это подстановочный знак с нижней границей,?superL.
-
-
Если T — тип массива, S
[], то если нисходящая проекция S равна S', результатом будет S'[]; если нисходящая проекция S не определена, то результат не определен. -
Если T — тип пересечения, то если нисходящая проекция определена для каждого элемента T, результатом является тип пересечения, элементами которого являются нисходящие проекции элементов T; если нисходящая проекция не определена для любого элемента T, то результат не определен.
Как и lub (§4.10.4), восходящая и нисходящая проекции могут создавать бесконечные типы из-за рекурсии на границах переменных типа.
Типы используются в большинстве типов объявлений и в некоторых типах выражений. Конкретно, существует 17 контекстов типов, где используются типы:
-
В объявлениях:
-
Тип в
extendsилиimplementsразделе объявления класса (§8.1.4, §8.1.5) -
Тип в
extendsразделе объявления интерфейса (§9.1.3) -
Тип возвращаемого значения метода (§8.4.5, §9.4), включая тип элемента аннотационного интерфейса (§9.6.1)
-
Тип в
throwsразделе метода или конструктора (§8.4.6, §8.8.5, §9.4) -
Тип в
extendsразделе объявления параметра типа дженерического класса, интерфейса, метода или конструктора (§8.1.2, §9.1.2, §8.4.4, §8.8.4) -
Тип в объявлении поля класса или интерфейса (§8.3, §9.3), включая константу перечисления (§8.9.1)
-
Тип в объявлении формального параметра метода, конструктора или лямбда-выражения (§8.4.1, §8.8.1, §9.4, §15.27.1)
-
Тип параметра получателя метода (§8.4)
-
Тип в объявлении локальной переменной в операторе (§14.4.2, §14.14.1, §14.14.2, §14.20.3) или шаблоне (§14.30.1)
-
Тип в объявлении параметра исключения (§14.20)
-
Тип в объявлении компонента записи класса записи (§8.10.1)
-
-
В выражениях:
-
Тип в списке явных аргументов типа явного вызова конструктора, выражении создания экземпляра класса, выражении вызова метода или выражении ссылки на метод (§8.8.7.1, §15.9, §15.12, §15.13)
-
В выражении создания экземпляра класса без указания класса, как тип создаваемого класса (§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, для типов не-дженерических классов, интерфейсов и переменных типа
-
§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.ArrayList;
import java.util.Collection;
import java.util.Random;
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); }
}
В этом примере типы используются в объявлениях следующего:
-
Поля, которые являются переменными класса и экземпляра класса (§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 может содержать null-ссылку или ссылку на экземпляр класса T или любого класса, являющегося подклассом T.
Переменная типа интерфейса может содержать null-ссылку или ссылку на любой экземпляр любого класса, реализующего интерфейс.
Обратите внимание, что переменная не гарантирует, что всегда ссылается на подтип своего объявленного типа, а только на подклассы или подинтерфейсы объявленного типа. Это связано с возможностью загрязнения кучи, обсуждаемой ниже.
Если T — примитивный тип, то переменная типа "массив T" может содержать null-ссылку или ссылку на любой массив типа "массив T".
Если T — ссылочный тип, то переменная типа "массив T" может содержать null-ссылку или ссылку на любой массив типа "массив S", где тип S является подклассом или подинтерфейсом типа T.
Переменная типа Object[] может содержать ссылку на массив любого ссылочного типа.
Переменная типа Object может содержать null-ссылку или ссылку на любой объект, является ли он экземпляром класса или массивом.
Возможна ситуация, когда переменная параметризованного типа будет ссылаться на объект, который не является объектом этого параметризованного типа. Такая ситуация известна как загрязнение кучи.
Загрязнение кучи может произойти только если программа выполнила какую-либо операцию, связанную с сырым типом, которая бы вызвала предупреждение времени компиляции, не проверяемое компилятором (§4.8, §5.1.6, §5.1.9, §8.4.1, §8.4.8.3, §8.4.8.4, §9.4.1.2, §15.12.4.2), или если программа использует псевдоним переменной массива с нереализуемым типом элемента через переменную массива супертипа, который является сырым или не генерическим.
Например, код:
List l = new ArrayList<Number>(); List<String> ls = l; // Unchecked warning
вызывает предупреждение времени компиляции, так как невозможно определить, либо во время компиляции (в рамках правил проверки типа времени компиляции), либо во время выполнения, ссылается ли переменная l на действительный List<String>.
Если код выше выполняется, возникает загрязнение кучи, так как переменная ls, объявленная как List<String>, ссылается на значение, которое на самом деле не является List<String>.
Проблема не может быть обнаружена во время выполнения, потому что переменные типа не являются реализованными, а это означает, что экземпляры не содержат никакой информации во время выполнения о типах аргументов, используемых для их создания.
В простом примере, как показано выше, может показаться, что легко обнаружить ситуацию во время компиляции и выдать ошибку. Однако в общем (и типичном) случае, значение переменной l может быть результатом вызова метода, скомпилированного отдельно, или его значение может зависеть от произвольного потока управления. Приведённый выше код, следовательно, очень нетипичен и очень плох по стилю.
Кроме того, тот факт, что Object[] является супертипом всех типов массивов, означает, что может произойти небезопасное алиасирование, что приводит к загрязнению кучи. Например, следующий код компилируется, потому что он статически правильно типизирован:
static void m(List<String>... stringLists) {
Object[] array = stringLists;
List<Integer> tmpList = Arrays.asList(42);
array[0] = tmpList; // (1)
String s = stringLists[0].get(0); // (2)
}
Загрязнение кучи происходит в (1), потому что компонент в массиве stringLists, который должен ссылаться на List<String>, теперь ссылается на List<Integer>. Нет способа обнаружить это загрязнение в присутствии как универсального супертипа (Object[]), так и нереализуемого типа (объявленный тип формального параметра, List<String>[]). Предупреждение о не проверенной компиляцией не обосновано в (1); тем не менее, во время выполнения произойдёт ClassCastException в (2).
Предупреждение о не проверенной компиляцией будет выдаваться при любом вызове метода выше, потому что вызов рассматривается статической системой типов языка программирования Java как создание массива, тип элемента которого, List<String>, не реализуем (§15.12.4.2). Только если тело метода было безопасно с точки зрения переменной аргумента переменной, то программист мог бы использовать аннотацию SafeVarargs для отключения предупреждений при вызовах (§9.6.4.7). Поскольку тело метода, как написано выше, вызывает загрязнение кучи, было бы совершенно неразумно использовать аннотацию для отключения предупреждений для вызывающих сторон.
Наконец, обратите внимание, что массив stringLists может быть связан через переменные других типов, кроме Object[], и может произойти загрязнение кучи. Например, тип переменной array может быть java.util.Collection[] — сырой тип элемента — и тело метода выше будет компилироваться без предупреждений или ошибок и всё ещё вызывать загрязнение кучи. И если платформа Java SE определит, скажем, Sequence как негенерический супертип List<T>, то использование Sequence в качестве типа для array также вызовет загрязнение кучи.
Переменная всегда будет ссылаться на объект, который является экземпляром класса, представляющего параметризованный тип.
Значение ls в приведённом выше примере всегда является экземпляром класса, который предоставляет представление List.
Присваивание из выражения сырого типа переменной параметризованного типа следует использовать только при объединении устаревшего кода, который не использует параметризованные типы, с более современным кодом, который их использует.
Если никакая операция, требующая выдачи предупреждения времени компиляции, не проверяемого компилятором, не выполняется, и не происходит небезопасного алиасирования переменных массивов с нереализуемыми типами элементов, то загрязнения кучи не произойдёт. Обратите внимание, что это не означает, что загрязнение кучи происходит только в том случае, если действительно возникло предупреждение времени компиляции, не проверяемое компилятором. Возможно выполнение программы, где некоторые двоичные файлы были созданы компилятором более старой версии языка программирования Java или из источников, которые явно подавляли предупреждения о не проверяемых компиляцией. Данная практика в лучшем случае вредна.
Наоборот, возможно, что, несмотря на выполнение кода, который мог (и, возможно, сделал) вызвать предупреждение времени компиляции, не проверяемое компилятором, не происходит загрязнения кучи. Действительно, хороший стиль программирования требует, чтобы программист убедилась, что, несмотря на любое предупреждение, код корректен и загрязнение кучи не произойдёт.
Существует восемь видов переменных:
-
Переменная класса — это поле, объявленное с помощью ключевого слова
staticвнутри объявления класса (§8.3.1.1), или с ключевым словомstaticили без него внутри объявления интерфейса (§9.3).Переменная класса создаётся, когда её класс или интерфейс подготавливаются (§12.3.2) и инициализируется значением по умолчанию (§4.12.5). Переменная класса фактически перестаёт существовать, когда её класс или интерфейс загружаются (§12.7).
-
Переменная экземпляра — это поле, объявленное внутри объявления класса без использования ключевого слова
static(§8.3.1.1).Если у класса T есть поле
a, являющееся переменной экземпляра, то новая переменная экземпляраaсоздаётся и инициализируется значением по умолчанию (§4.12.5) как часть каждого вновь созданного объекта класса T или любого класса, являющегося подклассом T (§8.1.4). Переменная экземпляра фактически перестаёт существовать, когда объект, к которому она относится, больше не ссылается на неё после завершения всех необходимых финализаций объекта (§12.6). -
Элементы массива — это безымянные переменные, которые создаются и инициализируются значениями по умолчанию (§4.12.5) всякий раз, когда создаётся новый объект-массив (§10 (Массивы), §15.10.2). Элементы массива фактически перестают существовать, когда массив больше не ссылается.
-
Параметры метода (§8.4.1) — имена значений аргументов, передаваемых методу.
Для каждого параметра, объявленного в объявлении метода, новая переменная параметра создаётся каждый раз, когда вызывается этот метод (§15.12). Новая переменная инициализируется соответствующим значением аргумента из вызова метода. Параметр метода фактически перестаёт существовать, когда завершается выполнение тела метода.
-
Параметры конструктора (§8.8.1) — имена значений аргументов, передаваемых конструктору.
Для каждого параметра, объявленного в объявлении конструктора, новая переменная параметра создаётся каждый раз, когда выражение создания экземпляра класса (§15.9) или явный вызов конструктора (§8.8.7) вызывает этот конструктор. Новая переменная инициализируется соответствующим значением аргумента из выражения создания или вызова конструктора. Параметр конструктора фактически перестаёт существовать, когда завершается выполнение тела конструктора.
-
Параметры лямбда-выражения (§15.27.1) — имена значений аргументов, передаваемых в тело лямбда-выражения (§15.27.2).
Для каждого параметра, объявленного в лямбда-выражении, новая переменная параметра создаётся каждый раз, когда вызывается метод, реализованный телом лямбда-выражения (§15.12). Новая переменная инициализируется соответствующим значением аргумента из вызова метода. Параметр лямбда-выражения фактически перестаёт существовать, когда завершается выполнение тела лямбда-выражения.
-
Переменная параметра исключения создаётся каждый раз, когда исключение перехватывается блоком
catchоператораtry(§14.20).Новая переменная инициализируется фактическим объектом, связанным с исключением (§11.3, §14.18). Переменная параметра исключения фактически перестаёт существовать, когда завершается выполнение блока, связанного с блоком
catch. -
Локальные переменные (§14.4) объявляются операторами (§14.4.2, §14.14.1, §14.14.2, §14.20.3) и шаблонами (§14.30). Локальная переменная, объявленная с помощью шаблона, называется переменной шаблона.
Локальная переменная, объявленная оператором, создаётся, когда поток управления входит в ближайший окружающий блок (§14.2), оператор
forили операторtry-with-resources.Локальная переменная, объявленная оператором, инициализируется в ходе выполнения оператора, если у декларатора переменной есть инициализатор. Правила определённой инициализации (§16 (Определённая инициализация)) предотвращают использование значения локальной переменной, объявленной оператором, до момента её инициализации или присвоения значения.
Локальная переменная, объявленная с помощью шаблона, создаётся и инициализируется при соответствии шаблона (§14.30.2). Правила области видимости (§6.3) предотвращают использование значения локальной переменной, объявленной с помощью шаблона, пока шаблон не соотнесён.
Локальная переменная перестаёт существовать, когда её объявление больше не находится в области видимости.
Если бы не было одной исключительной ситуации, локальную переменную, объявленную оператором, всегда можно рассматривать как созданную при выполнении оператора. Исключение связано с оператором
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;
}
boolean equalAtX(Object o) {
if (o instanceof Point p) // p is a pattern variable
return this.x == p.x;
else
return false;
}
}
Переменная может быть объявлена final. Переменной типа final можно присвоить значение только один раз. Если переменной final присваивается значение, это ошибка времени компиляции, если до присваивания переменная не была явно инициализирована (§16 (Явное присваивание)).
После присваивания значения переменной final, она всегда содержит одно и то же значение. Если переменная final содержит ссылку на объект, состояние объекта может быть изменено операциями над объектом, но переменная всегда будет ссылаться на тот же объект. Это также относится к массивам, так как массивы являются объектами; если переменная final содержит ссылку на массив, компоненты массива могут быть изменены операциями над массивом, но переменная всегда будет ссылаться на тот же массив.
Пустая переменная — это переменная final, объявление которой не содержит инициализатора.
Константная переменная — это final переменная примитивного типа или типа String, которая инициализируется константным выражением (§15.29). Является ли переменная константной или нет, может иметь последствия для инициализации класса (§12.4.1), бинарной совместимости (§13.1), достижимости (§14.22) и явного присваивания (§16.1.1).
Три вида переменных неявно объявляются final: поле интерфейса (§9.3), локальная переменная, объявленная как ресурс в операторе try-с-ресурсами (§14.20.3) и параметр исключения в блоке многократного catch (§14.20). Параметр исключения в блоке однократного catch никогда неявно не объявляется 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, вместо этого считаются эффективно конечными:
-
Локальная переменная, объявленная оператором, и у которой декларатор имеет инициализатор (§14.4), или локальная переменная, объявленная с шаблоном (§14.30.1), является эффективно конечной, если все перечисленные ниже условия выполняются:
-
Она не объявлена
final. -
Она никогда не используется в качестве левой части в выражении присваивания (§15.26). (Обратите внимание, что декларатор локальной переменной, содержащий инициализатор, не является выражением присваивания.)
-
Она никогда не используется в качестве операнда префиксного или постфиксного оператора инкремента или декремента (§15.14, §15.15).
-
-
Локальная переменная, объявленная оператором, и у которой декларатор не имеет инициализатора, является эффективно конечной, если все перечисленные ниже условия выполняются:
-
Она не объявлена
final. -
Всякий раз, когда она используется в качестве левой части в выражении присваивания, она не имеет явного присваивания и не имеет явного присваивания до присваивания; то есть, она не имеет явного присваивания и не имеет явного присваивания после правой части выражения присваивания (§16 (Явное присваивание)).
-
Она никогда не используется в качестве операнда префиксного или постфиксного оператора инкремента или декремента.
-
-
Параметр метода, конструктора, лямбда-выражения или параметр исключения (§8.4.1, §8.8.1, §9.4, §15.27.1, §14.20) рассматривается, для определения, является ли она эффективно конечной, как локальная переменная, декларатор которой имеет инициализатор.
Если переменная является эффективно конечной, добавление модификатора 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.2, §14.14.1, §14.14.2, §14.20.3) должна быть явно присвоена значение до её использования, либо через инициализацию (§14.4), либо через присваивание (§15.26), способом, проверяемым с помощью правил определённой инициализации (§16 (Определённая инициализация)).
Локальная переменная, объявленная шаблоном (§14.30.1), инициализируется неявно процессом сопоставления шаблонов (§14.30.2).
Пример 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 24 Edition для получения более подробной информации.
-
Переменные типов (§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совместим по присваиванию (§5.2) с типом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.