Н.А. Вязовик - Программирование на Java
Теперь значение переменной y на самом деле равно не 2147483648, поскольку такое число не укладывается в область значений типа int, а в точности равно значению x! Другими словами, в этом примере выражение -x==x истинно!
Дело в том, что если при выполнении числовых операций над целыми числами возникает переполнение и результат не может быть сохранен в данном примитивном типе, то Java не создает никаких ошибок. Вместо этого все старшие биты, которые превышают вместимость типа, просто отбрасываются. Это может привести не только к потере точной абсолютной величины результата, но даже к искажению его знака, если на месте знакового бита окажется противоположное значение.
int x= 300000;
print(xx);
Результатом такого примера будет:
-194313216
Возвращаясь к инвертированию числа -2147483648, мы видим, что математический результат равен в точности +231, или, в двоичном формате, 1000 0000 0000 0000 0000 0000 0000 0000 (единица и 31 ноль). Но тип int рассматривает первую единицу как знаковый бит, и результат получается равным -2147483648.
Таким образом, явное выписывание в коде литералов, которые слишком велики для используемых типов, приводит к ошибке компиляции (см. лекцию 3). Если же переполнение возникает в результате выполнения операции, "лишние" биты просто отбрасываются.
Подчеркнем, что выражение типа -5 не является целочисленным литералом. На самом деле оно состоит из литерала 5 и оператора -. Напомним, что некоторые литералы (например, 2147483648 ) могут встречаться только в сочетании с унарным оператором -.
Кроме того, числовые операции в Java обладают еще одной особенностью. Хотя целочисленные типы имеют длину 8, 16, 32 и 64 бита, вычисления проводятся только с 32-х и 64-х битной точностью. А это значит, что перед вычислениями может потребоваться преобразовать тип одного или нескольких операндов.
Если хотя бы один аргумент операции имеет тип long, то все аргументы приводятся к этому типу и результат операции также будет типа long. Вычисление будет произведено с точностью в 64 бита, а более старшие биты, если таковые появляются в результате, отбрасываются.
Если же аргументов типа long нет, то вычисление производится с точностью в 32 бита, и все аргументы преобразуются в int (это относится к byte, short, char ). Результат также имеет тип int. Все биты старше 32-го игнорируются.
Никакого способа узнать, произошло ли переполнение, нет. Расширим рассмотренный пример:
int i=300000;
print(i*i);
// умножение с точностью 32 бита
long m=i;
print(m*m);
// умножение с точностью 64 бита
print(1/(m-i));
// попробуем получить разность
// значений int и long
Результатом такого примера будет:
-194313216
90000000000
затем мы получим ошибку деления на ноль, поскольку переменные i и m хоть и разных типов, но хранят одинаковое математическое значение и их разность равна нулю. Первое умножение производилось с точностью в 32 бита, более старшие биты были отброшены. Второе – с точностью в 64 бита, ответ не исказился.
Вопрос приведения типов, и в том числе специальный оператор для такого действия, подробно рассматривается в следующих лекциях. Однако здесь хотелось бы отметить несколько примеров, которые не столь очевидны и могут создать проблемы при написании программ. Во-первых, подчеркнем, что результатом операции с целочисленными аргументами всегда является целое число. А значит, в следующем примере
double x = 1/2;
переменной x будет присвоено значение 0, а не 0.5, как можно было бы ожидать. Подробно операции с дробными аргументами рассматриваются ниже, но чтобы получить значение 0.5, достаточно написать 1./2 (теперь первый аргумент дробный и результат не будет округлен).
Как уже упоминалось, время в Java измеряется в миллисекундах. Попробуем вычислить, сколько миллисекунд содержится в неделе и в месяце:
print(1000*60*60*24*7);
// вычисление для недели
print(1000*60*60*24*30);
// вычисление для месяца
Необходимо перемножить количество миллисекунд в одной секунде (1000), секунд – в минуте (60), минут – в часе (60), часов – в дне (24) и дней — в неделе и месяце (7 и 30, соответственно). Получаем:
604800000
-1702967296
Очевидно, во втором вычислении произошло переполнение. Достаточно сделать последний аргумент величиной типа long:
print(1000*60*60*24*30L);
// вычисление для месяца
Получаем правильный результат:
2592000000
Подобные вычисления разумно переводить на 64-битную точность не на последней операции, а заранее, чтобы избежать переполнения.
Понятно, что типы большей длины могут хранить больший спектр значений, а потому Java не позволяет присвоить переменной меньшего типа значение большего типа. Например, такие строки вызовут ошибку компиляции:
// пример вызовет ошибку компиляции
int x=1;
byte b=x;
Хотя для программиста и очевидно, что переменная b должна получить значение 1, что легко укладывается в тип byte, однако компилятор не может вычислять значение переменной x при обработке второй строки, он знает лишь, что ее тип – int.
А вот менее очевидный пример:
// пример вызовет ошибку компиляции
byte b=1;
byte c=b+1;
И здесь компилятор не сможет успешно завершить работу. При операции сложения значение переменной b будет преобразовано в тип int и таким же будет результат сложения, а значит, его нельзя так просто присвоить переменной типа byte.
Аналогично:
// пример вызовет ошибку компиляции
int x=2;
long y=3;
int z=x+y;
Здесь результат сложения будет уже типа long. Точно так же некорректна такая инициализация:
// пример вызовет ошибку компиляции
byte b=5;
byte c=-b;
Даже унарный оператор " - " проводит вычисления с точностью не меньше 32 бит.
Хотя во всех случаях инициализация переменных приводилась только для примера, а предметом рассмотрения были числовые операции, укажем корректный способ преобразовать тип числового значения:
byte b=1;
byte c=(byte)-b;
Итак, все числовые операторы возвращают результат типа int или long. Однако существует два исключения.
Первое из них – операторы инкрементации и декрементации. Их действие заключается в прибавлении или вычитании единицы из значения переменной, после чего результат сохраняется в этой переменной и значение всей операции равно значению переменной (до или после изменения, в зависимости от того, является оператор префиксным или постфиксным). А значит, и тип значения совпадает с типом переменной. (На самом деле, вычисления все равно производятся с точностью минимум 32 бита, однако при присвоении переменной результата его тип понижается.)
byte x=5;
byte y1=x++;
// на момент начала исполнения x равен 5
byte y2=x--;
// на момент начала исполнения x равен 6
byte y3=++x;
// на момент начала исполнения x равен 5
byte y4=--x;
// на момент начала исполнения x равен 6
print(y1);
print(y2);
print(y3);
print(y4);
В результате получаем:
5
6
6
5
Никаких проблем с присвоением результата операторов ++ и -- переменным типа byte. Завершая рассмотрение этих операторов, приведем еще один пример:
byte x=-128;
print(-x);
byte y=127;
print(++y);
Результатом будет:
128
-128
Этот пример иллюстрирует вопросы преобразования типов при вычислениях и случаи переполнения.
Вторым исключением является оператор с условием ?:. Если второй и третий операнды имеют одинаковый тип, то и результат операции будет такого же типа.
byte x=2;
byte y=3;
byte z=(x>y) ? x : y;
// верно, x и y одинакового типа
byte abs=(x>0) ? x : -x;
// неверно!
Последняя строка неверна, так как третий аргумент содержит числовую операцию, стало быть, его тип int, а значит, и тип всей операции будет int, и присвоение некорректно. Даже если второй аргумент имеет тип byte, а третий – short, значение будет типа int.
Наконец, рассмотрим оператор конкатенации со строкой. Оператор + может принимать в качестве аргумента строковые величины. Если одним из аргументов является строка, а вторым – целое число, то число будет преобразовано в текст и строки объединятся.
int x=1;
print("x="+x);
Результатом будет:
x=1
Обратите внимание на следующий пример:
print(1+2+"text");
print("text"+1+2);
Его результатом будет:
3text
text12
Отдельно рассмотрим работу с типом char. Значения этого типа могут полноценно участвовать в числовых операциях:
char c1=10;
char c2='A';
// латинская буква A (u0041, код 65)
int i=c1+c2-'B';
Переменная i получит значение 9.
Рассмотрим следующий пример:
char c='A';
print(c);
print(c+1);
print("c="+c);
print('c'+'='+с);
Его результатом будет:
A
66
c=A
225
В первом случае в метод print было передано значение типа char, поэтому отобразился символ. Во втором случае был передан результат сложения, то есть число, и именно число появилось на экране. Далее при сложении со строкой тип char был преобразован в текст в виде символа. Наконец в последней строке произошло сложение трех чисел: 'c' (код 99), '=' (код 61) и переменной c (т.е. код 'A' - 65 ).