Spec-Zone.ru › Java Language Specification 7

Глава 5. Преобразования и повышения типов

Содержание

5.1. Виды преобразований
5.1.1. Тождественное преобразование
5.1.2. Расширяющее преобразование примитивных типов
5.1.3. Сужающее преобразование примитивных типов
5.1.4. Расширяющее и сужающее преобразование примитивных типов
5.1.5. Расширяющее преобразование ссылочных типов
5.1.6. Сужающее преобразование ссылочных типов
5.1.7. Преобразование в обёртку
5.1.8. Преобразование из обёртки
5.1.9. Преобразование без проверки
5.1.10. Преобразование захвата
5.1.11. Преобразование в строку
5.1.12. Запрещённые преобразования
5.1.13. Преобразование множества значений
5.2. Преобразование при присваивании
5.3. Преобразование при вызове метода
5.4. Преобразование в строку
5.5. Преобразование при приведении типов
5.5.1. Приведение типов ссылочных типов
5.5.2. Проверяемые и непроверяемые приведения
5.5.3. Проверяемые приведения во время выполнения
5.6. Повышение числовых типов
5.6.1. Повышение числового типа в унарных операциях
5.6.2. Повышение числового типа в бинарных операциях

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

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

Пример 5.0-1. Преобразования во время компиляции и во время выполнения

  • Преобразование из типа Object в тип Thread требует проверки во время выполнения, чтобы убедиться, что значение во время выполнения является фактически экземпляром класса Thread или одного из его подклассов; если нет, выбрасывается исключение.

  • Преобразование из типа Thread в тип Object не требует действий во время выполнения; Thread является подклассом Object, поэтому любая ссылка, созданная выражением типа Thread, является допустимой ссылкой типа Object.

  • Преобразование из типа int в тип long требует расширения знака во время выполнения 32-битного целочисленного значения до 64-битного представления long. Информация не теряется.

  • Преобразование из типа double в тип long требует нетривиального перевода 64-битового значения с плавающей запятой в 64-битовое целое представление. В зависимости от фактического значения во время выполнения, информация может быть потеряна.


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

  • Тождественные преобразования

  • Расширяющие преобразования примитивных типов

  • Сужающие преобразования примитивных типов

  • Расширяющие преобразования ссылочных типов

  • Сужающие преобразования ссылочных типов

  • Преобразования в обёртку

  • Преобразования из обёртки

  • Преобразования без проверки

  • Преобразования захвата

  • Преобразования в строку

  • Преобразования множества значений

Существует пять контекстов преобразований, в которых может происходить преобразование выражений. Каждый контекст допускает преобразования в некоторых из перечисленных выше категорий, но не в других. Термин «преобразование» также используется для описания процесса выбора конкретного преобразования для такого контекста. Например, мы говорим, что выражение, являющееся фактическим аргументом в вызове метода, подвергается «преобразованию при вызове метода», что означает, что конкретное преобразование будет неявным образом выбрано для этого выражения в соответствии с правилами контекста аргумента вызова метода.

Один контекст преобразования — это операнд числового оператора, такого как + или *. Процесс преобразования для таких операндов называется повышением числовых типов. Повышение является специальным в том, что в случае бинарных операторов выбранное преобразование для одного операнда может частично зависеть от типа другого операнда-выражения.

В этой главе сначала описываются одиннадцать категорий преобразований (§5.1), включая специальные преобразования в String, разрешенные для оператора конкатенации строк + (§15.18.1). Затем описываются пять контекстов преобразований:

  • Преобразование при присваивании (§5.2, §15.26) преобразует тип выражения в тип указанной переменной.

    Преобразование при присваивании может привести к возникновению OutOfMemoryError (в результате преобразования в обёртку (§5.1.7)), NullPointerException (в результате преобразования из обёртки (§5.1.8)) или ClassCastException (в результате непроверяемого преобразования (§5.1.9)) для выбрасывания во время выполнения.

  • Преобразование при вызове метода (§5.3, §15.9, §15.12) применяется к каждому аргументу в вызове метода или конструктора и, за исключением одного случая, выполняет те же преобразования, что и преобразование при присваивании.

    Преобразование при вызове метода может привести к возникновению OutOfMemoryError (в результате преобразования в обёртку (§5.1.7)), NullPointerException (в результате преобразования из обёртки (§5.1.8)) или ClassCastException (в результате непроверяемого преобразования (§5.1.9)) для выбрасывания во время выполнения.

  • Преобразование при приведении типов (§5.5) преобразует тип выражения в тип, явно указанный оператором приведения (§15.16).

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

  • Преобразование в строку (§5.4) применяется только к операнду бинарного оператора +, который не является String, когда другой операнд является String.

    Преобразование в строку может вызвать OutOfMemoryError (в результате создания экземпляра класса (§12.5)) для выбрасывания во время выполнения.

  • Повышение числовых типов (§5.6) приводит операнды числового оператора к общему типу, чтобы операция могла быть выполнена.

Пример 5.0-2. Контексты преобразований

class Test {			
    public static void main(String[] args) {
        // Casting conversion (5.4) of a float literal to
        // type int. Without the cast operator, this would
        // be a compile-time error, because this is a
        // narrowing conversion (5.1.3):
        int i = (int)12.5f;
        // String conversion (5.4) of i's int value:
        System.out.println("(int)12.5f==" + i);
        // Assignment conversion (5.2) of i's value to type
        // float. This is a widening conversion (5.1.2):
        float f = i;
        // String conversion of f's float value:
        System.out.println("after float widening: " + f);
        // Numeric promotion (5.6) of i's value to type
        // float. This is a binary numeric promotion.
        // After promotion, the operation is float*float:
        System.out.print(f);
        f = f * i;
        // Two string conversions of i and f:
        System.out.println("*" + i + "==" + f);
        // Method invocation conversion (5.3) of f's value
        // to type double, needed because the method Math.sin
        // accepts only a double argument:
        double d = Math.sin(f);
        // Two string conversions of f and d:
        System.out.println("Math.sin(" + f + ")==" + d);
    }
}

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

(int)12.5f==12
after float widening: 12.0
12.0*12==144.0
Math.sin(144.0)==-0.49102159389846934

5.1. Типы преобразований

Конкретные типы преобразований в языке программирования Java делятся на 13 категорий.

5.1.1. Преобразование тождества

Преобразование типа в тот же тип разрешено для любого типа.

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

5.1.2. Расширяющее преобразование примитивных типов

19 конкретных преобразований примитивных типов называются расширяющими преобразованиями примитивных типов:

  • byte к short, int, long, float или double

  • short к int, long, float или double

  • char к int, long, float или double

  • int к long, float или double

  • long к float или double

  • float к double

Расширяющее преобразование примитивных типов не теряет информации о величине числового значения.

Расширяющее преобразование примитивного типа от целочисленного типа к другому целочисленному типу или от float к double в выражении strictfp (§15.4) не теряет никакой информации; числовое значение сохраняется точно.

Расширяющее преобразование примитивного типа от float к double, которое не является strictfp, может потерять информацию о величине преобразованного значения.

Расширяющее преобразование значения int или long к float, или значения long к double, может привести к потере точности — то есть результат может потерять некоторые младшие биты значения. В этом случае результирующее значение с плавающей точкой будет правильно округленным вариантом целочисленного значения с использованием режима округления до ближайшего значения IEEE 754 (§4.2.4).

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

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

Несмотря на то, что может произойти потеря точности, расширяющее преобразование примитивного типа никогда не приводит к исключению во время выполнения (§11.1.1).

Пример 5.1.2-1. Расширяющее преобразование примитивных типов

class Test {
    public static void main(String[] args) {
        int big = 1234567890;
        float approx = big;
        System.out.println(big - (int)approx);
    }
}

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

-46

что указывает на то, что информация была потеряна во время преобразования из типа int в тип float, потому что значения типа float не точны до девяти значащих цифр.


5.1.3. Преобразование примитивных типов сужения

22 определённых преобразования примитивных типов называются преобразованиями примитивных типов сужения:

  • short к byte или char

  • char к byte или short

  • int к byte, short или char

  • long к byte, short, char или int

  • float к byte, short, char, int или long

  • double к byte, short, char, int, long или float

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

Преобразование примитивных типов сужения из double в float регулируется правилами округления IEEE 754 (§4.2.4). Это преобразование может привести к потере точности, но также и к потере диапазона, что может привести к нулю float из ненулевого double и к бесконечности float из конечной double. double NaN преобразуется в float NaN, а double бесконечность преобразуется в бесконечность с тем же знаком float.

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

Преобразование сужения char в целочисленный тип T аналогичным образом просто отбрасывает все, кроме n младших разрядов, где n — количество разрядов, используемых для представления типа T. Помимо возможной потери информации о величине числового значения, это может привести к тому, что результирующее значение будет отрицательным, даже если символы представляют 16-разрядные беззнаковые целые значения.

Преобразование сужения числа с плавающей запятой в целочисленный тип T выполняется в два шага:

  1. На первом шаге число с плавающей запятой преобразуется либо в long, если T является long, либо в int, если T является byte, short, char или int, следующим образом:

    • Если число с плавающей запятой является NaN (§4.2.3), результат первого шага преобразования — это int или long 0.

    • В противном случае, если число с плавающей запятой не является бесконечностью, значение с плавающей запятой округляется до целого значения V, округляя до ближайшего целого значения с использованием режима округления IEEE 754 к нулю (§4.2.3). Затем есть два случая:

      1. Если T является long, и это целое значение может быть представлено как long, то результат первого шага — это long значение V.

      2. В противном случае, если это целое значение может быть представлено как int, то результат первого шага — это int значение V.

    • В противном случае должен быть истинным один из следующих двух случаев:

      1. Значение должно быть слишком маленьким (отрицательное значение большой величины или отрицательная бесконечность), и результат первого шага — это наименьшее представимое значение типа int или long.

      2. Значение должно быть слишком большим (положительное значение большой величины или положительная бесконечность), и результат первого шага — это наибольшее представимое значение типа int или long.

  2. На втором шаге:

    • Если T является int или long, результат преобразования — результат первого шага.

    • Если T является byte, char или short, результат преобразования — результат преобразования сужения к типу T (§5.1.3) результата первого шага.

Пример 5.1.3-1. Преобразование примитивных типов сужения

class Test {
    public static void main(String[] args) {
        float fmin = Float.NEGATIVE_INFINITY;
        float fmax = Float.POSITIVE_INFINITY;
        System.out.println("long: " + (long)fmin +
                           ".." + (long)fmax);
        System.out.println("int: " + (int)fmin +
                           ".." + (int)fmax);
        System.out.println("short: " + (short)fmin +
                           ".." + (short)fmax);
        System.out.println("char: " + (int)(char)fmin +
                           ".." + (int)(char)fmax);
        System.out.println("byte: " + (byte)fmin +
                           ".." + (byte)fmax);
    }
}

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

long: -9223372036854775808..9223372036854775807
int: -2147483648..2147483647
short: 0..-1
char: 0..65535
byte: 0..-1

Результаты для char, int и long предсказуемы, они выдают минимальные и максимальные представимые значения типа.

Результаты для byte и short теряют информацию о знаке и величине числовых значений, а также теряют точность. Результаты можно понять, изучив младшие разряды минимальных и максимальных int. Минимальное int — это, в шестнадцатеричном формате, 0x80000000, а максимальное int — 0x7fffffff. Это объясняет результаты short, которые представляют младшие 16 разрядов этих значений, а именно 0x0000 и 0xffff; это объясняет результаты char, которые также представляют младшие 16 разрядов этих значений, а именно '\u0000' и '\uffff'; и это объясняет результаты byte, которые представляют младшие 8 разрядов этих значений, а именно 0x00 и 0xff.


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

Пример 5.1.3-2. Преобразования примитивных типов сужения, теряющие информацию

class Test {
    public static void main(String[] args) {
        // A narrowing of int to short loses high bits:
        System.out.println("(short)0x12345678==0x" +
                           Integer.toHexString((short)0x12345678));
        // An int value too big for byte changes sign and magnitude:
        System.out.println("(byte)255==" + (byte)255);
        // A float value too big to fit gives largest int value:
        System.out.println("(int)1e20f==" + (int)1e20f);
        // A NaN converted to int yields zero:
        System.out.println("(int)NaN==" + (int)Float.NaN);
        // A double value too large for float yields infinity:
        System.out.println("(float)-1e100==" + (float)-1e100);
        // A double value too small for float underflows to zero:
        System.out.println("(float)1e-50==" + (float)1e-50);
    }
}

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

(short)0x12345678==0x5678
(byte)255==-1
(int)1e20f==2147483647
(int)NaN==0
(float)-1e100==-Infinity
(float)1e-50==0.0

5.1.4. Увеличение и уменьшение преобразования примитивных типов

Следующее преобразование объединяет как увеличение, так и уменьшение преобразования примитивных типов:

  • byte к char

Сначала byte преобразуется в int с помощью преобразования примитивных типов увеличения (§5.1.2), а затем результирующее int преобразуется в char с помощью преобразования примитивных типов сужения (§5.1.3).

5.1.5. Увеличение преобразования ссылок

Преобразование увеличения ссылки существует от любого типа ссылки S к любому типу ссылки T, при условии, что S является подтипом (§4.10) T.

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

5.1.6. Сужение преобразования ссылок

Шесть видов преобразований называются сужением преобразования ссылок:

  • От любого ссылочного типа S к любому ссылочному типу T, при условии, что S является надтипом для T (§4.10).

    Важным частным случаем является сужение преобразования ссылок из типа класса Object в любой другой ссылочный тип (§4.12.4).

  • От любого типа класса C к любому непараметризованному типу интерфейса K, при условии, что C не final и не реализует K.

  • От любого типа интерфейса J к любому непараметризованному типу класса C, который не final.

  • От любого типа интерфейса J к любому непараметризованному типу интерфейса K, при условии, что J не является подунтерфейсом K.

  • От типов интерфейса Cloneable и java.io.Serializable к любому типу массива T[].

  • От любого типа массива SC[] к любому типу массива TC[], при условии, что SC и TC являются ссылочными типами, и существует сужение преобразования ссылок от SC к TC.

Такие преобразования требуют проверки во время выполнения, чтобы выяснить, является ли фактическое значение ссылки допустимым значением нового типа. Если нет, то выбрасывается ClassCastException.

5.1.7. Преобразование в оболочку (Boxing Conversion)

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

  • От типа boolean к типу Boolean

  • От типа byte к типу Byte

  • От типа short к типу Short

  • От типа char к типу Character

  • От типа int к типу Integer

  • От типа long к типу Long

  • От типа float к типу Float

  • От типа double к типу Double

  • От нулевого типа к нулевому типу

    Это правило необходимо, потому что условный оператор (§15.25) применяет преобразование в оболочку к типам своих операндов и использует результат в дальнейших вычислениях.

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

  • Если p является значением типа boolean, то преобразование в оболочку преобразует p в ссылку r класса и типа Boolean, такую что r.booleanValue() == p

  • Если p является значением типа byte, то преобразование в оболочку преобразует p в ссылку r класса и типа Byte, такую что r.byteValue() == p

  • Если p является значением типа char, то преобразование в оболочку преобразует p в ссылку r класса и типа Character, такую что r.charValue() == p

  • Если p является значением типа short, то преобразование в оболочку преобразует p в ссылку r класса и типа Short, такую что r.shortValue() == p

  • Если p является значением типа int, то преобразование в оболочку преобразует p в ссылку r класса и типа Integer, такую что r.intValue() == p

  • Если p является значением типа long, то преобразование в оболочку преобразует p в ссылку r класса и типа Long, такую что r.longValue() == p

  • Если p является значением типа float, то:

    • Если p не NaN, то преобразование в оболочку преобразует p в ссылку r класса и типа Float, такую что r.floatValue() вычисляется как p

    • В противном случае преобразование в оболочку преобразует p в ссылку r класса и типа Float, такую что r.isNaN() вычисляется как true

  • Если p является значением типа double, то:

    • Если p не NaN, то преобразование в оболочку преобразует p в ссылку r класса и типа Double, такую что r.doubleValue() вычисляется как p

    • В противном случае преобразование в оболочку преобразует p в ссылку r класса и типа Double, такую что r.isNaN() вычисляется как true

  • Если p является значением любого другого типа, преобразование в оболочку эквивалентно преобразованию идентичности (§5.1.1).

Если значение p, которое заключается в оболочку, является true, false, byte, или char в диапазоне \u0000 к \u007f, или int или short число между -128 и 127 (включительно), то пусть r1 и r2 будут результатами любых двух преобразований в оболочку p. Всегда имеет место то, что r1 == r2.

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

Это гарантирует, что в большинстве распространенных случаев поведение будет желаемым, не налагая чрезмерных штрафов за производительность, особенно на небольших устройствах. Менее ограниченные по памяти реализации могут, например, кэшировать все char и short значения, а также int и long значения в диапазоне от -32K до +32K.

Преобразование в оболочку может привести к OutOfMemoryError, если необходимо выделить новый экземпляр одного из классов-оболочек (Boolean, Byte, Character, Short, Integer, Long, Float, или Double) и недостаточно памяти.

5.1.8. Преобразование распаковки

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

  • Из типа Boolean в тип boolean

  • Из типа Byte в тип byte

  • Из типа Short в тип short

  • Из типа Character в тип char

  • Из типа Integer в тип int

  • Из типа Long в тип long

  • Из типа Float в тип float

  • Из типа Double в тип double

Во время выполнения преобразование распаковки выполняется следующим образом:

  • Если r является ссылкой типа Boolean, то преобразование распаковки преобразует r в r.booleanValue()

  • Если r является ссылкой типа Byte, то преобразование распаковки преобразует r в r.byteValue()

  • Если r является ссылкой типа Character, то преобразование распаковки преобразует r в r.charValue()

  • Если r является ссылкой типа Short, то преобразование распаковки преобразует r в r.shortValue()

  • Если r является ссылкой типа Integer, то преобразование распаковки преобразует r в r.intValue()

  • Если r является ссылкой типа Long, то преобразование распаковки преобразует r в r.longValue()

  • Если r является ссылкой типа Float, преобразование распаковки преобразует r в r.floatValue()

  • Если r является ссылкой типа Double, то преобразование распаковки преобразует r в r.doubleValue()

  • Если r является null, преобразование распаковки генерирует исключение NullPointerException

Тип считается преобразуемым в числовой тип, если он является числовым типом (§4.2) или это ссылочный тип, который может быть преобразован в числовой тип с помощью преобразования распаковки.

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

5.1.9. Непроверенное преобразование

Пусть G обозначает объявление обобщённого типа с n параметрами типа.

Существует непроверенное преобразование от примитивного класса или интерфейса типа (§4.8) G к любому параметризованному типу вида G<T1,...,Tn>.

Существует непроверенное преобразование от примитивного массива типа G[] к любому массиву типа вида G<T1,...,Tn>[].

Использование непроверенного преобразования вызывает предупреждение о непроверенном преобразовании во время компиляции, если G<...> — это параметризованный тип, в котором все аргументы типа являются не ограниченными универсальными символами (§4.5.1), или предупреждение о непроверенном преобразовании подавляется аннотацией SuppressWarnings (§9.6.3.5).

Непроверенное преобразование используется для обеспечения плавного взаимодействия устаревшего кода, написанного до введения обобщённых типов, с библиотеками, которые были преобразованы для использования обобщённости (процесс, который мы называем генерацией). В таких случаях (в первую очередь, клиенты Collections Framework в java.util), устаревший код использует примитивные типы (например, Collection вместо Collection<String>). Выражения примитивных типов передаются в качестве аргументов методам библиотек, которые используют параметризованные версии тех же самых типов в качестве типов соответствующих формальных параметров.

Такие вызовы не могут быть признаны статически безопасными в рамках системы типов, использующей обобщённые типы. Отклонение таких вызовов привело бы к недействительности больших объёмов существующего кода и помешало бы им использовать новые версии библиотек. В свою очередь, это бы удерживало поставщиков библиотек от использования обобщённости. Чтобы предотвратить такое нежелательное развитие событий, примитивный тип может быть преобразован в произвольное обращение к обобщённому типу объявления, на который ссылается примитивный тип. Хотя преобразование не является корректным, оно допускается как уступка практичности. В таких случаях выдаётся предупреждение о непроверенном преобразовании.

5.1.10. Преобразование захвата

Пусть G обозначает объявление универсального типа (§8.1.2, §9.1.2) с n параметрами типа A1,...,An и соответствующими ограничениями U1,...,Un.

Существует преобразование захвата из параметризованного типа G<T1,...,Tn> (§4.5) в параметризованный тип G<S1,...,Sn>, где для 1 ≤ i ≤ n :

  • Если Ti — аргумент типа с подстановкой (§4.5.1) в форме ?, то Si — свежая переменная типа с верхним ограничением Ui[A1:=S1,...,An:=Sn] и нижним ограничением нулевой тип (§4.1).

  • Если Ti — аргумент типа с подстановкой в форме ? extends Bi, то Si — свежая переменная типа с верхним ограничением glb(Bi, Ui[A1:=S1,...,An:=Sn]) и нижним ограничением нулевой тип.

    glb(V1,...,Vm) определяется как V1 & ... & Vm.

    Это ошибка времени компиляции, если для любых двух классов (не интерфейсов) Vi и Vj Vi не является подклассом Vj или наоборот.

  • Если Ti — аргумент типа с подстановкой в форме ? super Bi, то Si — свежая переменная типа с верхним ограничением Ui[A1:=S1,...,An:=Sn] и нижним ограничением Bi.

  • В противном случае Si = Ti.

Преобразование захвата для любого типа, отличного от параметризованного типа (§4.5), действует как преобразование тождества (§5.1.1).

Преобразование захвата не применяется рекурсивно.

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

Преобразование захвата разработано для повышения полезности подстановок. Чтобы понять мотивацию, давайте начнем с рассмотрения метода java.util.Collections.reverse():


public static void reverse(List<?> list);

Метод инвертирует переданный в качестве параметра список. Он работает для любого типа списка, поэтому использование типа подстановки List<?> в качестве типа формального параметра полностью оправдано.

Теперь рассмотрим, как реализовать reverse():


public static void reverse(List<?> list) { rev(list); }
private static <T> void rev(List<T> list) {
    List<T> tmp = new ArrayList<T>(list);
    for (int i = 0; i < list.size(); i++) {
        list.set(i, tmp.get(list.size() - i - 1));
    }
}

Для реализации требуется скопировать список, извлечь элементы из копии и вставить их в исходный. Для обеспечения типабезопасности нам необходимо присвоить имя T типу элементов входного списка. Это делается в приватном служебном методе rev(). Это требует передачи входного списка типа List<?> в качестве аргумента в rev(). В общем случае List<?> — это список неизвестного типа. Он не является подтипом List<T> для любого типа T. Допускать такое отношение подтипизации было бы некорректно. Учитывая метод:


public static <T> void fill(List<T> l, T obj)

следующий код подорвал бы систему типов:


List<String> ls = new ArrayList<String>();
List<?> l = ls;
Collections.fill(l, new Object());  // not legal - but assume it was!
String s = ls.get(0); // ClassCastException - ls contains
                      // Objects, not Strings.

Таким образом, без специальных привилегий вызов из reverse() в rev() был бы запрещен. В этом случае автору reverse() пришлось бы писать его сигнатуру как:


public static <T> void reverse(List<T> list)

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

Вызов из reverse() в rev() на самом деле безвреден, но его нельзя оправдать на основе общего отношения подтипизации между List<?> и List<T>. Вызов безвреден, потому что входной аргумент, несомненно, является списком определенного типа (хотя и неизвестного). Если мы можем захватить этот неизвестный тип в переменной типа X, мы можем сделать вывод, что T соответствует X. В этом суть преобразования захвата. Спецификация, конечно, должна учитывать сложности, такие как нетривиальные (и, возможно, рекурсивно определенные) верхние или нижние границы, наличие нескольких аргументов и т. д.

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

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

Классическая операция open над экзистенциалами требует, чтобы захваченная переменная типа не выходила за пределы открытого выражения. Преобразование захвата, соответствующее open, всегда находится в достаточно большой области, чтобы захваченная переменная типа никогда не была видна за пределами этой области. Преимущество этой схемы заключается в том, что нет необходимости в операции close, как определено в статье О подтипизации на основе вариативности для параметризованных типов Ацуши Игараси и Мирко Вироли в материалах 16-й Европейской конференции по объектно-ориентированному программированию (ECOOP 2002). Подробнее о подстановках см. в Дикий FJ Мадса Торгерсена, Эрика Эрнста и Кристиана Плеснера Хансена в 12-м семинаре по основам объектно-ориентированного программирования (FOOL 2005).

5.1.11. Преобразование строк

Любой тип можно преобразовать в тип String с помощью преобразования строк.

Значение x примитивного типа T сначала преобразуется в значение ссылки, как если бы оно было передано в качестве аргумента в выражение создания экземпляра соответствующего класса (§15.9):

  • Если T — boolean, то используйте new Boolean(x).

  • Если T — char, то используйте new Character(x).

  • Если T — byte, short или int, то используйте new Integer(x).

  • Если T — long, то используйте new Long(x).

  • Если T — float, то используйте new Float(x).

  • Если T — double, то используйте new Double(x).

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

Теперь следует учитывать только значения ссылок:

  • Если ссылка — null, она преобразуется в строку "null" (четыре символа ASCII n, u, l, l).

  • В противном случае преобразование выполняется, как если бы было вызван метод toString со ссылкой на объект без аргументов; но если результат вызова метода toString равен null, используется строка "null".

    Метод toString определен базовым классом Object (§4.3.2). Многие классы его переопределяют, в частности, Boolean, Character, Integer, Long, Float, Double и String.

Подробности преобразования строк см. в §5.4.

5.1.12. Запрещённые преобразования

Любое преобразование, которое явно не разрешено, запрещено.

5.1.13. Преобразование набора значений

Преобразование набора значений — это процесс сопоставления значения с плавающей точкой из одного набора значений другому без изменения его типа.

В выражении, которое не является FP-строгим (§15.4), преобразование набора значений предоставляет варианты реализации языка программирования Java:

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

  • Если значение является элементом набора значений с расширенным порядком экспоненты чисел с двойной точностью, реализация может по своему выбору сопоставить значение ближайшему элементу набора значений чисел с двойной точностью. Это преобразование может привести к переполнению (в этом случае значение заменяется бесконечностью того же знака) или подполнению (в этом случае значение может потерять точность, так как оно заменяется ненормализованным числом или нулём того же знака).

В FP-строгом выражении (§15.4) преобразование набора значений не предоставляет вариантов; каждая реализация должна вести себя одинаково:

  • Если значение имеет тип float и не является элементом набора значений чисел с плавающей точкой, то реализация должна сопоставить значение ближайшему элементу набора значений чисел с плавающей точкой. Это преобразование может привести к переполнению или подполнению.

  • Если значение имеет тип double и не является элементом набора значений чисел с двойной точностью, то реализация должна сопоставить значение ближайшему элементу набора значений чисел с двойной точностью. Это преобразование может привести к переполнению или подполнению.

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

Как в FP-строгом коде, так и в коде, который не является FP-строгим, преобразование набора значений всегда оставляет неизменным любое значение, тип которого не является ни float, ни double.

5.2. Преобразование присваивания

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

Контексты присваивания допускают использование одного из следующих вариантов:

  • преобразование тождества (§5.1.1)

  • расширяющее преобразование примитивного типа (§5.1.2)

  • расширяющее преобразование ссылочного типа (§5.1.5)

  • преобразование упаковки (§5.1.7), необязательно, с последующим расширяющим преобразованием ссылочного типа

  • преобразование распаковки (§5.1.8), необязательно, с последующим расширяющим преобразованием примитивного типа.

Если после применения указанных выше преобразований полученный тип является сырым типом (§4.8), может быть применено преобразование без проверки (§5.1.9).

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

Пример такой недопустимой цепочки:

Integer, Comparable<Integer>, Comparable, Comparable<String>

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

Кроме того, если выражение является константным выражением (§15.28) типа byte, short, char или int:

  • Можно использовать сужающее преобразование примитивного типа, если тип переменной равен byte, short или char, и значение константного выражения представимо в типе переменной.

  • Можно использовать сужающее преобразование примитивного типа, за которым следует преобразование упаковки, если тип переменной равен:

    • Byte и значение константного выражения представимо в типе byte.

    • Short и значение константного выражения представимо в типе short.

    • Character и значение константного выражения представимо в типе char.

Сужение констант во время компиляции означает, что разрешен такой код, как:

byte theAnswer = 42;

Без сужения тот факт, что целочисленная константа 42 имеет тип int, означал бы, что требуется приведение к типу byte:

byte theAnswer = (byte)42;  // cast is permitted but not required

Значение типа null (единственное такое значение — null-ссылка) может быть присвоено любому ссылочному типу, в результате чего получается null-ссылка того типа.

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

Если тип выражения может быть преобразован в тип переменной посредством преобразования присваивания, то говорят, что выражение (или его значение) совместимо с переменной или, что тип выражения совместим с типом переменной.

Если тип переменной равен float или double, то к значению v, являющемуся результатом преобразования типа, применяется преобразование множества значений (§5.1.13):

  • Если v имеет тип float и является элементом множества значений с расширенной экспонентой для чисел с плавающей запятой, то реализация должна сопоставить v с ближайшим элементом множества значений для чисел с плавающей запятой. Это преобразование может привести к переполнению или потере точности.

  • Если v имеет тип double и является элементом множества значений с расширенной экспонентой для чисел с двойной точностью, то реализация должна сопоставить v с ближайшим элементом множества значений для чисел с двойной точностью. Это преобразование может привести к переполнению или потере точности.

Единственные исключения, которые может вызвать преобразование присваивания, это:

  • Ошибка ClassCastException, если после применения вышеуказанных преобразований типа полученное значение является объектом, который не является экземпляром подкласса или подинтерфейса стирания (§4.6) типа переменной.

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

  • Ошибка OutOfMemoryError в результате преобразования упаковки.

  • Ошибка NullPointerException в результате преобразования распаковки для null-ссылки.

  • Ошибка ArrayStoreException в особых случаях, связанных с элементами массивов или обращением к полям (§10.5, §15.26.1).

Пример 5.2-1. Преобразование присваивания для примитивных типов

class Test {
    public static void main(String[] args) {
        short s = 12;      // narrow 12 to short
        float f = s;       // widen short to float
        System.out.println("f=" + f);
        char c = '\u0123';
        long l = c;        // widen char to long
        System.out.println("l=0x" + Long.toString(l,16));
        f = 1.23f;
        double d = f;      // widen float to double
        System.out.println("d=" + d);
    }
}

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

f=12.0
l=0x123
d=1.2300000190734863

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

class Test {
    public static void main(String[] args) {
        short s = 123;
        char c = s;    // error: would require cast
        s = c;         // error: would require cast
    }
}

поскольку не все значения short являются значениями char, и не все значения char являются значениями short.


Пример 5.2-2. Преобразование присваивания для ссылочных типов

class Point { int x, y; }
class Point3D extends Point { int z; }
interface Colorable { void setColor(int color); }

class ColoredPoint extends Point implements Colorable {
    int color;
    public void setColor(int color) { this.color = color; }
}

class Test {
    public static void main(String[] args) {
        // Assignments to variables of class type:
        Point p = new Point();
        p = new Point3D(); 
          // OK because Point3D is a subclass of Point
        Point3D p3d = p; 
          // Error: will require a cast because a Point
          // might not be a Point3D (even though it is,
          // dynamically, in this example.)

        // Assignments to variables of type Object:
        Object o  = p;          // OK: any object to Object
        int[] a   = new int[3];
        Object o2 = a;          // OK: an array to Object

        // Assignments to variables of interface type:
        ColoredPoint cp = new ColoredPoint();
        Colorable c = cp;
          // OK: ColoredPoint implements Colorable

        // Assignments to variables of array type:
        byte[] b = new byte[4];
        a = b;
          // Error: these are not arrays of the same primitive type
        Point3D[] p3da = new Point3D[3];
        Point[] pa = p3da; 
          // OK: since we can assign a Point3D to a Point
        p3da = pa;
          // Error: (cast needed) since a Point
          // can't be assigned to a Point3D
    }
}

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

class Point { int x, y; }
interface Colorable { void setColor(int color); }
class ColoredPoint extends Point implements Colorable {
    int color;
    public void setColor(int color) { this.color = color; }
}

class Test {
    public static void main(String[] args) {
        Point p = new Point();
        ColoredPoint cp = new ColoredPoint();
        // Okay because ColoredPoint is a subclass of Point:
        p = cp;
        // Okay because ColoredPoint implements Colorable:
        Colorable c = cp;
        // The following cause compile-time errors because
        // we cannot be sure they will succeed, depending on
        // the run-time type of p; a run-time check will be
        // necessary for the needed narrowing conversion and
        // must be indicated by including a cast:
        cp = p;    // p might be neither a ColoredPoint
                   // nor a subclass of ColoredPoint
        c = p;     // p might not implement Colorable
    }
}

Пример 5.2-3. Преобразование присваивания для типов массивов

class Point { int x, y; }
class ColoredPoint extends Point { int color; }

class Test {
    public static void main(String[] args) {
        long[] veclong = new long[100];
        Object o = veclong;          // okay
        Long l = veclong;            // compile-time error
        short[] vecshort = veclong;  // compile-time error
        Point[] pvec = new Point[100];
        ColoredPoint[] cpvec = new ColoredPoint[100];
        pvec = cpvec;                // okay
        pvec[0] = new Point();       // okay at compile time,
                                     // but would throw an
                                     // exception at run time
        cpvec = pvec;                // compile-time error
    }
}

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

  • Значение veclong не может быть присвоено переменной типа Long, потому что Long — это тип класса, отличный от Object. Массив может быть присвоен только переменной совместимого типа массива или переменной типа Object, Cloneable или java.io.Serializable.

  • Значение veclong не может быть присвоено vecshort, потому что это массивы примитивных типов, и short и long не являются одним и тем же примитивным типом.

  • Значение cpvec может быть присвоено pvec, так как любая ссылка, которая может быть значением выражения типа ColoredPoint, может быть значением переменной типа Point. Последующее присвоение нового Point компоненту pvec затем вызовет исключение ArrayStoreException (если программа была бы иначе исправлена, чтобы её можно было скомпилировать), потому что массив типа ColoredPoint не может иметь экземпляр Point в качестве значения компонента.

  • Значение pvec не может быть присвоено cpvec, потому что не каждая ссылка, которая может быть значением выражения типа ColoredPoint, может быть корректно значением переменной типа Point. Если значение pvec во время выполнения было бы ссылкой на экземпляр Point[], и присвоение cpvec было бы разрешено, простая ссылка на компонент cpvec, например, cpvec[0], могла бы возвращать Point, а Point не является ColoredPoint. Таким образом, разрешение такого присвоения позволило бы нарушить систему типов. Можно использовать приведение (§5.5, §15.16) для обеспечения того, что pvec ссылается на ColoredPoint[]:

          cpvec = (ColoredPoint[])pvec;  // OK, but may throw an 
                                   // exception at run time
        

5.3. Преобразование вызова метода

Преобразование вызова метода применяется к каждому значению аргумента в вызове метода или конструктора (§8.8.7.1, §15.9, §15.12): тип выражения аргумента должен быть преобразован к типу соответствующего параметра.

Контексты вызова методов позволяют использовать одно из следующего:

  • преобразование тождества (§5.1.1)

  • расширяющее преобразование примитивных типов (§5.1.2)

  • расширяющее преобразование ссылочных типов (§5.1.5)

  • преобразование в объект (§5.1.7) с последующим (необязательным) расширяющим преобразованием ссылочных типов

  • преобразование из объекта в примитивный тип (§5.1.8) с последующим (необязательным) расширяющим преобразованием примитивных типов.

Если после применения перечисленных выше преобразований полученный тип является сырым типом (§4.8), то может быть применено преобразование без проверок (§5.1.9).

Возникает ошибка компиляции, если цепочка преобразований содержит два параметризованных типа, которые не находятся в отношении подтипа.

Значение типа null (единственное такое значение) может быть преобразовано в любой ссылочный тип.

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

Если тип выражения-аргумента равен либо float, либо double, то после преобразования типа применяется преобразование набора значений (§5.1.13):

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

  • Если значение аргумента типа double является элементом набора значений с расширенным порядком экспоненты для чисел с двойной точностью, то реализация должна сопоставить значение с ближайшим элементом набора значений для чисел с двойной точностью. Это преобразование может привести к переполнению или потере точности.

Единственные исключения, которые может вызвать преобразование вызова метода, это:

  • ClassCastException, если после применения вышеперечисленных преобразований полученное значение является объектом, который не является экземпляром подкласса или подинтерфейса стёртого (§4.6) типа соответствующего формального параметра.

    Эта ситуация может возникнуть только в результате загрязнения кучи (§4.12.2).

  • OutOfMemoryError в результате преобразования в объект.

  • NullPointerException в результате преобразования из объекта в примитивный тип по значению null.

Преобразования вызова метода специально не включают неявное сужение целочисленных констант, которое является частью преобразования присваивания (§5.2). Разработчики языка Java посчитали, что включение этих неявных сужающих преобразований добавит дополнительной сложности к процессу разрешения перегрузки методов (§15.12.2).

Таким образом, программа:

class Test {
    static int m(byte a, int b) { return a+b; }
    static int m(short a, short b) { return a-b; }
    public static void main(String[] args) {
        System.out.println(m(12, 2));  // compile-time error
    }
}

вызывает ошибку компиляции, так как целочисленные литералы 12 и 2 имеют тип int, поэтому ни один метод m не соответствует правилам (§15.12.2). Язык, который включал неявное сужение целочисленных констант, потребовал бы дополнительных правил для разрешения таких случаев, как этот пример.

5.4. Преобразование в строку

Преобразование в строку применяется только к операнду бинарного оператора +, который не является String, когда другой операнд является String.

В этом единственном специальном случае операнд, не являющийся String, для оператора + преобразуется в String (§5.1.11), и вычисление оператора + продолжается в соответствии с §15.18.1.

5.5. Преобразование приведения типов

Преобразование приведения типов применяется к операнду оператора приведения типов (§15.16): тип выражения операнда должен быть преобразован к типу, явно указанному оператором приведения типов.

Контексты приведения типов допускают использование одного из:

  • преобразование тождества (§5.1.1)

  • расширяющее преобразование примитивных типов (§5.1.2)

  • сужающее преобразование примитивных типов (§5.1.3)

  • расширяющее и сужающее преобразование примитивных типов (§5.1.4)

  • расширяющее преобразование ссылочных типов (§5.1.5), за которым необязательно следует либо преобразование разыменования (§5.1.8), либо преобразование без проверки (§5.1.9)

  • сужающее преобразование ссылочных типов (§5.1.6), за которым необязательно следует либо преобразование разыменования (§5.1.8), либо преобразование без проверки (§5.1.9)

  • преобразование упаковки (§5.1.7), за которым необязательно следует расширяющее преобразование ссылочных типов (§5.1.5)

  • преобразование разыменования (§5.1.8), за которым необязательно следует расширяющее преобразование примитивных типов (§5.1.2).

Преобразование множества значений (§5.1.13) применяется после преобразования типа.

Легитимность преобразования приведения типов на этапе компиляции определяется следующим образом:

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

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

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

  • Выражение ссылочного типа может подвергнуться преобразованию приведения типов к другому ссылочному типу, если на этапе компиляции не возникает ошибка, учитывая правила в §5.5.1.

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

  • - указывает, что преобразование приведения типов запрещено

  • ≈ указывает преобразование тождества (§5.1.1)

  • ω указывает расширяющее преобразование примитивных типов (§5.1.2)

  • η указывает сужающее преобразование примитивных типов (§5.1.3)

  • ωη указывает расширяющее и сужающее преобразование примитивных типов (§5.1.4)

  • ⇑ указывает расширяющее преобразование ссылочных типов (§5.1.5)

  • ⇓ указывает сужающее преобразование ссылочных типов (§5.1.6)

  • ⊡ указывает преобразование упаковки (§5.1.7)

  • ⊔ указывает преобразование разыменования (§5.1.8)

В таблицах запятая между символами означает, что преобразование приведения типов использует одно преобразование, за которым следует другое. Тип Object обозначает любой ссылочный тип, кроме восьми обертывающих классов Boolean, Byte, Short, Character, Integer, Long, Float, Double.

Таблица 5.1. Преобразования при приведении к примитивным типам

К → byte short char int long float double boolean
От ↓
byte ≈ ω ωη ω ω ω ω -
short η ≈ η ω ω ω ω -
char η η ≈ ω ω ω ω -
int η η η ≈ ω ω ω -
long η η η η ≈ ω ω -
float η η η η η ≈ ω -
double η η η η η η ≈ -
boolean - - - - - - - ≈
Byte ⊔ ⊔,ω - ⊔,ω ⊔,ω ⊔,ω ⊔,ω -
Short - ⊔ - ⊔,ω ⊔,ω ⊔,ω ⊔,ω -
Character - - ⊔ ⊔,ω ⊔,ω ⊔,ω ⊔,ω -
Integer - - - ⊔ ⊔,ω ⊔,ω ⊔,ω -
Long - - - - ⊔ ⊔,ω ⊔,ω -
Float - - - - - ⊔ ⊔,ω -
Double - - - - - - ⊔ -
Boolean - - - - - - - ⊔
Object ⇓,⊔ ⇓,⊔ ⇓,⊔ ⇓,⊔ ⇓,⊔ ⇓,⊔ ⇓,⊔ ⇓,⊔

Таблица 5.2. Преобразования при приведении типов к ссылкам

К → Byte Short Character Integer Long Float Double Boolean Object
От ↓
byte ⊡ - - - - - - - ⊡,⇑
short - ⊡ - - - - - - ⊡,⇑
char - - ⊡ - - - - - ⊡,⇑
int - - - ⊡ - - - - ⊡,⇑
long - - - - ⊡ - - - ⊡,⇑
float - - - - - ⊡ - - ⊡,⇑
double - - - - - - ⊡ - ⊡,⇑
boolean - - - - - - - ⊡ ⊡,⇑
Byte ≈ - - - - - - - ⇑
Short - ≈ - - - - - - ⇑
Character - - ≈ - - - - - ⇑
Integer - - - ≈ - - - - ⇑
Long - - - - ≈ - - - ⇑
Float - - - - - ≈ - - ⇑
Double - - - - - - ≈ - ⇑
Boolean - - - - - - - ≈ ⇑
Object ⇓ ⇓ ⇓ ⇓ ⇓ ⇓ ⇓ ⇓ ≈

5.5.1. Приведение типов ссылочных типов

Учитывая тип ссылки S (источник) и тип ссылки T (цель) на этапе компиляции, преобразование приведения типа существует от S к T, если не происходит ошибок на этапе компиляции из-за следующих правил.

Если S является типом класса:

  • Если T является типом класса, то либо |S| <: |T|, либо |T| <: |S|. В противном случае возникает ошибка на этапе компиляции.

    Кроме того, если существует супертип X типа T и супертип Y типа S, такие что оба X и Y являются доказуемо различными параметризованными типами (§4.5), и стирания типов X и Y совпадают, возникает ошибка на этапе компиляции.

  • Если T является типом интерфейса:

    • Если S не является final классом (§8.1.1), то, если существует супертип X типа T и супертип Y типа S, такие что оба X и Y являются доказуемо различными параметризованными типами, и стирания типов X и Y совпадают, возникает ошибка на этапе компиляции.

      В противном случае приведение всегда разрешено на этапе компиляции (потому что даже если S не реализует T, подкласс S может).

    • Если S является final классом (§8.1.1), то S должен реализовывать T, в противном случае возникает ошибка на этапе компиляции.

  • Если T является переменной типа, то этот алгоритм применяется рекурсивно, используя верхнюю границу T вместо T.

  • Если T является типом массива, то S должен быть классом Object, в противном случае возникает ошибка на этапе компиляции.

Если S является типом интерфейса:

  • Если T является типом массива, то S должен быть типом java.io.Serializable или Cloneable (единственные интерфейсы, реализуемые массивами), в противном случае возникает ошибка на этапе компиляции.

  • Если T является типом, который не является final (§8.1.1), то если существует супертип X типа T и супертип Y типа S, такие что оба X и Y являются доказуемо различными параметризованными типами, и стирания типов X и Y совпадают, возникает ошибка на этапе компиляции.

    В противном случае приведение всегда разрешено на этапе компиляции (потому что даже если T не реализует S, подкласс T может).

  • Если T является типом, который final, то:

    • Если S не является параметризованным типом или типом-сырым, то T должен реализовывать S, в противном случае возникает ошибка на этапе компиляции.

    • В противном случае S является либо параметризованным типом, являющимся вызовом некоторого объявления обобщенного типа G, либо типом-сырым, соответствующим объявлению обобщенного типа G. Тогда должен существовать супертип X типа T, такой что X является вызовом G, в противном случае возникает ошибка на этапе компиляции.

      Кроме того, если S и X являются доказуемо различными параметризованными типами, то возникает ошибка на этапе компиляции.

Если S является переменной типа, то этот алгоритм применяется рекурсивно, используя верхнюю границу S вместо S.

Если S является пересечением типов A1 & ... & An, то это ошибка на этапе компиляции, если существует Ai (1 ≤ i ≤ n), такой что тип S не может быть приведён к типу Ai по этому алгоритму. То есть, успех приведения определяется наиболее ограниченным компонентом типа пересечения.

Если S является типом массива SC[], то есть массивом компонентов типа SC:

  • Если T является типом класса, то если T не Object, то возникает ошибка на этапе компиляции (потому что Object — единственный тип класса, которому могут быть присвоены массивы).

  • Если T является типом интерфейса, то возникает ошибка на этапе компиляции, если T не является типом java.io.Serializable или типом Cloneable (единственными интерфейсами, реализуемыми массивами).

  • Если T является переменной типа, то:

    • Если верхней границей T является Object или java.io.Serializable или Cloneable, или переменная типа, к которой S мог бы быть приведён, то приведение разрешено (хотя и непроверяемо).

    • Если верхней границей T является тип массива TC[], то ошибка на этапе компиляции возникает, если тип SC[] не может быть приведён к типу TC[].

    • В противном случае возникает ошибка на этапе компиляции.

  • Если T является типом массива TC[], то есть массивом компонентов типа TC, то ошибка на этапе компиляции возникает, если не выполняется одно из следующих условий:

    • TC и SC — один и тот же примитивный тип.

    • TC и SC — ссылочные типы, и тип SC может быть приведён к типу TC.

Пример 5.5.1-1. Преобразование приведения для ссылочных типов

class Point { int x, y; }
interface Colorable { void setColor(int color); }
class ColoredPoint extends Point implements Colorable {
    int color;
    public void setColor(int color) { this.color = color; }
}
final class EndPoint extends Point {}

class Test {
    public static void main(String[] args) {
        Point p = new Point();
        ColoredPoint cp = new ColoredPoint();
        Colorable c;
        // The following may cause errors at run time because
        // we cannot be sure they will succeed; this possibility
        // is suggested by the casts:
        cp = (ColoredPoint)p;  // p might not reference an
                               // object which is a ColoredPoint
                               // or a subclass of ColoredPoint
        c = (Colorable)p;      // p might not be Colorable
        // The following are incorrect at compile time because
        // they can never succeed as explained in the text:
        Long l = (Long)p;            // compile-time error #1
        EndPoint e = new EndPoint();
        c = (Colorable)e;            // compile-time error #2
    }
}

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

Вторая ошибка на этапе компиляции возникает, потому что переменная типа EndPoint никогда не может ссылаться на значение, которое реализует интерфейс Colorable. Это потому, что EndPoint — это тип final, и переменная типа final всегда содержит значение того же времени выполнения, что и её тип компиляции. Поэтому тип времени выполнения переменной e должен быть точно таким же, как тип EndPoint, а тип EndPoint не реализует Colorable.


Пример 5.5.1-2. Преобразование приведения для типов массивов

class Point {
    int x, y;
    Point(int x, int y) { this.x = x; this.y = y; }
    public String toString() { return "("+x+","+y+")"; }
}
interface Colorable { void setColor(int color); }
class ColoredPoint extends Point implements Colorable {
    int color;
    ColoredPoint(int x, int y, int color) {
        super(x, y); setColor(color);
    }
    public void setColor(int color) { this.color = color; }
    public String toString() {
        return super.toString() + "@" + color;
    }
}

class Test {
    public static void main(String[] args) {
        Point[] pa = new ColoredPoint[4];
        pa[0] = new ColoredPoint(2, 2, 12);
        pa[1] = new ColoredPoint(4, 5, 24);
        ColoredPoint[] cpa = (ColoredPoint[])pa;
        System.out.print("cpa: {");
        for (int i = 0; i < cpa.length; i++)
            System.out.print((i == 0 ? " " : ", ") + cpa[i]);
        System.out.println(" }");
    }
}

Эта программа компилируется без ошибок и выводит:

cpa: { (2,2)@12, (4,5)@24, null, null }

5.5.2. Проверенные и непроверенные приведения типов

Приведение типа S к типу T считается статически правильным тогда и только тогда, когда S <: T (§4.10).

Приведение типа S к параметризованному типу (§4.5) T считается непроверенным, если не выполняется хотя бы одно из следующих условий:

  • S <: T

  • Все аргументы типа (§4.5.1) типа T являются неограниченными подстановочными знаками

  • T <: S и S не имеет подтипа X, отличного от T, где аргументы типа X не содержатся в аргументах типа T.

Приведение типа S к переменной типа T считается непроверенным, если S <: T.

Непроверенное приведение от S к T является полностью непроверенным, если приведение от |S| к |T| статически известно как правильное. В противном случае оно является частично непроверенным.

Непроверенное приведение вызывает предупреждение компилятора о непроверенном приведении, если не подавлена аннотация SuppressWarnings (§9.6.3.5).

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

Если приведение к ссылочному типу не является ошибкой на этапе компиляции, существуют несколько случаев:

  • Приведение статически известно как правильное.

    Для такого приведения не выполняется никаких действий во время выполнения.

  • Приведение является полностью непроверенным приведением.

    Для такого приведения не выполняется никаких действий во время выполнения.

  • Приведение является частично непроверенным приведением.

    Такое приведение требует проверки корректности во время выполнения. Проверка выполняется так, как если бы приведение было проверенным приведением между |S| и |T|, как описано ниже.

  • Приведение является проверенным приведением.

    Такое приведение требует проверки корректности во время выполнения. Если значение во время выполнения является null, то приведение разрешено. В противном случае, пусть R — класс объекта, на который ссылается значение ссылки во время выполнения, а T — стирание (§4.6) типа, указанного в операторе приведения. Приведение должно проверять во время выполнения, что класс R совместим с типом T по алгоритму в §5.5.3.

    Обратите внимание, что R не может быть интерфейсом, когда эти правила применяются впервые для любого данного приведения, но R может быть интерфейсом, если правила применяются рекурсивно, так как значение ссылки во время выполнения может ссылаться на массив, тип элемента которого является типом интерфейса.

5.5.3. Проверенные приведения во время выполнения

Вот алгоритм проверки того, является ли тип R во время выполнения объекта совместимым с типом T, который является стиранием (§4.6) типа, указанного в операторе приведения. Если генерируется исключение во время выполнения, это ClassCastException.

Если R является обычным классом (не классом массива):

  • Если T — класс, то R должен быть либо тем же классом (§4.3.4), что и T, либо подклассом T, иначе выбрасывается исключение во время выполнения.

  • Если T — тип интерфейса, то R должен реализовывать (§8.1.5) интерфейс T, иначе выбрасывается исключение во время выполнения.

  • Если T — тип массива, то выбрасывается исключение во время выполнения.

Если R является интерфейсом:

  • Если T — класс, то T должен быть Object (§4.3.2), иначе выбрасывается исключение во время выполнения.

  • Если T — тип интерфейса, то R должен быть тем же интерфейсом, что и T, или подинтерфейсом T, иначе выбрасывается исключение во время выполнения.

  • Если T — тип массива, то выбрасывается исключение во время выполнения.

Если R является классом, представляющим тип массива RC[], то есть массив компонентов типа RC:

  • Если T — класс, то T должен быть Object (§4.3.2), иначе выбрасывается исключение во время выполнения.

  • Если T — тип интерфейса, то выбрасывается исключение во время выполнения, если T не является типом java.io.Serializable или типом Cloneable (единственными интерфейсами, реализуемыми массивами).

    Этот случай может пройти мимо проверки на этапе компиляции, если, например, ссылка на массив хранится в переменной типа Object.

  • Если T — тип массива TC[], то есть массив компонентов типа TC, то выбрасывается исключение во время выполнения, если не выполняется одно из следующих условий:

    • TC и RC являются одним и тем же примитивным типом.

    • TC и RC являются ссылочными типами, и тип RC можно привести к типу TC с помощью рекурсивного применения этих правил выполнения приведения.

Пример 5.5.3-1. Несовместимые типы во время выполнения

class Point { int x, y; }
interface Colorable { void setColor(int color); }
class ColoredPoint extends Point implements Colorable {
    int color;
    public void setColor(int color) { this.color = color; }
}

class Test {
    public static void main(String[] args) {
        Point[] pa = new Point[100];

        // The following line will throw a ClassCastException:
        ColoredPoint[] cpa = (ColoredPoint[])pa;
        System.out.println(cpa[0]);
        int[] shortvec = new int[2];
        Object o = shortvec;

        // The following line will throw a ClassCastException:
        Colorable c = (Colorable)o;
        c.setColor(0);
    }
}

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


5.6. Числовые преобразования

Числовое преобразование применяется к операндам арифметического оператора.

Контексты числового преобразования позволяют использовать:

  • преобразование тождества (§5.1.1)

  • расширяющее преобразование примитивных типов (§5.1.2)

  • преобразование распаковки (§5.1.8)

Числовые преобразования используются для преобразования операндов числового оператора к общему типу, чтобы операцию можно было выполнить. Существуют два вида числовых преобразований: унарное числовое преобразование (§5.6.1) и бинарное числовое преобразование (§5.6.2).

5.6.1. Унарное числовое преобразование

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

  • Если операнд имеет тип Byte, Short, Character или Integer, он подвергается преобразованию распаковки (§5.1.8). Результат затем преобразуется в значение типа int с помощью расширяющего преобразования примитивных типов (§5.1.2) или преобразования тождества (§5.1.1).

  • В противном случае, если операнд имеет тип Long, Float или Double, он подвергается преобразованию распаковки (§5.1.8).

  • В противном случае, если операнд имеет тип byte, short или char, он преобразуется в значение типа int с помощью расширяющего преобразования примитивных типов (§5.1.2).

  • В противном случае, унарный числовой операнд остается без изменений.

В любом случае, затем применяется преобразование множества значений (§5.1.13).

Унарное числовое преобразование выполняется для выражений в следующих ситуациях:

  • Каждое выражение размера в выражении создания массива (§15.10)

  • Выражение индекса в выражении доступа к элементу массива (§15.13)

  • Операнд унарного оператора плюс + (§15.15.3)

  • Операнд унарного оператора минус - (§15.15.4)

  • Операнд оператора поразрядного отрицания ~ (§15.15.5)

  • Каждый операнд по отдельности битового оператора сдвига >>, >>> или << (§15.19).

    Расстояние сдвига long (правый операнд) не преобразует значение, которое сдвигается (левый операнд), к long.

Пример 5.6.1-1. Унарное числовое преобразование

class Test {
    public static void main(String[] args) {
        byte b = 2;
        int a[] = new int[b];  // dimension expression promotion
        char c = '\u0001';
        a[c] = 1;              // index expression promotion
        a[0] = -c;             // unary - promotion
        System.out.println("a: " + a[0] + "," + a[1]);
        b = -1;
        int i = ~b;            // bitwise complement promotion
        System.out.println("~0x" + Integer.toHexString(b)
                           + "==0x" + Integer.toHexString(i));
        i = b << 4L;           // shift promotion (left operand)
        System.out.println("0x" + Integer.toHexString(b)
                           + "<<4L==0x" + Integer.toHexString(i));
    }
}

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

a: -1,1
~0xffffffff==0x0
0xffffffff<<4L==0xfffffff0

5.6.2. Бинарное числовое преобразование

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

  1. Если какой-либо операнд имеет ссылочный тип, он подвергается преобразованию распаковки (§5.1.8).

  2. Применяется расширяющее преобразование примитивных типов (§5.1.2) для преобразования одного или обоих операндов в соответствии со следующими правилами:

    • Если любой операнд имеет тип double, другой преобразуется в double.

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

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

    • В противном случае, оба операнда преобразуются в тип int.

После преобразования типа (если таковое имеется), если необходимо, применяется преобразование множества значений (§5.1.13) к каждому операнду.

Бинарное числовое преобразование выполняется для операндов определенных операторов:

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

  • Операторы сложения и вычитания для числовых типов + и - (§15.18.2)

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

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

  • Целочисленные поразрядные операторы &, ^ и | (§15.22.1)

  • В некоторых случаях условный оператор ? : (§15.25)

Пример 5.6.2-1. Бинарное числовое преобразование

class Test {
    public static void main(String[] args) {
        int i    = 0;
        float f  = 1.0f;
        double d = 2.0;
        // First int*float is promoted to float*float, then
        // float==double is promoted to double==double:
        if (i * f == d) System.out.println("oops");
		
        // A char&byte is promoted to int&int:
        byte b = 0x1f;
        char c = 'G';
        int control = c & b;
        System.out.println(Integer.toHexString(control));
		
        // Here int:float is promoted to float:float:
        f = (b==0) ? i : 4.0f;
        System.out.println(1.0/f);
    }
}

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

7
0.25

Пример преобразует ASCII-символ G в управляющий символ ASCII-G (BEL), с помощью маскирования всех, кроме нижних 5 бит символа. 7 — числовое значение этого управляющего символа.


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

Spec-Zone.ru

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