JavaRush /Java блог /Random UA /Java Core. Запитання до співбесіди, ч. 2
Andrey
26 рівень

Java Core. Запитання до співбесіди, ч. 2

Стаття з групи Random UA
Для тих, хто вперше чує слово 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 наносекунд.
    Додавання фіналізатора збільшує час до 2400 наносекунд. Іншими словами, приблизно в 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
Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ