8. Основы Kotlin. Простые классы

Сравнение объектов класса на равенство

Как нам уже известно, на равенство == можно сравнивать не только числа, но и переменные более сложных типов, например, строки или списки. Часто необходимо уметь сравнить на равенство переменные с типом, определённым пользователем в виде класса. Например, в группе задач lesson8.task2 про шахматную доску и фигуры имеется класс Square, описывающий одну клетку шахматной доски. В наиболее простой форме он выглядел бы так:

class Square(val column: Int, val row: Int)

Проверим, что будет, если сравнить две одинаковых клетки first и second на равенство:

fun main(args: Array<String>) {
    val first = Square(3, 6)
    val second = Square(3, 6)
    println(first == second)
}

Если запустить эту главную функцию, мы увидим на консоли результат false. Почему?

Всё дело в способе работы, принятом в JVM для любых объектов. Каждый раз, когда мы вызываем конструктор какого-либо класса, в динамической памяти JVM создаётся объект этого класса. Ссылка на него запоминается в стеке JVM (подробности будут в разделе 4.5). По умолчанию, при сравнении объектов на равенство сравниваются друг с другом ссылки, а не содержимое объектов.

Немного изменим определение класса Square, добавив впереди модификатор data. Такое определение обычно читается как «класс с данными».

data class Square(val column: Int, val row: Int)

Запустив главную функцию ещё раз, мы увидим результат true. При наличии модификатора data, для объектов класса работает другой способ сравнения на равенство: все свойства первого объекта сравниваются с соответствующими свойствами второго. Поскольку для обоих объектов column = 3 и row = 6, данные объекты равны.

Помимо этой возможности, классы с данными позволяют представить объект в виде строки, например:

fun main(args: Array<String>) {
    val first = Square(3, 6)
    println(first)
}

Эта функция выведет на консоль Square(x=3, y=6). Попробуйте теперь убрать модификатор data в определении класса и посмотрите, как изменится вывод. Заметим, что строковое представление используется не только при выводе на консоль, но и в отладчике.

Каким же образом осуществляется переопределение способа сравнения объектов и способа их представления в виде строки? Для этой цели в Java придуманы две специальные функции. Первая из них называется equals, она имеет объект-получатель, принимает ещё один объект как параметр и выдаёт результат true, если эти два объекта равны. Чуть ниже приведён пример переопределения equals для класса Segment.

Вторая функция называется toString. Она также имеет объект-получатель, но не имеет параметров. Её результат — это строковое представление объекта. Например:

class Square(val column: Int, val row: Int) {
    override fun toString() = "$row - $column"
}

Запустив главную функцию выше, мы увидим на консоли строку 6 - 3. Обратите внимание на модификатор overrideперед определением toString(). Он указывает на тот факт, что данная функция переопределяет строковое представление по умолчанию. Подробнее об этом опять-таки в разделе 9.

О других возможностях классов с данными можно прочитать здесь: https://kotlinlang.org/docs/reference/data-classes.html.

Включение классов

Система классов была бы очень неполноценной, если бы нам приходилось использовать классы сами по себе, в отрыве друг от друга. Поэтому у классов есть множество способов взаимодействовать друг с другом. Самый простой из них — включение объекта одного класса внутрь другого класса. Например:

data class Triangle(val a: Point, val b: Point, val c: Point) {
    // ...
}
data class Segment(val begin: Point, val end: Point) {
    // ...
}

Здесь треугольник (Triangle) имеет три свойства ab и c, каждое из которых, в свою очередь, имеет тип Point — точка. В таких случаях говорят, что треугольник включает три точки, состоит из трёх точек или описывается тремя точками. Отрезок (Segment) имеет два таких же свойства begin и end — то есть описывается своим началом и концом.

Точки, в свою очередь, описываются двумя вещественными координатами. Например:

fun main(args: Array<String>) {
    val t = Triangle(Point(0.0, 0.0), Point(3.0, 0.0), Point(0.0, 4.0))
    println(t.b.x) // 3.0
}

При вызове println мы прочитали свойство x свойстваb треугольника t. Для этого мы дважды использовали точку для обращения к свойству объекта.

Переопределение equals для класса

Рассмотрим пример переопределения equals для класса Segment. Дело в том, что для отрезка, вообще говоря, всё равно, в каком порядке в нём идут начало и конец, то есть отрезок AB равен отрезку BA. Применение способа сравнения на равенство, действующего для классов с данными по умолчанию, даст нам другой результат: AB не равно BA.

data class Segment(val begin: Point, val end: Point) {

    override fun equals(other: Any?) =
            other is Segment && ((begin == other.begin && end == other.end) ||
                                 (begin == other.end && end == other.begin))
}

Модификатор override перед определением equals говорит о том, что мы хотим изменить уже имеющийся метод сравнения на равенство. Единственный параметр other данного метода обязан иметь тип Any?, то есть «любой, в том числе null». В Котлине действует правило: абсолютно любой тип является разновидностью Any?, то есть значение любой переменной или константы можно использовать как значение типа Any?. Это обеспечивает возможность сравнения на равенство чего угодно с чем угодно.

Результат equals имеет тип Boolean. В первую очередь, мы должны проверить, что переданный нам аргумент — тоже отрезок: other is Segment. Ключевое слово is в Котлине служит для определения принадлежности значения к заданному типу. Аналогично !is делает проверку на не принадлежность.

Если аргумент — отрезок, мы сравниваем точки двух имеющихся отрезков на равенство, с точностью до их перестановки. Если же аргумент — не отрезок, то логическое И в любом случае даст результат false. Обратите внимание, что справа от && мы вправе использовать other как отрезок (например, используя его begin и end), поскольку проверка этого факта была уже сделана.

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