Spec-Zone.ru › Java Language Specification 21

Глава 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-битным двоичным форматом float и 64-битным двоичным форматом double для значений и операций IEEE 754, как указано в стандарте IEEE 754 (§1.7).

В Java SE 15 и более поздних версиях язык программирования Java использует версию стандарта IEEE 754 2019 года. В версиях Java SE до Java SE 15 язык программирования Java использовал версию стандарта IEEE 754 1985 года, где формат binary32 был известен как формат single, а формат binary64 — как формат double.

Стандарт IEEE 754 включает не только положительные и отрицательные числа, состоящие из знака и модуля, но также положительный и отрицательный ноль, положительную и отрицательную бесконечность и специальные значения Not-a-Number (в дальнейшем сокращённо NaN). Значение NaN используется для представления результата некоторых недопустимых операций, таких как деление нуля на ноль. Константы NaN типов float и double предварительно определены как Float.NaN и Double.NaN.

Конечные ненулевые значения типа с плавающей точкой могут быть выражены в форме s ⋅ m ⋅ 2(e - N + 1), где:

  • s равно +1 или -1,

  • m — положительное целое число, меньшее 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 для каждого из его форматов чисел с плавающей точкой binary32 и binary64. Однако платформа Java SE обычно рассматривает значения NaN данного типа с плавающей точкой как сворачивающиеся в одно каноническое значение, и поэтому данное описание обычно относится к произвольному NaN как к каноническому значению.

Согласно IEEE 754, операция с плавающей точкой с аргументами, не являющимися NaN, может сгенерировать результат NaN. IEEE 754 определяет набор шаблонов бит NaN, но не предписывает конкретный шаблон NaN для представления результата NaN; это оставляется на усмотрение архитектуры оборудования. Программист может создавать NaN с различными шаблонами бит для кодирования, например, ретроспективной диагностической информации. Эти значения NaN могут быть созданы с помощью методов Float.intBitsToFloat и Double.longBitsToDouble для float и double соответственно. И наоборот, для просмотра шаблонов бит значений NaN можно использовать методы Float.floatToRawIntBits и Double.doubleToRawLongBits для float и double соответственно.

Положительный и отрицательный ноль сравниваются как равные, поэтому результат выражения 0.0==-0.0 равен true, а результат 0.0>-0.0 — false. Другие операции могут различать положительный и отрицательный ноль; например, 1.0/0.0 имеет значение положительная бесконечность, а значение 1.0/-0.0 — отрицательная бесконечность.

NaN не упорядочен, поэтому:

  • Числовые операторы сравнения <, <=, > и >= возвращают false, если один или оба операнда равны NaN (§15.20.1).

    В частности, (x<y) == !(x>=y) будет false, если x или y является NaN.

  • Оператор равенства == возвращает false, если один из операндов равен NaN.

  • Оператор неравенства != возвращает true, если один из операндов равен NaN (§15.21.1).

    В частности, x!=x будет true, если и только если x является NaN.

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 ТипКлассаИлиИнтерфейса {ДополнительнаяГраница}
ДополнительнаяГраница:
& ТипИнтерфейса

Область видимости переменной типа, объявленной как параметр типа, указана в §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). Эта работа сама по себе опирается на более ранние работы Крстен Торупа и Мадса Торгерсена (Unifying Genericity, ECOOP 99), а также на богатую традицию работы над вариативностью на основе объявлений, которая восходит к работам Пьера Америки по POOL (OOPSLA 89).

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

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

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

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

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

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

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


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


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

boolean addAll(Collection<? extends E> c)

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

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

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

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

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


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.ArrayList;
import java.util.Collection;
import java.util.Iterator;

class NonGeneric {
    Collection<Number> myNumbers() { return null; }
}

abstract class RawMembers<T> extends NonGeneric
                             implements Collection<String> {
    static Collection<NonGeneric> cng =
        new ArrayList<NonGeneric>();

    public static void main(String[] args) {
        RawMembers rw = null;
        
        Collection<Number> cn = rw.myNumbers();
                                 // OK
        
        Iterator<String> is   = rw.iterator();
                                 // Unchecked warning
        
        Collection<NonGeneric> cnn = rw.cng;
                                      // OK, static member
    }
}

В этой программе (которую не нужно запускать), RawMembers<T> наследует метод:

Iterator<String> iterator()

от суперинтерфейса Collection<String>. Необработанный тип RawMembers наследует iterator() от Collection, стирания Collection<String>, что означает, что тип возврата iterator() в RawMembers является Iterator. В результате попытка присвоить rw.iterator() переменной Iterator<String> требует неявного преобразования, поэтому выдается предупреждение времени компиляции.

В отличие от этого, RawMembers наследует myNumbers() от класса NonGeneric, стирание которого также равно NonGeneric. Таким образом, тип возврата myNumbers() в RawMembers не стирается, и попытка присвоить rw.myNumbers() переменной Collection<Number> не требует неявного преобразования, поэтому предупреждение времени компиляции не выдается.

Аналогично, член static cng сохраняет свой параметризованный тип даже при обращении через объект необработанного типа. Обратите внимание, что доступ к члену static через экземпляр считается плохим стилем и не рекомендуется.

Этот пример показывает, что некоторые члены необработанного типа не стираются, а именно члены static, типы которых параметризованы, и члены, унаследованные от необобщенного супертипа.


Необработанные типы тесно связаны с дикими картами. Оба основаны на экзистенциальных типах. Необработанные типы можно рассматривать как дикие карты, правила типов которых преднамеренно некорректны, чтобы обеспечить взаимодействие со старым кодом. Исторически необработанные типы предшествовали диким картам; они были впервые введены в GJ и описаны в статье Обеспечение безопасности будущего для прошлого: добавление обобщенности в язык программирования Java авторами Гиладом Брахой, Мартином Одески, Дэвидом Стаутамиром и Филипом Уэдлером, в Труды конференции ACM по объектно-ориентированному программированию, системам, языкам и приложениям (OOPSLA 98), октябрь 1998 года.

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, а также тип null. Мы пишем T <: S, чтобы указать, что отношение подтипа имеет место между типами T и S.

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

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

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

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

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

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

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

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 - это сам тип: lub(U) = U.

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

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

    Пусть ST(Ui) будет набором супертипов Ui.

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

    EST(Ui) = { |W| | W in ST(Ui) } where |W| is the erasure of 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, and for all W ≠ V in EC, it is not the case that 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) and V = G<...> }

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

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

    Пусть «кандидатской» параметризацией G, Candidate(G), будет наиболее конкретная параметризация обобщенного типа G, которая содержит все соответствующие параметризации G:

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

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

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

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

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

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

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

    • lcta(U, V) = U if U = V, otherwise ? 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) = ? if U's upper bound is Object, otherwise ? 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) if X is generic; X otherwise.

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

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

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

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

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

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

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

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

Нисходящая проекция типа T по отношению к набору ограниченных переменных типа является частичной функцией, определяемой следующим образом:

  • Если T не упоминает ни одной ограниченной переменной типа, то результатом является T.

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

  • Если T — параметризованный тип класса или параметризованный тип интерфейса, G<A1,...,An>, то результатом является G<A1',...,An'>, если для 1 ≤ i ≤ n аргумент типа Ai' может быть выведен из Ai следующим образом; в противном случае результат не определен:

    • Если Ai не упоминает ограниченную переменную типа, то Ai' = Ai.

    • Если Ai — тип, который упоминает ограниченную переменную типа, то Ai' не определен.

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

    • Если Ai — подстановочный знак с нижней границей, который упоминает ограниченную переменную типа, то пусть L будет восходящей проекцией границы подстановочного знака. Ai' — это подстановочный знак с нижней границей, ? 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.ArrayList;
import java.util.Collection;
import java.util.Random;

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

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

  • Поля, которые являются переменными класса и экземпляра класса (§8.3), и константы интерфейсов (§9.3); здесь поле divisor в классе MiscMath объявлено как типа int

  • Параметры метода (§8.4.1); здесь параметр l метода ratio объявлен как типа long

  • Результаты метода (§8.4); здесь результат метода ratio объявлен как типа float, а результат метода gausser объявлен как типа double

  • Параметры конструктора (§8.8.1); здесь параметр конструктора для MiscMath объявлен как типа int

  • Локальные переменные (§14.4, §14.14); локальные переменные r и val метода gausser объявлены как типов Random и double[] (массив double)

  • Параметры исключения (§14.20); здесь параметр исключения e блока catch объявлен как типа Exception

  • Параметры типа (§4.4); здесь параметр типа MiscMath — переменная типа T, с типом Number в качестве объявленного ограничения

  • В любом объявлении, использующем параметризованный тип; здесь тип Number используется в качестве аргумента типа (§4.5.1) в параметризованном типе Collection<Number>.

и в выражениях следующих типов:

  • Создание экземпляра класса (§15.9); здесь локальная переменная r метода gausser инициализируется выражением создания экземпляра класса, использующим тип Random

  • Создание экземпляра обобщённого класса (§8.1.2) (§15.9); здесь Number используется как аргумент типа в выражении new ArrayList<Number>()

  • Создание массива (§15.10.1); здесь локальная переменная val метода gausser инициализируется выражением создания массива, создающим массив double размером 2

  • Вызов обобщённого метода (§8.4.4) или конструктора (§8.8.4) (§15.12); здесь метод loop вызывает себя с явным аргументом типа S

  • Приведения типов (§15.16); здесь оператор return метода ratio использует тип float в приведении

  • Оператор instanceof (§15.20.2); здесь оператор instanceof проверяет, совместим ли e по присваиванию с типом ArithmeticException


4.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). Локальная переменная, объявленная шаблоном, называется переменной шаблона.

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

  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 совместим по присваиванию (§5.2) с типом Point. Объект ColoredPoint включает поддержку всех методов класса Point. В дополнение к своим собственным полям r, g и b, он имеет поля класса Point, а именно x и y.

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

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


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

Spec-Zone.ru

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