Методы класса Object

Методы класса Object

Дорогу осилит идущий




ВАЖНО: ниже находится устаревшая версия статьи. Актуальную можно найти здесь: ссылка






Сегодня мы познакомимся с содержимым класса Object, а именно, с методами, которые он содержит. Не все из них действительно востребованы, часть методов мы пока не сможем применить, потому что не знакомы с концепциями, для которых они нужны (например, несколько методов связаны с многопоточностью). Соответственно, периодически мы будем возвращаться к этой теме в дальнейшем.

Для начала, ознакомимся со статьей: https://metanit.com/java/tutorial/3.9.php

Кроме представленных в ней методов есть также:

  1. Три перегруженных метода wait(). Относятся к разработке многопоточных приложений. В явном виде обычно не используются. Указывают потоку на необходимость ожидать (в течении какого-то времени или до вызова notify()/notifyAll());
  2. Методы notify() и notifyAll(). Относятся к разработке многопоточных приложений. В явном виде обычно не используются. Вкратце, необходимы для оповещения произвольного потока (или всех потоков), ожидающих доступ к объекту, занятому другим потоком, о том, что объект доступен;
  3. Метод finalize(). Помечен аннотацией @Deprecated — устарел. Не рекомендуется к использованию. Задумывался для очистки ресурсов и, в принципе, описания действий, которые необходимо выполнить перед удалением объекта из памяти. В явном виде его вызов недопустим. Очистка памяти в Java полностью автоматизирована;
  4. Метод clone(). Необходим для клонирования (создания нового объекта с теми же значениями полей) объектов. В некотором смысле заменяет конструктор копирования (если кто-то с ним знаком в других языках). Практически не используется на практике, но в отдельном уроке мы рассмотрим тему клонирования и его виды. Для переопределения clone() необходимо в классе реализовать маркерный интерфейс Cloneable.

Также стоит сказать несколько слов о методе toString(), описанном в статье выше. Несмотря на кажущуюся полезность, он редко используется в реальных задачах. При необходимости конвертировать объект в строку (обычно, для создания JSON-объектов, с ними мы еще познакомимся) используются сторонние библиотеки. Таким образом, этот метод оказался на обочине жизни, как и многие другие. Но в рамках практических задач мы можем его использовать по мере необходимости.

Учтите, что метод toString() вызывается внутри методов print(), printf(), println(), если в них передать объект. Также при конкатенации строк через «+» для не строковых ссылочных типов также происходит неявный вызов toString().

Я рекомендую ознакомиться с документацией по Object самостоятельно, для закрепления информации. Для разных версий JDK описание может незначительно отличаться. Ссылка для Java 17: https://docs.oracle.com/en/java/javase/17/docs/api/index.html

Также документация доступна в виде Java-doc в IDEA, достаточно открыть класс Object.

Прежде чем мы продолжим, советую обратить внимание на ключевое слово native, оно используется по отношению к некоторым методам в Object (и не только). Означает, что метод был реализован на языке, отличном от Java. Мы можем такой метод использовать, но просмотр тела метода нам недоступен.


Правила переопределения equals()

Метод equals() — один из наиболее используемых методов Object. Но в базовой реализации он сравнивает ссылки на объекты. Т.е. true он вернет только если сравнивать две переменные, которые ссылаются на один и тот же объект.

Чтобы использовать equals() для настоящего сравнения объектов, его необходимо переопределить.

Правила переопределения основаны на контракте equals() — договоренности, какое поведение ожидается от этого метода.

Контракт определяет следующие характеристики:

  1. Рефлексивность. Иными словами, x.equals(x) == true;
  2. Симметричность: если x.equals(y), то и y.equals(x);
  3. Переносимость: если x.equals(y) и y.equals(z), то и x.equals(z);
  4. Консистентность: повторное выполнение x.equals(y) без изменения состояния (полей) объектов x и y должно возвращать один и тот же результат;
  5. Сравнение с NULL: x.equals(null) == false.

Таким образом, классический переопределенный equals выглядит примерно так:

public boolean equals(Object o) {
  if (this == o) { //Гарантируем рефлексивность
    return true;
  }
  if (o == null) {
    return false;
  }
  if (!getClass().equals(o.getClass())) { //Возможны вариации. Использование instanceof или сравнение типа параметра с явно вызванным литералом класса: o.getClass().equals(SthClass.class)
  return false;
  }

  SthClass sthClass = (SthClass) o; //К этой строке мы уже уверены, что тип верный и можно кастить. При использовании instanceof это можно описать немного лакончинее

  //Ниже будет сравнение по полям. Если значение по всем проверяемым полям совпадают возвращаем из метода true, если хоть в одном поле значения отличаются - false

}

При проверке типа в equals() обычно используют проверку типа через getClass(). Но если в рамках вашей задачи допустимо сравнение с объектами наследников — допустимо использовать instanceof. Это не слишком частый, но возможный сценарий.

При выборе полей для сравнения, стоит также руководствоваться здравым смыслом. Конечно, можно реализовать сравнение по всем полям. Но если в рамках задачи (или логики сущности в целом) некоторые поля не являются ключевыми — вполне логично их опустить в equals().


Правила переопределения hashcode()

Хэшкод возвращает «хэш» объекта. Иными словами, некоторое число, рассчитанное на основании значения полей.

Метод возвращает int, соответственно, количество уникальных значений хэшкода ограничено и он может совпадать у различных по значениям полей объектов. Совпадение хэшей (хэшкодов) при разных входных данных называется коллизией.

Поведение по умолчанию у hashcode() зависит от настроек JVM. В любом случае, если мы хотим его использовать (например, hashcode() используется во многих коллекциях), мы обязаны его переопределить. Переопределенный хэшкод выглядит примерно так:

public int hashCode() {
  int result = field1 != null ? field1.hashCode() : 0;
  result = 31 * result + ( field2 != null ? field2.hashCode() : 0);
  ...
  return result;
}

Алгоритмы расчета хэшкода могут отличаться, это не критично на данном этапе. В рамках этого алгоритма мы можем увидеть умножение на 31. Оно необходимо для более равномерного распределения значений по множеству int.

При выборе полей для расчета hashcode() необходимо брать те же поля, что и для equals(). Подробнее ниже.


Контракт equals() и hashcode()

Эти два метода зачастую нужны вместе (например, во многих коллекциях), поэтому существует определенный контракт их взаимодействия:

  1. Переопределяя один метод, необходимо переопределить второй;
  2. Равенство объектов по equals() гарантирует равенство их хэшкодов;
  3. Равенство хэшкодов не гарантирует равенства объектов по equals().

Последний пункт не очень удачен с точки зрения формулировки контракта, но распространен в сообществе. Его также можно заменить на: неравенство хэшкодов гарантирует неравенство объектов по equals().

Именно для соблюдения контракта equals() и hashcode() возникает необходимость использовать при их переопределении одни и те же поля.


Вопросы о методах класса Object и рассмотренных выше контрактах часто задают на собеседованиях для junior-специалистов. А иногда и не только им:)


С теорией на сегодня все!


Приступаем к практике:

Задача:

Реализуйте класс «Машина». Поля допустимо выбрать на свое усмотрение, но необходимо, чтобы по ним можно было однозначно идентифицировать каждую машину. Скажем, в рамках базы ГАИ.

Создайте массив машин. Реализуйте максимально эффективную проверку на вхождение машины в ваш массив. Данные для проверки необходимо запрашивать с клавиатуры.

Если машина найдена — выведите ее строковое представление в консоль.


Опциональное усложнение: номер машины может быть не уникальным.


Если что-то непонятно или не получается – welcome в комменты к посту или в лс:)

Канал: https://t.me/+relA0-qlUYAxZjI6

Мой тг: https://t.me/ironicMotherfucker

 

Дорогу осилит идущий!

Report Page