Правила приведения примитивных типов java. Преобразование типов в Java

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

Каким образом работает явное приведение типов

В вашем примере показано восходящее преобразование (Upcasting ):

List coll = new ArrayList();

На русский язык переводится так: создай ворону, типа птицы. Создай динамический массив, типа лист. В большинстве ситуаций восходящее преобразование совершенно не нужно .
Однако, приведение типов работает на собеседованиях, когда вам дают вопросы на наследование. К примеру, сайт quizful.net вообще содержит в себе множество вопросов на приведение типов. Поэтому разъясню особенности, которые знаю.

Итак, в вышеприведенном примере мы создали объект типа ArrayList , а ссылка типа List . Запомните аксиомы для этого способа:

1. Ссылку можно указать на любого родителя. Даже очень давнего. То есть, можно привести ссылку coll даже к типу Object . Компилятор пропустит любую ссылку на класс родителя, или родителя-родителя, или родителя-родителя...родителя

2. Обращение к полю - всегда идёт возврат поля ссылки, не поля объекта. Если такого поля нет в классе-ссылке будет ошибка компиляции.

Class A{ int x = 2; //Поле родителя } Class B extends A { int x = 3; //Поле которое должно перекрыть родительское int y = 5; //Поле, которого нет в родительском классе. } Class Test{ public static void main(String args) { A ab = new B(); //Восходящее преобразование System.out.println("Int x = " + ab.x); } }

Вернет Int x = 2 . Если вы попробуете обратиться к полю объекта:

System.out.println("Int y = " + ab.y); //Ошибка компилляции

Ваш компилятор скажет, что вы не правы, так как он по ссылке (A ab) не видит такого поля. Всё вышесказанное сохраняет силу, даже если ваши поля пометить модификаторами static.

3. Обращение к нестатическому методу: в этом случае вернёт метод объекта. Но при обращении к статическому методу - возвращает метод ссылки.

Class D{ public void doSome(){ //Нестатический метод System.out.println("Nonstatic doSome from D"); } public static void Action(){ //Статический метод System.out.println("static Action from D"); } } public class Okey extends D{ public void doSome(){ System.out.println("doSome from Okey"); } public static void Action(){ System.out.println("static Action from Okey"); } public static void main(String args) { D o=new Okey(); o.doSome(); //Из класса Okey o.Action(); //Из класса D } }

Nonstatic doSome from Okey

static Action from D

Разгадка проста, нестатический метод - это метод объекта, статический - метод класса. Когда мы вызываем не статический метод - компилятор понимает так: летай как ворона. Когда мы вызываем статический - буквально, летай как птица.

4. Если идёт вызов метода, который описан в классе объекта, но не описан в классе ссылки - пойдёт ошибка компилляции. Потому что, вызов метода происходит по ссылке:

Class A {} Class B extends A { void someMethod(){}; public static void main(String args) { A ab = new B(); ab.someMethod(); //Ошибка компилляции. } }

5. Конструктор объекта (при создании командой new) работает также, как если давать ссылку на свой класс.

Java является строго типизированным языком программирования, а это означает, то что каждое выражение и каждая переменная имеет строго определенный тип уже на момент компиляции.
Виды приведений
В Java предусмотрено семь видом приведений:

  • Тождественное (identity);

  • Расширение примитивного типа (widening primitive);

  • Сужение примитивного типа (narrowing primitive);

  • Расширение объектного типа (widening reference);

  • Сужение объектного типа (narrowing reference);

  • Преобразование к строке (String);

  • Запрещенные преобразования (forbidden);
Рассмотрим их по отдельности.
Тождественное преобразование
Самым простым является тождественное преобразование. В Java преобразование выражения любого типа к точно такому же типу всегда допустимо и успешно выполняется.
Это важно для возможности утверждать с теоретической точки зрения, что любой тип в Java может участвовать в преобразовании, хотя бы в тождественном.
Преобразование примитивных типов (расширение и сужение)
Для простых типов расширение означает, что осуществляется переход от менее емкого типа к более ёмкому. Например, от типа byte (длина 1 байт) к типу int (длина 4 байта). Такие преобразование безопасны в том смысле, что новый тип всегда гарантировано вмещает в себя все данные, которые хранились в старом типе, и таким образом не происходит потери данных. Именно поэтому компилятор осуществляет его сам, незаметно для разработчика:

byte b=3;
int a=b;

Следующие 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
Обратите внимание, что нельзя провести преобразование к типу char от типов меньшей или равной длины (byte, short) или, наоборот, к short от char без потери данных. Это связано с тем, что char, в отличие от остальных целочисленных типов, является знаковым.
Тем не менее, следует помнить, что даже при расширении данные все таки могут быть искажены. Это приведение значений int к типу float и приведение значений типа long к типу float или double. Хотя эти дробные типы вмещают гораздо большие числа, чем соответствующие целые, но у них меньше значащих разрядов.
Например:

long a = 111111111111L;
float f=a;
a=(long)f; // () это как раз и есть операция преобразования типа
System.out.println(a); //результат 111111110656

Обратите внимание – сужение – означает, что переход осуществляется от боле емкого типа к менее емкому. При таком преобразовании есть риск потерять данные. Например, если число типа int было больше 127, то при приведении его к byte значения битов старше восьмого будут потеряны. В Java такое преобразование должно совершаться явным образом, т.е. программист в коде должен явно указать, то он намеревается осуществить такое преобразование и готов потерять данные.
Следующие 23 преобразования являются сужающими:

  • От byte к char

  • От 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
При сужении целочисленного типа к более узкому целочисленному все старшие биты, не попадающие в новый тип,просто отбрасывается. Не производится никакого округления или других действий для получения более корректного результата:

System.out.println((byte)383);
System.out.println((byte)384);
System.out.println((byte)-384);

Результатом будет:

127
-128
-128
Видно, что знаковый бит при сужении не оказал никакого влияния, так как был просто отброшен – результат приведения обратных чисел (384, -384) оказался одинаковым. Следовательно, может быть потеряно не только точное абсолютное значение, но и знак величины.
Это верно и для char:

char c=4000;
System.out.println((short)c);

Результат:

-25536
Преобразование ссылочных типов (расширение и сужение)
Преобразование объектных типов лучше всего иллюстрируется с помощью дерева наследования. Рассмотрим небольшой пример наследования:

class Parent {
int x;
}

class ChildY extends Parent {
int y;
}

class ChildZ extends Parent {
int z;
}

В каждом классе объявлено поле с уникальным именем. Будем рассматривать это поле как пример набора уникальных свойств, присущи некоторому объектному типу.
Объекты класса Parent обладают только одним полем x, а значит, только ссылки типа Parent могут ссылаться на такие объекты. Объекты класса ChildY обладают полем y и полем x, полученным по наследству от класса Parent. Стало быть, на такие объекты могут указывать ссылки типа ChildY или Parent. Пример:

Parent p = new ChildY();

Обратите внимание, что с помощью такой ссылки p можно обращаться лишь к полю x созданного объекта. Поле y недоступно, так как компилятор, проверяя корректность выражения p.y, не может предугадать, что ссылка p будет указывать на объект типа ChildY во время исполнения программы. Он анализирует лишь тип самой переменной, а она объявлена как Parent, но в этом классе нет поля y, что и вызовет ошибку компиляции.
Аналогично, объекты класса ChildZ обладают полем z и полем x, полученным по наследству от класса Parent. Значит, на такие объекты могут указывать ссылки типа ChildZ и Parent.
Таким образом, ссылки типа Parent могут указать на объект любого из трех рассматриваемых типов, а ссылки типа ChildY и ChildZ – только на объекты точно такого же типа. Теперь можно перейти к преобразования ссылочных типов на основе такого дерева наследования.
Расширение означает переход от более конкретного типа к менее конкретному, т.е. переход от детей к родителям. Подобно случаю с примитивными типами, этот переход производиться самой JVM при необходимости и «незаметен» для разработчика, то есть не требует никаких специальных преобразования.

Parent p1=new ChildY();
Parent p2=new ChildZ();

В обеих строках переменным типа Parent присваивается значение другого типа, а значит, происходит преобразование. Поскольку это расширение, оно производиться автоматически и всегда успешно.
Нужно заметить, что при подобном преобразовании с самим объектом ничего не происходит. Несмотря на то что, например, поле y класса ChildY теперь недоступно, это не значит, что оно исчезло. Такое существенно изменение объекта не возможно. Он был порожден от класса ChildY и сохраняет все его свойства. Изменился лишь тип ссылки, через которую идет обращение к объекту.
Обратный переход, то есть движение по дереву наследования вниз, к наследникам, является сужением. Например, для рассматриваемого случая, переход от ссылки типа Parent , которая может ссылаться на объекты трех классов, к ссылке типа ChildY, которая может ссылаться только на один класс из трех, очевидно, является сужением. Такой переход может оказаться невозможным. Если ссылка типа Parent ссылается на объект типа Parent или ChildZ, то переход к ChildY невозможен, так как в обоих случаях объект не обладает полем y, которое объявлено в классе ChildY. Поэтому при сужении разработчику необходимо явным образом указывать на то, что необходимо попытаться провести такое преобразование. JVM во время исполнения проверит корректность перехода. Если он возможен, преобразование будет проведено. Если же нет – возникнет ошибка (обычно ClassCastException).

Parent p=new ChildY();
ChildY cy = (ChildY)p; //верно
Parent p2=new ChildZ();
ChildY cy2 = (ChildY)p2; //ошибка

Чтобы проверить, возможен ли желаемый переход, можно воспользоваться оператором instanceof:

Parent p=new ChildY();
if (p instanceof ChildY) {
ChildY cy = (ChildY)p;
}

Parent p2=new ChildZ();
if (p2 instanceof ChildY) {
ChildY cy = (ChildY)p2;
}

Parent p3=new Parent();
if (p3 instanceof ChildY) {
ChildY cy = (ChildY)p3;
}

В данном примере ошибок не возникнет. Первое преобразование возможно, и оно будет осуществлено. Во втором и третьем случаях условия операторов if не сработают и следовательно некорректного перехода не будет.
Преобразование к строке
Любой тип может быть приведен к строке, т.е. к экземпляру класса String. Такое преобразование является исключительным в силу того, что охватывает абсолютно все типы.
Различные типы преобразуются к строке следующим образом:

  • Числовые типы записываются в текстовом виде без потери точности представления. Сначала на основе примитивного значения порождается экземпляр соответствующего класса-«обертки», затем у него вызывается метод toString(). Но поскольку эти действия снаружи незаметны, JVM оптимизирует их и преобразует примитивные значения в текст напрямую.

  • Булевские величины приводятся к строке «true» или «false» в зависимости от значения.

  • Для объектных величин вызывается метод toString(). Если метод возвращает null, то результатом будет строка “null”.

  • Для null-значения генерируется строка “null”.
Запрещенные преобразования
Не все переходы между произвольными типами допустимы. Например, к запрещенным преобразованиям относятся: переходы от любого ссылочного типа к примитивному и наоборот (кроме преобразования к строке), boolean можно привести только к этому типу или же к строке. Кроме того невозможно привести друг к другу, классы находящиеся на соседних ветвях дерева наследования. В примере, который рассматривался для иллюстрации ссылочных типов, переход от ChildY к ChildZ запрещен.
Этим список запрещенных преобразований не исчерпывается. Он довольно широк и в тоже время все варианты достаточно очевидны, поэтому подробно рассматриваться не будут. Желающие могут получить полную информацию из спецификации.
Разумеется, попытка осуществить запрещенное преобразование вызовет ошибку.

Применение приведений
Ситуации применения преобразования типов могут быть сгруппированы следующим образом:

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

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

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

  • Оператор конкатенации производит преобразование к строке своих аргументов.

  • Числовое расширение. Числовые операции могут потребовать изменения типа аргумента(ов). Это преобразование имеет особое название – расширенное, так как выбор целевого типа может зависеть не только от исходного значения, но и от второго аргумента операции.
Задание #8
Добавить в проект использование приведения для иерархии ваших классов.

Это достаточно большая тема, но мы постараемся рассмотреть ее как можно более полно и вместе с тем компактно. Частично мы уже касались этой темы когда рассматривали примитивные типы Java.

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

Преобразование типов в Java бывает двух видов: неявное и явное .

Неявное преобразование типов выполняется в случае если выполняются условия:

  1. Оба типа совместимы
  2. Длина целевого типа больше или равна длине исходного типа

Во всех остальных случаях должно использоваться явное преобразование типов .

Так же существуют два типа преобразований:

  1. Расширяющее преобразование (widening conversion)
  2. Сужающее преобразование (narrowing conversion)

Расширяющее преобразование (widening conversion ) происходит, если значение одного типа преобразовывается в более широкий тип, с большим диапазоном допустимых значений. Java выполняет расширяющие преобразования автоматически, например, если вы присвоили литерал типа int переменной типа double или значение пепременной типа char переменной типа int. Неявное преобразование всегда имеет расширяющий тип .

Но у тут могут быть свои небольшие грабельки. Например если преобразуется значение int в значение типа float. И у значения int в двоичном представлении больше чем 23 значащих бита, то возможна потеря точности, так как у типа float под целую часть отведено 23 бита. Все младшие биты значения int, которые не поместятся в 23 бита мантиссы float, будут отброшены, поэтому хотя порядок числа сохраниться, но точность будет утеряна. То же самое справедливо для преобразования типа long в тип double.

Расширяющее преобразование типов Java можно изобразить еще так:

Сплошные линии обозначают преобразования, выполняемые без потери данных. Штриховые линии говорят о том, что при преобразовании может произойти потеря точности.

Стоит немного пояснить почему, к примеру тип byte не преобразуется автоматически (не явно) в тип char, хотя тип byte имеет ширину 8 бит, а char 16, тоже самое касается и преобразования типа short в char. Это происходит потому, что byte и short знаковые типы данных, а char без знаковый. Поэтому в данном случае требуется использовать явное приведение типов, поскольку компилятору надо явно указать что вы знаете чего хотите и как будет обрабатываться знаковый бит типов byte и short при преобразовании к типу char.

Поведение величины типа char в большинстве случаев совпадает с поведением величины целого типа, следовательно, значение типа char можно использовать везде, где требуются значения int или long. Однако напомним, что тип char не имеет знака, поэтому он ведет себя отлично от типа short, несмотря на то что диапазон обоих типов равен 16 бит.

short s = ( short ) 0xffff ; // Данные биты представляют число –1
char c = "\uffff" ; // Те же биты представляют символ юникода
int i1 = s ; // Преобразование типа short в int дает –1
int i2 = c ; // Преобразование char в int дает 65535

Сужающее преобразование (narrowing conversion ) происходит, если значение преобразуется в значение типа, диапазон которого не шире изначального. Сужающие преобразования не всегда безопасны: например, преобразование целого значения 13 в byte имеет смысл, а преобразование 13000 в byte неразумно, поскольку byte может хранить только числа от −128 до 127. Поскольку во время сужающего преобразования могут быть потеряны данные, Java компилятор возражает против любого такого преобразования, даже если преобразуемое значение укладывается в более узкий диапазон указанного типа:

int i = 13 ;
byte b = i ; // Компилятор не разрешит это выражение

Единственное исключение из правила – присвоение целого литерала (значения типа int) переменной byte или short, если литерал соответствует диапазону переменной.

Сужающее преобразование это всегда явное преобразование типов .

Явное преобразование примитивных типов

Оператором явного преобразования типов или точнее говоря приведения типов являются круглые скобки, внутри которых указан тип, к которому происходит преобразование – (type) . Например:

int i = 13 ;
byte b = ( byte ) i ; // Принудительное преобразование int в byte
i = ( int ) 13.456 ; // Принудительное преобразование литерала типа double в int 13

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

При приведении более емкого целого типа к менее емкому старшие биты просто отбрасываются . По существу это равнозначно операции деления по модулю приводимого значения на диапазон целевого типа (например для типа byte это 256).

Слишком большое дробное число при приведении к целому превращается в MAX_VALUE или MIN_VALUE .

Слишком большой double при приведении к float превращается в Float.POSITIVE_INFINITY или Float.NEGATIVE_INFINITY .

Таблица представленная ниже представляет собой сетку, где для каждого примитивного типа указаны типы, в которые их можно преобразовать, и способ преобразования. Буква N в таблице означает невозможность преобразования. Буква Y означает расширяющее преобразование, которое выполняется автоматически. Буква С означает сужающее преобразование, требующее явного приведения. Наконец, Y* означает автоматическое расширяющее преобразование, в процессе которого значение может потерять некоторые из наименее значимых разрядов. Это может произойти при преобразовании int или long во float или double. Типы с плавающей точкой имеют больший диапазон, чем целые типы, поэтому int или long можно представить посредством float или double. Однако типы с плавающей точкой являются приближенными числами и не всегда могут содержать так много значащих разрядов в мантиссе, как целые типы.

Автоматическое расширение типов в выражениях

Так же стоит еще раз упомянуть об автоматическом повышении (расширении) типов в выражениях. Мы с этим уже сталкивались когда рассматривали целочисленные типы данных и операции над ними, но все же стоит и тут напомнить, чтобы усвоилось еще лучше и к тому же это имеет непосредственное отношение к данной теме. В примере ниже знак @ + , , * , / и т.п.

То есть, все целочисленные литералы в выражениях, а так же типы byte , short и char расширяются до int . Если, как описано выше, в выражении не присутствуют другие, более большие типы данных (long , float или double ). Поэтому приведенный выше пример вызовет ошибку компиляции, так как переменная c имеет тип byte , а выражение b+1, в результате автоматического повышения имеет тип int .

Неявное приведение типов в выражениях совмещенного присваивания

Хоть данный раздел и относится к неявному преобразованию (приведению) типов, его объяснение мы привели тут, поскольку в данном случае так же работает и автоматическое расширение типов в выражениях, а затем уже неявное приведение типов. Вот такой кордебалет. Пример ниже я думаю все разъяснит. Так же как и в предыдущем объяснении знак @ означает любой допустимый оператор, например + , , * , / и т.п.

Это стоит пояснить на простом примере:

byte b2 = 50 ;
b2 = b2 * 2 ; // не скомпилируется
b2 *= 2 ; //скомпилируется, хотя и равнозначна b2 = b2 * 2

Вторя строка, приведенная в примере не скомпилируется из-за автоматического расширения типов в выражениях, так как выражение b2*2 имеет тип int, так как происходит автоматическое расширение типа (целочисленные литералы в выражении всегда int). Третья же строка спокойно скомпилируется, так как в ней сработает неявное приведение типов в совмещенном выражении присваивания.

Boxing/unboxing – преобразование примитивных типов в объекты обертки

Boxing и unboxin – это тоже достаточно большая тема, но она достаточно простая.

По существу boxing и unboxing это преобразование примитивных типов в объекты обертки и обратно .

Для объектов оберток примитивных типов применимо все что было сказано выше.

Об классах обертках упоминалось в таблицах, при разборе каждого из примитивных типов. Но тогда это было лишь упоминание в таблице.

Так вот, для каждого примитивного типа есть его старший брат, и он совсем не примитивный, а является настоящим классом, с полями и методами. И для каждой такой парочки возможно автоматическое преобразование.

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

Приведу простой пример:

int i3 ;
byte b2 = 3 ;
Byte myB ;
myB = b2 ;
myB ++;
b2 = myB ;
i3 = myB ;

Если пока не понятно зачем это нужно, то это не страшно, просто завяжите узелок на память.

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

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

  • Преобразование типа int в тип long требует во время выполнения программы расширения знака 32-битового целого значения до 64-битового целого. Потери информации при этом не происходит.
  • Преобразование типа float в тип long требует во время выполнения программы нетривиального преобразования 32-битового плавающего значения в 64-битовое целое. В зависимости от исходного значения может произойти или не произойти потеря информации.
  • Преобразование типа Thread в тип Object не требует никаких действий: поскольку класс Thread является потомком класса Object , любая ссылка на объект типа Thread автоматически является ссылкой на объект типа Object .
  • Преобразование типа Object в тип Thread требует проверки в период исполнения. Если преобразуемая ссылка действительно является ссылкой на объект типа Thread , то она возвращается как результат преобразования, в противном случае генерируется исключение.

5.4.1.1. Расширяющие преобразования чисел

Расширяющие преобразования чисел — это преобразования числового типа в "больший" числовой тип, которые считаются безопасными, т. к. не приводят к потере величины преобразуемого значения. Такими преобразованиями в Java являются:

  • преобразования 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 .

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

Class Test { public static void main(String args) { int bigNumber = 1234567890; float approximate = bigNumber; System.out.println(approximate); } }

выведет на экран строку 1234567936 . Это связано с тем, что при преобразовании int в float результирующее значение равно 1.2345679E9 из-за того, что мантисса чисел типа float вмещает только 8 десятичных цифр (здесь для правильной работы следует использовать преобразование к типу double ). Тем не менее, исполняющая система никогда не генерирует ошибок при выполнении перечисленных преобразований.

5.4.1.2. Сужающие преобразования чисел

Сужающие преобразования чисел — это преобразования числового типа в "меньший" числовой тип, которые могут привести как к потере величины, так и к потере точности. Такими преобразованиями в Java являются:

  • преобразования byte в char ;
  • преобразования short в byte и char ;
  • преобразования int в byte , short и char ;
  • преобразования long в byte , short , int и char ;
  • преобразования float в byte , short , int , long и char ;
  • преобразования double в byte , short , int , long , float и char ;

Мы не будем здесь подробно рассматривать правила, по которым происходят эти преобразования, поскольку интуитивно они понятны, а формально достаточно громоздки. При их применении важно помнить, что Java, в отличие от других языков, не генерирует ошибок при переполнении (overflow) или потере значения (underflow), поэтому контроль за корректностью преобразований полностью ложится на программиста.

5.4.1.3. Расширяющие преобразования ссылок

Расширяющие преобразования ссылок — это преобразования производных ссылочных типов в типы их предков, которые не требуют никаких действий на этапе исполнения и никогда не генерируют ошибок. Такими преобразованиями в Java являются:

  • преобразование любого класса или интерфейса в его предка (в частности, в тип Object);
  • преобразование класса в интерфейс, который он реализует;
  • преобразование любого массива в тип Object или тип Cloneable ;
  • преобразование массива типа S в массив типа T, если S и T — ссылочные типы, и преобразование S в T является расширяющим;
  • преобразование нулевого типа в любой ссылочной тип.

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

Сужающие преобразования ссылок — это преобразования производных ссылочных типов в типы их потомков. Эти преобразования требуют проверки своей легитимности на этапе исполнения и могут генерировать исключение ClassCastException . Такими преобразованиями в Java являются:

  • преобразование любого класса в его потомка (в частности, преобразование типа Object в любой другой класс);
  • преобразование класса в интерфейс, когда класс не является финальным и не реализует данный интерфейс (в частности, преобразование типа Object в любой интерфейс);
  • преобразование типа Object в любой массив;
  • преобразование любого интерфейса в класс, который не является финальным;
  • преобразование любого интерфейса в класс, который является финальным и реализует данный интерфейс;
  • преобразование интерфейса J в интерфейс K, когда J не является потомком K, и не существует метода, декларированного и в J, и в K с одинаковой сигнатурой, но разными типами результата;
  • преобразование массива типа S в массив типа T, если S и T — ссылочные типы, и преобразование S в T является сужающим.

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

Любое выражение в Java, включая null , может быть преобразовано в тип String .

5.4.1.6. Недопустимые преобразования

Следующие преобразования типов в Java запрещены:

  • преобразование любого ссылочного типа в любой примитивный тип;
  • преобразование любого примитивного типа в любой ссылочной тип, кроме типа String ;
  • преобразование нулевого типа в любой примитивный тип;
  • преобразования в нулевой тип или тип boolean ;
  • преобразования типа boolean в любой другой тип, кроме типа String ;
  • преобразование одного класса в другой, если ни один из них не является предком другого (кроме преобразования в тип String);
  • преобразование класса в интерфейс, если класс является финальным и не реализует данный интерфейс;
  • преобразование класса в массив, если класс отличен от Object ;
  • преобразование интерфейс в класс, который является финальным и не реализует данный интерфейс (кроме преобразования в тип String);
  • преобразование интерфейса J в интерфейс K, если существует метод, декларированный и в J, и в K с одинаковой сигнатурой, но разными типами результата;
  • преобразование массива в класс, отличный от Object и String ;
  • преобразование массива в интерфейс, отличный от Cloneable ;
  • преобразование массива типа S в массив типа T, если преобразование S в T является запрещенным

5.4.2. Контексты преобразований

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

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

  • переменная имеет тип byte , short или char ;
  • значением выражения является константа типа int , которая попадает в диапазон возможных значений переменной.

Например, оператор byte x = 123; допустим, поскольку константа 123 (имеющая тип int ) лежит в диапазоне допустимых значений типа byte .

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

Short s = 123; char c = s; // генерирует ошибку компиляции

приведет к генерации ошибки, поскольку типы char и short несовместимы по присваиванию согласно данных выше определениям (первый реализован 16-битовыми словами без знака, а второй — со знаком).

5.4.2.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(1, 2) ); // генерирует ошибку компиляции } }

Здесь класс Test содержит два одноименных метода, которые различаются только типами параметров. Если бы сужающие преобразования аргументов были в Java разрешены, то исполняющей системе пришлось бы определять, к какому из этих методов относится вызов m(1, 2) . Чтобы избежать подобных двусмысленностей, разработчики языка решили проблему радикально: они запретили подобные вызовы методов. В данной ситуации для вызова, к примеру, первого метода мы должны явно указать тип первого операнда (второй по умолчанию уже имеет тип int ), а именно m((byte)1, 2) .

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

Преобразование в строку происходит только в одном случае: когда бинарная операция + применяется к двум операндам, один из которых имеет тип String . В этой ситуации второй операнд также преобразуется к типу String , и результатом операции является конкатенация полученных строк. Подробнее этот процесс описан в гл. 5.14 .

5.4.2.4. Явное преобразование типа

Явное преобразование типа происходит, когда к операнду явно применяется операция приведения типа (type cast). В этой ситуации могут применяться все описанные выше виды преобразований типов, кроме преобразования в строку. Попытка явного преобразования к типу, отмеченная выше, как запрещенная, вызовет ошибку компиляции. Кроме того, на этапе выполнения возможна генерация исключения ClassCastException, если заданное преобразование недопустимо.

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

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

Перед выполнением унарной операции:

  • если операнд имеет тип byte , short или char , он преобразуется к типу int ;
  • в остальных случаях его тип не изменяется.

Перед выполнением бинарной операции:

  • если один из операндов типа double , то второй также преобразуется к типу double ;
  • float , то второй также преобразуется к типу float ;
  • в противном случае, если один из операндов типа long , то второй также преобразуется к типу long ;
  • в противном случае, оба операнда преобразуются к типу int .

Аннотация: Эта лекция посвящена вопросам преобразования типов. Поскольку Java – язык строго типизированный, компилятор и виртуальная машина всегда следят за работой с типами, гарантируя надежность выполнения программы. Однако во многих случаях то или иное преобразование необходимо осуществить для реализации логики программы. С другой стороны, некоторые безопасные переходы между типами Java позволяет осуществлять неявным для разработчика образом, что может привести к неверному пониманию работы программы. В лекции рассматриваются все виды преобразований, а затем все ситуации в программе, где они могут применяться. В заключение приводится начало классификации типов переменных и типов значений, которые они могут хранить. Этот вопрос будет подробнее рассматриваться в следующих лекциях.

Что все это означает? Начнем по порядку. Для простых типов расширение означает, что осуществляется переход от менее емкого типа к более емкому. Например, от типа byte (длина 1 байт) к типу int (длина 4 байта). Такие преобразования безопасны в том смысле, что новый тип всегда гарантированно вмещает в себя все данные, которые хранились в старом типе, и таким образом не происходит потери данных. Именно поэтому компилятор осуществляет его сам, незаметно для разработчика:

byte b=3; int a=b;

В последней строке значение переменной b типа byte будет преобразовано к типу переменной a (то есть, int ) автоматически, никаких специальных действий для этого предпринимать не нужно.

Следующие 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

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

Тем не менее, следует помнить, что даже при расширении данные все-таки могут быть в особых случаях искажены. Они уже рассматривались в предыдущей лекции, это приведение значений int к типу float и приведение значений типа long к типу float или double . Хотя эти дробные типы вмещают гораздо большие числа, чем соответствующие целые, но у них меньше значащих разрядов.

Повторим этот пример:

long a=111111111111L; float f = a; a = (long) f; print(a);

Результатом будет:

Обратное преобразование - сужение - означает, что переход осуществляется от более емкого типа к менее емкому. При таком преобразовании есть риск потерять данные. Например, если число типа int было больше 127, то при приведении его к byte значения битов старше восьмого будут потеряны. В Java такое преобразование должно совершаться явным образом, т.е. программист в коде должен явно указать, что он намеревается осуществить такое преобразование и готов потерять данные.

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

  • от byte к char
  • от 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

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

print((byte)383); print((byte)384); print((byte)-384);

Результатом будет:

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

Это верно и для типа char :

char c=40000; print((short)c);

Результатом будет:

Сужение дробного типа до целочисленного является более сложной процедурой. Она проводится в два этапа.

На первом шаге дробное значение преобразуется в long , если целевым типом является long , или в int - в противном случае (целевой тип byte , short , char или int ). Для этого исходное дробное число сначала математически округляется в сторону нуля, то есть дробная часть просто отбрасывается.

Например, число 3,84 будет округлено до 3 , а -3,84 превратится в -3 . При этом могут возникнуть особые случаи:

  • если исходное дробное значение является NaN , то результатом первого шага будет 0 выбранного типа (т.е. int или long );
  • если исходное дробное значение является положительной или отрицательной бесконечностью, то результатом первого шага будет, соответственно, максимально или минимально возможное значение для выбранного типа (т.е. для int или long );
  • наконец, если дробное значение было конечной величиной, но в результате округления получилось слишком большое по модулю число для выбранного типа (т.е. для int или long ), то, как и в предыдущем пункте, результатом первого шага будет, соответственно, максимально или минимально возможное значение этого типа. Если же результат округления укладывается в диапазон значений выбранного типа, то он и будет результатом первого шага.
  • и int вполне очевидны - дробные бесконечности преобразовались в, соответственно, минимально и максимально возможные значения этих типов. Результат для следующих трех типов (short , char , byte ) есть, по сути, дальнейшее сужение значений, полученных для int , согласно второму шагу процедуры преобразования. А делается это, как было описано, просто за счет отбрасывания старших битов. Вспомним, что минимально возможное значение в битовом виде представляется как 1000..000 (всего 32 бита для int , то есть единица и 31 ноль). Максимально возможное - 1111..111 (31 единица). Отбрасывая старшие биты, получаем для отрицательной бесконечности результат 0 , одинаковый для всех трех типов. Для положительной же бесконечности получаем результат, все биты которого равняются 1

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