Вступление

Примитивные типы данных в Java — это основа, на чём держится всё. Нет, я нисколько не преувеличиваю. У Oracle примитивам посвящён отдельный Tutorial: Primitive Data Types Примитивные типы в Java: Не такие уж они и примитивные - 1И немного истории. Вначале был ноль. Но ноль — это скучно. И тогда появился bit (бит). Почему его так назвали? Назвали его так от сокращения "binary digit" (двоичное число). То есть у него есть только два значения. А так как был ноль, то логично, что теперь стало или 0 или 1. И стало жить веселее. Биты начали собираться в стаи. И эти стаи стали называть byte (байт). В современном мире byte = 2 в третьей степени, т.е. 8. Но, оказывается, так было не всегда. Существует множество догадок, легенд и слухов, откуда пошло название byte. Кто-то считает, что всё дело в кодировках того времени, а кто-то считает, что так было выгоднее считать информацию. Байт – это наименьшая адресуемая часть памяти. Именно байты имеют уникальные адреса в памяти. Есть легенда о том, что ByTe это сокращение от Binary Term – машинное слово. Машинное слово – если просто, то это количество данных, которые процессор может обработать за одну операцию. Раньше размер машинного слова совпадал с наименьшей адресуемой памятью.

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 addition 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: «you can use a short to save memory in large arrays».

int

Вот мы и добрались до самого частоиспользуемого типа. Занимает он 32 бита, или 4 байта. В общем, мы продолжаем удваивать. Диапазон значений от -2^31 до 2^31 – 1. Верхняя граница 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);

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 при работе с базами данных.

float и 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. Вот что ещё должно быть важно и интересно: даже в документации сказано: «This data type should never be used for precise values, such as currency. For that, you will need to use the java.math.BigDecimal class instead.Numbers and Strings covers BigDecimal and other useful classes provided by the 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 в тему:
Примеры работы с BigDecimal можно увидеть здесь: Make 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);
}
Вот ещё отличный обзор на тему чисел с плавающей точкой: Где ваша точка?
Что еще почитать:

Архив info.javarush.ru:

boolean

Следующий тип – булевский (логический тип). Он может принимать значения только true или false, которые являются ключевыми словами. Используется в логических операциях, таких как циклы while, и в ветвлении при помощи if, switch. Что тут можно интересного узнать? Ну, например, теоретически, нам достаточно 1 бита информации, 0 или 1, то есть true или false. Но на самом деле Boolean будет занимать больше памяти и это будет зависеть от конкретной реализации JVM. Обычно на это тратится столько же, сколько на int. Как вариант – использовать BitSet. Вот краткое описание из книги «Основы Java»: BitSet

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

Классы обёртки

Примитивы имеют свои классы-обёртки, чтобы с примитивами работать как с объектами. Подробнее тут стоит смотреть официальный tutorial: Autoboxing Данные классы позволяют сохранять внутри объекта примитив, а сам объект будет вести себя как Object. При этом добавляют множество полезных статических методов, таких как сравнение чисел, перевод символа в регистр, определение того, является ли символ буквой или числом, поиск минимального числа и т.п. Естественно, предоставляемые функции зависят от самоё обёртки. Подробнее см. документацию.

Приведение типов

Ну и напоследок. При работе с примитивами существует такое понятие как приведение типов. Существует преобразование с расширением и преобразование сужающее. Всё на самом деле просто. Если тип данных становится больше (допустим, был 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 вовсе не примитивные и позволяют решать многие задачи эффективно. Но это и привносит некоторые особенности, о которых следует помнить, если мы не хотим столкнуться с непредсказуемым поведением нашей программы. #Viacheslav