Java и числа. От простого к сложному
Наш постоянный читатель Кирилл Сергеев делится с нами особенностями хранения чисел в памяти компьютера, используя примеры из Java. В первой части статьи он ярко и сочно рассказывает, как числа влияли на человечество на заре веков, а во второй объясняет, в каких форматах память компьютера хранит разные виды чисел, о числах с плавающей точкой в java и о многом другом интересном. Желаем приятного чтения!
Числа сквозь века: мифы, легенды, развитие
Люди на заре человечества создали огромное количество удивительных изобретений. Простых, может быть, даже примитивных. Но удивительно полезных. Сумевших не просто вывести человека на вершину пищевой цепочки, но и сделать его именно тем, кем он является ныне. Палка как примитивное орудие и отесанный камень вместе превратились в грозное оружие. Человек подчинил огонь и покорил воду. Человек приручил животных. Человек заговорил. А главное – стал использовать числа.
Сначала числа использовались исключительно для определения количества тех или иных однотипных предметов. Это были натуральные числа – как и предметы, счет которым с их помощью вели. Три шкуры, пять наконечников для стрел, два топора. Один, два, три, …, 15 членов общины, а дальше – «нас тьмы, и тьмы, и тьмы. Попробуйте, сразитесь с нами!».
Для нужд первобытного человека таких чисел хватало вполне. Но прогресс не удержать. Люди научились обменивать «что-нибудь ненужное» на предметы, необходимые в быту. А когда зародилась примитивная торговля, тут же появились профессиональные менялы и ростовщики. Чтобы начать охотиться, человек сначала должен где-то получить топор, копье или гарпун. Старшие товарищи уже обладают этими предметами. Так почему не одолжить у них орудие труда в счет будущей добычи или улова? Люди научились давать и брать в долг. Но если вы должны кому-то три беличьи шкурки, а у вас нет ни одной – получается, у вас минус три беличьи шкурки. Сначала не было топора, не было и шкурок. То есть – «все по нулям». Теперь – один топор и минус три охотничьих трофея. Люди научились считать количество целых неделимых предметов: как положительное, так и отрицательное. Так появились целые числа.
Человек развивался, менялись язык и представление о числах. Школ и гимназий еще не было – опыт поколений передавался из уст в уста. В том числе и с помощью сказок. Давайте вспомним и мы одну. Про лису и медведя.
Хитрая лиса поселилась у медведя и тайком по вечерам «хавала его ништяки» – опорожняла кадку с медом. Медведю она говорила, что уходит то на родины, то на крестины. А сама забиралась на чердак и лакомилась медком. В первый вечер она съела четверть кадушки и вернулась в дом. Медведь поинтересовался, как ребенка назвали. Лиса и говорит: «Верхушечкой». Во второй вечер лиса слямзила еще четверть кадки, осталась половина. Лиса вернулась в дом и на вопрос медведя ответила, что ребенка назвали Середочкой. В третий вечер лиса разъелась до того, что навернула всю оставшуюся половину меда. Медведю с гордостью сообщила, что ребенка окрестили Поскребушком. И то правда, мало ли чудных имен на свете?
Сказка – ложь, да в ней намек… Лиса не просто объедала медведя. Делала она это «дискретно», в три подхода, каждый раз съедая часть целого. Сначала она осилила четверть кадки, после этого осталось три четверти. Во второй раз лиса вновь съела четверть. Кадушка стала наполовину пустой. А может быть, наполовину полной. Как бы то ни было, осталась половина. В третий раз лиса до того распробовала медок, что съела всю оставшуюся половину разом. В результате лисьей «рационализации» целая кадка натурального меда превратилась в ноль. Люди тем временем освоили рациональные дроби.
Человек рос. Росли города. Развивались цивилизации: шумерская, египетская, древнегреческая. И такие там жили люди, что хлебом их не корми, а дай построить зиккурат, пирамиду или храм Артемиды. Касательно храма Артемиды – если не построить, то уж хотя бы сжечь.
Для создания всего этого великолепия древние строители пользовались «золотым сечением», извлекали квадратные корни, выводили на все лады число Пи. Почти всякий раз они сталкивались с бесконечными непериодическими десятичными дробями. Они казались им настолько чудными и отличными от рациональных дробей, что они их так и назвали – иррациональные числа. Правда, дальше этого древние геометры не пошли.
Только в Новое и Новейшее время появляется строгая теория вещественных чисел. Множество вещественных чисел, кроме рациональных, включает множество иррациональных чисел.
Сегодня человек при операциях с числами использует не единичную систему счисления, пальцы на руках и ногах, набор косточек или камушков, не устный счет, не абак, не хитроумные механические счетные машины. Компьютер теперь – не роскошь, а средство вычисления. Предлагаю рассмотреть, как хранятся числа в памяти компьютера. Особый упор сделаем на рассмотрение формата хранения вещественных чисел. Посмотрим, как это делается, и разберем примеры на современном, ультрановомодном и востребованном языке программирования высокого уровня – Java.
Как хранятся числа в памяти компьютера
Формат представления целых чисел в Java
С целыми положительными числами все предельно просто. Выделяется n-бит на число. Число в java переводится в двоичную систему счисления. Затем записывается последовательно с нулевого бита по n-1 бит. Старшие не значащие разряды обнуляются.
Рассмотрим пример. Пусть есть целое число 389. Как определить, в каком формате хранится это число?
Решение:
Переведем число 38910 в двоичную систему счисления.
38910 = 1100001012
Так как тип целочисленный, под его хранение отводится четыре байта или 32 бита. Таким образом, ответ может быть записан так:
31 0 |
00000000000000000000000110000101 |
Для проверки результата выполним небольшой код с выводом чисел на Java:
int i = 389; String intBits = Integer.toBinaryString(i); System.out.println("Разряды числа: " + intBits); Результат работы программы: Разряды числа: 110000101
Результат вычислений совпал с результатом работы программы.
Формат хранения целых отрицательных чисел в java уже интересней. Отрицательное целое число представлено в дополнительном коде. Для перевода числа в дополнительный код нужно перевести его в двоичную систему счисления. Результат перевода представить в обратном коде. Для этого нужно поразрядно заменить все «0» на «1», а «1» на «0». К полученному результату нужно прибавить «1».
Рассмотрим пример. Дано целое число -386. Как определить, в каком формате хранится это число?
Решение:
Переведем число 38610 в двоичную систему счисления.
38610 = 000000000000000000000001100000102
Представим результат перевода в обратном коде.
Обр. код: 11111111111111111111111001111101
Представим результат в дополнительном коде.
Доп. код: 11111111111111111111111001111101 + 1 =
= 11111111111111111111111001111110
Так как тип целочисленный, под его хранение отводится четыре байта или 32 бита. Таким образом, ответ может быть записан в следующем виде:
31 0 |
11111111111111111111111001111110 |
Для проверки результата выполним небольшой код на Java:
int i = -386; String intBits = Integer.toBinaryString(i); System.out.println("Разряды числа: " + intBits); Результат работы программы: Разряды числа: 11111111111111111111111001111110
Результат вычислений совпал с результатом работы программы.
Формат представления вещественных чисел
Вещественные числа хранятся в формате чисел в java с плавающей точкой, в которой число представлено в виде мантиссы и степени базы старшего разряда. Например, 75.3810 может быть записано в следующих видах:
0.7538 * 100 = 0.7538 * 102, здесь мантисса – 0.7538, база (основание системы счисления) – 10, степень старшего разряда (разряд десятков) – 2,
0.007538 * 10000 = 0.007538 * 104,
7538.0 / 100 = 7538 * 10-2.
В общем виде число с плавающей запятой состоит из знака мантиссы, знака порядка, порядка и мантиссы. Знак мантиссы определяет, больше нуля или меньше. Знак порядка показывает, в каком направлении смещается точка, а порядок определяет, на сколько знаков смещается точка. Наконец, мантисса представляет само число.
S*M*BQ,
где
S – знак числа (мантиссы);
M – мантисса числа;
B – основание системы счисления, у нас 10;
Q – порядок числа.
Приведенный пример демонстрирует, как точка перемещается вдоль цифр числа. Точка мечется, как стрелка осциллографа. Очевидно, что таких представлений может быть столько, на сколько хватит фантазии.
Такое положение дел никак не может устроить программистов и инженеров, разрабатывающих электронно-вычислительную аппаратуру и программы для нее. Представление чисел с плавающей точкой в java должно быть унифицировано и стандартизировано. Таким стандартом является IEEE 754. Этот стандарт предусматривает, что число всегда хранится в нормализованной форме. Для чисел, представленных в двоичном коде, это означает, что точка будет сдвигаться влево или вправо до тех пор, пока в старшем бите мантиссы не окажется «1». При этом «1» в мантиссу не записывается. Она становится «неявной». Делается это для экономии одного разряда. Аппаратные средства устроены так, что они сами «помнят» о ее существовании и действуют с мантиссой так, как будто она там есть. То есть, мантисса будет иметь вид 1.M.
Показатель степени хранится в виде целого числа в коде со сдвигом 1023 или 127, зависит от точности. Это означает, что сдвиг точки для числа с двойной точностью хранится не в виде +2, -1, +6, а в виде 2+1023, -1+1023, 6+1023. Поэтому он всегда положительный. А оборудование само вычисляет, на сколько порядков переместить точку в мантиссе, и определяет направление сдвига. Разберем вышесказанное на примере.
Представим число 75.3810 в формате представления числа с плавающей точкой двойной точности. В Java это тип double. Построим это представление, исходя из определений и стандарта IEEE 754. В общем виде формат представления будет таким:
63 |
62 52 |
51 0 |
Знак |
Порядок |
Мантисса |
Под все число отводится 64 бита. Под знак – 1 бит. Под порядок – 11 бит. Под мантиссу – 52 бита.
Сначала переведем число 75.3810 в двоичную систему счисления. Перевод целой и дробной части осуществляется по-разному. Целая часть получается путем деления ее на 2 и записи остатков от деления в порядке, обратном их возникновению. Дробная часть получается путем ее умножения на 2 и записи целых частей в порядке их возникновения.
7510 = 10010112
0.3810 = 01100001010001111010111000010100011110101110002
75.3810 = 1001011.01100001010001111010111000010100011110101110002
Теперь нужно сдвинуть целую часть вправо так, чтобы в целой части осталась одна единица. Эта единица неявная. Она не будет записана в мантиссу числа.
Получим:
1.M = 1.0010110110000101000111101011100001010001111010111000,
точка сдвинулась на шесть разрядов влево.
M = 0010110110000101000111101011100001010001111010111000
Представление мантиссы получили. Дело за малым – получить представление порядка. Помним, что порядок хранится в коде со сдвигом 1023. Поэтому, если мы сдвигали точку влево на шесть разрядов, мы должны вычислить выражение
610 + 102310 = 102910
и перевести результат в двоичную систему счисления.
Q = 102910 = 100000001012
Со знаком все просто. Число у нас положительное, поэтому
S = 0.
Запишем результат:
63 |
62 52 |
51 0 |
0 |
10000000101 |
0010110110000101000111101011100001010001111010111000 |
Теперь воспроизведем полученный результат программно. Для этого выполним следующий код:
double d = 75.38; String sResult = ""; long numberBits = Double.doubleToLongBits(d); sResult = Long.toBinaryString(numberBits); System.out.println("Представление вещественного числа в формате чисел с плавающей точкой"); System.out.format("Число: %5.2f\n", d); System.out.println("Формат чисел с плавающей точкой:"); //ведущий ноль заботливо сокращен системой, поэтому его нужно восстановить System.out.println(d > 0 ? "0" + sResult : sResult); Результат работы программы: Представление вещественного числа в формате чисел с плавающей точкой Число: 75,38 Формат чисел с плавающей точкой: 0100000001010010110110000101000111101011100001010001111010111000
Результат вычислений совпал с результатом работы программы.
Рассмотрим пример. Для закрепления представим число Пи в форме представления чисел с плавающей точкой.
Решение:
Переведем Пи из десятичной системы счисления в двоичную.
3,14159265358979310 = 11.0010010000111111011010101000100010000101101000110002
Число положительное – значит, знак равен 0.
Определяем мантиссу. В старшем разряде должна остаться одна 1. Поэтому нужно сдвинуть точку на один разряд влево.
1.M = 1.1001001000011111101101010100010001000010110100011000
M = 1001001000011111101101010100010001000010110100011000
Теперь определим порядок. Точка была смещена на один разряд влево. Получаем
Q = 1 + 1023 = 102410 = 100000000002
Запишем результат:
63 |
62 52 |
51 0 |
0 |
10000000000 |
100100100001111110110101010001000100001011010001100 |
Воспроизведем полученный результат программно. Для этого выполним следующий код:
double d = Math.PI; String sResult = ""; long numberBits = Double.doubleToLongBits(d); sResult = Long.toBinaryString(numberBits); System.out.println("Представление вещественного числа в формате чисел с плавающей точкой"); System.out.format("Число: %10.15f\n", d); System.out.println("Формат чисел с плавающей точкой:"); //ведущий ноль заботливо сокращен системой, поэтому его нужно восстановить System.out.println(d > 0 ? "0" + sResult : sResult); Результат работы программы: Представление вещественного числа в формате чисел с плавающей точкой Число: 3,141592653589793 Формат чисел с плавающей точкой: 0100000000001001001000011111101101010100010001000010110100011000
Результат вычислений совпал с результатом работы программы.
Вместо выводов и заключения
Запустим на выполнение небольшой код и посмотрим на результат.
double d = 5.0/7; System.out.format("Число: %10.16f\n", d); d += 300000; System.out.format("Число: %10.16f\n", d); Результат работы программы Число: 0,7142857142857143 Число: 300000,7142857142600000
Что стало с точностью? Откуда эти нули? Ее вытеснила целая часть. Перемещаясь вправо, целая часть вытеснила из мантиссы младшие разряды. Ведь всего под нее предоставлено 52 бита. Поэтому младшие разряды просто вытолкнуло за правую границу мантиссы. Точность оказалась за нулевым разрядом. Таким образом, очевидно, что представление вещественных чисел в формате чисел с плавающей точкой – это компромисс между точностью и диапазоном представляемых значений.