Сколько памяти будут занимать примитивные типы в классе

Обновлено: 04.07.2024

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

  1. Целые числа - byte, short, int, long
  2. Числа с плавающей точкой (иначе вещественные) - float, double
  3. Логический - boolean
  4. Символьный - char

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

Тип Размер (бит) Диапазон
byte 8 бит от -128 до 127
short 16 бит от -32768 до 32767
char 16 бит беззнаковое целое число, представляющее собой символ UTF-16 (буквы и цифры)
int 32 бит от -2147483648 до 2147483647
long 64 бит от -9223372036854775808L до 9223372036854775807L

Пример использования целочисленных типов:

Символы тоже относят к целочисленным типам из-за особенностей представления в памяти и традиций.

Тип Размер (бит) Диапазон
float 32 от 1.4e-45f до 3.4e+38f
double 64 от 4.9e-324 до 1.7e+308
Тип Размер (бит) Значение
boolean 8 (в массивах), 32 (не в массивах используется int) true (истина) или false (ложь)

В стандартной реализации Sun JVM и Oracle HotSpot JVM тип boolean занимает 4 байта (32 бита), как и тип int. Однако, в определенных версиях JVM имеются реализации, где в массиве boolean каждое значение занимает по 1-му биту.

Ссылочные типы - это все остальные типы: классы, перечисления и интерфейсы, например, объявленные в стандартной библиотеке Java, а также массивы.

Строки это объекты класса String, они очень распространены, поэтому в некоторых случаях обрабатываются отлично от всех остальных объектов. Строковые литералы записываются в двойных кавычках.

Если требуется создать ссылку на один из примитивных типов данных, необходимо использовать соответствующий класс-обертку. Также в таких классах есть некоторые полезные методы и константы, например минимальное значение типа int можно узнать использовав константу Integer.MIN_VALUE. Оборачивание примитива в объект называется упаковкой (boxing), а обратный процесс распаковкой (unboxing).

Тип Класс-обертка
byte Byte
short Short
int Integer
long Long
char Character
float Float
double Double
boolean Boolean

Рекомендуется использовать valueOf, он может быть быстрее и использовать меньше памяти потому что применяет кэширование, а конструктор всегда создает новый объект.

Получить примитив из объекта-обертки можно методом <имя примитивного типа>Value.

Integer vs int

Все мы знаем, что в java — everything is an object. Кроме, пожалуй, примитивов и ссылок на сами объекты. Давайте рассмотрим две типичных ситуации:

В этих простых строках разница просто огромна, как для JVM так и для ООП. В первом случае, все что у нас есть — это 4-х байтная переменная, которая содержит значение из стека. Во втором случае у нас есть ссылочная переменная и сам объект, на который эта переменная ссылается. Следовательно, если в первом случае мы определено знаем, что занимаемый размер равен:

Забегая вперед скажу — во втором случае количество потребляемой памяти приблизительно в 5 раз больше и зависит от JVM. А теперь давайте разберемся, почему разница настолько огромна.

Из чего же состоит объект?
  • Заголовок объекта;
  • Память для примитивных типов;
  • Память для ссылочных типов;
  • Смещение/выравнивание — по сути, это несколько неиспользуемых байт, что размещаются после данных самого объекта. Это сделано для того, чтобы адрес в памяти всегда был кратным машинному слову, для ускорения чтения из памяти + уменьшения количества бит для указателя на объект + предположительно для уменьшения фрагментации памяти. Стоит также отметить, что в java размер любого объекта кратен 8 байтам!
Структура заголовка объекта
  • Маркировочное слово (mark word) — к сожалению мне так и не удалось найти назначение этой информации, подозреваю что это просто зарезервированная на будущее часть заголовка.
  • Hash Code — каждый объект имеет хеш код. По умолчанию результат вызова метода Object.hashCode() вернет адрес объекта в памяти, тем не менее некоторые сборщики мусора могут перемещать объекты в памяти, но хеш код всегда остается одним и тем же, так как место в заголовке объекта как раз может быть использовано для хранения оригинального значения хеш кода.
  • Garbage Collection Information — каждый java объект содержит информацию нужную для системы управления памятью. Зачастую это один или два бита-флага, но также это может быть, например, некая комбинация битов для хранения количества ссылок на объект.
  • Type Information Block Pointer — содержит информацию о типе объекта. Этот блок включает информацию о таблице виртуальных методов, указатель на объект, который представляет тип и указатели на некоторые дополнительные структуры, для более эффективных вызовов интерфейсов и динамической проверки типов.
  • Lock — каждый объект содержит информацию о состоянии блокировки. Это может быть указатель на объект блокировки или прямое представление блокировки.
  • Array Length — если объект — массив, то заголовок расширяется 4 байтами для хранения длины массива.
Спецификация Java

Известно, что примитивные типы в Java имеют предопределенный размер, этого требует спецификация для переносимости кода. Поэтому не будем останавливаться на примитивах, так как все прекрасно описано по ссылке выше. А что же говорит спецификация для объектов? Ничего, кроме того, что у каждого объекта есть заголовок. Иными словами, размеры экземпляров Ваших классов могут отличатся от одной JVM к другой. Собственно, для простоты изложения я буду приводить примеры на 32-х разрядной Oracle HotSpot JVM. А теперь давайте разберем самые используемые классы Integer и String.

Integer и String

Итак, давайте попробуем подсчитать сколько же будет занимать объект класса Integer в нашей 32-х разрядной HotSpot JVM. Для этого нужно будет заглянуть в сам класс, нам интересны все поля, которые не объявлены как static. Из таких видим только одно — int value. Теперь исходя из информации выше получаем:

Теперь заглянем в класс строки:

И подсчитаем размер:

Ну и это еще не все… Так как строка содержит ссылку на массив символов, то, по сути, мы имеем дело с двумя разными объектами — объектом класса String и самим массивом, который хранит строку. Это, как бы, верно с точки зрения ООП, но если посмотреть на это со стороны памяти, то к полученному размеру нужно добавить и размер выделенного для символов массива. А это еще 12 байт на сам объект массива + 2 байта на каждый символ строки. Ну и, конечно же, не забываем добавлять выравнивание для кратности 8 байтам. Итого в конечном итоге простая, казалось бы, строка new String(«a») выливается в:

Важно отметить, что new String(«a») и new String(«aa») будут занимать одинаковое количество памяти. Это важно понимать. Типичный пример использования этого факта в свою пользу — поле hash в классе String. Если бы его не было, то объект строки так или иначе занимал бы 24 байта, за счет выравнивания. А так получается что для этих 4-х байтов нашлось очень достойное применение. Гениальное решение, не правда ли?

Размер ссылки

Немножко хотел бы оговорится о ссылочных переменных. В принципе, размер ссылки в JVM зависит от ее разрядности, подозреваю, что для оптимизации. Поэтому в 32-х разрядных JVM размер ссылки обычно 4 байта, а в 64-х разрядных — 8 байт. Хотя это условие и не обязательно.

Группировка полей
Зачем все это?

Иногда возникает ситуация в которой Вам необходимо прикинуть приблизительный объем памяти для хранения тех или иных объектов, например словаря, эта маленькая справка поможет быстро сориентироваться. Также, это потенциально возможный способ оптимизации, особенно в том окружении, где доступ к его настройкам не доступен.

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».

Вот мы и добрались до самого частоиспользуемого типа. Занимает он 32 бита, или 4 байта. В общем, мы продолжаем удваивать. Диапазон значений от -2^31 до 2^31 – 1.

Максимальное значение int

Продолжаем удваивать. 32 умножаем на 2 и получаем 64 бита. По традиции, это 4 * 2, то есть 8 байт. Диапазон значений от -2^63 до 2^63 – 1. Более чем достаточно. Данный тип позволяет считать большие-большие числа. Часто используется при работе со временем. Или с большими расстояниями, например. Для обозначения того, что число это long после числа ставят литерал L – Long. Пример: Хочется забежать вперёд. Далее мы будем рассматривать тот факт, что для примитивов есть соответствующие обёртки, которые дают возможность работать с примитивами как с объектами. Но есть интересная особенность. Вот пример: На том же Tutorialspoint online compiler можете проверить такой вот код: Данный код работает без ошибок, всё хорошо. Но стоит в методе printLong заменить тип с long на Long (т.е. тип становится не примитивным, а объектным), как становится джаве непонятно, какой параметр мы передаём. Она начинает считать, что передаётся int и будет ошибка. Поэтому, в случае с методом необходимо будет явно указывать 4L. Очень часто long используется как ID при работе с базами данных.

Java float и Java double

Java boolean

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

Java char

  • Таблица Unicode символов
  • Таблица символов ASCII

Пример в студию: Кстати, char, являясь по своей сути всё таки числом, поддерживает математические действия, такие как сумма. А иногда это может привести к забавным последствиям: Настоятельно советую проверить в онлайн IDE от tutorialspoint. Когда я увидел этот пазлер на одной из конференций мне это подняло настроение. Надеюсь, Вам пример тоже понравится) UPDATED: Это было на Joker 2017, доклад: "Java Puzzlers NG S03 — Откуда вы все лезете-то?!".

Литералы

  • Десятеричная система: 10
  • Шестнадцатеричная система: 0x1F4, начинается с 0x
  • Восьмеричная система: 010, начинается с нуля.
  • Двоичная система (начиная с Java7): 0b101, начинается с 0b

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

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

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

Примитивы имеют свои классы-обертки, чтобы можно было работать с ними как с объектами. То есть, для каждого примитивного типа существует, соответствующий ему ссылочный тип. Классы-обертки являются immutable (неизменяемыми): это означает, что после создания объекта его состояние — значение поля value — не может быть изменено. Классы-обертки задекларированы как final: объекты, так сказать, read-only. Также хотелось бы упомянуть, что от этих классов невозможно наследоваться. Java автоматически делает преобразования между примитивными типами и их обертками: Процесс преобразования примитивных типов в ссылочные (int->Integer) называется autoboxing (автоупаковкой), а обратный ему — unboxing (автораспаковкой). Эти классы дают возможность сохранять внутри объекта примитив, а сам объект будет вести себя как Object (ну как любой другой объект). При всём этом мы получаем большое количество разношерстных, полезных статических методов, как например — сравнение чисел, перевод символа в регистр, определение того, является ли символ буквой или числом, поиск минимального числа и т.п. Предоставляемый набор функционала зависит лишь от самой обертки. Пример собственной реализации обёртки для int: В основном пакете, java.lang, уже есть реализации классы Boolean, Byte, Short, Character, Integer, Float, Long, Double, и нам не нужно ничего городить своего, а только переиспользовать готовое. К примеру, такие классы дают нам возможность создать, скажем, List , ведь List должен содержать только объекты, чем примитивы не являются. Для преобразования значения примитивного типа есть статические методы valueOf, например, Integer.valueOf(4) вернёт объект типа Integer. Для обратного преобразования есть методы intValue(), longValue() и т. п. Компилятор вставляет вызовы valueOf и *Value самостоятельно, это и есть суть autoboxing и autounboxing. Как выглядит пример автоупаковки и автораспаковки, представленный выше, на самом деле: Подробнее про автоупаковку и автораспаковку можно почитать вот в этой статье.

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

При работе с примитивами существует такое понятие как приведение типов, одно из не очень приятных свойств C++, тем не менее приведение типов сохранено и в языке Java. Иногда мы сталкиваемся с такими ситуациями, когда нам нужно совершать взаимодействия с данными разных типов. И очень хорошо, что в некоторых ситуациях это возможно. В случае с ссылочными переменными, там свои особенности, связанные с полиморфизмом и наследованием, но сегодня мы рассматриваем простые типы и соответственно приведение простых типов. Существует преобразование с расширением и преобразование сужающее. Всё на самом деле просто. Если тип данных становится больше (допустим, был int, а стал long), то тип становится шире (из 32 бит становится 64). И в этом случае мы не рискуем потерять данные, т.к. если влезло в int, то в long влезет тем более, поэтому данное приведение мы не замечаем, так как оно осуществляется автоматически. А вот в обратную сторону преобразование требует явного указания от нас, данное приведение типа называется — сужение. Так сказать, чтобы мы сами сказали: «Да, я даю себе отчёт в этом. В случае чего — виноват сам». Чтобы потом в таком случае не говорили что «Ваша Джава плохая», когда получат внезапно -128 вместо 128 ) Мы ведь помним, что в байте 127 верхнее значение и всё что находилось выше него соответственно можно потерять. Когда мы явно превратили наш int в байт, то произошло переполнение и значение стало -128.

I am ready for a long road flight for working with a week- or months-long projects.

В этом руководстве мы рассмотрим плюсы и минусы использования примитивов в Java и соответствующих им классов-оберток.

В языке Java существует два типа переменных: примитивные, например int и boolean, а также ссылочные типы вроде Integer и Boolean (классы-обертки). Для каждого примитивного типа существует, соответствующий ему ссылочный тип.

Классы-обертки являются неизменяемыми: это означает, что после создания объекта его состояние (значение поля value) не может быть изменено; и задекларированы, как final (от этих классов невозможно наследоваться).

Java автоматически производит преобразования между примитивными типами и их обертками:

Процесс преобразования примитивных типов в ссылочные называется автоупаковкой (autoboxing), а обратный ему — автораспаковкой (unboxing).

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

2.1 Использование памяти единичными объектами

В зависимости от реализации виртуальной машины, эти значения могут изменяться. Например, в виртуальной машине Oracle значения типа boolean сопоставляются со значениями 0 и 1 типа int (это связано с тем, что в VM нет инструкций для работы с булевыми значениями) и, как результат, занимают в памяти 32 бита.

Переменные примитивных типов хранятся в стеке, позволяя иметь к ним быстрый доступ.

Ссылочные переменные ссылаются на объекты, которые хранятся в куче, к ним мы имеем более медленный доступ (рекомендуем ознакомиться с понятиями Стек и Куча, для лучшего усвоения материала). Ссылочные типы имеют определенные накладные расходы относительно их примитивных аналогов.

Накладные расходы зависят от реализации конкретной JVM. Здесь мы приведем результаты для 64-х битной виртуальной машины со следующими параметрами:


В результате, один экземпляр ссылочного типа на данной JVM занимает 128 бит. За исключением Long и Double, которые занимают 192 бита:

  • Boolean — 128 бит
  • Byte — 128 бит
  • Short, Character — 128 бит
  • Integer, Float — 128 бит
  • Long, Double — 192 бита

Обратите внимание, переменная типа Boolean занимает в 128 раз больше места чем соответствующий ей примитив, тогда как Integer занимает памяти как 4 int переменные.

Более интересно обстоят дела с объемом памяти который занимают массивы, рассматриваемых нами типов.

При создании массивов с различным количеством элементов для каждого типа, мы получаем график:

который демонстрирует как массивы различных типов потребляют память (m) в зависимости от количества содержащихся элементов (e):

  • long, double: m = 128 + 64e
  • short, char: m = 128 + 64(e/4)
  • byte, boolean: m) = 128 + 64(e/8)
  • the rest: m = 128 + 64(e/2)

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

Это может показаться неожиданным, но массивы примитивных типов long и double занимают больше памяти чем их классы-обертки Long и Double соответственно.

Производительность Java кода, довольно тонкий вопрос, сильно зависящий от аппаратной части устройства на котором он исполняется, от различных оптимизационных процессов, выполняемых компилятором, от конкретной JVM, а также от других процессов, происходящих в операционной системе.

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

Чтобы продемонстрировать насколько операции над примитивными типами быстрее чем над их классами-обертками, создадим массив из миллиона элементов в котором все элементы одинаковы, кроме последнего. Затем мы попытаемся найти этот элемент и сравним результаты производительности работы массива переменных примитивных типов с массивом ссылочных переменных.

Мы используем известный инструмент тестирования производительности JMH, а результаты операции суммируем в диаграмме:

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

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

Значение по умолчанию для примитивных числовых типов равно 0 (0 для целочисленных, 0.0d и 0.0f для double и float соответственно), для boolean — false, а для типа char — \u0000. Для классов-оберток значение по умолчанию равно null.

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

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

С переменными ссылочных типов такой проблемы не возникает, так как значение null является очевидным признаком того, что переменная не была проинициализирована.

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

С другой стороны, текущая спецификация языка Java не позволяет использовать примитивные типы при параметризации (generics), в коллекциях Java или в Reflection API.

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

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

Быстрое и практичное сравнение объектов и примитивов в Java.

1. Обзор

В этом уроке мы покажем плюсы и минусы использования примитивных типов Java и их обернутых аналогов.

2. Система типов Java

Java имеет двойную систему типов, состоящую из примитивов, таких как int , boolean , и ссылочных типов, таких как Integer, | Boolean . Каждый примитивный тип соответствует ссылочному типу.

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

Под капотом Java выполняет преобразование между примитивным и ссылочным типами, если фактический тип отличается от объявленного:

Процесс преобразования примитивного типа в эталонный называется автобоксом, противоположный процесс называется распаковкой.

3. Плюсы и минусы

Решение о том, какой объект будет использоваться, основано на том, какой производительности приложения мы пытаемся достичь, сколько доступной памяти у нас есть, объем доступной памяти и какие значения по умолчанию мы должны обрабатывать.

Если мы не сталкиваемся ни с одним из них, мы можем игнорировать эти соображения, хотя их стоит знать.

3.1. Объем памяти Одного Элемента

Просто для справки, переменные примитивного типа оказывают следующее влияние на память:

  • логическое значение – 1 бит
  • байт – 8 бит
  • короткие, char – 16 бит
  • int, float – 32 бита
  • длинный, двойной – 64 бита

На практике эти значения могут варьироваться в зависимости от реализации виртуальной машины. В виртуальной машине Oracle логический тип, например, сопоставляется со значениями int 0 и 1, поэтому он занимает 32 бита, как описано здесь: Примитивные типы и значения .

Переменные этих типов находятся в стеке и, следовательно, быстро доступны. Для получения более подробной информации мы рекомендуем наш учебник по модели памяти Java.

Ссылочные типы-это объекты, они живут в куче и относительно медленно доступны. У них есть определенные накладные расходы по отношению к их примитивным аналогам.

Конкретные значения накладных расходов, как правило, зависят от JVM. Здесь мы представляем результаты для 64-разрядной виртуальной машины с этими параметрами:

Чтобы получить внутреннюю структуру объекта, мы можем использовать инструмент Java Object Layout (см. Наш Другой учебник о том, как получить размер объекта).

Оказывается, что один экземпляр ссылочного типа в этой JVM занимает 128 бит, за исключением Long и Double , которые занимают 192 бита:

  • Логическое значение – 128 бит
  • Байт – 128 бит
  • Короткий, символьный – 128 бит
  • Целое число, с плавающей точкой – 128 бит
  • Длинные, двойные 192 бита

Мы видим, что одна переменная типа Boolean занимает столько же места, сколько 128 примитивных, в то время как одна переменная Integer занимает столько же места, сколько четыре int единицы.

3.2. Объем памяти для массивов

Ситуация становится более интересной, если мы сравним, сколько памяти занимают массивы рассматриваемых типов.

Когда мы создаем массивы с различным количеством элементов для каждого типа, мы получаем график:

это демонстрирует, что типы сгруппированы в четыре семейства в зависимости от того, как память m(s) зависит от количества элементов s массива:

  • длинный, двойной: + 64 с
  • короткий, char: + 64 [s/4]
  • байт, логическое значение: + 64 [s/8]
  • остальные: + 64 [s/2]

где квадратные скобки обозначают стандартную функцию потолка.

Удивительно, но массивы примитивных типов long и double потребляют больше памяти, чем их классы-оболочки Long и Double .

Мы можем видеть, что одноэлементные массивы примитивных типов почти всегда дороже (за исключением длинных и двойных), чем соответствующий ссылочный тип .

3.3. Производительность

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

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

Чтобы продемонстрировать, насколько операции для примитивных типов быстрее, чем для классов-оболочек, давайте создадим массив из пяти миллионов элементов, в котором все элементы равны, за исключением последнего; затем мы выполним поиск для этого элемента:

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

Мы используем хорошо известный инструмент JMH бенчмаркинга (см. Наш учебник о том, как его использовать), и результаты операции поиска можно обобщить на этой диаграмме:

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

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

3.4. Значения по умолчанию

Значения по умолчанию примитивных типов 0 (в соответствующем представлении, т. е. 0 , 0.0 d etc) для числовых типов, false для логического типа, \u0000 для типа char. Для классов-оболочек значение по умолчанию равно null .

Это означает, что примитивные типы могут получать значения только из своих доменов, в то время как ссылочные типы могут получать значение ( null ), которое в некотором смысле не принадлежит их доменам.

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

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

С переменными класса-оболочки такой проблемы нет, поскольку значение null является вполне очевидным признаком того, что переменная не была инициализирована.

4. Использование

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

С другой стороны, текущая спецификация языка Java не позволяет использовать примитивные типы в параметризованных типах (универсальных), в коллекциях Java или API отражения.

5. Заключение

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

Читайте также: