Последнее изменение: 1 сентября 2007г.

Массивы в Java

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

Статья разделена на три части:

Небольшое замечание с самого начала. Массив, какого бы типа и размерности он ни был, является объектом. Это значит, что он обладает всеми свойствами и методами объекта.

Итак, начнем мы с простейших случаев, а именно –

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

Эти массивы практически не отличаются от того, к чему привыкли программисты, использующие другие языки. Создание массива производится с помощью все того же оператора new:

// creating array of integers with 5 elements:
int array[] = new int[5];

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

int array[] = new int[5];
System.out.println("Array size: "+array.length);
// will print: Array size: 5

Свойство это является final, потому выставить через него новый размер массива, увы, не получится.

Вторая мелочь – контроль выхода за границы массива. Это делает интерпретатор, в случае выхода индекса за пределы массива будет инициировано исключение java.lang.ArrayIndexOutOfBoundsException. Перехватывать его не обязательно, и, я бы даже сказал, нежелательно, т.к. это RuntimeException и сигнализирует оно о том, что программа работает неправильно. И уж совсем не стоит проектировать приложение в расчете на то, что после прохождения всего массива будет брошено это исключение, и это будет сигналом к завершению цикла обработки. Это очень плохая идея.

После создания массив инициализируется значением по умолчанию для типа его элементов. Это гарантируется спецификацией языка.

Есть одна тонкость. При создании размер массива может быть задан равным 0. Это будет вполне полноценный массив, содержащий 0 элементов. Его свойство .length равно 0. Такая конструкция временами оказывается весьма полезной.

Пара слов о копировании. Тривиальный способ – создать массив такого же размера и в цикле перенести содержимое. Хорошо, надежно, но не очень быстро. Есть способ быстрее – использовать метод System.arraycopy(...). В этом методе после всех проверок происходит просто копирование области памяти. Сигнатура этого метода такая:

public static void arraycopy(Object src, int srcPos, Object dest, int destPos, int length)

Копирование происходит из массива src, начиная с позиции srcPos, в массив dest, начиная с позиции destPos. Всего копируется length элементов. Обратите внимание, что src и dest имеют тип Object. Это сделано для того, чтобы этот метод мог обрабатывать массивы любого типа. Если src или dest не является массивом, будет инициировано исключение java.lang.ArrayStoreException.

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

Одномерные массивы объектов

Большая часть из того, что сказано о массивах примитивных типов, верна и для массивов объектов. Их размер может быть получен через .length, выход за границы контролируется. Создаются они через оператор new:

// array of java.awt.Point with 10 elements:
java.awt.Point[] points = new java.awt.Point[10];

Заметьте, вызова конструктора тут НЕТ. java.awt.Point после оператора new указывает лишь на тип элементов создаваемого массива.

После создания массив точно так же инициализируется значением по умолчанию... И вот тут есть большой подводный камень. Вопрос на засыпку. А каково это значение по умолчанию? Объект, созданный вызовом конструктора без параметров? А если такого конструктора нет? Ответ кроется в ответе на другой вопрос: а что, собственно, хранится в этом массиве?

А хранятся в этом массиве не объекты. Там хранятся ссылки на них. Как любая переменная объектного типа является ссылкой на объект, так и любой элемент массива тоже является ссылкой. А для ссылки значение по умолчанию – null!

Эта ошибка чуть ли не классическая. Очень часто приходится встречать вопросы "откуда тут берется NullPointerException" по отношению к коду следующего вида:

java.awt.Point[] points = new java.awt.Point[10];
points[0].x = 1; // <-- here is a NullPointerException

Теперь, думаю, понятно, откуда берется эта ошибка. Массив создан и его элементы инициализированы значением null. Однако сами объекты не созданы. Ситуация в точности аналогична использованию переменной со значением null. Правильный фрагмент выглядит так:

java.awt.Point[] points = new java.awt.Point[10];
for(int i=0; i<points.length; i++){
    points[i] = new java.awt.Point();
}
// now you can use array
points[0].x = 1; // <-- NO NullPointerException here!

Хочу специально коснуться вопроса копирования элементов массива объектов. Поскольку содержит он ссылки, именно они и копируются. Т.е. после копирования новый массив будет указывать на тот же самый набор объектов! И если изменить внутреннее состояние какого-либо из объектов – это будет видно при доступе к нему через любой из массивов.

Итак, мы потихоньку подобрались к самому интересному разделу –

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

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

Прежде всего хочу упомянуть такой факт. Для двумерного массива не существует такого понятия как его размеры. Можно определить размер массива только по первому индексу. Причина кроется в организации массива. Он представляет собой массив ссылок на массивы, каждый из которых содержит реальные данные. И эти массивы могут иметь разную длину!

Непрямоугольный двухмерный массив

Рассмотрим вот этот двумерный массив. Его длина по первому индексу равна 3. Ее можно получить через a.length. Элементами этого массива являются ссылки на массивы с данными. Длина каждого из этих массивов может быть получена так же через .length. Доступ к каждому из этих массивов осуществляется через его индекс – первый индекс в массиве a.

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

Пойдем дальше. Стандартный способ создания двумерного массива – это использование обычной конструкции:

// 2d array of ints with 10 rows of 5 elements each
int[][] array2d = new int[10][5];

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

Есть, однако, и другой способ. Для примера создадим массив, приведенный выше на рисунке:

int[][] a = new int[3][]; // line 1
a[0] = new int[2];        // line 2
a[1] = new int[4];        // line 3
a[2] = new int[3];        // line 4

Обратите внимание, в строке 1 при инициализации массива не указана размерность по второму индексу!

Что происходит в этом коде? В строке 1 создается массив по первому индексу, размером 3. Это означает, что выделяется память под три ссылки на массивы-строки. Сами массивы не создаются, выделенная память инициализирована значениями null.

В строке 2 создается массив длиной 2. Ссылка на этот массив сохраняется в первом элементе массива по первому индексу. Точно так же в строках 3 и 4 создаются массивы, ссылки на которые сохраняются во втором и третьем элементах массива по первому индексу.

В итоге мы имеем массив произвольной формы. Разумеется, мы могли бы создать все три массива-строки одинаковой длины, скажем, 5. Эти действия были бы полностью эквивалентны конструкции new int[3][5].

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

Перейдем теперь к n-мерным массивам. Ничего нового тут нет. Как двумерный массив представляет собой массив ссылок на одномерные массивы, так и трехмерный массив представляет собой список ссылок, но уже на двумерные массивы. Общее правило таково:

N-мерный массив является одномерным массивом, элементами которого являются ссылки на массивы размерности N-1.

Соответственно, N-мерные массивы можно создавать одним выражением, с использованием оператора new и указанием размеров по всем измерениям. А можно и последовательно – создать массив, указав размерность по одному или нескольким индексам, а оставшиеся неинициализированными ссылки проинициализировать вручную, создавая массивы нужной мерности, но произвольного размера/формы. Однако для массивов большой мерности это довольно сложная задача, на мой взгляд. Собственно, как и вообще работа с многомерными массивами.

Отдельно несколько слов о копировании многомерных массивов с помощью System.arraycopy. Массивы будут копироваться ТОЛЬКО по первому индексу. Иначе говоря, во второй массив будут перенесены ссылки на массивы размерности N-1 из первого массива. В случае двумерного массива – это ссылки на массивы-строки. Это обстоятельство весьма облегчает копирование массивов, скажем, при добавлении новой строки – память нужно выделять только под массив по первому индексу. Далее ссылки на все строки будут скопированны в новый массив, а последняя ссылка, которая должна указывать на новую строку, будет проинициализирована вручную:

int[][] initArray;                              // initial array
int newArrayLength = initArray.length+1;
int[][] newArray = new int[newArrayLength][];
for(int i=0; i<initArray.length; i++){
    newArray[i] = initArray[i];
}
newArray[newArrayLength – 1] = new int[<row size>];

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

* * *

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

Всем спасибо! Надеюсь, кому-то эта информация оказалась полезной и интересной.