Preparazione ai colloqui backend su Java

Domande e risposte per colloqui su Java 2026

Un set di 30 domande da colloquio su Java per sviluppatori backend, dai fondamenti del linguaggio e dal comportamento della JVM fino alle collezioni, alla Stream API e alla concorrenza.

Inizia la simulazione di colloquio su JavaNessuna carta di credito richiesta. 1 sessione gratuita disponibile.
Pratica per colloqui tecnici in ingleseUna modalità in cui chi non è madrelingua può esercitarsi a superare i colloqui tecnici.

Java e JVM

1Cos'è Java e perché è considerato indipendente dalla piattaforma?

Java è un linguaggio di uso generale a tipizzazione statica. Il codice sorgente viene compilato in bytecode, e il bytecode viene eseguito sulla JVM (Java Virtual Machine). Lo stesso file `.class` o `.jar` può generalmente essere eseguito su diversi sistemi operativi senza essere ricompilato, a patto che sia disponibile una JVM compatibile. Questa non è un'indipendenza assoluta: librerie native, percorsi specifici della piattaforma, comandi di sistema e comportamenti del sistema operativo possono comunque rendere un'applicazione dipendente dalla piattaforma.

Java source -> javac -> bytecode -> JVM -> machine code
Prova a rispondere a questa domanda con un coach IA

2Qual è la differenza tra JVM (Java Virtual Machine), JRE (Java Runtime Environment) e JDK (Java Development Kit)?

La JVM carica ed esegue il bytecode e gestisce la memoria, i thread e la garbage collection. Il JRE indicava storicamente la JVM più le librerie necessarie per eseguire le applicazioni. Il JDK include il runtime e strumenti di sviluppo come javac, java, debugger, javadoc, jar e strumenti diagnostici. Nelle distribuzioni moderne un JRE separato è meno comune perché le applicazioni vengono spesso distribuite con un'immagine runtime personalizzata.

JDK = development tools + runtime
Runtime = JVM + standard libraries
Prova a rispondere a questa domanda con un coach IA

3Come esegue il codice Java la JVM (Java Virtual Machine) e perché è necessaria la compilazione JIT (Just-In-Time)?

`javac` compila il codice sorgente in bytecode. La JVM carica le classi e inizia a interpretare o eseguire le istruzioni del bytecode. Le parti di codice eseguite frequentemente e critiche per le prestazioni vengono rilevate a runtime e compilate dal compilatore JIT in codice macchina ottimizzato. Il compilatore JIT può effettuare l'inlining dei metodi, ottimizzare i cicli, applicare l'escape analysis ed eliminare alcune allocazioni. Questo è il motivo per cui le applicazioni Java possono diventare più veloci dopo la fase di riscaldamento (warm-up), e perché i microbenchmark senza JMH sono spesso fuorvianti.

Prova a rispondere a questa domanda con un coach IA

4Come funziona il ClassLoader?

Un ClassLoader carica le definizioni delle classi nella JVM (Java Virtual Machine). I caricatori comuni sono il Bootstrap, il Platform, l'Application e i caricatori personalizzati. Una classe è identificata non solo dal suo nome completamente qualificato (fully qualified name), ma anche dal ClassLoader che l'ha caricata, pertanto due classi con lo stesso nome caricate da ClassLoader diversi possono essere tipi differenti. Di solito, Java utilizza la delega al genitore (parent delegation): un caricatore chiede prima al proprio genitore per ridurre il rischio di sostituire le classi della piattaforma.

Loading
Linking: verification, preparation, resolution
Initialization
Prova a rispondere a questa domanda con un coach IA

5Come è organizzata la memoria della JVM (Java Virtual Machine)?

Le principali aree di memoria della JVM includono l'heap per gli oggetti e gli array, gli stack Java con i frame di chiamata per ciascun thread, il Metaspace per i metadati delle classi, la code cache per il codice compilato JIT (Just-In-Time), il registro PC (Program Counter) e gli stack nativi. Bisogna evitare l'eccessiva semplificazione secondo cui i tipi primitivi risiedono sempre nello stack e gli oggetti risiedono sempre nell'heap: un campo primitivo viene memorizzato all'interno del suo oggetto, e l'escape analysis del JIT può rimuovere le allocazioni o modificare la rappresentazione degli oggetti.

Prova a rispondere a questa domanda con un coach IA

6Come funziona il GC (Garbage Collector)?

Il GC (Garbage Collector) libera la memoria occupata dagli oggetti che non sono più raggiungibili dai GC Root. I root includono le variabili locali dei metodi attivi, i campi statici, i thread attivi e i riferimenti JNI (Java Native Interface). Molti collector si basano sull'osservazione che la maggior parte dei nuovi oggetti muore giovane, quindi l'heap viene spesso diviso in generazioni, tipicamente "young generation" e "old generation". Il GC non chiude in modo deterministico i file o le connessioni; le risorse esterne dovrebbero essere chiuse utilizzando il costrutto `try-with-resources`.

try (var stream = Files.newInputStream(path)) {
    // use stream
}
Prova a rispondere a questa domanda con un coach IA

OOP e tipi

7Quali quattro principi della OOP (Object-Oriented Programming) supporta Java?

Java supporta l'incapsulamento, l'astrazione, l'ereditarietà e il polimorfismo. L'incapsulamento nasconde lo stato interno dietro un'API pubblica. L'astrazione si concentra sul comportamento essenziale. L'ereditarietà crea un nuovo tipo basato su un tipo esistente. Il polimorfismo consente al codice di funzionare con implementazioni diverse tramite un contratto condiviso. In pratica, la composizione è spesso preferibile all'ereditarietà perché crea un accoppiamento minore.

interface PaymentService {
    void pay(BigDecimal amount);
}

class CardPaymentService implements PaymentService {
    @Override
    public void pay(BigDecimal amount) {
        // payment implementation
    }
}
Prova a rispondere a questa domanda con un coach IA

8Qual è la differenza tra il sovraccarico (overloading) e la sovrascrittura (overriding) dei metodi?

Il sovraccarico significa che più metodi hanno lo stesso nome ma parametri diversi; il compilatore sceglie il metodo in base ai tipi statici degli argomenti. La sovrascrittura significa che una sottoclasse sostituisce l'implementazione di un metodo di istanza di una classe genitore; l'invio a tempo di esecuzione (runtime dispatch) sceglie il metodo in base al tipo effettivo dell'oggetto. I metodi statici vengono nascosti, non sovrascritti, e i metodi privati non vengono sovrascritti perché non sono ereditati come membri accessibili.

void send(String message) {}
void send(String message, int priority) {}

class Animal {
    void speak() { System.out.println("Animal"); }
}

class Dog extends Animal {
    @Override
    void speak() { System.out.println("Dog"); }
}
Prova a rispondere a questa domanda con un coach IA

9Qual è la differenza tra un'interfaccia e una classe astratta?

Un'interfaccia definisce un contratto e può contenere metodi astratti, metodi di default, metodi statici, costanti e metodi ausiliari privati. Una classe astratta può inoltre avere stato dell'istanza, costruttori, campi protetti, un'implementazione condivisa e metodi astratti o concreti. Una classe può estendere una sola classe ma implementare più interfacce. Utilizza un'interfaccia per un contratto indipendente e una classe astratta per tipi strettamente correlati con implementazione e stato condivisi.

interface Repository<T> {
    Optional<T> findById(long id);
}
Prova a rispondere a questa domanda con un coach IA

10Cosa significa la parola chiave `final`?

Una variabile `final` può essere assegnata una sola volta, ma un riferimento `final` non rende immutabile l'oggetto a cui punta. Un metodo `final` non può essere sovrascritto (overridden). Una classe `final` non può essere estesa. `final` comunica l'intento e limita le modifiche ai riferimenti o l'ereditarietà, ma da solo non fornisce un modello immutabile completo o thread safety.

final int maxAttempts = 3;

final List<String> names = new ArrayList<>();
names.add("Alice"); // allowed

public final void validate() {}

public final class Token {}
Prova a rispondere a questa domanda con un coach IA

11Cosa sono i record in Java?

Un `record` è una sintassi compatta per i contenitori di dati immutabili. Il compilatore genera i campi privati e `final`, il costruttore, i metodi di accesso, `equals`, `hashCode` e `toString`. I `record` sono utili per i DTO (Data Transfer Object), i value object e i risultati dei metodi. Non sostituiscono tutte le entità di dominio: la validazione può essere aggiunta in un costruttore compatto, ma i comportamenti mutabili complessi, il caricamento ritardato (lazy loading) e i modelli basati sull'ereditarietà in genere si adattano male ai `record`.

public record User(long id, String name) {}

public record Order(List<String> items) {
    public Order {
        items = List.copyOf(items);
    }
}
Prova a rispondere a questa domanda con un coach IA

12Come si crea un oggetto immutabile in Java?

Dichiara la classe come final o restringi attentamente l'ereditarietà, rendi i campi private final, evita i metodi setter ed esegui la validazione nel costruttore. Per gli input mutabili, crea copie difensive e restituisci copie o viste non modificabili. L'immutabilità profonda richiede che anche tutto lo stato annidato sia immutabile. I campi final aiutano la pubblicazione sicura, ma un oggetto può comunque essere logicamente mutabile se espone una collezione mutabile o un oggetto annidato mutabile.

public final class User {
    private final String name;
    private final List<String> roles;

    public User(String name, List<String> roles) {
        this.name = name;
        this.roles = List.copyOf(roles);
    }

    public List<String> getRoles() {
        return roles;
    }
}
Prova a rispondere a questa domanda con un coach IA

Riferimenti, stringhe e confronti

13Java passa i parametri per valore o per riferimento?

Java passa sempre gli argomenti per valore. Per i tipi primitivi, viene copiato il valore stesso. Per gli oggetti, viene copiato il valore del riferimento. Un metodo può modificare l'oggetto tramite il riferimento copiato, ma non può sostituire il riferimento del chiamante con un altro oggetto. Questa distinzione spiega perché la modifica di un campo possa essere visibile al chiamante, mentre l'assegnazione del parametro a un nuovo oggetto non lo sia.

void change(int value) {
    value = 100;
}

void rename(User user) {
    user.setName("Kate");
}

void replace(User user) {
    user = new User("New");
}
Prova a rispondere a questa domanda con un coach IA

14Qual è la differenza tra `==` e `equals()`?

Per i tipi primitivi, `==` confronta i valori. Per i riferimenti agli oggetti, `==` verifica se entrambi i riferimenti puntano allo stesso oggetto. `equals()` è un metodo che dovrebbe confrontare l'uguaglianza logica, a patto che la classe lo implementi correttamente. `String`, `Integer`, `LocalDate` e molte classi valore sovrascrivono `equals`. Per gli oggetti di dominio personalizzati, `equals` deve riflettere la semantica di identità o di valore desiderata.

int a = 10;
int b = 10;
System.out.println(a == b); // true

String first = new String("java");
String second = new String("java");

System.out.println(first == second);      // false
System.out.println(first.equals(second)); // true
Prova a rispondere a questa domanda con un coach IA

15In cosa consiste il contratto equals/hashCode?

Se due oggetti sono uguali secondo equals, devono restituire lo stesso hashCode. Il contrario non è richiesto: oggetti diversi possono avere lo stesso hashCode. Il metodo equals dovrebbe essere riflessivo, simmetrico, transitivo, coerente e restituire false per null. Violare il contratto corrompe il comportamento in HashMap, HashSet e in altre collezioni basate su hash: potrebbe diventare impossibile trovare un oggetto anche se è presente.

@Override
public boolean equals(Object object) {
    if (this == object) return true;
    if (!(object instanceof User other)) return false;
    return id == other.id;
}

@Override
public int hashCode() {
    return Long.hashCode(id);
}
Prova a rispondere a questa domanda con un coach IA

16Perché String è immutabile?

String è immutabile per motivi di sicurezza, prevedibilità e prestazioni. L'immutabilità consente l'interning delle stringhe, la condivisione sicura tra thread, la memorizzazione affidabile nella cache dell'hashCode e l'uso sicuro in percorsi, URL, nomi di classi e valori sensibili per la sicurezza. Operazioni come concat, replace e substring creano nuove stringhe. A partire da Java 9, String può utilizzare internamente byte[] con una rappresentazione compatta, ma il contratto pubblico di immutabilità rimane lo stesso.

String value = "Java";
value.toUpperCase();

System.out.println(value); // Java

String a = "java";
String b = "java";

System.out.println(a == b); // often true due to interning
Prova a rispondere a questa domanda con un coach IA

17Qual è la differenza tra `String`, `StringBuilder` e `StringBuffer`?

`String` è immutabile, quindi la concatenazione ripetuta nei cicli può creare molti oggetti intermedi, sebbene il compilatore possa ottimizzare i casi più semplici. `StringBuilder` è mutabile ed è preferibile per costruire stringhe in un singolo thread. `StringBuffer` è simile ma sincronizzato, quindi è solitamente più lento ed è rilevante principalmente per le vecchie API thread-safe. Per la formattazione, `String.format` o le API basate su template possono risultare più leggibili, ma non sono sempre più veloci.

StringBuilder builder = new StringBuilder();

for (String item : items) {
    builder.append(item).append(',');
}

String result = builder.toString();

String message = "Hello, " + name;
Prova a rispondere a questa domanda con un coach IA

Generici e collezioni

18Cosa sono i generics e cos'è la cancellazione dei tipi (type erasure)?

I generics permettono al codice di esprimere contenitori e API type-safe, come List<String>. A tempo di compilazione, il compilatore controlla i tipi e inserisce i cast dove necessario. A tempo di esecuzione, la maggior parte delle informazioni sui tipi generici viene cancellata, quindi List<String> e List<Integer> condividono la stessa classe a runtime. Questo è il motivo per cui non è possibile creare direttamente un new T(), usare parametri di tipo primitivo, o controllare in modo affidabile instanceof List<String>.

List<String> names = new ArrayList<>();
names.add("Alice");

List<? extends Number> producer;
List<? super Integer> consumer;
Prova a rispondere a questa domanda con un coach IA

19Cos'è il Java Collections Framework?

Il Collections Framework è un insieme di interfacce e implementazioni per memorizzare ed elaborare gruppi di oggetti. Le interfacce principali includono List, Set, Queue, Deque e Map. Le implementazioni includono ArrayList, LinkedList, HashSet, TreeSet, HashMap, TreeMap e PriorityQueue. La scelta di una collezione si basa sul modello di accesso, sui requisiti di ordinamento, sull'unicità, sulla complessità di ricerca, sull'overhead di memoria e sulle esigenze di concorrenza.

List<String> list = new ArrayList<>();
Set<Long> ids = new HashSet<>();
Map<Long, User> usersById = new HashMap<>();
Deque<String> queue = new ArrayDeque<>();
Prova a rispondere a questa domanda con un coach IA

20Come funziona HashMap?

HashMap memorizza le entry in bucket selezionati tramite un hash della chiave. Per la ricerca, calcola l'hash, trova il bucket e confronta le chiavi utilizzando equals. Le collisioni vengono gestite all'interno del bucket; nel Java moderno, le catene lunghe possono essere convertite in un albero bilanciato (treeified) quando si soddisfano determinate condizioni. HashMap consente un'unica chiave null e valori null, non è thread-safe e può ridimensionarsi (resize) quando viene superata la soglia del fattore di carico (load factor).

Map<Long, User> users = new HashMap<>();
users.put(user.id(), user);

User found = users.get(user.id());
Prova a rispondere a questa domanda con un coach IA

21Qual è la differenza tra HashMap, LinkedHashMap, TreeMap e ConcurrentHashMap?

HashMap fornisce un accesso rapido e non ordinato e non è thread-safe. LinkedHashMap mantiene l'ordine di inserimento o l'ordine di accesso ed è utile per cache di tipo LRU (Least Recently Used). TreeMap mantiene le chiavi ordinate secondo l'ordine naturale o tramite un Comparator e garantisce operazioni con complessità O(log n). ConcurrentHashMap è progettata per l'accesso concorrente con sincronizzazione segmentata/interna, non consente chiavi o valori null ed è preferibile a synchronizedMap sotto carico.

map.computeIfAbsent(key, ignored -> loadValue());
Prova a rispondere a questa domanda con un coach IA

22Qual è la differenza tra ArrayList e LinkedList?

ArrayList è basata su un array. Fornisce un accesso indicizzato rapido, un layout di memoria compatto e un'iterazione efficiente, ma l'inserimento nel mezzo richiede lo spostamento degli elementi. LinkedList è una lista doppiamente concatenata. Ha un costo di inserimento minore se il nodo è già noto, ma l'accesso indicizzato ha una complessità O(n), l'overhead di memoria è maggiore e la località della cache è peggiore. In molte applicazioni reali ArrayList risulta più veloce, anche quando sono previsti alcuni inserimenti.

Prova a rispondere a questa domanda con un coach IA

23Qual è la differenza tra `Comparable` e `Comparator`?

`Comparable` definisce l'ordinamento naturale all'interno della classe tramite `compareTo`. `Comparator` è un oggetto separato o una lambda che definisce un ordinamento esterno. `Comparable` è utile quando un tipo ha un ovvio ordinamento predefinito. `Comparator` è preferibile quando esistono molteplici criteri di ordinamento, ad esempio per nome, data o priorità. `compare` e `compareTo` devono essere coerenti e preferibilmente compatibili con `equals` quando usati in set o mappe ordinate.

class User implements Comparable<User> {
    private int age;

    @Override
    public int compareTo(User other) {
        return Integer.compare(age, other.age);
    }
}

Comparator<User> byName =
    Comparator.comparing(User::getName);

Comparator<User> byAgeThenName =
    Comparator.comparingInt(User::getAge)
              .thenComparing(User::getName);
Prova a rispondere a questa domanda con un coach IA

Eccezioni e risorse

24Qual è la differenza tra eccezioni checked e unchecked?

Le eccezioni checked ereditano da `Exception` ma non da `RuntimeException`, e il compilatore richiede ai chiamanti di gestirle o dichiararle. Le eccezioni unchecked ereditano da `RuntimeException` e non sono richieste nelle firme dei metodi. Le eccezioni checked sono adatte per situazioni recuperabili ai confini dell'API. Le eccezioni unchecked sono adatte per errori di programmazione, stati non validi e violazioni di contratto. Nel codice dei servizi moderni, i team spesso evitano un uso eccessivo di eccezioni checked perché si propagano attraverso i vari livelli creando rumore nel codice.

void load() throws IOException {
    // ...
}

throw new IllegalArgumentException("Invalid id");
Prova a rispondere a questa domanda con un coach IA

25Qual è la differenza tra `throw`, `throws` e `try-with-resources`?

`throw` solleva immediatamente un'eccezione. `throws` dichiara una possibile eccezione nella firma di un metodo. `try-with-resources` chiude automaticamente le risorse `AutoCloseable` in ordine inverso. Se sia il blocco principale che la chiusura sollevano eccezioni, la seconda viene mantenuta come eccezione soppressa. Questo approccio è più sicuro e più chiaro rispetto ai blocchi `finally` manuali quando sono coinvolte diverse risorse.

throw new IllegalArgumentException("Invalid id");

void readFile() throws IOException {}

try (var reader = Files.newBufferedReader(path)) {
    return reader.readLine();
}
Prova a rispondere a questa domanda con un coach IA

Stream API e stile funzionale

26Cos'è la Stream API?

La Stream API permette al codice di elaborare sequenze in modo dichiarativo. Le operazioni intermedie come filter, map e sorted sono lazy (pigre); le operazioni terminali come toList, collect, reduce e forEach innescano l'esecuzione. Uno stream non è una collezione, di solito viene utilizzato una sola volta e funziona al meglio con funzioni pure senza effetti collaterali. parallelStream non garantisce un'esecuzione più rapida e può compromettere le prestazioni o la correttezza.

List<String> names = users.stream()
    .filter(User::isActive)
    .sorted(Comparator.comparing(User::getName))
    .map(User::getName)
    .toList();
Prova a rispondere a questa domanda con un coach IA

27Cos'è Optional e quando dovresti usarlo?

Optional<T> rappresenta esplicitamente l'assenza di un risultato e funziona bene come tipo di ritorno. I metodi utili includono map, flatMap, filter, orElse, orElseGet e orElseThrow. orElse valuta il suo argomento in modo eager (immediato), mentre il supplier in orElseGet viene eseguito solo quando il valore è assente. Di solito, l'uso di Optional non è raccomandato per i campi delle entità, i parametri dei metodi o i DTO (Data Transfer Object) serializzati, a meno che non ci sia una valida ragione.

Optional<User> user = repository.findById(id);

String name = user
    .map(User::getName)
    .orElse("Unknown");

optional.orElse(expensiveOperation());
optional.orElseGet(() -> expensiveOperation());
Prova a rispondere a questa domanda con un coach IA

Concorrenza

28Cosa garantiscono synchronized, volatile e le classi atomiche?

synchronized fornisce mutua esclusione e una relazione happens-before. volatile garantisce la visibilità delle scritture più recenti e limita il riordino delle istruzioni, ma non rende atomiche le operazioni composte. Le classi atomiche forniscono singole operazioni atomiche come incrementAndGet. Utilizza i lock o synchronized per modifiche multiple correlate, volatile per semplici flag di stato e AtomicInteger o LongAdder per i contatori in condizioni di contesa.

synchronized (lock) {
    counter++;
}

private volatile boolean running = true;

AtomicInteger counter = new AtomicInteger();
counter.incrementAndGet();
Prova a rispondere a questa domanda con un coach IA

29Cosa sono una race condition, un deadlock e la thread safety?

Una race condition si verifica quando il risultato dipende dall'ordine imprevedibile di esecuzione dei thread. Un deadlock avviene quando i thread si attendono all'infinito per le rispettive risorse. Per ottenere la thread safety, riduci lo stato mutabile condiviso, mantieni un ordine coerente dei lock, accorcia le sezioni critiche, evita di chiamare codice esterno all'interno dei lock e utilizza oggetti immutabili, collezioni concorrenti, timeout o il passaggio di messaggi. La thread safety è una proprietà dell'intero protocollo di accesso allo stato.

Prova a rispondere a questa domanda con un coach IA

30Qual è la differenza tra ExecutorService, CompletableFuture e i virtual thread?

ExecutorService gestisce un pool di thread e l'esecuzione dei task. CompletableFuture aiuta a comporre operazioni asincrone. I virtual thread sono thread leggeri della JVM (Java Virtual Machine) che risultano convenienti per un elevato numero di task di I/O bloccanti. Semplificano il modello thread-per-request, ma non velocizzano il lavoro CPU-bound, non rimuovono i limiti del database o delle risorse esterne e non rendono automaticamente thread-safe lo stato mutabile.

try (var executor = Executors.newFixedThreadPool(4)) {
    Future<Integer> future = executor.submit(() -> calculate());
    int result = future.get();
}

CompletableFuture<User> userFuture =
    CompletableFuture.supplyAsync(this::loadUser)
        .thenApply(this::enrichUser)
        .exceptionally(error -> fallbackUser());

try (var executor =
         Executors.newVirtualThreadPerTaskExecutor()) {
    executor.submit(() -> handleRequest());
}
Prova a rispondere a questa domanda con un coach IA