Ответы на вопросы на собеседование Java core (часть 2).

  • Возможно ли при переопределении (override) метода изменить: 

    1. Модификатор доступа
    2. Возвращаемый тип
    3. Тип аргумента или количество
    4. Имя аргументов
    5. Изменять порядок, количество или вовсе убрать секцию throws?
  1. Да, если расширять (package -> protected -> public)
  2. Да, если выполняется Downcasting(понижающее преобразование, преобразование вниз по иерархии) то есть возвращаемый тип в переопределенном методе класса наследника должен быть НЕ шире чем в классе родителе (Object -> Number -> Integer)
  3. Нет, в таком случае происходит Overload(перегрузка)
  4. Да
  5. Возможно изменять порядок. Возможно вовсе убрать секцию throws в методе, так как она уже определена. Так же возможно добавлять новые исключения, которые наследуются от объявленных или исключения времени выполнения.
Переопределение методов действует при наследовании классов, т.е. в классе наследнике объявлен метод с такой же сигнатурой что и в классе родителе. Значит этот метод переопределил метод своего суперкласса.
Несколько нюансов по этому поводу:
  • Модификатор доступа в методе класса наследника должен быть НЕ уже чем в классе родителе, иначе будет ошибка компиляции.
  • Описание исключения в переопределенном методе класса наследника должен быть НЕ шире чем в классе родителе, иначе ошибка компиляции.
  • Метод обьявленный как "private" в классе родителе нельзя переопределить!

  • Что такое autoboxing?

Autoboxing/Unboxing - автоматическое преобразование между скалярными типами Java и соответствующими типами-врапперами (например, между int - Integer). Наличие такой возможности сокращает код, поскольку исключает необходимость выполнения явных преобразований типов в очевидных случаях.

  • Что такое Generics?

"Java Generics" - это технический термин, обозначающий набор свойств языка позволяющих определять и использовать обобщенные типы и методы. Обобщенные типы или методы отличаются от обычных тем, что имеют типизированные параметры. 
Примером дженериков или обобщенных типов может служить библитека с коллекциями в Java. Например, класс LinkedList<E> - типичный обобщенный тип. Он содержит параметр E, который представляет тип элементов, которые будут храниться в коллекции. Вместо того, чтобы просто использовать LinkedList, ничего не говоря о типе элемента в списке, мы можем использовать LinkedList<String> или LinkedList<Integer>.  Создание объектов обобщенных типов происходит посредством замены параметризированных типов реальными типами данных. Класс типа LinkedList<E> -  обобщенный тип, который содержит параметр E. Создание объектов, типа LinkedList<String> или LinkedList<Integer> называются параметризированными типами, а String и Integer - реальные типы аргументов. 

  • Какова истинная цель использования обобщенных типов в Java? 

Обобщенные типы в Java были изобретены, в первую очередь, для реализации обобщенных коллекций.

  • Каким образом передаются переменные в методы, по значению или по ссылке?

В java параметры в методы передаются по значению, тоесть создаются копии параметров и с ними ведется работа в методе. В случае с примитивными типами, то при передачи параметра сама переменная не будет меняться так как в метод просто копируется ее значение.
А вот при передачи объекта копируется ссылка на объект, тоесть если в методе мы поменяем состояние объекта, то и за методом состояние объекта тоже поменяется. Но если мы этой копии ссылки попытаемся присвоить новую ссылку на обьект, то старая ссылка у нас не изменится.
 В случае передачи по значению параметр копируется. Изменение параметра не будет заметно на вызывающей стороне.
 В Java объекты всегда передаются по ссылке, а примитивы - по значениюю.

  • Какие методы есть у класса Object?

Object это базовый класс для всех остальных объектов в Java. Каждый класс наследуется от Object. Соответственно все классы наследуют методы класса Object.
Методы класса Object:
  • public final native Class getClass()
  • public native int hashCode()
  • public boolean equals(Object obj)
  • protected native Object clone() throws CloneNotSupportedException
  • public String toString()
  • public final native void notify()
  • public final native void notifyAll()
  • public final native void wait(long timeout) throws InterruptedException
  • public final void wait(long timeout, int nanos) throws InterruptedException
  • public final void wait() throws InterruptedException
  • protected void finalize() throws Throwable

  • Правила переопределения метода Object.equals().

  1. Используйте оператор == что бы проверить ссылку на объект, переданную в метод equals. Если ссылки совпадают - вернуть true. Это не обязательно, нужно скорее для оптимизации, но может сэкономить время в случае "тяжёлых" сравнений.
  2. Используйте оператор instanceof для проверки типа аргумента. Если типы не совпадают, вернуть false. 
  3. Преобразуйте аргумент к корректному типу. Так как на предыдущем шаге мы выполнили проверку, преобразование корректно.
  4. Пройтись по всем значимым полям объектов и сравнить их друг с другом. Если все поля равны - вернуть true. Для сравнения простых типов использовать ==. Для полей со ссылкой на объекты использовать equals. float преобразовывать в int  с помощью Float.floatToIntBits и сравнить с помощью ==. double преобразовывать в long  с помощью Double.doubleToLongBits и сравнить с помощью ==. Для коллекций вышеперечисленные правила применяются к каждому элементу коллекции. Нужно учитывать возможность null полей/объектов. Очерёдность сравнения полей может существенно влиять на производительность.
  5. Закончив реализацию equals задайте себе вопрос, является ли метод симметричным, транзитивным и непротиворечивым.
И ещё несколько дополнительных правил.
  • Переопределив equals, всегда переопределять hashCode.
  • Не использовать сложную семантику в equals (типа определения синонимов). equals должен сравнивать поля объектов, не более.

  • Если вы хотите переопределить equals(), какие условия должны удовлетворяться для переопределенного метода?

Метод equals() обозначает отношение эквивалентности объектов. Эквивалентным называется отношение, которое является симметричным, транзитивным и рефлексивным.
  • Рефлексивность: для любого ненулевого x, x.equals(x) вернет true;
  • Транзитивность: для любого ненулевого x, y и z, если x.equals(y) и y.eqals(z) вернет true, тогда и x.equals(z) вернет true;
  • Симметричность: для любого ненулевого x и y, x.equals(y) должно вернуть true, тогда и только тогда, когда y.equals(x) вернет true.
Также для любого ненулевого x, x.equals(null) должно вернуть false.

  • Какая связь между hashCode и equals?

Объекты равны, когда a.equals(b)=true и a.hashCode==b.hashcode ->true Но необязательно, чтобы два различных объекта возвращали различные хэш коды(такая ситуация называется коллизией).

  • Каким образом реализованы методы hashCode и equals в классе Object?

Реализация метода equals в классе Object сводится к проверке на равенство двух ссылок:
Реализация же метода hashCode класса Object сделана нативной, т.е. определенной не с помощью Java-кода:
Он обычно возвращает адрес объекта в памяти.

  • Что будет, если переопределить equals не переопределяя hashCode? Какие могут возникнуть проблемы?

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

  • Есть ли какие-либо рекомендации о том, какие поля следует использовать при подсчете hashCode?

Есть. Необходимо использовать уникальные, лучше примитивные поля, такие как id, uuid, например. Причем, если эти поля задействованы при вычислении hashCode, то нужно их задействовать при выполнении equals.
Общий совет: выбирать поля, которые с большой долью вероятности будут различаться.

  • Для чего нужен метод hashCode()?

Существуют коллекции(HashMap, HashSet), которые используют хэш код, как основу при работе с объектами. А если хэш для равных объектов будет разным, то в HashMap будут два равных значения, что является ошибкой. Поэтому необходимо соответствующим образом переопределить метод hashCode().
Х   Хеширование - преобразование входного массива данных произвольной длины в выходную битовую строку фиксированной длины. Такие преобразования также называются хеш-функциями или функциями свёртки, а их результаты называют хешем или хеш-кодом.
Хе Хеш-таблице - это структура данных, реализующая интерфейс ассоциативного массива, а именно, она позволяет хранить пары (ключ, значение) и выполнять три операции: операцию добавления новой пары, операцию поиска и операцию удаления пары по ключу.
Выполнение операции в хеш-таблице начинается с вычисления хеш-функции от ключа. Получающееся хеш-значение i = hash(key) играет роль индекса в массиве H. Затем выполняемая операция (добавление, удаление или поиск) перенаправляется объекту, который хранится в соответствующей ячейке массива H[i].
Одним из методов построения хеш-функции есть метод деления с остатком (division method) состоит в том, что ключу k ставится в соответствие остаток от деления k на m, где m - число возможных хеш-значений.

  • Правила переопределения метода Object.hashCode().

При реализации hashCode используется несколько простых правил. Прежде всего, при вычислении хеш-кода следует использовать те же поля, которые сравниваются в equals. Это, во-первых, даст равенство хеш-кодов для равных обектов, во-вторых, распределено полученное значение будет точно так же, как и исходные данные. Теоретически, можно сделать так, чтобы хеш-код всегда был равен 0, и это будет абсолютно легальная реализация. Другое дело, что ее ценность будет равна тому же самому нулю.
Далее. Несмотря на то, что хеш-коды равных объектов должны быть равны, обратное неверно! Два неравных объекта могут иметь равные хеш-коды. Решающее значение имеет не уникальность, а скорость вычисления, потому как это приходится делать очень часто. Потому, в некоторых случаях имеет смысл посчитать хеш-код заранее и просто выдавать его по запросу. Прежде всего это стоит делать тогда, когда вычисление трудоемко, а объект неизменен.

  • Расскажите про клонирование объектов. В чем отличие между поверхностным и глубоким клонированием?

Чтобы объект можно было клонировать, он должен реализовать интерфейс Cloneable(маркер). Использование этого интерфейса влияет на поведение метода "clone" класс Object. Таким образом 
myObj.clone() создаст нам клон нашего объекта, но этот клон будет поверхностный.
Что значит поверхностным? Это значит что клонируется только примитивные поля класса, ссылочные поля не клонируются!
Для того, чтоб произвести глубокое клонирование, необходимо в клонируемом классе переопределить метод clone() и в нем произвести клонирование изменяемых полей объекта.

  • Правила переопределения метода Object.clone().

Метод clone() в Java используется для клонирования объектов. Т.к. Java работает с объектами с помощью ссылок, то простым присваиванием тут не обойдешься, ибо в таком случае копируется лишь адрес, и мы получим две ссылки на один и тот же объект, а это не то, что нам нужно. Механизм копирования обеспечивает метод clone() класса Object.
 clone() действует как конструктор копирования. Обычно он вызывает метод clone() суперкласса и т.д. пока не дойдет до Object.
 Метод clone() класса Object создает и возвращает копию объекта с такими же значениями полей. Object.clone() кидает исключение CloneNotSupportedException если вы пытаетесь клонировать объект не реализующий интерфейс Cloneable. Реализация по умолчанию метода Object.clone() выполняет неполное/поверхностное (shallow) копирование. Если вам нужно полное/глубокое (deep) копирование класса то в методе clone() этого класса, после получения клона суперкласса, необходимо скопировать нужные поля.
 Синтаксис вызова clone() следующий:
 или чаще:
 Один из недостатков метода clone(), это тот факт, что возвращается тип Object, поэтому требуется нисходящее преобразование типа. Однако начиная с версии Java 1.5 при переопределении метода вы можете сузить возвращаемый тип.
 Пару слов о clone() и final полях.
 Метод clone() несовместим с final полями. Если вы попробуете клонировать final поле компилятор остановит вас. Единственное решение - отказаться от final.
 Ну и пример использования clone():
 Консоль:
 Как видите, изменение объекта a повлекло за собой изменение объекта c, а вот с b всё в порядке.

  • Где и как вы можете использовать  закрытый конструктор?

Например в качестве паттерна Синглетон. В том же классе создается статический метод. Где и создается экземпляр класса, конечно если он уже не создан, тогда он просто возвращается методом.

  • Что такое конструктор по умолчанию?

В Java если нет явным образом опредёленных конструкторов в классе, то компилятор использует конструктор по умолчанию, опредёленный неявным способом, который аналогичен "чистому", конструктору по умолчанию. Конструктор по умолчанию - это довольно простая конструкция, которая сводится к созданию для типа конструктора без параметров. Так, например, если при объявлении нестатического класса не объявить пользовательский конструктор (не важно, с параметрами или без них), то компилятор самостоятельно сгенерирует конструктор без параметров. Некоторые программисты явным образом задают конструктор по умолчанию по привычке, чтобы не забыть в дальнейшем, но это не обязательно
В Java если производный класс не вызывает явным образом конструктор базового класса (в Java используя super() в первой строчке), то конструктор по умолчанию вызывается неявно. Если базовый класс не имеет конструктора по умолчанию, то это считается ошибкой.

  • Опишите метод Object.finalize().

Метод finalize(). Java обеспечивает механизм, который является аналогичным использованию деструкторов в С ++, который может использоваться для того, чтобы произвести процесс очистки перед возвращением управления операционной системе. 
Применяя метод finalize(), можно определять специальные действия, которые будут выполняться тогда, когда объект будет использоваться сборщиком мусора. Данный метод вызывается при уничтожении объекта автоматическим сборщиком мусора (garbage collector). В классе Object он ничего не делает, однако в классе-наследнике позволяет описать все действия, необходимые для корректного удаления объекта, такие как закрытие соединений с БД, сетевых соединений, снятие блокировок на файлы и т.д. В обычном режиме напрямую этот метод вызывать не нужно, он отработает автоматически. Если необходимо, можно обратиться к нему явным образом.
Его синтаксис: 
protected void finalize( ) throws Throwable 
Ссылки не являются собранным мусором; только объекты - собранный мусор.

  • Чем отличаются слова final, finally и finalize?

final - Нельзя наследоваться от файнал класса. Нельзя переопределить файнал метод. Нельзя изменить сначение файнал поля.
finally - используется при обработке ошибок, вызывается всегда, даже если произошла ошибка(кроме System.exit(0)). Удобно использовать для освобождения ресурсов.
finalize() - вызывается перед тем как сборщик мусора будет проводить освобождение памяти. Не рекомендуется использовать для освобождения системных ресурсов, так как не известно когда сборщик мусора будет производить свою очистку. Вообще данный метод мало кто использует. Единственно что можно использовать этот метод для закрытия ресурса что должен работать на протяжении всей работы программы и закрываться по ее окончанию. Еще можно использовать метод для защиты от так называемых «дураков», проверять, освобождены ли ресурсы, если нет, то закрыть их.


Java core (часть 3).

Рассказать друзьям:

10 коментарі :

  1. Не вводите в заблуждение новичков, в Java все передается по значению, нет никакой передачи по ссылке. Лучше потратьте свое время изучение Java Language Specification http://docs.oracle.com/javase/specs/jls/se8/html/jls-8.html#jls-8.4.1
    Так же рекомендую ознакомиться с этой статьей http://javadude.com/articles/passbyvalue.htm

    ОтветитьУдалить
    Ответы
    1. При работе не с примитивами метод создает ссылку на ячейку памяти в которой хранится ссылка на обьект(если до этого мы прописали что ссылка на этот обьект ссылаеться на какой то другой обьект(например в main пропишем Intreger a=new Integer(5); Integer b=a; а потом вызовем функцию с параметром b а именно function(b); то в function создастся ссылка на обьект bа имено на ссылку на обьект a) то есть если вы поменяете значение b в методе(не создатите новый обьект) то поменяется значение и у a и у b в main и у b в function.
      Вобще тема с памятью в Java очень тонкая главное помнить принцип что обьект это ссылка на место в памяти где хранятся его поля а примитив это собственно поле(БЕЗ ссылки)

      Удалить
  2. "Философия Java" Эккеля. Он там уточняет, что это вопрос терминологии. Но проще считать, что объекты передаются по ссылке.

    ОтветитьУдалить
  3. Скажем так, для того, кто еще не в курсе java модели памяти, лучше понимается термин передачи объектов по ссылке. А когда человек изучит данный материал, то пусть называет это как ему удобней. Например, по значению ссылки ))

    ОтветитьУдалить
  4. Что за ересь про finalize()? Джошуа Блош в своей книге "Эффективное программирование" прямо говорит, что "Остерегайтесь финализаторов - они непредсказуемы и небезопасны", единственную причину их использования я вижу, если вызывается, например, какой-то нативный метод для выделения памяти, например "malloc()" и, тогда, в финализаторе нужно будет вызвать "free()", чтобы эту память освободить.

    ОтветитьУдалить
  5. "Какова истинная цель использования обобщенных типов в java?
    ... в первую очередь, для реализации обобщенных коллекций."
    А, может, для обеспечения типовой безопасности?

    ОтветитьУдалить
  6. Можно ли при переопределении(override) метода изменить возвращаемый тип?
    У вас написано: да, если выполняется восходящее преобразование;
    Но на сколько я понимаю, восходящее преобразование в иерархии классов это "от подкласса к суперклассу": Integer -> Number -> Object. А при переопределении как раз допустимо обратное: если наш первоначальный класс возвращал Number, то мы можем переопределить калсс так, чтобы он возвращаял Integer, но не Object. А с примитивами никакое преобразование не допускается.
    Поправьте, если я не прав.

    ОтветитьУдалить
    Ответы
    1. Вот оригинал из Доки.
      The overriding method has the same name, number and type of parameters, and return type as the method that it overrides. An overriding method can also return a subtype of the type returned by the overridden method. This subtype is called a covariant return type.

      Удалить
  7. Не рассказали самое интересное про конструктор: если определить в классе конструктор с параметрами, то дефолтный(без параметров) станет недоступен. И если он где-то используется, то должен быть определен явно.

    ОтветитьУдалить
    Ответы
    1. На этот вопрос будут ловить на собеседовании))

      Удалить