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

Простейшим способом классификации типов данных в Java является их разделение на примитивы и объекты. К примитивам, как известно большинству разработчиков Java, относятся булевы числа, байты, символы, варианты целых чисел (short, int и long), а также варианты чисел с плавающей точкой (floats и doubles). Внутри JVM эти примитивы инстанцируются в сыром виде. Объявление переменной типа int создает в JVM 32-битное целое поле, с которым она может работать. Чаще всего эти примитивы создаются в стеке операндов, который строится при каждом вызове метода. (Заметным исключением являются статические примитивы, которые создаются в куче).

В отличие от простых выделенных примитивов, объекты — это сущности, которые окружают элемент данных методами, а иногда и дополнительными вспомогательными полями. Например, объект String содержит массив (мы вернемся к этому немного позже), в котором хранится содержимое строки, а также вспомогательные поля, которые используются различными методами, определенными для String. Объекты создаются в области памяти, называемой "куча", то есть они выделяются из свободной памяти.

Все объекты, за исключением массивов, имеют конструктор. Если в исходном коде не определен конструктор для нового объекта, Java-компилятор создает для него конструктор без параметров. Чаще всего этот конструктор вызывает конструктор по умолчанию в классе Object, который по факту ничего не делает.

Природа массивов

Массивы являются объектами. Однако внутри JVM массивы заметно отличаются от всех остальных объектов. Первое существенное отличие заключается в том, что массивы создаются JVM, а не явным или неявным вызовом функции new() руками разработчика. Когда компилятор Java впервые встречает набор скобок, прикрепленных к имени переменной, он выдает определенный бай-ткод, который указывает JVM на создание массива. Компилятор также указывает, какого типа элементы данных будет содержать массив (примитивы или объекты) и какова его размерность.

Далее JVM создает массив подходящего размера и типа и оборачивает его в виде объекта. То есть все методы, доступные в классе Object, которые наследуются, например, toString(), доступны и для массивов. Элементы последней размерности во вновь созданном массиве инициализируются значением по умолчанию для данного типа данных (нулевым значением для числовых типов, null для объектов).

Инициализация массивов

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

Статью в оригинале можно прочитать тут

importantYears = new int[] {800, 1066, 1492,};
(Заметим, что запятая после последнего значения допустима в Java и не выдает ошибку). В Java нельзя инициализировать отдельные выбранные элементы с помощью приведенного выше синтаксиса. Необходимо инициализировать конкретный элемент индивидуально.

Еще одной особенностью массивов Java является то, что они могут иметь размер, равный нулю.

unimportantYears = new int[0];
Такой код тоже не выдает ошибку. Эта прекрасная фича в основном используется генераторами кода, которые могут создать массив, а затем внезапно обнаружить, что в нем нет значений. В данном примере unimportantYears не является null, а представляет собой пустой массив. Точно так же строка нулевой длины не является нулевой, а представляет собой весьма жизнеспособный объект.

Многомерные массивы

Если одномерные массивы имеют свои причуды, то многомерные массивы таят в себе гораздо больше некоторой магии. Приведем пример трехмерного массива, представляющего размеры x, y и z трех значений для финансовой транзакции.

points = new int[2][3][4]; // a point or 1% in interest
Когда компилятор обрабатывает подобный код, он создает уникальный байткод MULTIANEWARRAY, который создаст массив с размерами, каждый из которых равен указанному размеру. Этот массив реализован как массив массивов. То есть первые два измерения содержат только указатели на другие массивы. Поэтому, например, при обращении к элементу данных по адресу 1, 2, 0, 1 указывает не на ряд значений, а на массив указателей на массивы значений указателей. Эти значения указывают на еще один массив - массив целых чисел. Другими словами, points - это массив из двух указателей на массивы из трех указателей на массивы из четырех целых чисел. На рис. 1 эта структура показана наглядно.
Рисунок 1. Трехмерный массив в процессе его создания в JVM
Если представить эту конструкцию в виде дерева, то можно заметить, что только массивы на листьях содержат реальные значения. Это несколько нелогично. Двумерные массивы часто можно представить как таблицу. (Но строго говоря, в представлении двумерного массива в JVM нет ничего общего с привычной таблицей, состоящей из строк и столбцов).

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

Один из способов уменьшить эту нагрузку — это развернуть массивы в одноразмерный массив. Например, сделать его массивом размером 1 x 24, а затем самостоятельно сопоставлять три координаты с нужным элементом массива. После этого обновление всех значений в массиве будет происходить быстро и со значительным снижением накладных расходов. Как и с любыми другими решениями, следует тщательно оценивать производительность, чтобы убедиться, что подобного рода компромисс оправдывает затраты.

Размер массива и понятие массивов массивов

Многие коллекции Java имеют метод size(), который возвращает целое число, указывающее на количество элементов в коллекции. У массивов нет такого метода. Это объясняется это несколькими причинами, но главная из них заключается в том, что массивы — это простые экземпляры Object, а не коллекции. Класс Object не имеет метода size(), поэтому и массивы не имеют его.

Однако массивы имеют свойство length, которое можно запросить для получения количества элементов в указанном массиве. В одномерном массиве (как например в первом примере в этой статье) следующий код отдаст 3:


importantYears.length
Для многомерных массивов этот же же запрос выдаст довольно неожиданный результат. Используя предыдущий массив points, значение points.length будет равно 2, а не 24, как можно было бы ожидать. Причина в том, что points рассматривается только как массив из двух элементов (которые, кстати, являются указателями на другие массивы). Если необходимо получить размеры всех измерений, то нужно сделать следующее:

System.out.printf("\n Length of points: %d", points.length);
System.out.printf("\n Length of points[0]: %d", points[0].length);
System.out.printf("\n Length of points[0][0]: %d", points[0][0].length);
И тогда мы получим следующее:

Length of points: 2
Length of points[0]: 3
Length of points[0][0]: 4
Как видите, это и правда три массива, которые работают вместе, создавая эквивалент трехмерного массива. Поэтому, чтобы получить размер каждого измерения, необходимо указать, какое именно измерение вам нужно. (Несколько нелогично, что нулевое измерение не является первым в массиве, но чтож).

Возникает интересный вопрос: Что произойдет в многомерном массиве, если одно из измерений будет объявлено с размером 0? Например,

strangePoints = new int[3][4][0][2]
В этом объявлении все размеры после после размерности с нулевым размером игнорируются. Таким образом, результат этого объявления эквивалентен многомерному массиву целых чисел. Это логично, поскольку размерность с нулевым размером не содержит указателей и не может указывать на последующие слои массива.

Возвращаемся в недра JVM

Зоркий глаз, прочитав мое предыдущее утверждение о том, что length является полем, а не вызовом метода, может удивиться, как у дочернего класса Object может появиться поле length, когда у Object такого поля нет? Ответ заключается в том, что внутри компилятора Java происходит некоторая магия. Когда компилятор обнаруживает ссылку на длину массива, он выдает специальный байт-код ARRAYLENGTH, который получает длину массива и возвращает ее. Это выглядит и ведет себя как вызов метода, но все вызовы методов в JVM требуют одного из небольшого набора байт-кодов, и они реализуются через создание нового фрейма с выделением стека и некоторыми другими операциями. Все это не происходит с этим особенным байт-кодом.

Никакие другие объекты Java не имеют соответствующего байткода для определения их размера. Это лишь один из многих аспектов, которые делают массивы совершенно уникальными сущностями в JVM. Итак, теперь, когда вы будете создавать и использовать массивы, вы будете знать, что здесь происходит некоторая магия, и понимать, как использовать эту магию, чтобы получить желаемое поведение.