JavaRush /Java Blog /Random-IT /Nucleo Java. Domande dell'intervista, parte 2
Andrey
Livello 26

Nucleo Java. Domande dell'intervista, parte 2

Pubblicato nel gruppo Random-IT
Per chi sente la parola Java Core per la prima volta, questi sono i principi fondamentali del linguaggio. Con questa conoscenza, puoi tranquillamente andare a fare uno stage/tirocinio.
Nucleo Java.  Domande per un'intervista, parte 2 - 1
Queste domande ti aiuteranno a rinfrescare le tue conoscenze prima del colloquio o a imparare qualcosa di nuovo per te stesso. Per acquisire competenze pratiche, studia presso JavaRush . Articolo originale Collegamenti ad altre parti: Java Core. Domande dell'intervista, parte 1 Java Core. Domande per un'intervista, parte 3

Perché il metodo finalize() dovrebbe essere evitato?

Conosciamo tutti l'affermazione secondo cui un metodo finalize()viene chiamato dal garbage collector prima di liberare la memoria occupata da un oggetto. Ecco un programma di esempio che dimostra che una chiamata al metodo finalize()non è garantita:
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();
	}
	}
}
Output: In try blocco In catch blocco In finalmente blocco In try blocco In catch blocco In finalmente blocco In try blocco In catch blocco In finalmente blocco Sorprendentemente, il metodo finalizenon è stato eseguito per nessun thread. Ciò dimostra le mie parole. Penso che il motivo sia che i finalizzatori vengono eseguiti da un thread separato del Garbage Collector. Se la Java Virtual Machine si arresta troppo presto, il Garbage Collector non avrà abbastanza tempo per creare ed eseguire i finalizzatori. Altri motivi per non utilizzare il metodo finalize()potrebbero essere:
  1. Il metodo finalize()non funziona con catene come i costruttori. Ciò significa che quando chiami un costruttore di classe, i costruttori della superclasse verranno chiamati incondizionatamente. Ma nel caso del metodo finalize(), ciò non accadrà. Il metodo della superclasse finalize()deve essere chiamato esplicitamente.
  2. Qualsiasi eccezione lanciata dal metodo finalizeviene ignorata dal thread del Garbage Collector e non verrà propagata ulteriormente, il che significa che l'evento non verrà registrato nei log. Questo è molto brutto, non è vero?
  3. Ottieni anche una significativa penalità in termini di prestazioni se il metodo finalize()è presente nella tua classe. In Effective Programming (2a ed.), Joshua Bloch ha detto:
    “Sì, e un’altra cosa: c’è una grande penalità in termini di prestazioni quando si utilizzano i finalizzatori. Sulla mia macchina, il tempo necessario per creare e distruggere oggetti semplici è di circa 5,6 nanosecondi.
    L'aggiunta di un finalizzatore aumenta il tempo a 2400 nanosecondi. In altre parole, è circa 430 volte più lento creare ed eliminare un oggetto con un finalizzatore.”

Perché HashMap non dovrebbe essere utilizzato in un ambiente multi-thread? Ciò potrebbe causare un ciclo infinito?

Sappiamo che HashMapsi tratta di una raccolta non sincronizzata, la cui controparte sincronizzata è HashTable. Pertanto, quando si accede a una raccolta e in un ambiente multi-thread in cui tutti i thread hanno accesso a una singola istanza della raccolta, è più sicuro da utilizzare HashTableper ovvi motivi, come evitare letture sporche e garantire la coerenza dei dati. Nel peggiore dei casi, questo ambiente multi-thread causerà un ciclo infinito. Si è vero. HashMap.get()potrebbe causare un ciclo infinito. Vediamo come? Se guardi il codice sorgente del metodo HashMap.get(Object key), appare così:
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)può sempre cadere vittima di un ciclo infinito in un ambiente di runtime multi-thread se per qualche motivo e.nextpuò puntare a se stesso. Ciò causerà un ciclo infinito, ma come e.nextpunterà a se stesso (cioè a e)? Ciò può verificarsi in un metodo void transfer(Entry[] newTable)chiamato durante il HashMapridimensionamento.
do {
    Entry next = e.next;
    int i = indexFor(e.hash, newCapacity);
    e.next = newTable[i];
    newTable[i] = e;
    e = next;
} while (e != null);
Questo pezzo di codice tende a creare un ciclo infinito se il ridimensionamento avviene nello stesso momento in cui un altro thread sta tentando di modificare l'istanza della mappa ( HashMap). L'unico modo per evitare questo scenario è utilizzare la sincronizzazione nel codice o, meglio ancora, utilizzare una raccolta sincronizzata.

Spiegare l'astrazione e l'incapsulamento. Come sono collegati?

In parole semplici , " L'astrazione mostra solo le proprietà di un oggetto che sono significative per la vista corrente . " Nella teoria della programmazione orientata agli oggetti, l'astrazione implica la capacità di definire oggetti che rappresentano "attori" astratti che possono eseguire lavori, cambiare e segnalare cambiamenti nel loro stato e "interagire" con altri oggetti nel sistema. L'astrazione in qualsiasi linguaggio di programmazione funziona in molti modi. Ciò può essere visto dalla creazione di routine per definire interfacce per comandi di linguaggio di basso livello. Alcune astrazioni cercano di limitare l'ampiezza della rappresentazione complessiva dei bisogni di un programmatore nascondendo completamente le astrazioni su cui sono costruite, come i design pattern. In genere, l'astrazione può essere vista in due modi: l'astrazione dei dati è un modo per creare tipi di dati complessi ed esporre solo operazioni significative per interagire con il modello dati, nascondendo allo stesso tempo tutti i dettagli di implementazione al mondo esterno. L'astrazione dell'esecuzione è il processo di identificazione di tutte le affermazioni significative e di esposizione come unità di lavoro. Di solito utilizziamo questa funzionalità quando creiamo un metodo per svolgere del lavoro. Il confinamento di dati e metodi all'interno delle classi in combinazione con l'esecuzione dell'occultamento (utilizzando il controllo degli accessi) è spesso chiamato incapsulamento. Il risultato è un tipo di dati con caratteristiche e comportamento. L'incapsulamento implica essenzialmente anche l'occultamento dei dati e l'occultamento dell'implementazione. "Incapsulare tutto ciò che può cambiare" . Questa citazione è un principio di progettazione ben noto. Del resto, in qualsiasi classe, le modifiche ai dati possono avvenire in fase di esecuzione e le modifiche all'implementazione possono avvenire nelle versioni future. Pertanto, l'incapsulamento si applica sia ai dati che all'implementazione. Quindi possono essere collegati in questo modo:
  • L'astrazione è principalmente ciò che una classe può fare [Idea]
  • L'incapsulamento è qualcosa di più Come ottenere questa funzionalità [Implementazione]

Differenze tra interfaccia e classe astratta?

Le principali differenze possono essere elencate come segue:
  • Un'interfaccia non può implementare alcun metodo, ma una classe astratta sì.
  • Una classe può implementare molte interfacce, ma può avere solo una superclasse (astratta o non astratta)
  • Un'interfaccia non fa parte di una gerarchia di classi. Le classi non correlate possono implementare la stessa interfaccia.
Quello che devi ricordare è questo: “Quando puoi descrivere completamente un concetto in termini di “cosa fa” senza dover specificare “come lo fa”, allora dovresti usare un’interfaccia. Se devi includere alcuni dettagli di implementazione, devi rappresentare il tuo concetto in una classe astratta." Inoltre, per dirla in altro modo: ci sono molte classi che possono essere "raggruppate insieme" e descritte da un singolo sostantivo? Se è così, crea una classe astratta con il nome di questo sostantivo ed eredita le classi da esso. Ad esempio, Cate Dogpuò ereditare dalla classe astratta Animale questa classe base astratta implementerà il metodo void Breathe()respira, che tutti gli animali eseguiranno quindi allo stesso modo. Quali verbi possono essere applicati alla mia classe e possono essere applicati agli altri? Crea un'interfaccia per ciascuno di questi verbi. Ad esempio, tutti gli animali possono mangiare, quindi creerò un'interfaccia IFeedablee farò in modo che Animalimplementi quell'interfaccia. Buono solo quanto basta per implementare un'interfaccia Dog( in grado di piacermi), ma non tutto. Qualcuno ha detto: la differenza principale è dove vuoi che venga implementata. Quando crei un'interfaccia, puoi spostare l'implementazione in qualsiasi classe che implementa la tua interfaccia. Creando una classe astratta, puoi condividere l'implementazione di tutte le classi derivate in un unico posto ed evitare molti problemi come la duplicazione del codice. HorseILikeable

In che modo StringBuffer risparmia memoria?

La classe Stringè implementata come un oggetto immutabile, il che significa che quando inizialmente decidi di inserire qualcosa nell'oggetto String, la macchina virtuale alloca un array di lunghezza fissa esattamente della dimensione del tuo valore originale. Questo verrà quindi trattato come una costante all'interno della macchina virtuale, che fornisce un significativo miglioramento delle prestazioni se il valore della stringa non cambia. Tuttavia, se decidi di modificare in qualsiasi modo il contenuto di una stringa, ciò che fa effettivamente la macchina virtuale è copiare il contenuto della stringa originale nello spazio temporaneo, apportare le modifiche, quindi salvarle in un nuovo array di memoria. Pertanto, apportare modifiche al valore di una stringa dopo l'inizializzazione è un'operazione costosa. StringBuffer, d'altro canto, è implementato come un array ad espansione dinamica all'interno della macchina virtuale, il che significa che qualsiasi operazione di modifica può avvenire su una cella di memoria esistente e la nuova memoria verrà allocata secondo necessità. Tuttavia, non è possibile che la macchina virtuale esegua l'ottimizzazione StringBufferpoiché i suoi contenuti sono considerati incoerenti in ciascuna istanza.

Perché i metodi wait e notify sono dichiarati nella classe Object anziché in Thread?

I metodi wait, notify, notifyAllsono necessari solo quando vuoi che i tuoi thread abbiano accesso alle risorse condivise e la risorsa condivisa potrebbe essere qualsiasi oggetto Java nell'heap. Pertanto, questi metodi sono definiti nella classe base Objectin modo che ogni oggetto abbia un controllo che consente ai thread di attendere sul proprio monitor. Java non dispone di alcun oggetto speciale utilizzato per condividere una risorsa condivisa. Nessuna struttura di dati di questo tipo è definita. Pertanto, è responsabilità della classe Objectpoter diventare una risorsa condivisa e fornire metodi di supporto come wait(), notify(), notifyAll(). Java si basa sull'idea dei monitor di Charles Hoare. In Java tutti gli oggetti hanno un monitor. I thread attendono sui monitor, quindi per eseguire l'attesa abbiamo bisogno di due parametri:
  • un filo
  • monitor (qualsiasi oggetto).
Nella progettazione Java, un thread non può essere definito con precisione; è sempre il thread corrente che esegue il codice. Tuttavia, possiamo definire un monitor (che è un oggetto su cui possiamo chiamare un metodo wait). Questa è una buona progettazione perché se riusciamo a forzare qualsiasi altro thread ad attendere su un monitor specifico, si verificherà una "invasione", rendendo difficile la progettazione/programmazione di programmi paralleli. Ricorda che in Java tutte le operazioni che interferiscono con altri thread sono deprecate (ad esempio stop()).

Scrivi un programma per creare un punto morto in Java e risolverlo

In Java deadlock, questa è una situazione in cui almeno due thread mantengono un blocco su risorse diverse ed entrambi aspettano che l'altra risorsa diventi disponibile per completare la propria attività. E nessuno di loro è in grado di lasciare un lucchetto sulla risorsa trattenuta. Nucleo Java.  Domande per un'intervista, parte 2 - 2 Programma di esempio:
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;
		}
	}
}
L'esecuzione del codice precedente risulterà in una situazione di stallo per ragioni molto ovvie (spiegate sopra). Ora dobbiamo risolvere questo problema. Credo che la soluzione a qualsiasi problema stia alla radice del problema stesso. Nel nostro caso il modello di accesso ad A e B è il problema principale. Pertanto, per risolverlo, basterà cambiare l'ordine degli operatori di accesso alle risorse condivise. Dopo la modifica apparirà così:
// 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");
			}
		}
	}
};
Esegui di nuovo questa classe e ora non vedrai lo stallo. Spero che questo ti aiuti a evitare blocchi e a sbarazzartene se li incontri.

Cosa succede se la tua classe che implementa l'interfaccia Serializable contiene un componente non serializzabile? Come risolvere questo problema?

In questo caso, verrà lanciato NotSerializableExceptiondurante l'esecuzione. Per risolvere questo problema, esiste una soluzione molto semplice: seleziona queste caselle transient. Ciò significa che i campi selezionati non verranno serializzati. Se si desidera memorizzare anche lo stato di questi campi, è necessario considerare le variabili di riferimento, che già implementano il file Serializable. Potrebbe anche essere necessario utilizzare i metodi readResolve()e writeResolve(). Riassumiamo:
  • Per prima cosa, rendi il tuo campo non serializzabile transient.
  • Per prima cosa writeObject, chiama defaultWriteObjectil thread per salvare tutti i non transientcampi, quindi chiama i metodi rimanenti per serializzare le singole proprietà del tuo oggetto non serializzabile.
  • In readObject, chiama prima defaultReadObjectlo stream per leggere tutti transienti campi non, quindi chiama altri metodi (corrispondenti a quelli aggiunti in writeObject) per deserializzare il tuo oggetto non transient.

Spiegare le parole chiave transitorie e volatili in Java

"La parola chiave transientviene utilizzata per indicare i campi che non verranno serializzati." Secondo la specifica del linguaggio Java: le variabili possono essere contrassegnate con l'indicatore transitorio per indicare che non fanno parte dello stato persistente dell'oggetto. Ad esempio, potresti contenere campi derivati ​​da altri campi ed è preferibile ottenerli a livello di codice anziché ripristinarne lo stato tramite la serializzazione. Ad esempio, in una classe, BankPayment.javacampi come principal(direttore) e rate(tasso) possono essere serializzati e interest(interessi maturati) possono essere calcolati in qualsiasi momento, anche dopo la deserializzazione. Se ricordiamo, ogni thread in Java ha la propria memoria locale ed esegue operazioni di lettura/scrittura su questa memoria locale. Al termine di tutte le operazioni, scrive lo stato modificato della variabile nella memoria condivisa, da dove tutti i thread accedono alla variabile. In genere, si tratta di un normale thread all'interno di una macchina virtuale. Ma il modificatore volatile dice alla macchina virtuale che l'accesso di un thread a quella variabile deve sempre far corrispondere la propria copia di quella variabile con la copia principale della variabile in memoria. Ciò significa che ogni volta che un thread vuole leggere lo stato di una variabile, deve cancellare lo stato della memoria interna e aggiornare la variabile dalla memoria principale. Volatilepiù utile negli algoritmi senza blocchi. Contrassegni una variabile che memorizza dati condivisi come volatile, quindi non usi i blocchi per accedere a quella variabile e tutte le modifiche apportate da un thread saranno visibili agli altri. Oppure, se desideri creare una relazione "accaduto-dopo" per garantire che i calcoli non vengano ripetuti, sempre per garantire che le modifiche siano visibili in tempo reale. Volatile dovrebbe essere utilizzato per pubblicare in modo sicuro oggetti immutabili in un ambiente multi-thread. La dichiarazione del campo public volatile ImmutableObjectgarantisce che tutti i thread vedano sempre il riferimento attualmente disponibile all'istanza.

Differenza tra Iterator e ListIterator?

Possiamo usare , o Iteratorper scorrere gli elementi . Ma può essere utilizzato solo per scorrere gli elementi . Altre differenze sono descritte di seguito. Puoi: SetListMapListIteratorList
  1. ripetere in ordine inverso.
  2. ottieni l'indice ovunque.
  3. aggiungi qualsiasi valore ovunque.
  4. impostare qualsiasi valore nella posizione corrente.
Buona fortuna con i tuoi studi!! Autore dell'articolo Lokesh Gupta Articolo originale Java Core. Domande dell'intervista, parte 1 Java Core. Domande per un'intervista, parte 3
Commenti
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION