Перейти к содержанию

Java Core

Базовые темы Java для Android-разработки: равенство объектов, коллекции, обобщения, доступ, приведения типов и время жизни singleton.

Объекты и равенство

Класс Object

Object - корень иерархии классов Java. Обычные классы без явно указанного родителя наследуют его неявно; массивы тоже являются объектами. Примитивы не являются объектами, а интерфейсы не наследуют Object как суперкласс.

Полезные методы: toString(), equals(), hashCode() и getClass(). Метод Object.clone() выполняет поверхностное копирование и обычно требует Cloneable. Когда важно владение данными, предпочитай явное копирование.

Для wait(), notify() и notifyAll() поток должен владеть монитором объекта; см. Java Concurrency. Не полагайся на finalize() для освобождения ресурсов. Используй явное закрытие или try-with-resources; см. Java Exceptions.

Контракт equals() / hashCode()

Для ссылок Java == проверяет идентичность объектов. Стандартный Object.equals() делает то же самое; классы вроде String переопределяют его для сравнения значений. Для сравнения с учётом null используй Objects.equals(a, b). Содержимое массивов сравнивают через Arrays.equals() или Arrays.deepEquals().

Корректный equals() рефлексивен, симметричен, транзитивен и даёт согласованные результаты, пока значимое состояние не меняется; ненулевой объект не равен null.

Равные объекты обязаны иметь одинаковый хэш. У неравных объектов хэши могут совпадать. При определении равенства по значению реализуй equals() и hashCode() вместе, используя согласованный набор данных.

Класс как ключ HashMap

Переопределение этих методов нужно для собственного равенства по значению, а не для любого класса ключа: унаследованное сравнение по идентичности корректно, если именно оно требуется.

HashMap сужает поиск по хэшу, затем определяет подходящий ключ по идентичности или равенству. Пока ключ хранится в коллекции, состояние, участвующее в равенстве и вычислении хэша, должно оставаться стабильным. Иначе поиск или удаление могут не сработать даже с тем же объектом.

Предпочитай неизменяемые идентификаторы. Kotlin data class генерирует равенство и хэш по свойствам первичного конструктора, но изменяемые свойства или вложенные объекты всё равно могут сделать ключ ненадёжным.

Обобщения и коллекции

Обобщения и примитивы

Аргументами типов Java могут быть только ссылочные типы: List<Integer>, но не List<int>. Boxing преобразует примитив в обёртку, unboxing выполняет обратное преобразование. Распаковка null вызывает NullPointerException, а == между двумя обёртками сравнивает идентичность, а не числовое значение.

Стирание типов заменяет параметр типа его первой верхней границей или Object, если граница не задана; компилятор вставляет необходимые приведения. Обобщённые сигнатуры могут сохраняться в метаданных, но обычный список не несёт проверяемый во время выполнения аргумент типа элементов.

List<Integer> не является подтипом List<Number>. Wildcards задают допустимое использование:

  • List<? extends Number>: можно читать элементы как Number, но нельзя безопасно добавить ненулевой элемент.
  • List<? super Integer>: можно добавлять Integer, но результат чтения имеет тип Object.

Избегай сырых типов и непроверяемых приведений. Вариантность и стирание подробнее описаны в Generics.

Коллекции обёрток могут потреблять больше памяти и создавать больше объектов, чем int[] или специализированные структуры. Boxing не обязательно создаёт новый объект каждый раз; оптимизируй участки, значимость которых подтверждена измерениями.

Iterator и Iterable

Iterable.iterator() создаёт итератор; hasNext() проверяет наличие элемента, а next() возвращает его либо выбрасывает NoSuchElementException, если элементы закончились. Цикл Java for-each поддерживает и Iterable, и массивы; сами массивы не реализуют Iterable.

Если итератор поддерживает удаление, вызывай remove() после next(), не более одного раза на возвращённый элемент. Иначе возможны UnsupportedOperationException или IllegalStateException.

Структурные изменения в обход обычного fail-fast итератора могут вызвать ConcurrentModificationException даже в одном потоке. Обнаружение не гарантировано и не обеспечивает потокобезопасность. У конкурентных коллекций собственные правила обхода.

Интерфейсы и реализации коллекций

Интерфейс Назначение Типичная реализация
List Индексированная последовательность, повторы разрешены ArrayList
Set Уникальные элементы HashSet
Queue Очередь обработки; порядок зависит от реализации PriorityQueue
Deque Очередь с операциями на обоих концах; расширяет Queue ArrayDeque
Map Уникальные ключи и соответствующие значения; не является Collection HashMap

Collection расширяет Iterable; Map предоставляет представления коллекций через keySet(), values() и entrySet().

HashMap не гарантирует порядок обхода и обеспечивает ожидаемое постоянное время базового поиска при хорошем распределении хэшей. LinkedHashMap обычно сохраняет порядок вставки и может использовать порядок доступа. TreeMap сортирует ключи по естественному порядку или компаратору и обеспечивает логарифмический поиск; нулевой результат сравнения определяет эквивалентность ключей.

Коллекция с запрещёнными изменениями не обязательно является неизменяемым снимком: обёртка может отражать изменения исходной коллекции. См. Collections.

Типы и доступ

Модификаторы доступа

Для членов класса:

  • public: доступ везде, где доступен объявляющий тип, с учётом границ модулей, если они применимы.
  • private: доступ внутри тела охватывающего класса верхнего уровня, включая его вложенные классы.
  • Без модификатора: package-private, доступ в том же пакете. Подпакеты являются отдельными пакетами.
  • protected: доступ в том же пакете и по правилам доступа из подклассов за его пределами.

Вне пакета подкласс не может обратиться к защищённому члену экземпляра через произвольный экземпляр родителя: ссылка должна иметь тип этого подкласса или его потомка. Классы верхнего уровня допускают public или package-private, но не private и не protected. У членов интерфейса другие неявные модификаторы.

Проверка и приведение типов: instanceof

instanceof проверяет совместимость типов во время выполнения и возвращает false для null. Приведение меняет тип ссылки, известный компилятору, а не сам объект. Несовместимое приведение ссылочного типа вызывает ClassCastException; приведение null к ссылочному типу даёт null.

static void printText(Object value) {
    if (value instanceof String) {
        String text = (String) value;
        System.out.println(text.length());
    }
}

Если инструменты сборки и уровень языка поддерживают сопоставление с образцом:

static void printText(Object value) {
    if (value instanceof String text) {
        System.out.println(text.length());
    }
}

В Android различай JDK, запускающий Gradle, настроенный уровень языка Java и доступные API среды выполнения. Установка нового JDK сама по себе не включает все возможности языка и библиотек. Предпочитай полиморфизм, если повторные проверки типов дублируют поведение подклассов.

Реализация и время жизни singleton

public final class MySingleton {
    private static final MySingleton INSTANCE = new MySingleton();

    private MySingleton() {}

    public static MySingleton getInstance() {
        return INSTANCE;
    }
}

Экземпляр создаётся при инициализации класса, не обязательно при запуске приложения. Гарантии инициализации обеспечивают безопасную публикацию объекта, но не потокобезопасность последующих изменений. Вложенный holder-класс позволяет отложить создание до вызова метода доступа. Другой вариант - enum; double-checked locking требует volatile и корректной синхронизации.

Статический экземпляр принадлежит конкретному загруженному классу и не является общим для всех процессов приложения. После смерти Android-процесса его состояние теряется. Не удерживай Activity или View в долгоживущих объектах; внедрение зависимостей помогает явно задать владельца и упростить замену в тестах. См. DI Basics.

Источники