In questa pagina trovi 10 esercizi di Java in ordine di difficoltà: dal primo «Ciao, mondo!» fino a collezioni, ereditarietà e polimorfismo.
Ogni esercizio contiene la consegna, un richiamo di teoria, la soluzione completa e la spiegazione passo passo. Ti serve un JDK recente (consigliato Java 17 o superiore): compila con javac NomeClasse.java ed esegui con java NomeClasse.
Indice degli esercizi
- Esercizio 1 – Ciao, mondo! (Base)
- Esercizio 2 – Area e perimetro di un rettangolo (Base)
- Esercizio 3 – Anno bisestile (Base)
- Esercizio 4 – Giorno della settimana con switch (Base)
- Esercizio 5 – Numeri primi fino a N (Intermedio)
- Esercizio 6 – Ordinare un array con il Bubble Sort (Intermedio)
- Esercizio 7 – Fibonacci: metodi e ricorsione (Intermedio)
- Esercizio 8 – Manipolare le stringhe (Avanzato)
- Esercizio 9 – Rubrica con ArrayList e HashMap (Avanzato)
- Esercizio 10 – Forme geometriche: OOP completa (Avanzato)
Esercizio 1 – Ciao, mondo!
Livello: Base · Argomenti: classi, metodo main, System.out.println
Consegna: Scrivi un programma Java che stampi «Ciao, mondo!».
Teoria in breve: In Java tutto il codice vive dentro una classe. Il programma parte dal metodo public static void main(String[] args). Il file deve chiamarsi come la classe pubblica (qui CiaoMondo.java).
Soluzione
public class CiaoMondo {
public static void main(String[] args) {
System.out.println("Ciao, mondo!");
}
}
Spiegazione
public class CiaoMondo definisce la classe. main è public (accessibile dall’esterno), static (si esegue senza creare oggetti) e void (non restituisce nulla).
System.out.println stampa una riga e va a capo. Ogni istruzione termina con il punto e virgola.
Per compilare ed eseguire: javac CiaoMondo.java e poi java CiaoMondo (dalla versione 11 puoi anche fare direttamente java CiaoMondo.java).
Esercizio 2 – Area e perimetro di un rettangolo
Livello: Base · Argomenti: variabili, tipi primitivi, Scanner
Consegna: Chiedi all’utente base e altezza di un rettangolo e stampa area e perimetro.
Teoria in breve: Java è un linguaggio a tipizzazione statica: ogni variabile ha un tipo dichiarato (int, double, boolean, char…). Per leggere da tastiera si usa la classe Scanner del pacchetto java.util.
Soluzione
import java.util.Scanner;
public class Rettangolo {
public static void main(String[] args) {
Scanner in = new Scanner(System.in);
System.out.print("Base: ");
double base = in.nextDouble();
System.out.print("Altezza: ");
double altezza = in.nextDouble();
double area = base * altezza;
double perimetro = 2 * (base + altezza);
System.out.printf("Area: %.2f%n", area);
System.out.printf("Perimetro: %.2f%n", perimetro);
in.close();
}
}
Spiegazione
import java.util.Scanner; rende disponibile la classe; new Scanner(System.in) crea un oggetto che legge dalla tastiera e nextDouble() legge un numero decimale.
printf funziona come in C: %.2f stampa due decimali e %n va a capo in modo portabile su ogni sistema operativo.
Alla fine chiudiamo lo Scanner con close() per liberare la risorsa.
Esercizio 3 – Anno bisestile
Livello: Base · Argomenti: if / else, operatori logici && e ||, boolean
Consegna: Leggi un anno e stabilisci se è bisestile. Un anno è bisestile se è divisibile per 4 ma non per 100, oppure se è divisibile per 400.
Teoria in breve: Le condizioni combinano confronti con gli operatori logici && (AND), || (OR) e ! (NOT). Il risultato è un valore boolean: true o false.
Soluzione
import java.util.Scanner;
public class Bisestile {
public static void main(String[] args) {
Scanner in = new Scanner(System.in);
System.out.print("Inserisci un anno: ");
int anno = in.nextInt();
boolean bisestile = (anno % 4 == 0 && anno % 100 != 0) || anno % 400 == 0;
if (bisestile) {
System.out.println(anno + " è bisestile");
} else {
System.out.println(anno + " non è bisestile");
}
}
}
Spiegazione
Traduciamo la regola direttamente in codice: (divisibile per 4 e non per 100) oppure divisibile per 400. Le parentesi rendono chiaro l’ordine di valutazione.
Salvare la condizione in una variabile boolean con un nome parlante rende l’if più leggibile. Esempi: 2024 è bisestile, 1900 no, 2000 sì.
Con + Java concatena numeri e stringhe: anno + " è bisestile".
Esercizio 4 – Giorno della settimana con switch
Livello: Base · Argomenti: switch moderno, String
Consegna: Leggi un numero da 1 a 7 e stampa il giorno della settimana corrispondente, indicando anche se è un giorno feriale o del weekend.
Teoria in breve: Dalla versione 14, Java offre lo switch expression con la freccia ->: è più compatto, non richiede break e può restituire direttamente un valore.
Soluzione
import java.util.Scanner;
public class Giorni {
public static void main(String[] args) {
Scanner in = new Scanner(System.in);
System.out.print("Numero del giorno (1-7): ");
int n = in.nextInt();
String giorno = switch (n) {
case 1 -> "Lunedì";
case 2 -> "Martedì";
case 3 -> "Mercoledì";
case 4 -> "Giovedì";
case 5 -> "Venerdì";
case 6 -> "Sabato";
case 7 -> "Domenica";
default -> "non valido";
};
String tipo = switch (n) {
case 1, 2, 3, 4, 5 -> "feriale";
case 6, 7 -> "weekend";
default -> "-";
};
System.out.println("Giorno: " + giorno + " (" + tipo + ")");
}
}
Spiegazione
Ogni case con la freccia restituisce un valore che viene assegnato alla variabile giorno; non serve il break perché non c’è il «fall-through» dello switch classico.
Più valori possono condividere lo stesso ramo separandoli con la virgola (case 6, 7). Il default gestisce gli input fuori intervallo.
Se usi una versione di Java precedente alla 14 dovrai scrivere lo switch tradizionale con case 1: ... break;.
Esercizio 5 – Numeri primi fino a N
Livello: Intermedio · Argomenti: cicli for annidati, break, flag
Consegna: Leggi un numero N e stampa tutti i numeri primi compresi tra 2 e N.
Teoria in breve: I cicli possono essere annidati: il ciclo esterno scorre i candidati, quello interno cerca i divisori. Una variabile flag (bandierina) booleana memorizza se abbiamo trovato un divisore; break interrompe subito il ciclo interno.
Soluzione
import java.util.Scanner;
public class NumeriPrimi {
public static void main(String[] args) {
Scanner in = new Scanner(System.in);
System.out.print("Inserisci N: ");
int n = in.nextInt();
for (int numero = 2; numero <= n; numero++) {
boolean primo = true;
for (int d = 2; d * d <= numero; d++) {
if (numero % d == 0) {
primo = false;
break;
}
}
if (primo) {
System.out.print(numero + " ");
}
}
System.out.println();
}
}
Spiegazione
Per ogni numero partiamo supponendo che sia primo (primo = true). Se troviamo un divisore, mettiamo il flag a false e usciamo dal ciclo interno con break.
La condizione d * d <= numero equivale a fermarsi alla radice quadrata: è inutile cercare divisori più grandi. Così il programma resta veloce anche con N grandi.
Esercizio 6 – Ordinare un array con il Bubble Sort
Livello: Intermedio · Argomenti: array, scambio di variabili, algoritmi di ordinamento
Consegna: Dato un array di interi, ordinalo in modo crescente implementando a mano l’algoritmo Bubble Sort, poi stampalo prima e dopo.
Teoria in breve: Un array Java ha dimensione fissa, si dichiara con int[] e la sua lunghezza si legge con .length. Il Bubble Sort confronta coppie di elementi vicini e li scambia se sono nell’ordine sbagliato: a ogni passata l’elemento più grande «sale» in fondo come una bolla.
Soluzione
import java.util.Arrays;
public class BubbleSort {
public static void main(String[] args) {
int[] numeri = {29, 3, 72, 14, 8, 51, 1};
System.out.println("Prima: " + Arrays.toString(numeri));
for (int i = 0; i < numeri.length - 1; i++) {
boolean scambiato = false;
for (int j = 0; j < numeri.length - 1 - i; j++) {
if (numeri[j] > numeri[j + 1]) {
int tmp = numeri[j];
numeri[j] = numeri[j + 1];
numeri[j + 1] = tmp;
scambiato = true;
}
}
if (!scambiato) break;
}
System.out.println("Dopo: " + Arrays.toString(numeri));
}
}
Spiegazione
Per scambiare due elementi serve una variabile d’appoggio tmp: senza di essa perderemmo uno dei due valori.
Dopo ogni passata gli ultimi i elementi sono già al loro posto, per questo il ciclo interno arriva fino a length - 1 - i. Se in una passata non avviene nessuno scambio l’array è già ordinato e usciamo in anticipo.
Arrays.toString converte l’array in un testo leggibile. Nei programmi reali si usa Arrays.sort(numeri), molto più efficiente, ma implementare il Bubble Sort è un ottimo allenamento sulla logica.
Esercizio 7 – Fibonacci: metodi e ricorsione
Livello: Intermedio · Argomenti: metodi statici, parametri, ricorsione, long
Consegna: Scrivi due metodi che calcolano l’n-esimo numero di Fibonacci (0, 1, 1, 2, 3, 5, 8…): uno ricorsivo e uno iterativo. Stampa i primi 15 valori.
Teoria in breve: Un metodo è un blocco di codice con un nome, dei parametri e un tipo di ritorno. Un metodo ricorsivo richiama sé stesso e necessita di un caso base. Ogni problema ricorsivo si può risolvere anche in modo iterativo, spesso con prestazioni migliori.
Soluzione
public class Fibonacci {
static long fibRicorsivo(int n) {
if (n < 2) return n; // caso base
return fibRicorsivo(n - 1) + fibRicorsivo(n - 2);
}
static long fibIterativo(int n) {
long a = 0, b = 1;
for (int i = 0; i < n; i++) {
long successivo = a + b;
a = b;
b = successivo;
}
return a;
}
public static void main(String[] args) {
for (int i = 0; i < 15; i++) {
System.out.print(fibIterativo(i) + " ");
}
System.out.println();
System.out.println("fib(30) ricorsivo = " + fibRicorsivo(30));
}
}
Spiegazione
La versione ricorsiva traduce la definizione matematica: fib(n) = fib(n-1) + fib(n-2), con fib(0)=0 e fib(1)=1. È elegante ma molto lenta, perché ricalcola gli stessi valori moltissime volte (prova con n = 45!).
La versione iterativa tiene in memoria solo gli ultimi due valori (a e b) e li fa «scorrere» in avanti: è velocissima.
I metodi sono static perché li chiamiamo da main senza creare un oggetto. Usiamo long perché i numeri di Fibonacci crescono rapidamente.
Esercizio 8 – Manipolare le stringhe
Livello: Avanzato · Argomenti: String, StringBuilder, metodi delle stringhe
Consegna: Leggi una frase e stampa: il numero di vocali, la frase con le parole in ordine inverso, la frase scritta al contrario e se è palindroma (ignorando spazi e maiuscole).
Teoria in breve: In Java le String sono immutabili: ogni modifica crea una nuova stringa. Per costruire testi un pezzo alla volta si usa StringBuilder, che è modificabile e più efficiente. Metodi utili: length(), charAt(), toLowerCase(), split(), replaceAll().
Soluzione
import java.util.Scanner;
public class Stringhe {
public static void main(String[] args) {
Scanner in = new Scanner(System.in);
System.out.print("Scrivi una frase: ");
String frase = in.nextLine();
int vocali = 0;
for (char c : frase.toLowerCase().toCharArray()) {
if ("aeiouàèéìòù".indexOf(c) >= 0) vocali++;
}
System.out.println("Vocali: " + vocali);
String[] parole = frase.trim().split("\\s+");
StringBuilder invertite = new StringBuilder();
for (int i = parole.length - 1; i >= 0; i--) {
invertite.append(parole[i]);
if (i > 0) invertite.append(" ");
}
System.out.println("Parole invertite: " + invertite);
String alContrario = new StringBuilder(frase).reverse().toString();
System.out.println("Al contrario: " + alContrario);
String pulita = frase.toLowerCase().replaceAll("[^a-z0-9]", "");
boolean palindroma = pulita.equals(new StringBuilder(pulita).reverse().toString());
System.out.println("Palindroma: " + (palindroma ? "sì" : "no"));
}
}
Spiegazione
Il ciclo for (char c : ...) è un for-each: scorre ogni carattere. indexOf(c) >= 0 verifica se il carattere è nella stringa delle vocali.
split("\\s+") usa un’espressione regolare per dividere su uno o più spazi. Poi scorriamo l’array al contrario e ricomponiamo la frase con StringBuilder.append.
reverse() di StringBuilder inverte il testo. Per la palindromia eliminiamo con replaceAll tutto ciò che non è lettera o cifra e confrontiamo con equals: in Java le stringhe non si confrontano con ==, che controlla se sono lo stesso oggetto in memoria.
L’operatore ternario condizione ? a : b è una forma compatta di if/else che restituisce un valore.
Esercizio 9 – Rubrica con ArrayList e HashMap
Livello: Avanzato · Argomenti: Collections, generics, ArrayList, HashMap, record
Consegna: Crea una rubrica che memorizzi i contatti (nome, telefono, città). Aggiungi alcuni contatti, cercane uno per nome e raggruppa i contatti per città, stampando quanti ce ne sono per ognuna.
Teoria in breve: Le Collections sono strutture dati dinamiche: ArrayList è una lista che cresce automaticamente, HashMap associa chiavi a valori con ricerche velocissime. I generics (<Contatto>) indicano il tipo degli elementi. Un record (Java 16+) è una classe compatta per contenere dati.
Soluzione
import java.util.*;
public class Rubrica {
record Contatto(String nome, String telefono, String citta) { }
public static void main(String[] args) {
List<Contatto> contatti = new ArrayList<>();
contatti.add(new Contatto("Anna", "333-1111111", "Roma"));
contatti.add(new Contatto("Luca", "333-2222222", "Milano"));
contatti.add(new Contatto("Sara", "333-3333333", "Roma"));
contatti.add(new Contatto("Marco", "333-4444444", "Napoli"));
// Indice per nome: ricerca immediata
Map<String, Contatto> perNome = new HashMap<>();
for (Contatto c : contatti) {
perNome.put(c.nome().toLowerCase(), c);
}
Contatto trovato = perNome.get("sara");
System.out.println(trovato != null ? "Trovato: " + trovato : "Non trovato");
// Raggruppamento per città
Map<String, List<Contatto>> perCitta = new TreeMap<>();
for (Contatto c : contatti) {
perCitta.computeIfAbsent(c.citta(), k -> new ArrayList<>()).add(c);
}
for (Map.Entry<String, List<Contatto>> voce : perCitta.entrySet()) {
System.out.println(voce.getKey() + ": " + voce.getValue().size() + " contatti");
}
}
}
Spiegazione
Il record Contatto genera automaticamente costruttore, metodi di accesso (nome(), citta()…), equals e toString: per questo possiamo stampare direttamente trovato.
Cercare in una lista richiede di scorrerla tutta; con una HashMap indicizzata per nome, get trova il contatto quasi istantaneamente. Salviamo i nomi in minuscolo per rendere la ricerca indipendente dalle maiuscole.
computeIfAbsent crea la lista di una città la prima volta che la incontriamo e poi vi aggiunge il contatto. Usiamo TreeMap invece di HashMap per avere le città in ordine alfabetico.
Programmare verso le interfacce (List, Map) invece che verso le classi concrete è una buona pratica: potrai cambiare implementazione senza modificare il resto del codice.
Esercizio 10 – Forme geometriche: OOP completa
Livello: Avanzato · Argomenti: classi astratte, ereditarietà, polimorfismo, interfacce, incapsulamento
Consegna: Progetta una gerarchia di forme geometriche: una classe astratta Forma con i metodi area() e perimetro(), e le sottoclassi Cerchio, Rettangolo e Quadrato. Inserisci le forme in una lista, stampale e ordinale per area.
Teoria in breve: I quattro pilastri della OOP sono incapsulamento (attributi private), ereditarietà (extends), polimorfismo (lo stesso metodo si comporta in modo diverso a seconda dell’oggetto) e astrazione (classi abstract e interfacce che definiscono «cosa» fare senza dire «come»).
Soluzione
import java.util.*;
abstract class Forma implements Comparable<Forma> {
private final String nome;
protected Forma(String nome) {
this.nome = nome;
}
public abstract double area();
public abstract double perimetro();
@Override
public int compareTo(Forma altra) {
return Double.compare(this.area(), altra.area());
}
@Override
public String toString() {
return String.format("%-10s area=%8.2f perimetro=%7.2f", nome, area(), perimetro());
}
}
class Cerchio extends Forma {
private final double raggio;
Cerchio(double raggio) { super("Cerchio"); this.raggio = raggio; }
public double area() { return Math.PI * raggio * raggio; }
public double perimetro() { return 2 * Math.PI * raggio; }
}
class Rettangolo extends Forma {
private final double base, altezza;
Rettangolo(double base, double altezza) { this("Rettangolo", base, altezza); }
protected Rettangolo(String nome, double base, double altezza) {
super(nome);
this.base = base;
this.altezza = altezza;
}
public double area() { return base * altezza; }
public double perimetro() { return 2 * (base + altezza); }
}
class Quadrato extends Rettangolo {
Quadrato(double lato) { super("Quadrato", lato, lato); }
}
public class Forme {
public static void main(String[] args) {
List<Forma> forme = new ArrayList<>(List.of(
new Cerchio(3),
new Rettangolo(4, 6),
new Quadrato(5),
new Cerchio(1.5)
));
Collections.sort(forme);
forme.forEach(System.out::println);
double totale = 0;
for (Forma f : forme) totale += f.area();
System.out.printf("Area totale: %.2f%n", totale);
}
}
Spiegazione
Forma è abstract: non si può istanziare direttamente e obbliga ogni sottoclasse a implementare area() e perimetro(). Gli attributi sono private (incapsulamento): dall’esterno si usano solo i metodi.
Quadrato extends Rettangolo: un quadrato è un rettangolo con i lati uguali, quindi riutilizza tutto il codice del genitore richiamandone il costruttore con super(...).
Il polimorfismo si vede nel main: la lista contiene oggetti di tipi diversi, ma per ciascuno chiamiamo semplicemente f.area() e Java esegue automaticamente la versione giusta.
Implementando l’interfaccia Comparable con compareTo diciamo a Java come confrontare due forme (per area), così Collections.sort può ordinarle. System.out::println è un method reference, una forma compatta di lambda.
Sfida finale: aggiungi una classe Triangolo senza modificare il codice esistente. Se ci riesci, hai capito perché la OOP rende il software facile da estendere!
Consiglio: prova sempre a risolvere l’esercizio da solo prima di leggere la soluzione, poi modificalo per sperimentare: è il modo più veloce per imparare.
