Что такое примитивный тип и объект-оболочка в Java? Как компилятор обрабатывает преобразование между ними? Когда следует использовать примитивный тип или объект-оболочку?

Примитивные типы

Java определяет восемь примитивных типов данных: byte, short, int, long, float, double, boolean и char. Все другие переменные в java являются ссылочными типами на объекты.

Примитивные типы в Java называются литералами. Литерал - это представление исходного кода фиксированного значения в памяти. Каждый примитивный тип различается по размеру и способу хранения.

Размеры примитивных типов данных

+-----------+---------+-------------------------+
| Data Type | Size    | Range                   |
+-----------+---------+-------------------------+
| byte      | 1 byte  | -128 to 127             |
| short     | 2 bytes | -32,768 to 32,767       |
| int       | 4 bytes | -2^31 to 2^31-1         |
| long      | 8 bytes | -2^63 to 2^63-1         |
| float     | 4 bytes | -3.4e38 to 3.4e38       |
| double    | 8 bytes | -1.7e308 to 1.7e308     |
| boolean   | 1 bit*  | true or false           |
| char      | 2 bytes | '\u0000' to '\uffff'    |
+-----------+---------+-------------------------+

* Хотя тип данных boolean представляет собой один бит, его размер в памяти точно не определен.

Примитивные типы в Java статически типизированы, что означает, что все переменные должны быть сначала объявлены, прежде чем их можно будет использовать, в отличие от таких языков, как Python. Следующее вызовет ошибку:

number = 10;

Определение примитивных типов данных

Вот несколько примеров того, как определять примитивные типы данных в Java.

byte b = '\u0045';
short s = 5;
int i = 10;
long l = 100L;
float f = 3.1415f;
double d = 500.25d;
boolean bool = false;
char c = 'A';

В Java, в отличие от других языков, таких как C ++, примитивные типы данных имеют значения по умолчанию, если они не инициализированы. Обычно значения по умолчанию равны нулю или нулю, но полагаться на это считается плохим стилем программирования.

Следующее не вызовет ошибку при компиляции:

public class Dog {
  private int age;
  public getAge() {
    return this.age;
  }
  Dog() {}
}
public class Example {
  public static void main(String[] args) {
    Dog d = new Dog();
    System.out.println(d.getAge());
  }
}

Значения по умолчанию для примитивных типов данных

При создании объекта с полями примитивного типа, если их значения не определены, они получат значения по умолчанию.

+-----------+---------------+
| Data Type | Default Value |
+-----------+---------------+
| byte      | 0             |
| short     | 0             |
| int       | 0             |
| long      | 0L            |
| float     | 0.0f          |
| double    | 0.0d          |
| boolean   | false         |
| char      | '\u0000'      |
+-----------+---------------+

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

Класс-оболочка - это объект, который инкапсулирует примитивный тип. У каждого примитивного типа есть соответствующая оболочка:

  • byte, short, int, long, float, double, boolean, char
  • Байт, Короткое, Целое, Длинное, Плавающее, Двойное, Логическое, Символьное
Integer number = 3;

Каждый класс-оболочка имеет суперкласс Object. Прямой суперкласс Byte, Short, Integer, Long Float и Double имеет Number. Это означает, что каждый класс-оболочка может реализовывать методы класса объекта, такие как hashCode (), equals (Object obj), clone () и toString ().

Значения по умолчанию для класса оболочки

Поскольку объекты-оболочки являются ссылочными типами, их значение по умолчанию будет нулевым. С другой стороны, примитивные типы никогда не могут быть нулевыми. Если примитивным типам не присвоено значение, язык присвоит им значение по умолчанию. Это поведение важно понимать, когда вы выбираете между использованием примитивного типа или объекта-оболочки.

Объекты оболочки неизменяемы

Все примитивные объекты-оболочки в Java являются окончательными, что означает, что они неизменяемы. Когда объект-оболочка изменяет свое значение, компилятор должен создать новый объект, а затем переназначить этот объект оригиналу.

public void addOne(Integer i) {
  i = i + 1;
}

Это создание и возможная сборка мусора добавят много накладных расходов, особенно при выполнении больших вычислений в циклах.

Прежде чем мы обсудим, когда использовать примитивные типы или классы-оболочки, мы должны сначала понять Autoboxing и Unboxing в Java.

Автобокс

Autoboxing, представленный в Java 5.0, представляет собой автоматическое преобразование примитивных типов в соответствующие им классы-оболочки объектов.

List<Integer> numbers = new ArrayList<Integer>();
for(int i = 0; i < 10; i++) {
  numbers.add(i); 
}

Поскольку примитивные типы не могут использоваться в Коллекциях или Обобщениях, каждый раз, когда i добавляется в numbers, создается новый объект Integer.

Распаковка

Точно так же распаковка - это автоматическое преобразование типов оболочки объекта в соответствующие им примитивные типы.

Integer a = new Integer(5);
int b = a;

Компилятор распознает необходимость распаковки и делает это автоматически. Будьте осторожны, это автоматическое преобразование может вызвать проблемы с производительностью и непредвиденное поведение.

Опасности автобокса и распаковки

Представление

Автобоксирование и распаковка могут снизить производительность из-за создания промежуточных объектов, что создает больше работы для сборщика мусора.

Ниже приведен пример того, как автобоксинг и распаковка создают множество ненужных объектов:

public class Example {
  private static Integer count = 0;
  public static void main(String[] args) {
    for(int i = 0; i < 1000; ++i) {
      count += 1;
    }
  }
}

Давайте посмотрим, что делает компилятор за кулисами.

count += 1 превращается в count = count + 1.

  • Поскольку count является типом оболочки, его необходимо распаковать в значение int.
  • Значение int count добавляется к 1 для создания нового int.
  • Новому типу примитива снова присваивается значение count, которое является типом оболочки, поэтому компилятор создает новый объект Integer для обратного присвоения переменной count.

Этот цикл создаст в памяти 1000 целочисленных объектов. Позже эти объекты нужно будет удалить сборщиком мусора. Это сильно повлияет на производительность вашей программы.

В Javadocs сказано:

Не использовать автобоксирование и распаковку для научных вычислений или другого числового кода, чувствительного к производительности. Integer не заменяет int; автобоксирование и распаковка стирают различие между примитивными типами и ссылочными типами, но не устраняют его.

Арифметические операторы и операторы сравнения

Когда используются арифметические операторы и операторы сравнения, всегда будут выполняться автобоксы и распаковка.

Integer x = new Integer(5);
if (x > 0) {
  System.out.println("The number is greater than 0.");
}

Переменная x распаковывается в примитивную форму int, а затем сравнивается со значением int 0.

Запутанное поведение равных

Автобоксирование и распаковка не всегда происходит с оператором «==».

Когда аргументы с обеих сторон оператора «==» являются объектами, распаковка не происходит, и оператор «==» сравнивает ссылку на объект в памяти.

public class Example {
  public static void compare(Integer a, Integer b) {
    if (a == b) {
      System.out.println("a and b are equal");
    }
    else {
      System.out.println("a and b are not equal");
    }
  }
public static void main(String[] args) {
    int a = 1000;
    int b = 1000;
    compare(a,b);
  }
}

Эта программа выведет a и b не равны. В формальном параметре compare значения int a и b автоматически помещаются в новые объекты оболочки Integer. Когда переменные сравниваются, оператор «==» сравнивает их объектные ссылки в памяти.

Когда только один из аргументов оператора «==» является объектом, а другой - примитивным типом, происходит распаковка.

public class Example {
  public static void compare(Integer a, int b) {
    if (a == b) {
      System.out.println("a and b are equal");
    }
    else {
      System.out.println("a and b are not equal");
    }
  }
public static void main(String[] args) {
    int a = 1000;
    int b = 1000;
    compare(a,b);
  }
}

Эта программа выведет a и b равны. Объект-оболочка a распаковывается, и его значение int сравнивается со значением int b.

Чтобы избежать подобных ситуаций, мы можем использовать оператор .equals ().

public class Example {
  public static void compare(Integer a, Integer b) {
    if (a.equals(b)) {
      System.out.println("a and b are equal");
    }
    else {
      System.out.println("a and b are not equal");
    }
  }
public static void main(String[] args) {
    int a = 1000;
    int b = 1000;
    compare(a,b);
  }
}

Это выведет a и b равны.

Когда использовать примитивные типы

  • При выполнении большого количества вычислений примитивные типы всегда быстрее - у них гораздо меньше накладных расходов.
  • Если вы не хотите, чтобы переменная могла иметь значение NULL.
  • Если вы не хотите, чтобы значение по умолчанию было нулевым.
  • Если метод должен возвращать значение

Когда использовать класс-оболочку

  • Когда вы используете Коллекции или Дженерики - это обязательно
  • Если вам нужен MIN_SIZE или MAX_SIZE типа.
  • Если вы хотите, чтобы переменная могла иметь значение NULL.
  • Если вы хотите, чтобы значение по умолчанию было нулевым.
  • Иногда метод может возвращать нулевое значение.

Вердикт

Как правило, выбирайте примитивные типы вместо классов-оболочек, если использование класса-оболочки не требуется. Примитивные типы никогда не будут медленнее, чем объекты-оболочки, однако у объектов-оболочек есть то преимущество, что они могут быть нулевыми.

Выбор между использованием примитивного типа или класса-оболочки всегда будет зависеть от вашей ситуации. Многие программисты имеют разные мнения по этому поводу, и это, конечно, не решенный спор.