JavaRush /Java блог /Random UA /Кава-брейк #130. Як правильно працювати з масивами Java -...

Кава-брейк #130. Як правильно працювати з масивами Java - поради від Oracle

Стаття з групи Random UA
Джерело: Oracle Робота з масивами може включати відображення, дженерики і лямбда-вираження. Нещодавно я розмовляв з колегою, який займається розробкою мовою C. Розмова зайшла про масиви і про те, як вони працюють у Java в порівнянні з C. Я знайшов це трохи дивним, враховуючи, що Java вважається С-подібною мовою. Вони дійсно багато схожого, але є й відмінності. Почнемо з простого. Кава-брейк #130.  Як правильно працювати з масивами Java - поради від Oracle - 1

Оголошення масиву

Якщо слідувати підручнику з Java, ви побачите, що є два способу оголосити масив. Перший прямолінійний:
int[] array; // a Java array declaration
Можна помітити, як він відрізняється від C де синтаксис наступний:
int array[]; // a C array declaration
Повертаємось знову до Java. Після оголошення масиву вам потрібно його виділити:
array = new int[10]; // Java array allocation
Чи можна оголосити та ініціалізувати масив одразу? Взагалі то ні:
int[10] array; // NOPE, ERROR!
Однак ви можете відразу оголосити та ініціалізувати масив, якщо ви вже знаєте значення:
int[] array = { 0, 1, 1, 2, 3, 5, 8 };
Що робити, якщо не знаєте значення? Ось код, з яким ви стикатиметеся найчастіше для оголошення, виділення та використання масиву int :
int[] array;
array = new int[10];
array[0] = 0;
array[1] = 1;
array[2] = 1;
array[3] = 2;
array[4] = 3;
array[5] = 5;
array[6] = 8;
...
Зверніть увагу, що я вказав масив int , який є масивом примітивних типів даних (primitive data types) Java. Давайте подивимося, що станеться, якщо ви спробуєте виконати той же процес із масивом об'єктів Java замість примітивів:
class SomeClass {
    int val;
    // …
}
SomeClass[] array = new SomeClass[10];
array[0].val = 0;
array[1].val = 1;
array[2].val = 1;
array[3].val = 2;
array[4].val = 3;
array[5].val = 5;
array[6].val = 8;
Якщо запустити наведений вище код, ми отримаємо виняток відразу після спроби використовувати перший елемент масиву. Чому? Незважаючи на те, що масив виділено, кожен сегмент масиву містить порожні посилання на об'єкти. Якщо ви введете цей код у свою IDE, він навіть автоматично заповнить .val для вас, тому помилка може збивати з пантелику. Щоб усунути баг, виконайте такі дії:
SomeClass[] array = new SomeClass[10];
for ( int i = 0; i < array.length; i++ ) {  //new code
    array[i] = new SomeClass();             //new code
}                                           //new code
array[0].val = 0;
array[1].val = 1;
array[2].val = 1;
array[3].val = 2;
array[4].val = 3;
array[5].val = 5;
array[6].val = 8;
Але це елегантно. Я задумався, чому я не можу легко виділити масив та об'єкти всередині масиву, написавши менше коду, може навіть все в одному рядку. Щоб знайти відповідь, я провів кілька експериментів.

У пошуках нірвани серед масивів Java

Наша мета - кодувати елегантно. Дотримуючись правил “чистого коду”, я вирішив створити код багаторазового використання, щоб очистити шаблон розподілу масиву. Ось перша спроба:
public class MyArray {

    public static Object[] toArray(Class cls, int size)
      throws Exception {
        Constructor ctor = cls.getConstructors()[0];
        Object[] objects = new Object[size];
        for ( int i = 0; i < size; i++ ) {
            objects[i] = ctor.newInstance();
        }

        return objects;
    }

    public static void main(String[] args) throws Exception {
        SomeClass[] array1 = (SomeClass[])MyArray.toArray(SomeClass.class, 32); // see this
        System.out.println(array1);
    }
}
Рядок коду, позначений як “see this”, виглядає саме так, як я хотів, завдяки реалізації toArray . Цей підхід використовує відображення, щоб знайти за замовчуванням конструктор для даного класу, а потім викликає цей конструктор для створення екземпляра об'єкта цього класу. Процес викликає конструктор один раз кожного елемента масиву. Чудово! Жаль тільки, що це не працює. Код компілюється нормально, але призводить до помилки ClassCastException під час запуску. Щоб використовувати цей код, вам потрібно створити масив Object елементів, а потім привести кожен елемент масиву до класу SomeClass таким чином:
Object[] objects = MyArray.toArray(SomeClass.class, 32);
SomeClass scObj = (SomeClass)objects[0];
...
Це не елегантно! Після додаткових експериментів я розробив кілька рішень, що використовують відображення, дженерики та лямбда-вирази.

Рішення 1. Використовуйте відображення

Тут ми використовуємо клас java.lang.reflect.Array для створення екземпляра масиву вказаного вами класу замість використання базового класу java.lang.Object . По суті, це однорядкова зміна коду:
public static Object[] toArray(Class cls, int size) throws Exception {
    Constructor ctor = cls.getConstructors()[0];
    Object array = Array.newInstance(cls, size);  // new code
    for ( int i = 0; i < size; i++ ) {
        Array.set(array, i, ctor.newInstance());  // new code
    }
    return (Object[])array;
}
Ви можете використовувати цей підхід, щоб отримати масив потрібного класу, а потім працювати з ним так:
SomeClass[] array1 = (SomeClass[])MyArray.toArray(SomeClass.class, 32);
Хоча це не обов'язкова зміна, другий рядок було змінено, щоб використовувати клас відображення Array для встановлення вмісту кожного елемента масиву. Це чудово! Але є ще одна деталь, яка здається не зовсім правильною: приведення до SomeClass не дуже красиво виглядає. На щастя, є рішення із дженериками.

Рішення 2. Використовуйте дженерики

Фреймворк Collections використовує дженерики для прив'язки до типу та усуває приведення до них у багатьох операціях. Тут можна використовувати дженерики. Візьмемо, наприклад, java.util.List .
List list = new ArrayList();
list.add( new SomeClass() );
SomeClass sc = list.get(0); // Error, needs a cast unless...
Третій рядок у наведеному вище фрагменті викликає помилку, якщо ви не оновите перший рядок таким чином:
List<SomeClass> = new ArrayList();
Ви можете досягти того ж результату, використовуючи дженерики у класі MyArray . Ось нова версія:
public class MyArray<E> {
    public <E> E[] toArray(Class cls, int size) throws Exception {
        E[] array = (E[])Array.newInstance(cls, size);
        Constructor ctor = cls.getConstructors()[0];
        for ( int element = 0; element < array.length; element++ ) {
            Array.set(array, element, ctor.newInstance());
        }
        return arrayOfGenericType;
    }
}
// ...
MyArray<SomeClass> a1 = new MyArray(SomeClass.class, 32);
SomeClass[] array1 = a1.toArray();
Це виглядає добре. Використовуючи дженерики і включивши цільовий тип оголошення, тип можна вивести в інших операціях. Крім того, цей код можна скоротити до одного рядка, якщо зробити так:
SomeClass[] array = new MyArray<SomeClass>(SomeClass.class, 32).toArray();
Місію виконано, вірно? Ну не зовсім. Це нормально, якщо вам байдуже, який конструктор класу ви викликаєте, але якщо ви хочете викликати конкретний конструктор, то це рішення не працює. Ви можете продовжувати використовувати відображення для вирішення цієї проблеми, але код стане складним. На щастя, є лямбда-вирази, які пропонують інше рішення.

Рішення 3. Використовуйте лямбда-вирази

Зізнаюся, раніше мене не надихали лямбда-вирази, але потім я навчився цінувати їх. Зокрема, мені сподобався інтерфейс java.util.stream.Stream , який обробляє набори об'єктів. Stream допоміг мені досягти нірван серед масивів Java. Ось моя перша спроба використати лямбди:
SomeClass[] array =
    Stream.generate(() -> new SomeClass())
    .toArray(SomeClass[]::new);
Я розбив код на три рядки для зручності читання. Ви можете побачити, що він відповідає всім вимогам: він простий і елегантний, створює заповнений масив екземплярів об'єктів і дозволяє викликати певний конструктор. Зверніть увагу на параметр методу toArray : SomeClass[]::new . Це функція-генератор, використовувана виділення масиву зазначеного типу. Однак у його нинішньому вигляді цей код має невелику проблему: він створює масив нескінченного розміру. Це не надто оптимально. Але проблему можна вирішити, викликавши метод limit :
SomeClass[] array =
    Stream.generate(() -> new SomeClass())
    .limit(32)   // calling the limit method
    .toArray(SomeClass[]::new);
Масив тепер обмежений 32 елементами. Ви можете встановити певні значення об'єкта для кожного елемента масиву, як показано нижче:
SomeClass[] array = Stream.generate(() -> {
    SomeClass result = new SomeClass();
    result.val = 16;
    return result;
    })
    .limit(32)
    .toArray(SomeClass[]::new);
Цей код демонструє ефективність лямбда-виразів, але код не є акуратним та компактним. На мій погляд, виклик іншого конструктора для встановлення значення буде набагато кращим.
SomeClass[] array6 = Stream.generate( () -> new SomeClass(16) )
    .limit(32)
    .toArray(SomeClass[]::new);
Мені подобається рішення з урахуванням лямбда-выражения. Воно ідеально підходить, коли потрібно викликати конкретний конструктор або працювати з кожним елементом масиву. Коли мені потрібно щось простіше, я зазвичай використовую рішення, засноване на дженериках, оскільки воно простіше. Однак ви самі можете бачити, що лямбда-вирази є елегантним і гнучким рішенням.

Висновок

Сьогодні ми дізналися, як у Java працювати з оголошенням та виділенням масивів примітивів, виділяти масиви елементів Object , використовувати відображення, дженерики та лямбда-вираження.
Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ