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 шт):

Автор решения: Nowhere Man

Наличие неизменяемых списков и вообще коллекций, а также иным образом ограниченных реализаций типа 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 an UnsupportedOperationException if the invocation would have no effect on the collection. For example, invoking the addAll(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:


Если взять 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.

→ Ссылка