“`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.

💡 In questa guida imparerai:

  • 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.

⚠️ Java e JavaScript non sono lo stesso linguaggio

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.

✅ Versione consigliata per imparare

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;
⚠️ Valori monetari

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.

⚠️ Preferire la composizione quando appropriato

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 No
Costruttore No
Metodi implementati 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
protected Sì, secondo le regole dell’ereditarietà No
Nessun modificatore No, se esterna al package No
private 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 non è una collezione

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, valid o hasPermission.

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.

🚀 Percorso di apprendimento consigliato

  1. installare il JDK e utilizzare terminale e JShell;
  2. studiare variabili, tipi, condizioni e cicli;
  3. creare metodi e piccoli programmi;
  4. comprendere classi, oggetti e incapsulamento;
  5. studiare interfacce, ereditarietà e polimorfismo;
  6. utilizzare collezioni, generics ed eccezioni;
  7. imparare lambda e Stream API;
  8. scrivere test automatici;
  9. creare un progetto con Maven o Gradle;
  10. passare a database, API REST e framework applicativi.

📖 Documentazione ufficiale

“`

Categorie: CoreJava