Сквозной список
В матрице высотой 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
в E
: MutableMap<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 }
Обратите внимание на то, как проверяется тип other
: is MatrixImpl<*>
, то есть E
заменяется на *
. Такая запись означает «MatrixImpl с элементами произвольного типа» и связана с особенностями реализации настраиваемых типов в JVM. Во время выполнения программы можно определить принадлежность к основному типу MatrixImpl
, но нельзясделать то же самое для какого-либо его конкретного варианта, например MatrixImpl<Int>
. Попытка написать other is MatrixImpl<E>
приведёт к предупреждению компиляции Unchecked Cast.
При реализации equals
в своём классе следует помнить о пяти различных свойствах, которым эта реализация должна удовлетворять:
- Что угодно равно самому себе
- Если A равно B, то B равно A
- Если A равно B и B равно C, то A равно С.
- Никакое значение из типа
Any
не может быть равно null. - Результат сравнения 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
и имеющее следующие свойства:
- Если A равно B, то хеш-код A ВСЕГДА равен хеш-коду B.
- Если A не равно B, то, КАК ПРАВИЛО (но не всегда!), хеш-код A не равен хеш-коду B.
Хеш-код используется в большинстве реализаций ассоциативных массивов и множеств — а конкретно, в тех реализациях, которые используют так называемые хеш-таблицы. Подробная информация о них выходит за рамки данного пособия, желающим я предлагаю прочитать одноимённую статью Википедии. Важно, однако, запомнить правило: если в классе определена функция equals
, следует определить в нём также и hashCode
. В противном случае вы рискуете получить некорректную работу с вашими объектами в ассоциативных массивах и множествах.