1. Контракты методов Object, нюансы String Pool и мифы о передаче параметров
Контракты методов Object, нюансы String Pool и мифы о передаче параметров
Добро пожаловать на курс Java Core: Edge Cases и подготовка к сложным техническим собеседованиям. Мы начинаем не с синтаксиса циклов, а с фундамента, на котором ломаются даже опытные разработчики. В этой статье мы разберем, как на самом деле работают базовые механизмы Java, о которых часто забывают или знают лишь поверхностно.
На собеседованиях уровня Senior вас не спросят, как написать equals. Вас спросят, как сломать HashMap, изменив поле ключа, или почему String.intern() может положить приложение.
Контракты методов Object: equals и hashCode
Казалось бы, методы equals() и hashCode() — это азбука. Однако именно здесь кроется множество edge cases (граничных случаев), которые приводят к трудноуловимым багам.
Контракт equals()
Метод equals должен обладать следующими свойствами:
a.equals(b), то и b.equals(a).a равно b, а b равно c, то a должно быть равно c.x.equals(null) всегда должно возвращать false.Edge Case: Нарушение симметрии при наследовании
Классическая ловушка на собеседовании: как правильно реализовать equals в иерархии наследования?
Представьте класс Point (точка) и его наследника ColorPoint (цветная точка). Если Point сравнивает только координаты, а ColorPoint добавляет сравнение цвета, мы легко нарушаем симметрию.
В этом случае point.equals(colorPoint) вернет true (так как цвет игнорируется в родителе), а colorPoint.equals(point) вернет false (так как ожидается ColorPoint).
Решение: Использовать getClass() вместо instanceof, если важна точная проверка типа, либо объявить метод equals как final в родительском классе, если наследование не должно менять логику равенства. Использование instanceof допустимо, только если иерархия спроектирована с учетом принципа подстановки Лисков (Liskov Substitution Principle), что для equals часто означает запрет на добавление значимых полей в подклассах.
Контракт hashCode() и потерянные данные
Контракт гласит: если объекты равны по equals, у них должен быть одинаковый hashCode. Обратное неверно (коллизия).
Самый опасный edge case здесь — использование изменяемых (mutable) полей в расчете хеш-кода.
Рассмотрим сценарий:
User с полем id. hashCode зависит от id.HashSet или HashMap.id у объекта.Результат: contains вернет false. Объект физически находится в коллекции, но в другой "корзине" (bucket), соответствующей старому хешу. Это приводит к утечкам памяти (объект нельзя удалить стандартным способом) и логическим ошибкам.
Вывод для собеседования: Всегда используйте неизменяемые (immutable) объекты в качестве ключей HashMap.
String Pool: Глубже, чем кажется
Строки в Java — это особые объекты. Для оптимизации памяти JVM использует String Pool (пул строк). До Java 7 он находился в PermGen, начиная с Java 7 — в Heap (куче), что спасло нас от частых OutOfMemoryError: PermGen space.
Литералы vs new String()
Разберем классический вопрос: сколько объектов создается в коде?
Ответ: Два (при условии, что "hello" еще нет в пуле).
"hello" создается и помещается в String Pool (если его там нет).new принудительно создает новый объект в куче (Heap), копируя содержимое из пула.Если вы напишете:
Здесь s1 и s2 ссылаются на один и тот же объект в пуле. s1 == s2 вернет true.
Метод intern()
Метод intern() возвращает каноническое представление строки. Если строка уже есть в пуле, возвращается ссылка на нее. Если нет — строка добавляется в пул, и возвращается ссылка на нее.
Edge Case: Злоупотребление intern().
Хотя пул строк находится в куче, он представляет собой хеш-таблицу фиксированного размера (до Java 11 размер настраивался сложно, сейчас динамичнее, но все же). Если вы начнете интернировать миллионы уникальных строк (например, ID транзакций), вы столкнетесь с:
Дедупликация строк (G1 GC)
Начиная с Java 8 update 20, сборщик мусора G1 умеет делать String Deduplication. Это не то же самое, что интернирование. GC находит в куче разные объекты String, у которых одинаковые массивы байт (значения), и перенаправляет их внутренние ссылки на один общий массив. Сами объекты String остаются разными (== вернет false), но потребление памяти снижается.
Мифы о передаче параметров в Java
Один из самых устойчивых мифов: "Примитивы передаются по значению, а объекты — по ссылке".
Это ложь. В Java всё передается по значению (Pass-by-Value).
Что такое "значение" для объекта?
Когда вы передаете объект в метод, вы передаете не сам объект и не ссылку на память в стиле C++. Вы передаете копию ссылки (битовую копию адреса).
!Визуализация передачи ссылки по значению: копируется адрес, а не сам объект.
Доказательство через Edge Case
Если бы Java передавала объекты по ссылке (Pass-by-Reference), мы могли бы изменить, на какой объект указывает переменная из вызывающего кода. Но мы не можем.
Рассмотрим код:
В методе swap мы поменяли местами копии ссылок a и b. Оригинальные ссылки x и y в методе main остались указывать на свои старые объекты.
Однако, поскольку копия ссылки указывает на тот же самый объект в куче, мы можем менять состояние этого объекта:
Итог по передаче параметров
* Передача примитива: копируются биты значения (например, число 5).
* Передача объекта: копируются биты адреса (например, 0x5F3E).
* Переназначение аргумента внутри метода (arg = new ...) никогда не влияет на внешнюю переменную.
* Изменение полей аргумента (arg.setField(...)) влияет на объект, так как копия адреса ведет к тому же участку памяти.
Заключение
Понимание контрактов Object, работы String Pool и механизма передачи параметров отличает инженера, который пишет код "методом тыка", от инженера, который прогнозирует поведение системы. В следующей статье мы углубимся в дебри Java Collections Framework и разберем, почему ConcurrentModificationException возникает даже в однопоточной среде.