Пользователь Andrey
Andrey
26 уровень

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

Статья из группы Архив info.javarush.ru
Для тех, кто впервые слышит слово Java Core – это фундаментальные основы языка. С этими знаниями уже смело можно идти на стажировку/интернатуру.
Java Core. Вопросы к собеседованию, ч. 2 - 1
Приведенные вопросы помогут вам освежить знания перед собеседованием, или почерпнуть для себя что-то новое. Для получения практических навыков занимайтесь на 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() могут быть:
  1. Метод finalize() не работает с цепочками, как конструкторы. Имеется ввиду, что когда вы вызываете конструктор класса, то конструкторы суперклассов будут вызваны безоговорочно. Но в случае с методом finalize(), этого не последует. Метод finalize() суперкласса должен быть вызван явно.
  2. Любое исключение, брошенное методом finalize игнорируется нитью сборщика мусора, и не будет распространяться далее, что означает, что событие, не будет занесено в ваши логи. Это очень плохо, не правда ли?
  3. Также вы получаете значительное ухудшение производительности, если метод 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 все объекты имеют монитор. Нити ожидают на мониторах, поэтому для выполнения ожидания нам необходимо два параметра:
  • нить
  • монитор (любой объект).
В Java проектировании, нить не может быть точно определена, это всегда текущая нить, исполняющая код. Однако мы можем определить монитор (который является объектом, у которого мы можем вызвать метод wait). Это хороший замысел, поскольку если мы можем заставить любую другую нить ожидать на определенном мониторе, это приведет к «вторжению», оказывая трудности проектирования/программирования параллельных программ. Помните, что в Java все операции, которые вторгаются в другие нити являются устаревшими(например, stop()).

Напишите программу для создания deadlock в Java и исправьте его

В Java deadlock – это ситуация, когда минимум две нити удерживают блок на разных ресурсах, и обе ожидают освобождения другого ресурса для завершения своей задачи. И ни одна не в состоянии оставить блокировку удерживаемого ресурса. Java Core. Вопросы к собеседованию, ч. 2 - 2 Пример программы:

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. Другие отличия описаны ниже. Вы можете:
  1. итерировать в обратном порядке.
  2. получить индекс в любом месте.
  3. добавить любое значение в любом месте.
  4. установить любое значение в текущей позиции.
Удачи в обучении!! Автор статьи Lokesh Gupta Оригинал статьи Java Core. Вопросы к собеседованию, ч. 1 Java Core. Вопросы к собеседованию, ч. 3
Комментарии (5)
Чтобы просмотреть все комментарии или оставить свой,
перейдите в полную версию
Katya Petrushenko 31 уровень, Санкт-Петербург
27 ноября 2019
Статья нуждается в правке, многие вещи неправильны или неактуальны. Не готовьтесь к собеседованию по ней.
tempys 31 уровень, Svyatoshino
31 октября 2014
статьи супер еще раз перечитал все )
Munch 12 уровень
27 февраля 2014
Не поняла разницу в статье между абстракцией и инкапсуляцией. По сути, они делают тоже самое — прячут реализацию, делают это для всех классов-родителей… Можно как-то объяснить по типу как в примере интерфейс-абстракция?

Кто-то сказал: главное отличие в том, где вы хотите вашу реализацию. Создавая интерфейс, вы можете переместить реализацию в любой класс, который реализует ваш интерфейс. Создавая абстрактный класс, вы можете разделить реализацию всех производных классов в одном месте и избежать много плохих вещей, таких как дублирование кода.
Munch 12 уровень
27 февраля 2014
*здесь было что-то не то*