В этом разделе мы плавно переходим от простых классов к более сложным. На самом деле, прямого разграничения классов на “простые” и “сложные” не существует; сложность класса приблизительно определяется количеством его свойств и функций.
С постепенным усложнением класса, его описание начинает увеличиваться в объёме. Кроме этого, могут появиться несколько вариантов выполнения классом тех или иных действий. Начиная с этого момента, есть смысл задуматься об отдельных ответах на вопросы “Что делает объект” и “Как он это делает”. В Котлине, ответ на первый вопрос дают интерфейсы (interface), на второй — классы (class).
Интерфейсы
Интерфейс определяет, что должен уметь делать объект. С точки зрения Котлина, это набор свойств (с их типами) и функций (с их параметрами и результатом), которые должны у этого объекта иметься.
Рассмотрим в качестве примера матрицу, то есть прямоугольную таблицу, имеющую M рядов и N колонок. В совокупности, матрица имеет M * N
ячеек, в каждой из которых хранится элемент матрицы определённого типа. Будет считать, что типы всех элементов матрицы совпадают. В этом случае, определение интерфейса матрицы на Котлине может выглядеть так:
data class Cell(val row: Int, val column: Int) interface Matrix<E> { val height: Int val width: Int operator fun get(row: Int, column: Int): E operator fun get(cell: Cell): E operator fun set(row: Int, column: Int, value: E) operator fun set(cell: Cell, value: E) }
Здесь Cell
— элементарный класс с данными для хранения координат (ряд, колонка) определённой ячейки матрицы. Заголовок interface Matrix<E>
определяет интерфейс с именем Matrix, использующий настраиваемый тип E
. Никаких ограничений на этот тип не задано, поэтому он может быть произвольным. В данном случае предполагается, что все элементы матрицы имеют тип E
.
Свойства height
и width
определяют высоту (число рядов) и ширину (число колонок) матрицы. Это целые числа, но значения их не заданы, поскольку интерфейс определяет лишь, что есть у матрицы. Две операторных функции get
предназначены для определения содержимого определённой ячейки матрицы (для удобства, одна из них работает с двумя целочисленными параметрами, другая — с одним параметром-ячейкой). Результат обеих функций имеет тип E
(поскольку элементы имеют этот тип), но как он определяется — в интерфейсе опять-таки неизвестно.
Наконец, операторные функции set
предназначены для замены содержимого определённой ячейки матрицы. Их последний параметр содержит элемент, который нужно записать в заданную ячейку. Результат у данных функций отсутствует, но они меняют внутреннее состояние матрицы, то есть имеют побочный эффект. Вызывать set
можно как непосредственно, так и с помощью индексации: matrix[cell] = value
эквивалентно matrix.set(cell, value)
. Как get
, так и set
предполагаеют, что номер ряда лежит в диапазоне 0..height - 1
, а номер колонки в диапазоне 0..width - 1
.
Интерфейсы определяют новый тип или множество типов — в данном случае Matrix<E>
— но не имеют конструкторов. Имея только интерфейс, создать объект данного типа нельзя. Тем не менее, имея только интерфейс, можно определять различные операции над уже имеющимся объектом. Например, следующая функция меняет знак всех элементов целочисленной матрицы на обратный:
fun invertMatrix(matrix: Matrix<Int>) { for (row in 0 until matrix.height) { for (column in 0 until matrix.width) { matrix[row, column] = -matrix[row, column] } } }
Заметьте, что здесь мы ничего не знаем о внутреннем устройстве матрицы и никак не используем информацию о нём. Сам код достаточно тривиален и не требует особых объяснений. Оператор matrix[row, column] = -matrix[row, column
использует сразу обе операторные функции get
, set
и эквивалентен следующему: matrix.set(row, column, -matrix.get(row, column))
.
В примере, интерфейс включает в себя только свойства без значений и функции без реализаций. Это довольно частый случай, но не единственный. В примере ниже в том же интерфейсе присутствуют две функции с реализациями:
interface Matrix<E> { val height: Int val width: Int operator fun get(row: Int, column: Int): E operator fun get(cell: Cell) = get(cell.row, cell.column) operator fun set(row: Int, column: Int, value: E) operator fun set(cell: Cell, value: E) = set(cell.row, cell.column, value) }
Здесь в парах get
/ set
первая из функций объявлена без реализации, а вторая использует первую.
Таким образом, интерфейсы позволяют описывать операции обобщённо, не задумываясь о внутренностях того или иного объекта. Кроме этого, интерфейсы — удобное средство для описания спецификаций, они позволяют заранее договориться о том, что можно будет делать с тем или иным объектом. По этим причинам, интерфейсы широко используются в программировании.
Неискушённого читателя это может удивить, но типы List<T>
и MutableList<T>
, так же, как и Set<T>
, MutableSet<T>
, Collection<T>
, MutableCollection<T>
— все являются интерфейсами. В описании списков ничего не говорится о том, как именно эти списки работают. Например, существует две широко известные реализации интерфейса MutableList<T>
— а именно, ArrayList<T>
(список, реализованный на основе массива, в котором все элементы хранятся единым куском), и LinkedList<T>
(так называемый связанный список, в котором каждый элемент содержит ссылку на следующий и на предыдущий, при этом хранятся все элементы отдельно). Но, имея MutableList<T>
, мы не знаем, с какой конкретно из этих двух реализаций мы имеем дело, а в случае List<T>
, могут также присутствовать и дополнительные реализации для неизменяемого или пустого списка.