Spec-Zone.ru › Java Language Specification 17

Глава 4. Типы, значения и переменные

Содержание

4.1. Виды типов и значений
4.2. Примитивные типы и значения
4.2.1. Целочисленные типы и значения
4.2.2. Операции над целыми числами
4.2.3. Типы с плавающей точкой и значения
4.2.4. Операции с числами с плавающей точкой
4.2.5. Тип boolean и булевы значения
4.3. Ссылочные типы и значения
4.3.1. Объекты
4.3.2. Класс Object
4.3.3. Класс String
4.3.4. Когда ссылочные типы одинаковы
4.4. Переменные типов
4.5. Параметризованные типы
4.5.1. Аргументы типа параметризованных типов
4.5.2. Члены и конструкторы параметризованных типов
4.6. Стирание типов
4.7. Реализуемые типы
4.8. Необработанные типы
4.9. Пересекающиеся типы
4.10. Подтипизация
4.10.1. Подтипизация среди примитивных типов
4.10.2. Подтипизация среди типов классов и интерфейсов
4.10.3. Подтипизация среди типов массивов
4.10.4. Наименьшее общее надмножество
4.10.5. Проекции типов
4.11. Где используются типы
4.12. Переменные
4.12.1. Переменные примитивного типа
4.12.2. Переменные ссылочного типа
4.12.3. Виды переменных
4.12.4. final Переменные
4.12.5. Начальные значения переменных
4.12.6. Типы, классы и интерфейсы

Язык программирования 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).

4.1. Виды типов и значений

В языке программирования 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 — это просто специальный литерал, который может быть любого ссылочного типа.

4.2. Примитивные типы и значения

Примитивный тип определяется языком программирования Java и обозначается его служебным словом (§3.9):

ПримитивныйТип:
{Аннотация} ЧисловойТип
{Аннотация} boolean
ЧисловойТип:
ЦелочисленныйТип
ТипСПлавающейТочкой
ЦелочисленныйТип:
(один из)
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.

4.2.1. Целочисленные типы и значения

Значения целочисленных типов — это целые числа в следующих диапазонах:

  • Для byte, от -128 до 127 включительно

  • Для short, от -32768 до 32767 включительно

  • Для int, от -2147483648 до 2147483647 включительно

  • Для long, от -9223372036854775808 до 9223372036854775807 включительно

  • Для char, от '\u0000' до '\uffff' включительно, то есть от 0 до 65535

4.2.2. Операции с целыми числами

Язык программирования Java предоставляет ряд операторов, действующих над целочисленными значениями:

  • Операторы сравнения, которые приводят к значению типа boolean:

    • Операторы числового сравнения <, <=, > и >= (§15.20.1)

    • Операторы числовой равенства == и != (§15.21.1)

  • Числовые операторы, которые приводят к значению типа int или long:

    • Унарные операторы плюс и минус + и - (§15.15.3, §15.15.4)

    • Множительные операторы *, / и % (§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.


4.2.3. Типы и значения с плавающей точкой

Типы с плавающей точкой представляют собой float и double, которые концептуально связаны с 32-битными двоичными 32-битными и 64-битными двоичными 64-битными форматами чисел с плавающей точкой для значений и операций IEEE 754, как указано в стандарте IEEE 754 (§1.7).

В Java SE 15 и более поздних версиях язык программирования Java использует версию IEEE 754 2019 года. До Java SE 15 язык программирования Java использовал версию IEEE 754 1985 года, где двоичный 32-битный формат был известен как формат single, а двоичный 64-битный формат — как формат 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 — положительное целое число, меньшее 2N,

  • e — целое число от Emin = -(2K-1-2) до Emax = 2K-1-1 включительно, и

  • N и K — параметры, которые зависят от типа.

Некоторые значения могут быть представлены в этой форме более чем одним способом. Например, предположим, что значение v типа с плавающей точкой может быть представлено в этой форме с использованием определенных значений для s, m и e, а затем, если m было чётным, а e было меньше 2K-1, можно было бы разделить m на 2 и увеличить e на 1, чтобы получить второе представление для того же значения v.

Представление в этом виде называется нормализованным, если m ≥ 2N-1; в противном случае представление называется субнормальным. Если значение типа с плавающей точкой не может быть представлено таким образом, что m ≥ 2N-1, то это значение называется субнормальным значением, потому что его величина ниже величины наименьшего нормализованного значения.

Ограничения на параметры N и K (и на производные параметры Emin и Emax) для 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 для каждого из его двоичных 32-битных и двоичных 64-битных форматов с плавающей точкой. Однако платформа 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 — ложно. Другие операции могут различать положительный и отрицательный ноль; например, 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.

4.2.4. Операции с числами с плавающей точкой

Язык программирования Java предоставляет ряд операторов, которые действуют над значениями с плавающей точкой:

  • Операторы сравнения, которые приводят к значению типа boolean:

    • Операторы числового сравнения <, <=, > и >= (§15.20.1)

    • Числовые операторы равенства == и != (§15.21.1)

  • Числовые операторы, которые приводят к значению типа float или double:

    • Унарные операторы плюс и минус + и - (§15.15.3, §15.15.4)

    • Множительные операторы *, / и % (§15.17)

    • Аддитивные операторы + и - (§15.18.2)

    • Оператор инкремента ++, как префиксный (§15.15.1) и постфиксный (§15.14.2)

    • Оператор декремента --, как префиксный (§15.15.2) и постфиксный (§15.14.3)

  • Условный оператор ? : (§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.


4.2.5. Тип boolean и булевы значения

Тип 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"), а затем создаёт новую строку, являющуюся конкатенацией этих двух строк

Булевы выражения определяют порядок выполнения в различных операторах:

  • Оператор if (§14.9)

  • Оператор while (§14.12)

  • Оператор do (§14.13)

  • Оператор for (§14.14)

Выражение типа boolean также определяет, какое подвыражение вычисляется в условном операторе ? : (§15.25).

В операторах управления потоком и в качестве первого операнда условного оператора ? : могут использоваться только выражения типа boolean и Boolean.

Целое или дробное выражение x может быть преобразовано в значение типа boolean, следуя соглашению языка C, согласно которому любое ненулевое значение соответствует значению true, при помощи выражения x!=0.

Ссылка на объект obj может быть преобразована в значение типа boolean, следуя соглашению языка C, согласно которому любая ссылка, отличная от null, соответствует значению true, при помощи выражения obj!=null.

Значение типа boolean может быть преобразовано в строку при помощи преобразования в строку (§5.4).

Значение типа boolean может быть приведено к типу boolean, Boolean или Object (§5.5). Другие приведения для типа boolean не допускаются.

4.3. Типы и значения ссылок

Существует четыре вида типов ссылок: типы классов (§8.1), типы интерфейсов (§9.1), переменные типов (§4.4) и типы массивов (§10.1).

ReferenceType:
ClassOrInterfaceType
TypeVariable
ArrayType
ClassOrInterfaceType:
ClassType
InterfaceType
ClassType:
{Annotation} TypeIdentifier [TypeArguments]
PackageName . {Annotation} TypeIdentifier [TypeArguments]
ClassOrInterfaceType . {Annotation} TypeIdentifier [TypeArguments]
InterfaceType:
ClassType
TypeVariable:
{Annotation} TypeIdentifier
ArrayType:
PrimitiveType Dims
ClassOrInterfaceType Dims
TypeVariable Dims
Dims:
{Annotation} [ ] {{Annotation} [ ]}

Пример кода:

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), иначе происходит ошибка компиляции. Тип класса или интерфейса обозначает этот вложенный тип.

4.3.1. Объекты

Объект — это экземпляр класса или массив.

Значения ссылок (часто просто ссылки) — это указатели на эти объекты, а также специальная ссылка 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)

  • Оператор приведения типа (§5.5, §15.16)

  • Оператор конкатенации строк + (§15.18.1), который, когда получает операнд типа String и ссылку, преобразует ссылку в строку, вызвав метод toString ссылкуемого объекта (используя "null", если либо ссылка, либо результат toString является null-ссылкой), а затем создает новую строку, являющуюся конкатенацией двух строк

  • Оператор instanceof (§15.20.2)

  • Операторы равенства ссылок == и != (§15.21.3)

  • Условный оператор ? : (§15.25).

Может быть много ссылок на один и тот же объект. Большинство объектов имеют состояние, хранящееся в полях объектов, являющихся экземплярами классов, или в переменных, составляющих объект массива. Если две переменные содержат ссылки на один и тот же объект, состояние объекта может быть изменено с помощью одной ссылки на объект, а затем измененное состояние может быть просмотрено через другую ссылку.

Пример 4.3.1-2. Примитивный и ссылочный идентификаторы

class Value { int val; }

class Test {
    public static void main(String[] args) {
        int i1 = 3;
        int i2 = i1;
        i2 = 4;
        System.out.print("i1==" + i1);
        System.out.println(" but i2==" + i2);
        Value v1 = new Value();
        v1.val = 5;
        Value v2 = v1;
        v2.val = 6;
        System.out.print("v1.val==" + v1.val);
        System.out.println(" and v2.val==" + v2.val);
    }
}

Эта программа выводит:

i1==3 but i2==4
v1.val==6 and v2.val==6

потому что v1.val и v2.val ссылаются на одну и ту же переменную экземпляра (§4.12.3) в единственном объекте Value, созданном единственным выражением new, в то время как i1 и i2 — разные переменные.


Каждый объект связан с монитором (§17.1), который используется методами synchronized (§8.4.3) и оператором synchronized (§14.19), чтобы обеспечить контроль над одновременным доступом к состоянию несколькими потоками (§17 (Потоки и блокировки)).

4.3.2. Класс Object

Класс 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 возвращает строковое представление объекта.

4.3.3. Класс String

Экземпляры класса String представляют последовательности точек кода Юникода.

Объект String имеет постоянное (неизменяемое) значение.

Строковые литералы (§3.10.5) и текстовые блоки (§3.10.6) являются ссылками на экземпляры класса String.

Оператор конкатенации строк + (§15.18.1) неявно создаёт новый объект String, когда результат не является константным выражением (§15.29).

4.3.4. Когда типы ссылок совпадают

Два типа ссылок являются одинаковыми на этапе компиляции, если они объявлены в единицах компиляции, связанных с одним и тем же модулем (§7.3), и имеют одинаковое бинарное имя (§13.1), и их типы аргументов, если таковые имеются, идентичны, применяя это определение рекурсивно.

Когда два типа ссылок совпадают, их иногда называют одним и тем же классом или одним и тем же интерфейсом.

Во время выполнения несколько типов ссылок с одинаковым бинарным именем могут быть загружены одновременно различными загрузчиками классов. Эти типы могут или не могут представлять одно и то же объявление типа. Даже если два таких типа представляют одно и то же объявление типа, они считаются разными.

Два типа ссылок являются одинаковыми на этапе выполнения, если:

  • Они оба являются типами классов или интерфейсов, определены одним и тем же загрузчиком классов и имеют одинаковое бинарное имя (§13.1), в этом случае их иногда называют одним и тем же классом на этапе выполнения или одним и тем же интерфейсом на этапе выполнения.

  • Они оба являются типами массивов, а их компоненты являются одинаковыми типами на этапе выполнения (§10 (Массивы)).

4.4. Переменные типов

Переменная типа — это неопределённый идентификатор, используемый как тип в телах классов, интерфейсов, методов и конструкторов.

Переменная типа вводится объявлением параметра типа обобщённого класса, интерфейса, метода или конструктора (§8.1.2, §9.1.2, §8.4.4, §8.8.4).

TypeParameter:
{МодификаторПараметраТипа} ИдентификаторТипа [СвязьТипа]
TypeParameterModifier:
Аннотация
TypeBound:
extends ПеременнаяТипа
extends ТипКлассаИлиИнтерфейса {ДополнительнаяСвязь}
AdditionalBound:
& ТипИнтерфейса

Область видимости переменной типа, объявленной как параметр типа, указана в §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.


4.5. Параметризованные типы

Класс или интерфейс, являющийся обобщённым (§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 не обобщённый.

4.5.1. Аргументы типа параметризованных типов

Аргументы типа могут быть либо типами ссылок, либо дикими картами. Дикие карты полезны в ситуациях, когда требуется лишь частичное знание о параметре типа.

TypeArguments:
< Список аргументов типа >
TypeArgumentList:
Аргумент типа {, Аргумент типа}
TypeArgument:
Тип ссылки
Дикая карта
Wildcard:
{Аннотация} ? [Границы дикой карты]
WildcardBounds:
extends Тип ссылки
super Тип ссылки

Диким картам можно задать явные границы, точно так же, как и обычным переменным типа при объявлении. Верхняя граница обозначается следующим синтаксисом, где 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)):

  • ? extends T <= ? extends S если T <: S

  • ? extends T <= ?

  • ? super T <= ? super S если S <: T

  • ? super T <= ?

  • ? super T <= ? extends Object

  • T <= T

  • T <= ? extends T

  • T <= ? super T

Связь диких карт с существующей теорией типов интересна, и мы кратко коснёмся её здесь. Дикие карты — это ограниченная форма экзистенциальных типов. Учитывая общее объявление типа G<T extends B>, G<?> примерно аналогично Some X <: B. G<X>.

Исторически дикие карты — это прямой потомок работы Ацуси Игараси и Мирко Вироли. Читателям, заинтересованным в более подробном обсуждении, следует обратиться к работе On Variance-Based Subtyping for Parametric Types Ацуси Игараси и Мирко Вироли, в Трудах 16-й европейской конференции по объектно-ориентированному программированию (ECOOP 2002). Эта работа сама по себе основана на более ранних работах Крстен Торупа и Мадса Торгерсена (Объединение обобщения, ECOOP 99), а также на долгой традиции работ по объявлению, основанному на вариативности, которая восходит к работе Пьера Америки над POOL (OOPSLA 89).

Дикие карты отличаются некоторыми деталями от конструкций, описанных в упомянутой статье, в частности, в использовании преобразования захвата (§5.1.10) вместо операции close, описанной Игараси и Вироли. Для формального описания диких карт см. Wild FJ Мадса Торгерсена, Эрика Эрнста и Кристиана Плеснера Хенсена в 12-м семинаре по основам объектно-ориентированного программирования (FOOL 2005).

Пример 4.5.1-1. Неограниченные дикие карты

import java.util.Collection;
import java.util.ArrayList;

class Test {
    static void printCollection(Collection<?> c) {
                                // a wildcard collection
        for (Object o : c) {
            System.out.println(o);
        }
    }

    public static void main(String[] args) {
        Collection<String> cs = new ArrayList<String>();
        cs.add("hello");
        cs.add("world");
        printCollection(cs);
    }
}

Обратите внимание, что использование Collection<Object> в качестве типа входного параметра, c, не было бы столь же полезным; метод мог бы использоваться только с выражением аргумента, имеющим тип Collection<Object>, что встречается довольно редко. В отличие от этого, использование неограниченной дикой карты позволяет передавать в качестве аргумента любые виды коллекций.

Вот пример, где тип элемента массива параметризован дикой картой:


public Method getMethod(Class<?>[] parameterTypes) { ... }


Пример 4.5.1-2. Ограниченные дикие карты

boolean addAll(Collection<? extends E> c)

Здесь метод объявлен в интерфейсе Collection<E> и предназначен для добавления всех элементов своего входного аргумента в коллекцию, к которой он вызывается. Естественным было бы использовать Collection<E> в качестве типа c, но это излишне ограничивает. Альтернативой было бы объявить сам метод обобщённым:

<T> boolean addAll(Collection<T> c)

Эта версия достаточно гибкая, но обратите внимание, что параметр типа используется только один раз в сигнатуре. Это отражает тот факт, что параметр типа не используется для выражения каких-либо взаимозависимостей между типом(ами) аргумента(ов), типом возвращаемого значения и/или типом исключения. В отсутствие такой взаимозависимости обобщенные методы считаются плохим стилем, и предпочитаются дикие карты.

Reference(T referent, ReferenceQueue<? super T> queue)

Здесь референт может быть вставлен в любую очередь, тип элемента которой является надтипом типа T референта; T — нижняя граница для дикой карты.


4.5.2. Члены и конструкторы параметризованных типов

Пусть 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, объявленный в объявлении обобщённого типа, с помощью параметризованного типа.

4.6. Стирание типов

Стирание типов — это отображение типов (возможно, включая параметризованные типы и переменные типов) на типы (которые никогда не являются параметризованными типами или переменными типов). Мы записываем |T| для стирания типа T. Отображение стирания определяется следующим образом:

  • Стирание параметризованного типа (§4.5) G<T1,...,Tn> — это |G|.

  • Стирание вложенного типа T.C — это |T|.C.

  • Стирание типа массива T[] — это |T|[].

  • Стирание переменной типа (§4.4) — это стирание её левого крайнего ограничения.

  • Стирание любого другого типа — это сам тип.

Стирание типов также отображает сигнатуру (§8.4.2) конструктора или метода на сигнатуру, не содержащую параметризованных типов или переменных типов. Стирание сигнатуры конструктора или метода s — это сигнатура, состоящая из того же имени, что и s, и стираний всех типов формальных параметров, заданных в s.

Тип возвращаемого значения метода (§8.4.5) и параметры типа обобщённого метода или конструктора (§8.4.4, §8.8.4) также подвергаются стиранию, если стирается сигнатура метода или конструктора.

Стирание сигнатуры обобщённого метода не имеет параметров типа.

END_OF_DOCUMENT_MARKER

4.7. Типы, допускающие реификацию

Поскольку некоторые типы данных стираются во время компиляции, не все типы доступны во время выполнения. Типы, которые полностью доступны во время выполнения, известны как типы, допускающие реификацию.

Тип допускает реификацию тогда и только тогда, когда выполняется одно из следующих условий:

  • Он ссылается на объявление класса или интерфейса без дженериков.

  • Это параметризованный тип, в котором все аргументы типа являются не ограниченными универсальными параметрами (§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.8. Необработанные типы

Для облегчения взаимодействия с устаревшим необобщенным кодом можно использовать стирание (§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.*;
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 года.

4.9. Типы пересечения

Тип пересечения имеет вид 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' и объявлен в пакете, в котором появляется тип пересечения.

Членами типа пересечения являются члены класса или интерфейса, которые он индуцирует.

Стоит остановиться на различии между типами пересечения и ограничениями переменных типов. Каждое ограничение переменной типа индуцирует тип пересечения. Этот тип пересечения часто является тривиальным, состоящим из одного типа. Форма ограничения ограничена (только первый элемент может быть классом или переменной типа, и только одна переменная типа может появиться в ограничении), чтобы предотвратить возникновение определённых неудобных ситуаций. Однако преобразование захвата может привести к созданию переменных типов, ограничения которых более общие, такие как типы массивов).

4.10. Подтипирование

Отношения подтипа и надтипа — это бинарные отношения типов.

Надтипы типа получаются путем рефлексивного и транзитивного замыкания над отношением прямого надтипа, записываемого как S >1 T, которое определяется правилами, приведенными далее в этом разделе. Мы записываем S :> T, чтобы указать, что отношение надтипа выполняется между S и T.

S является собственным надтипом T, записывается как S > T, если S :> T и S ≠ T.

Подтипы типа T — это все типы U, такие, что T является надтипом U, и нулевой тип. Мы записываем T <: S, чтобы указать, что отношение подтипа выполняется между типами T и S.

T является собственным подтипом S, записывается как T < S, если T <: S и S ≠ T.

T является прямым подтипом S, записывается как T <1 S, если S >1 T.

Подтипирование не распространяется на параметризованные типы: T <: S не подразумевает, что C<T> <: C<S>.

4.10.1. Подтипирование среди примитивных типов

Следующие правила определяют отношение прямого надтипа среди примитивных типов:

  • double >1 float

  • float >1 long

  • long >1 int

  • int >1 char

  • int >1 short

  • short >1 byte

4.10.2. Подтипирование среди типов классов и интерфейсов

Учитывая непростой класс или интерфейс C, прямыми надтипами типа C являются все следующие:

  • Тип прямого суперкласса C (§8.1.4), если C — это класс.

  • Типы прямого суперинтерфейса C (§8.1.5, §9.1.3).

  • Тип Object, если C — это интерфейс без прямых типов суперинтерфейсов (§9.1.3).

Учитывая параметризованный класс или интерфейс 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).

Прямые надтипы переменной типа — это типы, перечисленные в ее границе.

Переменная типа является прямым надтипом своей нижней границы.

Прямыми надтипами нулевого типа являются все ссылочные типы, кроме самого нулевого типа.

4.10.3. Подтипизация среди типов массивов

Следующие правила определяют отношение прямого наследования среди типов массивов:

  • Если S и T — ссылочные типы, то S[] >1 T[] тогда и только тогда, когда S >1 T.

  • Object >1 Object[]

  • Cloneable >1 Object[]

  • java.io.Serializable >1 Object[]

  • Если P — примитивный тип, то:

    • Object >1 P[]

    • Cloneable >1 P[]

    • java.io.Serializable >1 P[]

4.10.4. Наибольшая нижняя граница

Наибольшая нижняя граница (lub) набора типов-ссылок — это общий супертип, более специфичный, чем любой другой общий супертип (то есть ни один другой общий супертип не является подтипом наибольшей нижней границы). Этот тип, lub(U1, ..., Uk), определяется следующим образом.

Если k = 1, то наибольшая нижняя граница — это сам тип: lub(U) = U.

В противном случае:

  • Для каждого Ui (1 ≤ i ≤ k):

    Пусть ST(Ui) — множество супертипов Ui.

    Пусть EST(Ui), множество стёртых супертипов Ui, равно:

    EST(Ui) = { |W| | W in ST(Ui) } где |W| — стёртый вид W.

    Причина вычисления множества стёртых супертипов заключается в том, чтобы обрабатывать ситуации, когда множество типов включает несколько различных параметризаций обобщённого типа.

    Например, заданы List<String> и List<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 in EC, и для всех W ≠ V in EC, неверно, что W <: V }

    Поскольку мы стремимся вывести более точные типы, мы хотим отфильтровать любые кандидаты, которые являются супертипами других кандидатов. Именно этого достигает вычисление MEC. В нашем примере, EC = { List, Collection, Object }, поэтому MEC = { List }. Следующим шагом является восстановление аргументов типа для стёртых типов в MEC.

  • Для любого элемента G множества MEC, являющегося обобщённым типом:

    Пусть "релевантные" параметризации G, Relevant(G), равны:

    Relevant(G) = { V | 1 ≤ i ≤ k: V in ST(Ui) и V = G<...> }

    В нашем примере, единственным обобщённым элементом MEC является List, и Relevant(List) = { List<String>, List<Object> }. Теперь мы будем искать аргумент типа для List, который содержит (§4.5.1) как String, так и Object.

    Это делается с помощью операции наименьшей содержащей параметризации (lcp), определённой ниже. Первая строка определяет lcp() на множестве, например, Relevant(List), как операцию над списком, состоящим из элементов множества. Следующая строка определяет операцию над таким списком как попарное уменьшение элементов списка. Третья строка — определение lcp() для пар параметризованных типов, которое в свою очередь опирается на понятие наименьшего содержащего аргумента типа (lcta). lcta() определена для всех возможных случаев.

    Пусть "кандидатская" параметризация G, Candidate(G), — это наиболее специфичная параметризация обобщённого типа G, которая содержит все релевантные параметризации G:

    Candidate(G) = lcp(Relevant(G))

    где lcp(), наименьшая содержащая параметризация, это:

    • lcp(S) = lcp(e1, ..., en) где ei (1 ≤ i ≤ n) in S

    • lcp(e1, ..., en) = lcp(lcp(e1, e2), e3, ..., en)

    • lcp(G<X1, ..., Xn>, G<Y1, ..., Yn>) = G<lcta(X1, Y1), ..., lcta(Xn, Yn)>

    • lcp(G<X1, ..., Xn>) = G<lcta(X1), ..., lcta(Xn)>

    и где lcta(), наименьший содержащий аргумент типа, это: (предполагая, что U и V — типы)

    • lcta(U, V) = U если U = V, в противном случае ? extends lub(U, V)

    • lcta(U, ? extends V) = ? extends lub(U, V)

    • lcta(U, ? super V) = ? super glb(U, V)

    • lcta(? extends U, ? extends V) = ? extends lub(U, V)

    • lcta(? extends U, ? super V) = ?

    • lcta(? super U, ? super V) = ? super glb(U, V)

    • lcta(U) = ? если верхняя граница U равна Object, в противном случае ? extends lub(U,Object)

    и где glb() определяется в §5.1.10.

  • Пусть lub(U1 ... Uk) равно:

    Best(W1) & ... & Best(Wr)

    где Wi (1 ≤ i ≤ r) — элементы MEC, минимального множества стёртых кандидатов U1 ... Uk;

    и где, если какие-либо из этих элементов являются обобщёнными, мы используем кандидатскую параметризацию (чтобы восстановить аргументы типа):

    Best(X) = Candidate(X) если X — обобщённый тип; X в противном случае.

Строго говоря, эта функция lub() только аппроксимирует наименьшую верхнюю границу. Формально может существовать другой тип T такой, что все U1 ... Uk являются подтипами T, а T является подтипом lub(U1, ..., Uk). Однако компилятор для языка программирования Java должен реализовать lub() как указано выше.

Возможно, что функция lub() выдает бесконечный тип. Это допустимо, и компилятор для языка программирования Java должен распознавать такие ситуации и представлять их соответствующим образом, используя циклические структуры данных.

Возможность бесконечного типа проистекает из рекурсивных вызовов lub(). Читатели, знакомые с рекурсивными типами, должны отметить, что бесконечный тип — это не то же самое, что рекурсивный тип.

4.10.5. Проекции типов

Синтетическая переменная типа — это переменная типа, вводимая компилятором во время преобразования захвата (§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' — это wildcard, определяемый тремя случаями:

      • Если U не Object, и если объявленная граница i-го параметра G, Bi, упоминает параметр типа G, или Bi не является подтипом U, то Ai' — это wildcard с верхней границей, ? extends U.

      • В противном случае, если нисходящая проекция Ai равна L, то Ai' — это wildcard с нижней границей, ? super L.

      • В противном случае нисходящая проекция Ai не определена, и Ai' — это wildcard без ограничений, ?.

    • Если Ai — wildcard с верхней границей, который упоминает ограниченную переменную типа, то пусть U будет восходящей проекцией границы wildcard. Ai' — это wildcard с верхней границей, ? extends U.

    • Если Ai — wildcard с нижней границей, который упоминает ограниченную переменную типа, то, если нисходящая проекция границы wildcard равна L, то Ai' — это wildcard с нижней границей, ? super L; если нисходящая проекция границы wildcard не определена, то Ai' — это wildcard без ограничений, ?.

  • Если 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 — wildcard с верхней границей, который упоминает ограниченную переменную типа, то, если нисходящая проекция границы wildcard равна U, то Ai' — это wildcard с верхней границей, ? extends U; если нисходящая проекция границы wildcard не определена, то Ai' не определена.

    • Если Ai — wildcard с нижней границей, который упоминает ограниченную переменную типа, то пусть L будет восходящей проекцией границы wildcard. Ai' — это wildcard с нижней границей, ? super L.

  • Если T — тип массива, S[], то, если нисходящая проекция S равна S', результатом является S'[]; если нисходящая проекция S не определена, то результатом является неопределенность.

  • Если T — тип пересечения, то, если нисходящая проекция определена для каждого элемента T, результатом является тип пересечения, элементами которого являются нисходящие проекции элементов T; если нисходящая проекция не определена для любого элемента T, то результатом является неопределенность.

Как и lub (§4.10.4), восходящая и нисходящая проекции могут создавать бесконечные типы из-за рекурсии по границам переменных типа.

4.11. Где используются типы

Типы используются в большинстве видов объявлений и в определенных видах выражений. Конкретно, существует 17 контекстов типов, где используются типы:

  • В объявлениях:

    1. Тип в extends или implements заключении объявления класса (§8.1.4, §8.1.5)

    2. Тип в extends заключении объявления интерфейса (§9.1.3)

    3. Тип возвращаемого значения метода (§8.4.5, §9.4), включая тип элемента интерфейса аннотации (§9.6.1)

    4. Тип в throws заключении метода или конструктора (§8.4.6, §8.8.5, §9.4)

    5. Тип в extends заключении объявления параметра типа для обобщенного класса, интерфейса, метода или конструктора (§8.1.2, §9.1.2, §8.4.4, §8.8.4)

    6. Тип в объявлении поля класса или интерфейса (§8.3, §9.3), включая константу перечисления (§8.9.1)

    7. Тип в объявлении формального параметра метода, конструктора или лямбда-выражения (§8.4.1, §8.8.1, §9.4, §15.27.1)

    8. Тип параметра получателя метода (§8.4)

    9. Тип в объявлении локальной переменной в операторе (§14.4.2, §14.14.1, §14.14.2, §14.20.3) или шаблоне (§14.30.1)

    10. Тип в объявлении параметра исключения (§14.20)

    11. Тип в объявлении компонента записи класса записи (§8.10.1)

  • В выражениях:

    1. Тип в списке явных аргументов типа для явного вызова конструктора, выражения создания экземпляра класса, выражения вызова метода или выражения ссылки на метод (§8.8.7.1, §15.9, §15.12, §15.13)

    2. В выражении создания экземпляра класса без квалификатора, как тип класса, который должен быть создан (§15.9) или как прямой тип суперкласса или прямой тип суперинтерфейса анонимного класса, который должен быть создан (§15.9.5)

    3. Тип элемента в выражении создания массива (§15.10.1)

    4. Тип в операторе приведения в выражении приведения (§15.16)

    5. Тип, который следует за оператором сравнения типов instanceof (§15.20.2)

    6. В выражении ссылки на метод (§15.13), как тип ссылки для поиска метода члена или как тип класса или тип массива для построения.

Также типы используются как:

  • Тип элемента типа массива в любом из указанных выше контекстов; и

  • Аргумент типа без подстановок или граница аргумента типа с подстановками параметризованного типа в любом из указанных выше контекстов.

Наконец, существуют три специальных термина в языке программирования Java, которые обозначают использование типа:

  • Неограниченный дикий тип (§4.5.1)

  • ... в типе параметра с переменным числом параметров (§8.4.1), чтобы указать тип массива

  • Простое имя типа в объявлении конструктора (§8.8), чтобы указать класс создаваемого объекта

Значение типов в контекстах типов задается:

  • §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).

  • Следующие контексты типов требуют, чтобы если тип является параметризованным ссылочным типом, то он имеет только аргументы типа с неограниченной подстановкой (т.е. это реифицируемый тип):

    • В качестве типа элемента в выражении создания массива (§15.10.1)

    • В качестве типа, который следует за оператором instanceof отношения (§15.20.2)

  • Следующие контексты типов не допускают параметризованный ссылочный тип вообще, потому что они связаны с исключениями, а тип исключения не является обобщённым (§6.1):

    • Как тип исключения, которое может быть выброшено методом или конструктором (§8.4.6, §8.8.5, §9.4)

    • В объявлении параметра исключения (§14.20)

В любом контексте типа, где используется тип, можно добавить аннотацию к ключевому слову, обозначающему примитивный тип, или к Идентификатору, обозначающему простое имя ссылочного типа. Также можно добавить аннотацию к типу массива, записав аннотацию слева от [ на требуемом уровне вложенности в типе массива. Аннотации в этих местах называются типовыми аннотациями, и они указаны в §9.7.4. Вот несколько примеров:

  • @Foo int[] f; аннотирует примитивный тип int

  • int @Foo [] f; аннотирует тип массива int[]

  • int @Foo [][] f; аннотирует тип массива int[][]

  • int[] @Foo [] f; аннотирует тип массива int[], который является типом компонента массива int[][]

Некоторые из контекстов типов, которые появляются в объявлениях, занимают такое же синтаксическое место, как ряд контекстов объявлений (§9.6.4.1):

  • Тип возвращаемого значения метода (включая тип элемента интерфейса аннотации)

  • Тип в объявлении поля класса или интерфейса (включая константу перечисления)

  • Тип в объявлении формального параметра метода, конструктора или лямбда-выражения

  • Тип в объявлении локальной переменной

  • Тип в объявлении параметра исключения

  • Тип в объявлении компонента записи класса записи

Тот факт, что одно и то же синтаксическое место в программе может быть как контекстом типа, так и контекстом объявления, возникает потому, что модификаторы объявления непосредственно предшествуют типу объявленной сущности. §9.7.4 объясняет, как аннотация в таком месте считается появившейся в контексте типа или контексте объявления или в обоих.

Пример 4.11-1. Использование типа

import java.util.Random;
import java.util.Collection;
import java.util.ArrayList;

class MiscMath<T extends Number> {
    int divisor;
    MiscMath(int divisor) { this.divisor = divisor; }
    float ratio(long l) {
        try {
            l /= divisor;
        } catch (Exception e) {
            if (e instanceof ArithmeticException)
                l = Long.MAX_VALUE;
            else
                l = 0;
        }
        return (float)l;
    }
    double gausser() {
        Random r = new Random();
        double[] val = new double[2];
        val[0] = r.nextGaussian();
        val[1] = r.nextGaussian();
        return (val[0] + val[1]) / 2;
    }
    Collection<Number> fromArray(Number[] na) {
        Collection<Number> cn = new ArrayList<Number>();
        for (Number n : na) cn.add(n);
        return cn;
    }
    <S> void loop(S s) { this.<S>loop(s); }  
}

В этом примере типы используются в объявлениях следующего:

  • Поля, которые являются переменными класса и экземпляра класса (§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.12. Переменные

Переменная — это место хранения, связанное с типом, иногда называемым типом во время компиляции, который является либо примитивным типом (§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).

4.12.1. Переменные примитивного типа

Переменная примитивного типа всегда содержит примитивное значение этого типа.

4.12.2. Переменные ссылочного типа

Переменная типа класса 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 или из источников, которые явно подавляли предупреждения о не проверенных во время компиляции ошибках. Такая практика в лучшем случае нездоровая.

И наоборот, возможно, что, несмотря на выполнение кода, который мог (и, возможно, сделал) вызвать предупреждение о не проверенных во время компиляции ошибках, не происходит загрязнения кучи. На самом деле, хорошая практика программирования требует, чтобы программист убедилась, что, несмотря на любые предупреждения, код правильный и загрязнение кучи не произойдет.

4.12.3. Виды переменных

Существует восемь видов переменных:

  1. Переменная класса — это поле, объявленное с использованием ключевого слова static в объявлении класса (§8.3.1.1), или с или без ключевого слова static в объявлении интерфейса (§9.3).

    Переменная класса создаётся при подготовке её класса или интерфейса (§12.3.2) и инициализируется значением по умолчанию (§4.12.5). Переменная класса фактически перестаёт существовать при разгрузке её класса или интерфейса (§12.7).

  2. Переменная экземпляра — это поле, объявленное в объявлении класса без использования ключевого слова static (§8.3.1.1).

    Если класс T имеет поле a, которое является переменной экземпляра, то новая переменная экземпляра a создаётся и инициализируется значением по умолчанию (§4.12.5) как часть каждого вновь созданного объекта класса T или любого класса, который является подклассом T (§8.1.4). Переменная экземпляра фактически перестаёт существовать, когда объект, которым она является полем, больше не ссылается на неё, после завершения всех необходимых финализаций объекта (§12.6).

  3. Элементы массива — это безымянные переменные, которые создаются и инициализируются значениями по умолчанию (§4.12.5) всякий раз, когда создаётся новый объект, являющийся массивом (§10 (Массивы), §15.10.2). Элементы массива фактически перестают существовать, когда на массив больше нет ссылок.

  4. Параметры метода (§8.4.1) — имена значений аргументов, переданных методу.

    Для каждого параметра, объявленного в объявлении метода, создаётся новая переменная параметра каждый раз, когда вызывается этот метод (§15.12). Новая переменная инициализируется соответствующим значением аргумента из вызова метода. Параметр метода фактически перестаёт существовать, когда завершается выполнение тела метода.

  5. Параметры конструктора (§8.8.1) — имена значений аргументов, переданных конструктору.

    Для каждого параметра, объявленного в объявлении конструктора, создаётся новая переменная параметра каждый раз, когда выражение создания экземпляра класса (§15.9) или явный вызов конструктора (§8.8.7) вызывает этот конструктор. Новая переменная инициализируется соответствующим значением аргумента из выражения создания или вызова конструктора. Параметр конструктора фактически перестаёт существовать, когда завершается выполнение тела конструктора.

  6. Параметры лямбда-выражения (§15.27.1) — имена значений аргументов, передаваемых в тело лямбда-выражения (§15.27.2).

    Для каждого параметра, объявленного в лямбда-выражении, создаётся новая переменная параметра каждый раз, когда вызывается метод, реализованный телом лямбда-выражения (§15.12). Новая переменная инициализируется соответствующим значением аргумента из вызова метода. Параметр лямбда-выражения фактически перестаёт существовать, когда завершается выполнение тела лямбда-выражения.

  7. Переменная параметра исключения создаётся каждый раз, когда исключение перехватывается блоком catch оператора try (§14.20).

    Новая переменная инициализируется фактическим объектом, связанным с исключением (§11.3, §14.18). Параметр исключения фактически перестаёт существовать, когда завершается выполнение блока, связанного с блоком catch.

  8. Локальные переменные (§14.4) объявляются операторами (§14.4.2, §14.14.1, §14.14.2, §14.20.3) и шаблонами (§14.30.1). Локальная переменная, объявленная шаблоном, называется переменной шаблона.

    Локальная переменная, объявленная оператором, создаётся, когда поток управления входит в ближайший охватывающий блок (§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;
    }
}


4.12.4. final Переменные

Переменная может быть объявлена 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 удаляется.

4.12.5. Начальные значения переменных

Каждая переменная в программе должна иметь значение до его использования:

  • Каждая переменная класса, экземпляра или компонент массива инициализируется значением по умолчанию при создании (§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 (Исполнение) для полного описания всех аспектов загрузки, компоновки и инициализации классов и интерфейсов, а также описания создания экземпляров классов для получения новых экземпляров классов.


4.12.6. Типы, классы и интерфейсы

В языке программирования Java каждая переменная и каждое выражение имеют тип, который можно определить на этапе компиляции. Тип может быть примитивным типом или ссылочным типом. К ссылочным типам относятся типы классов и типы интерфейсов. Ссылочные типы вводятся объявлениями типов, которые включают объявления классов (§8.1) и объявления интерфейсов (§9.1). Мы часто используем термин тип для обозначения класса или интерфейса.

В виртуальной машине Java каждый объект принадлежит какому-то определенному классу: классу, который был упомянут в выражении создания, которое породило объект (§15.9), или классу, чей Class объект был использован для вызова рефлексивного метода для создания объекта, или классу String для объектов, неявно созданных оператором конкатенации строк + (§15.18.1). Этот класс называется классом объекта. Говорят, что объект является экземпляром своего класса и всех своих суперклассов.

Каждый массив также имеет класс. Метод getClass, когда он вызывается для объекта массива, вернет объект класса Class, который представляет собой класс массива (§10.8).

Тип переменной на этапе компиляции всегда объявляется, а тип выражения на этапе компиляции можно вывести на этапе компиляции. Тип на этапе компиляции ограничивает возможные значения, которые может содержать переменная во время выполнения, или выражение, которое может произвести во время выполнения. Если значение во время выполнения является ссылкой, которая не null, оно ссылается на объект или массив, имеющий класс, и этот класс обязательно будет совместим с типом на этапе компиляции.

Даже если переменная или выражение могут иметь тип на этапе компиляции, который является типом интерфейса, экземпляров интерфейсов не существует. Переменная или выражение, тип которых является типом интерфейса, может ссылаться на любой объект, чей класс реализует (§8.1.5) этот интерфейс.

Иногда переменная или выражение говорят, что имеют "тип во время выполнения". Это относится к классу объекта, на который ссылается значение переменной или выражения во время выполнения, при условии, что значение не null.

Соответствие между типами на этапе компиляции и типами во время выполнения является неполным по двум причинам:

  1. Во время выполнения классы и интерфейсы загружаются виртуальной машиной Java с помощью загрузчиков классов. Каждый загрузчик классов определяет свой собственный набор классов и интерфейсов. В результате возможно, что два загрузчика загрузят одинаковое определение класса или интерфейса, но создадут разные классы или интерфейсы во время выполнения. Следовательно, код, который скомпилировался правильно, может завершиться неудачей на этапе компоновки, если загрузчики классов, которые его загружают, несовместимы.

    См. статью Динамическая загрузка классов в виртуальной машине Java, авторы Шенг Лиан и Гилад Браха, в Труды OOPSLA'98, опубликованные как ACM SIGPLAN Notices, том 33, номер 10, октябрь 1998 г., страницы 36-44, и Спецификация виртуальной машины Java, издание Java SE 17 для получения более подробной информации.

  2. Параметрические переменные (§4.4) и параметрические типы (§4.5.1) не являются реифицированными во время выполнения. В результате один и тот же класс или интерфейс во время выполнения представляет собой несколько параметризованных типов (§4.5) на этапе компиляции. В частности, все параметризации на этапе компиляции данного обобщенного типа (§8.1.2, §9.1.2) имеют единое представление во время выполнения.

    В определенных условиях возможно, что переменная параметризованного типа ссылается на объект, который не является объектом этого параметризованного типа. Эта ситуация известна как загрязнение кучи (§4.12.2). Переменная всегда будет ссылаться на объект, который является экземпляром класса, представляющего параметризованный тип.

Пример 4.12.6-1. Тип переменной против класса объекта

interface Colorable {
    void setColor(byte r, byte g, byte b);
}

class Point { int x, y; }

class ColoredPoint extends Point implements Colorable {
    byte r, g, b;
    public void setColor(byte rv, byte gv, byte bv) {
        r = rv; g = gv; b = bv;
    }
}

class Test {
    public static void main(String[] args) {
        Point p = new Point();
        ColoredPoint cp = new ColoredPoint();
        p = cp;
        Colorable c = cp;
    }
}

В этом примере:

  • Локальная переменная p метода main класса Test имеет тип Point и изначально ей присваивается ссылка на новый экземпляр класса Point.

  • Локальная переменная cp аналогичным образом имеет тип ColoredPoint и изначально ей присваивается ссылка на новый экземпляр класса ColoredPoint.

  • Присваивание значения cp переменной p приводит к тому, что p содержит ссылку на объект ColoredPoint. Это разрешено, поскольку ColoredPoint является подклассом Point, поэтому класс ColoredPoint совместим с типом Point. Объект ColoredPoint включает поддержку всех методов класса Point. В дополнение к своим собственным полям r, g и b, он имеет поля класса Point, а именно x и y.

  • Локальная переменная c имеет тип интерфейса Colorable, поэтому она может содержать ссылку на любой объект, чей класс реализует Colorable; в частности, она может содержать ссылку на ColoredPoint.

Обратите внимание, что выражение, такое как new Colorable(), не является допустимым, так как создать экземпляр интерфейса невозможно, можно только создать экземпляр класса. Однако выражение new Colorable() { public void setColor... } является допустимым, потому что оно объявляет анонимный класс (§15.9.5), реализующий интерфейс Colorable.


© Oracle and/or its affiliates. All rights reserved.
Licensed under the Oracle Technology Network License Agreement.

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API