Что такое статический импорт?

Если мы хотим использовать статическую переменную другого класса, как правило, мы импортируем сразу весь класс

import java.lang.Math;
 
// внутри класса
double test = Math.PI * 5;

Мы можем сделать то же самое, импортируя только нужную константу или метод

import static java.lang.Math.PI;
 
// обращаемся напрямую
double test = PI * 5;

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

Что такое интерфейс-маркер?

Интерфейс-маркер – это пустой интерфейс (без методов), который используется чтобы добавить в класс некоторую функциональность. Он используется как признак наличия у класса какого-то свойства. Примером такого интерфейса может быть Serializable или Cloneable.

Что такое тернарный оператор?

Это единственный условный оператор с тремя операндами. Он работает следующим образом: проверяется значение первого операнда, который является логическим выражением, и если оно правдивое – оператор возвращает значение второго операнда, если нет – третьего. Выглядит это так:

operator1 ? operator2 : operator3;

Напишем пример оператора, который возвращает абсолютное значение числа x.

x = (x > 0) ? x : 0;

Переопределение метода hashCode

Если вы переопределили метод equals – вы должны переопределить и метод hashCode. Иначе класс будет некорректно работать с использующими hashCode коллекциями (HashMap, HashSet, Hashtable).

Ваш hashCode должен выполнять следующие требования:

  • Должен каждый раз возвращать одно число для неизменяемого объекта;
  • Если два объекта равны согласно методу equals, то и hashCode у нах должен совпадать;
  • Если два объекта НЕ равны, то hashCode может совпадать, но для высокой производительности работы коллекций желательно чтобы такое встречалось как можно реже;

Пример метода

@Override public int hashCode() {
    	int result = hashCode;
    	if (result == 0) {
        	result = 17;
        	result = 31 * result + (var1?1:0); // boolean
        	result = 31 * result + var2; // integer
        	result = 31 * result + var3.hashCode(); // custom object
        	hashCode = result;
    	}
    	return result;
}

В подсчете hashCode должны участвовать ВСЕ и ТОЛЬКО те поля, которые используются в equals.

Переопределяем equals правильно

Проще всего избежать проблем с переопределением equals не переопределяя его вовсе. По умолчанию для Object equals сравнивает адреса в памяти.

Когда не нужно переопределять equals?

Каждый экземпляр класса уникальный. Например, у класса Thread все объекты разные.

Особенность класса такова, что метод ему equals не нужен. Например в классе Random, генерирующем числа, он не нужен.

Если метод уже переопределен в родительском классе. В классе Set метод переопределен и поэтому в AbstractSet нет нужды менять логику его работы.

Если класс приватный и вы уверенны что equal не будет вызван. На всякий случай можно дополнительно бросать исключение внутри этого метода, если его все-таки вызовут.

Если тип класса – Enum или другой immutable класс, который пользуется статическим методом вместо конструктора чтобы не создавать одинаковые сущности.

Требования при переопределении equals

Для любых не-null объектов x, y, z:

  • Рефлексивность – x.equals(x) = true
  • Симметричность – x.equals(y) = y.equals(x)
  • Транзитивность – если x.equals(y) = true и y.equals(z) = true ТО x.equals(z) = true
  • Постоянность – результат x.equals(y) должен быть постоянным для всех вызовов equals, при условии что x и y не менялись в промежутках между вызовами
  • x.equals(null) = false

Важно

  • Переопределять нужно именно equals(Object o), а не equals(MyClass o), иначе это будет просто перегрузка;
  • После переопределения equals обязательно(!) переопределите hashCode;

В чем разница между String, StringBuilder, StringBuffer?

String – final и immutable класс для работы со строками. Это значит:

  • его нельзя наследовать;
  • при модификации создается новый объект;

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

StringBuilder – работает быстрее StringBuffer, но не потокобезопасный.

StringBuffer – позволяет работу в многопоточность среде за счет снижения производительности.

Реализуйте паттерн Singleton

Это задание мне давали более на чем половине собеседований. Это, наверное, самый популярный вопрос у рекрутеров. Вот простая реализация потокобезопастного Singleton’a.

public class Singleton {
    private static Singleton instance;
    
    private Singleton(){}

    public static synchronized Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

Какими способами можно создать String?

String s1 = new String("abc");
String s2 = "abc";

В случае использование оператора new создается новый объект типа String и в s1 возвращается ссылка на него.

Для второго случая сначала произойдет проверка на наличие уже существующей строки “а” в String Pool. Если такая строки будет найдена – возвращается ссылка на нее. Если нет – создается новая. Аналогично будет работать “аbc”.intern();

Что такое JVM, JRE, JDK?

JVM – Java Virtual Machine, виртуальная машина java, которая отвечает за конвертацию байт-кода в машинный код. Для каждой ОС существует своя версия JVM, что позволяет создать единственный механизм взаимодействия для всех ОС.

JRE – Java Runtime Environment, среда исполнения. Содержит в себе JVM и дополнительно некоторые системные библиотеки, которые иногда импортируются разработчиками в их приложениях.

JDK – Java Development Kit, инструмент разработчика. Он содержит JRE и плюс необходимые для разработки компилятор, дебаггер, которые позволяют создавать свои приложения.

Избегайте finalize методов

Почему их нужно избегать?

Они опасны, непредсказуемы, и часто в них нет необходимости. Они понижают производительность и могут вызывать проблемы при портировании.

Finalize метод – не аналог деструктора в C++, где освобождение ресурсов в деструкторе – нормальная практика. В Java этим занимается garbage collector вместе с finally блоком try-catch оператора.

Не совершайте в finalize действий: касающихся целостности данных, зависящих от времени, освобождающих ресурсы. Garbage collector не гарантирует вызов finalize даже если вызвать System.gc и System.runFinalization.

Что делать?

Писать свой метод для удаления объекта. Вызывать его в try-catch блоке класса-клиента.

Для чего ж тогда использовать finalize?

В finalize можно делать проверку: были ли освобождены все ресурсы и/или вызван пользовательский класс удаления. Если нет – писать предупреждения в лог.

Еще одна особенность метода: он не вызывает автоматически finalize метод родительского класса, это нужно делать вручную (super.finalize).