JavaRush /Java блог /Random UA /Примітивні типи в Java: Не такі вони і примітивні
Viacheslav
3 рівень

Примітивні типи в Java: Не такі вони і примітивні

Стаття з групи Random UA

Вступ

Розробку додатків можна як роботу з деякими даними, а точніше — їх зберігання та обробку. Сьогодні хотілося б торкнутися першого ключового аспекту. Як дані зберігаються у Java? Тут у нас є два можливі формати: посилальний та примітивний тип даних. Давайте поговоримо про види примітивних типів та можливості роботи з ними (як не крути, це фундамент наших знань мови програмування). Примітивні типи даних Java - це основа, де тримається все. Ні, я анітрохи не перебільшую. Oracle примітивам присвячений окремий Tutorial: Primitive Data Types Примітивні типи в Java: Не такі вони і примітивні - 1 Трохи історії. Спочатку був нуль. Але нуль – це нудно. І тоді з'явився bit(біт). Чому його так назвали? Назвали його від скорочення " bi nary digi t " (двійкове число). Тобто він має лише два значення. А оскільки був нуль, то логічно, що тепер стало чи 0, чи 1. І стало жити веселіше. Біти почали збиратися у зграї. І ці зграї стали називати byte(Байт). У світі byte = 2 у третій ступеня, тобто. 8. Але виявляється, так було не завжди. Існує безліч здогадів, легенд та чуток, звідки пішла назва byte. Хтось вважає, що вся справа в кодуваннях того часу, а хтось вважає, що так було вигідніше рахувати інформацію. Байт - це найменша частина пам'яті, що адресаується. Саме байти мають унікальні адресаи у пам'яті. Є легенда про те, що ByTe – це скорочення від Binary Term – машинне слово. Машинне слово – якщо говорити просто, це кількість даних, які процесор може обробити за операцію. Раніше розмір машинного слова збігався з найменшою пам'яттю, що адресаується. У Java змінні можуть зберігати тільки значення байтів. Як я й говорив вище, у Java існує два види змінних:
  • примітивні типи java, зберігають безпосередньо значення байтів даних (детальніше типи цих примітивів ми розберемо трохи нижче);
  • посилання тип, зберігає байти адресаи об'єкта в Heap, тобто через ці змінні ми отримуємо доступ безпосередньо до самого об'єкта (такий собі пульт від об'єкта)

Java byte

Отже, історія подарувала нам байт – мінімальний обсяг пам'яті, який ми можемо використати. І складається він із 8 біт. Найменший цілий тип даних у java - byte. Це знаковий 8-бітовий тип. Що це означає? Давайте рахувати. 2 ^ 8 буде 256. Але що робити, якщо ми хочемо негативне число? І вирішабо розробники Java, що двійковий код «10000000» позначатиме -128, тобто старший біт (найлівіший біт) позначатиме, чи негативне число. Двійкове «0111 1111» дорівнює 127. Тобто 128 не позначити, т.к. це буде -128. Повний розрахунок наведений у цій відповіді: Why is the range of bytes -128 to 127 in Java? Щоб зрозуміти, як виходять числа, варто подивитися на картинку:
Примітивні типи в Java: Не такі вони і примітивні - 2
Відповідно, щоб обчислити розмір 2 ^ (8-1) = 128. Значить мінімальна межа (а вона з мінусом) буде -128. А максимальна 128 – 1 (віднімаємо нуль). Тобто максимум буде 127. Насправді з типом byte працюємо ми не так часто на "високому рівні". Здебільшого це обробка «сирих» даних. Наприклад, при роботі з передачею даних через мережу, коли дані це набір 0 і 1, переданих через якийсь канал зв'язку. Або під час читання даних із файлів. Також можуть бути використані при роботі з рядками та кодуваннями. Приклад коду:
public static void main(String []args){
        byte value = 2;
        byte shortByteValue = 0b10; // 2
        System.out.println(shortByteValue);
        // Начиная с JDK7 мы можем разделять литералы подчёркиваниями
        byte minByteValue = (byte) 0B1000_0000; // -128
        byte maxByteValue = (byte) 0b0111_1111; // 127
        byte minusByteValue = (byte) 0b1111_1111; // -128 + 127
        System.out.println(minusByteValue);
        System.out.println(minByteValue + " to " + maxByteValue);
}
До речі, не варто думати, що використання типу byte знижуватиме споживання пам'яті. В основному byte використовується для зменшення витрати пам'яті при зберіганні даних у масивах (наприклад, зберігання даних, отриманих по мережі в деякому буфері, який буде реалізований у вигляді масиву байт). А ось при операціях над даними використання byte не виправдає ваших очікувань. Це пов'язано з реалізацією Java Virtual Machine (JVM). Так як більшість систем 32 або 64 розрядні, то byte і short при обчисленнях будуть приведені до 32-бітного int, про яке ми поговоримо далі. Так простіше робити обчислення. Докладніше див. Is adition of byte converts to int because of java language rules or because of jvm?. У відповіді дано посилання на JLS (Java Language Specification). Крім того, використання byte в неправильному місці може призвести до незручних моментів:
public static void main(String []args){
        for (byte i = 1; i <= 200; i++) {
            System.out.println(i);
        }
}
Тут буде зациклювання. Тому що значення лічильника сягне максимуму (127), відбудеться переповнення і значення стане -128. І ми ніколи не вийдемо із циклу.

short

Ліміт значень із byte досить малий. Тому для наступного типу даних вирішабо збільшити кількість біт удвічі. Тобто тепер не 8 біт, а 16. Тобто 2 байти. Значення можна вважати так само. 2 ^ (16-1) = 2 ^ 15 = 32768. Отже, діапазон від -32768 до 32767. Використовують його дуже рідко для будь-яких спеціальних випадків. Як каже нам документація мови Java: " ви можете використовувати як шорти до збереження пам'яті в великих arrays ".

int

Ось ми і дісталися найчастіше використовуваного типу. Займає він 32 біти, або 4 байти. Загалом ми продовжуємо подвоювати. Діапазон значень від -2 ^ 31 до 2 ^ 31 - 1.

Максимальне значення int

Максимальне значення int 2147483648 - 1, що зовсім не мало. Як було зазначено, оптимізації обчислень, т.к. сучасним комп'ютерам з урахуванням їхньої розрядності зручніше вважати, дані можуть бути неявно перетворені до int. Ось простий приклад:
byte a = 1;
byte b = 2;
byte result = a + b;
Такий невинний код, а ми отримаємо помилку: "error: incompatible types: possible lossy conversion from int to byte". Прийде виправити на byte result = (byte)(a + b); І ще один невинний приклад. Що буде, якщо запустимо наступний код?
int value = 4;
System.out.println(8/value);
System.out.println(9/value);
System.out.println(10/value);
System.out.println(11/value);
А ми отримаємо висновок
2
2
2
2
*звуки паніки*
Справа в тому, що при роботі з int значеннями залишок відкидається, залишаючи тільки цілу частину (у таких випадках краще вже використовувати double).

long

Продовжуємо подвоювати. 32 множимо на 2 і отримуємо 64 біти. За традицією, це 4*2, тобто 8 байт. Діапазон значень від -2 63 до 2 63 - 1. Більш ніж достатньо. Цей тип дозволяє вважати великі-великі числа. Часто використовується під час роботи з часом. Або з великими відстанями, наприклад. Для позначення те, що число це long після числа ставлять літерал L – Long. Приклад:
long longValue = 4;
longValue = 1l; // Не ошибка, но плохо читается
longValue = 2L; // Идеально
Хочеться забігти вперед. Далі ми розглядатимемо той факт, що для примітивів є відповідні обгортки, які дають можливість працювати з примітивами як з об'єктами. Але є цікава особливість. Ось приклад: На тому ж Tutorialspoint online compiler можете перевірити такий код:
public class HelloWorld {

     public static void main(String []args) {
        printLong(4);
     }

    public static void printLong(long longValue) {
        System.out.println(longValue);
    }
}
Цей код працює без помилок, все добре. Але варто в методі printLong замінити тип з long на Long (тобто тип стає не примітивним, а об'єктним), як стає джаві незрозуміло, який параметр ми передаємо. Вона починає вважати, що передається int і помилка. Тому, у разі методом необхідно буде явно вказувати 4L. Дуже часто long використовується як ID під час роботи з базами даних.

Java float та Java double

Дані типи називаються типами з плаваючою точкою. Тобто це не цілі типи. Тип float є 32бітним (як int), а double називається типом з подвійною точністю, тому він 64бітний (множимо на 2, все як ми любимо). Приклад:
public static void main(String []args){
        // float floatValue = 2.3; lossy conversion from double to float
        float floatValue = 2.3F;
        floatValue = 2.3f;
        double doubleValue = 2.3;
        System.out.println(floatValue);
        double cinema = 7D;
}
А ось приклад різниці значень (через точність типів):
public static void main(String []args){
        float piValue = (float)Math.PI;
        double piValueExt = Math.PI;
        System.out.println("Float value: " + piValue );
        System.out.println("Double value: " + piValueExt );
 }
Ці примітивні типи використовуються в математиці, наприклад. Ось доказ константа для обчислення числа PI . Ну і взагалі можна переглянути API класу Math. Ось що ще має бути важливим і цікавим: навіть у документації сказано: « Цей тип даних повинен бути необхідним для цінних цінностей, так як цінності. Для того, що ви повинні використовувати для використання java.math.BigDecimal class instead.Numbers and Strings covers BigDecimal і інші useful classes передбачені Java platform. ». Тобто гроші у float та double не треба рахувати. Приклад про точність на прикладі роботи в NASA: Java BigDecimal, Dealing with high precision calculations Ну і щоб самим відчути:
public static void main(String []args){
        float amount = 1.0000005F;
        float avalue = 0.0000004F;
        float result = amount - avalue;
        System.out.println(result);
}
Виконайте цей приклад, а потім додайте 0 перед цифрами 5 і 4. І ви побачите весь жах) Є цікава доповідь російською про float і double в тему : cents with BigDecimal До речі, float і double можуть повернути не тільки число. Наприклад, приклад нижче поверне Infinity (тобто нескінченність):
public static void main(String []args){
        double positive_infinity = 12.0 / 0;
        System.out.println(positive_infinity);
}
А цей поверне NAN:
public static void main(String []args){
        double positive_infinity = 12.0 / 0;
        double negative_infinity = -15.0 / 0;
        System.out.println(positive_infinity + negative_infinity);
}
Про нескінченність відомо. А що таке NaN? Це Not a number , тобто результат може бути вирахований і є числом. Ось приклад: Ми хочемо обчислити квадратне коріння з -4. Квадратний корінь із 4 це 2. Тобто 2 треба звести квадрат і тоді ми отримаємо 4. А що треба звести в квадрат, щоб отримати -4? Не вийде, т.к. якщо позитивне число буде, воно і залишиться. А якщо було негативне, то мінус на мінус дасть плюс. Тобто це не обчислювано.
public static void main(String []args){
        double sqrt = Math.sqrt(-4);
        System.out.println(sqrt + 1);
        if (Double.isNaN(sqrt)) {
           System.out.println("So sad");
        }
        System.out.println(Double.NaN == sqrt);
}
Ось ще чудовий огляд на тему чисел з плаваючою точкою: Де ваша точка?
Що ще почитати:

Java boolean

Наступний тип – булевський (логічний тип). Він може набувати значення тільки true або false, які є ключовими словами. Використовується у логічних операціях, таких як цикли while, та у розгалуженні за допомогою if, switch. Що тут можна цікавого дізнатися? Ну, наприклад, теоретично нам достатньо 1 біта інформації, 0 або 1, тобто true або false. Але насправді Boolean буде займати більше пам'яті, і це залежатиме від конкретної реалізації JVM. Зазвичай на це витрачається стільки ж, скільки на int. Як варіант – використовувати BitSet. Ось короткий опис із книги «Основи Java»: BitSet

Java char

Ось ми й дісталися останнього примітивного типу. Отже, дані char займають 16 біт і описують символ. У Java для char використовується кодування Unicode. Символ можна задати відповідно до двох таблиць (подивитися можна тут ):
  • Таблиця Unicode символів
  • Таблиця символів ASCII
Примітивні типи в Java: Не такі вони і примітивні - 3
Приклад у студію:
public static void main(String[] args) {
    char symbol = '\u0066'; // Unicode
    symbol = 102; // ASCII
    System.out.println(symbol);
}
До речі, char, будучи за своєю суттю числом, підтримує математичні дії, такі як сума. А іноді це може призвести до кумедних наслідків:
public class HelloWorld{

    public static void main(String []args){
        String costForPrint = "5$";
        System.out.println("Цена только для вас " +
        + costForPrint.charAt(0) + getCurrencyName(costForPrint.charAt(1)));
    }

    public static String getCurrencyName(char symbol) {
        if (symbol == '$') {
            return " долларов";
        } else {
            throw new UnsupportedOperationException("Not implemented yet");
        }
    }

}
Настійно раджу перевірити в онлайн IDE від tutorialspoint . Коли я побачив цей пазлер на одній із конференцій, мені це підняло настрій. Сподіваюся, Вам приклад теж сподобається) UPDATED: Це було на Joker 2017, доповідь: " Java Puzzlers NG S03 - Звідки ви все лізете?! ".

Літерали

Літерал – явно задане значення. За допомогою літералів можна вказувати значення у різних системах числення:
  • Десятерична система: 10
  • Шістнадцяткова система: 0x1F4, починається з 0x
  • Вісімкова система: 010, починається з нуля.
  • Двійкова система (починаючи з Java7): 0b101, починається з 0b
На восьмеричній системі я б трохи докладніше зупинився, бо це смішно:
int costInDollars = 08;
Цей рядок коду не скомпілюється:
error: integer number too large: 08
Здається, що за марення. А тепер згадаємо про двійкову та вісімкову системи. У двійковій системі немає двійки, т.к. Існують два значення (починаючи з 0). А восьмеричній системі є 8 значень, починаючи з нуля. Тобто самого значення 8 немає. Тому й помилка, яка, на перший погляд, здається абсурдною. І щоб згадати ось «навздогін» правила перекладу значень:
Примітивні типи в Java: Не такі вони і примітивні - 4

Класи-обгортки

Примітиви Java мають свої класи-обгортки, щоб можна було працювати з ними як з об'єктами. Тобто, для кожного примітивного типу існує відповідний йому тип посилання. Примітивні типи в Java: Не такі вони і примітивні - 5Класи-обертки є immutable (незмінними): це означає, що після створення об'єкта його стан - значення поля value - не може бути змінено. Класи-обертки задекларовані як final: об'єкти, так би мовити, read-only. Також хотілося б згадати, що від цих класів неможливо успадковуватись. Java автоматично робить перетворення між примітивними типами та їх обгортками:
Integer x = 9;          // autoboxing
int n = new Integer(3); // unboxing
Процес перетворення примітивних типів на посилання (int->Integer) називається autoboxing (автоупаковкою), а зворотний йому - unboxing (автораспаковкой). Ці класи дають можливість зберігати всередині об'єкта примітив, а сам об'єкт поводитиметься як Object (ну як будь-який інший об'єкт). При всьому цьому ми отримуємо велику кількість різношерстих, корисних статичних методів, як-от порівняння чисел, переведення символу в регістр, визначення того, чи є символ буквою або числом, пошук мінімального числа і т.п. Набір функціоналу, що надається, залежить лише від самої обгортки. Приклад власної реалізації обгортки для int:
public class CustomerInt {

   private final int value;

   public CustomerInt(int value) {
       this.value = value;
   }

   public int getValue() {
       return value;
   }
}
В основному пакеті, java.lang, вже є реалізації класів Boolean, Byte, Short, Character, Integer, Float, Long, Double, і нам не потрібно нічого городити свого, а лише перевикористовувати готове. Наприклад, такі класи дають можливість створити, скажімо, List , адже List повинен містити лише об'єкти, ніж примітиви не є. Для перетворення значення примітивного типу є статичні методи valueOf, наприклад, Integer.valueOf(4) поверне об'єкт типу Integer. Для зворотного перетворення є методи intValue(), longValue() і т. п. Компілятор вставляє виклики valueOf та *Value самостійно, це і є суть autoboxing та autounboxing. Як виглядає приклад автоупаковки та автораспаковки, представлений вище, насправді:
Integer x = Integer.valueOf(9);
int n = new Integer(3).intValue();
Детальніше про автоупаковку і автоупаковку можна почитати ось у цій статті .

Приведення типів

Працюючи з примітивами існує таке поняття як приведення типів, одне з дуже приємних властивостей C++, проте приведення типів збережено й у мові Java. Іноді ми стикаємося з такими ситуаціями, коли ми повинні здійснювати взаємодії з даними різних типів. І дуже добре, що у деяких ситуаціях це можливо. Що стосується посилальних змінних, там свої особливості, пов'язані з поліморфізмом і успадкуванням, але сьогодні ми розглядаємо прості типи і відповідно приведення простих типів. Існує перетворення з розширенням і перетворення, що звужує. Все насправді просто. Якщо тип даних стає більше (припустимо, був int, а став long), тип стає ширше (з 32 біт стає 64). І тут ми ризикуємо втратити дані, т.к. якщо влізло в int, то в long влізе тим більше, тому це приведення ми не помічаємо, так як воно здійснюється автоматично. звуження . Так би мовити, щоб ми самі сказали: Так, я даю собі звіт у цьому. У разі чого винен сам».
public static void main(String []args){
   int intValue = 128;
   byte value = (byte)intValue;
   System.out.println(value);
}
Щоб потім у такому разі не говорабо що «Ваша Джава погана», коли отримають раптово -128 замість 128) Адже ми пам'ятаємо, що в байті 127 верхнє значення і все що знаходилося вище за нього відповідно можна втратити. Коли ми явно перетворабо наш int на байт, то відбулося переповнення та значення стало -128.

Область видимості

Це місце в коді, де ця змінна виконуватиме свої функції і зберігатиме в собі якесь значення. Коли ж ця область закінчиться, змінна перестане існувати і буде стерта з пам'яті. як вже можна здогадатися, подивитися чи набути її значення буде неможливо! То що це таке — область видимості? Примітивні типи в Java: Не такі вони і примітивні - 6Область визначається "блоком" - взагалі будь-якою областю, замкненою у фігурні дужки, вихід за які обіцяє видалення даних оголошених у ній. Або, як мінімум, приховування їх від інших блоків, відкритих поза поточним. У Java область видимості визначається двома основними способами:
  • Класом.
  • методом.
Як я й сказав, змінна не видно коду, якщо її визначено за межами блоку, в якому вона була ініціалізована. Дивимося приклад:
int x;
x = 6;
if (x >= 4) {
   int y = 3;
}
x = y;// переменная y здесь не видна!
І як результат ми отримаємо помилку:

Error:(10, 21) java: cannot find symbol
  symbol:   variable y
  location: class com.codeGym.test.type.Main
Області видимості можуть бути вкладеними (якщо ми оголосабо змінну в першому, зовнішньому блоці, то у внутрішньому вона буде помітна).

Висновок

Сьогодні ми познайомабося з вісьмома примітивними типами Java. Ці типи можна розділити на чотири групи:
  • Цілі числа: byte, short, int, long — є цілими числами зі знаком.
  • Числа з плаваючою точкою – ця група включає собі float та double – типи, які зберігають числа з точністю до певного знака після коми.
  • Булеві значення - boolean - зберігають значення типу "істина/брехня".
  • Символи - до цієї групи входить типу char.
Як показав текст вище, примітиви Java не такі вже примітивні і дозволяють вирішувати багато завдань ефективно. Але це й приносить деякі особливості, про які слід пам'ятати, якщо ми не хочемо зіткнутися з непередбачуваною поведінкою нашої програми. Як то кажуть, за все треба платити. Якщо ми хочемо примітив з "крутим" (широким) діапазоном - щось на зразок long - ми жертвуємо виділенням більшого шматка пам'яті та у зворотний бік. Заощаджуючи пам'ять та використовуючи byte, ми отримуємо обмежений діапазон від -128 до 127.
Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ