JavaRush /Java Blog /Random-IT /Analisi di domande e risposte da interviste per sviluppat...

Analisi di domande e risposte da interviste per sviluppatori Java. Parte 15

Pubblicato nel gruppo Random-IT
Ciao Ciao! Quanto deve sapere uno sviluppatore Java? Puoi discutere a lungo su questo tema, ma la verità è che al colloquio sarai guidato al massimo dalla teoria. Anche in quelle aree di conoscenza che non avrai la possibilità di utilizzare nel tuo lavoro. Analisi di domande e risposte da interviste per sviluppatori Java.  Parte 15 - 1Bene, se sei un principiante, le tue conoscenze teoriche verranno prese molto sul serio. Poiché non esistono ancora esperienza e grandi risultati, non resta che verificare la forza della base di conoscenza. Oggi continueremo a rafforzare proprio questa base esaminando le domande più popolari nelle interviste per gli sviluppatori Java. Voliamo!

Nucleo Java

9. Qual è la differenza tra associazione statica e dinamica in Java?

Ho già risposto a questa domanda in questo articolo alla domanda 18 sul polimorfismo statico e dinamico, ti consiglio di leggerlo.

10. È possibile utilizzare variabili private o protette in un'interfaccia?

No, non puoi. Perché quando dichiari un'interfaccia, il compilatore Java aggiunge automaticamente le parole chiave public e abstract prima dei metodi dell'interfaccia e le parole chiave public , static e final prima dei membri dati. In realtà, se aggiungi private o protected , sorgerà un conflitto e il compilatore si lamenterà del modificatore di accesso con il messaggio: "Modificatore '<modificatore selezionato>' non consentito qui." Perché il compilatore aggiunge public , static e final variabili nell'interfaccia? Scopriamolo:
  • public : l'interfaccia consente al client di interagire con l'oggetto. Se le variabili non fossero pubbliche, i client non vi avrebbero accesso.
  • statico : non è possibile creare interfacce (o meglio, i loro oggetti), quindi la variabile è statica.
  • final - poiché l'interfaccia viene utilizzata per ottenere un'astrazione del 100%, la variabile ha la sua forma finale (e non verrà modificata).

11. Cos'è Classloader e a cosa serve?

Classloader - o Class Loader - fornisce il caricamento delle classi Java. Più precisamente, il caricamento è assicurato dai suoi discendenti: caricatori di classi specifici, perché ClassLoader stesso è astratto. Ogni volta che viene caricato un file .class, ad esempio, dopo aver chiamato un costruttore o un metodo statico della classe corrispondente, questa azione viene eseguita da uno dei discendenti della classe ClassLoader . Esistono tre tipi di eredi:
  1. Bootstrap ClassLoader è un caricatore di base, implementato a livello JVM e non riceve feedback dall'ambiente runtime, poiché fa parte del kernel JVM ed è scritto in codice nativo. Questo caricatore funge da genitore di tutte le altre istanze di ClassLoader.

    Principalmente responsabile del caricamento delle classi interne JDK, solitamente rt.jar e altre librerie principali situate nella directory $JAVA_HOME/jre/lib . Piattaforme diverse possono avere implementazioni diverse di questo caricatore di classi.

  2. Extension Classloader è un caricatore di estensioni, un discendente della classe caricatore di base. Si occupa di caricare l'estensione delle classi base Java standard. Caricato dalla directory delle estensioni JDK, in genere $JAVA_HOME/lib/ext o qualsiasi altra directory menzionata nella proprietà di sistema java.ext.dirs (questa opzione può essere utilizzata per controllare il caricamento delle estensioni).

  3. System ClassLoader è un caricatore di sistema implementato a livello JRE che si occupa di caricare tutte le classi a livello di applicazione nella JVM. Carica i file trovati nella variabile di ambiente della classe -classpath o nell'opzione della riga di comando -cp .

Analisi di domande e risposte da interviste per sviluppatori Java.  Parte 15 - 2I caricatori di classi fanno parte del runtime Java. Nel momento in cui la JVM richiede una classe, il caricatore della classe tenta di trovare la classe e caricare la definizione della classe nel runtime utilizzando il nome completo della classe. Il metodo java.lang.ClassLoader.loadClass() è responsabile del caricamento della definizione della classe in fase di runtime. Tenta di caricare una classe in base al suo nome completo. Se la classe non è stata ancora caricata, delega la richiesta al caricatore della classe genitore. Questo processo avviene in modo ricorsivo e si presenta così:
  1. System Classloader tenta di trovare la classe nella cache.

    • 1.1. Se la classe viene trovata, il caricamento viene completato con successo.

    • 1.2. Se la classe non viene trovata, il caricamento viene delegato all'Extension Classloader.

  2. Extension Classloader tenta di trovare la classe nella propria cache.

    • 2.1. Se la classe viene trovata, viene completata correttamente.

    • 2.2. Se la classe non viene trovata, il caricamento viene delegato al Bootstrap Classloader.

  3. Bootstrap Classloader tenta di trovare la classe nella propria cache.

    • 3.1. Se la classe viene trovata, il caricamento viene completato con successo.

    • 3.2. Se la classe non viene trovata, il Bootstrap Classloader sottostante tenterà di caricarla.

  4. In caso di caricamento:

    • 4.1. Riuscito: il caricamento della classe è stato completato.

    • 4.2. Se fallisce, il controllo viene trasferito all'Extension Classloader.

  5. 5. Extension Classloader tenta di caricare la classe e, durante il caricamento:

    • 5.1. Riuscito: il caricamento della classe è stato completato.

    • 5.2. In caso di esito negativo, il controllo viene trasferito a System Classloader.

  6. 6. System Classloader tenta di caricare la classe e, in caso di caricamento:

    • 6.1. Riuscito: il caricamento della classe è stato completato.

    • 6.2. Non è stato superato correttamente: viene generata un'eccezione: ClassNotFoundException.

L'argomento dei caricatori di classi è vasto e non dovrebbe essere trascurato. Per conoscerlo più in dettaglio, ti consiglio di leggere questo articolo e non ci dilungheremo e andremo avanti.

12. Cosa sono le aree dati di runtime?

Aree dati runtime : aree dati runtime JVM. La JVM definisce alcune aree di dati di runtime necessarie durante l'esecuzione del programma. Alcuni di essi vengono creati all'avvio della JVM. Altri sono thread-local e vengono creati solo quando viene creato il thread (e distrutti quando il thread viene distrutto). Le aree dei dati di runtime JVM hanno questo aspetto: Analisi di domande e risposte da interviste per sviluppatori Java.  Parte 15 - 3
  • PC Register è locale per ciascun thread e contiene l'indirizzo dell'istruzione JVM che il thread sta attualmente eseguendo.

  • Lo stack JVM è un'area di memoria utilizzata come spazio di archiviazione per variabili locali e risultati temporanei. Ogni thread ha il proprio stack separato: non appena il thread termina, anche questo stack viene distrutto. Vale la pena notare che il vantaggio dello stack rispetto all'heap è rappresentato dalle prestazioni, mentre l'heap presenta sicuramente un vantaggio in termini di scalabilità dello storage.

  • Stack di metodi nativi: un'area dati per thread che memorizza elementi di dati, simili allo stack JVM, per l'esecuzione di metodi nativi (non Java).

  • Heap: utilizzato da tutti i thread come archivio che contiene oggetti, metadati di classi, array, ecc., creati in fase di runtime. Quest'area viene creata all'avvio della JVM e viene distrutta quando si spegne.

  • Area del metodo: quest'area di runtime è comune a tutti i thread e viene creata all'avvio della JVM. Memorizza strutture per ciascuna classe, come il pool di costanti di runtime, il codice per costruttori e metodi, i dati dei metodi, ecc.

13. Cos'è un oggetto immutabile?

In questa parte dell'articolo, alle domande 14 e 15, c'è già una risposta a questa domanda, quindi date un'occhiata senza perdere tempo.

14. Cosa rende speciale la classe String?

All'inizio dell'analisi abbiamo parlato più volte di alcune funzionalità di String (per questo c'era una sezione separata). Ora riassumiamo le caratteristiche di String :
  1. È l'oggetto più popolare in Java e viene utilizzato per vari scopi. In termini di frequenza di utilizzo, non è inferiore nemmeno ai tipi primitivi.

  2. Un oggetto di questa classe può essere creato senza utilizzare la parola chiave new - direttamente tra virgolette String str = “string”; .

  3. La stringa è una classe immutabile : quando crei un oggetto di questa classe, i suoi dati non possono essere modificati (quando aggiungi + "un'altra stringa" a una determinata stringa, di conseguenza otterrai una nuova, terza stringa). L'immutabilità della classe String la rende thread-safe.

  4. La classe String è finalizzata (ha il modificatore final ), quindi non può essere ereditata.

  5. La stringa ha il proprio pool di stringhe, un'area di memoria nell'heap che memorizza nella cache i valori di stringa che crea. In questa parte della serie , nella domanda 62, ho descritto lo string pool.

  6. Java ha analoghi di String , progettati anch'essi per funzionare con le stringhe: StringBuilder e StringBuffer , ma con la differenza che sono mutabili. Puoi leggere di più su di loro in questo articolo .

Analisi di domande e risposte da interviste per sviluppatori Java.  Parte 15 - 4

15. Cos'è la covarianza del tipo?

Per comprendere la covarianza, esamineremo un esempio. Diciamo che abbiamo una classe animale:
public class Animal {
 void voice() {
   System.out.println("*тишина*");
 }
}
E alcune classi di cani lo estendono :
public class Dog extends Animal {

 @Override
 public void voice() {
   System.out.println("Гав, гав, гав!!!");
 }
}
Come ricordiamo, possiamo facilmente assegnare oggetti del tipo erede al tipo genitore:
Animal animal = new Dog();
Questo non sarà altro che polimorfismo. Comodo, flessibile, non è vero? E che dire dell'elenco degli animali? Possiamo dare ad una lista con un generico Animal una lista con oggetti Dog ?
List<Dog> dogs = new ArrayList<>();
List<Animal> animals = dogs;
In questo caso la riga per l'assegnazione dell'elenco dei cani all'elenco degli animali sarà sottolineata in rosso, ovvero il compilatore non passerà questo codice. Nonostante il fatto che questa assegnazione sembri abbastanza logica (dopo tutto, possiamo assegnare un oggetto Dog a una variabile di tipo Animal ), non può essere eseguita. Questo perché se fosse consentito, potremmo inserire un oggetto Animale in una lista che originariamente doveva essere un Cane , pensando che nella lista ci fossero solo Cani . E poi, ad esempio, useremo il metodo get() per prendere un oggetto da quella lista di cani , pensando che sia un cane, e chiamare su di esso un metodo dell'oggetto Dog , che Animal non ha . E come capisci, questo è impossibile: si verificherà un errore. Ma, fortunatamente, il compilatore non trascura questo errore logico assegnando una lista di discendenti a una lista di genitori (e viceversa). In Java è possibile assegnare oggetti elenco solo a variabili elenco con generici corrispondenti. Questa è chiamata invariazione. Se potessero farlo, si chiamerebbe e si chiama covarianza. Cioè, la covarianza è se potessimo impostare un oggetto di tipo ArrayList<Dog> su una variabile di tipo List<Animal> . Si scopre che la covarianza non è supportata in Java? Non importa come sia! Ma questo viene fatto in un modo speciale. A cosa serve il disegno ? estende Animale . Viene posizionato con un generico della variabile a cui vogliamo impostare l'oggetto lista, con un generico del discendente. Questa costruzione generica significa che andrà bene qualsiasi tipo che sia un discendente del tipo Animale (e anche il tipo Animale rientra in questa generalizzazione). A sua volta, Animal può essere non solo una classe, ma anche un'interfaccia (non fatevi ingannare dalla parola chiave extends ). Possiamo svolgere il nostro compito precedente in questo modo: Analisi di domande e risposte da interviste per sviluppatori Java.  Parte 15 - 5
List<Dog> dogs = new ArrayList<>();
List<? extends Animal> animals = dogs;
Di conseguenza, vedrai nell'IDE che il compilatore non si lamenterà di questa costruzione. Controlliamo la funzionalità di questo design. Diciamo che abbiamo un metodo che fa sì che tutti gli animali che gli passano emettano suoni:
public static void animalsVoice(List<? extends Animal> animals) {
 for (Animal animal : animals) {
   animal.voice();
 }
}
Diamogli un elenco di cani:
List<Dog> dogs = new ArrayList<>();
dogs.add(new Dog());
dogs.add(new Dog());
dogs.add(new Dog());
animalsVoice(dogs);
Nella console vedremo il seguente output:
Bau bau bau!!! Bau bau bau!!! Bau bau bau!!!
Ciò significa che questo approccio alla covarianza funziona con successo. Vorrei notare che questo generico è incluso nell'elenco ? estende Animale non possiamo inserire nuovi dati di nessun tipo: né il tipo Cane , né tantomeno il tipo Animale :
List<Dog> dogs = new ArrayList<>();
List<? extends Animal> animals = dogs;
animals.add(new Dog());
dogs.add(new Animal());
Nelle ultime due righe infatti il ​​compilatore evidenzierà in rosso l'inserimento degli oggetti. Ciò è dovuto al fatto che non possiamo essere sicuri al cento per cento di quale elenco di oggetti di quale tipo verrà assegnato all'elenco con dati dal generico <? estende Animale> . Analisi di domande e risposte da interviste per sviluppatori Java.  Parte 15 - 6Vorrei parlare anche di controvarianza , poiché di solito questo concetto va sempre di pari passo con la covarianza, e di regola se ne chiede conto insieme. Questo concetto è in qualche modo l'opposto della covarianza, poiché questo costrutto utilizza il tipo erede. Diciamo che vogliamo un elenco a cui può essere assegnato un elenco di oggetti di tipo che non sono antenati dell'oggetto Dog . Tuttavia, non sappiamo in anticipo quali saranno le tipologie specifiche. In questo caso, una costruzione della forma ? super Cane , per il quale sono adatti tutti i tipi - i progenitori della classe Cane :
List<Animal> animals = new ArrayList<>();
List<? super Dog> dogs = animals;
dogs.add(new Dog());
dogs.add(new Dog());
Possiamo tranquillamente aggiungere oggetti di tipo Dog all'elenco con un nome così generico , perché in ogni caso ha tutti i metodi implementati di uno qualsiasi dei suoi antenati. Ma non potremo aggiungere un oggetto di tipo Animal , poiché non c'è la certezza che ci saranno oggetti di questo tipo al suo interno, e non, ad esempio, Dog . Dopotutto, possiamo richiedere a un elemento di questa lista un metodo della classe Dog , che Animal non avrà . In questo caso si verificherà un errore di compilazione. Inoltre, se volessimo implementare il metodo precedente, ma con questo generico:
public static void animalsVoice(List<? super Dog> dogs) {
 for (Dog dog : dogs) {
   dog.voice();
 }
}
otterremmo un errore di compilazione nel ciclo for , poiché non possiamo essere sicuri che l'elenco restituito contenga oggetti di tipo Dog e siamo liberi di utilizzare i suoi metodi. Se chiamiamo il metodo dogs.get(0) in questo elenco . - otterremo un oggetto di tipo Object . Cioè, affinché il metodo AnimalsVoice() funzioni , dobbiamo almeno aggiungere piccole manipolazioni restringendo il tipo di dati:
public static void animalsVoice(List<? super Dog> dogs) {
 for (Object obj : dogs) {
   if (obj instanceof Dog) {
     Dog dog = (Dog) obj;
     dog.voice();
   }
 }
}
Analisi di domande e risposte da interviste per sviluppatori Java.  Parte 15 - 7

16. Come sono presenti i metodi nella classe Object?

In questa parte della serie, nel paragrafo 11, ho già risposto a questa domanda, quindi ti consiglio vivamente di leggerlo se non l'hai ancora fatto. Ecco dove finiremo per oggi. Ci vediamo nella prossima parte! Analisi di domande e risposte da interviste per sviluppatori Java.  Parte 15 - 8
Commenti
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION