Для тех, кто впервые слышит слово Java Core – это фундаментальные основы языка. С этими знаниями уже смело можно идти на стажировку/интернатуру.
Приведенные вопросы помогут вам освежить знания перед собеседованием, или почерпнуть для себя что-то новое. Для получения практических навыков занимайтесь на JavaRush.
Оригинал статьи
Ссылки на остальные части:
Java Core. Вопросы к собеседованию, ч. 1
Java Core. Вопросы к собеседованию, ч. 3
Пример программы:

Почему необходимо избегать метода finalize()?
Все мы знаем утверждение, что методfinalize()
вызывается сборщиком мусора перед освобождением памяти, занимаемой объектом. Вот пример программы, которая доказывает, что вызов метода finalize()
не гарантирован:
public class TryCatchFinallyTest implements Runnable {
private void testMethod() throws InterruptedException
{
try
{
System.out.println("In try block");
throw new NullPointerException();
}
catch(NullPointerException npe)
{
System.out.println("In catch block");
}
finally
{
System.out.println("In finally block");
}
}
@Override
protected void finalize() throws Throwable {
System.out.println("In finalize block");
super.finalize();
}
@Override
public void run() {
try {
testMethod();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public class TestMain
{
@SuppressWarnings("deprecation")
public static void main(String[] args) {
for(int i=1;i< =3;i++)
{
new Thread(new TryCatchFinallyTest()).start();
}
}
}
Вывод:
In try block
In catch block
In finally block
In try block
In catch block
In finally block
In try block
In catch block
In finally block
Удивительно, метод finalize
не был выполнен ни для одной нити. Это доказывает мои слова. Я думаю, причина в том, что финализаторы выполняются отдельной нитью сборщика мусора. Если виртуальная машина Java завершается слишком рано, тогда сборщик мусора не имеет достаточно времени для создания и выполнения финализаторов.
Другими причинами не использовать метод finalize()
могут быть:
- Метод
finalize()
не работает с цепочками, как конструкторы. Имеется ввиду, что когда вы вызываете конструктор класса, то конструкторы суперклассов будут вызваны безоговорочно. Но в случае с методомfinalize()
, этого не последует. Методfinalize()
суперкласса должен быть вызван явно. - Любое исключение, брошенное методом
finalize
игнорируется нитью сборщика мусора, и не будет распространяться далее, что означает, что событие, не будет занесено в ваши логи. Это очень плохо, не правда ли? Также вы получаете значительное ухудшение производительности, если метод
finalize()
присутствует в вашем классе. В «Эффективном программировании» (2-е изд.) Джошуа Блох сказал:
«Да, и еще одно: есть большая потеря производительности при использовании финализаторов. На моей машине время создания и уничтожения простых объектов составляет примерно 5,6 наносекунд.
Добавление финализатора увеличивает время до 2 400 наносекунд. Другими словами, примерно в 430 раз медленнее происходит создание и удаление объекта с финализатором.»
Почему HashMap не должна использоваться в многопоточном окружении? Может ли это вызвать бесконечный цикл?
Мы знаем, чтоHashMap
— это не синхронизированная коллекция, синхронизированным аналогом которой является HashTable
. Таким образом, когда вы обращаетесь к коллекции и многопоточном окружении, где все нити имеют доступ к одному экземпляру коллекции, тогда безопасней использовать HashTable
по очевидным причинам, например во избежание грязного чтения и обеспечения согласованности данных. В худшем случае это многопоточное окружение вызовет бесконечный цикл.
Да, это правда. HashMap.get()
может вызвать бесконечный цикл. Давайте посмотрим как?
Если вы посмотрите на исходный код метода HashMap.get(Object key)
, он выглядит так:
public Object get(Object key) {
Object k = maskNull(key);
int hash = hash(k);
int i = indexFor(hash, table.length);
Entry e = table[i];
while (true) {
if (e == null)
return e;
if (e.hash == hash && eq(k, e.key))
return e.value;
e = e.next;
}
}
while(true)
всегда может стать жертвой бесконечного цикла в многопоточном окружении времени исполнения, если по какой-то причине e.next
сможет указать на себя. Это станет причиной бесконечного цикла, но как e.next
укажет на себя(то есть на e
)?
Это может произойти в методе void transfer(Entry[] newTable)
, который вызывается, в то время как HashMap
изменяет размер.
do {
Entry next = e.next;
int i = indexFor(e.hash, newCapacity);
e.next = newTable[i];
newTable[i] = e;
e = next;
} while (e != null);
Этот фрагмент кода склонен к созданию бесконечного цикла, если изменение размера происходит в то же время, когда другая нить пытается изменить экземпляр карты (HashMap
).
Единственный способ избежать описанного сценария – использовать синхронизацию в коде, или еще лучше, использовать синхронизированную коллекцию.
Объясните абстракцию и инкапсуляцию. Как они связаны?
Простыми словами «Абстракция отображает только те свойства объекта, которые значимы для текущего ракурса». В теории объектно-ориентированного программирования, абстракция включает возможность определения объектов, представляющих абстрактных «действующих лиц», которые могут выполнять работу, изменять и сообщать об изменении своего состояния, и «взаимодействовать» с другими объектами в системе. Абстракция в любом языке программирования работает во многих отношениях. Это видно начиная от создания подпрограмм для определения интерфейсов низкоуровневых языковых команд. Некоторые абстракции стараются ограничить ширину общего представления потребностей программиста, полностью скрывая абстракции, на которых они построены, например шаблоны проектирования. Как правило, абстракцию можно увидеть в двух отношениях: Абстракция данных – это способ создания сложных типов данных и выставляя только значимые операции для взаимодействия с моделью данных, в то же время, скрывая все детали реализации от внешнего мира. Абстракция исполнения – это процесс выявления всех значимых операторов и выставляя их как рабочую единицу. Мы обычно используем эту особенность, когда мы создаем метод для выполнения какой-либо работы. Заключение данных и методов внутри классов в комбинации с осуществлением сокрытия (используя контроль доступа) часто называется инкапсуляцией. Результатом является тип данных с характеристиками и поведением. Инкапсуляция, в сущности, содержит также сокрытие данных и сокрытие реализации. «Инкапсулируйте все, что может измениться». Эта цитата является известным принципом проектирования. Если на то пошло, в любом классе, изменения данных могут произойти во время исполнения и изменения в реализации может произойти в следующих версиях. Таким образом, инкапсуляция применима как к данным, так и к реализации. Итак, они могут быть связаны таким образом:- Абстракция по большей части является Что класс может делать [Идея]
- Инкапсуляция более является Как достигнуть данной функциональности [Реализация]
Различия между интерфейсом и абстрактным классом?
Основные различия могут быть перечислены следующим:- Интерфейс не может реализовать никаких методов, зато абстрактный класс может.
- Класс может реализовать множество интерфейсов, но может иметь только один суперкласс (абстрактный или не абстрактный)
- Интерфейс не является частью иерархии классов. Несвязанные классы могут реализовывать один и тот же интерфейс.
Cat
и Dog
могут наследоваться от абстрактного класса Animal
, и этот абстрактный базовый класс будет реализовывать метод void Breathe()
– дышать, который все животные будут таким образом выполнять одинаковым способом.
Какие глаголы могут быть применены к моему классу и могут применяться к другим? Создайте интерфейс к каждому из этих глаголов. Например, все животные могут питаться, поэтому я создам интерфейс IFeedable
и сделаю Animal
реализующим этот интерфейс. Только Dog
и Horse
достаточно хороши для реализации интерфейса ILikeable
(способны мне нравиться), но не все.
Кто-то сказал: главное отличие в том, где вы хотите вашу реализацию. Создавая интерфейс, вы можете переместить реализацию в любой класс, который реализует ваш интерфейс. Создавая абстрактный класс, вы можете разделить реализацию всех производных классов в одном месте и избежать много плохих вещей, таких как дублирование кода.
Как StringBuffer экономит память?
КлассString
реализован как неизменный (immutable) объект, то есть, когда вы изначально решили положить что-то в объект String
, виртуальная машина выделяет массив фиксированной длины, точно такого размера, как и ваше первоначальное значение. В дальнейшем это будет обрабатываться как константа внутри виртуальной машины, что предоставляет значительное улучшение производительности в случае, если значение строки не изменяется. Однако если вы решите изменить содержимое строки любым способом, на самом деле виртуальная машина копирует содержимое исходной строки во временное пространство, делает ваши изменения, затем сохраняет эти изменения в новый массив памяти. Таким образом, внесение изменений в значение строки после инициализации является дорогостоящей операцией.
StringBuffer
, с другой стороны выполнен в виде динамически расширяемого массива внутри виртуальной машины, что означает, что любая операция изменения может происходить на существующей ячейке памяти, и новая память будет выделяться по мере необходимости. Однако нет никакой возможности виртуальной машине сделать оптимизацию StringBuffer
, поскольку его содержимое считается непостоянным в каждом экземпляре.
Почему методы wait и notify объявлены у класса Object взамен Thread?
Методыwait
, notify
, notifyAll
необходимы только тогда, когда вы хотите, чтобы ваши нити имели доступ к общим ресурсам и общий ресурс мог быть любым java объектом в хипе(heap). Таким образом эти методы определены на базовом классе Object
, так что каждый объект имеет контроль, позволяющий нитям ожидать на своем мониторе. Java не имеет какого-либо специального объекта, который используется для разделения общего ресурса. Никакая такая структура данных не определена. Поэтому на класс Object
возложена ответственность иметь возможность становиться общим ресурсом, и предоставлять вспомогательные методы, такие как wait()
, notify()
, notifyAll()
.
Java основывается на идее мониторов Чарльза Хоара (Hoare). В Java все объекты имеют монитор. Нити ожидают на мониторах, поэтому для выполнения ожидания нам необходимо два параметра:
- нить
- монитор (любой объект).
wait
). Это хороший замысел, поскольку если мы можем заставить любую другую нить ожидать на определенном мониторе, это приведет к «вторжению», оказывая трудности проектирования/программирования параллельных программ. Помните, что в Java все операции, которые вторгаются в другие нити являются устаревшими(например, stop()
).
Напишите программу для создания deadlock в Java и исправьте его
В Javadeadlock
– это ситуация, когда минимум две нити удерживают блок на разных ресурсах, и обе ожидают освобождения другого ресурса для завершения своей задачи. И ни одна не в состоянии оставить блокировку удерживаемого ресурса.

package thread;
public class ResolveDeadLockTest {
public static void main(String[] args) {
ResolveDeadLockTest test = new ResolveDeadLockTest();
final A a = test.new A();
final B b = test.new B();
// Thread-1
Runnable block1 = new Runnable() {
public void run() {
synchronized (a) {
try {
// Добавляем задержку, чтобы обе нити могли начать попытки
// блокирования ресурсов
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
// Thread-1 заняла A но также нуждается в B
synchronized (b) {
System.out.println("In block 1");
}
}
}
};
// Thread-2
Runnable block2 = new Runnable() {
public void run() {
synchronized (b) {
// Thread-2 заняла B но также нуждается в A
synchronized (a) {
System.out.println("In block 2");
}
}
}
};
new Thread(block1).start();
new Thread(block2).start();
}
// Resource A
private class A {
private int i = 10;
public int getI() {
return i;
}
public void setI(int i) {
this.i = i;
}
}
// Resource B
private class B {
private int i = 20;
public int getI() {
return i;
}
public void setI(int i) {
this.i = i;
}
}
}
Запуск приведенного кода приведет к deadlock по весьма очевидным причинам (объяснены выше). Теперь нам необходимо решить эту проблему.
Я верю, что решение любой проблемы лежит в корне самой проблемы. В нашем случае модель доступа к А и В является главной проблемой. Поэтом для решения её, мы просто изменим порядок операторов доступа к разделяемым ресурсам.
После изменения это будет выглядеть так:
// Thread-1
Runnable block1 = new Runnable() {
public void run() {
synchronized (b) {
try {
// Добавляем задержку, чтобы обе нити могли начать попытки
// блокирования ресурсов
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
// Thread-1 заняла B но также нуждается в А
synchronized (a) {
System.out.println("In block 1");
}
}
}
};
// Thread-2
Runnable block2 = new Runnable() {
public void run() {
synchronized (b) {
// Thread-2 заняла B но также нуждается в А
synchronized (a) {
System.out.println("In block 2");
}
}
}
};
Запустите снова этот класс, и теперь вы не увидите deadlock. Я надеюсь, это поможет вам избежать deadlocks и избавиться от них, если столкнетесь.
Что случится, если ваш класс, реализующий Serializable интерфейс содержит несериализуемый компонент? Как исправить это?
В таком случае будет выброшеноNotSerializableException
в процессе выполнения. Для исправления этой проблемы, есть очень простое решение – отметить эти поля transient
. Это означает, что отмеченные поля не будут сериализованы. Если вы также хотите сохранить состояние этих полей, тогда вам необходимо рассмотреть ссылочные переменные, которые уде реализуют интерфейс Serializable
.
Также вам может понадобиться использовать методы readResolve()
и writeResolve()
.
Подведем итоги:
- Во-первых, сделайте ваше несериализуемое поле
transient
. - В
writeObject
первым делом вызовитеdefaultWriteObject
на потоке, для сохранения всех неtransient
полей, затем вызовите остальные методы для сериализации индивидуальных свойств вашего несериализуемого объекта. - В
readObject
, сперва вызовитеdefaultReadObject
на потоке для чтения всех неtransient
полей, затем вызовите другие методы (соответствующие тем, которые вы добавили вwriteObject
) для десериализации вашего неtransient
объекта.
Объясните ключевые слова transient и volatile в Java
«Ключевое словоtransient
используется для обозначения полей, которые не будут сериализованы». Согласно спецификации языка Java: Переменные могут быть маркированы индикатором transient для обозначения, что они не являются частью устойчивого состояния объекта. Например, вы можете содержать поля, полученные из других полей, и их предпочтительнее получать программно, чем восстанавливать их состояние через сериализацию.
К примеру, в классе BankPayment.java
такие поля, как principal
(директор) и rate
(ставка) могут быть сериализованы, а interest
(начисленные проценты) могут быть вычислены в любое время, даже после десериализации.
Если мы вспомним, каждая нить в Java имеет собственную локальную память и производит операции чтения/записи в эту локальную память. Когда все операции сделаны, она записывает модифицированное состояние переменной в общую память, откуда все нити получают доступ к переменной. Как правило, это обычный поток внутри виртуальной машины. Но модификатор volatile говорит виртуальной машине, что обращение нити к этой переменной всегда должно согласовывать свою собственную копию этой переменной с основной копией переменной в памяти. Это означает, что каждый раз, когда нить хочет прочитать состояние переменной, она должна очистить состояние внутренней памяти и обновить переменную из основной памяти.
Volatile
наиболее полезно в свободных от блокировок алгоритмах. Вы отмечаете переменную, хранящую общие данные как volatile, тогда вы не используете блокировки для доступа к этой переменной, и все изменения, сделанные одной нитью, будут видимы для других. Или если вы хотите создать отношение «случилось-после» для обеспечения того, чтобы не повторялись вычисления, опять же для обеспечения видимости изменений в реальном времени.
Volatile должно использоваться для безопасной публикации неизменяемых объектов в многопоточном окружении. Объявление поля public volatile ImmutableObject
обеспечивает, что все нити всегда видят текущую доступную ссылку на экземпляр.
Разница между Iterator и ListIterator?
Мы можем использоватьIterator
для перебора элементов Set
, List
или Map
. Но ListIterator
может быть применим только для перебора элементов List
. Другие отличия описаны ниже.
Вы можете:
- итерировать в обратном порядке.
- получить индекс в любом месте.
- добавить любое значение в любом месте.
- установить любое значение в текущей позиции.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ
Для достижения абстракции мы инкапсулирем…