Comments 41
В очередной раз в интернете не разобрались в Kotlin коллекциях:
val list: List<Int> = listOf(1, 2, 3)
val mutableList: MutableList<Int> = list as MutableList<Int>
Дока на listOf:
Returns a new read-only list of given elements.
read-only это не immutable, а интерфейс который запрещает изменять, реализация же может быть основана на mutable коллекциях, никаких гарантий на это не давали.
Как и нету гарантий что в следующем релизе этот код упадет еще на касте list as MutableList<Int>
.
На практике никаких проблем это не создает, т.к. пользовательском коде нету смысла кастатить листы к мутабальным листам. Работаем поверх интерфейсов и получаем ожидаемое поведение.
Я не знаю нюансов Kotlin, но мне кажется что как раз об этом речь в статье: если я пишу метод, указываю в сигнатуре в параметрах List то у меня нет никакой возможности быть уверенным что мне не дадут read-only List. По правилам SOLID я в такую ситуацию не должен попадать.
Вам могут передать любой объект, который удовлетворяет интерфейсу List
, т.к. MutableList: List, то в метод под видом List могут передать мутабальную реализацию. В методе естественно вам будут недоступны мутирущие методы. Принцип подстановки Лисков не нарушен. А вот когда происходит каст из List в MutableList — это уже нарушение принципа, т.к. разработчик полагается не на стабильный интерфейс, а на деталь реализации.
Тут проблема не в listOf
, он просто для примера. Дело в самой организации интерфейсов
val mutableList: MutableList<Int> = ...
// какие-то действия с изменяемым списком
...
startJobInSeparateThread(mutableList) // принимает List<Int>
mutableList.clear() // непредвиденное изменение списка
Суть в том, что если метод принимает на вход List<Int>
ожидается, что список неизменяемый. Но по факту получается так, что мы можем передать мутабельную сущность и, как в данном примере, изменить ее, хотя по сути список вроде как был read-only.
Я согласен с тем, что чаще всего вышеописанного не произойдет, но чаще всего != всегда. При полном же разделении интерфейсов такого даже теоретически случиться не может. Конечно, при условии, что мы не будем изменять объект с помощью рефлексии, но это уже другая история.
А вообще, в этой статье человек лучше меня объяснил этот феномен)
Тут проблема не в listOf, он просто для примера. Дело в самой организации интерфейсов
Ну пример прям плохой тогда выбран.
Суть в том, что если метод принимает на вход List<Int>
ожидается, что список неизменяемый.
Вообще нет, нигде в Kotlin этого не обещают. Он read-only, не immutable. Это значит что ожидается что метод принимающий List<Int>
не будет изменять лист, а только читать.
Я согласен с тем, что чаще всего вышеописанного не произойдет, но чаще всего != всегда. При полном же разделении интерфейсов такого даже теоретически случиться не может.
Ничто не мешает реализации правильно-разделенного интерфейса мутировать данные скрытые за интерфейсом:
interface ImmutableList<T> {
fun get(index: Int): T
}
class HabrImmutableList<T> : ImmutableList<T> {
private val list = mutableListOf<T>()
override fun get(index: Int) = list[index]
// Only for testing! :)
fun add(element: T) = list.add(element)
}
Так что теоретически этот метод не спасает, да и спасать не нужно, все и так работает.
А вообще, в этой статье человек лучше меня объяснил этот феномен)
Неизменяемых коллекций в Java не будет – ни сейчас, ни когда-либо
Где-то на просторах openjdk читал, что иметь у каждого объекта заголовок с синхронизацией расточительно и было бы неплохо переиспользовать его для других целей, в т.ч. и для флага заморозки. Правда не понятно что делать с существующим фреймворком коллекций, озвученных проблем с API это не решит. Но заголовок той статьи слишком резок, что ли..
Вроде бы вот этот черновик
Если честно, я не знал про эту библиотеку, ну и начал я писать свою, соответственно, до того. К тому же, много проектов, похожих друг на друга, но это ведь не повод, чтобы не написать что-то свое)
UPD: что вряд ли
Vavr невозможно использовать в реальных проектах. Вам нужен интероп с кучей других библиотек, которые про Vavr ни сном ни духом. А у вас и там, и сям простые имена классов называются одинаково со стандартными. В итоге ваш код превратится просто в ад. Хорошо и красиво для маленьких учебных проектов. Для кровавого продакшна — извините.
Кому как. В кроваво экскрементальный легаси у вас навряд ли получится запихнуть еще и Vavr, а собственно зачем это делать, пущай себе догнивает в уголочке.
А вот в современный ентерпрайз, почиканный на микросервесы или ламбды(что суть есть похожее) и без извращений типа Spring/Guava, Vavr встраевается на ура. Так что, уж это вы извините, продакшен продакшену рознь.
ICollection: IEnumerableIReadOnlyCollection: IEnumerable
Eclipse Collections не смотрел. В работе использую преимущественно Apache Commons и Guava. Я не сомневаюсь, что есть множество библиотек, которые так или иначе решают проблему иммутабельных коллекций в Java. Но сама проблема из Java от этого никуда не уходит.
Тогда выходит, что любой вызов add в любой имплементации List надо оборачивать в try-catch, а то вдруг unsupported operation выскочет) То, что метод описан в джавадоке не значит, что он должен быть в интерфейсе. Думаю, именно поэтому в kotlin изменили иерархию интерфейсов коллекций, а не просто взяли то, что уже есть в java
> То, что метод описан в джавадоке не значит, что он должен быть в интерфейсе
Вот тут не распарсил, можете развернуть?
Я имею в виду, что раз джавадок для метода есть, это не значит, что методу место в интерфейсе. На самом деле, ему было бы место, если в java не было неизменяемых коллекций типа Collections.emptyList()
. Собственно, именно это и вызывает проблемы. Мы находимся в состоянии неопределённости — этот список можно менять, или нет? Будет исключение, или нет? А то, что оно unchecked, не значит, что оно не может навредить. Потом, если это все не обернуто в try-catch уровнем выше, придётся копаться в логах и искать, в чем же причина ошибки.
В который раз, читаем джавадок
Returns an empty list (immutable).
Потенциальные NPE тоже в try-catch оборачиваете?
Это как это?
Метод есть, документация есть, а места нет.
Или изначально не должно было быть ни метода, ни документации на него, ни даже места для такого метода?
Хотел спросить о том же самом. Больше того: в скалке коллекции неизменяемы по умолчанию (type scala.collections.List = scala.collections.immubale.List и т. д.). Ну, в случае с Java всё понятно: надо обеспечивать обратную совместимость. Но Guava и Kotlin просто «радуют».
Правильный ответ: Immutable persistent collections for Kotlin
Принцип Лисков тут не при чем, нарушается принцип инкапсуляции. Детали мутабельной абстракции протекают в иммутабельную. В Котлине это нормально сделано, проблема в том, что Котлин без Java пока еще не может, нужен interop.
Нужно не две независимых иерархии, а три: mutable, immutable и read-only.
/**
* Returns a fixed-size list backed by the specified array. (Changes to
* the returned list "write through" to the array.) This method acts
* as bridge between array-based and collection-based APIs, in
* combination with {@link Collection#toArray}. The returned list is
* serializable and implements {@link RandomAccess}.
*
* <p>This method also provides a convenient way to create a fixed-size
* list initialized to contain several elements:
* <pre>
* List<String> stooges = Arrays.asList("Larry", "Moe", "Curly");
* </pre>
*
* @param <T> the class of the objects in the array
* @param a the array by which the list will be backed
* @return a list view of the specified array
*/
@SafeVarargs
@SuppressWarnings("varargs")
public static <T> List<T> asList(T... a) {
return new ArrayList<>(a);
}
Следовательно, меняя элементы в полученном списке они будут заменены и в исходном массиве:
String[] strings = new String[]{ "foo", "boo" };
List<String> stringsList = Arrays.asList(strings);
stringsList.set(0, "Hello");
stringsList.set(1, "Java!");
System.out.println(Arrays.toString(strings)); // выведет [Hello , Java!]
А вот что касается принципов SOLID, и буквы «L», и что имплементации интерфейсов должны быть заменяемыми без нарушения корректности программы, тут хотелось бы обратить внимание, что бросаемые исключения не являются нарушением корректности программы, просто, надо делать как надо, а как не надо делать не надо)
Вот код из статьи:
List<Integer> list = Arrays.asList(21, 22, 23);
Здесь некий статический метод возвращает известный интерфейс. Далее в статье приводятся аналогичные примеры, когда другие методы точно так же возвращают тот же самый интерфейс, то есть проблема общая. А проблемность этой общности в том, что везде авторы реализации игнорируют ряд методов из возвращаемого типа. Небольшая аналогия — на двери написано «туалет», заходишь, а там только душ. А кто вам обещал унитаз? Вот примерно так и оправдываются авторы кода из приведённых в статье примеров.
Вторая проблема вообще до безумия глупая. Вот котлиновский код:
val mutableList: MutableList<Int> = list as MutableList<Int>
И оказывается, что при приведении типа к его наследнику виртуальная машина не проверяет допустимость приведения несовместимых типов! Это просто адский угар и абсолютный трэш. Разработчики котлина, оказывается, не знают, что нужно проверять совместимость типов! В аналогии с туалетом мы бы увидели такую картину — нажимаешь на смыв и всё помещение заливается нечистотами. А что, нужно было читать документацию на входе в туалет! Так что сам дурак.
В общем вижу банальное отсутствие здравого смысла у разработчиков приведённого в примерах кода и виртуальной машины. Либо наплевательское отношение к качеству, отсутствие тестирования и т.д.
В итоге после написания proof-of-concept забросил, поскольку не ощутил практической пользы и желания вставить это в какой-нибудь реальный проект. Была бы в Java такая возможность «из коробки» — было бы чуть интереснее жить. Ну а нету — так и без неё неплохо.
Javadoc на List.add
ясно говорит:
/**
* Appends the specified element to the end of this list (optional
* operation).
...
* @throws UnsupportedOperationException if the <tt>add</tt> operation
* is not supported by this list
*/
boolean add(E e);
Что касается kotlin, то, во-первых, в примере делается небезопасное приведение типов, поэтому автор сам себе стреляет в ногу. А, во-вторых, коллекции в kotlin так себя ведут по причине идеологии максимальной совместимости с java (для наиболее легкой и прозрачной миграции на новый язык) и при этом насколько это только возможно исправляет его недостатки.
Что не так с коллекциями в Java и почему Guava не поможет