Перейти к содержанию

List против ArrayList в Java

В этой статье рассмотрим различия между использованием типов 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.

Оригинал