Как изменить длину массива java

В этом посте вы узнаете, как изменить размер массива в Java, сохранив при этом его текущие элементы.
  1. Изменить размер массива в Java
  2. Измените размер массива с помощью метода arraycopy() в Java
  3. Изменение размера массива с помощью метода copyOf() в Java
  4. Изменение размера массива с помощью цикла for в Java

Изменение размера массива с сохранением текущих элементов в Java

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

Массив определяется как контейнер, используемый для хранения подобных типов элементов в Java. Это контейнер фиксированного размера, что означает, что если массив имеет размер 10, он может хранить только 10 элементов — это одно из ограничений массива.

В этой статье мы научимся изменять размер массива с помощью некоторых встроенных методов, таких как функции arraycopy() и copyOf(), а также некоторых пользовательских кодов.

Изменить размер массива в Java

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

import java.util.ArrayList;
import java.util.List;
public class SimpleTesting{
    public static void main(String[] args) {
        int arr[] = new int[]{12,34,21,33,22,55};
        for(int a: arr) {
            System.out.print(a+" ");
        }
        List<Integer> list = new ArrayList<>();
        for(int a: arr) {
            list.add(a);
        }
        System.out.println("n"+list);
        list.add(100);
        System.out.println(list);
    }
}

Выход:

12 34 21 33 22 55 
[12, 34, 21, 33, 22, 55]
[12, 34, 21, 33, 22, 55, 100]

Измените размер массива с помощью метода arraycopy() в Java

Java предоставляет метод arraycopy(), который принадлежит классу System и может использоваться для создания копии массива. В этом примере мы создаем новый массив большего размера, а затем копируем в него все исходные элементы массива с помощью метода arraycopy(). Следуйте приведенному ниже примеру программы:

public class SimpleTesting{
    public static void main(String[] args) {
        int arr[] = new int[]{12,34,21,33,22,55};
        for(int a: arr) {
            System.out.print(a+" ");
        }
        int arr2[] = new int[10];
        System.arraycopy(arr, 0, arr2, 0, arr.length);
        System.out.println();
        for(int a: arr2) {
            System.out.print(a+" ");
        }
        System.out.println();
        arr2[6] = 100;
        for(int a: arr2) {
            System.out.print(a+" ");
        }
    }
}

Выход:

12 34 21 33 22 55
12 34 21 33 22 55 0 0 0 0
12 34 21 33 22 55 100 0 0 0

Изменение размера массива с помощью метода copyOf() в Java

Класс Java Arrays предоставляет метод copyOf(), который можно использовать для создания массива нового размера путем копирования всех исходных элементов массива. Этот процесс принимает два аргумента: первый — это исходный массив, а второй — размер нового массива. См. Пример ниже:

import java.util.Arrays;
public class SimpleTesting{
    public static void main(String[] args) {
        int arr[] = new int[]{12,34,21,33,22,55};
        for(int a: arr) {
            System.out.print(a+" ");
        }
        int arr2[] = Arrays.copyOf(arr, 10);
        System.out.println();
        for(int a: arr2) {
            System.out.print(a+" ");
        }
        System.out.println();
        arr2[6] = 100;
        for(int a: arr2) {
            System.out.print(a+" ");
        }
    }
}

Выход:

12 34 21 33 22 55 
12 34 21 33 22 55 0 0 0 0 
12 34 21 33 22 55 100 0 0 0 

Изменение размера массива с помощью цикла for в Java

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

public class SimpleTesting{
    public static void main(String[] args) {
        int arr[] = new int[]{12,34,21,33,22,55};
        for(int a: arr) {
            System.out.print(a+" ");
        }
        int arr2[] = new int[10];
        for (int i = 0; i < arr.length; i++) {
            arr2[i] = arr[i];
        }
        System.out.println();
        for(int a: arr2) {
            System.out.print(a+" ");
        }
    }
}

Выход:

12 34 21 33 22 55 
12 34 21 33 22 55 0 0 0 0

Here are a couple of ways to do it.


Method 1: System.arraycopy():

Copies an array from the specified source array, beginning at the specified position, to the specified position of the destination array. A subsequence of array components are copied from the source array referenced by src to the destination array referenced by dest. The number of components copied is equal to the length argument. The components at positions srcPos through srcPos+length-1 in the source array are copied into positions destPos through destPos+length-1, respectively, of the destination array.

Object[] originalArray = new Object[5];   
Object[] largerArray = new Object[10];
System.arraycopy(originalArray, 0, largerArray, 0, originalArray.length);

Method 2: Arrays.copyOf():

Copies the specified array, truncating or padding with nulls (if necessary) so the copy has the specified length. For all indices that are valid in both the original array and the copy, the two arrays will contain identical values. For any indices that are valid in the copy but not the original, the copy will contain null. Such indices will exist if and only if the specified length is greater than that of the original array. The resulting array is of exactly the same class as the original array.

Object[] originalArray = new Object[5];   
Object[] largerArray = Arrays.copyOf(originalArray, 10);

Note that this method usually uses System.arraycopy() behind the scenes.


Method 3: ArrayList:

Resizable-array implementation of the List interface. Implements all optional list operations, and permits all elements, including null. In addition to implementing the List interface, this class provides methods to manipulate the size of the array that is used internally to store the list. (This class is roughly equivalent to Vector, except that it is unsynchronized.)

ArrayList functions similarly to an array, except it automatically expands when you add more elements than it can contain. It’s backed by an array, and uses Arrays.copyOf.

ArrayList<Object> list = new ArrayList<>();

// This will add the element, resizing the ArrayList if necessary.
list.add(new Object());

Here are a couple of ways to do it.


Method 1: System.arraycopy():

Copies an array from the specified source array, beginning at the specified position, to the specified position of the destination array. A subsequence of array components are copied from the source array referenced by src to the destination array referenced by dest. The number of components copied is equal to the length argument. The components at positions srcPos through srcPos+length-1 in the source array are copied into positions destPos through destPos+length-1, respectively, of the destination array.

Object[] originalArray = new Object[5];   
Object[] largerArray = new Object[10];
System.arraycopy(originalArray, 0, largerArray, 0, originalArray.length);

Method 2: Arrays.copyOf():

Copies the specified array, truncating or padding with nulls (if necessary) so the copy has the specified length. For all indices that are valid in both the original array and the copy, the two arrays will contain identical values. For any indices that are valid in the copy but not the original, the copy will contain null. Such indices will exist if and only if the specified length is greater than that of the original array. The resulting array is of exactly the same class as the original array.

Object[] originalArray = new Object[5];   
Object[] largerArray = Arrays.copyOf(originalArray, 10);

Note that this method usually uses System.arraycopy() behind the scenes.


Method 3: ArrayList:

Resizable-array implementation of the List interface. Implements all optional list operations, and permits all elements, including null. In addition to implementing the List interface, this class provides methods to manipulate the size of the array that is used internally to store the list. (This class is roughly equivalent to Vector, except that it is unsynchronized.)

ArrayList functions similarly to an array, except it automatically expands when you add more elements than it can contain. It’s backed by an array, and uses Arrays.copyOf.

ArrayList<Object> list = new ArrayList<>();

// This will add the element, resizing the ArrayList if necessary.
list.add(new Object());

First things first:

  • In Java, once an array is created, it’s length is fixed. Arrays cannot be resized.
  • You can copy the elements of an array to a new array with a different size. The easiest way to do this, is to use one of the Arrays.copyOf() methods.
  • If you need a collection of variable size, you’re probably better off using an ArrayList instead of an array.

That being said, there might be situations where you have no other choice than to change the size of an array that is created somewhere outside of your code.1 The only way to do that is to manipulate the generated bytecode of the code that creates the array.

Proof-of-concept

Below is a small proof-of-concept project that uses Java instrumentation to dynamically change the size of an array2. The sample project is a maven project with the following structure:

.
├─ pom.xml
└─ src
   └─ main
      └─ java
         └─ com
            └─ stackoverflow
               └─ agent
                  ├─ Agent.java
                  └─ test
                     └─ Main.java

Main.java

This file contains the target class of which we’re going to manipulate the bytecode:

package com.stackoverflow.agent.test;

import java.util.Arrays;

public class Main {
    public static void main(String[] args) {
        String[] array = {"Zero"};

        fun(array);

        System.out.println(Arrays.toString(array));
    }

    public static void fun(String[] array) {
        array[1] = "One";
        array[2] = "Two";
        array[3] = "Three";
        array[4] = "Four";
    }
}

In the main method, we create a String array of size 1. In the fun method, 4 additional values are assigned outside of the array’s bounds. Running this code as-is will obviously result in an error.

Agent.java

This file contains the class that will perform the bytecode manipulation:

package com.stackoverflow.agent;

import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.Instrumentation;
import java.security.ProtectionDomain;

public class Agent {
    public static void premain(String args, Instrumentation instrumentation) {
        instrumentation.addTransformer(new ClassFileTransformer() {
            public byte[] transform(ClassLoader l, String name, Class<?> c,
                    ProtectionDomain d, byte[] b) {

                if (name.equals("com/stackoverflow/agent/test/Main")) {
                    byte iconst1 = (byte) 0x04;
                    byte iconst5 = (byte) 0x08;
                    byte anewarray = (byte) 0xbd;

                    for (int i = 0; i <= b.length - 1; i++) {
                        if (b[i] == iconst1 && b[i + 1] == anewarray) {
                            b[i] = iconst5;
                        }
                    }

                    return b;
                }

                return null;
            }
        });
    }
}

On the bytecode level, the creation of the String array in the Main class consists of two commands:

  • iconst_1, which pushes an int constant with value 1 onto the stack (0x04).
  • anewarray, which pops the value of the stack and creates a reference array3 of the same size (0xbd).
    The above code looks for that combination of commands in the Main class, and if found, replaces the const_1 command with a const_5 command (0x08), effectively changing the dimensions of the array to 5.4

pom.xml

The maven POM file is used to build the application JAR and configure the main class and the Java agent class.5

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
                             http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>

  <groupId>com.stackoverflow</groupId>
  <artifactId>agent</artifactId>
  <version>1.0-SNAPSHOT</version>

  <build>
    <finalName>${project.artifactId}</finalName>
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-jar-plugin</artifactId>
        <version>3.1.1</version>
        <configuration>
          <archive>
            <manifestEntries>
              <Main-Class>com.stackoverflow.agent.test.Main</Main-Class>
              <Premain-Class>com.stackoverflow.agent.Agent</Premain-Class>
              <Agent-Class>com.stackoverflow.agent.Agent</Agent-Class>
              <Can-Retransform-Classes>true</Can-Retransform-Classes>
            </manifestEntries>
          </archive>
        </configuration>
      </plugin>
    </plugins>
  </build>
</project>

Build and execute

The sample project can be built using the standard mvn clean package command.

Executing without referencing the agent code will yield the expected error:

$> java -jar target/agent.jar 
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: 1
        at com.stackoverflow.agent.test.Main.fun(Main.java:15)
        at com.stackoverflow.agent.test.Main.main(Main.java:9)

While executing with the agent code will yield:

$> java -javaagent:target/agent.jar -jar target/agent.jar
[Zero, One, Two, Three, Four]

This demonstrates that the size of the array was successfully changed using bytecode manipulation.


1 Such situations came up in questions here and here, the latter of which prompted me to write this answer.
2 Technically, the sample project doesn’t resize the array. It just creates it with a different size than the size specified in code. Actually resizing an existing array while maintaining its reference and copying its elements would be a fair bit more complicated.
3 For a primitive array, the corresponding bytecode operation would be newarray (0xbc) instead.
4 As noted, this is just a proof of concept (and a very hacky one at that). Instead of randomly replacing bytes, a more robust implementation could use a bytecode manipulation library like ASM to insert a pop command followed by an sipush command before any newarray or anewarray command. Some more hints towards that solution can be found in the comments to this answer.
5 In a real-world scenario, the agent code would obviously be in a separate project.

Java_Deep_19.8-5020-2f283e.png

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

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

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

Как происходит объявление массива в Java

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

1-20219-84f278.png

Вне зависимости от способа, dataType — это тип переменных в массиве. Посмотрите внимательно на примеры — в них объявлены 2 массива. Один предназначен для целых чисел типа int, другой — для объектов типа Object.

Можно сказать, что во время объявления массива ему присваивается как имя (ArrayName), так и тип переменных.

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

Чтобы создать массив в Java, нужно зарезервировать место в памяти, для чего используем оператор new:

new typeOfArray [length];

Здесь у нас typeOfArray — тип массива, length — длина массива или число ячеек, выраженное в целых числах (int). Но мы лишь выделили память под массив, не связав его ни с какой переменной, ранее объявленной. Как правило, сначала массив объявляют, потом создают:

int[] myArray; // объявление массива
myArray = new int[10]; // создание массива, выделение памяти на 10 элементов типа int

Итак, объявлен массив из целых чисел с именем myArray. После объявления мы сообщили, что массив состоит из 10 ячеек. Но можно использовать и более сокращённый синтаксис:

int[] myArray = new int[10]; // объявление и выделение памяти за один раз

Что же, мы создали массив с помощью new. После этого в его ячейках будут записаны значения по умолчанию. Например, для численных типов — это нули (0), для boolean — false, а если говорить о ссылочных типах, то null. Это значит, что после выполнения кода

int[] myArray = new int[10];

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

Длина массива length

Длина массива — число элементов, под которое этот массив рассчитан. Длину массива изменить после создания нельзя.

Ещё нюанс: элементы массива в Java нумеруются с нуля. Таким образом, массив на 10 элементов состоит из чисел в диапазоне 0-9.

2-20219-9b55f8.png

Если нужно получить доступ к длине нашего массива, используют переменную length:

int[] myArray = new int[10]; // создали массив, присвоили имя myArray
System.out.println(myArray.length); // вывели в консоль длину массива

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

Инициализация — это заполнение массива конкретными данными, а не данными по умолчанию.

Нижеследующий код позволит создать массив, включающий в себя 4 сезона года. Также мы выполним заполнение массива строками-названиями сезонов:

String[] seasons  = new String[4]; /* выполнили объявление и создание массива из 4 строк, где по умолчанию записано null, ведь строка — ссылочный тип данных*/

seasons[0] = "Winter"; /* в первую ячейку записали строку Winter*/
seasons[1] = "Spring"; /* во вторую ячейку (номер 1) записали строку Spring и т. д.*/
seasons[2] = "Summer"; 
seasons[3] = "Autumn"; 

Так мы записали названия всех сезонов. Но в принципе можно всё сделать проще, совместив инициализацию и объявление:

String[] seasons  = new String[] {"Winter", "Spring", "Summer", "Autumn"};

Или даже так, опустив оператор new:

String[] seasons  = {"Winter", "Spring", "Summer", "Autumn"};

Динамический массив в Java

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

3-20219-286ccf.png

А динамические массивы в Java функционируют несколько иначе:

4-20219-c45fc5.png

Так как для копирования массива используется специальная нативная функция, проблем с «переездом» не возникает.

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

Example

The simple answer is that you cannot do this. Once an array has been created, its size cannot be changed. Instead, an array can only be «resized» by creating a new array with the appropriate size and copying the elements from the existing array to the new one.

String[] listOfCities = new String[3];   // array created with size 3.
listOfCities[0] = "New York";
listOfCities[1] = "London";
listOfCities[2] = "Berlin";

Suppose (for example) that a new element needs to be added to the listOfCities array defined as above. To do this, you will need to:

  1. create a new array with size 4,
  2. copy the existing 3 elements of the old array to the new array at offsets 0, 1 and 2, and
  3. add the new element to the new array at offset 3.

There are various ways to do the above. Prior to Java 6, the most concise way was:

String[] newArray = new String[listOfCities.length + 1];
System.arraycopy(listOfCities, 0, newArray, 0, listOfCities.length);
newArray[listOfCities.length] = "Sydney";

From Java 6 onwards, the Arrays.copyOf and Arrays.copyOfRange methods can do this more simply:

String[] newArray = Arrays.copyOf(listOfCities, listOfCities.length + 1);
newArray[listOfCities.length] = "Sydney";

For other ways to copy an array, refer to the following example. Bear in mind that you need an array copy with a different length to the original when resizing.

  • Copying arrays

A better alternatives to array resizing

There two major drawbacks with resizing an array as described above:

  • It is inefficient. Making an array bigger (or smaller) involves copying many or all of the existing array elements, and allocating a new array object. The larger the array, the more expensive it gets.
  • You need to be able to update any «live» variables that contain references to the old array.

One alternative is to create the array with a large enough size to start with. This is only viable if you can determine that size accurately before allocating the array. If you cannot do that, then the problem of resizing the array arises again.

The other alternative is to use a data structure class provided by the Java SE class library or a third-party library. For example, the Java SE «collections» framework provides a number of implementations of the List, Set and Map APIs with different runtime properties. The ArrayList class is closest to performance characteristics of a plain array (e.g. O(N) lookup, O(1) get and set, O(N) random insertion and deletion) while providing more efficient resizing without the reference update problem.

(The resize efficiency for ArrayList comes from its strategy of doubling the size of the backing array on each resize. For a typical use-case, this means that you only resize occasionally. When you amortize over the lifetime of the list, the resize cost per insert is O(1). It may be possible to use the same strategy when resizing a plain array.)

Для хранения данных, используемых при работе программы, применяются переменные того или иного типа. Например, если требуется сохранить имя игрока, то создается переменная 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;

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

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

Чтобы создать массив, необходимо использовать ключевое слово new с указанием его размерности и типа:


int[] array = new int[10];

Такого рода запись следует читать справа налево следующим образом:

  • Оператор new выделяет в памяти (heap, куче) место под хранение десяти значений типа int
  • Затем он возвращает ссылку (будет храниться в стеке) на начало области памяти (совпадает с первым элементом), выделенной под массив
  • Ссылка помещается в переменную array того же типа, что и массив

На картинке схематично в памяти изображен массив int размерностью 10, заполненный числами:

Следует помнить, что после создания массива его размер (длину) изменить нельзя.

Еще пример, но уже не с примитивами, а со ссылочным типом String:


String[] strings = new String[4];

это то же самое, что и 
String[] strings;
strings = new String[4];

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

Как написано в документации, массив является объектом: «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[]. В итоге обе переменные (их ссылки) указывают на один и тот же массив.

Написанный выше код отобразит следующий результат:

Как известно, если попытаться вывести, например, в println значение объекта, просто записав его имя в метод в качестве аргумента, то неявно будет вызван метод toString(). А так как массивы его не переопределяют, т. е. у них нет своей реализации этого метода, то используется реализация по умолчанию, записанная в классе Object.

Как написано в документации, данный метод возвращает строку, получаемую с помощью следующего кода:


getClass().getName() + '@' + Integer.toHexString(hashCode())

В итоге она состоит из имени класса, в нашем случае — [I, разделителя в виде @ и шестнадцатеричного представления хеш-кода объекта. Для человека эта информация не несет никакой полезной нагрузки. Решение этой проблемы обсуждается во второй части статьи.

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

И на закуску немного сумасбродства и неочевидных вещей — создадим пустой массив с нулевой длиной:


String[] array1 = {};
char[] array2 = new char[0];

Как вы думаете, как в памяти представляются эти массивы?

Массив с нулевым размером представляет собой объект, у которого есть заголовок объекта (Object header) и размер, но нет места в памяти, выделенного для его элементов. При этом ссылка на такой массив является рабочей и сохраняется в переменную.

Массив нулевой длины не равен null:

null — это указатель в никуда, который является маркером отсутствия указателя на объект.

В спецификации написано: «Поле 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.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.

Для работы с массивами цикл 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 используется, когда:

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

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), т. е. за константное время.

Оцените статью, если она вам понравилась!

Понравилась статья? Поделить с друзьями:

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

  • Как изменить длину линии автокад
  • Как изменить длину линии html
  • Как изменить длину ладони
  • Как изменить длину колонтитула
  • Как изменить длину зубов

  • 0 0 голоса
    Рейтинг статьи
    Подписаться
    Уведомить о
    guest

    0 комментариев
    Старые
    Новые Популярные
    Межтекстовые Отзывы
    Посмотреть все комментарии