Spec-Zone.ru › Java Language Specification 7

Глава 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.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.7, §15.8.1), который не имеет имени.

Поскольку тип null не имеет имени, невозможно объявить переменную типа null или выполнить приведение к типу null.

Нулевая ссылка является единственно возможным значением выражения типа null.

Нулевая ссылка всегда может быть преобразована к любому ссылочному типу с помощью расширяющего преобразования ссылки.

На практике программист может игнорировать тип null и просто представить, что null — это всего лишь специальная литерал, которая может быть любого ссылочного типа.

END_OF_DOCUMENT_MARKER

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

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


PrimitiveType:
NumericType
boolean

NumericType:
IntegralType
FloatingPointType

IntegralType: одно из
byte short int long char

FloatingPointType: одно из
float double

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

Числовые типы — это целые и типы с плавающей точкой.

Целочисленные типы — это byte, short, int и long, значения которых представляют собой 8-битные, 16-битные, 32-битные и 64-битные знакомые целые числа в дополнительном коде 2 соответственно, и char, значения которых представляют собой 16-битные беззнаковые целые числа, представляющие код UTF-16 (§3.1).

Типы с плавающей точкой — это float, значения которых включают 32-битные числа с плавающей точкой IEEE 754, и double, значения которых включают 64-битные числа с плавающей точкой IEEE 754.

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

Другие полезные конструкторы, методы и константы определены в классах Byte, Short, Integer, Long и Character.

Если целочисленный оператор, отличный от оператора сдвига, имеет по крайней мере один операнд типа long, то операция выполняется с 64-битной точностью, а результат числового оператора имеет тип long. Если другой операнд не long, он сначала расширяется (§5.1.5) до типа long с помощью числового продвижения (§5.6).

В противном случае операция выполняется с 32-битной точностью, а результат числового оператора имеет тип int. Если хотя бы один операнд не int, он сначала расширяется до типа int с помощью числового продвижения.

Любое значение любого целочисленного типа может быть приведено к любому числовому типу или из него. Не существует приведений между целочисленными типами и типом boolean.

См. §4.2.5 для примера преобразования целочисленных выражений в boolean.

Целочисленные операторы не указывают переполнение или недополнение никоим образом.

Целочисленный оператор может сгенерировать исключение (§11) по следующим причинам:

  • Любой целочисленный оператор может сгенерировать исключение NullPointerException, если требуется преобразование распаковки (§5.1.8) нулевого указателя.

  • Оператор целочисленного деления / (§15.17.2) и оператор целочисленного остатка % (§15.17.3) могут сгенерировать исключение ArithmeticException, если правой операнд равен нулю.

  • Операторы инкремента и декремента ++ (§15.14.2, §15.15.1) и -- (§15.14.3, §15.15.2) могут сгенерировать исключение OutOfMemoryError, если необходимо преобразование упаковки (§5.1.7), и для выполнения преобразования недостаточно памяти.

Пример 4.2.2-1. Операции над целыми числами

class Test {
    public static void main(String[] args) {
        int i = 1000000;
        System.out.println(i * i);
        long l = i;
        System.out.println(l * l);
        System.out.println(20296 / (l - i));
    }
}

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

-727379968
1000000000000

а затем столкнется с исключением ArithmeticException при делении на l - i, потому что l - i равно нулю. Первое умножение выполняется с 32-битной точностью, в то время как второе — это long умножение. Значение -727379968 — это десятичное значение низших 32 битов математического результата, 1000000000000, значение которого слишком велико для типа int.


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

Типы с плавающей точкой — это float и double, которые концептуально связаны со значениями и операциями формата с одинарной точностью 32 бита и двойной точностью 64 бита IEEE 754, как указано в Стандарте IEEE для двоичной арифметики с плавающей точкой, ANSI/IEEE Standard 754-1985 (IEEE, Нью-Йорк).

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

Каждая реализация языка программирования Java обязана поддерживать два стандартных набора значений с плавающей точкой, называемые множеством значений float и множеством значений double. Кроме того, реализация языка программирования Java может поддерживать один или оба расширенных множества значений с плавающей точкой с расширенным порядком, называемые множеством значений float-extended-exponent и множеством значений double-extended-exponent. Эти множества значений с расширенным порядком могут в определенных случаях использоваться вместо стандартных множеств значений для представления значений выражений типа float или double (§5.1.13, §15.4).

Все конечные отличные от нуля значения любого множества значений с плавающей точкой могут быть выражены в форме s · m · 2(e - N + 1), где s равно +1 или -1, m — положительное целое число, меньшее 2N, а e — целое число между Emin = -(2K-1-2) и Emax = 2K-1-1 включительно, а N и K — параметры, зависящие от множества значений. Некоторые значения могут быть представлены в этой форме более чем одним способом; например, предположим, что значение v в множестве значений может быть представлено в этой форме с определенными значениями для s, m и e, тогда, если m было чётным, а e меньше 2K-1, можно уменьшить m вдвое и увеличить e на 1, чтобы получить второе представление для того же значения v. Представление в этой форме называется нормализованным, если m ≥ 2N-1; в противном случае представление называется денормализованным. Если значение в множестве значений не может быть представлено таким образом, что m ≥ 2N-1, то значение считается денормализованным значением, так как у него нет нормализованного представления.

Ограничения на параметры N и K (и на производные параметры Emin и Emax) для двух требуемых и двух необязательных множеств значений с плавающей точкой приведены в таблице 4.1.

Таблица 4.1. Параметры множества значений с плавающей точкой

Параметр float float-extended-exponent double double-extended-exponent
N 24 24 53 53
K 8 ≥ 11 11 ≥ 15
Emax +127 ≥ +1023 +1023 ≥ +16383
Emin -126 ≤ -1022 -1022 ≤ -16382

Если реализация поддерживает одно или оба расширенных множества значений с плавающей точкой, то для каждого поддерживаемого множества значений с расширенным порядком существует специфическая константа K, значение которой ограничено таблицей 4.1; это значение K в свою очередь определяет значения для Emin и Emax.

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

Обратите внимание, что ограничения в таблице 4.1 разработаны таким образом, чтобы каждый элемент множества значений float обязательно также являлся элементом множества значений float-extended-exponent, double и double-extended-exponent. Аналогично, каждый элемент множества значений double обязательно также является элементом множества значений double-extended-exponent. Каждое множество значений с расширенным порядком имеет больший диапазон значений показателя, чем соответствующее стандартное множество значений, но не имеет большей точности.

Элементы множества значений float — это в точности те значения, которые могут быть представлены с использованием формата одинарной точности, определённого в стандарте IEEE 754. Элементы множества значений double — это в точности те значения, которые могут быть представлены с использованием формата двойной точности, определённого в стандарте IEEE 754. Однако элементы множеств значений float-extended-exponent и double-extended-exponent, определённые здесь, не соответствуют значениям, которые могут быть представлены с использованием форматов IEEE 754 single extended и double extended соответственно.

Множества значений float, float-extended-exponent, double и double-extended-exponent не являются типами. Для реализации языка программирования Java всегда корректно использовать элемент множества значений float для представления значения типа float; однако в некоторых областях кода реализация может использовать элемент множества значений float-extended-exponent вместо этого. Аналогично, для реализации всегда корректно использовать элемент множества значений double для представления значения типа double; однако в некоторых областях кода реализация может использовать элемент множества значений double-extended-exponent вместо этого.

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

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

В основном платформа Java SE обрабатывает значения NaN данного типа так, как будто они сводятся к одному каноническому значению, и поэтому в этом спецификации обычно упоминается произвольное NaN как каноническое значение.

Однако версия 1.3 платформы Java SE представила методы, позволяющие программисту различать значения NaN: методы Float.floatToRawIntBits и Double.doubleToRawLongBits. Заинтересованный читатель может найти дополнительную информацию в спецификациях классов Float и Double.

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

NaN является неупорядоченным, поэтому:

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

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

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

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

    В частности, x!=x равно true тогда и только тогда, когда x равно NaN.

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

В противном случае операция выполняется с использованием 32-битной арифметики с плавающей точкой, а результат числового оператора — значение типа float. (Если другой операнд не является float, он сначала расширяется до типа float путём числового повышения.)

Любое значение типа с плавающей точкой может быть приведено к любому числовому типу или из него. Нет приведений между типами с плавающей точкой и типом boolean.

См. §4.2.5 для примера преобразования выражений с плавающей точкой в boolean.

Операторы над числами с плавающей точкой ведут себя в соответствии со спецификацией IEEE 754 (за исключением оператора остатка (§15.17.3)). В частности, язык программирования Java требует поддержки IEEE 754 денормированных чисел с плавающей точкой и постепенного переполнения, что облегчает доказательство желаемых свойств конкретных численных алгоритмов. Операции с плавающей точкой не "обнуляются", если вычисленное значение — денормированное число.

Язык программирования Java требует, чтобы арифметика с плавающей точкой работала так, как будто каждый оператор с плавающей точкой округлял свой результат до точности результата. Неточные результаты должны быть округлены до ближайшего представимого значения к бесконечно точному результату; если два ближайших представимых значения одинаково близки, выбирается значение с нулевым младшим битом. Это стандартное округлениe IEEE 754 по умолчанию, известное как округление до ближайшего.

Язык программирования Java использует округление к нулю при преобразовании значения с плавающей точкой в целое число (§5.1.3), которое в этом случае действует как усечение, отбрасывая биты мантиссы. Округление к нулю выбирает в результате форматное значение, наиболее близкое к бесконечно точному результату и не большее по величине.

Операция с плавающей точкой, которая переполняется, генерирует бесконечность со знаком.

Операция с плавающей точкой, которая недополняется, генерирует денормированное значение или нуль со знаком.

Операция с плавающей точкой, для которой нет математически определенного результата, генерирует NaN.

Все числовые операции с NaN в качестве операнда генерируют NaN в качестве результата.

Оператор с плавающей точкой может выбросить исключение (§11) по следующим причинам:

  • Любой оператор с плавающей точкой может выбросить NullPointerException, если требуется преобразование unboxing (§5.1.8) нулевого ссылочного значения.

  • Операторы инкремента и декремента ++ (§15.14.2, §15.15.1) и -- (§15.14.3, §15.15.2) могут выбросить OutOfMemoryError, если необходимо преобразование boxing (§5.1.7), и памяти недостаточно для выполнения преобразования.

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

class Test {
    public static void main(String[] args) {
        // An example of overflow:
        double d = 1e308;
        System.out.print("overflow produces infinity: ");
        System.out.println(d + "*10==" + d*10);
        // An example of gradual underflow:
        d = 1e-305 * Math.PI;
        System.out.print("gradual underflow: " + d + "\n   ");
        for (int i = 0; i < 4; i++)
            System.out.print(" " + (d /= 100000));
        System.out.println();
        // An example of NaN:
        System.out.print("0.0/0.0 is Not-a-Number: ");
        d = 0.0/0.0;
        System.out.println(d);
        // An example of inexact results and rounding:
        System.out.print("inexact results with float:");
        for (int i = 0; i < 100; i++) {
            float z = 1.0f / i;
            if (z * i != 1.0f)
                System.out.print(" " + i);
        }
        System.out.println();
        // Another example of inexact results and rounding:
        System.out.print("inexact results with double:");
        for (int i = 0; i < 100; i++) {
            double z = 1.0 / i;
            if (z * i != 1.0)
                System.out.print(" " + i);
        }
        System.out.println();
        // An example of cast to integer rounding:
        System.out.print("cast to int rounds toward 0: ");
        d = 12345.6;
        System.out.println((int)d + " " + (int)(-d));
    }
}

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

overflow produces infinity: 1.0e+308*10==Infinity
gradual underflow: 3.141592653589793E-305
3.1415926535898E-310 3.141592653E-315 3.142E-320 0.0
0.0/0.0 is Not-a-Number: NaN
inexact results with float: 0 41 47 55 61 82 83 94 97
inexact results with double: 0 49 98
cast to int rounds toward 0: 12345 -12345

Этот пример демонстрирует, среди прочего, что постепенное подпороговое переполнение может привести к постепенной потере точности.

Результаты, когда i равно 0, включают деление на ноль, так что z становится положительной бесконечностью, а z * 0 — NaN, которое не равно 1.0.


4.2.5. Тип boolean и значения boolean

Тип boolean представляет логическое значение с двумя возможными значениями, обозначаемыми литералами true и false (§3.10.3).

Операторы для boolean:

  • Операторы сравнения == и != (§15.21.2)

  • Оператор логического отрицания ! (§15.15.6)

  • Логические операторы &, ^ и | (§15.22.2)

  • Операторы логического И и ИЛИ && (§15.23) и || (§15.24)

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

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

Выражения boolean определяют порядок выполнения в нескольких видах операторов:

  • Оператор 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 может быть преобразовано в String путём преобразования в строку (§5.4).

Разрешается приведение значения типа boolean к типу boolean или Boolean (§5.1.1, §5.1.7). Другие приведения для типа boolean не допускаются.

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

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


ReferenceType:
ClassOrInterfaceType
TypeVariable
ArrayType

ClassOrInterfaceType:
ClassType
InterfaceType

ClassType:
TypeDeclSpecifier TypeArgumentsopt

InterfaceType:
TypeDeclSpecifier TypeArgumentsopt

TypeDeclSpecifier:
TypeName
ClassOrInterfaceType . Identifier

TypeName:
Identifier
TypeName . Identifier

TypeVariable:
Identifier

ArrayType:
Type [ ]

Пример кода:

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.5), либо типом класса или интерфейса, за которым следует "." и идентификатор. В последнем случае спецификатор имеет вид T.id, где id должен быть простым именем доступного (§6.6) типа-члена (§8.5, §9.5) типа T, в противном случае возникает ошибка компиляции. Спецификатор обозначает этот тип-член.

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

Например, объявление импорта одного типа import java.util.List; помещает простое имя типа List в область видимости в единице компиляции, так что параметризованные типы вида List<...> могут быть использованы. Еще один пример: вызов статического метода обобщенного класса требует только имени (возможно, квалифицированного) обобщенного класса без каких-либо аргументов типа, потому что такие аргументы типа не имеют отношения к статическому методу. (Сам метод может быть обобщенным и принимать свои собственные аргументы типа, но параметры типа статического метода обязательно не связаны с параметрами типа его содержащего обобщенного класса (§6.5.5)).

Из-за периодической необходимости использовать имя обобщенного класса или интерфейса без аргументов типа, имена типов отличаются от спецификаторов объявления типа. Имя типа всегда квалифицируется с помощью другого имени типа. В некоторых случаях это необходимо для доступа к внутреннему классу, который является членом параметризованного типа.

Вот пример, где спецификатор объявления типа отличается от имени типа:

class GenericOuter<T extends Number> {
    public class Inner<S extends Comparable<S>> {
        T getT() { return null;}
        S getS() { return null;}
    }
}

class Test {
    public static void main(String[] args) {
        GenericOuter<Integer>.Inner<Double> x1 = null;
        Integer i = x1.getT();
        Double d = x1.getS();
    }
}

Если мы обратимся к Inner, квалифицируя его именем типа, как в:

GenericOuter.Inner x2 = null;

мы заставим его использоваться как необработанный тип, потеряв информацию о типе.

4.3.1. Объекты

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

Значения ссылок (часто просто ссылки) являются указателями на эти объекты и специальной ссылкой null, которая не ссылается ни на один объект.

Экземпляр класса явно создается выражением создания экземпляра класса (§15.9).

Массив явно создается выражением создания массива (§15.10).

Новый экземпляр класса неявно создается, когда используется оператор конкатенации строк + (§15.18.1) в неконстантном (§15.28) выражении, что приводит к созданию нового объекта типа String (§4.3.3).

Новый объект массива неявно создается при вычислении выражения инициализатора массива (§10.6); это может произойти при инициализации класса или интерфейса (§12.4), при создании нового экземпляра класса (§15.9) или при выполнении оператора объявления локальной переменной (§14.4).

Новые объекты типов Boolean, Byte, Short, Character, Integer, Long, Float и Double могут неявно создаваться преобразованием упаковки (§5.1.7).

Пример 4.3.1-1. Создание объекта

class Point {
    int x, y;
    Point() { System.out.println("default"); }
    Point(int x, int y) { this.x = x; this.y = y; }

    /* A Point instance is explicitly created at 
       class initialization time: */
    static Point origin = new Point(0,0);

    /* A String can be implicitly created 
       by a + operator: */
    public String toString() { return "(" + x + "," + y + ")"; }
}

class Test {
    public static void main(String[] args) {
        /* A Point is explicitly created
           using newInstance: */
        Point p = null;
        try {
            p = (Point)Class.forName("Point").newInstance();
        } catch (Exception e) {
            System.out.println(e);
        }

        /* An array is implicitly created 
           by an array constructor: */
        Point a[] = { new Point(0,0), new Point(1,1) };

        /* Strings are implicitly created 
           by + operators: */
        System.out.println("p: " + p);
        System.out.println("a: { " + a[0] + ", " + a[1] + " }");
    
        /* An array is explicitly created
           by an array creation expression: */
        String sa[] = new String[2];
        sa[0] = "he"; sa[1] = "llo";
        System.out.println(sa[0] + sa[1]);
    }
}

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

default
p: (0,0)
a: { (0,0), (1,1) }
hello

Операторы на ссылках на объекты:

  • Доступ к полю, используя либо квалифицированное имя (§6.6), либо выражение доступа к полю (§15.11)

  • Вызов метода (§15.12)

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

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

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

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

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

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

Пример 4.3.1-2. Тождественность примитивных и ссылочных типов

class Value { int val; }

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

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

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

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


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

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 — искомый класс или интерфейс (§15.12.1) для getClass.

    Метод класса, объявленный synchronized (§8.4.3.6), синхронизируется на мониторе, связанном с объектом класса Class.

  • Метод hashCode очень полезен, вместе с методом equals, в хеш-таблицах, таких как java.util.Hashmap.

  • Методы wait, notify и notifyAll используются в многопоточном программировании с использованием потоков (§17.2).

  • Метод toString возвращает представление объекта в формате String.

4.3.3. Класс String

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

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

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

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

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

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

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

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

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

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

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

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

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

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


TypeParameter:
TypeVariable TypeBoundopt

TypeBound:
extends TypeVariable
extends ClassOrInterfaceType AdditionalBoundListopt

AdditionalBoundList:
AdditionalBound AdditionalBoundList
AdditionalBound

AdditionalBound:
& InterfaceType

Область видимости переменной типа, объявленной как параметр типа, указана в §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 mCDefault()   {}
    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.mCDefault();    // OK
        t.mCPrivate();    // Compile-time error
    } 
}

Переменная типа T имеет те же члены, что и тип пересечения C & I, который, в свою очередь, имеет те же члены, что и пустой класс CT, определенный в той же области видимости с эквивалентными надтипами. Члены интерфейса всегда public, и поэтому всегда наследуются (если не переопределены). Следовательно, mI является членом CT и T. Среди членов C все, кроме mCPrivate, наследуются CT и, следовательно, являются членами как CT, так и T.

Если C был объявлен в другом пакете, чем T, вызов mCDefault породил бы ошибку времени компиляции, поскольку этот член не был бы доступен в точке объявления T.


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

Декларация обобщенного класса или интерфейса C (§8.1.2, §9.1.2) с одним или несколькими параметрами типа A1,...,An, которые имеют соответствующие ограничения B1,...,Bn, определяет набор параметризованных типов, по одному для каждого возможного вызова раздела параметров типа.

Каждый параметризованный тип в наборе имеет вид C<T1,...,Tn>, где каждый аргумент типа Ti пробегает все типы, которые являются подтипами всех типов, указанных в соответствующем ограничении. То есть, для каждого типа ограничения Si в Bi, Ti является подтипом Si[F1:=T1,...,Fn:=Tn].

Параметризованный тип записывается как ТипКласса или ТипИнтерфейса, который содержит по меньшей мере один спецификатор объявления типа, непосредственно за которым следует список аргументов типа <T1,...,Tn>. Список аргументов типа обозначает конкретный вызов параметров типа обобщенного типа, указанного спецификатором объявления типа.

Учитывая спецификатор объявления типа, непосредственно за которым следует список аргументов типа, пусть C — конечный Идентификатор в спецификаторе.

Это ошибка времени компиляции, если C не является именем обобщенного класса или интерфейса или если количество аргументов типа в списке аргументов типа отличается от количества параметров типа C.

Пусть P = C<T1,...,Tn> — параметризованный тип. Должно быть так, что после применения преобразования захвата (§5.1.10) к P, в результате чего получается тип C<X1,...,Xn>, для каждого аргумента типа Xi (1 ≤ i ≤ n), Xi <: Bi[A1:=X1,...,An:=Xn] (§4.10), или возникает ошибка времени компиляции.

Запись [Ai:=Ti] обозначает подстановку переменной типа Ai типом Ti для 1 ≤ i ≤ n и используется в этом спецификаторе.

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

Примеры параметризованных типов:

  • Vector<String>

  • Seq<Seq<A>>

  • Seq<String>.Zipper<Integer>

  • Collection<Integer>

  • Pair<String,String>

Примеры неправильных вызовов обобщенного типа:

  • Vector<int> не допускается, так как примитивные типы не могут быть аргументами типа.

  • Pair<String> не допускается, так как недостаточно аргументов типа.

  • Pair<String,String,String> не допускается, так как слишком много аргументов типа.

Параметризованный тип может быть вызовом обобщенного класса или интерфейса, который вложен. Например, если необобщенный класс C имеет обобщенный вложенный класс D<T>, то C.D<Object> — параметризованный тип. И если обобщенный класс C<T> имеет необобщенный вложенный класс D, то тип члена C<String>.D является параметризованным типом, даже если класс D не обобщенный.

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

  • Они являются вызовами различных деклараций обобщенных типов.

  • Любой из их аргументов типа доказуемо различен.

4.5.1. Аргументы типов и подстановочные знаки

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


TypeArguments:
< TypeArgumentList >

TypeArgumentList:
TypeArgument
TypeArgumentList , TypeArgument

TypeArgument:
ReferenceType
Wildcard

Wildcard:
? WildcardBoundsopt

WildcardBounds:
extends ReferenceType
super ReferenceType

Пример 4.5.1-1. Подстановочные знаки

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

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

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

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

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


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


Подстановочные знаки могут иметь явные границы, как и обычные объявления переменных типа. Верхняя граница обозначается следующим синтаксисом, где B — граница:

? extends B

В отличие от обычных переменных типа, объявленных в сигнатуре метода, при использовании подстановочного знака не требуется вывод типа. Следовательно, разрешается объявлять нижние границы для подстановочного знака, используя следующий синтаксис, где B — нижняя граница:

? super B

Пример 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 является нижней границей для подстановочного знака.


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

  • Ни один из аргументов не является переменной типа или подстановочным знаком, и оба аргумента не являются одним и тем же типом.

  • Один аргумент типа — это переменная типа или подстановочный знак с верхней границей (из преобразования захвата, если необходимо) S; а другой аргумент типа T не является переменной типа или подстановочным знаком; и ни |S| <: |T| ни |T| <: |S|.

  • Каждый аргумент типа — это переменная типа или подстановочный знак с верхними границами (из преобразования захвата, если необходимо) S и T; и ни |S| <: |T| ни |T| <: |S|.

Говорят, что аргумент типа T1 содержит другой аргумент типа T2, записывается как T2 <= T1, если множество типов, обозначаемых T2, доказуемо является подмножеством множества типов, обозначаемых T1, по рефлексивному и транзитивному замыканию следующих правил (где <: обозначает субобразование (§4.10)):

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

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

  • T <= T

  • T <= ? extends T

  • T <= ? super T

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

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

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

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

Пусть C — объявление обобщённого класса или интерфейса с параметрами типа A1,...,An, и пусть C<T1,...,Tn> — вызов C, где для 1 ≤ i ≤ n, Ti являются типами (а не дикими картами). Тогда:

  • Пусть m — объявление элемента или конструктора (§8.2, §8.8.6) в C, тип которого по объявленному типу — T.

    Тип m в C<T1,...,Tn> равен T[A1:=T1,...,An:=Tn].

  • Пусть m — объявление элемента или конструктора в D, где D — класс, расширенный C, или интерфейс, реализованный C. Пусть D<U1,...,Uk> — супертип C<T1,...,Tn>, соответствующий D.

    Тип m в C<T1,...,Tn> — это тип m в D<U1,...,Uk>.

Если какие-либо аргументы типа в вызове C являются дикими картами, то:

  • Типы полей, методов и конструкторов в C<T1,...,Tn> — это типы полей, методов и конструкторов в преобразовании захвата (§5.1.9) C<T1,...,Tn>.

  • Пусть D — (возможно, обобщённое) объявление класса или интерфейса в C. Тогда тип D в C<T1,...,Tn> равен D, где, если D обобщённый, все аргументы типа являются неограниченными дикими картами.

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

Единственное исключение из предыдущего абзаца — когда параметризованный вложенный тип используется как выражение в операторе instanceof (§15.20.2), где преобразование захвата не применяется.

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.4) и тип возврата (§8.4.5) метода также подвергаются стиранию, если стирается сигнатура конструктора или метода.

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

4.7. Типы, подлежащие реификации

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

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

  • Он относится к объявлению негенерического класса или интерфейса.

  • Это параметризованный тип, в котором все аргументы типа являются неограниченными универсальными символами (§4.5.1).

  • Это примитивный тип (§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) или нестатического поля (§8.3) M необработанного типа C, который не унаследован от его суперклассов или суперинтерфейсов, — это необработанный тип, соответствующий стиранию его типа в дженеризованном объявлении, соответствующем C.

Тип статического метода или статического поля необработанного типа C совпадает с его типом в дженеризованном объявлении, соответствующем C.

Попытка передачи аргументов типа в не-дженеризованному члену типа необработанного типа, который не унаследован от его суперклассов или суперинтерфейсов, является ошибкой компиляции.

Попытка использовать член параметризованного типа как необработанный тип является ошибкой компиляции.

Это означает, что запрет на «редкие» типы распространяется на случай, когда квалифицирующий тип параметризован, но мы пытаемся использовать внутренний класс как необработанный тип:

Outer<Integer>.Inner x = null; // illegal

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

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

Использование необработанных типов разрешено только как уступка для совместимости устаревшего кода. Использование необработанных типов в коде, написанном после внедрения дженериков в язык программирования Java, настоятельно не рекомендуется. Возможно, в будущих версиях языка программирования Java использование необработанных типов будет запрещено.

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

  • При присваивании значению поля: если тип левой части оператора является необработанным типом, то предупреждение о неявном преобразовании во время компиляции возникает, если стирание изменяет тип поля.

  • При вызове метода или конструктора: если тип класса или интерфейса для поиска (§15.12.1) является необработанным типом, то предупреждение о неявном преобразовании во время компиляции возникает, если стирание изменяет любой из типов формальных параметров метода или конструктора.

  • Предупреждение о неявном преобразовании во время компиляции не возникает для вызова метода, когда типы формальных параметров не изменяются при стирании (даже если тип результата и/или throws фраза меняется), при чтении из поля или при создании экземпляра класса необработанного типа.

Обратите внимание, что вышеуказанные предупреждения о неявном преобразовании отличаются от предупреждений о неявном преобразовании, возможных при неявном преобразовании (§5.1.9), приведениях типов (§5.5.2), объявлениях методов (§8.4.1, §8.4.8.3, §8.4.8.4, §9.4.1.2) и вызовах методов с переменным числом аргументов (§15.12.4.2).

Здесь предупреждения охватывают случай, когда устаревший клиент использует дженеризованную библиотеку. Например, библиотека объявляет дженеризованный класс Foo<T extends String>, имеющий поле f типа Vector<T>, но клиент присваивает вектор целых чисел полю e.f, где e имеет необработанный тип Foo. Устаревший клиент получает предупреждение, потому что он может вызвать проблему с затоплением кучи (§4.12.2) для дженеризованных клиентов дженеризованной библиотеки.

(Обратите внимание, что устаревший клиент может присвоить Vector<String> из библиотеки своей переменной Vector без получения предупреждения. То есть, правила подтипирования (§4.10.2) языка программирования Java позволяют переменной необработанного типа принимать значения любого параметризованного экземпляра типа).

Предупреждения о неявном преобразовании охватывают двойной случай, когда дженеризованный клиент использует устаревшую библиотеку. Например, метод библиотеки имеет необработанный возвращаемый тип Vector, но клиент присваивает результат вызова метода переменной типа Vector<String>. Это небезопасно, так как у необработанного вектора мог быть другой тип элементов, отличный от String, но всё ещё разрешено с помощью неявного преобразования, чтобы обеспечить взаимодействие со устаревшим кодом. Предупреждение о неявном преобразовании указывает, что дженеризованный клиент может столкнуться с проблемами затопления кучи в других частях программы.

Пример 4.8-1. Необработанные типы

class Cell<E> {
    E value;

    Cell(E v)     { value = v; }
    E get()       { return value; }
    void set(E v) { value = v; }

    public static void main(String[] args) {
        Cell x = new Cell<String>("abc");
        System.out.println(x.value);  // OK, has type Object
        System.out.println(x.get());  // OK, has type Object
        x.set("def");                 // unchecked warning
    }
}

Пример 4.8-2. Необработанные типы и наследование

import java.util.*;
class NonGeneric {
    Collection<Number> myNumbers() { return null; }
}

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

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

В этой программе RawMembers<T> наследует метод:

Iterator<String> iterator()

от суперинтерфейса Collection<String>. Однако тип RawMembers наследует iterator() от стирания Collection<String>, что означает, что тип возвращаемого значения iterator() является стиранием Iterator<String>, Iterator.

В результате попытка присвоить значение rw.iterator() требует неявного преобразования (§5.1.9) от Iterator к Iterator<String>, вызывая выдачу предупреждения о неявном преобразовании.

В отличие от этого, статический член cng сохраняет свой полный параметризованный тип даже при доступе через объект необработанного типа. (Обратите внимание, что обращение к статическому члену через экземпляр считается плохим стилем и должно быть избегаемо.) Член myNumbers унаследован от класса NonGeneric (чьё стирание также является NonGeneric) и поэтому сохраняет свой полный параметризованный тип.


Необработанные типы тесно связаны с дикими картами. Оба основаны на экзистенциальных типах. Необработанные типы можно рассматривать как дикие карты, правила типа которых намеренно некорректны, чтобы обеспечить взаимодействие со устаревшим кодом. Исторически необработанные типы появились раньше диких карт; они были введены в GJ и описаны в статье Making the future safe for the past: Adding Genericity to the Java Programming Language авторами Gilad Bracha, Martin Odersky, David Stoutamire и Philip Wadler, в Proceedings of the ACM Conference on Object-Oriented Programming, Systems, Languages and Applications (OOPSLA 98), октябрь 1998 года.

4.9. Пересекающиеся типы

Тип пересечения имеет вид T1 & ... & Tn (n > 0), где Ti (1 ≤ i ≤ n) — выражения типов.

Пересекающиеся типы возникают в процессах преобразования захвата (§5.1.10) и вывода типов (§15.12.2.7). Невозможно напрямую записать тип пересечения в рамках программы; никакой синтаксис этого не поддерживает.

Значения типа пересечения — это те объекты, которые являются значениями всех типов Ti для 1 ≤ i ≤ n.

Члены типа пересечения T1 & ... & Tn определяются следующим образом:

  • Для каждого Ti (1 ≤ i ≤ n) пусть Ci — наиболее специфичный класс или массив типов, такой что Ti <: Ci. Тогда обязательно должен существовать какой-то Tk <: Ck, такой что Ck <: Ci для любого i (1 ≤ i ≤ n), иначе произойдёт ошибка на этапе компиляции.

  • Для 1 ≤ j ≤ n, если Tj — переменная типа, то пусть Tj' — интерфейс, члены которого совпадают с общедоступными членами Tj; в противном случае, если Tj — интерфейс, то пусть Tj' — Tj.

  • Тогда тип пересечения имеет те же члены, что и тип класса (§8) с пустым телом, прямым суперклассом 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<F1,...,Fn>, непосредственные надтипы параметризованного типа C<T1,...,Tn> включают в себя всё следующее:

  • Непосредственные суперклассы C.

  • Непосредственные суперинтерфейсы C.

  • Тип Object, если C — тип интерфейса без непосредственных суперинтерфейсов.

  • Исходный тип C.

Непосредственные надтипы параметризованного типа C<T1,...,Tn>, где Ti (1 ≤ i ≤ n) — тип, включают в себя всё следующее:

  • D<U1 θ,...,Uk θ>, где D<U1,...,Uk> — непосредственный надтип C<T1,...,Tn>, и θ — подстановка [F1:=T1,...,Fn:=Tn].

  • C<S1,...,Sn>, где Si содержит Ti (1 ≤ i ≤ n) (§4.5.1).

Непосредственные надтипы параметризованного типа C<R1,...,Rn>, где по крайней мере один из Ri (1 ≤ i ≤ n) — аргумент типа-подстановка, являются непосредственными надтипами C<X1,...,Xn>, который является результатом применения преобразования захвата (§5.1.10) к C<R1,...,Rn>.

Непосредственные надтипы пересечения типов 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.11. Где используются типы

Типы используются, когда они появляются в объявлениях или в определённых выражениях.

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

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

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

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

  • Импортированные типы (§7.5); здесь тип Random, импортированный из типа java.util.Random пакета java.util, объявляется

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

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

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

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

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

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

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

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

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

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

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

  • Создание массива (§15.10); здесь локальная переменная 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.9, §5.5.2, §8.4.1, §8.4.8.3, §8.4.8.4, §9.4.1.2, §15.12.4.2), или если программа использует alias переменной массива с нереализуемым типом элемента через переменную массива супертипа, которая является необработанной или непараметризованной.

Например, код:

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.3.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). Элементы массива фактически перестают существовать, когда на массив больше нет ссылок.

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

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

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

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

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

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

  7. Локальные переменные объявляются операторами объявления локальных переменных (§14.4).

    Всякий раз, когда поток управления входит в блок (§14.2) или оператор for (§14.14), для каждой локальной переменной, объявленной в операторе объявления локальной переменной, находящегося непосредственно внутри этого блока или оператора for, создается новая переменная.

    Оператор объявления локальной переменной может содержать выражение, которое инициализирует переменную. Однако локальная переменная с инициализирующим выражением не инициализируется до тех пор, пока не выполнится оператор объявления локальной переменной, который ее объявляет. (Правила определенного присваивания (§16) предотвращают использование значения локальной переменной до тех пор, пока оно не будет инициализировано или иным образом присвоено.) Локальная переменная фактически перестает существовать, когда завершается выполнение блока или оператора for.

    Если бы не было одного исключительного случая, локальную переменную всегда можно было бы считать созданной при выполнении оператора объявления локальной переменной. Исключение связано с оператором switch (§14.11), где возможно, что управление войдет в блок, но минует выполнение оператора объявления локальной переменной. Однако из-за ограничений, накладываемых правилами определенного присваивания (§16), локальная переменная, объявленная таким пропущенным оператором объявления локальной переменной, не может быть использована до тех пор, пока не будет явно присвоено значение выражением присваивания (§15.26).

Пример 4.12.3-1. Разные типы переменных

class Point {
    static int numPoints;   // numPoints is a class variable
    int x, y;               // x and y are instance variables
    int[] w = new int[10];  // w[0] is an array component
    int setX(int x) {       // x is a method parameter
        int oldx = this.x;  // oldx is a local variable
        this.x = x;
        return oldx;
    }
}

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

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

Это ошибка времени компиляции, если переменная типа final присваивается значение, если она не явным образом не инициализирована (§16) непосредственно перед присваиванием.

A пустая final - это final переменная, объявление которой не содержит инициализатора.

После того, как переменная типа final была присвоена значение, она всегда содержит то же значение. Если переменная типа final ссылается на объект, то состояние объекта может быть изменено операциями над объектом, но переменная всегда будет ссылаться на тот же объект.

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

Пример 4.12.4-1. Переменные 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 координаты.


Переменная примитивного типа или типа String, которая является final и инициализируется константным выражением времени компиляции (§15.28), называется константной переменной.

То, является ли переменная константной переменной или нет, может иметь последствия в отношении инициализации класса (§12.4.1), бинарной совместимости (§13.1, §13.4.9) и определённого присваивания (§16).

Ресурс оператора try-с-ресурсами (§14.20.3) и параметр исключения много-catch блока (§14.20) неявно объявляются final.

Параметр исключения одно-catch блока (§14.20) может быть эффективно final вместо того, чтобы быть явно объявлено final. Такой параметр никогда не объявляется неявно final.

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

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

  • Каждая переменная класса, экземпляра или элемент массива инициализируется стандартным значением при создании (§15.9, §15.10):

    • Для типа byte, стандартное значение равно нулю, то есть, значению (byte)0.

    • Для типа short, стандартное значение равно нулю, то есть, значению (short)0.

    • Для типа int, стандартное значение равно нулю, то есть, 0.

    • Для типа long, стандартное значение равно нулю, то есть, 0L.

    • Для типа float, стандартное значение равно положительному нулю, то есть, 0.0f.

    • Для типа double, стандартное значение равно положительному нулю, то есть, 0.0d.

    • Для типа char, стандартное значение равно нулевому символу, то есть, '\u0000'.

    • Для типа boolean, стандартное значение равно false.

    • Для всех типов ссылок (§4.3), стандартное значение равно null.

  • Каждый параметр метода (§8.4.1) инициализируется соответствующим значением аргумента, предоставленным вызывающим методом (§15.12).

  • Каждый параметр конструктора (§8.8.1) инициализируется соответствующим значением аргумента, предоставленным выражением создания экземпляра класса (§15.9) или явным вызовом конструктора (§8.8.7).

  • Параметр исключения (§14.20) инициализируется объектом исключения, сгенерированным исключением (§11.3, §14.18).

  • Локальная переменная (§14.4, §14.14) должна быть явно присвоена значение до использования, либо инициализацией (§14.4), либо присваиванием (§15.26), способом, который может быть проверен с помощью правил определения присваивания (§16).

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

class Point {
    static int npoints;
    int x, y;
    Point root;
}

class Test {
    public static void main(String[] args) {
        System.out.println("npoints=" + Point.npoints);
        Point p = new Point();
        System.out.println("p.x=" + p.x + ", p.y=" + p.y);
        System.out.println("p.root=" + p.root);
    }
}

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

npoints=0
p.x=0, p.y=0
p.root=null

иллюстрируя начальную инициализацию npoints, которая происходит при подготовке класса Point (§12.3.2), и начальную инициализацию x, y, и root, которые происходят при создании нового экземпляра Point. См. §12 для полного описания всех аспектов загрузки, связывания и инициализации классов и интерфейсов, а также описания создания экземпляров классов для создания новых экземпляров класса.


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, авторы Sheng Liang и Gilad Bracha, в Труды OOPSLA'98, опубликованные как ACM SIGPLAN Notices, том 33, номер 10, октябрь 1998 г., страницы 36-44, и в Спецификация виртуальной машины Java, издание Java SE 7.

  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