Продолжаем разговор. Сегодня разбираем примитивные типы данных. В этом посте будет очень много нудной информации и матана, хотя это всё нужно для понимания и превентивных мер против ошибок в коде. Читаем на свой страх и риск уснуть:) Кто хочет - может затариться поллитрой пива для лучшего понимания:)
В конце напишем программку, которая покажет эти самые типы данных.
Что есть данные вообще?
"Есть только 0 и 1, остальное от лукавого..."(с)
Сей раздел полезен для понимания и к программированию имеет косвенное отношение в целом. Сия информация, как я помню, дается на уроках информатики ещё в школе, но вдруг кто забыл:)
Для компьютера любые данные, будь то цифры, символы или картинки - это просто набор электрических сигналов, составленных по определенным правилам. Возьмем цифири. Вот мы видим число 5, как "5", а для компьютера это 101. Почему так? Это называется двоичной системой счисления. Как правило, мы пользуемся десятичной, которая использует цифры от 0 до 9. И десятка записывается как 10. В двоичной системе всё несколько иначе. Она использует только 0 и 1. То есть, тут главное прочувствовать такую вещь: в десятичной системе десятка это 10, а в двоичной десяток это 2 в десятичной. Кажется, это называется переносом в старший разряд. Или это я так называю... Старший разряд - это крайняя левая цифра в числе, младший, соответственно, крайняя правая. Суть в том, что как только в самом старшем разряде хочет появиться 10 для десятичной системы, например, в результате переноса из разряда помладше при сложении, скажем 99+1=100, то в двоичной системе такое происходит когда хочет появиться 2. Например, сложим 1+1. В десятичной системе должно получиться 2. А в двоичной - получится 10. Потому что 10 в двоичной это 2 в десятичной:) А 3 десятичное это 11 в бинарном коде. Бинарный код - второе название двоичной системы счисления. А 4 уже 100. Вроде пояснил. У меня это больше уже на подкорковом понимании, а вот объяснить толково пока не получается:) Если не понятно - напишите в комментариях, я ещё попытаюсь как-нибудь:)
Компьютер использует именно двоичную систему счисления, потому что для него 0 - это нет электрического сигнала, а 1 - есть сигнал. И вот на такой простой вещи строится вся цифровая электроника.
Так же для понимания полезно знать, что 8-разрядное двоичное число, или оно же однобайтное, это число которое имеет нолики и единички в количестве 8 штук, например, 11110000, что равно 240 в десятичной системе, или 1111, что равно 15 в десятичной системе. Скажете 1111 - это 4 единицы, а я сказал про 8 цифр? А тут действует хитрость: можно дополнить до 8 цифр незначащими нулями в старших разрядах, то есть получится 00001111, а это одно и тоже. Кстати, каждый разряд бинарного числа называется битом.
Правило перевода из бинарного кода в десятичное число - очень простое. Берем двоичное число, например, 1101 и переводим:) Шучу. Теперь берем цифры, начиная справа, и умножаем их на 2 в степени номера разряда посчитанного с нуля и справа налево:) Так, не надо хвататься за голову и выпадать в нерастворимый осадок:) Вот у нас число 1101, раскладываем: 1*2^0 + 0*2^1 + 1*2^2 + 1*2^3. Знак ^ - это возведение в степень. Проще всего, чтобы не запутаться в степенях в которые надо возводить двойку, слева направо написать над разрядами двоичного числа цифры от нуля, получится примерно так:
3 2 1 0 - это степени двойки
1 1 0 1 - а это наше число.
Считаем, получаем 13.
Тут можно проследить некоторую аналогию с десятичной системой, для понимания. В десятичной системе любое число можно разложить по такому же принципу, только заместо 2 будет 10 в определенной степени. То есть получится число единиц, плюс число десятков, плюс число тысяч и так далее:) 2 в двоичной системе и 10 в десятичной системе называется основанием системы счисления, то есть количеством цифр, которые используются для записи числа. Так же, в программировании распространена шестнадцатиричная система исчисления. По аналогии, роль 10 в десятичной системе тут играет 16, а для записи используются цифры 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, A(10), B(11), C(12), D(13), E(14), F(15). В скобках даны значения цифры в десятичной системе.
Основные типы данных в Java.
"Восемь бит - это байт, а 9 бит - толпа хулиганов."(с)
Этих типов данных не очень много, целых 8: 4 для целых чисел, 2 для дробных, 1 для символов, 1 для логических значений.
Переменная каждого из этих типов занимает строго определенное количество байт в памяти.
Соотвественно по группам:
для целых чисел это byte, int, short и long, которые занимают 1, 4, 2 и 8 байт в памяти соответственно;
для дробных чисел это float и double - 4 и 8 байт соотвественно;
для символов это char занимающий 2 байта;
для логических значений тип boolean, занимающий всего 1 бит.
В языке Си, о котором я писал в прошлом посте набор типов почти такой же. Но есть отличия - в нём нет отдельного типа данных для логических значений и количество байт, отведенных под переменную определенного типа, задано не строго, как в Java, а зависит от архитектуры машины. То есть можно поиметь много проблем, перенеся на очень мощный компьютер программу, написанную без предосторожностей на очень слабой машине.
Вы можете задаться вопросом: а на кой фиг для целых чисел аж 4 типа данных и для дробных 2? А я вам отвечу - у каждого из этих типов есть ограничения на число, которое он может вместить. Это ограничение накладывает количество байт используемых под переменную.
Например, тип byte, которому отводится всего 1 байтик в памяти, может хранить числа из диапазона 0...255 или -128...127. Почему так? Переведем число 255 в двоичную систему счисления. А, я ж не рассказал как:) Как правило, это делается делением уголком. Поскольку я не могу нарисовать здесь уголок, попробую объяснить так. Суть в том, что нужно делить число в десятичной системе на 2, то есть на основание новой системы счисления, до появления в остатке числа, которое при делении на 2 или основание новой системы счисления даст дробь меньше 1. На примере будет понятнее:
255/2=127, остаток 1; ^
127/2=63, остаток 1; |
63/2=31, остаток 1; |
31/2=15, остаток 1; ^
15/2=7, остаток 1; |
7/2=3, остаток 1; |
3/2=1, остаток 1. ^
----->-----------------|
Дальше делить бесполезно, получится 1/2, что меньше 1. А теперь суть метода. Берем последний результат деления и пишем его как старший разряд, остаток - следующий разряд, остаток от предыдущего деления - следующий разряд и так далее, то есть идем по стрелочке. Извиняюсь за кривость, рисовать никогда не умел:) То есть, результат последнего деления - старший разряд или крайняя левая цифра, а остатки пишутся в следующих разрядах в порядке получения этих самых остатков:) Кипит мозг? Охладите его кофейком, пивком или перекуром:)
В итоге мы получили 11111111 в двоичной системе. А теперь прибавим 1. Получим число 256 в десятичной или 100000000. Это уже 9 бит. Поскольку, у нас отведено всего 8 бит, старший разряд отсечется и останется число 00000000 или 0. Это называется переполнением и это может принести очень много проблем. Раньше когда особых вычислительных мощностей не было, программисты экономили память и там, где не могло быть числа больше 255, использовали тип byte(аналогично char в Си, он мог хранить символы, а мог числа). При обработке больших массивов данных это очень сильно снижало требования к компьютеру.
Кстати, такого вида диапазон определяется очень просто: берем количество
значащих бит, отведенных под переменную, в нашем случае это 8, и возводим 2 в такую степень. Получаем 2 в 8 степени или 256. То есть, 8 бит данных могут дать 256 различных комбинаций. Поскольку одна комбинация "0" вычитаем одну и получаем предельное значение 255. Итого получаем формулку: 2 в степени количества
значащих бит, отведенных под переменную. Всё просто:) И лишний раз напомню, что 1 байт это в бит.
Теперь относительно диапазона -128...127. Тут своя шутка юмора. Дело в том, что переменные в Java отводят старший бит под знак. Если старший бит 0 - значит число положительное, если 1 - значит отрицательное.
А теперь самое забавное: как говорил один препод: "Компьютер - это очень исполнительный, но крайне тупой работник"(с). К чему это я... Ах да, компьютер не умеет вычитать. То есть совсем не умеет. Вычитание заменили сложением с отрицательным числом. А делается это вот так...
Для примера посчитаем выражение 128-100.
Берем число 100 и переводим в двоичную систему счисления, получим 01100100. Это называется прямым кодом числа. Теперь инвертируем биты, то есть где был 0 - станет 1, где была 1 - станет 0. Получим 10011011. Это называется обратным кодом числа. А теперь прибавим 1. Получим 10011100. Это называется дополнительным кодом числа. Теперь переведём 128 в двоичную систему, получим 10000000. А теперь побитово сложим:
10000000
10011100
-----------
100011100
Произошло переполнение - получилось 9 бит. Не беда, отбрасываем старший бит, получаем 00011100, переводим в десятичную систему и получаем 28. Всё чин по чину.
Суть в том, что вычитание заменяется сложением, только вычитаемое переводится в дополнительный код.
Кстати, знаковый бит не является значащим. И если у нас тип byte(8 бит) и один бит отведен под знак, диапазон получается от -(2^7) до 2^7-1.
И наконец, зачем я всё это расписал. В языке Си можно указать какой диапазон значений переменная может принимать, используя специальное слово unsigned, в переводе "беззнаковый". Если оно есть - переменная принимает только положительные числа. Если нет, значит переменная знаковая и она принимает и положительные, и отрицательные значения.
А вот в Java такого нет. Здесь только знаковые переменные, то есть нельзя заставить переменную принимать только положительные значения. Не знаю, хорошо это или плохо - в моих задачах это пока что роли не играло. Хотя, многие это хаят...
Допустимые диапазоны значений переменных для целочисленных типов:
byte: -128...127
short: -32768...32767
int: -2147483648...2147483647
long: -9223372036854775808...9223372036854775807
Маленькие циферки, правда?:)
Теперь о типах float и double, которыми задаются дробные числы. Я не буду расписывать их формат, что там и как, я просто приведу пределы. Если кто-то сильно заинтересуется - напишите в комментах и я чуть позже допишу это. Итак,
float: −3.4·10^38..3.4·10^38
double: −1.8·10^308..1.8·10^308
Ещё меньше чиселки:) Завалиться можно:)
Тип char используется для хранения символов. В Си он занимает 1 байт, а в Java 2. Почему? Потому что используются разные кодировки для символов. В Си, если я не ошибаюсь, кодировка ASCII, символы которой занимают максимум 1 байт, а в Java - Unicode, символы которой кодируются двумя байтами каждый. Что такое кодировка? Всё просто. Кодировка - это таблица соотвествия символов и кодов. Каждому символу соответствует определенный код и при отображении текста читается 1 или 2 байта, находится код в таблице и по этому коду определяется, какой символ нужно вывести:)
Ну и наконец тип boolean. Он выдает всего два значения: true или
труп false. Истина или ложь. Это из алгебры логики, её я коснусь в следующем посте.
Программка о типах данных
Итак, ниже я приведу код программки, которая показывает описанное выше. Открываем Eclipse, с прошлого раза у нас остался проект, так что не будем создавать новый. Просто удаляем лишнее и дописываем нужное:)
public class Hello {
public static void main(String[] args) {
// TODO Auto-generated method stub
int varInt;
byte varByte = Byte.MAX_VALUE;
short varShort = Short.MAX_VALUE;
long varLong;
boolean varBool = true;
float varFloat = 150.1F;
double varDouble = 150.1;
char varChar = 'X';
System.out.println("Число типа byte до переполнения " + varByte);
varByte++;
System.out.println("Число типа byte после переполнения " + varByte);
varInt = Integer.MAX_VALUE;
System.out.println("Число типа int в битовом и обычном видах до переполнения " + Integer.toBinaryString(varInt) + " " + varInt);
varInt = varInt + 1;
System.out.println("Число типа int в битовом и обычном видах посе переполнения " + Integer.toBinaryString(varInt) + " " + varInt);
varLong = Long.MIN_VALUE;
System.out.println("Диапазон для числа типа long от " + varLong + " до " + Long.MAX_VALUE);
System.out.println("Переменная типа float будет выглядеть так: " + varFloat);
System.out.println("Хотя обозначается она как 150.1F и если забыть F, программа не заработает и будет громко ругаться матом");
System.out.println("А типа double вот так " + varDouble);
varLong = 100000L;
System.out.println("Переменная типа long в коде программы будет обозначаться как 100000L");
System.out.println("А выведется как " + varLong);
System.out.println("L в конце числа типа long забыть можно, но это дурной тон, лучше ставить");
System.out.println("А вот так выведется символ char " + varChar);
System.out.println("Ну и наконец тип boolean, который при выводе выглядит так: " + varBool);
varBool = false;
System.out.println("Ну или так: " + varBool);
}
}
Написали? Умнички:) А теперь выбираем в меню Run-Run или нажимаем Ctrl+F11 и наблюдаем результат выполнения в окошке Console.
А теперь давайте разбираться, что мы тут понаписали. В принципе, выводимые строки говорят сами за себя, но остановлюсь на нескольких моментах. System.out.println() все помнят что такое?:)
Во-первых, вы наверняка заметили, что первую переменную varInt я не сразу приравнял какому-либо значению. Есть такая шутка юмора как объявление и инициализация переменной. Объявление переменной - это когда переменной выделяется какой-то адрес и нужное количество байт в памяти, а инициализация - когда в эту память что-то ещё и записывается. На ум приходит такая аналогия: объявили переменную - взяли бутылку с чем-то непонятным, инициализировали - вылили ненужное и налили хорошего виски:)
Объявляются переменные так:
тип имя;
А инициализируются оператором присвоения "=":
тип имя = выражение;
Выражение в данном случае может быть как просто числом, так и результатом какого-либо математического выражения с другими переменными.
Здесь надо быть осторожным, чтобы не допустить такого неприятного момента: по ошибке можно использовать неинициализированную переменную. Это чревато тем, что после объявления по этому адресу памяти как правило мусор и никто не знает, какое значение примет эта переменная. И если использовать это мусорное значение - может произойти большая печалька, бо программа может повести себя непредсказуемо.
Во-вторых, вы наверняка посмотрели на надписи типа Integer.toBinaryString(varInt) и выпали в осадок:) Тут всё просто: каждому примитивному типу данных сделан в соответствие класс, объекты которого по сути своей могут заменять переменные данного примитивного типа. В данном случае Integer - класс, объекты которого могут заменить переменные типа int. А toBinaryString() - всего лишь метод, который переводит указанное число в двоичный или бинарный вид и отдает его в виде строки, то есть объекта класса String. Кстати, метод этот статический, то есть его можно вызывать даже когда нет ни одного экземпляра класса, который мог бы его вызвать. Это можно пока запомнить, но не сильно забивать себе голову.
Так же можно посмотреть на фразочки типа Long.MAX_VALUE. Тут тоже ничего страшного. Это выражение выдает нам статическую константу MAX_VALUE, в которой содержится максимальное значение для типа Long. О статических и не статических методах, константах и прочем мы ещё поговорим, когда будем обсуждать объектно-ориентированное программирование как концепцию:) Не пугайтесь этих слов, они на самом деле добрые и аняняшные:)
Если кто-то что-то недопонял или нашёл в моих выкладках ошибку - напишите в комментариях, я попробую объяснить подробнее и понятнее или же в случае ошибки - исправлю оную:)
Ну и напоследок - гифка, показывающая, что может сделать переполнение:)
[400x130]