JavaRush /Java блог /Random UA /Влаштування дійсних чисел
Professor Hans Noodles
41 рівень

Влаштування дійсних чисел

Стаття з групи Random UA
Вітання! У сучасній лекції розповімо про числах в Java, саме — про матеріальних числах. Влаштування дійсних чисел - 1Без паніки! :) Жодних математичних складнощів у лекції не буде. Говоритимемо про речові числа виключно з нашої, «програмістської» точки зору. Отже, що таке «речові числа»? Речові числа - це числа, які мають дробову частину (вона може бути нульовою). Вони можуть бути позитивними чи негативними. Ось кілька прикладів: 15 56.22 0.0 1242342343445246 -232336.11 Як же влаштовано речовинне число? Досить просто: воно складається з цілої частини, дробової частини та знаку. У позитивних чисел знак зазвичай не вказують явно, а негативні вказують. Раніше ми докладно розібрали, які операції над числами можна здійснювати Java. Серед них було багато стандартних математичних операцій — додавання, віднімання тощо. буд. Було й дещо нове для тебе: наприклад, залишок від поділу. Але як саме влаштовано роботу з числами всередині комп'ютера? Як вони зберігаються у пам'яті?

Зберігання дійсних чисел у пам'яті

Думаю, тобі не стане відкриттям, що числа бувають великими і маленькими :) Їх можна порівнювати один з одним. Наприклад, число 100 менше від числа 423324. Чи впливає це на роботу комп'ютера та нашої програми? Насправді так . Кожне число представлено в Java певним діапазоном значень :
Тип Розмір у пам'яті (біт) Діапазон значень
byte 8 біт від -128 до 127
short 16 біт від -32768 до 32767
char 16 біт беззнакове ціле число, яке є символом UTF-16 (літери та цифри)
int 32 біта від -2147483648 до 2147483647
long 64 біта від -9223372036854775808 до 9223372036854775807
float 32 біта від 2 -149 до (2-2 -23 )*2 127
double 64 біта від 2 -1074 до (2-2 -52 )*2 1023
Сьогодні поговоримо саме про останні два типи — floatі double. Обидва виконують одну й ту саму задачу — являють собою дробові числа. Їх ще дуже часто називають " числа з плаваючою точкою" . Запам'ятай цей термін на майбутнє:) Наприклад, число 2.3333 або 134.1212121212. Досить дивно. Адже виходить, немає жодної різниці між цими двома типами, якщо вони виконують одну й ту саму задачу? Але різниця є. Зверніть увагу на стовпець «розмір у пам'яті» у таблиці вище. Усі числа (та й не тільки числа – взагалі вся інформація) зберігається у пам'яті комп'ютера у вигляді бітів. Біт - це найменша одиниця виміру інформації. Вона досить проста. Будь-який біт дорівнює або 0, або 1. Та й саме слово " bit " походить від англійської " binary digit"» - Двійкове число. Думаю, ти, напевно, чув про існування двійкової системи числення в математиці. Будь-яке звичне нам десяткове число можна у вигляді набору одиниць і нулів. Наприклад, число 584.32 у двійковій системі буде виглядати так: 100100100001010001111 . Кожна одиниця та нуль у цьому числі є окремим бітом. Тепер тобі має бути зрозуміліша різниця між типами даних. Наприклад, якщо ми створюємо число типу float, у нашому розпорядженні є лише 32 біти. При створенні числа floatстільки місця буде виділено для нього в пам'яті комп'ютера. Якщо ж ми хочемо створити число 123456789.65656565656565, у двійковому вигляді воно виглядатиме так: 111010110111100110100010101101010000000. Воно складається з 38 одиниць та нулів, тобто для його зберігання в пам'яті потрібно 38 біт. У тип floatце число просто не влізе! Тому число 123456789 можна як типу double. Для його зберігання виділяється цілих 64 біти: це нам підходить! Зрозуміло, і діапазон значень теж буде сприятливим. Для зручності ти можеш представляти число як маленький ящик із осередками. Якщо осередків вистачає для зберігання кожного біта, значить, тип даних обраний правильно :) Влаштування дійсних чисел - 2Зрозуміло, різна кількість пам'яті, що виділяється, впливає і на саме число. Зверніть увагу, що у типів floatі doubleвідрізняється діапазон значень. Що це означає на практиці? Число doubleможе виразити більшу точність, ніж число float. У 32-бітних чисел з плаваючою точкою (у Java це якраз типfloat) точність становить приблизно 24 біти, тобто близько 7 знаків після коми. А у 64-бітних чисел (в Java це тип double) - точність приблизно 53 біти, тобто приблизно 16 знаків після коми. Ось приклад, який добре демонструє цю різницю:
public class Main {

   public static void main(String[] args)  {

       float f = 0.0f;
       for (int i=1; i <= 7; i++) {
           f += 0.1111111111111111;
       }

       System.out.println(f);
   }
}
Що ми повинні отримати тут як результат? Здавалося б, усе досить просто. У нас є число 0.0, і ми 7 разів поспіль додаємо до нього 0.11111111111111111. У результаті має вийти 0.77777777777777777. Але ми створабо число float. Його розмір обмежений 32 бітами і, як ми сказали раніше, він здатний відобразити число приблизно до 7 знак після коми. Тому результат, який ми отримаємо в консолі, відрізнятиметься від того, що ми очікували:

0.7777778
Число ніби було «обрізане». Ти вже знаєш як зберігаються дані в пам'яті - у вигляді бітів, тому тебе не повинно це дивувати. Зрозуміло, чому це сталося: результат 0.77777777777777777 просто не вліз у виділені нам 32 біти, тому і був обрізаний так, щоб поміститися в змінну типу :) Ми можемо змінити floatтип змінної на doubleнашому прикладі, і тоді підсумковий результат не буде обрізаний:
public class Main {

   public static void main(String[] args)  {

       double f = 0.0;
       for (int i=1; i <= 7; i++) {
           f += 0.1111111111111111;
       }

       System.out.println(f);
   }
}

0.7777777777777779
Тут уже 16 знаків після коми, результат «змістився» у 64 біти. До речі, можливо, ти помітив, що в обох випадках результати вийшли не зовсім коректними? Підрахунок було зроблено з невеликими помилками. Про причини цього ми поговоримо нижче:) Тепер скажемо пару слів про те, як можна порівняти числа між собою.

Порівняння дійсних чисел

Ми частково вже торкалися цього питання в минулій лекції, коли говорабо про операції порівняння. Такі операції як >, <, >=, <=повторно розбирати ми не будемо. Натомість розглянемо цікавіший приклад:
public class Main {

   public static void main(String[] args)  {

       double f = 0.0;
       for (int i=1; i <= 10; i++) {
           f += 0.1;
       }

       System.out.println(f);
   }
}
Як ти думаєш, скільки буде виведено на екран? Логічною відповіддю була б відповідь: число 1. Ми починаємо відлік із числа 0.0 і послідовно додаємо до нього 0.1 десять разів поспіль. Начебто все правильно, повинна вийти одиниця. Спробуй запустити цей код, і відповідь сильно здивує тебе :) Висновок в консоль:

0.9999999999999999
Але чому такому простому прикладі виникла помилка? О_о Тут би навіть п'ятикласник легко відповів, але програма на Java видала неточний результат. «Неточний» тут найкраще слово, ніж «неправильний». Ми таки отримали дуже близьке до одиниці число, а не просто якесь рандомне значення :) Воно відрізняється від правильного буквально на міліметр. Але чому? Можливо, це просто одноразова помилка. Може, комп заглючив? Спробуємо написати інший приклад.
public class Main {

   public static void main(String[] args)  {

       //додаємо до нуля 0.1 одинадцять разів поспіль
       double f1 = 0.0;
       for (int i = 1; i <= 11; i++) {
           f1 += .1;
       }

       //Помножуємо 0.1 на 11
       double f2 = 0.1 * 11;

       //повинно вийти одне й те саме - 1.1 в обох випадках
       System.out.println("f1 = " + f1);
       System.out.println("f2 = " + f2);

       // Перевіримо!
       if (f1 == f2)
           System.out.println("f1 та f2 рівні!");
       else
           System.out.println("f1 та f2 не рівні!");
   }
}
Виведення в консоль:

f1 = 1.0999999999999999
f2 = 1.1
f1 и f2 не равны!
Так, справа явно не в глюках компа :) Що відбувається? Подібні помилки пов'язані з тим, як числа представлені у двійковому вигляді в пам'яті комп'ютера. Справа в тому, що в двійковій системі неможливо точно уявити число 0,1 . У десятковій системі, до речі, теж є подібна проблема: в ній не можна правильно уявити дроби (і замість ⅓ ми отримаємо 0.333333333333333, що теж не зовсім правильний результат). Здавалося б, дрібниця: за таких підрахунків різниця може бути в одну стотисячну частину (0,00001) або навіть менша. Але якщо від цього порівняння залежатиме весь результат роботи твоєї Дуже Серйозної Програми?
if (f1 == f2)
   System.out.println("Ракета летить у космос");
else
   System.out.println("Запуск скасовується, всі розходяться додому");
Ми явно очікували, що два числа будуть рівними, але через особливості внутрішнього пристрою пам'яті ми скасували запуск ракети. Влаштування дійсних чисел - 3Раз так, нам потрібно визначитися, як все ж таки порівняти два числа з плаваючою точкою, щоб результат порівняння був більш ... еммм ... передбачуваним. Отже, правило №1 при порівнянні дійсних чисел ми вже засвоїли: ніколи не використовуй ==при порівнянні чисел із плаваючою точкою. Ок, поганих прикладів, думаю, досить:) Давай розглянемо гарний приклад!
public class Main {

   public static void main(String[] args)  {

       final double threshold = 0.0001;

       //додаємо до нуля 0.1 одинадцять разів поспіль
       double f1 = .0;
       for (int i = 1; i <= 11; i++) {
           f1 += .1;
       }

       //Помножуємо 0.1 на 11
       double f2 = .1 * 11;

       System.out.println("f1 = " + f1);
       System.out.println("f2 = " + f2);

       if (Math.abs(f1 - f2) < threshold)
           System.out.println("f1 та f2 рівні");
       else
           System.out.println("f1 та f2 не рівні");
   }
}
Тут ми по суті робимо те саме, але змінюємо спосіб порівняння чисел. Ми маємо спеціальне «порогове» число — 0.0001, одна десятитисячна. Воно може бути іншим. Це залежить від того, наскільки точне порівняння тобі потрібне в конкретному випадку. Можна зробити його і більше, і менше. За допомогою методу Math.abs()ми одержуємо модуль числа. Модуль — це значення числа, незалежно від знака. Наприклад, у чисел -5 і 5 модуль буде однаковим і дорівнює 5. Ми віднімаємо друге число з першого, і якщо отриманий результат, незалежно від знака, буде меншим за той поріг, який ми встановабо, значить наші числа рівні. У всякому разі, вони рівні настільки точності, яку ми встановабо за допомогою нашого «порогового числа», тобто як мінімум вони дорівнюють аж до однієї десятитисячної. Такий спосіб порівняння позбавить тебе несподіваної поведінки, яку ми побачабо у випадку з ==. Ще один хороший спосіб порівняння дійсних чисел - використовувати спеціальний клас BigDecimal. Цей клас спеціально був створений для зберігання дуже великих чисел із дрібною частиною. На відміну від doubleі floatпри використанні BigDecimalдодавання, віднімання та інші математичні операції виконуються не за допомогою операторів ( +-і т.д.), а за допомогою методів. Ось як це буде виглядати у нашому випадку:
import java.math.BigDecimal;

public class Main {

   public static void main(String[] args)  {

       /*Створюємо два об'єкти BigDecimal - нуль та 0.1.
       Робимо те саме, що й раніше - додаємо 0.1 до нуля 11 разів поспіль
       У класі BigDecimal додавання здійснюється за допомогою методу add()*/
       BigDecimal f1 = new BigDecimal(0.0);
       BigDecimal pointOne = new BigDecimal(0.1);
       for (int i = 1; i <= 11; i++) {
           f1 = f1.add(pointOne);
       }

       /*Тут теж нічого не змінилося: створюємо два об'єкти BigDecimal
       та множимо 0.1 на 11
       У класі BigDecimal множення здійснюється за допомогою методу multiply()*/
       BigDecimal f2 = new BigDecimal(0.1);
       BigDecimal eleven = new BigDecimal(11);
       f2 = f2.multiply(eleven);

       System.out.println("f1 = " + f1);
       System.out.println("f2 = " + f2);

       /*Ще одна особливість BigDecimal - об'єкти чисел потрібно порівнювати між
       собою за допомогою спеціального методу compareTo()*/
       if (f1.compareTo(f2) == 0)
           System.out.println("f1 та f2 рівні");
       else
           System.out.println("f1 та f2 не рівні");
   }
}
Який висновок у консоль ми отримаємо?

f1 = 1.1000000000000000610622663543836097232997417449951171875
f2 = 1.1000000000000000610622663543836097232997417449951171875
f1 и f2 равны
Ми отримали саме той результат, на який розраховували. І зверни увагу, наскільки точними вийшли наші числа, і скільки знаків після коми у них вмістилося! Набагато більше, ніж у floatі навіть у double! Запам'ятай клас BigDecimalна майбутнє, він тобі обов'язково стане в нагоді :) Фух! Лекція вийшла чималенькою, але ти впорався: молодець! :) Побачимося на наступному занятті, майбутній програміст!
Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ