В этой статье рассмотрим различия между использованием типов List и ArrayList.
Во-первых, увидим пример реализации с использованием ArrayList. Затем переключимся на интерфейс List и сравним различия.
Использование ArrayList
ArrayList – одна из наиболее часто используемых реализаций List в Java. Он построен на основе массива, который может динамически увеличиваться и уменьшаться по мере добавления или удаления элементов. Хорошо инициализировать список начальной емкостью, если знаем, что он станет большим:
ArrayList<String> list = new ArrayList<>(25);
Используя ArrayList в качестве ссылочного типа, можно использовать методы в ArrayList API, которых нет в List API, например, sureCapacity, trimToSize или removeRange.
Быстрый пример
Напишем базовое приложение для обработки пассажиров:
public class ArrayListDemo {
private ArrayList<Passenger> passengers = new ArrayList<>(20);
public ArrayList<Passenger> addPassenger(Passenger passenger) {
passengers.add(passenger);
return passengers;
}
public ArrayList<Passenger> getPassengersBySource(String source) {
return new ArrayList<Passenger>(passengers.stream()
.filter(it -> it.getSource().equals(source))
.collect(Collectors.toList()));
}
// Другие функции, чтобы удалить пассажира, добраться до места назначения, ...
}
Здесь использовали тип ArrayList для хранения и возврата списка пассажиров. Поскольку максимальное количество пассажиров равно 20, исходная вместимость списка установлена в это значение.
Проблема с данными переменного размера
Вышеупомянутая реализация работает нормально, пока не нужно менять тип списка, который используем. В примере выбрали ArrayList и решили, что он соответствует нашим потребностям.
Однако предположим, что по мере развития приложения становится понятно, что количество пассажиров очень сильно изменяется. Например, если есть только пять забронированных пассажиров с начальной вместимостью 20, потери памяти составляют 75%. Допустим, решили переключиться на список, более эффективно использующий память.
Изменение типа реализации
Java предоставляет другую реализацию списка, называемую LinkedList, для хранения данных переменного размера. LinkedList использует набор связанных узлов для хранения и извлечения элементов. Допустим, что решили изменить базовую реализацию с ArrayList на LinkedList:
private LinkedList<Passenger> passengers = new LinkedList<>();
Это изменение затрагивает больше частей приложения, поскольку предполагается, что все функции в демонстрационном приложении будут работать с типом ArrayList.
Переключение на List
Посмотрим, как можно справиться с этой ситуацией, используя тип интерфейса List:
private List<Passenger> passengers = new ArrayList<>(20);
Здесь используем интерфейс List в качестве ссылочного типа вместо более конкретного типа ArrayList. Можно применить тот же принцип ко всем вызовам функций и типам возвращаемых значений. Например:
public List<Passenger> getPassengersBySource(String source) {
return passengers.stream()
.filter(it -> it.getSource().equals(source))
.collect(Collectors.toList());
}
Теперь рассмотрим ту же постановку задачи и изменим базовую реализацию на тип LinkedList. Классы ArrayList и LinkedList являются реализациями интерфейса List. Итак, теперь можно безопасно изменить базовую реализацию, не создавая помех другим частям приложения. Класс по-прежнему компилируется и работает нормально, как и раньше.
Сравнение подходов
Если используем конкретный тип списка во всей программе, то весь код будет без необходимости связан с этим типом списка. Это усложняет изменение типов списков в будущем. Кроме того, служебные классы, доступные в Java, возвращают абстрактный тип, а не конкретный тип. Например, приведенные ниже служебные функции возвращают тип List:
Collections.singletonList(...), Collections.unmodifiableList(...)
Arrays.asList(...), ArrayList.sublist(...)
В частности, ArrayList.sublist возвращает тип List, даже если исходный объект имеет тип ArrayList. Таким образом, методы в List API не гарантируют возврата списка того же типа.
Заключение
В этой статье рассмотрели различия и лучшие практики использования типов List и ArrayList.
Мы увидели, как ссылка на определенный тип может сделать приложение уязвимым для изменения в более поздний момент времени. В частности, когда базовая реализация изменяется, это влияет на другие уровни приложения. Следовательно, использование наиболее абстрактного типа (класс/интерфейс верхнего уровня) часто предпочтительнее использования определенного ссылочного типа.
Исходный код примеров доступен на GitHub.