“`html
Java è uno dei linguaggi di programmazione più utilizzati per realizzare applicazioni web, servizi aziendali, API, software desktop, sistemi distribuiti, strumenti di sviluppo e applicazioni eseguite nel cloud.
La sua caratteristica più conosciuta è la portabilità: il codice sorgente viene compilato in un formato intermedio chiamato bytecode, che può essere eseguito da una Java Virtual Machine compatibile con il sistema operativo utilizzato.
Java è un linguaggio a tipizzazione statica, orientato agli oggetti e multiparadigma. Oltre alla programmazione a oggetti, supporta infatti programmazione procedurale, generica, funzionale, concorrente e reattiva.
- che cosa sono Java, JVM, JDK e bytecode;
- come installare e verificare il JDK;
- come compilare ed eseguire un programma;
- come utilizzare JShell;
- come dichiarare variabili e tipi di dati;
- come utilizzare operatori, condizioni e cicli;
- come creare metodi, classi e oggetti;
- come funzionano incapsulamento, ereditarietà e polimorfismo;
- la differenza tra classi astratte e interfacce;
- come utilizzare record, enum e classi sealed;
- come organizzare il codice in package;
- come gestire eccezioni e risorse;
- come utilizzare array, collezioni e generics;
- come lavorare con lambda e Stream API;
- quali strumenti usare per creare un progetto Java moderno.
☕ Che cos’è Java
Java è un linguaggio di programmazione sviluppato originariamente da Sun Microsystems e presentato pubblicamente nel 1995. In seguito all’acquisizione di Sun Microsystems, lo sviluppo della piattaforma è proseguito sotto Oracle e all’interno del progetto OpenJDK.
Il linguaggio è stato progettato con l’obiettivo di offrire:
- portabilità tra sistemi operativi differenti;
- una sintassi relativamente semplice e leggibile;
- gestione automatica della memoria;
- controlli sui tipi eseguiti durante la compilazione;
- supporto alla programmazione a oggetti;
- strumenti per la concorrenza e il multithreading;
- una libreria standard ampia e riutilizzabile;
- un ambiente di esecuzione controllato dalla JVM.
Lo slogan tradizionalmente associato a Java è:
Write Once, Run Anywhere
Il principio indica che lo stesso bytecode può essere eseguito su sistemi differenti, purché sia disponibile una JVM compatibile.
Nonostante il nome simile, Java e JavaScript hanno sintassi, ambienti di esecuzione, sistemi di tipi e casi d’uso differenti.
⭐ Caratteristiche principali di Java
Portabilità
Il compilatore Java genera bytecode invece di codice macchina specifico per un solo processore. La JVM traduce o compila questo bytecode per il sistema sul quale viene eseguito.
Tipizzazione statica
Il tipo di una variabile viene determinato durante la compilazione. Il compilatore può quindi individuare numerosi errori prima dell’esecuzione.
int quantity = 10;
/*
* Errore di compilazione:
* una String non può essere assegnata a un int.
*/
// quantity = "dieci";
Programmazione a oggetti
Java permette di modellare il software attraverso classi e oggetti. Supporta incapsulamento, ereditarietà, astrazione e polimorfismo.
Non è però corretto descrivere Java come un linguaggio esclusivamente o puramente orientato agli oggetti: il linguaggio include tipi primitivi, metodi statici e diversi costrutti utilizzabili anche con uno stile procedurale o funzionale.
Gestione automatica della memoria
Gli oggetti vengono normalmente creati nell’area di memoria gestita dalla JVM. Il Garbage Collector individua gli oggetti non più raggiungibili e può recuperare la memoria che occupano.
Lo sviluppatore non deve quindi liberare manualmente ogni oggetto, come avviene in linguaggi che richiedono una gestione esplicita della memoria.
Concorrenza
Java offre API per thread, executor, sincronizzazione, strutture concorrenti, operazioni asincrone e virtual thread.
Compatibilità e stabilità
La piattaforma attribuisce grande importanza alla compatibilità del codice esistente. Molte applicazioni sviluppate con versioni precedenti possono continuare a essere eseguite su JVM moderne, salvo l’utilizzo di API eliminate, interne o incompatibili.
🧩 Differenza tra JDK, JVM e JRE
Quando si inizia a studiare Java è importante distinguere tre concetti: JVM, JRE e JDK.
| Componente | Significato | Funzione |
|---|---|---|
| JVM | Java Virtual Machine | Carica ed esegue il bytecode Java. |
| JRE | Java Runtime Environment | Indica l’ambiente necessario per eseguire applicazioni Java. |
| JDK | Java Development Kit | Contiene il runtime e gli strumenti necessari per sviluppare, compilare, documentare e analizzare applicazioni Java. |
La Java Virtual Machine
La JVM è responsabile di attività come:
- caricamento delle classi;
- verifica del bytecode;
- gestione della memoria;
- Garbage Collection;
- esecuzione delle istruzioni;
- compilazione Just-In-Time;
- gestione dei thread;
- controlli di sicurezza e accesso.
Il Java Development Kit
Il JDK include strumenti come:
| Comando | Utilizzo |
|---|---|
java |
Avvia un’applicazione Java. |
javac |
Compila il codice sorgente in bytecode. |
jar |
Crea e gestisce archivi JAR. |
javadoc |
Genera documentazione HTML dal codice sorgente. |
jshell |
Permette di provare istruzioni Java in modo interattivo. |
jlink |
Crea immagini runtime personalizzate. |
jpackage |
Crea pacchetti installabili per applicazioni Java. |
Storicamente era comune installare separatamente un JRE per eseguire le applicazioni. Nei progetti moderni si utilizza spesso un JDK completo oppure un runtime personalizzato creato con strumenti come jlink.
⚙️ Come viene eseguito un programma Java
Il percorso tradizionale di un programma Java è composto da due fasi principali.
Codice sorgente
Hello.java
│
│ javac
▼
Bytecode
Hello.class
│
│ java
▼
Java Virtual Machine
│
▼
Sistema operativo e processore
1. Compilazione
Il compilatore javac legge il file sorgente con estensione .java e genera uno o più file .class.
javac Hello.java
2. Esecuzione
Il comando java avvia la JVM e indica la classe contenente il metodo principale.
java Hello
Il nome della classe viene scritto senza l’estensione .class.
Compilazione Just-In-Time
Durante l’esecuzione, la JVM può individuare le parti di codice utilizzate più frequentemente e compilarle in codice macchina ottimizzato. Questo processo viene chiamato compilazione Just-In-Time.
📥 Installare e verificare il JDK
Per sviluppare in Java è necessario installare una distribuzione del JDK compatibile con il proprio sistema operativo.
È possibile scegliere tra diverse distribuzioni basate su OpenJDK. Per un progetto destinato alla produzione è importante verificare:
- versione di Java richiesta dal progetto;
- durata del supporto;
- licenza della distribuzione;
- disponibilità degli aggiornamenti di sicurezza;
- compatibilità con framework e librerie;
- architettura del sistema, per esempio x64 oppure ARM64.
Per verificare la versione installata:
java --version
Per verificare il compilatore:
javac --version
Un risultato possibile è:
java 26
javac 26
Java 26 è la feature release pubblicata nel marzo 2026, mentre Java 25 è una versione LTS scelta per il supporto di lungo periodo da numerosi fornitori.
Per seguire una guida introduttiva è ragionevole utilizzare una versione moderna e supportata. Gli esempi di questo articolo utilizzano funzionalità stabili e sono compatibili con Java 25 e Java 26.
🚀 Creare il primo programma Java
Creiamo un file chiamato HelloJava.java:
public class HelloJava {
public static void main(String[] args) {
/*
* println() stampa il testo e termina
* la riga con un carattere di nuova linea.
*/
System.out.println(
"Ciao, Java!"
);
}
}
Compiliamo il file:
javac HelloJava.java
Il compilatore genera:
HelloJava.class
Eseguiamo il programma:
java HelloJava
Il risultato sarà:
Ciao, Java!
Esecuzione diretta di un file sorgente
Per piccoli programmi è possibile chiedere al comando java di eseguire direttamente il file sorgente:
java HelloJava.java
Questa modalità è comoda per esercizi, script e dimostrazioni. Nei progetti strutturati si utilizzano normalmente compilazione separata e strumenti di build.
🧪 Provare Java con JShell
JShell è un ambiente interattivo incluso nel JDK. Permette di provare espressioni, variabili e metodi senza creare immediatamente un progetto completo.
Per avviarlo:
jshell
All’interno della console:
int price = 25;
int quantity = 4;
int total = price * quantity;
System.out.println(total);
È possibile definire anche un metodo:
int square(int number) {
return number * number;
}
square(5);
Per uscire:
/exit
🏗️ Struttura di un programma Java
Una classe Java può contenere:
- campi;
- costruttori;
- metodi;
- blocchi di inizializzazione;
- classi e interfacce annidate;
- record o enum annidati.
public class Product {
/*
* Campi dell'oggetto.
*/
private String name;
private double price;
/*
* Costruttore.
*/
public Product(
String name,
double price
) {
this.name = name;
this.price = price;
}
/*
* Metodo di istanza.
*/
public double calculatePriceWithTax(
double taxRate
) {
return price + price * taxRate;
}
/*
* Punto di ingresso per questo esempio.
*/
public static void main(String[] args) {
Product product =
new Product(
"Tastiera",
49.90
);
double finalPrice =
product.calculatePriceWithTax(
0.22
);
System.out.println(finalPrice);
}
}
Il nome del file
Quando un file contiene una classe top-level dichiarata public, il nome del file deve coincidere con il nome della classe:
public class Product → Product.java
Un file sorgente può contenere più tipi top-level, ma soltanto uno può essere pubblico.
Il metodo main
La dichiarazione classica del punto di ingresso è:
public static void main(String[] args)
| Elemento | Significato |
|---|---|
public |
Il metodo è accessibile al launcher Java. |
static |
Non è necessario creare un oggetto della classe. |
void |
Il metodo non restituisce un valore. |
String[] args |
Contiene gli argomenti passati dalla riga di comando. |
Esempio con argomenti:
public class Greeting {
public static void main(String[] args) {
if (args.length == 0) {
System.out.println(
"Inserisci il tuo nome"
);
return;
}
System.out.println(
"Ciao, " + args[0] + "!"
);
}
}
Esecuzione:
java Greeting Mario
💬 Commenti e documentazione
Commento su una riga
// Questo è un commento su una riga.
int quantity = 5;
Commento su più righe
/*
* Questo commento può occupare
* più righe.
*/
double price = 19.90;
Commento Javadoc
/**
* Calcola il prezzo totale.
*
* @param unitPrice prezzo di una singola unità
* @param quantity numero di unità
* @return prezzo complessivo
* @throws IllegalArgumentException se i valori non sono validi
*/
public static double calculateTotal(
double unitPrice,
int quantity
) {
if (unitPrice < 0 || quantity < 0) {
throw new IllegalArgumentException(
"I valori non possono essere negativi"
);
}
return unitPrice * quantity;
}
La documentazione può essere generata con:
javadoc NomeClasse.java
📦 Variabili e costanti
Una variabile associa un nome a un valore di un determinato tipo.
int age = 30;
double price = 49.90;
String name = "Marco";
boolean active = true;
Dichiarazione e assegnazione
/*
* Dichiarazione.
*/
int quantity;
/*
* Assegnazione.
*/
quantity = 10;
Inferenza locale con var
Per le variabili locali il compilatore può dedurre il tipo dall’espressione iniziale:
var city = "Roma";
var population = 2_748_109;
var temperature = 24.5;
var non rende Java dinamicamente tipizzato. Il tipo viene comunque stabilito durante la compilazione e non può cambiare.
var value = 10;
/*
* Errore: value è stato dedotto come int.
*/
// value = "dieci";
var può essere utilizzato per variabili locali con un inizializzatore, ma non per campi, parametri o tipi restituiti dai metodi.
Costanti
Una variabile dichiarata final può essere assegnata una sola volta:
final double VAT_RATE = 0.22;
Le costanti di classe vengono normalmente dichiarate static final:
public static final int MAX_USERS = 100;
🔢 Tipi primitivi e tipi riferimento
I tipi Java possono essere suddivisi in due gruppi principali.
Tipi primitivi
| Tipo | Esempio | Utilizzo |
|---|---|---|
byte |
byte level = 10; |
Interi molto piccoli o dati binari. |
short |
short year = 2026; |
Interi con intervallo ridotto. |
int |
int users = 500; |
Tipo intero più utilizzato. |
long |
long views = 5_000_000L; |
Interi molto grandi. |
float |
float rate = 1.5F; |
Decimali a precisione singola. |
double |
double price = 29.90; |
Decimali a doppia precisione. |
char |
char grade = 'A'; |
Singola unità UTF-16. |
boolean |
boolean valid = true; |
Valore logico vero o falso. |
Tipi riferimento
Le variabili di tipo riferimento contengono un riferimento a un oggetto.
String title = "Guida Java";
Product product =
new Product(
"Mouse",
24.90
);
List<String> names =
new ArrayList<>();
I riferimenti possono avere valore null:
String description = null;
L’utilizzo di un riferimento null come se contenesse un oggetto può provocare una NullPointerException.
Wrapper dei tipi primitivi
Ogni tipo primitivo possiede una classe wrapper:
| Primitivo | Wrapper |
|---|---|
int |
Integer |
long |
Long |
double |
Double |
boolean |
Boolean |
char |
Character |
/*
* Autoboxing: int → Integer.
*/
Integer quantity = 10;
/*
* Unboxing: Integer → int.
*/
int value = quantity;
Per importi monetari è generalmente preferibile utilizzare BigDecimal invece di float o double, che possono introdurre approssimazioni binarie.
BigDecimal price =
new BigDecimal("19.90");
BigDecimal quantity =
new BigDecimal("3");
BigDecimal total =
price.multiply(quantity);
➕ Operatori Java
Operatori aritmetici
int a = 10;
int b = 3;
int sum = a + b;
int difference = a - b;
int product = a * b;
int quotient = a / b;
int remainder = a % b;
La divisione tra numeri interi produce un risultato intero:
int result = 10 / 3;
/*
* result vale 3.
*/
Per ottenere un risultato decimale:
double result = 10.0 / 3.0;
Operatori di confronto
int age = 20;
boolean adult = age >= 18;
boolean minor = age < 18;
boolean exactlyTwenty = age == 20;
boolean different = age != 30;
Operatori logici
boolean registered = true;
boolean emailVerified = true;
boolean canAccess =
registered && emailVerified;
boolean requiresVerification =
!emailVerified;
boolean canUseTrial =
registered || emailVerified;
Confrontare oggetti
L’operatore == confronta i riferimenti, non necessariamente il contenuto degli oggetti.
String first =
new String("Java");
String second =
new String("Java");
System.out.println(
first == second
); // false
System.out.println(
first.equals(second)
); // true
Per confrontare il contenuto delle stringhe bisogna utilizzare equals().
🔀 Condizioni e switch
if, else if ed else
int age = 20;
if (age < 0) {
System.out.println(
"Età non valida"
);
} else if (age < 18) {
System.out.println(
"Utente minorenne"
);
} else {
System.out.println(
"Utente maggiorenne"
);
}
Operatore ternario
String result =
age >= 18
? "Maggiorenne"
: "Minorenne";
switch tradizionale
int day = 2;
switch (day) {
case 1:
System.out.println("Lunedì");
break;
case 2:
System.out.println("Martedì");
break;
default:
System.out.println(
"Giorno non riconosciuto"
);
}
switch expression
String dayName =
switch (day) {
case 1 -> "Lunedì";
case 2 -> "Martedì";
case 3 -> "Mercoledì";
case 4 -> "Giovedì";
case 5 -> "Venerdì";
case 6, 7 -> "Fine settimana";
default ->
throw new IllegalArgumentException(
"Giorno non valido: " + day
);
};
Quando un caso richiede più istruzioni è possibile utilizzare yield:
String category =
switch (day) {
case 1, 2, 3, 4, 5 -> {
System.out.println(
"Giorno lavorativo"
);
yield "WORK";
}
case 6, 7 -> "WEEKEND";
default -> "UNKNOWN";
};
🔁 Cicli
for
for (
int index = 0;
index < 5;
index++
) {
System.out.println(index);
}
Enhanced for
String[] languages = {
"Java",
"Kotlin",
"Scala"
};
for (String language : languages) {
System.out.println(language);
}
while
int counter = 0;
while (counter < 3) {
System.out.println(counter);
counter++;
}
do-while
int value = 0;
do {
System.out.println(value);
value++;
} while (value < 3);
break e continue
for (int number = 1; number <= 10; number++) {
if (number == 3) {
/*
* Salta soltanto questa iterazione.
*/
continue;
}
if (number == 8) {
/*
* Termina l'intero ciclo.
*/
break;
}
System.out.println(number);
}
🧰 Metodi e parametri
Un metodo raccoglie una porzione di logica riutilizzabile.
public static int sum(
int first,
int second
) {
return first + second;
}
Utilizzo:
int total = sum(10, 20);
Metodo senza valore restituito
public static void printMessage(
String message
) {
System.out.println(message);
}
Overloading
È possibile dichiarare più metodi con lo stesso nome, purché abbiano parametri differenti.
public static int sum(
int first,
int second
) {
return first + second;
}
public static double sum(
double first,
double second
) {
return first + second;
}
public static int sum(
int first,
int second,
int third
) {
return first + second + third;
}
Il tipo restituito, da solo, non è sufficiente per distinguere due metodi sovraccaricati.
Parametri variabili
public static int sum(int... numbers) {
int total = 0;
for (int number : numbers) {
total += number;
}
return total;
}
Invocazione:
int total = sum(10, 20, 30, 40);
Passaggio dei parametri
Java utilizza sempre il passaggio per valore. Nel caso degli oggetti, il valore copiato è il riferimento all’oggetto.
public static void changeNumber(int number) {
number = 100;
}
public static void main(String[] args) {
int value = 10;
changeNumber(value);
/*
* value rimane 10.
*/
System.out.println(value);
}
🧱 Classi e oggetti
Una classe descrive lo stato e il comportamento di una categoria di oggetti. Un oggetto è un’istanza concreta della classe.
public class Customer {
private String name;
private String email;
public void printInformation() {
System.out.println(
name + " - " + email
);
}
}
Creazione di un oggetto:
Customer customer =
new Customer();
L’operatore new crea una nuova istanza e invoca un costruttore.
Campi e variabili locali
public class Counter {
/*
* Campo: appartiene all'oggetto.
*/
private int value;
public void increment() {
/*
* Variabile locale:
* esiste soltanto durante
* l'esecuzione del metodo.
*/
int increment = 1;
value += increment;
}
}
La parola chiave this
this rappresenta l’istanza corrente.
public class User {
private String name;
public void setName(String name) {
/*
* this.name identifica il campo.
* name identifica il parametro.
*/
this.name = name;
}
}
🏭 Costruttori
Un costruttore viene eseguito durante la creazione di un oggetto. Ha lo stesso nome della classe e non dichiara un tipo restituito.
public class Book {
private final String title;
private final String author;
public Book(
String title,
String author
) {
this.title = title;
this.author = author;
}
}
Overloading dei costruttori
public class Book {
private final String title;
private final String author;
private final int pages;
public Book(
String title,
String author
) {
/*
* Invoca un altro costruttore
* della stessa classe.
*/
this(title, author, 0);
}
public Book(
String title,
String author,
int pages
) {
this.title = title;
this.author = author;
this.pages = pages;
}
}
Costruttore predefinito
Se una classe non dichiara alcun costruttore, il compilatore può aggiungere un costruttore senza parametri. Quando viene dichiarato almeno un costruttore, quello predefinito non viene aggiunto automaticamente.
🔒 Incapsulamento
L’incapsulamento consiste nel proteggere lo stato interno di un oggetto e nel controllare il modo in cui può essere modificato.
Una classe ben incapsulata non espone direttamente i propri campi modificabili.
public class BankAccount {
private BigDecimal balance =
BigDecimal.ZERO;
public BigDecimal getBalance() {
return balance;
}
public void deposit(BigDecimal amount) {
if (
amount == null
|| amount.signum() <= 0
) {
throw new IllegalArgumentException(
"L'importo deve essere positivo"
);
}
balance = balance.add(amount);
}
public void withdraw(BigDecimal amount) {
if (
amount == null
|| amount.signum() <= 0
) {
throw new IllegalArgumentException(
"L'importo deve essere positivo"
);
}
if (balance.compareTo(amount) < 0) {
throw new IllegalStateException(
"Saldo insufficiente"
);
}
balance = balance.subtract(amount);
}
}
Il saldo non può essere modificato liberamente dall’esterno. Ogni operazione passa attraverso metodi che verificano le regole del conto.
🌳 Ereditarietà e polimorfismo
L’ereditarietà permette a una classe di estendere un’altra classe.
public class Animal {
public void makeSound() {
System.out.println(
"Suono generico"
);
}
}
public class Dog extends Animal {
@Override
public void makeSound() {
System.out.println("Bau");
}
}
Utilizzo:
Animal animal =
new Dog();
animal.makeSound();
/*
* Il metodo eseguito è quello di Dog.
*/
Questo comportamento è un esempio di polimorfismo: una variabile del tipo padre può riferirsi a un oggetto di una sottoclasse.
La parola chiave super
public class Employee {
private final String name;
public Employee(String name) {
this.name = name;
}
public String getName() {
return name;
}
}
public class Manager extends Employee {
private final String department;
public Manager(
String name,
String department
) {
/*
* Invoca il costruttore
* della classe padre.
*/
super(name);
this.department = department;
}
}
Ereditarietà singola
Una classe Java può estendere direttamente una sola classe. Può però implementare più interfacce.
L’ereditarietà crea un legame forte tra classe padre e sottoclasse. Quando la relazione non rappresenta realmente un “è un”, spesso è preferibile comporre più oggetti.
📐 Classi astratte e interfacce
Classe astratta
Una classe astratta non può essere istanziata direttamente. Può contenere campi, costruttori, metodi concreti e metodi astratti.
Una classe dichiarata abstract non è obbligata ad avere metodi astratti.
public abstract class Shape {
private final String color;
protected Shape(String color) {
this.color = color;
}
public String getColor() {
return color;
}
public abstract double area();
}
public final class Circle extends Shape {
private final double radius;
public Circle(
String color,
double radius
) {
super(color);
this.radius = radius;
}
@Override
public double area() {
return Math.PI * radius * radius;
}
}
Interfaccia
Un’interfaccia definisce un contratto che può essere implementato da più classi.
public interface Printable {
void print();
}
public class Invoice
implements Printable {
@Override
public void print() {
System.out.println(
"Stampa della fattura"
);
}
}
Le interfacce moderne possono contenere:
- metodi astratti;
- metodi
default; - metodi statici;
- metodi privati di supporto;
- costanti implicitamente
public static final.
public interface NotificationService {
void send(String message);
default void sendWelcomeMessage() {
send(createWelcomeMessage());
}
static boolean isValid(String message) {
return message != null
&& !message.isBlank();
}
private String createWelcomeMessage() {
return "Benvenuto!";
}
}
Classe astratta o interfaccia?
| Caratteristica | Classe astratta | Interfaccia |
|---|---|---|
| Campi di istanza | Sì | No |
| Costruttore | Sì | No |
| Metodi implementati | Sì | Sì, tramite metodi default, statici o privati |
| Implementazione multipla | No | Una classe può implementarne più di una |
🧬 Record, enum e classi sealed
Record
Un record è un tipo particolarmente adatto a rappresentare dati immutabili e trasportare valori.
public record Product(
long id,
String name,
BigDecimal price
) {
}
Il compilatore genera automaticamente:
- campi finali per i componenti;
- costruttore canonico;
- metodi di accesso;
equals();hashCode();toString().
Utilizzo:
Product product =
new Product(
1L,
"Monitor",
new BigDecimal("249.90")
);
System.out.println(
product.name()
);
Validare un record
public record Product(
long id,
String name,
BigDecimal price
) {
public Product {
if (id <= 0) {
throw new IllegalArgumentException(
"L'identificativo deve essere positivo"
);
}
if (
name == null
|| name.isBlank()
) {
throw new IllegalArgumentException(
"Il nome è obbligatorio"
);
}
if (
price == null
|| price.signum() < 0
) {
throw new IllegalArgumentException(
"Il prezzo non è valido"
);
}
}
}
Enum
Un enum rappresenta un insieme chiuso di valori.
public enum OrderStatus {
CREATED,
PAID,
SHIPPED,
DELIVERED,
CANCELLED
}
Un enum può avere campi e metodi:
public enum UserRole {
USER(false),
ADMIN(true);
private final boolean administrative;
UserRole(boolean administrative) {
this.administrative = administrative;
}
public boolean isAdministrative() {
return administrative;
}
}
Classi sealed
Una gerarchia sealed limita esplicitamente le classi autorizzate a estendere o implementare un tipo.
public sealed interface Payment
permits CardPayment,
BankTransfer {
}
public record CardPayment(
String cardToken
) implements Payment {
}
public record BankTransfer(
String iban
) implements Payment {
}
Queste gerarchie sono utili quando il dominio prevede un insieme controllato di alternative.
🗂️ Package e import
I package organizzano le classi e riducono i conflitti tra nomi.
package it.example.shop.domain;
public class Product {
// Implementazione.
}
La struttura delle directory deve corrispondere al package:
src/
└── it/
└── example/
└── shop/
└── domain/
└── Product.java
Importare una classe
package it.example.shop.service;
import it.example.shop.domain.Product;
import java.math.BigDecimal;
public class ProductService {
public Product create() {
return new Product();
}
}
Le classi del package java.lang, come String, Object, Math e System, vengono importate automaticamente.
Import statico
import static java.lang.Math.PI;
import static java.lang.Math.pow;
double area =
PI * pow(radius, 2);
Gli import statici devono essere utilizzati con moderazione, evitando di rendere poco chiara la provenienza di metodi e costanti.
🔐 Modificatori di accesso
| Modificatore | Stessa classe | Stesso package | Sottoclasse esterna | Altre classi |
|---|---|---|---|---|
public |
Sì | Sì | Sì | Sì |
protected |
Sì | Sì | Sì, secondo le regole dell’ereditarietà | No |
| Nessun modificatore | Sì | Sì | No, se esterna al package | No |
private |
Sì | No | No | No |
Una classe top-level può essere public oppure avere accesso package-private. I modificatori private e protected possono invece essere applicati a tipi annidati.
📌 Modificatori static e final
static
Un membro statico appartiene alla classe e non a una singola istanza.
public class IdGenerator {
private static long nextId = 1;
public static long generate() {
return nextId++;
}
}
Utilizzo:
long id = IdGenerator.generate();
final su una variabile
final List<String> names =
new ArrayList<>();
/*
* Consentito: l'oggetto può essere modificabile.
*/
names.add("Anna");
/*
* Non consentito:
* il riferimento non può essere riassegnato.
*/
// names = new ArrayList<>();
final su un metodo
public final void validate() {
// Le sottoclassi non possono fare override.
}
final su una classe
public final class SecurityToken {
// La classe non può essere estesa.
}
📊 Array
Un array contiene un numero fisso di elementi dello stesso tipo.
int[] numbers =
new int[3];
numbers[0] = 10;
numbers[1] = 20;
numbers[2] = 30;
Inizializzazione diretta:
String[] cities = {
"Roma",
"Milano",
"Torino"
};
Accesso e lunghezza:
String firstCity = cities[0];
int size = cities.length;
Gli indici iniziano da zero. L’accesso a un indice inesistente provoca una ArrayIndexOutOfBoundsException.
Array multidimensionali
int[][] matrix = {
{1, 2, 3},
{4, 5, 6}
};
int value = matrix[1][2];
🧺 Java Collection Framework
Le collezioni permettono di gestire gruppi di oggetti con dimensione variabile.
| Interfaccia | Caratteristiche | Implementazioni comuni |
|---|---|---|
List |
Sequenza ordinata, duplicati consentiti. | ArrayList, LinkedList |
Set |
Elementi unici. | HashSet, LinkedHashSet, TreeSet |
Queue |
Gestione di elementi in attesa. | ArrayDeque, PriorityQueue |
Map |
Associa chiavi uniche a valori. | HashMap, LinkedHashMap, TreeMap |
List
List<String> names =
new ArrayList<>();
names.add("Luca");
names.add("Anna");
names.add("Luca");
String first = names.get(0);
names.remove("Anna");
Set
Set<String> codes =
new HashSet<>();
codes.add("A");
codes.add("B");
codes.add("A");
/*
* La seconda "A" non crea
* un nuovo elemento.
*/
Map
Map<Long, String> users =
new HashMap<>();
users.put(1L, "Mario");
users.put(2L, "Lucia");
String user = users.get(1L);
boolean present =
users.containsKey(2L);
Collezioni non modificabili
List<String> languages =
List.of(
"Java",
"Kotlin",
"Scala"
);
Set<String> roles =
Set.of(
"USER",
"ADMIN"
);
Map<String, Integer> scores =
Map.of(
"Anna", 90,
"Luca", 85
);
Queste factory non accettano normalmente elementi null e restituiscono collezioni che non possono essere modificate tramite add(), remove() o put().
🏷️ Generics
I generics permettono di parametrizzare classi, interfacce e metodi con uno o più tipi.
Senza generics:
List values =
new ArrayList();
values.add("Java");
values.add(10);
/*
* Il cast è necessario e può fallire.
*/
String language =
(String) values.get(0);
Con generics:
List<String> languages =
new ArrayList<>();
languages.add("Java");
/*
* Il compilatore impedisce l'inserimento
* di un Integer.
*/
// languages.add(10);
String language =
languages.get(0);
Classe generica
public class Box<T> {
private T value;
public Box(T value) {
this.value = value;
}
public T getValue() {
return value;
}
public void setValue(T value) {
this.value = value;
}
}
Utilizzo:
Box<String> textBox =
new Box<>("Java");
Box<Integer> numberBox =
new Box<>(100);
Metodo generico
public static <T> T first(
List<T> values
) {
if (values.isEmpty()) {
throw new IllegalArgumentException(
"La lista è vuota"
);
}
return values.get(0);
}
Wildcard
public static double sum(
List<? extends Number> numbers
) {
double total = 0;
for (Number number : numbers) {
total += number.doubleValue();
}
return total;
}
🚨 Gestione delle eccezioni
Le eccezioni rappresentano condizioni anomale che interrompono il normale flusso di esecuzione.
Gerarchia semplificata
Throwable
├── Error
└── Exception
├── RuntimeException
└── Altre eccezioni checked
Checked exception
Una checked exception deve essere gestita oppure dichiarata nel metodo.
public String readFile(Path path)
throws IOException {
return Files.readString(path);
}
Unchecked exception
Le sottoclassi di RuntimeException non devono essere dichiarate obbligatoriamente.
public int divide(
int first,
int second
) {
if (second == 0) {
throw new IllegalArgumentException(
"Il divisore non può essere zero"
);
}
return first / second;
}
try-catch
try {
String content =
Files.readString(
Path.of("data.txt")
);
System.out.println(content);
} catch (NoSuchFileException exception) {
System.err.println(
"Il file non esiste"
);
} catch (IOException exception) {
System.err.println(
"Errore durante la lettura: "
+ exception.getMessage()
);
}
finally
try {
System.out.println(
"Esecuzione"
);
} finally {
/*
* Viene eseguito normalmente
* anche se si verifica un errore.
*/
System.out.println(
"Pulizia finale"
);
}
Eccezione personalizzata
public class ProductNotFoundException
extends RuntimeException {
public ProductNotFoundException(
long productId
) {
super(
"Prodotto non trovato: "
+ productId
);
}
}
🧹 Try-with-resources
Le risorse che implementano AutoCloseable possono essere chiuse automaticamente.
public List<String> readLines(
Path path
) throws IOException {
try (
BufferedReader reader =
Files.newBufferedReader(path)
) {
return reader.lines()
.toList();
}
}
Il metodo close() viene richiamato automaticamente anche quando si verifica un’eccezione.
Il try-with-resources dovrebbe essere preferito alla chiusura manuale di:
- file;
- stream;
- reader e writer;
- connessioni JDBC;
- statement;
- socket;
- altre risorse chiudibili.
➡️ Espressioni lambda
Una lambda permette di rappresentare un comportamento in modo compatto quando è richiesta un’interfaccia funzionale.
Un’interfaccia funzionale dichiara un solo metodo astratto:
@FunctionalInterface
public interface Calculator {
int calculate(
int first,
int second
);
}
Implementazione tradizionale:
Calculator sum =
new Calculator() {
@Override
public int calculate(
int first,
int second
) {
return first + second;
}
};
La stessa logica con una lambda:
Calculator sum =
(first, second) ->
first + second;
Interfacce funzionali comuni
| Interfaccia | Metodo | Utilizzo |
|---|---|---|
Predicate<T> |
boolean test(T) |
Verifica una condizione. |
Function<T, R> |
R apply(T) |
Trasforma un valore. |
Consumer<T> |
void accept(T) |
Consuma un valore senza restituirne uno. |
Supplier<T> |
T get() |
Produce un valore. |
Predicate<Integer> adultAge =
age -> age >= 18;
Function<String, Integer> length =
text -> text.length();
Consumer<String> printer =
text -> System.out.println(text);
Supplier<UUID> identifier =
() -> UUID.randomUUID();
Method reference
Consumer<String> printer =
System.out::println;
Function<String, Integer> parser =
Integer::parseInt;
🌊 Stream API
La Stream API permette di elaborare sequenze di elementi attraverso pipeline dichiarative.
List<String> names =
List.of(
"Anna",
"Luca",
"Alessandro",
"Marta"
);
List<String> result =
names.stream()
/*
* Mantiene i nomi che iniziano per A.
*/
.filter(
name ->
name.startsWith("A")
)
/*
* Converte ogni nome in maiuscolo.
*/
.map(
String::toUpperCase
)
/*
* Ordina i valori.
*/
.sorted()
/*
* Raccoglie il risultato.
*/
.toList();
Operazioni intermedie
filter();map();flatMap();distinct();sorted();limit();skip();peek().
Operazioni terminali
toList();collect();forEach();count();reduce();findFirst();anyMatch();allMatch().
Raggruppare elementi
Map<OrderStatus, List<Order>> ordersByStatus =
orders.stream()
.collect(
Collectors.groupingBy(
Order::status
)
);
Calcolare un totale
BigDecimal total =
products.stream()
.map(Product::price)
.reduce(
BigDecimal.ZERO,
BigDecimal::add
);
Uno Stream descrive un’elaborazione e viene normalmente consumato una sola volta. Non conserva autonomamente gli elementi come una lista o un set.
🎁 Optional
Optional<T> rappresenta un risultato che può contenere oppure non contenere un valore.
public Optional<Product> findById(
long productId
) {
return products.stream()
.filter(
product ->
product.id()
== productId
)
.findFirst();
}
Utilizzo:
Optional<Product> result =
repository.findById(10L);
result.ifPresent(
product ->
System.out.println(
product.name()
)
);
Valore alternativo
Product product =
result.orElseGet(
() ->
createDefaultProduct()
);
Lanciare un’eccezione
Product product =
result.orElseThrow(
() ->
new ProductNotFoundException(
10L
)
);
Optional è particolarmente utile come tipo restituito. Non dovrebbe essere utilizzato automaticamente per ogni campo, parametro o elemento di una collezione.
🛒 Esempio pratico completo
Creiamo una piccola applicazione per gestire prodotti e calcolare il valore complessivo di un catalogo.
Struttura del progetto
src/
└── it/
└── example/
└── catalog/
├── CatalogApplication.java
├── Product.java
├── ProductCategory.java
├── ProductNotFoundException.java
└── ProductService.java
Categoria del prodotto
package it.example.catalog;
public enum ProductCategory {
COMPUTER,
ACCESSORY,
SMARTPHONE
}
Record Product
package it.example.catalog;
import java.math.BigDecimal;
import java.util.Objects;
public record Product(
long id,
String name,
ProductCategory category,
BigDecimal price,
boolean available
) {
public Product {
if (id <= 0) {
throw new IllegalArgumentException(
"L'identificativo deve essere positivo"
);
}
if (
name == null
|| name.isBlank()
) {
throw new IllegalArgumentException(
"Il nome è obbligatorio"
);
}
Objects.requireNonNull(
category,
"La categoria è obbligatoria"
);
Objects.requireNonNull(
price,
"Il prezzo è obbligatorio"
);
if (price.signum() < 0) {
throw new IllegalArgumentException(
"Il prezzo non può essere negativo"
);
}
}
}
Eccezione personalizzata
package it.example.catalog;
public class ProductNotFoundException
extends RuntimeException {
public ProductNotFoundException(long id) {
super(
"Prodotto non trovato: " + id
);
}
}
Servizio del catalogo
package it.example.catalog;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
public class ProductService {
private final List<Product> products =
new ArrayList<>();
public void add(Product product) {
if (product == null) {
throw new IllegalArgumentException(
"Il prodotto non può essere null"
);
}
boolean duplicated =
products.stream()
.anyMatch(
existing ->
existing.id()
== product.id()
);
if (duplicated) {
throw new IllegalArgumentException(
"Esiste già un prodotto con ID "
+ product.id()
);
}
products.add(product);
}
public Product findById(long id) {
return products.stream()
.filter(
product ->
product.id() == id
)
.findFirst()
.orElseThrow(
() ->
new ProductNotFoundException(
id
)
);
}
public List<Product> findAvailable() {
return products.stream()
.filter(Product::available)
.sorted(
Comparator.comparing(
Product::name
)
)
.toList();
}
public BigDecimal calculateTotalValue() {
return products.stream()
.map(Product::price)
.reduce(
BigDecimal.ZERO,
BigDecimal::add
);
}
public Map<ProductCategory, List<Product>>
groupByCategory() {
return products.stream()
.collect(
Collectors.groupingBy(
Product::category
)
);
}
public int size() {
return products.size();
}
}
Classe principale
package it.example.catalog;
import java.math.BigDecimal;
import java.util.List;
import java.util.Map;
public class CatalogApplication {
public static void main(String[] args) {
ProductService service =
new ProductService();
service.add(
new Product(
1L,
"Laptop Pro",
ProductCategory.COMPUTER,
new BigDecimal("1299.90"),
true
)
);
service.add(
new Product(
2L,
"Mouse Wireless",
ProductCategory.ACCESSORY,
new BigDecimal("39.90"),
true
)
);
service.add(
new Product(
3L,
"Smartphone X",
ProductCategory.SMARTPHONE,
new BigDecimal("749.00"),
false
)
);
System.out.println(
"Prodotti disponibili:"
);
service.findAvailable()
.forEach(product ->
System.out.printf(
"- %s: %s €%n",
product.name(),
product.price()
)
);
BigDecimal total =
service.calculateTotalValue();
System.out.println(
"Valore totale: "
+ total
+ " €"
);
Map<ProductCategory, List<Product>>
grouped =
service.groupByCategory();
grouped.forEach(
(category, products) -> {
System.out.println(
category
+ ": "
+ products.size()
);
}
);
try {
Product product =
service.findById(99L);
System.out.println(product);
} catch (
ProductNotFoundException exception
) {
System.err.println(
exception.getMessage()
);
}
}
}
Compilare il progetto
Dalla directory che contiene src:
javac -d out \
src/it/example/catalog/*.java
Il parametro -d out indica la directory nella quale salvare i file compilati.
Eseguire il progetto
java -cp out \
it.example.catalog.CatalogApplication
🛠️ IDE, Maven, Gradle e Git
IDE
Un ambiente di sviluppo integrato può offrire:
- completamento automatico;
- controllo degli errori;
- refactoring;
- debugger;
- esecuzione dei test;
- gestione delle dipendenze;
- integrazione con Git.
Tra gli IDE più utilizzati per Java troviamo IntelliJ IDEA, Eclipse e Apache NetBeans. È possibile utilizzare anche editor estensibili come Visual Studio Code.
Maven
Maven utilizza normalmente un file pom.xml per descrivere il progetto e le dipendenze.
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>it.example</groupId>
<artifactId>java-introduction</artifactId>
<version>1.0.0</version>
<properties>
<maven.compiler.release>25</maven.compiler.release>
<project.build.sourceEncoding>
UTF-8
</project.build.sourceEncoding>
</properties>
</project>
Comandi comuni:
mvn compile
mvn test
mvn package
Gradle
Un progetto Gradle può utilizzare un file build.gradle:
plugins {
id 'java'
}
group = 'it.example'
version = '1.0.0'
java {
toolchain {
languageVersion =
JavaLanguageVersion.of(25)
}
}
repositories {
mavenCentral()
}
Comandi comuni:
./gradlew compileJava
./gradlew test
./gradlew build
Git
Git permette di conservare la cronologia del codice e collaborare con altri sviluppatori.
git init
git add .
git commit -m "Primo progetto Java"
🧯 Errori comuni dei principianti
| Problema | Possibile causa | Soluzione |
|---|---|---|
javac non riconosciuto |
JDK non installato o PATH non configurato. | Verificare installazione, JAVA_HOME e PATH. |
| La classe pubblica non coincide con il file | Nome del file errato. | Rinominare il file in base alla classe pubblica. |
Could not find or load main class |
Classpath, package o directory errati. | Eseguire dalla directory corretta e usare il nome completo della classe. |
NullPointerException |
Utilizzo di un riferimento null. |
Validare i dati e progettare contratti chiari. |
Confronto di String con == |
Confusione tra riferimento e contenuto. | Utilizzare equals() o Objects.equals(). |
| Divisione intera inattesa | Entrambi gli operandi sono interi. | Convertire almeno un operando in double. |
Modifica di una lista creata con List.of() |
La lista non è modificabile. | Creare un nuovo ArrayList se servono modifiche. |
| Risorsa non chiusa | Stream o connessione lasciati aperti. | Utilizzare try-with-resources. |
✅ Buone pratiche per iniziare con Java
Utilizzare nomi descrittivi
// Poco chiaro.
int x = 10;
// Più chiaro.
int maximumRetryAttempts = 10;
Seguire le convenzioni di denominazione
- classi e interfacce:
PascalCase; - metodi e variabili:
camelCase; - costanti:
UPPER_SNAKE_CASE; - package: lettere minuscole;
- nomi booleani: preferire forme come
active,validohasPermission.
Mantenere i metodi brevi
Un metodo dovrebbe avere una responsabilità chiara. Quando contiene validazione, accesso al database, trasformazione, logging e invio di notifiche, probabilmente dovrebbe essere suddiviso.
Preferire l’immutabilità
Quando possibile:
- utilizzare campi
final; - evitare setter non necessari;
- restituire collezioni non modificabili;
- utilizzare record per semplici valori di dominio;
- evitare di condividere stato mutabile.
Validare i dati all’ingresso
public void registerUser(
String email
) {
if (
email == null
|| email.isBlank()
) {
throw new IllegalArgumentException(
"L'email è obbligatoria"
);
}
// Registrazione.
}
Non ignorare le eccezioni
try {
processFile();
} catch (IOException exception) {
/*
* Evitare catch completamente vuoti.
* Registrare, gestire o propagare l'errore.
*/
logger.error(
"Impossibile elaborare il file",
exception
);
}
Programmare verso le interfacce
/*
* Preferibile:
* la variabile dipende dall'interfaccia.
*/
List<String> names =
new ArrayList<>();
/*
* Più rigido:
* espone inutilmente l'implementazione.
*/
ArrayList<String> otherNames =
new ArrayList<>();
Utilizzare BigDecimal per il denaro
Per prezzi, saldi e imposte è preferibile modellare chiaramente scala e arrotondamento.
BigDecimal price =
new BigDecimal("19.99");
BigDecimal tax =
price.multiply(
new BigDecimal("0.22")
);
BigDecimal finalPrice =
price.add(tax)
.setScale(
2,
RoundingMode.HALF_UP
);
Scrivere test automatici
Un test consente di verificare rapidamente che una parte del programma continui a comportarsi come previsto.
import static org.junit.jupiter.api.Assertions.*;
import org.junit.jupiter.api.Test;
class CalculatorTest {
@Test
void shouldAddTwoNumbers() {
int result =
Calculator.sum(10, 20);
assertEquals(30, result);
}
}
Evitare ottimizzazioni premature
Prima di rendere il codice più complesso per motivi di prestazioni bisogna misurare il comportamento reale dell’applicazione e individuare il vero collo di bottiglia.
🗺️ Cosa studiare dopo le basi
Dopo aver acquisito familiarità con gli argomenti di questa guida, è possibile proseguire con:
- testing con JUnit e Mockito;
- gestione dei file e API NIO;
- date e orari con
java.time; - concorrenza ed ExecutorService;
- virtual thread;
- moduli Java;
- accesso ai database con JDBC;
- persistenza con Jakarta Persistence e Hibernate;
- sviluppo web con Spring Boot;
- API REST;
- sicurezza delle applicazioni;
- container e deployment nel cloud;
- profilazione e monitoraggio della JVM.
🏁 Conclusioni
Java è una piattaforma completa che comprende il linguaggio, la Java Virtual Machine, le librerie standard e numerosi strumenti per compilare, eseguire, documentare e distribuire applicazioni.
Il codice sorgente viene normalmente compilato in bytecode, che viene eseguito dalla JVM. Questa architettura permette di utilizzare lo stesso programma su sistemi operativi differenti senza ricompilarlo per ogni piattaforma.
Le basi del linguaggio comprendono variabili, tipi, operatori, condizioni, cicli e metodi. La programmazione a oggetti aggiunge classi, oggetti, incapsulamento, ereditarietà, interfacce e polimorfismo.
Le versioni moderne di Java offrono inoltre strumenti come record, classi sealed, espressioni lambda, Stream API, switch expression e un’ampia libreria per collezioni, concorrenza, rete, file e gestione dei dati.
Per imparare in modo efficace è consigliabile alternare teoria ed esercizi pratici. Ogni nuovo concetto dovrebbe essere applicato in piccoli programmi, per poi essere combinato gradualmente in progetti più strutturati.
- installare il JDK e utilizzare terminale e JShell;
- studiare variabili, tipi, condizioni e cicli;
- creare metodi e piccoli programmi;
- comprendere classi, oggetti e incapsulamento;
- studiare interfacce, ereditarietà e polimorfismo;
- utilizzare collezioni, generics ed eccezioni;
- imparare lambda e Stream API;
- scrivere test automatici;
- creare un progetto con Maven o Gradle;
- passare a database, API REST e framework applicativi.
📖 Documentazione ufficiale
-
Portale ufficiale per imparare Java
-
Documentazione del JDK 26
-
Java Platform API
-
Java Language Specification
-
Progetto OpenJDK
-
OpenJDK 25
-
OpenJDK 26
-
Tutorial Java ufficiali
“`