Пользователь Vadim625
Vadim625
27 уровень

Java Core. Вопросы к собеседованию, ч. 3

Статья из группы Архив info.javarush.ru
В предыдущих двух статьях мы обсудили некоторые важные вопросы, которые Вам чаще всего задают на собеседованиях. Настало время продолжить и рассмотреть остальную часть вопросов.
Java Core. Вопросы к собеседованию, ч. 3 - 1

Глубокое копирование и поверхностное копирование

Точной копией оригинала является его клон. В Java это означает возможность создавать объект с аналогичной структурой, как и у исходного объекта. Метод clone() обеспечивает эту функциональность. Поверхностное копирование копирует настолько малую часть информации, насколько это возможно. По умолчанию, клонирование в Java является поверхностным, т.е. Object class не знает о структуре класса, которого он копирует. При клонировании, JVM делает такие вещи:
  1. Если класс имеет только члены примитивных типов, то будет создана совершенно новая копия объекта и возвращена ссылка на этот объект.
  2. Если класс содержит не только члены примитивных типов, а и любого другого типа класса, тогда копируются ссылки на объекты этих классов. Следовательно, оба объекта будут иметь одинаковые ссылки.
Глубокое копирование дублирует все. Глубокое копирование — это две коллекции, в одну из которых дублируются все элементы оригинальной коллекции. Мы хотим сделать копию, при которой внесение изменений в любой элемент копии не затронет оригинальную коллекцию. Глубокое клонирование требует выполнения следующих правил:
  1. Нет необходимости копировать отдельно примитивные данные;
  2. Все классы-члены в оригинальном классе должны поддерживать клонирование. Для каждого члена класса должен вызываться super.clone() при переопределении метода clone();
  3. Если какой-либо член класса не поддерживает клонирование, то в методе клонирования необходимо создать новый экземпляр этого класса и скопировать каждый его член со всеми атрибутами в новый объект класса, по одному.
Узнайте больше о клонировании здесь

Что такое синхронизация? Блокировка на уровне объекта и блокировка на уровне класса?

Синхронизация относится к многопоточности. Синхронизированный блок кода может исполняться одновременно только одним потоком. Java позволяет обрабатывать одновременно несколько потоков. Это может привести к тому, что два или более потока хотят получить доступ к одному и тому же полю. Синхронизация позволяет избежать ошибок памяти, возникающих при неправильном использовании ресурсов памяти. Когда метод объявлен как синхронизированный, нить удерживает его монитор. Если другая нить пытается в это время получить доступ к синхронизированному методу, то поток блокируется, и ждет освобождения монитора. Синхронизация в Java осуществляется специальным ключевым словом synchronized. Вы можете помечать таким образом отдельные блоки или методы в вашем классе. Ключевое слово synchronized не может быть использовано вместе с переменными или атрибутами класса. Блокировка на уровне объекта – механизм, когда вы хотите синхронизировать non-static метод или non-static блок кода таким образом, что только один поток сможет выполнить блок кода в данном экземпляре класса. Это нужно всегда делать, чтобы сделать экземпляр класса потокобезопасным. Блокировка на уровне класса предотвращает вход нескольких потоков в синхронизированный блок для всех доступных экземпляров класса. Например, если есть 100 экземпляров класса DemoClass, то только 1 поток сможет выполнить demoMethod () используя одну из переменных в определенный момент времени. Это должно быть всегда сделано, что бы обеспечить безопасность статического потока. Узнайте больше о синхронизации здесь.

Какая разница между sleep() и wait()?

Sleep() — это метод, который используется, чтобы задержать процесс на несколько секунд. В случае сwait(), нить находится в состоянии ожидания, пока мы не вызовем метод notify() или notifyAll(). Основное различие заключается в том, что wait() снимает блокировку монитора, в то время как sleep() не освобождает блокировку. Wait() используется для многопотоковых приложений, sleep() используют просто для паузы выполнения нити. Thread.sleep() ставит текущий поток в «Not Runnable» состояние на определенное количество времени. Нить сохраняет состояние монитора, которое было до вызова данного метода. Если же другая нить вызывает t.interrupt(), нить которая "заснула" — проснется. Обратите внимание, что sleep() является статическим методом, что означает, что он всегда влияет на текущий поток (тот, который выполняет метод sleep()). Распространенной ошибкой является вызов t.sleep(), где t является другим потоком; даже тогда, когда текущая нить, которая вызвала метод sleep(), не является t нитью. Object.wait() посылает текущий поток в "Not Runnable" состояние на некоторое время, так же как и sleep(), но все же с некоторым нюансом. Wait() вызывается для объекта, а не для нити; мы называем этот объект “lock object”. Перед вызовом lock.wait(), текущая нить должна быть синхронизирована с “lock object”; wait() после этого снимает эту блокировку, и добавляет нить в ”wait list” связанный с этой блокировкой. Позже, другая нить может быть синхронизирована с тем же самым lock object и вызвать метод lock.notify(). Этот метод «разбудит» оригинальную нить, которая все еще ждет. В принципе, wait()/notify() можно сравнить с sleep()/interrupt(), только активной нити не нужен прямой указатель на спящую нить, нужно только знать общий lock object. Читайте детальную разницу здесь.

Можно ли присвоить null к this ссылочной переменной?

Нет, нельзя. В Java левая часть оператора присваивания должен быть переменной. "This" — это специальное ключевое слово, которое всегда дает текущий экземпляр класса. Это не любая переменная. Точно также, null нельзя присвоить переменной, используя ключевое слово “super” или любое другое подобное.

Какая разница между && и &?

& — побитово а && — логически.
  1. & оценивает обе стороны операции;
  2. && оценивает левую сторону операции. Если она true, он продолжает оценить правую сторону.
Посмотрите здесь для более глубокого понимания.

Как переопределить equals() и hachCode() методы?

hashCode() и equals() методы определены в классе Object, который является родительским классом для Java объектов. По этой причине, все объекты Java наследуют реализацию по умолчанию для методов. Метод hashCode() используется для получения уникального целого числа для данного объекта. Это целое число используется для определения местоположения объекта, когда этот объект необходимо сохранить, например к HashTable. По умолчанию, hashCode() возвращает integer представление адреса ячейки памяти, где хранится объект. Метод equls(), как и следует из его имени, используется, чтобы просто проверить равенство двух объектов. Реализация по умолчанию проверяет ссылки на объекты на предмет их равенства. Ниже приведены важные рекомендации для перезагрузки данных методов:
  1. Всегда используйте одинаковые атрибуты объектов при генерации hashCode() и equals();
  2. Симметричность. Т.е. если для каких-либо объектов x и y x.equals(y) возвращает true, то и y.equals(x) должен возвращать true;
  3. Рефлексивность. Для любого объекта x x.equals(x) должен возвращать true;
  4. Постоянство. Для любых объектов x и y x.equals(y) возвращает одно и то же, если информация, используемая в сравнениях, не меняется;
  5. Транзитивность. Для любых объектов x, y и z, если x.equals(y) вернет true и y.equals(z) вернет true, то и x.equals(z) должен вернуть true;
  6. Всякий раз, когда метод вызывается у одного и того же объекта во время выполнения приложения, он должен возвращать одно и то же число, если используемая информация не изменяется. hashCode может возвращать разные значения для идентичных объектов в различных экземплярах приложения;
  7. Если два объекта равны, согласно equals, то их hashCode должны возвращать одинаковые значения;
  8. Обратное требование необязательно. Два неравных объекта могут возвращать одинаковый hashCode. Однако для повышения производительности, лучше, чтобы разные объекты возвращали разные коды.
Интересные факты о данных методах читайте здесь.

Расскажите про модификаторы доступа

Классы Java, поля, конструкторы и методы могут иметь один из четырех различных модификаторов доступа: private Если метод или переменная помечены как private, то только код внутри одного класса может получить доступ к переменной, или вызвать метод. Код внутри подклассов не может получить доступ к переменной или методу, также как и не может получить доступ из любого другого класса. Модификатор доступа private чаще всего используется для конструкторов, методов и переменных. default Модификатор доступа default объявляется в том случае, если модификатор не указан вообще. Данный модификатор означает, что доступ к полям, конструкторам и методам данного класса может получить код внутри самого класса, код внутри классов в том же пакете. Подклассы не могут получить доступ к методам и переменным – членам суперкласса, если они объявлены как default, если подкласс не находится в том же пакете, что и суперкласс. protected Модификатор protected работает так же, как и default, за исключением того, что подклассы так же могут получить доступ к защищенным методам и переменным суперкласса. Данное утверждение является верным, даже если подкласс не находится в том же пакете, что и суперкласс. public Модификатор доступа public означает, что весь код может получить доступ к классу, его переменным, конструкторам или методам, независимо от того, где расположен этот код. Java Core. Вопросы к собеседованию, ч. 3 - 2

Что такое сборщик мусора? Можем ли мы вызвать его?

Сбор мусора является функцией автоматического управления памятью во многих современных языках программирования, таких как Java и языков в NET.Framework. Языки, которые используют сбор мусора, часто интерпретируют сбор мусора в виртуальной машине, такой как JVM. Сбор мусора имеет две цели: любая неиспользованная памяти должна быть освобождены, и память не должна быть освобождены, если программа еще будет ее использовать. Можете ли вы запустить сбор мусора вручную? Нет, System.gc() предоставляет вам настолько большой доступ, насколько можно. Лучшим вариантом является вызов метода System.gc(), который намекнет сборщику мусора про необходимость запуска. Нет никакого способа запустить его немедленно, так как сборщик мусора является недетерминированным. Кроме того, согласно документации, OutOfMemoryError не будет проброшена, если виртуальной машине не удалось освободить память после полной сборки мусора. Узнайте больше о сборщике мусора здесь.

Что означает ключевое слово native? Объясните в деталях

Ключевое слово native применяется, чтобы указать, что метод реализован не в файле Java, а на другом языке программирования. Native методы использовались в прошлом. В текущих версиях Java это нужно реже. В настоящее время, native методы необходимы, когда:
  1. Вы должны вызвать библиотеку из Java, которая написана на другом языке.
  2. Вам нужен доступ к системным или аппаратным ресурсам, к которым можно получить доступ только используя другой язык (как правило, С). На самом деле, многие системные функции, которые взаимодействуют с реальным компьютером (например диски или сетевые данные) могут быть вызваны только native методом.
Недостатки использования библиотек native методов тоже значительны:
  1. JNI/JNA могут дестабилизировать JVM, особенно если вы попытаетесь сделать что-то сложное. Если ваш native метод делает что-то неправильно, есть вероятность аварийного завершения JVM. Также, неприятные вещи могут произойти, если ваш native метод вызывается из нескольких нитей. И так далее.
  2. Программу с native кодом сложнее дэбажить.
  3. Native код требует отдельного построения фрэймворков, что может создать проблемы с переносом на другие платформы.

Что такое сериализация?

В компьютерных науках, в контексте хранения и передачи данных, сериализация – это процесс перевода структуры данных или состояния объекта в формат, который может быть сохранен и восстановлен потом в другой компьютерной среде. После приема серии битов, они пересчитываются в соответствии с форматом сериализации, и могут быть использованы для создания семантически идентичного клона исходного объекта. Java предоставляет автоматическую сериализацию, которая требует, чтобы объект реализовал интерфейс java.io.Serializable. Реализация интерфейса помечает класс как «сериализуемый». В интерфейсе java.io.Serializable нет методов сериализации, но сериализуемый класс может дополнительно определить методы, которые будут вызваны как часть процесса сериализации/дисериализации. При внесении изменений в классы, необходимо учитывать, какие из них будут совместимы и не совместимы с сериализацией. Вы можете прочитать полную инструкцию здесь. Самые главные пункты я приведу: Несовместимые изменения:
  1. Удаление поля;
  2. Перемещение класса вверх или вниз по иерархии;
  3. Изменение non-static поля на static или non-transient на transient;
  4. Изменение объявленного типа примитивный данных;
  5. Изменение метода WriteObject или ReadObject так, что они больше не пишут или не читают поля по умолчанию;
  6. Изменение класса Serializable на Externalizable или наоборот;
  7. Изменение класса enum на non-enum или наоборот;
  8. Удаление Serializable или Externalizable;
  9. Добавление writeReplace или readResolve метода к классу.
Совместимые изменения:
  1. Добавление полей;
  2. Добавление/удаление классов;
  3. Добавление методов WriteObject/ReadObject [методы defaultReadObject или defaultWriteObject должны быть вызваны в начале];
  4. Удаление методов WriteObject/ReadObject;
  5. Добавление java.io.Serializable;
  6. Изменение доступа к полю;
  7. Изменение static поля на non-static или transient на non-transient.
Ссылки на предыдущие части: Java Core. Вопросы к собеседованию, ч. 1 Java Core. Вопросы к собеседованию, ч. 2 Оригинал статьи Счастливого обучения!
Комментарии (3)
Чтобы просмотреть все комментарии или оставить свой,
перейдите в полную версию
Arjun 37 уровень, Санкт-Петербург
12 августа 2019
Вроде бы и полезная тема, но слишком вымороченные определения, с трудом дочитал.
losemind 36 уровень, Киев
29 июня 2016
Когда метод объявлен как синхронизированный, нить удерживает его монитор.
Насколько мне известно, у метода нету монитора и если метод объявлен как syncronized(кроме статических) это значит что он удерживает монитор текущего экземпляра(this) объекта.
FedoraLinux 21 уровень, Москва
28 января 2015
Неплохой цикл статей, спасибо большое за перевод.
В дополнение к вопросу о native-методах можно добавить пункт о том, что обращение к native-методам «дорого» стоит. Даже если некоторый алгоритм, реализованный на C++ работает ощутимо быстрее, чем если бы оно было написано на Java, то обращение к такой реализации, подключенной через native-метод может нивелировать преимущество в скорости.