Класс Object в Java
Методы, которые каждый объект Java наследует от java.lang.Object — toString, equals, hashCode, getClass, clone и finalize.
Каждый класс в Java — независимо от того, пишете ли вы extends Object или нет — находится в иерархии типов под java.lang.Object. Это означает, что каждая ссылка, которую вы когда-либо держите, имеет небольшой набор доступных методов: toString, equals, hashCode, getClass и ещё несколько. Понимание того, что они делают по умолчанию и когда их следует переопределять, — это разница между объектами, которые нормально работают с коллекциями, отладчиками и фреймворками, и объектами, которые не работают.
Эта глава — карта территории. Следующие несколько глав подробно рассматривают конкретные методы (equals/hashCode, toString, clone) по одному за раз.
Неявное расширение
Напишите класс без клаузы extends:
public class Box {
int contents;
}Компилятор обрабатывает это так, как будто вы написали public class Box extends Object. Именно поэтому вы можете передать Box в любое место, объявленное как Object — каждый ссылочный тип в конечном итоге является Object.
Краткий обзор унаследованных методов
| Метод | Что делает по умолчанию |
|---|---|
toString() | Возвращает ClassName@hashCodeHex — почти никогда не то, что нужно |
equals(Object) | Равенство ссылок (==) |
hashCode() | Значение, производное от идентичности объекта, а не его содержимого |
getClass() | Токен времени выполнения Class<?> — никогда не null |
clone() | Поверхностная копия поле за полем, но только для классов, реализующих Cloneable; иначе выбрасывает исключение |
wait(), notify(), notifyAll() | Низкоуровневые примитивы монитора, используемые с synchronized |
finalize() | Хук, вызываемый сборщиком мусора перед уничтожением объекта — устарел, не использовать |
Первые три — toString, equals и hashCode — это те, которые почти каждый класс, несущий значения, должен переопределять, поскольку унаследованные версии описывают идентичность объекта, а не его содержимое. getClass вы вызываете на объектах, но никогда не переопределяете. Примитивы работы с потоками вы редко трогаете напрямую. clone и finalize лучше избегать в современном коде (clone рассматривается в отдельной главе; finalize заменяется try-with-resources и Cleaner).
record (Java 16+), компилятор генерирует toString, equals и hashCode за вас из компонентов — все три основаны на содержимом, а не на идентичности. Это устраняет большую часть шаблонного кода, о котором рассказывает эта глава. См. Java Records.toString — значение по умолчанию бесполезно
Унаследованный toString возвращает что-то вроде Box@1540e19d. Это имя класса и идентификационный хеш-код в шестнадцатеричном виде — бесполезно для логирования, отладки или любой другой диагностики:
Box b = new Box();
System.out.println(b); // Box@1540e19dПереопределите его, чтобы возвращать что-то читаемое — println, конкатенация строк, фреймворки логирования и отладчики IDE — все они вызывают toString за вас, поэтому хорошая реализация окупается повсюду:
@Override
public String toString() {
return "Box[contents=" + contents + "]";
}Следующая глава подробно рассматривает, как сделать это правильно.
equals и hashCode — они неразрывно связаны
Унаследованный equals возвращает true только тогда, когда обе ссылки указывают на один и тот же объект:
String a = new String("hi");
String b = new String("hi");
System.out.println(a == b); // false — different objects
System.out.println(a.equals(b)); // true — String overrides equalsString переопределяет equals для сравнения содержимого. Класс, который вы пишете, этого не делает, если только вы сами не переопределите его. И как только вы переопределяете equals, вы должны также переопределить hashCode — иначе ваши объекты будут незаметно некорректно работать в HashSet и HashMap. Глава о equals и hashCode охватывает точные правила.
equals без переопределения hashCode, и два объекта, которые вы считаете равными, могут оказаться в разных хеш-бакетах — поэтому HashSet с радостью хранит оба, а map.get(key) возвращает null для ключа, который явно присутствует. Всегда переопределяйте их вместе и сохраняйте согласованность: равные объекты должны возвращать равные хеш-коды.getClass — токен типа времени выполнения
getClass() возвращает объект Class<?>, описывающий тип времени выполнения:
Object o = "hello";
System.out.println(o.getClass().getName()); // java.lang.StringОн используется в рефлективном коде, реализациями equals, которым нужны сравнения точных типов, и в отладочном выводе. Обратите внимание, что getClass() является final — его нельзя переопределить.
clone и Cloneable
Object.clone() является protected и работает только на классах, которые явно объявляют implements Cloneable. Результатом является поверхностная копия: новый экземпляр с теми же значениями полей, включая те же ссылки на изменяемые подобъекты. Это заведомо неудобный API, подробно рассматриваемый в главе о клонировании.
wait / notify
Эти методы, вместе с notifyAll, являются частью оригинальных низкоуровневых примитивов параллелизма Java. Они привязаны к блокам synchronized и используются для построения паттернов ожидания условия. В современном коде предпочитайте высокоуровневые утилиты в java.util.concurrent (блокировки, BlockingQueue, CompletableFuture); необработанный wait/notify редко встречается за пределами внутренних компонентов библиотек.
finalize — не трогайте его
finalize() когда-то был способом, которым сборщик мусора давал объекту последний шанс освободить ресурсы. Он устарел начиная с Java 9 и удалён в более новых версиях. Используйте try-with-resources для ввода-вывода и java.lang.ref.Cleaner в очень редких случаях, когда необходимо выполнить код после сборки мусора.
Рабочий пример
Что дальше
Наиболее распространённое — и наиболее подверженное ошибкам — переопределение метода Object — это equals в паре с hashCode. Следующая глава рассматривает контракт, который оба метода должны соблюдать, и паттерны для правильной реализации. Перейдите к Java equals и hashCode.