User Viacheslav
Viacheslav
3 уровень
Санкт-Петербург

Примитивные типы в Java: Не такие уж они и примитивные

Статья из группы Random

Вступление

Разработку приложений можно рассматривать как работу с некоторыми данными, а точнее — их хранение и обработку. Сегодня хотелось бы затронуть первый ключевой аспект. Как данные хранятся в Java? Тут у нас есть два возможные формата: ссылочный и примитивный тип данных. Давайте поговорим о видах примитивных типов и возможностях работы с ними (как ни крути, это фундамент наших знаний языка программирования). Примитивные типы данных Java — это основа, на которой держится всё. Нет, я нисколько не преувеличиваю. У Oracle примитивам посвящён отдельный Tutorial: Primitive Data Types Примитивные типы в Java: Не такие уж они и примитивные - 1Немного истории. Вначале был ноль. Но ноль — это скучно. И тогда появился bit (бит). Почему его так назвали? Назвали его так от сокращения "binary digit" (двоичное число). То есть у него есть только два значения. А так как был ноль, то логично, что теперь стало или 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 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.

Максимальное значение 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. Вот что ещё должно быть важно и интересно: даже в документации сказано: «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 в тему: https://youtu.be/1RCn5ruN1fk Примеры работы с 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:

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: Не такие уж они и примитивные - 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.javaRush.test.type.Main
Области видимости могут быть вложенными (если мы объявили переменную в первом, внешнем блоке, то во внутреннем она будет видна).

Заключение

Сегодня мы познакомились с восемью примитивными типами в Java. Эти типы можно разделить на четыре группы:
  • Целые числа: byte, short, int, long — представляют собой целые числа со знаком.
  • Числа с плавающей точкой — эта группа включает себе float и double — типы, которые хранят числа с точностью до определённого знака после запятой.
  • Булевы значения — boolean — хранят значения типа "истина/ложь".
  • Символы — в эту группу входит типа char.
Как показал текст выше, примитивы в Java не такие уж примитивные и позволяют решать многие задачи эффективно. Но это и привносит некоторые особенности, о которых следует помнить, если мы не хотим столкнуться с непредсказуемым поведением нашей программы. Как говорится, за всё нужно платить. Если мы хотим примитив с “крутым” (широким) диапазоном — что-то вроде long — мы жертвуем выделением большего куска памяти и в обратную сторону. Экономя память и используя byte, мы получаем ограниченный диапазон от -128 до 127.
Комментарии (11)
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ СДЕЛАТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ
Roman Pantyukhin Уровень 17, Ижевск, Россия
23 октября 2020
Кто может пояснить? В данной статье int Диапазон значений от -2^31 до 2^31 – 1 В лекции о списке базовых типах данных int Диапазон значений -2*10^9 .. 2*10^9 Базовые типы данных - это теория, а примитивные типы Java - это конкретная реализация в Java?
Илья Уровень 41, Санкт-Петербург, Россия
20 сентября 2020
почему в числе 127 девять цифр 0_111_111_11 (в самом начале статьи таблица)
ANDRUSHA Уровень 5, Харьков
20 апреля 2020
ПОДСКАЖИТЕ где можно ОЧЕНЬ подробноузнать о восьмиричной и шестнадцатиричных системах исчисления? как правильно переводить числа с десятичной и тд? Вообще не разбираюсь в этом
barracuda Уровень 41, Санкт-Петербург, Россия Expert
6 июня 2019
http://voidexception.weebly.com/java-bigdecimal---dealing-with-high-precision-calculations.html Эта ссылка у меня не открывается... Пишет: ERR_CONNECTION_TIMED_OUT
Арман Меркулов Уровень 40, Россия
26 марта 2019
По поводу boolean не понятно, согласен, что зависит от реализации JVM, но ведь понятно, что одного байта хватает, а так как JVM удобно работать с байтами, а не с битами, то как бы получается оптимально 1 байт памяти для boolean достаточно. Если я ошибаюсь, приведите пожалуйста источник.
Aleksandr Zimin Уровень 1, Санкт-Петербург, Россия
6 мая 2018
Viacheslav, подскажите, а где бы можно было почитать про это? Очень заинтересовало. Кстати, не стоит думать, что byte будет потреблять меньше памяти. Так как большинство систем 32 или 64 разрядные, то byte и short в вычислениях будут приведены к int, о котором мы поговорим дальше. Тот как раз 32битный, поэтому производить вычисления на нём компьютеру проще.