Руководство по массивам в Java (ч. 1)

Введение
Для хранения данных, используемых при работе программы, применяются переменные того или иного типа. Например, если требуется сохранить имя игрока, то создается переменная String name, если целочисленное значение, то int number и т. д. Когда таких значений немного или их количество заранее известно, то для них приемлемо использовать отдельные переменные.
Но что делать, когда в программе задействовано не одно, а 10 чисел, или их количество становится известно только на этапе запуска приложения, когда пользователь вводит его с клавиатуры? Или в какой-то игре может участвовать разное количество игроков, устанавливаемое при ее старте. В таких ситуациях отдельными переменными уже не обойтись, т. к. их точное количество будет невозможно определить. Да и, если все же попытаться это сделать, код станет громоздким и не универсальным, а работа с ним будет крайне ограниченной из-за невозможности использования цикла для автоматизированной обработки данных.
Вот тут-то и приходит на выручку такая структура данных как массив, которая позволяет хранить в себе значения одного типа, что значительно упрощает их обработку.
Дадим определение массиву.
Массив — это структура данных фиксированного размера, являющаяся объектом и состоящая из ячеек, расположенных последовательно в памяти, которые могут хранить в себе значения только одного, заранее заданного типа. При этом каждая ячейка обладает адресом (порядковым номером, индексом), позволяющим получать к ней доступ.
На картинке ниже схематично изображен массив размерностью 10, заполненный числами, а также индексы ячеек:
В повседневной жизни мы постоянно сталкиваемся с массивами, например, пчелиные соты, контейнер для куриных яиц, коробка с конфетами, формочки для заморозки льда и т. д. Все эти хранилища тоже содержат ячейки, где размещается тот или иной продукт одного типа (яйца не могут храниться в сотах, как и лед вместо конфет — для каждого продукта используется свой контейнер).
Далее мы подробно и обстоятельно рассмотрим массив и его устройство.
1. Объявление переменной массива
Для того, чтобы можно было работать с массивом, необходимо иметь к нему доступ. Для этого требуется объявить (создать, декларировать) переменную, с помощью которой, как через пульт, мы могли бы обращаться к конкретным ячейкам массива, указывая номер нужного «канала» (индекса).
Пример объявления таких переменных:
double[] numbers;
char[] letters;
int[] ids;
Такие переменные в Java объявляются точно так же, как любые другие (у них есть тип данных и имя). Отличие лишь в наличии квадратных скобок [], размещаемых после типа хранимых в массиве данных (при этом перед [] пробел не ставится). Скобки являются специальным маркером, указывающим на то, что переменная предназначена для массива.
Существуют и другие варианты размещения [], но указанный выше способ является наиболее предпочтительным, и именно он активно используется Java-разработчиками. Кроме того, скобки являются частью типа массива и должны стоять рядом с обозначением типа хранимых им значений.
Все возможные варианты оформления:
int[] arr; — общепринятый Java-стиль
int arr[];
int [] arr;
int arr [];
По историческим причинам (наследство от языка С/С++) Java позволяет размещать [] в разных местах, но это не значит, что нужно использовать какой-то иной стиль, кроме общепринятого (или используемого в конкретной компании), даже если вы увидите подобный код у кого-то в интернете или книге. Всегда придерживайтесь одного стиля, не смешивайте разные способы написания в одной программе.
Т. к. массив всегда хранит множество значений, то для именования переменной используется:
  • существительное во множественном числе, например, cats, cars, resumes, playerAttempts
  • местоимение, например, allNumbers, myBooks
  • прилагательное, например, physicalConstants
Еще парочка примеров:
String[] fullNames;
Player[] players;
short[] nums;
Массив может хранить не только значения примитивных, но и ссылочных типов. И сам он тоже является ссылочным типом данных. В первой строке объявляется переменная массива строк, во второй — массива игроков, а в третьей — переменная для хранения примитивных целых значений.
Забегая вперед, необходимо отметить, что переменные массивов хранят не сам массив, а ссылку на него, которая, в свою очередь, указывает на начало области памяти, содержащую его значения.
2. Создание массива
Чтобы создать массив, необходимо использовать ключевое слово new с указанием его размерности и типа:
int[] array = new int[10];
Такого рода запись следует читать справа налево следующим образом:
  • Оператор new выделяет в памяти (heap, куче) место под хранение десяти значений типа int
  • Затем он возвращает ссылку (будет храниться в стеке) на начало области памяти (совпадает с первым элементом), выделенной под массив
  • Ссылка помещается в переменную array того же типа, что и массив
На картинке схематично в памяти изображен массив int размерностью 10, заполненный числами:
Следует помнить, что после создания массива его размер (длину) изменить нельзя.
Еще пример, но уже не с примитивами, а со ссылочным типом String:
String[] strings = new String[4];

это то же самое, что и 
String[] strings;
strings = new String[4];
Первый вариант предпочтительней, т. к. не стоит без явной причины объявлять переменную и создавать массив на разных строках — это и больше кода, и подобная запись всегда вызовет у опытного программиста недоумение: «А нафига так было делать, если можно в одну строку?».
3. Тип данных массива
Как написано в документации, массив является объектом: «An object is a class instance or an array». Но наблюдательный читатель, возможно, заметил, что хоть он и является таковым, но при этом не имеет класса, который описывал бы его. Мы просто используем массив, как есть. Но это «как есть» хранит в себе множество скрытых процессов и тайн, которые мы по возможности приоткроем.
Хоть тип массива и не является классом, например int[], но все же имеет ассоциируемый с ним класс, который автоматически создает виртуальная машина (Java Virtual Machine, JVM). При этом данный класс неявно наследуется от java.lang.Object, что делает доступным все его методы (кроме clone) и интерфейсы Cloneable и Serializable. Но у программиста нет доступа к его коду, его нельзя посмотреть глазами.
Ассоциируемые классы создаются для каждого типа хранимой в массиве информации, например, для boolean[], или Player[], или String и т. д. И мы можем узнать, как эти классы называются. Они-то и являются реальными типами массивов. Чтобы увидеть их названия, необходимо воспользоваться Java-магией (рефлексией), которая позволит узнать настоящий тип массива, его суперкласс (от которого он наследуется) и список доступных методов.
Создадим массив нулевой длины (пустой) и отобразим нужную нам информацию (не забудьте сделать импорт java.lang.reflect.Method):
int[] arr = {};
System.out.println("Тип массива - " + arr.getClass());
System.out.println("Суперкласс типа массива - " + arr.getClass().getSuperclass());

System.out.println("Доступные методы:");
for (Method m : arr.getClass().getMethods()) {
    System.out.println(m.getName());
}
Вывод:
Что нам вывел код:
  • [I — это сигнатура типа (класса, который JVM создает во время выполнения) для объекта «массив с элементами типа int». Это и есть настоящий тип данных массива, как объекта
  • [ говорит, что это одномерный массив
  • I, что он содержит целые числа
  • Имя суперкласса, записанного с помощью его полного имени — java.lang.Object
Все возможные сигнатуры типов имеют следующий формат:
[B - byte
[C - char
[D - double
[F - float
[I - int
[J - long
[S - short
[Z - boolean
[L - любой объект
Можно сделать вывод, что тип массива и тип хранимых им значений — это формально разные типы. В нашем примере типом массива является тип [I, а типом хранимых значений — int.
Разберем еще одно доказательство того, что Object является суперклассом для массивов:
Object obj = new int[]{3, 6, 9};
int[] arr = (int[]) obj;
System.out.println(obj);
System.out.println(arr);
В первой строке объявляется и инициализируется массив, ссылка на который присваивается переменной типа Object. И при этом никакой ошибки не возникнет. Во второй строке происходит приведение Object к int[]. В итоге обе переменные (их ссылки) указывают на один и тот же массив.
Написанный выше код отобразит следующий результат:
[I@2f490758
[I@2f490758
Как известно, если попытаться вывести, например, в println значение объекта, просто записав его имя в метод в качестве аргумента, то неявно будет вызван метод toString(). А так как массивы его не переопределяют, т. е. у них нет своей реализации этого метода, то используется реализация по умолчанию, записанная в классе Object.
Как написано в документации, данный метод возвращает строку, получаемую с помощью следующего кода:
getClass().getName() + '@' + Integer.toHexString(hashCode())
В итоге она состоит из имени класса, в нашем случае — [I, разделителя в виде @ и шестнадцатеричного представления хеш-кода объекта. Для человека эта информация не несет никакой полезной нагрузки. Решение этой проблемы обсуждается во второй части статьи.
Нам же сейчас важно, что обе переменные указывают на один и тот же массив, т. е. хранят одну и ту же ссылку на него. Это говорит также о том, что на один массив может указывать любое количество переменных.
И на закуску немного сумасбродства и неочевидных вещей — создадим пустой массив с нулевой длиной:
String[] array1 = {};
char[] array2 = new char[0];
Как вы думаете, как в памяти представляются эти массивы?
Массив с нулевым размером представляет собой объект, у которого есть заголовок объекта (Object header) и размер, но нет места в памяти, выделенного для его элементов. При этом ссылка на такой массив является рабочей и сохраняется в переменную.
Массив нулевой длины не равен null:
short[] array3 = null;
null — это указатель в никуда, который является маркером отсутствия указателя на объект.
4. Длина массива
В спецификации написано: «Поле length является public final (общедоступным и неизменяемым) и содержит в себе количество элементов массива (длина может быть положительной или нулевой)».
Это поле вычисляется один раз при создании массива, т. к. его размер никогда не меняется.
Вызов этого поля часто путают с вызовом метода length() получения длины String. Но length — не метод, а поле. Доступ к нему можно получить напрямую, поскольку оно является общедоступным и неизменяемым (хотя тут, возможно, что это просто плохое архитектурное решение из далекого прошлого).
Т. к. данное поле является частью объекта и не меняется, то это позволяет JVM не пересчитывать количество элементов массива всякий раз, когда в коде используется его длина — она просто берет его из заголовка объекта, когда это требуется.
Чтобы вас окончательно запутать, сошлюсь на документацию, в которой написано: «Длина массива не является частью его типа». Значит length не хранится в ассоциируемом классе!
«Да, это какой-то вынос мозга», — скажете вы: класса нет, но массив — это объект. При этом он наследуется от Object. А length — вроде бы, поле, но не совсем.
А где тогда хранится длина массива?
Она размещается в заголовке объекта. Что такое заголовок? Это часть любого объекта, содержащая метаинформацию о нем. Давайте взглянем на этот заголовок, используя утилиту Java Object Layout (JOL).
Чтобы программа ниже у вас заработала, необходимо в IDEA нажать Ctrl + Alt + Shift + S, выбрать Modules, а затем в Dependencies нажать + → Library… → From Maven… В строке поиска ввести org.openjdk.jol.
import org.openjdk.jol.info.ClassLayout;

public class JOLArray {

    public static void main(String[] args) throws Exception {
        System.out.println(ClassLayout.parseInstance(new int[10]).toPrintable());
    }
}
В итоге на консоль будет выведена следующая метаинформация из заголовка объекта:
Из всего вывода нас интересует строка: 12 4 (array length) 10.
Это и есть длина созданного массива new int[10], которая имеет размер 4 байта. Поле length является особенным в том смысле, что оно находится в заголовке объекта массива, а для получения его длины используется специальный байт-код.
Больше о заголовке объекта можно узнать в видео Алексея Шипилева.
Так же следует заметить, что в наборе инструкций JVM все объекты создаются с использованием одних и тех же байт-кодов, за исключением массивов — они обрабатываются специальными командами.
Рассмотрим код:
public class ByteCodeArray {

    public static void main(String[] args) {
        int[] nums = new int[5];
        int len = nums.length;
    }
}
Отобразим байт-код этого класса с помощью javap:
javap -c ByteCodeArray.class

Code:
       0: iconst_5
       1: newarray       int
       3: astore_1
       4: aload_1
       5: arraylength
       6: istore_2
       7: return
Нас в нем интересуют две строки: newarray int, которая создает массив int-ов и arraylength, которая получает длину массива. Таким образом, доступ к нему не осуществляется, как если бы это было обычное поле. Этого поля просто не существует в типе массива. Для работы с ним у JVM есть отдельная инструкция, которая берет его из заголовка объекта.
5. Инициализация массива
Создавать и инициализировать массив значениями можно разными способами. Выбор того или иного решения зависит от различных факторов, которые, в том числе, будут упомянуты ниже.
5.1. Сокращенная форма создания и инициализации
Если вам заранее известно, какие значения должен содержать массив, то их можно указать через запятую, используя быструю инициализацию.
Например:
String[] names = new String[]{"Max", "Grigoriy", "Java"};

// та же запись, но короче
String[] names = {"Max", "Grigoriy", "Java"};
Длина массива, объявленного таким образом, зависит от количества перечисленных элементов. Во втором варианте тип массива определяется автоматически, исходя из хранимых данных.
Исходя из примеров выше следует отметить, что в Java в массивах хранятся не сами объекты, а ссылки на них, которые, в свою очередь, указывают на место в куче (heap), где находится соответствующий объект.
Схематично это можно изобразить так:
Еще пример:
float[] partsExpression = {1.83, 1.67, 1.75, 1.50};
Можно даже так:
Resume[] resumes = {new Resume("uuid1"), new Resume("uuid2")};
Как видите, это удобный и компактный способ, применяемый, когда значений не так много, и они заранее известны. Его используют при тестировании программ, в алгоритмических задачах, небольших программах и т. д.
5.2. Ручная инициализация по индексу
Элементы массива начинают храниться с нулевого индекса: самая первая ячейка находится под индексом 0, а не 1. Это может показаться непривычным, но на то есть причина.
На самом деле, индекс в Java является ничем иным, как смещением памяти относительно начала массива (расстоянием от его начала до любого элемента). А т. к. начальный адрес совпадает с адресом первого элемента, то смещение будет равно 0, а значит и индекс тоже.
Адрес любой ячейки массива рассчитывается по следующей формуле:
адрес начала массива + индекс * количество памяти, выделенное на один элемент
Например, есть массив int[] nums = {3, 2, 1};
Предположим, что nums хранит адрес 0xFF00. Тогда ячейки массива будут иметь следующие адреса (с учетом того, что на каждое значение int выделяется 4 байта):
nums[0] → 0xFF00;
nums[1] → 0xFF04;
nums[2] → 0xFF08;
Более научное объяснение того, почему индексация начинается с 0, а не с 1, можно прочитать в следующей статье.
С индексацией разобрались. Скажем теперь пару слов про то, чему равно количество индексов в массиве. Оно всегда равно length — 1. Это связано с тем, что размер массива (его длина) всегда больше на единицу самого последнего индекса из-за того, что индексация начинается 0.
Количественно размер массива и число индексов равно, но в числовом представлении оно отличается на 1 в меньшую сторону, если считать индексы по порядку. Таким образом, в приведенном ниже примере с массивом из 2 элементов индексы идут от 0 до 1, а длина массива равна 2.
Благодаря индексу можно получать доступ к любому элементу, указав его в квадратных скобках, размещенных после имени массива.
Пример:
int[] nums = new int[2];
nums[0] = 3;
nums[1] = 7;
int num = nums[0];
int num1 = nums[1];
System.out.println(num);
System.out.println(num1);

// или можно сразу вывести значение без создания переменной
System.out.println("Под индексом 0 хранится значение " + nums[0]);
System.out.println("Под индексом 1 хранится значение " + nums[1]);
Вывод:
В этом примере в первом случае сначала в ячейки массива под индексами 0 и 1 сохраняются числа 3 и 7. Затем они же считываются из него и размещаются в переменные num и num1. Далее их значения выводятся на консоль.
Второй вариант отличается только тем, что значения берутся из массива и сразу выводятся без создания промежуточных переменных. Этот способ предпочтительнее, когда переменные далее нигде не используются.
В консоли в обоих случаях отобразятся числа 3 и 7, взятые из массива nums по индексам 0 и 1.
Подобный способ инициализации, по сути, является более многословной формой ее сокращенного варианта, если массив целиком инициализируется сразу после создания, но при этом несет в себе большую гибкость в случае, когда инициализация проходит в разных местах программы, исходя из каких-то условий задачи.
Данный способ все же не совсем подходит для автоматической инициализации массива. Но для понимания того, как обращаться к значениям массива по индексу, он очень показателен.
Приведем для примера «странный» код:
System.out.println((new int[]{3, 6, 9})[1]);
В итоге в консоли отобразится число 6, находящееся под индексом 1.
Данный код позволил «подключиться» к массиву без использования переменной. Подобный способ может показаться не совсем очевидным, но, как видим, он работает. Пользы от него немного, но как демонстрация того, что к массиву можно обращаться и без переменной, а сразу через «голую» ссылку, вполне подходит. В реальном проекте вы, скорее всего, не будете использовать подобные выкрутасы. Они нужны лишь для понимания работы тонкостей массива.
5.3. Инициализация и доступ к элементам в цикле
Согласитесь, что инициализировать массив вручную, указывая явно каждый его индекс и значение — громоздко и не всегда оправдано. Как правило, массивы инициализируют и отображают их значения автоматически с помощью цикла — это и универсальнее и короче по строкам кода.
При использовании цикла необходимо знать следующие параметры:
  • начальное значение индекса (откуда стартует итерация)
  • значение, ограничивающее количество итераций (обычно используется длина массива или 0 — если итерация стартует с конца массива)
  • конечное значение индекса (для завершения числа итераций)
Для работы с массивами с учетом вышеописанных требований удобнее всего использовать цикл for. Как мы знаем, он состоит из трех секций: инициализация счетчика (начальное значение индекса), условие выполнения (число итераций) и изменение счетчика (для достижения конечного значения индекса).
В качестве типа данных индекса массива можно использовать int, short, byte или char. Использование типа long приведет к ошибке компиляции, т. к. индексы массива в Java преобразуются в int-значения (4 байта). А т. к. под long выделяется больше памяти (8 байт), то преобразование к int потенциально может привести к потерям данных, о чем и сообщит компилятор.
Приведем пример такого кода:
int[] ints = new int[5];
for (long lng = 0L; lng < ints.length; lng++) {
    System.out.println(ints[lng]);
}
Вывод:
Проблема решается принудительным приведением типов. Тут компилятор ничего не скажет, т. к. всю ответственность за эту операцию вы берете на себя:
System.out.println((int) ints[lng]);
Закончив это небольшое отступление, продолжим разбираться с циклом for.
Начальное значение индекса — это значение, с которого будет начинаться доступ к ячейкам массива. Обычно начинают с самого начала, т. е. с 0 (с первой ячейки). Но не обязательно. Есть ряд задач, в которых удобнее начинать с конца (длина массива — 1).
Значение, ограничивающее количество итераций, позволяет не выйти за правую границу массива, когда индекс >= длине массива. Если цикл реализован так, что проход по массиву осуществляется с конца к началу, то тут все наоборот: длина — 1 будет началом, а условие index >= 0 будет защищать от выхода за его левую границу.
Цикл прерывается, когда конечное значение индекса совпадает с его длиной или оно < 0.
Пример работы циклов по массиву слева направо:
String[] strings = new String[4];
 
for(int i = 0; i < strings.length; i++) {
    strings[i] = "Строка по индексу " + i;
}
 
for(int i = 0; i < strings.length; i++) {
    System.out.println(strings[i]);
}
Вывод:
В этом примере создается массив строк размерностью 4.
В первом цикле в массив сохраняется сконкатенированный (склеенный) текст с номерами индексов.
Второй цикл выводит на консоль значения каждой ячейки. Число в квадратных скобках — это конкретная позиция массива, к которой мы хотим получить доступ.
В обоих циклах переменная i используется в качестве счетчика для обозначения индекса. Она имеет начальное значение 0, которое в процессе работы циклов меняется на единицу (инкрементируется) после каждой итерации (одного прохода цикла). Как только условие i < strings.length перестает выполняться, т. е. проверка вернет false, то цикл прекращается.
Вы можете спросить, почему я использовал идентификатор «i»? Разве это не «плохое» имя для переменной? Да, обычно для имен переменных выбираются имена, глядя на которые сразу бывает понятно, что они хранят. Но в данном случае действует негласное соглашение, о котором можно прочитать тут.
Если в двух словах, то счетчики циклов принято именовать с помощью i, j и k, где i — находится во внешнем цикле, j — во вложенном в него цикле и т. д.
Считается, что этот стиль возник в языке программирования Фортран, где имена переменных, начинающиеся с букв I по Q, по умолчанию считались целыми, остальные были действительными. А еще раньше это появилось в математике, где индексы для сумм и умножений именуются как i, j, k.
Приведем еще один пример работы с массивом в цикле, но уже в обратном порядке:
char[] chars = new char[32];
int i = 0; 
for(char ch = 'Я'; ch >= 'А'; ch--) {
    chars[i++] = ch;
}

for(i = chars.length - 1; i >= 0; i--) {
    System.out.print(chars[i]);
}
В первом цикле массив chars заполняется буквами от А до Я. Он работает до тех пор, пока выполняется условие ch >= 'А'. Во втором цикле все буквы выводятся на консоль.
Что будет, если вывести массив без инициализации?
Проверим эту идею, создав массивы размерностью 1 всех примитивных типов и String. Отобразим их значения в консоль без инициализации:
System.out.println("Значение по умолчанию для byte " + (new byte[1])[0]);
System.out.println("Значение по умолчанию для short " + (new short[1])[0]);
System.out.println("Значение по умолчанию для int " + (new int[1])[0]);
System.out.println("Значение по умолчанию для long " + (new long[1])[0]);
System.out.println("Значение по умолчанию для float " + (new float[1])[0]);
System.out.println("Значение по умолчанию для double " + (new double[1])[0]);
System.out.println("Значение по умолчанию для char " + (new char[1])[0]);
System.out.println("Значение по умолчанию для boolean " + (new boolean[1])[0]);

System.out.println("Значение по умолчанию для String " + (new String[1])[0]);
Вывод:
Из всего увиденного можно сделать вывод, что после создания массива каждая его ячейка инициализируется значением по умолчанию, исходя из типа хранимых данных.
Значение переменной char не отображается (не имеет никакого представления), т. к. соответствует коду '\u0000', являющимся пустым символом.
Известно, что локальные переменные по умолчанию не инициализируются, но с массивом, как мы видим, все иначе. Спецификация (1, 2) языка требует его инициализации значениями по умолчанию, что фактически мы и наблюдали в примере.
Что будет, если выйти за границы массива?
За границу массива мы можем выйти с двух сторон: слева (когда индекс отрицательный) и справа (когда он >= длине массива). В обоих случаях произойдет ошибка (исключительная ситуация) и вылетит исключение java.lang.ArrayIndexOutOfBoundsException.
Этот тип исключения всегда свидетельствует об ошибке в коде, совершенной программистом. С ней можно сделать только одно — исправить код так, чтобы она не возникала, в принципе.
Например:
int[] nums = new int[4];
 
for(int i = 0; i <= nums.length; i++) {
    nums[i] = i + 1;
}
Вывод:
Из-за того, что условие i <= nums.length при i = 4 вернуло true, в строке nums[i] = i + 1; возникла ошибка связанная с тем, что в массиве нет ячейки под индексом 4. Ситуации nums[4] = 4 + 1 быть не должно.
Еще пример:
String[] str = {"A", "B", "C"};

for(int i = str.length - 1; i >= 0; i--) {
    System.out.println(str[i - 1]);
}
Вывод:
В коде ошибка произошла из-за того, что в какой-то момент в строке str[i — 1] индекс стал равен str[0 — 1]. Информация в консоли сообщает, что индекс не может быть -1, т. к. он вне пределов длины 3.
6. Цикл for-each
Для работы с массивами цикл for является более предпочтительным, чем while, но и он не идеален, т. к. имеет массу мест, где можно совершить ошибку: не так инициализировать счетчик, неверно написать условие работы цикла, перепутать название счетчика и т. д.
Чтобы как-то минимизировать количество ошибок, упростить работу с массивами и коллекциями (далее будут упоминаться только массивы) и сделать код более лаконичным, начиная с JDK 5, была внедрена упрощенная версия цикла for под названием for-each.
Код, записанный в традиционном стиле:
int[] nums = new int[10];

for (int i = 0; i < nums.length; i++) {
    System.out.print(nums[i]);
}
Тот же самый код, но с использованием for-each:
int[] nums = new int[10];

for (int num : nums) {
    System.out.print(num);
}
Цикл for-each состоит из двух секций, отделенных друг от друга двоеточием (при этом перед и после него обычно ставят пробел): в правой части размещается имя массива, а в левой создается переменная того же типа, что и элементы массива. Элементы по порядку берутся из массива и помещаются в эту переменную, значение из которой в каждой итерации выводится на консоль.
Данный вид цикла является так называемым «синтаксическим сахаром», который отличается от традиционного цикла for только внешне, но для JVM в них нет никаких различий.
Отличие for-each от for:
  • Отсутствие счетчика индекса — все ячейки перебираются по порядку, начиная с нулевой
  • Отсутствие условия работы цикла — это действие выполняется автоматически и скрыто от программиста
Цикл for-each позволяет получить доступ к элементам массива, но не дает возможности использовать индексы (к ним банально нет доступа) или обращаться к значениям в отличном от последовательного доступа порядке. Кроме того, он не позволяет менять или удалять значения массива. В основном, его используют для вывода элементов массива на консоль. Если вам нужно больше возможностей, то используйте обычный цикл for.
Цикл for-each используется, когда:
  • необходимо вывести значения массива по порядку
  • не нужно изменять массив
  • не нужны индексы
Еще пример с for-each:
char[] chars = {'q', 'w'};
 
for(char symbol : chars) {
    System.out.println(symbol);
}

Вывод:
q
w
Цикл for-each для работы с массивами предоставляет массу преимуществ и удобств, нежели for и, тем более, while и do-while. Старайтесь его использовать везде, где это возможно.
7. Удаление элементов из массива
Помимо сохранения данных в массив, очень часто приходится их удалять.
Под удалением не имеется в виду, что массив станет меньше из-за того, что ячейка со значением исчезнет или есть какая-то специальная команда, которая выполняет эту операцию. Удаление в массиве — это всего лишь перезапись ячейки любым значением, отличным от удаляемого.
Разберем пример:
float[] nums = {1.5f, 0.22f, 1.0f};

System.out.println(nums[1]);
nums[1] = 3.31f;
System.out.println(nums[1]);

Вывод:
0.22
3.31
В этом примере создается массив из float. Затем выводится начальное значение по индексу 1. Далее по этому индексу устанавливается новое значение, которое фактически удаляет (перезаписывает) старое. И новое значение отображается в консоли.
Рассмотрим другой пример. Пусть имеется массив целых чисел размерностью 5. Необходимо сдвинуть все его значения на одну позицию вправо, а затем отобразить результат.
Для решения этой задачи запишем следующий код:
int[] nums = {0, 1, 2, 3, 4};

for(int i = nums.length - 1; i > 0; i--) {
    nums[i] = nums[i - 1];
}

for(int num : nums) {
    System.out.print(num);
}
В консоли отобразятся следующие значения: 123. Число 4 было удалено значением 3, которое в цикле переместилось на его место. В этом примере все значения цикла поменяли свои позиции — сдвинулись вправо на одну ячейку.
8. Скорость работы массива
Чтобы понять, насколько массив является быстрой (но не всегда) структурой данных, необходимо определиться, хотя бы вкратце, с теоретической частью вычисления этой скорости. Для этого необходимо сказать пару слов про сложность алгоритмов (Big O Notation).
Big O Notation можно перевести как О большое. Это способ (из теории алгоритмов) обозначения сложности алгоритма.
Любой алгоритм характеризуется двумя параметрами: временем выполнения (Time Complexity) и расходом памяти (Space Complexity). Например, чем длиннее массив, тем больше нужно памяти и времени на его заполнение.
При этом время считают не в секундах, а в количестве совершаемых алгоритмом операций, что позволяет устранить зависимость скорости работы алгоритма от железа.
Например, Time Complexity для поиска числа в массиве будет обозначаться, как O(n), где n — это число ячеек в массиве. Чем их больше, тем дольше работает алгоритм (выполняется большее количество операций (итераций массива), где n — их максимальное число). Т. к. поиск происходит линейно, то ячейки перебираются одна за другой по порядку. Соответственно, такой алгоритм называется линейным.
Элементы массива в памяти размещаются в едином блоке. Это сделано для более эффективного и быстрого доступа к ним.
Если индекс ячейки известен, то алгоритм имеет константное время (постоянное, не зависящее ни от чего) и обозначается O(1), т. е. доступ к ячейкам массива происходит мгновенно и не зависит от его размера.
Если массив отсортирован по возрастанию и для поиска значений используется алгоритм под названием двоичный поиск (1, 2), то количество операций, необходимых алгоритму для поиска числа будет O(log(n)).
Не беда, если вы ничего не поняли из этой главы — она больше является обзорной. Более подробно об этой теме вы можете узнать в данной статье и в книге Грокаем алгоритмы.
А дорого ли вычислять каждый раз длину массива?
Как мы выяснили, длина массива — это фиксированное значение, которое создается в момент создания массива и хранится в заголовке массива. JVM знает, что это значение никогда не изменится, а, значит, оно никогда не вычисляется, а просто берется из заголовка. Этот тип операции чтения очень быстрый и выполняется за O(1), т. е. за константное время.
Автор: Чимаев Максим
Оцените статью, если она вам понравилась!