Сравнение объектов класса на равенство
Как нам уже известно, на равенство ==
можно сравнивать не только числа, но и переменные более сложных типов, например, строки или списки. Часто необходимо уметь сравнить на равенство переменные с типом, определённым пользователем в виде класса. Например, в группе задач 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) имеет три свойства a
, b
и 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
), поскольку проверка этого факта была уже сделана.