Источник: Oracle
Работа с массивами может включать в себя отражение, дженерики и лямбда-выражения.
Недавно я разговаривал с коллегой, который занимается разработкой на языке C. Разговор зашел о массивах и о том, как они работают в Java по сравнению с C. Я нашел это немного странным, учитывая, что Java считается С-подобным языком. В них действительности много схожего, но есть и отличия. Давайте начнем с простого.
Объявление массива
Если следовать учебнику по 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);
Мне нравится решение на основе лямбда-выражения. Оно идеально подходит, когда вам нужно вызвать конкретный конструктор или работать с каждым элементом массива. Когда мне нужно что-то более простое, я обычно использую решение, основанное на дженериках, поскольку оно проще. Однако вы сами можете видеть, что лямбда-выражения представляют собой элегантное и гибкое решение.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ