Domande di intervista su Java Generics (+ risposte)

Questo articolo fa parte di una serie: • Domande di intervista sulle raccolte Java

• Domande di intervista sul sistema di tipo Java

• Domande di intervista sulla concorrenza Java (+ risposte)

• Struttura delle classi Java e domande di colloquio sull'inizializzazione

• Domande di intervista Java 8 (+ risposte)

• Gestione della memoria in Java Intervista Domande (+ risposte)

• Domande di intervista Java Generics (+ risposte) (articolo corrente) • Domande di intervista sul controllo del flusso Java (+ risposte)

• Domande di intervista sulle eccezioni Java (+ risposte)

• Domande di intervista sulle annotazioni Java (+ risposte)

• Domande principali per l'intervista su Spring Framework

1. Introduzione

In questo articolo, esamineremo alcuni esempi di domande e risposte dell'intervista ai generici Java.

I generici sono un concetto fondamentale in Java, introdotto per la prima volta in Java 5. Per questo motivo, quasi tutti i codebase Java ne faranno uso, quasi garantendo che uno sviluppatore li incontrerà a un certo punto. Questo è il motivo per cui è essenziale comprenderli correttamente ed è più che probabile che vengano interrogati durante un colloquio.

2. Domande

Q1. Che cos'è un parametro di tipo generico?

Il tipo è il nome di una classe o di un'interfaccia . Come implicito dal nome, un parametro di tipo generico è quando un tipo può essere utilizzato come parametro in una dichiarazione di classe, metodo o interfaccia.

Cominciamo con un semplice esempio, uno senza generici, per dimostrarlo:

public interface Consumer { public void consume(String parameter) }

In questo caso, il tipo di parametro del metodo del metodo consume () è String. Non è parametrizzato e non configurabile.

Ora sostituiamo il nostro tipo String con un tipo generico che chiameremo T. Si chiama così per convenzione:

public interface Consumer { public void consume(T parameter) }

Quando implementiamo il nostro consumatore, possiamo fornire il tipo che vogliamo che utilizzi come argomento. Questo è un parametro di tipo generico:

public class IntegerConsumer implements Consumer { public void consume(Integer parameter) }

In questo caso, ora possiamo consumare interi. Possiamo sostituire questo tipo con quello di cui abbiamo bisogno.

Q2. Quali sono alcuni vantaggi dell'utilizzo di tipi generici?

Un vantaggio dell'utilizzo di generici è evitare i cast e fornire l'indipendenza dai tipi. Ciò è particolarmente utile quando si lavora con le raccolte. Dimostriamo questo:

List list = new ArrayList(); list.add("foo"); Object o = list.get(0); String foo = (String) o;

Nel nostro esempio, il tipo di elemento nel nostro elenco è sconosciuto al compilatore. Ciò significa che l'unica cosa che può essere garantita è che si tratta di un oggetto. Quindi, quando recuperiamo il nostro elemento, un oggetto è ciò che otteniamo. Come autori del codice, sappiamo che è una stringa, ma dobbiamo eseguire il cast del nostro oggetto su uno per risolvere il problema in modo esplicito. Questo produce molto rumore e boilerplate.

Successivamente, se iniziamo a pensare alla possibilità di errore manuale, il problema del casting peggiora. E se avessimo accidentalmente un numero intero nella nostra lista?

list.add(1) Object o = list.get(0); String foo = (String) o;

In questo caso, avremmo un'eccezione ClassCastException in fase di esecuzione, poiché non è possibile eseguire il cast di un numero intero su String.

Ora proviamo a ripeterci, questa volta usando i generici:

List list = new ArrayList(); list.add("foo"); String o = list.get(0); // No cast Integer foo = list.get(0); // Compilation error

Come possiamo vedere, usando i generics abbiamo un controllo del tipo di compilazione che impedisce ClassCastExceptions e rimuove la necessità di casting.

L'altro vantaggio è evitare la duplicazione del codice . Senza generici, dobbiamo copiare e incollare lo stesso codice ma per tipi diversi. Con i generici, non dobbiamo farlo. Possiamo anche implementare algoritmi che si applicano a tipi generici.

Q3. Che cos'è la cancellazione del tipo?

È importante rendersi conto che le informazioni sul tipo generico sono disponibili solo per il compilatore, non per la JVM. In altre parole , la cancellazione del tipo significa che le informazioni sul tipo generico non sono disponibili per la JVM in fase di runtime, ma solo in fase di compilazione .

Il ragionamento alla base della scelta di implementazione principale è semplice: preservare la compatibilità con le versioni precedenti di Java. Quando un codice generico viene compilato in bytecode, sarà come se il tipo generico non fosse mai esistito. Ciò significa che la compilation:

  1. Sostituisci i tipi generici con gli oggetti
  2. Sostituisci i tipi limitati (Maggiori informazioni su questi in una domanda successiva) con la prima classe legata
  3. Inserire l'equivalente dei cast durante il recupero di oggetti generici.

È importante comprendere la cancellazione del tipo. Altrimenti, uno sviluppatore potrebbe confondersi e pensare che sarebbe in grado di ottenere il tipo in fase di esecuzione:

public foo(Consumer consumer) { Type type = consumer.getGenericTypeParameter() }

L'esempio sopra è uno pseudo codice equivalente a come potrebbero apparire le cose senza la cancellazione del tipo, ma sfortunatamente è impossibile. Ancora una volta, le informazioni sul tipo generico non sono disponibili in fase di esecuzione.

Q4. Se un tipo generico viene omesso durante la creazione di un'istanza di un oggetto, il codice verrà comunque compilato?

Poiché i generici non esistevano prima di Java 5, è possibile non utilizzarli affatto. Ad esempio, i generici sono stati adattati alla maggior parte delle classi Java standard come le collezioni. Se guardiamo il nostro elenco dalla domanda uno, vedremo che abbiamo già un esempio di omissione del tipo generico:

List list = new ArrayList();

Nonostante sia in grado di compilare, è ancora probabile che ci sarà un avviso dal compilatore. Questo perché stiamo perdendo il controllo extra in fase di compilazione che otteniamo dall'uso dei generici.

Il punto da ricordare è che mentre la compatibilità con le versioni precedenti e la cancellazione del tipo consentono di omettere i tipi generici, è una cattiva pratica.

Q5. In che modo un metodo generico differisce da un tipo generico?

Un metodo generico è dove un parametro di tipo viene introdotto in un metodo, che vive nell'ambito di tale metodo. Proviamo con un esempio:

public static  T returnType(T argument) { return argument; }

Abbiamo usato un metodo statico ma avremmo potuto usarne anche uno non statico se lo avessimo voluto. Sfruttando l'inferenza del tipo (trattata nella prossima domanda), possiamo invocarlo come qualsiasi metodo ordinario, senza dover specificare alcun argomento di tipo quando lo facciamo.

Q6. Che cos'è l'inferenza del tipo?

L'inferenza del tipo è quando il compilatore può esaminare il tipo di un argomento del metodo per dedurre un tipo generico. Ad esempio, se passiamo T a un metodo che restituisce T, il compilatore può capire il tipo restituito. Proviamolo invocando il nostro metodo generico dalla domanda precedente:

Integer inferredInteger = returnType(1); String inferredString = returnType("String");

Come possiamo vedere, non è necessario un cast e non è necessario passare alcun argomento di tipo generico. Il tipo di argomento deduce solo il tipo restituito.

Q7. Che cos'è un parametro di tipo limitato?

Finora tutte le nostre domande hanno coperto argomenti di tipi generici che non hanno limiti. Ciò significa che i nostri argomenti di tipo generico potrebbero essere qualsiasi tipo desideriamo.

Quando usiamo parametri limitati, limitiamo i tipi che possono essere usati come argomenti di tipo generico.

Ad esempio, supponiamo di voler forzare il nostro tipo generico a essere sempre una sottoclasse di animale:

public abstract class Cage { abstract void addAnimal(T animal) }

Usando le estensioni , costringiamo T a essere una sottoclasse di animali . Potremmo quindi avere una gabbia di gatti:

Cage catCage;

Ma non potremmo avere una gabbia di oggetti, poiché un oggetto non è una sottoclasse di un animale:

Cage objectCage; // Compilation error

Un vantaggio di questo è che tutti i metodi di animal sono disponibili per il compilatore. Sappiamo che il nostro tipo lo estende, quindi potremmo scrivere un algoritmo generico che opera su qualsiasi animale. Ciò significa che non dobbiamo riprodurre il nostro metodo per diverse sottoclassi di animali:

public void firstAnimalJump() { T animal = animals.get(0); animal.jump(); }

Q8. È possibile dichiarare un parametro di tipo con limiti multipli?

È possibile dichiarare più limiti per i nostri tipi generici. Nel nostro esempio precedente, abbiamo specificato un singolo limite, ma potremmo anche specificarne di più se lo desideriamo:

public abstract class Cage

Nel nostro esempio, l'animale è una classe e paragonabile è un'interfaccia. Ora, il nostro tipo deve rispettare entrambi questi limiti superiori. Se il nostro tipo fosse una sottoclasse di animale ma non implementasse comparabile, il codice non verrebbe compilato. Vale anche la pena ricordare che se uno dei limiti superiori è una classe, deve essere il primo argomento.

Q9. Che cos'è un tipo di carattere jolly?

Un tipo di carattere jolly rappresenta un tipo sconosciuto . È fatto esplodere con un punto interrogativo come segue:

public static void consumeListOfWildcardType(List list)

Qui stiamo specificando un elenco che potrebbe essere di qualsiasi tipo . Potremmo passare un elenco di qualsiasi cosa in questo metodo.

Q10. Che cos'è un carattere jolly delimitato superiore?

Un carattere jolly delimitato superiore è quando un tipo di carattere jolly eredita da un tipo concreto . Ciò è particolarmente utile quando si lavora con raccolte ed eredità.

Proviamo a dimostrarlo con una classe farm che conserverà gli animali, prima senza il tipo di carattere jolly:

public class Farm { private List animals; public void addAnimals(Collection newAnimals) { animals.addAll(newAnimals); } }

Se avessimo più sottoclassi di animali , come cane e gatto , potremmo supporre erroneamente che possiamo aggiungerli tutti alla nostra fattoria:

farm.addAnimals(cats); // Compilation error farm.addAnimals(dogs); // Compilation error

Questo perché il compilatore si aspetta una raccolta del tipo concreto animale , non una sottoclasse.

Ora, introduciamo un carattere jolly delimitato superiore al nostro metodo di aggiunta di animali:

public void addAnimals(Collection newAnimals)

Ora se proviamo di nuovo, il nostro codice verrà compilato. Questo perché ora stiamo dicendo al compilatore di accettare una raccolta di qualsiasi sottotipo di animale.

Q11. Che cos'è un carattere jolly illimitato?

Un carattere jolly illimitato è un carattere jolly senza limite superiore o inferiore, che può rappresentare qualsiasi tipo.

È anche importante sapere che il tipo di carattere jolly non è sinonimo di oggetto. Questo perché un carattere jolly può essere qualsiasi tipo mentre un tipo di oggetto è specificamente un oggetto (e non può essere una sottoclasse di un oggetto). Dimostriamolo con un esempio:

List wildcardList = new ArrayList(); List objectList = new ArrayList(); // Compilation error

Di nuovo, il motivo per cui la seconda riga non viene compilata è che è richiesto un elenco di oggetti, non un elenco di stringhe. La prima riga viene compilata perché è accettabile un elenco di qualsiasi tipo sconosciuto.

Q12. Che cos'è un carattere jolly con limite inferiore?

Un carattere jolly con limite inferiore è quando invece di fornire un limite superiore, forniamo un limite inferiore utilizzando la parola chiave super . In altre parole, un carattere jolly con limite inferiore significa che stiamo forzando il tipo a essere una superclasse del nostro tipo limitato . Proviamo con un esempio:

public static void addDogs(List list) { list.add(new Dog("tom")) }

Usando super, potremmo chiamare addDogs su un elenco di oggetti:

ArrayList objects = new ArrayList(); addDogs(objects);

Questo ha senso, poiché un oggetto è una superclasse di animali. Se non usassimo il carattere jolly del limite inferiore, il codice non verrebbe compilato, poiché un elenco di oggetti non è un elenco di animali.

Se ci pensiamo, non saremmo in grado di aggiungere un cane a un elenco di qualsiasi sottoclasse di animali, come i gatti o persino i cani. Solo una superclasse di animali. Ad esempio, questo non compilerebbe:

ArrayList objects = new ArrayList(); addDogs(objects);

Q13. Quando sceglieresti di utilizzare un tipo con limite inferiore rispetto a un tipo con limite superiore?

Quando si ha a che fare con le raccolte, una regola comune per la selezione tra caratteri jolly delimitati superiori o inferiori è PECS. PECS sta per produttore si estende, consumatore super.

Ciò può essere facilmente dimostrato mediante l'uso di alcune interfacce e classi Java standard.

Producer extends significa semplicemente che se stai creando un producer di un tipo generico, usa la parola chiave extends . Proviamo ad applicare questo principio a una raccolta, per capire perché ha senso:

public static void makeLotsOfNoise(List animals) { animals.forEach(Animal::makeNoise); }

Qui, vogliamo chiamare makeNoise () su ogni animale nella nostra raccolta. Ciò significa che la nostra collezione è un produttore , poiché tutto ciò che stiamo facendo è convincerla a restituire gli animali su cui eseguire la nostra operazione. Se ci sbarazzassimo delle estensioni , non saremmo in grado di passare in elenchi di gatti , cani o altre sottoclassi di animali. Applicando il principio del produttore estende, abbiamo la massima flessibilità possibile.

Consumer super significa il contrario di producer si estende. Significa solo che se abbiamo a che fare con qualcosa che consuma elementi, allora dovremmo usare la parola chiave super . Possiamo dimostrarlo ripetendo il nostro esempio precedente:

public static void addCats(List animals) { animals.add(new Cat()); }

Stiamo solo aggiungendo al nostro elenco di animali, quindi il nostro elenco di animali è un consumatore. Questo è il motivo per cui utilizziamo la parola chiave super . Significa che potremmo passare in un elenco di qualsiasi superclasse di animale, ma non una sottoclasse. Ad esempio, se provassimo a passare un elenco di cani o gatti, il codice non verrebbe compilato.

L'ultima cosa da considerare è cosa fare se una collezione è sia un consumatore che un produttore. Un esempio potrebbe essere una raccolta in cui gli elementi vengono aggiunti e rimossi. In questo caso, dovrebbe essere utilizzato un carattere jolly illimitato.

Q14. Esistono situazioni in cui sono disponibili informazioni sul tipo generico in fase di esecuzione?

C'è una situazione in cui un tipo generico è disponibile in fase di esecuzione. Questo è quando un tipo generico fa parte della firma della classe in questo modo:

public class CatCage implements Cage

Usando la riflessione, otteniamo questo parametro di tipo:

(Class) ((ParameterizedType) getClass() .getGenericSuperclass()).getActualTypeArguments()[0];

Questo codice è piuttosto fragile. Ad esempio, dipende dal parametro di tipo definito nella superclasse immediata. Tuttavia, dimostra che la JVM dispone di questo tipo di informazioni.

Successivo » Domande di intervista sul controllo del flusso Java (+ Risposte) « Precedente Gestione della memoria in Domande di intervista su Java (+ Risposte)