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
UnsupportedOperationExceptionif this collection does not support the operation. If this is the case, these methods may, but are not required to, throw anUnsupportedOperationExceptionif 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.asLista violation of Liskov Substitution Principle? 2016 - Do
Collections.unmodifiableXXXmethods 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.