Type List против типа ArrayList в Java
Описание проблемы
У меня возник вопрос относительно использования обобщений в Java, особенно когда дело касается интерфейсов.
Я сталкиваюсь с двумя способами объявления списка:
(1) List<?> myList = new ArrayList<?>();
(2) ArrayList<?> myList = new ArrayList<?>();
Я понимаю, что в случае (1) можно легко менять реализации интерфейса List
. Однако, мне кажется, что (1) обычно используется в приложениях независимо от необходимости (я, например, всегда использую этот вариант).
Возникает вопрос: кто-нибудь использует вариант (2)?
Также мне интересно, насколько часто (и можно ли привести пример) ситуация на самом деле требует использования (1) вместо (2). В каких случаях (2) не подходит? Имеется в виду не просто соблюдение принципа "кодирования под интерфейсы" и "лучшие практики", а конкретные сценарии использования.
5 ответ(ов)
Да, некоторые люди действительно используют (2), но, на мой взгляд, крайне редко, и только по веским причинам.
Многие сталкиваются с проблемами, когда используют ArrayList
, когда им следовало бы использовать List
. Например:
- Утилитарные методы, такие как
Collections.singletonList(...)
илиArrays.asList(...)
, не возвращаютArrayList
. - Методы в API
List
не гарантируют возврат списка того же типа.
Пример неудачного использования можно найти в ответе на Stack Overflow (https://stackoverflow.com/a/1481123/139985), где автор столкнулся с проблемами из-за "срезов", так как ArrayList.subList(...)
не возвращает ArrayList
, а его код был разработан с использованием ArrayList
как типа для всех переменных списка. В итоге, ему пришлось "решать" проблему, вручную копируя срез в новый ArrayList
.
Что касается аргумента о том, что нужно знать, как ведёт себя List
, его отчасти можно упростить с помощью маркерного интерфейса RandomAccess
. Да, это может показаться немного громоздким, но альтернатива хуже.
Как часто на самом деле требуется использовать (1) вместо (2) (т.е. где (2) не подойдет... кроме 'программирования под интерфейсы' и лучших практик и т.д.)?
Вопрос о том, "как часто", в объективной реальности невозможно однозначно ответить.
(И можно ли получить пример)
Иногда приложение может требовать использования методов в API ArrayList
, которые не представлены в API List
. Например: ensureCapacity(int)
, trimToSize()
или removeRange(int, int)
. (И последний случай возникнет только в том случае, если вы создали подкласс ArrayList
, который объявил этот метод как public
.)
Это единственная веская причина для программирования под класс, а не под интерфейс, на мой взгляд.
(Теоретически, возможно, вы получите небольшое улучшение производительности... в определенных условиях... на некоторых платформах... но если вам действительно не нужно это последнее 0,05%, то это не стоит усилий. По моему мнению, это не является веской причиной.)
Вы не сможете писать эффективный код, если не знаете, эффективен ли случайный доступ или нет.
Это справедливое замечание. Однако Java предлагает более удобные способы решения этой проблемы. Например:
public <T extends List & RandomAccess> void test(T list) {
// выполняем действия
}
Если вы вызовете это с списком, который не реализует RandomAccess
, вы получите ошибку компиляции.
Вы также можете проверять динамически... с помощью instanceof
, если статическая типизация оказывается слишком неудобной. Вы даже можете написать свой код так, чтобы использовать разные алгоритмы (динамически), в зависимости от того, поддерживает ли список случайный доступ.
Обратите внимание, что ArrayList
не единственный класс списка, который реализует RandomAccess
. Другими примерами являются CopyOnWriteArrayList
, Stack
и Vector
.
Я видел, как люди делают аналогичный аргумент по поводу Serializable
(поскольку List
не реализует его)... но приведенный выше подход решает эту проблему тоже. (В той мере, в какой ее можно решить вообще с использованием типов во время выполнения. ArrayList
не удастся сериализовать, если любой элемент не сериализуем.)
Наконец, я не собираюсь говорить "потому что это хороший стиль". Эта "причина" является как бы круговым аргументом ("Почему это 'хороший стиль'?") и апелляцией к не заявленному (и, вероятно, несуществующему!) высшему авторитету ("Кто говорит, что это 'хороший стиль'?").
(Я считаю, что программирование под интерфейс - это хороший стиль, но не буду давать это в качестве причины. Лучше, чтобы вы поняли реальные причины и пришли к (на мой взгляд) правильным выводам самостоятельно. Правильный вывод может не всегда совпадать... в зависимости от контекста.)
С точки зрения хорошего стиля, рекомендуется хранить ссылку на HashSet
или TreeSet
в переменной типа Set.
Set<String> names = new HashSet<String>();
Таким образом, если вы решите использовать TreeSet
, вам нужно будет изменить всего лишь одну строку кода.
Кроме того, методы, работающие с множествами, должны указывать параметры типа Set:
public static void print(Set<String> s)
Тогда метод можно использовать для всех реализаций множеств.
В теории, мы могли бы сделать то же самое для связанных списков, а именно, хранить ссылки на LinkedList в переменных типа List. Однако в библиотеке Java интерфейс List является общим как для класса ArrayList
, так и для LinkedList
. В частности, он имеет методы get и set для произвольного доступа, даже несмотря на то что эти методы являются очень неэффективными для связанных списков.
Вы не сможете писать эффективный код, если не знаете, эффективен ли произвольный доступ или нет.
Это явно является серьезной ошибкой дизайна в стандартной библиотеке, и я не могу рекомендовать использование интерфейса List по этой причине.
Чтобы увидеть, насколько это неудобно, взгляните на исходный код метода binarySearch
класса Collections. Этот метод принимает параметр типа List, но бинарный поиск не имеет смысла для связанного списка. Затем код неуклюже пытается выяснить, является ли список связанным, и переключается на линейный поиск!
Интерфейс Set
и интерфейс Map
хорошо спроектированы, и вам следует их использовать.
В этом коде я использую (2), если класс "владеет" списком. Это, например, верно для локальных переменных. Нет необходимости использовать абстрактный тип List
, если можно использовать конкретный ArrayList
.
Вот пример, который иллюстрирует концепцию владения:
public class Test {
// Этот объект владеет строками, поэтому используем конкретный тип.
private final ArrayList<String> strings = new ArrayList<>();
// Этот объект использует аргумент, но не владеет им, поэтому используйте абстрактный тип.
public void addStrings(List<String> add) {
strings.addAll(add);
}
// Здесь мы возвращаем список, но не передаем владение, поэтому используем абстрактный тип. Это также позволяет создать, при необходимости, неизменяемый список.
public List<String> getStrings() {
return Collections.unmodifiableList(strings);
}
// Здесь мы создаем новый список и передаем владение вызывающему коду. Используем конкретный тип.
public ArrayList<String> getStringsCopy() {
return new ArrayList<>(strings);
}
}
Таким образом, использование конкретных и абстрактных типов помогает четко определить, кто является владельцем данных и какие операции допустимы с ними.
На самом деле есть ситуации, когда (2) не только предпочтительно, но и обязательно, и я очень удивлен, что никто этого не упоминает здесь.
Сериализация!
Если у вас есть сериализуемый класс, и вы хотите, чтобы он содержал список, то вам необходимо объявить поле конкретным и сериализуемым типом, таким как ArrayList
, потому что интерфейс List
не является наследником java.io.Serializable
.
Очевидно, что большинству людей сериализация не нужна, и они забывают об этом.
Вот пример:
public class ExampleData implements java.io.Serializable {
// Следующее также гарантирует, что 'strings' всегда будет ArrayList.
private final ArrayList<String> strings = new ArrayList<>();
Так что имейте это в виду, если вы планируете использовать сериализацию в своих классах!
Ваша строка кода:
Collection myCollection = new ArrayList<?>();
не совсем корректна. Обратите внимание, что ArrayList
не может быть инициализирован с использованием "?" (wildcard) в этом контексте. Вместо этого вы можете использовать что-то вроде:
Collection<MyType> myCollection = new ArrayList<>();
где MyType
— это конкретный тип, который вы хотите хранить в коллекции.
Что касается вашего подхода, вы правы: если вам нужны методы интерфейса List
, вы можете использовать List
, а если вас интересуют методы интерфейса Collection
, то можно использовать его. Главное преимущество здесь в том, что вы можете начинать с более общего интерфейса и при необходимости переключаться на более специфичный, например, List
или ArrayList
, если это необходимо для вашего использования.
В общем, рекомендуется выбирать интерфейс, который соответствует вашим нуждам, и использовать типы, которые представляют структуру данных наилучшим образом.
Как объединить два списка в Java?
Как создать новый список в Java
Преобразование Set в List без создания нового списка
Как преобразовать Map в List в Java?
Почему возникает UnsupportedOperationException при попытке удалить элемент из списка?