JavaRush /Blog Java /Random-PL /Rdzeń Javy. Pytania do wywiadu, część 2
Andrey
Poziom 26

Rdzeń Javy. Pytania do wywiadu, część 2

Opublikowano w grupie Random-PL
Dla tych, którzy po raz pierwszy słyszą słowo Java Core, są to podstawowe podstawy tego języka. Mając tę ​​wiedzę możesz bezpiecznie udać się na staż/staż.
Rdzeń Javy.  Pytania do rozmowy kwalifikacyjnej, część 2 - 1
Dzięki tym pytaniom odświeżysz swoją wiedzę przed rozmową kwalifikacyjną lub nauczysz się czegoś nowego dla siebie. Aby zdobyć praktyczne umiejętności, studiuj w JavaRush . Artykuł oryginalny Linki do innych części: Java Core. Pytania do rozmowy kwalifikacyjnej, część 1 Java Core. Pytania do rozmowy kwalifikacyjnej, część 3

Dlaczego należy unikać metody finalize()?

Wszyscy znamy stwierdzenie, że finalize()moduł zbierający elementy bezużyteczne wywołuje metodę przed zwolnieniem pamięci zajmowanej przez obiekt. Oto przykładowy program, który dowodzi, że wywołanie metody finalize()nie jest gwarantowane:
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();
	}
	}
}
Dane wyjściowe: W bloku try W bloku catch W bloku wreszcie W bloku try W bloku catch W bloku wreszcie W bloku try W bloku catch W bloku wreszcie W bloku finalnie metoda finalizenie została wykonana dla żadnego wątku. To potwierdza moje słowa. Myślę, że powodem jest to, że finalizatory są wykonywane przez oddzielny wątek modułu zbierającego elementy bezużyteczne. Jeśli wirtualna maszyna Java zakończy działanie zbyt wcześnie, moduł zbierający elementy bezużyteczne nie będzie miał wystarczająco dużo czasu na utworzenie i wykonanie finalizatorów. Innymi powodami niestosowania tej metody finalize()mogą być:
  1. Metoda finalize()nie działa z łańcuchami takimi jak konstruktory. Oznacza to, że gdy wywołasz konstruktor klasy, konstruktory nadklasy zostaną wywołane bezwarunkowo. Jednak w przypadku metody finalize()tak się nie stanie. Metodę nadklasy finalize()należy wywołać jawnie.
  2. Każdy wyjątek zgłoszony przez metodę finalizejest ignorowany przez wątek modułu zbierającego elementy bezużyteczne i nie będzie dalej propagowany, co oznacza, że ​​zdarzenie nie zostanie zapisane w logach. To bardzo źle, prawda?
  3. finalize()Jeśli metoda jest obecna w Twojej klasie, otrzymujesz również znaczną obniżkę wydajności . W „Effective Programming” (wyd. 2) Joshua Bloch powiedział:
    „Tak i jeszcze jedno: używanie finalizatorów powoduje duży spadek wydajności. Na moim komputerze czas tworzenia i niszczenia prostych obiektów wynosi około 5,6 nanosekundy.
    Dodanie finalizatora zwiększa czas do 2400 nanosekund. Innymi słowy, tworzenie i usuwanie obiektu za pomocą finalizatora jest około 430 razy wolniejsze.

Dlaczego nie należy używać HashMap w środowisku wielowątkowym? Czy może to spowodować nieskończoną pętlę?

Wiemy, że HashMapjest to niezsynchronizowana kolekcja, której zsynchronizowanym odpowiednikiem jest HashTable. Tak więc, gdy uzyskujesz dostęp do kolekcji w środowisku wielowątkowym, w którym wszystkie wątki mają dostęp do pojedynczej instancji kolekcji, bezpieczniej jest używać HashTablez oczywistych powodów, takich jak unikanie brudnych odczytów i zapewnienie spójności danych. W najgorszym przypadku to wielowątkowe środowisko spowoduje nieskończoną pętlę. Tak, to prawda. HashMap.get()może spowodować nieskończoną pętlę. Zobaczmy jak? Jeśli spojrzysz na kod źródłowy metody HashMap.get(Object key), wygląda to tak:
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)zawsze może paść ofiarą nieskończonej pętli w wielowątkowym środowisku wykonawczym, jeśli z jakiegoś powodu e.nextmoże wskazywać na siebie. Spowoduje to nieskończoną pętlę, ale w jaki sposób e.nextbędzie wskazywała na siebie (to znaczy na e)? Może się to zdarzyć w przypadku metody void transfer(Entry[] newTable)wywoływanej podczas zmiany jej HashMaprozmiaru.
do {
    Entry next = e.next;
    int i = indexFor(e.hash, newCapacity);
    e.next = newTable[i];
    newTable[i] = e;
    e = next;
} while (e != null);
Ten fragment kodu może utworzyć nieskończoną pętlę, jeśli zmiana rozmiaru nastąpi w tym samym czasie, gdy inny wątek będzie próbował zmienić instancję mapy ( HashMap). Jedynym sposobem uniknięcia tego scenariusza jest użycie synchronizacji w kodzie lub jeszcze lepiej użycie zsynchronizowanej kolekcji.

Wyjaśnij abstrakcję i enkapsulację. Jak są połączone?

W prostych słowach Abstrakcja wyświetla tylko te właściwości obiektu, które są istotne dla bieżącego widoku ” . W teorii programowania obiektowego abstrakcja obejmuje zdolność definiowania obiektów reprezentujących abstrakcyjnych „aktorów”, którzy mogą wykonywać pracę, zmieniać i raportować zmiany swojego stanu oraz „wchodzić w interakcję” z innymi obiektami w systemie. Abstrakcja w dowolnym języku programowania działa na wiele sposobów. Można to zobaczyć na podstawie tworzenia procedur definiujących interfejsy dla poleceń języka niskiego poziomu. Niektóre abstrakcje próbują ograniczyć zakres ogólnej reprezentacji potrzeb programisty, całkowicie ukrywając abstrakcje, na których są zbudowane, takie jak wzorce projektowe. Zazwyczaj abstrakcję można postrzegać na dwa sposoby: Abstrakcja danych to sposób tworzenia złożonych typów danych i udostępniania tylko znaczących operacji do interakcji z modelem danych, przy jednoczesnym ukrywaniu wszystkich szczegółów implementacji przed światem zewnętrznym. Abstrakcja wykonania to proces identyfikowania wszystkich znaczących instrukcji i eksponowania ich jako jednostki pracy. Zwykle używamy tej funkcji, gdy tworzymy metodę wykonania jakiejś pracy. Zamykanie danych i metod w klasach w połączeniu z ukrywaniem (za pomocą kontroli dostępu) jest często nazywane enkapsulacją. Wynikiem jest typ danych z charakterystyką i zachowaniem. Hermetyzacja zasadniczo obejmuje również ukrywanie danych i ukrywanie implementacji. „Zawrzyj wszystko, co może się zmienić” . Ten cytat jest dobrze znaną zasadą projektowania. W każdej klasie zmiany danych mogą nastąpić w czasie wykonywania, a zmiany w implementacji mogą nastąpić w przyszłych wersjach. Zatem enkapsulacja dotyczy zarówno danych, jak i implementacji. Można je zatem połączyć w następujący sposób:
  • Abstrakcja to głównie to, co może zrobić klasa [Pomysł]
  • Enkapsulacja to więcej Jak osiągnąć tę funkcjonalność [Wdrożenie]

Różnice między interfejsem a klasą abstrakcyjną?

Główne różnice można wymienić w następujący sposób:
  • Interfejs nie może implementować żadnych metod, ale klasa abstrakcyjna może.
  • Klasa może implementować wiele interfejsów, ale może mieć tylko jedną nadklasę (abstrakcyjną lub nieabstrakcyjną)
  • Interfejs nie jest częścią hierarchii klas. Niepowiązane klasy mogą implementować ten sam interfejs.
Należy pamiętać o tym: „Jeśli możesz w pełni opisać koncepcję w kategoriach „co robi” bez konieczności określania „jak to robi”, wówczas powinieneś użyć interfejsu. Jeśli chcesz uwzględnić szczegóły implementacji, musisz przedstawić swoją koncepcję w klasie abstrakcyjnej. ” Ujmując to inaczej: czy istnieje wiele klas, które można „zgrupować” i opisać jednym rzeczownikiem? Jeśli tak, utwórz klasę abstrakcyjną o nazwie tego rzeczownika i odziedzicz z niej klasy. Przykładowo Cati Dogmoże dziedziczyć po klasie abstrakcyjnej Animal, a ta abstrakcyjna klasa bazowa będzie implementować metodę void Breathe()– Breathe, którą dzięki temu wszystkie zwierzęta wykonają w ten sam sposób. Jakie czasowniki można zastosować do mojej klasy i można je zastosować do innych? Utwórz interfejs dla każdego z tych czasowników. Na przykład wszystkie zwierzęta mogą jeść, więc stworzę interfejs IFeedablei sprawię, że będzie on Animalimplementowany. Tylko wystarczająco dobry, aby zaimplementować interfejs Dog( który może mnie polubić), ale nie wszystkie. Ktoś powiedział: główna różnica polega na tym, gdzie chcesz wdrożyć. Tworząc interfejs, możesz przenieść implementację do dowolnej klasy, która implementuje Twój interfejs. Tworząc klasę abstrakcyjną, możesz udostępnić implementację wszystkich klas pochodnych w jednym miejscu i uniknąć wielu złych rzeczy, takich jak duplikowanie kodu. HorseILikeable

W jaki sposób StringBuffer oszczędza pamięć?

Klasa Stringjest zaimplementowana jako niezmienny obiekt, co oznacza, że ​​kiedy początkowo zdecydujesz się umieścić coś w obiekcie String, maszyna wirtualna przydziela tablicę o stałej długości dokładnie o rozmiarze oryginalnej wartości. Będzie to następnie traktowane jako stała wewnątrz maszyny wirtualnej, co zapewnia znaczną poprawę wydajności, jeśli wartość ciągu nie ulegnie zmianie. Jeśli jednak zdecydujesz się w jakikolwiek sposób zmienić zawartość ciągu, tak naprawdę maszyna wirtualna skopiuje zawartość oryginalnego ciągu do przestrzeni tymczasowej, wprowadzi zmiany, a następnie zapisze je w nowej tablicy pamięci. Zatem dokonanie zmian w wartości ciągu po inicjalizacji jest kosztowną operacją. StringBuffer, natomiast jest realizowany w postaci dynamicznie rozwijającej się tablicy wewnątrz maszyny wirtualnej, co oznacza, że ​​dowolna operacja modyfikacji może nastąpić na istniejącej komórce pamięci, a nowa pamięć zostanie przydzielona w miarę potrzeb. Jednak maszyna wirtualna nie ma możliwości przeprowadzenia optymalizacji, StringBufferponieważ jej zawartość jest uważana za niespójną w każdej instancji.

Dlaczego metody Wait i Notify są zadeklarowane w klasie Object zamiast w Thread?

Metody wait, notify, notifyAllsą potrzebne tylko wtedy, gdy chcesz, aby Twoje wątki miały dostęp do współdzielonych zasobów, a współdzielonym zasobem może być dowolny obiekt Java na stercie. Zatem metody te są zdefiniowane w klasie bazowej, Objectdzięki czemu każdy obiekt ma kontrolę, która pozwala wątkom czekać na swoim monitorze. Java nie ma żadnego specjalnego obiektu używanego do współużytkowania współdzielonego zasobu. Nie zdefiniowano takiej struktury danych. Dlatego obowiązkiem klasy jest Objectmóc stać się zasobem współdzielonym i zapewnić metody pomocnicze, takie jak wait(), notify(), notifyAll(). Java opiera się na koncepcji monitorów Charlesa Hoare’a. W Javie wszystkie obiekty mają monitor. Wątki oczekują na monitorach, więc aby wykonać oczekiwanie, potrzebujemy dwóch parametrów:
  • wątek
  • monitor (dowolny obiekt).
W projektowaniu Java nie można precyzyjnie zdefiniować wątku; zawsze jest to bieżący wątek wykonujący kod. Możemy jednak zdefiniować monitor (czyli obiekt, na którym możemy wywołać metodę wait). To dobry projekt, ponieważ jeśli uda nam się zmusić inny wątek do oczekiwania na konkretnym monitorze, spowoduje to „inwazję”, co utrudni projektowanie/programowanie programów równoległych. Pamiętaj, że w Javie wszystkie operacje zakłócające inne wątki są przestarzałe (na przykład stop()).

Napisz program tworzący zakleszczenie w Javie i naprawiający go

W Javie deadlockjest to sytuacja, w której co najmniej dwa wątki blokują różne zasoby i oba czekają, aż drugi zasób stanie się dostępny, aby zakończyć swoje zadanie. I żaden z nich nie jest w stanie pozostawić blokady na przechowywanym zasobie. Rdzeń Javy.  Pytania do rozmowy kwalifikacyjnej, część 2 - 2 Przykładowy program:
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;
		}
	}
}
Uruchomienie powyższego kodu spowoduje zakleszczenie z bardzo oczywistych powodów (wyjaśnionych powyżej). Teraz musimy rozwiązać ten problem. Wierzę, że rozwiązanie każdego problemu leży u podstaw samego problemu. W naszym przypadku głównym problemem jest model dostępu do A i B. Dlatego, aby go rozwiązać, po prostu zmieniamy kolejność operatorów dostępu do zasobów współdzielonych. Po zmianie będzie to wyglądać następująco:
// 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");
			}
		}
	}
};
Uruchom tę klasę ponownie, a teraz nie zobaczysz impasu. Mam nadzieję, że pomoże Ci to uniknąć zakleszczeń i pozbyć się ich, jeśli je napotkasz.

Co się stanie, jeśli Twoja klasa implementująca interfejs Serializable zawiera komponent, którego nie można serializować? Jak to naprawić?

W takim przypadku zostanie on wyrzucony NotSerializableExceptionpodczas wykonywania. Aby rozwiązać ten problem, istnieje bardzo proste rozwiązanie — zaznacz te pola transient. Oznacza to, że zaznaczone pola nie będą serializowane. Jeśli chcesz także przechowywać stan tych pól, musisz wziąć pod uwagę zmienne referencyjne, które już implementują Serializable. Może być również konieczne użycie metod readResolve()i writeResolve(). Podsumujmy:
  • Najpierw spraw, aby Twoje pole nie podlegało serializacji transient.
  • Najpierw writeObjectwywołaj defaultWriteObjectwątek, aby zapisać wszystkie inne niż transientpola, a następnie wywołaj pozostałe metody, aby serializować poszczególne właściwości obiektu, którego nie można serializować.
  • W readObject, najpierw wywołaj defaultReadObjectstrumień, aby odczytać wszystkie transientpola inne niż, a następnie wywołaj inne metody (odpowiadające tym, które dodałeś w writeObject), aby deserializować swój obiekt niebędący transientobiektem.

Wyjaśnij przejściowe i niestabilne słowa kluczowe w Javie

„Słowo kluczowe transientsłuży do wskazania pól, które nie będą serializowane.” Zgodnie ze specyfikacją języka Java: Zmienne można oznaczyć wskaźnikiem przejściowym, aby wskazać, że nie są częścią trwałego stanu obiektu. Na przykład możesz zawierać pola pochodzące z innych pól i lepiej jest uzyskać je programowo, niż przywracać ich stan poprzez serializację. Na przykład w klasie BankPayment.javapola takie jak principal(dyrektor) i rate(stawka) mogą być serializowane, a interest(naliczone odsetki) mogą być obliczane w dowolnym momencie, nawet po deserializacji. Jeśli pamiętamy, każdy wątek w Javie ma własną pamięć lokalną i wykonuje operacje odczytu/zapisu w tej pamięci lokalnej. Po wykonaniu wszystkich operacji zapisuje zmodyfikowany stan zmiennej do pamięci współdzielonej, skąd wszystkie wątki uzyskują dostęp do zmiennej. Zazwyczaj jest to normalny wątek wewnątrz maszyny wirtualnej. Jednak modyfikator volatile informuje maszynę wirtualną, że dostęp wątku do tej zmiennej musi zawsze odpowiadać jego własnej kopii tej zmiennej z główną kopią zmiennej w pamięci. Oznacza to, że za każdym razem, gdy wątek chce odczytać stan zmiennej, musi wyczyścić stan pamięci wewnętrznej i zaktualizować zmienną z pamięci głównej. Volatilenajbardziej przydatne w algorytmach bez blokad. Oznaczasz zmienną przechowującą udostępnione dane jako niestabilną, następnie nie używasz blokad, aby uzyskać dostęp do tej zmiennej, a wszystkie zmiany dokonane przez jeden wątek będą widoczne dla innych. Lub jeśli chcesz utworzyć relację „zdarzenie późniejsze”, aby mieć pewność, że obliczenia nie zostaną powtórzone, ponownie, aby zapewnić widoczność zmian w czasie rzeczywistym. Volatile powinno być używane do bezpiecznego publikowania niezmiennych obiektów w środowisku wielowątkowym. Deklaracja pola public volatile ImmutableObjectgwarantuje, że wszystkie wątki zawsze zobaczą aktualnie dostępne odwołanie do instancji.

Różnica między Iteratorem a ListIteratorem?

Możemy używać lub do Iteratoriteracji po elementach . Można go jednak używać tylko do iteracji po elementach . Inne różnice opisano poniżej. Możesz: SetListMapListIteratorList
  1. iteruj w odwrotnej kolejności.
  2. uzyskać indeks w dowolnym miejscu.
  3. dodaj dowolną wartość w dowolnym miejscu.
  4. ustawić dowolną wartość w bieżącej pozycji.
Powodzenia z Twoimi studiami!! Autor artykułu Lokesh Gupta Artykuł oryginalny Java Core. Pytania do rozmowy kwalifikacyjnej, część 1 Java Core. Pytania do rozmowy kwalifikacyjnej, część 3
Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION