Java List.of и принцип Лисков
Как известно List.of
в Java
возвращает неизменяемый List.
Не является ли это плохим дизайном языка с точки зрения Liskov Substitution Principle
?
Определение из wiki:
если S является подтипом T, тогда объекты типа T в программе могут быть замещены объектами типа S без каких-либо изменений желательных свойств этой программы.
Ведь если существует некий метод:
void addElement(List<String> list){
list.add("Liskov");
}
то будет брошено исключение, если в метод передать List, созданный через List.of
, что противоречит LSP
. Хотя, конечно, весь solid
всего лишь набор рекомендаций.
Если взять Kotlin
, то там в плане дизайна языка поступили на мой взгляд более правильно. Создали коллекции List
(immutable) и MutableList
.
Или я всё же неверно понимаю LSP
?
Ответы (1 шт):
Наличие неизменяемых списков и вообще коллекций, а также иным образом ограниченных реализаций типа Arrays.asList
может рассматриваться как нарушение чистого принципа LSP в SOLID по указанной в вопросе причине. Кроме того, списки, полученные при помощи фабричных методов List.of
не могут также содержать null
значения. Также может различаться поведение для разных реализаций потокобезопасных коллекций
Однако, возможность различного поведения для разных реализаций официально задокументирована, в частности, в документации к соответствующим интерфейсам List
/ Collection
говорится, что все операции, которые приводят к изменению коллекции и её содержимого, являются опциональными, то есть, они могут, но НЕ обязаны выбрасывать исключение UnsupportedOperationException
:
Java 8 Collection
The "destructive" methods contained in this interface, that is, the methods that modify the collection on which they operate, are specified to throw
UnsupportedOperationException
if this collection does not support the operation. If this is the case, these methods may, but are not required to, throw anUnsupportedOperationException
if the invocation would have no effect on the collection. For example, invoking theaddAll(Collection)
method on an unmodifiable collection may, but is not required to, throw the exception if the collection to be added is empty.
Также для списков допускается возможность в некоторых реализациях запрещать наличие дубликатов, что также можно рассматривать как некое разрешённое нарушение принципа LSP:
It is not inconceivable that someone might wish to implement a list that prohibits duplicates, by throwing runtime exceptions when the user attempts to insert them, but we expect this usage to be rare.
Опциональные операции для модификации:
Collection
:add(E e)
,addAll(Collection<? extends E> c)
,clear
,remove(Object o)
,removeAll(Collection<?> c)
,retainAll(Collection<?> c)
; методremoveIf(Predicate<? super E> filter)
не помечен как опциональный, но также может выбрасыватьUnsupportedOperationException
для неизменяемой коллекции.List
: операции с индексами в спискеadd(int index, E element)
,addAll(int index, Collection<? extends E> c)
,remove(int index)
,set(int index, E element)
, а также реализации по умолчаниюreplaceAll(UnaryOperator<E> operator)
,sort(Comparator<? super E> c)
Set
: те же операции, что и в родительском интерфейсеCollection
То есть, можно считать, что наличие в контракте интерфейсов Collection
/ List
подобного рода оговорки об их опциональном изменении и выбрасывании в общем случае указанного исключения, позволяет избежать нарушения принципа LSP.
Связанные вопросы на основном SO:
- Is
Arrays.asList
a violation of Liskov Substitution Principle? 2016 - Do
Collections.unmodifiableXXX
methods violate LSP? 2014
Если взять
Kotlin
, то там в плане дизайна языка поступили на мой взгляд более правильно. Создали коллекцииList
(immutable) иMutableList
.
Котлин создавался значительно позже, и очевидно его разработчики не побоялись "раздуть" количество интерфейсов. В Java сознательно пошли на некий компромисс, о чём также указано в документации Java Collections API Design FAQ :: Core Interfaces - General Questions
Why don't you support immutability directly in the core collection interfaces so that you can do away with optional operations (and UnsupportedOperationException
)?:
This is the most controversial design decision in the whole API. Clearly, static (compile time) type checking is highly desirable, and is the norm in Java. We would have supported it if we believed it were feasible. Unfortunately, attempts to achieve this goal cause an explosion in the size of the interface hierarchy, and do not succeed in eliminating the need for runtime exceptions (though they reduce it substantially).
...
When all was said and done, we felt that it was a sound engineering compromise to sidestep the whole issue by providing a very small set of core interfaces that can throw a runtime exception.