Вступ
Розробку додатків можна як роботу з деякими даними, а точніше — їх зберігання та обробку. Сьогодні хотілося б торкнутися першого ключового аспекту. Як дані зберігаються у Java? Тут у нас є два можливі формати:
посилальний та
примітивний тип даних. Давайте поговоримо про види примітивних типів та можливості роботи з ними (як не крути, це фундамент наших знань мови програмування).
Примітивні типи даних Java - це основа, де тримається все. Ні, я анітрохи не перебільшую. Oracle примітивам присвячений окремий Tutorial:
Primitive Data Types Трохи історії. Спочатку був нуль. Але нуль – це нудно. І тоді з'явився
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? Щоб зрозуміти, як виходять числа, варто подивитися на картинку:
Відповідно, щоб обчислити розмір 2 ^ (8-1) = 128. Значить мінімальна межа (а вона з мінусом) буде -128. А максимальна 128 – 1 (віднімаємо нуль). Тобто максимум буде 127. Насправді з типом byte працюємо ми не так часто на "високому рівні". Здебільшого це обробка «сирих» даних. Наприклад, при роботі з передачею даних через мережу, коли дані це набір 0 і 1, переданих через якийсь канал зв'язку. Або під час читання даних із файлів. Також можуть бути використані при роботі з рядками та кодуваннями. Приклад коду:
public static void main(String []args){
byte value = 2;
byte shortByteValue = 0b10;
System.out.println(shortByteValue);
byte minByteValue = (byte) 0B1000_0000;
byte maxByteValue = (byte) 0b0111_1111;
byte minusByteValue = (byte) 0b1111_1111;
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.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
Приклад у студію:
public static void main(String[] args) {
char symbol = '\u0066';
symbol = 102;
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 мають свої класи-обгортки, щоб можна було працювати з ними як з об'єктами. Тобто, для кожного примітивного типу існує відповідний йому тип посилання.
Класи-обертки є immutable (незмінними): це означає, що після створення об'єкта його стан - значення поля value - не може бути змінено. Класи-обертки задекларовані як final: об'єкти, так би мовити, read-only. Також хотілося б згадати, що від цих класів неможливо успадковуватись. Java автоматично робить перетворення між примітивними типами та їх обгортками:
Integer x = 9;
int n = new Integer(3);
Процес перетворення примітивних типів на посилання (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 область видимості визначається двома основними способами:
Як я й сказав, змінна не видно коду, якщо її визначено за межами блоку, в якому вона була ініціалізована. Дивимося приклад:
int x;
x = 6;
if (x >= 4) {
int y = 3;
}
x = 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.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ