Перейти к основному содержимому

Массивы в Java

Разработчику Архитектору

Play ITЗагрузка интерактивного демо…


Массивы в Java

Суть и природа массива

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

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

Массив в Java — это объект. У него есть поле length, которое хранит размер массива, и возможность доступа к элементам по индексу [i]. Больше в нем ничего нет. Это делает его легковесным и предсказуемым с точки зрения производительности.

Ещё раз. Массив не обладает методами и полями, которые присущи коллекциям. Он только хранит данные, даёт доступ по индексу и обладает размером.


Отличие от коллекций

Частая ошибка новичков — путать массивы и коллекции. Важно четко разграничивать эти понятия.

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

Массивы и коллекции — это разные сущности с разными целями.

ХарактеристикаМассив (Array)Коллекция (например, ArrayList)
РазмерФиксирован при создании. Изменить нельзя.Динамический. Может расти и уменьшаться.
Типы данныхМожет хранить примитивы (int, double) и объекты.Хранит только объекты (обертки Integer, Double).
МетодыНет методов добавления/удаления. Только доступ по индексу.Есть методы add(), remove(), size(), contains() и др.
ПроизводительностьМаксимальная. Прямой доступ к памяти.Ниже из-за накладных расходов на методы и автоупаковку.
ПриродаВстроенная конструкция языка (keyword/syntax).Часть библиотеки java.util (классы и интерфейсы).

Почему важно различать

Java проектировалась как язык, сочетающий эффективность C++ и безопасность высокоуровневых языков. Разделение на массивы и коллекции возникло из-за трех ключевых факторов.


Производительность и работа с памятью

Массивы в Java — это прямая обертка над участком памяти. Доступ по индексу arr[i]O(1) (константное время): при создании int[n] JVM знает тип элемента и его размер, поэтому адрес ячейки i вычисляется как база + i × sizeof(элемент) без перебора.

Коллекции, такие как ArrayList, внутри себя также используют массивы. В ArrayList по индексу хранятся ссылки одинакового размера; сами объекты лежат в куче отдельно — поэтому get(i) тоже O(1) по индексу, хотя объекты разных классов могут занимать разный объём памяти. Однако каждый вызов метода get() или set() — это вызов метода объекта. Хотя JIT-компилятор часто оптимизирует такие вызовы (inlining), дополнительные проверки границ, модификации и логика расширения массива создают накладные расходы.


Работа с примитивами

Коллекции в Java могут хранить только объекты. Если вы хотите сохранить число 5 в ArrayList<Integer>, Java автоматически упаковывает его в объект Integer. Объект Integer занимает в памяти 16–24 байта (заголовок объекта + ссылка на значение + само значение), тогда как примитив int занимает всего 4 байта.

Для массива из миллиона элементов разница будет колоссальной:

  • int[] займет ~4 МБ.
  • ArrayList<Integer> займет значительно больше из-за объектов-оберток и структуры самого списка. Это создает нагрузку на сборщик мусора (GC).

Низкоуровневые операции

Многомерные массивы критичны для задач, требующих работы с большими объемами однородных данных:

  • Матричные вычисления (тензоры, обработка изображений).
  • Буферы ввода-вывода (чтение файлов в byte[]).
  • Сетевые пакеты (заголовки фиксированной длины).
  • Взаимодействие с нативным кодом (JNI работает напрямую с массивами).

Пример обработки изображения:

// Массив пикселей: 1920x1080, 4 канала (RGBA)
byte[][][] image = new byte[1920][1080][4];

Использование коллекции List<List<List<Byte>>> для такой задачи было бы в десятки раз медленнее и потребовало бы огромного количества памяти.


Синтаксис объявления и создания

Синтаксис массивов в Java имеет свои особенности. Существует два способа объявления переменной массива, но рекомендуемым является первый.


Объявление переменной

// Рекомендуемый стиль: тип указывает, что это массив
int[] numbers;

// Допустимый, но менее читаемый стиль (в стиле C/C++)
int numbers[];

Рекомендуемый стиль int[] numbers предпочтительнее, так как тип переменной явно говорит о том, что это массив целых чисел. Это улучшает читаемость кода, особенно когда объявляется несколько переменных.


Создание массива

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

// Объявление
int[] numbers;

// Создание массива из 5 элементов
numbers = new int[5];

// Комбинированный вариант (объявление и создание)
int[] arr = new int[5];

Размер массива фиксируется в момент создания и не может быть изменен в дальнейшем. Попытка изменить поле length приведет к ошибке компиляции.

int[] fixedArray = new int[5];
// fixedArray.length = 10; // Ошибка компиляции: cannot assign a value to final variable length

Инициализация значений

Массив можно заполнить значениями сразу при создании.

// Инициализация при создании (размер определяется автоматически)
int[] scores = {10, 20, 30, 40, 50};

// Инициализация через анонимный массив
int[] values = new int[]{1, 2, 3, 4, 5};

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


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

Одномерный массив — это линейная последовательность элементов. Каждый элемент имеет свой индекс, начинающийся с нуля.


Доступ к элементам

Доступ к элементам осуществляется через квадратные скобки.

int[] numbers = {10, 20, 30, 40, 50};

// Чтение элемента по индексу 2 (третий элемент)
int value = numbers[2]; // value = 30

// Запись значения в элемент по индексу 0
numbers[0] = 100; // Теперь массив: {100, 20, 30, 40, 50}

Разбор:

  • Массив numbers создаётся и инициализируется литералом, поэтому размер и значения фиксируются в момент создания.
  • Выражение numbers[2] показывает чтение по индексу с константной сложностью O(1) за счёт прямого вычисления адреса элемента.
  • Операция numbers[0] = 100 выполняет замену значения "на месте", не создавая новый массив и не сдвигая остальные элементы.
  • Пример подчёркивает ключевую модель массивов: быстрый доступ по индексу при фиксированной длине структуры.

Индексация начинается с нуля. Первый элемент имеет индекс 0, последний — length - 1.


Поле length

У каждого массива есть публичное финальное поле length, которое хранит количество элементов.

int[] data = new int[10];
System.out.println(data.length); // Выведет 10

Это поле удобно использовать в циклах для обхода всех элементов.

for (int i = 0; i < data.length; i++) {
System.out.println(data[i]);
}

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

for (int item : data) {
System.out.println(item);
}

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

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


Объявление и создание

Двумерный массив объявляется с двумя парами квадратных скобок.

// Объявление
int[][] matrix;

// Создание матрицы 3x4 (3 строки, 4 столбца)
matrix = new int[3][4];

// Комбинированный вариант
int[][] grid = new int[3][4];

Инициализация

Многомерный массив можно инициализировать значениями сразу.

int[][] matrix = {
{1, 2, 3},
{4, 5, 6},
{7, 8, 9}
};

Неравномерные массивы (Jagged Arrays)

Поскольку многомерный массив — это массив ссылок на другие массивы, внутренние массивы могут иметь разную длину. Это называется "зубчатым" или неравномерным массивом.

int[][] jagged = {
{1, 2}, // Длина 2
{3, 4, 5}, // Длина 3
{6} // Длина 1
};

Разбор:

  • int[][] в Java является массивом ссылок на внутренние массивы, поэтому строки могут иметь разную длину.
  • В примере первая строка содержит 2 элемента, вторая — 3, третья — 1; это и есть jagged-структура.
  • Такой формат экономит память, когда данные "неровные" и не требуют прямоугольной матрицы фиксированной ширины.
  • При обходе нужно использовать jagged[i].length, потому что единое количество столбцов для всех строк не гарантируется.

Это возможно потому, что jagged[0], jagged[1] и jagged[2] — это отдельные объекты-массивы, созданные независимо друг от друга.


Доступ к элементам

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

// Чтение элемента во второй строке, третьем столбце
int value = matrix[1][2]; // значение 6

// Запись элемента
matrix[1][2] = 10;

// Получение размеров
int rows = matrix.length; // количество строк (внешний массив)
int cols = matrix[0].length; // количество столбцов в первой строке

При работе с неравномерными массивами нужно быть осторожным, так как matrix[i].length может отличаться для разных i.


Ковариантность массивов

Одна из важных и потенциально опасных особенностей массивов в Java — их ковариантность.

Ковариантность означает, что если тип B является подтипом типа A, то массив B[] является подтипом массива A[].

Например, String является подтипом Object. Следовательно, String[] является подтипом Object[].

String[] strings = {"Hello", "World"};
Object[] objects = strings; // Это допустимо!

Это позволяет передавать массивы строк в методы, принимающие массивы объектов. Однако это создает риск ошибки времени выполнения.

Object[] objects = new String[2];
objects[0] = "Hello"; // OK
objects[1] = 123; // Ошибка времени выполнения: ArrayStoreException

Компилятор не может предотвратить эту ошибку, так как на этапе компиляции objects имеет тип Object[], и присваивание целого числа кажется допустимым. Однако JVM проверяет реальный тип массива во время выполнения и выбрасывает исключение ArrayStoreException, если тип не совпадает.

Коллекции, в отличие от массивов, инвариантны. List<String> не является подтипом List<Object>, что предотвращает подобные ошибки на этапе компиляции.


Сравнение с другими языками

Python

В Python списки (list) являются динамическими массивами. Их размер может меняться, и они могут хранить элементы разных типов.

Python:

dynamic_list = [1, 2, 3]
dynamic_list.append(4) # Размер изменяется
dynamic_list.extend([5, 6]) # Добавление нескольких элементов
dynamic_list[0] = "Text" # Можно менять тип элемента

Java:

int[] fixedArray = new int[5];
// fixedArray.add(4); // Ошибка: у массивов нет метода add
// fixedArray[0] = "Text"; // Ошибка компиляции: несовместимые типы

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


JavaScript

В JavaScript массив — это по сути объект с цифровыми ключами и набором встроенных методов. Он может менять размер, хранить любые типы данных и имеет методы push, pop, map, filter и т.д.

В Java массивы — это низкоуровневая конструкция для максимальной производительности. Весь "комфорт" работы с динамическими данными вынесен в отдельные классы коллекций.


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

Несмотря на наличие мощных коллекций, массивы остаются незаменимыми в следующих случаях:

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

  1. Известный и неизменный размер данных. Если вы точно знаете, сколько элементов будет храниться, и это количество не меняется, массив будет эффективнее.
  2. Работа с примитивами. Для хранения больших объемов чисел (int, double, byte) массивы экономят память и ускоряют работу.
  3. Высокие требования к производительности. В алгоритмах, где критична скорость доступа к данным (например, сортировка, поиск, математические вычисления), массивы предпочтительнее.
  4. Взаимодействие с API. Многие стандартные библиотеки Java и сторонние фреймворки используют массивы как основной способ передачи данных (например, методы read() в InputStream возвращают byte[]).
  5. Многомерные данные. Для представления матриц, таблиц, изображений и других структур, где важна локальность данных и быстрый доступ по индексам.

Примеры использования

Пример 1 — Подсчет суммы элементов

public class ArraySum {
public static void main(String[] args) {
int[] numbers = {1, 2, 3, 4, 5};
int sum = 0;

for (int i = 0; i < numbers.length; i++) {
sum += numbers[i];
}

System.out.println("Сумма: " + sum); // Выведет: Сумма: 15
}
}

Пример 2 — Поиск максимального элемента

Код ITЗагрузка примера кода…


Пример 3 — Работа с двумерным массивом (матрицей)

Код ITЗагрузка примера кода…

Разбор:

  • Двумерный массив matrix задаёт табличные данные, где первый индекс — номер строки, второй — номер столбца.
  • Внешний цикл for (int i = 0; i < matrix.length; i++) проходит по строкам, а внутренний — по элементам конкретной строки.
  • Конструкция matrix[i].length делает код устойчивым даже к неравномерным строкам, где ширина может отличаться.
  • System.out.print(...) и System.out.println() формируют человекочитаемый вывод матрицы построчно без промежуточных структур.

Сценарии, где массивы действительно лучше

  • Буферы ввода-вывода (byte[]) в сетевом и файловом коде.
  • Численные вычисления, где важна плотность хранения.
  • Преобразование данных перед сериализацией/десериализацией.
  • Подготовка данных для библиотек, ожидающих массив на вход.

Мини-памятка по выбору

Если нужноИспользуйте
Фиксированный набор значенийT[]
Динамический рост и удалениеArrayList<T>
Уникальность элементовSet<T>
Пара "ключ-значение"Map<K, V>

Связанные статьи