9. Основы Kotlin. Классы и интерфейсы

Сквозной список

В матрице высотой height и шириной width всего имеется height * width элементов. Они все могут быть сохранены внутри мутирующего списка (мутирующего — потому что в матрицу включены возможности изменения элементов). Определение этого списка могло бы выглядеть так:

class MatrixImpl<E>(override val height: Int, override val width: Int
                    //, something other?
                    ) : Matrix<E> {
    private val list = mutableListOf<E>() // content???

    override fun get(row: Int, column: Int): E  = TODO()
    // Other functions...
}

Чтобы после создания матрицы из списка можно было читать элементы, его необходимо чем-то заполнить. Напомним, что функция-создатель матрицы была ранее определена так:

fun <E> createMatrix(height: Int, width: Int, e: E): Matrix<E> = TODO()

Её третьим параметром был элемент для заполнения матрицы, и его нам необходимо передать в конструктор:

class MatrixImpl<E>(override val height: Int, override val width: Int, e: E) : Matrix<E> {
    private val list = mutableListOf<E>()

    init {
        for (i in ...) {
            list.add(e)
        }
    }

    override fun get(row: Int, column: Int): E  = TODO()
    // Other functions...
}

Здесь init { …​ } — это так называемый анонимный инициализатор. Операторы, указанные в этом блоке, выполняются сразу же после создания класса и записи начальной информации в его свойства.

Таким образом, наш список будет заполнен height * width элементами e сразу после создания матрицы. В дальнейшем, в функциях get и set мы должны будем прочесть или перезаписать элемент списка list по определённому индексу, зависящему от row и column.

Список списков

Элементы матрицы высотой height и шириной width можно также представить как список размера height, состоящий, в свою очередь, из списков размера width (состоящих из отдельных элементов типа E). Тип подобного контейнера определяется как List<List<E>>.

Список заранее неизвестного размера может быть создан с помощью функции List(size: Int, init: (Int) → E). Её первый параметр — требуемый размер списка, а второй — функция, определяющая, какой элемент хранится по какому индексу. Например, следующий вызов конструктора создаст список размера width из одинаковых элементов e:

val array = List(width) { index -> e } // или просто List(width) { e }

При создании списка списков следует иметь в виду, что элементами внешнего списка в свою очередь являются списки, и создавать их тоже надо с помощью функции List.

Примерно аналогичным образом можно представить элементы матрицы в виде одного сквозного массива, или же в виде массива массивов. Возможны и другие варианты. В частности:

Ассоциативный массив

Элементы матрицы также можно представить в качестве ассоциативного массива, отображающего Cell в EMutableMap<Cell, E>. В такой карте каждой ячейке матрицы будет соответствовать свой элемент, причём ячейка будет служить индексом. Например:

class MatrixImpl<E>(override val height: Int, override val width: Int) : Matrix<E> {
    private val map = mutableMapOf<Cell, E>()
    // ...
}

При такой реализации заполнение матрицы может быть выполнено как внутри анонимного инициализатора, так и непосредственно в функции-создателе. Например:

fun <E> createMatrix(height: Int, width: Int, e: E): Matrix<E> {
    val result = MatrixImpl(height, width)
    result[0, 0] = e
    result[0, 1] = e
    // ... Конечно, здесь лучше бы написать цикл
}

При использовании ассоциативного массива следует помнить, что выражение map[cell], обеспечивающее чтение элемента из определённой ячейки, имеет тип E?, а не E. Операторная функция get, однако, имеет результат типа E(отличающийся тем, что null не входит в его множество значений). Поэтому в функции get следует явно написать, как нужно обрабатывать полученный null. При наивном коде вроде этого:

class MatrixImpl<E>(override val height: Int, override val width: Int) : Matrix<E> {
    private val map = mutableMapOf<Cell, E>()

    override fun get(cell: Cell): E = map[cell] // Type mismatch: expected E, actual E?
    // ...
}

мы получим ошибку компиляции в функции get.

Реализация equals / hashCode

Часть вопросов, связанных с реализацией equals, мы рассмотрели в 8-м разделе. Когда необходимо написать эту функцию, следует ответить себе на вопрос: а когда, собственно, матрицы считаются равными? В данном случае очевидный ответ таков: когда равны их высоты и ширины, а также равны все соответствующие друг другу элементы. Также следует помнить, что тип параметра equals — Any?, а значит, перед сравнением следует проверить, что этот параметр принадлежит к типу Matrix<E> или MatrixImpl<E> (проще второе; в первом случае мы оставляем за собой возможность признать равными две разных реализации одной и той же матрицы — скажем, сравнение списков работает именно так).

Шаблон для реализации equals выглядит примерно так:

class MatrixImpl<E> : Matrix<E> {
    override val height: Int = TODO()
    override val width: Int = TODO()

    // ... Other functions ...

    override fun equals(other: Any?) =
            other is MatrixImpl<*> &&
            height == other.height &&
            width == other.width // && elements comparison
}

Обратите внимание на то, как проверяется тип otheris MatrixImpl<*>, то есть E заменяется на *. Такая запись означает «MatrixImpl с элементами произвольного типа» и связана с особенностями реализации настраиваемых типов в JVM. Во время выполнения программы можно определить принадлежность к основному типу MatrixImpl, но нельзясделать то же самое для какого-либо его конкретного варианта, например MatrixImpl<Int>. Попытка написать other is MatrixImpl<E> приведёт к предупреждению компиляции Unchecked Cast.

При реализации equals в своём классе следует помнить о пяти различных свойствах, которым эта реализация должна удовлетворять:

  1. Что угодно равно самому себе
  2. Если A равно B, то B равно A
  3. Если A равно B и B равно C, то A равно С.
  4. Никакое значение из типа Any не может быть равно null.
  5. Результат сравнения A и B не должен меняться при повторном вызове equals, ЕСЛИ внутреннее состояние A и B не изменилось между вызовами.

Реализовав equals в MatrixImpl, посмотрите на определение класса внимательнее. Вы заметите, что название класса подсвечено, и имеется предупреждение о реализации функции equals при отсутствующей реализации функции hashCode. Эту реализацию можно сгенерировать автоматически, если зайти в меню действий IDEA (Alt+Enter) и выбрав пункт Generate hashCode(). В результате мы получим что-то вроде:

class MatrixImpl<E> : Matrix<E> {
    override val height: Int = TODO()
    override val width: Int = TODO()

    // ... Other functions ...

    override fun equals(other: Any?) =
            other is MatrixImpl<*> &&
            height == other.height &&
            width == other.width // && elements comparison

    override fun hashCode(): Int {
        var result = 5
        result = result * 31 + height
        result = result * 31 + width
        // Something for elements...
        return result
    }
}

Что же такое этот хеш-код? Это целое число, «привязанное» к любому значению типа Any и имеющее следующие свойства:

  1. Если A равно B, то хеш-код A ВСЕГДА равен хеш-коду B.
  2. Если A не равно B, то, КАК ПРАВИЛО (но не всегда!), хеш-код A не равен хеш-коду B.

Хеш-код используется в большинстве реализаций ассоциативных массивов и множеств — а конкретно, в тех реализациях, которые используют так называемые хеш-таблицы. Подробная информация о них выходит за рамки данного пособия, желающим я предлагаю прочитать одноимённую статью Википедии. Важно, однако, запомнить правило: если в классе определена функция equals, следует определить в нём также и hashCode. В противном случае вы рискуете получить некорректную работу с вашими объектами в ассоциативных массивах и множествах.

Добавить комментарий