Как стать автором
Обновить

Комментарии 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 это не решит. Но заголовок той статьи слишком резок, что ли..

Если уж на то пошло, то мутабельные коллекции должны наследоваться от иммутабельных.

Тогда можно в метод подсунуть мутабельную коллекцию под видом иммутабельной. И получить разное содержимое одной и той же коллекции в разных строчках метода, потому что другой поток туда что-то пишет.

В данный момент я как раз работаю над open-source библиотекой, которая предоставляют полностью иммутабельные Java-коллекции.

А зачем все это когда есть Vavr?

Если честно, я не знал про эту библиотеку, ну и начал я писать свою, соответственно, до того. К тому же, много проектов, похожих друг на друга, но это ведь не повод, чтобы не написать что-то свое)

А про какие библиотеки вы знали, кроме java collections и guava, прежде чем начать свою писать? Ну так, чтобы предварительно рынок исследовать, так сказать.

Vavr невозможно использовать в реальных проектах. Вам нужен интероп с кучей других библиотек, которые про Vavr ни сном ни духом. А у вас и там, и сям простые имена классов называются одинаково со стандартными. В итоге ваш код превратится просто в ад. Хорошо и красиво для маленьких учебных проектов. Для кровавого продакшна — извините.

Кому как. В кроваво экскрементальный легаси у вас навряд ли получится запихнуть еще и Vavr, а собственно зачем это делать, пущай себе догнивает в уголочке.


А вот в современный ентерпрайз, почиканный на микросервесы или ламбды(что суть есть похожее) и без извращений типа Spring/Guava, Vavr встраевается на ура. Так что, уж это вы извините, продакшен продакшену рознь.

Я судя по всему, чего-то не понимаю, но почему не зделать так как .net?
ICollection: IEnumerableIReadOnlyCollection: IEnumerable
Нет, он не смотрел. 146%

Eclipse Collections не смотрел. В работе использую преимущественно Apache Commons и Guava. Я не сомневаюсь, что есть множество библиотек, которые так или иначе решают проблему иммутабельных коллекций в Java. Но сама проблема из Java от этого никуда не уходит.

Eclipse Collections это библиотека написанная в Goldman Sachs ипотом прееданная в open source. Там много чего вкусного есть
Претензии к мутирующим методам интерфейса j.u.Collection вытекают из нежелания читать джавадоки, в которых такие методы являются optional operations и могут выкинуть UnsupportedOperationException.
SOLIDный L также не нарушается. Опять же, надо джавадоки читать на j.u.Collection и его наследников, чтобы это понять.

Тогда выходит, что любой вызов add в любой имплементации List надо оборачивать в try-catch, а то вдруг unsupported operation выскочет) То, что метод описан в джавадоке не значит, что он должен быть в интерфейсе. Думаю, именно поэтому в kotlin изменили иерархию интерфейсов коллекций, а не просто взяли то, что уже есть в java

Оборачивать не надо, UnsupportedOperationException unchecked же.

> То, что метод описан в джавадоке не значит, что он должен быть в интерфейсе

Вот тут не распарсил, можете развернуть?

Я имею в виду, что раз джавадок для метода есть, это не значит, что методу место в интерфейсе. На самом деле, ему было бы место, если в java не было неизменяемых коллекций типа Collections.emptyList(). Собственно, именно это и вызывает проблемы. Мы находимся в состоянии неопределённости — этот список можно менять, или нет? Будет исключение, или нет? А то, что оно unchecked, не значит, что оно не может навредить. Потом, если это все не обернуто в try-catch уровнем выше, придётся копаться в логах и искать, в чем же причина ошибки.

> Collections.emptyList()… этот список можно менять, или нет?

В который раз, читаем джавадок

Returns an empty list (immutable).
> А то, что оно unchecked, не значит, что оно не может навредить

Потенциальные NPE тоже в try-catch оборачиваете?
> раз джавадок для метода есть, это не значит, что методу место в интерфейсе

Это как это?
Метод есть, документация есть, а места нет.

Или изначально не должно было быть ни метода, ни документации на него, ни даже места для такого метода?

Хотел спросить о том же самом. Больше того: в скалке коллекции неизменяемы по умолчанию (type scala.collections.List = scala.collections.immubale.List и т. д.). Ну, в случае с Java всё понятно: надо обеспечивать обратную совместимость. Но Guava и Kotlin просто «радуют».

Guava и Kotlin вынуждены использовать коллекции java по ряду причин (из-за совместимости главным образом)

Принцип Лисков тут не при чем, нарушается принцип инкапсуляции. Детали мутабельной абстракции протекают в иммутабельную. В Котлине это нормально сделано, проблема в том, что Котлин без Java пока еще не может, нужен interop.

Нужно не две независимых иерархии, а три: mutable, immutable и read-only.

А так к слову, чем read-only от immutable чем отличаются?

Read-only означает, что объект не может быть изменен через этот интерфейс, но может быть изменен другим способом.


Immutable означает, что объект неизменен в принципе.

Вот как в OpenJDK 10 (и мне кажется, что в последующих версиях ничего менялось) реализовано и описано, ничего про Mutable или Immutable, просто fixed-size. Пусть не смущает ArrayList — это вложенный класс, реализующий простую обертку над массивом. Суть этого метода — предоставить доступ к элементам массива через интерфейс List, что и сказано в документации «This method acts as bridge between array-based and collection-based APIs».
    /**
     * 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», и что имплементации интерфейсов должны быть заменяемыми без нарушения корректности программы, тут хотелось бы обратить внимание, что бросаемые исключения не являются нарушением корректности программы, просто, надо делать как надо, а как не надо делать не надо)
Проблема не в Java. Совсем не в ней, а в разработчиках, которые нарушают базовые принципы логичности.

Вот код из статьи:
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 (для наиболее легкой и прозрачной миграции на новый язык) и при этом насколько это только возможно исправляет его недостатки.

Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации

Истории