Casting dei tipi in Java spiegato (da principiante ad esperto): casting numerico, upcasting/downcasting, instanceof e errori comuni

目次

1. Cos’è il casting in Java? (Risposta rapida)

In Java, il casting significa trattare un valore o un oggetto come un tipo diverso.
Usi il casting quando vuoi convertire numeri (come double in int) o quando vuoi gestire un oggetto come un tipo di classe più specifico (come Animal in Dog).

Il casting è potente, ma può anche essere rischioso:

  • Il casting numerico può cambiare il valore reale (troncando i decimali o provocando overflow).
  • Il casting di riferimento può causare errori a runtime se il tipo reale dell’oggetto non corrisponde.

Questo articolo spiega il casting in Java in modo da aiutarti a scrivere codice sicuro ed evitare trappole comuni.

1.1 Cosa imparerai in questa guida

Il casting in Java diventa molto più semplice una volta che lo separi in due categorie:

  • Casting numerico (tipi primitivi) Casting tra tipi come int, long, double, ecc. La domanda chiave è se la conversione è sicura o perde informazioni.
  • Casting di riferimento (classi e interfacce) Casting di oggetti in una gerarchia di ereditarietà, come upcasting e downcasting. La domanda chiave è se il tipo reale a runtime dell’oggetto corrisponde al cast.

Se mescoli questi due argomenti troppo presto, il casting risulta confuso. Quindi li impareremo passo dopo passo.

1.2 Situazioni comuni in cui si usa il casting

Esempio 1: Convertire un numero decimale in un intero

double price = 19.99;
int yen = (int) price; // 19 (decimal part is removed)

Questa è una conversione di restringimento, quindi Java richiede un casting esplicito.

Esempio 2: Trattare un riferimento di tipo genitore come tipo figlio

Animal a = new Dog();   // upcasting
Dog d = (Dog) a;        // downcasting

Questo funziona solo se l’oggetto reale è davvero un Dog. Altrimenti, il programma si bloccherà a runtime.

1.3 I due principali rischi del casting

Il casting diventa pericoloso per due motivi principali:

1) Il casting numerico può cambiare il valore

  • I decimali vengono troncati (non arrotondati)
  • I valori possono andare in overflow quando il tipo di destinazione è troppo piccolo

2) Il casting di riferimento può far crashare il tuo programma

  • I downcast errati causano ClassCastException

Se ricordi questi due rischi, eviterai la maggior parte dei bug di casting.

1.4 Termini che la gente confonde con il casting

Prima di continuare, ecco alcuni termini che sembrano simili ma hanno significati diversi:

  • Conversione implicita Java converte automaticamente i tipi in situazioni sicure (di solito conversioni di ampliamento).
  • Casting esplicito Scrivi manualmente (type) quando potrebbe andare persa informazione.
  • Boxing / unboxing Conversione automatica tra tipi primitivi (int) e classi wrapper (Integer). Non è lo stesso del casting, ma spesso causa bug.

2. Il casting in Java ha due tipi: Numerico vs Riferimento

Per comprendere correttamente il casting in Java, devi suddividerlo in:

  • Casting numerico (tipi primitivi)
  • Casting di riferimento (oggetti)

Questi due seguono regole diverse e causano diversi tipi di problemi.

2.1 Casting numerico (tipi primitivi)

Il casting numerico converte un tipo numerico primitivo in un altro, ad esempio:

  • Interi: byte, short, int, long
  • Virgola mobile: float, double
  • Caratteri: char (numericamente interno)

Esempio: Casting numerico

double d = 10.5;
int i = (int) d; // 10

Questa è una conversione di restringimento, quindi è richiesto un casting esplicito.

Conversioni di ampliamento vs restringimento (Importante)

Le conversioni numeriche sono raggruppate in:

  • Conversione di ampliamento (intervallo più piccolo → più grande) Esempio: int → long, int → double Di solito sicura, quindi Java permette la conversione implicita.
  • Conversione di restringimento (intervallo più grande → più piccolo) Esempio: double → int, long → short Rischiosa, quindi Java richiede un casting esplicito.

2.2 Casting di riferimento (classi e interfacce)

Il casting di riferimento cambia il modo in cui un oggetto è trattato all’interno di una gerarchia di ereditarietà.
Non riguarda la modifica dell’oggetto stesso, ma la modifica del tipo del riferimento.

Example hierarchy:

  • Animal (parent)
  • Dog (child)
    Animal a = new Dog(); // upcasting
    Dog d = (Dog) a;      // downcasting
    

Il casting di riferimento è strettamente collegato al polimorfismo nella programmazione orientata agli oggetti.

2.3 “Pericolo” Significa Cose Diverse nel Casting Numerico vs Casting di Riferimento

Questo è il modello mentale più importante:

Rischio del casting numerico

  • Il codice viene eseguito, ma il valore può cambiare in modo inatteso.

Rischio del casting di riferimento

  • Il codice compila, ma il programma può andare in crash a runtime.

2.4 Punto Chiave (Memorizzalo)

Se ti trovi mai bloccato, torna a questo:

  • Casting numerico → “Puoi convertire, ma il valore potrebbe cambiare.”
  • Casting di riferimento → “Puoi castare, ma un cast errato può causare un crash.”

3. Casting Numerico in Java: Casting Implicito vs Esplicito

Il casting numerico in Java diventa facile una volta che comprendi una semplice regola:

  • Le conversioni di allargamento sono solitamente automatiche (implicite).
  • Le conversioni di restringimento richiedono un cast manuale (esplicito) perché i dati possono andare persi.

Questa sezione spiega la differenza con esempi pratici e errori comuni.

3.1 Esempi di Casting Implicito (Conversione di Allargamento)

Java consente la conversione implicita quando il tipo di destinazione può rappresentare in modo sicuro l’intervallo di valori originale.

Esempio: intdouble

int i = 10;
double d = i;

System.out.println(d); // 10.0

Questo è sicuro perché double può rappresentare un intervallo molto più ampio rispetto a int.

Esempio: byteint

byte b = 100;
int i = b;

System.out.println(i); // 100

Poiché int ha un intervallo più ampio rispetto a byte, Java converte automaticamente.

3.2 Casting Esplicito (Conversione di Restringimento) e Comportamento di Troncamento

Quando si converte a un tipo più piccolo o più limitato, Java richiede un cast esplicito.

Esempio: doubleint

double d = 10.9;
int i = (int) d;

System.out.println(i); // 10

Dettaglio importante:

  • Il cast a int non arrotonda .
  • Tronca la parte decimale.

Quindi:

  • 10.9 diventa 10
  • 10.1 diventa 10
  • -10.9 diventa -10 (si avvicina a zero)

Esempio: longint

long l = 100L;
int i = (int) l;

System.out.println(i); // 100

Questo sembra sicuro, ma diventa pericoloso quando il valore long supera l’intervallo di int.

3.3 Overflow e Underflow: Il Pericolo Nascosto

Le conversioni di restringimento sono rischiose non solo perché i decimali vengono rimossi, ma anche perché i valori possono overfloware.

Esempio: Cast di un long grande in int

long l = 3_000_000_000L; // 3 billion (too large for int)
int i = (int) l;

System.out.println(i); // unexpected result

Questo è uno dei peggiori tipi di bug perché:

  • Il codice compila
  • Il programma viene eseguito
  • Ma il valore diventa errato silenziosamente

3.4 Errore Comune del Compilatore: “Possible Lossy Conversion”

Se provi una conversione di restringimento senza un cast esplicito, Java ti blocca con un errore del compilatore.

Esempio: double a int senza cast

double d = 1.5;
int i = d; // compile-time error

Il messaggio di solito include qualcosa come:

  • possible lossy conversion

Significa:

  • “Questa conversione può perdere informazioni.”
  • “Devi scrivere manualmente il cast se lo desideri davvero.”

Esempio: long a int senza cast

long l = 100L;
int i = l; // compile-time error

Anche se 100 è sicuro, Java lo blocca perché long potrebbe contenere valori molto più grandi.

3.5 Comportamento Inaspettato del Casting nelle Espressioni

Un errore comune dei principianti è supporre che Java produca automaticamente decimali nella divisione.

Esempio: La divisione intera tronca i risultati

int a = 5;
int b = 2;

System.out.println(a / b); // 2

Poiché entrambi gli operandi sono int, anche il risultato è int.

Correzione: Cast di un operando a double

int a = 5;
int b = 2;

System.out.println((double) a / b); // 2.5

Questo forza la divisione in virgola mobile.

3.6 Regola Pratica (Per Progetti Reali)

Per evitare bug di casting numerico, segui queste regole:

  • Le conversioni di allargamento sono sufficientemente sicure per la conversione implicita.
  • Le conversioni di restringimento devono essere esplicite, e devi accettare che i valori possano cambiare.
  • Fai sempre attenzione quando converti: wp:list /wp:list

    • virgola mobile → intero (troncamento)
    • tipo grande → tipo più piccolo (overflow)

4. Casting di Riferimento: Upcasting vs Downcasting

Il casting di riferimento riguarda oggetti, non valori numerici.
Invece di convertire un numero, stai cambiando il modo in cui Java tratta un riferimento a un oggetto all’interno di una gerarchia di ereditarietà.

La regola più importante è:

Nel casting di riferimento, ciò che conta è il tipo reale a runtime dell’oggetto, non il tipo della variabile.

4.1 Upcasting (Figlio → Genitore) è Sicuro e Solitamente Implicito

L’upcasting significa trattare un oggetto di classe figlia come se fosse del tipo del suo genitore.
Questo è estremamente comune in Java ed è generalmente sicuro.

Esempio: Upcasting di un Dog in un Animal

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

class Dog extends Animal {
    void bark() {
        System.out.println("bark");
    }
}

public class Main {
    public static void main(String[] args) {
        Dog dog = new Dog();

        Animal a = dog; // upcasting (implicit)
        a.eat();        // OK
    }
}

Questo funziona perché ogni Dog è un Animal.

4.2 Cosa Perdi Quando Fai Upcasting

L’upcasting è sicuro, ma cambia i metodi visibili attraverso il tipo di riferimento.

Animal a = new Dog();
a.bark(); // compile-time error

Anche se l’oggetto reale è un Dog, il tipo della variabile è Animal, quindi Java consente solo i metodi definiti in Animal.

4.3 Downcasting (Genitore → Figlio) è Rischioso e Richiede un Casting Esplicito

Il downcasting significa convertire un riferimento di tipo genitore nuovamente in un tipo figlio.

Animal a = new Dog();
Dog d = (Dog) a; // downcasting (explicit)
d.bark();        // OK

Questo funziona solo se l’oggetto reale è effettivamente un Dog.

4.4 Perché il Downcasting è Pericoloso: ClassCastException

Se l’oggetto reale non è del tipo target, il programma va in crash a runtime.

Animal a = new Animal();
Dog d = (Dog) a; // ClassCastException at runtime

Questo compila perché Java vede una possibile relazione di ereditarietà, ma a runtime l’oggetto non può diventare un Dog.

4.5 Tipo Variabile vs Tipo a Runtime (Il Concetto Chiave)

Animal a = new Dog();

In questo caso:

  • Tipo variabile (tempo di compilazione): Animal
  • Tipo a runtime (oggetto reale): Dog

Java lo consente grazie al polimorfismo, ma il downcasting deve corrispondere al tipo a runtime.

5. Usa instanceof Prima del Downcasting (Il Pattern Sicuro)

Per prevenire ClassCastException, dovresti prima verificare il tipo a runtime.

5.1 Cosa Fa instanceof

instanceof verifica se un oggetto è un’istanza di un determinato tipo.

if (obj instanceof Dog) {
    // obj can be treated as a Dog safely
}

Questo verifica il tipo reale a runtime, non il tipo dichiarato della variabile.

5.2 Il Pattern Standard di Downcasting Sicuro

Animal a = new Dog();

if (a instanceof Dog) {
    Dog d = (Dog) a;
    d.bark();
}

Questo previene i crash assicurando che il cast avvenga solo quando è valido.

5.3 Cosa Succede Senza instanceof (Crash Comune)

Animal a = new Animal();
Dog d = (Dog) a; // runtime crash

Ecco perché instanceof è considerato lo strumento di sicurezza predefinito.

5.4 Pattern Matching per instanceof (Sintassi Moderna più Pulita)

Le versioni più recenti di Java supportano una forma più leggibile:

Stile tradizionale

if (a instanceof Dog) {
    Dog d = (Dog) a;
    d.bark();
}

Stile pattern matching

if (a instanceof Dog d) {
    d.bark();
}

Vantaggi:

  • Nessun cast manuale necessario
  • La variabile d esiste solo all’interno del blocco if
  • Codice più pulito e sicuro

5.5 Quando instanceof diventa un odore di design

Se inizi a scrivere codice del genere ovunque:

if (a instanceof Dog) {
    ...
} else if (a instanceof Cat) {
    ...
} else if (a instanceof Bird) {
    ...
}

Potrebbe significare che nel tuo design manca il polimorfismo o una struttura di interfaccia migliore.

Nella parte successiva, discuteremo come ridurre i cast migliorando il design.

6. Come ridurre i cast in un design Java reale

Il cast è a volte necessario, ma nei codebase professionali, l’uso frequente di cast spesso segnala un problema di design.

Se vedi molto di questo:

  • controlli instanceof
  • downcast ripetuti
  • lunghe catene if/else basate sul tipo

…di solito significa che il codice sta lottando contro la programmazione orientata agli oggetti invece di usarla.

6.1 Il classico pattern “esplosione di cast”

Ecco un anti‑pattern comune:

void process(Animal a) {
    if (a instanceof Dog) {
        Dog d = (Dog) a;
        d.bark();
    } else if (a instanceof Cat) {
        Cat c = (Cat) a;
        c.meow();
    }
}

Questo funziona, ma crea problemi a lungo termine:

  • L’aggiunta di un nuovo sottotipo ti costringe a modificare process()
  • Il metodo deve conoscere ogni sottotipo
  • Il codice diventa più difficile da mantenere ed estendere

6.2 Usa il polimorfismo invece del cast

La soluzione pulita è spostare il comportamento nella gerarchia delle classi.

class Animal {
    void sound() {
    }
}

class Dog extends Animal {
    @Override
    void sound() {
        System.out.println("bark");
    }
}

class Cat extends Animal {
    @Override
    void sound() {
        System.out.println("meow");
    }
}

Ora la tua logica diventa semplice e priva di cast:

void process(Animal a) {
    a.sound(); // no casting needed
}

Questa è l’idea centrale del polimorfismo:

  • La chiamata al metodo rimane la stessa
  • Il tipo a runtime decide il comportamento

6.3 Definisci il comportamento richiesto in un tipo genitore o in un’interfaccia

Molti cast avvengono per una sola ragione:

“Ho bisogno di un metodo specifico per la sottoclasse, ma il tipo genitore non lo definisce.”

Se il comportamento è davvero necessario, definiscilo al livello più alto.

Esempio: design basato su interfacce

interface Speaker {
    void speak();
}

class Dog implements Speaker {
    public void speak() {
        System.out.println("bark");
    }
}

class Cat implements Speaker {
    public void speak() {
        System.out.println("meow");
    }
}

Ora puoi scrivere:

void process(Speaker s) {
    s.speak(); // no downcast needed
}

6.4 Quando il downcast è effettivamente ragionevole

Il downcast non è sempre negativo. Può essere accettabile quando:

  • un framework restituisce tipi generici come Object
  • stai integrando API legacy
  • gestisci eventi o callback con tipi base condivisi

Anche in questi casi, mantienilo sicuro:

  • verifica con instanceof
  • tieni il cast in un’area piccola e controllata
  • evita di spargere cast in tutto il codice

7. Boxing/Unboxing vs Cast (confusione comune)

Java ha due mondi di tipi numerici:

  • primitivi : int, double, ecc.
  • classi wrapper : Integer, Double, ecc.

La conversione automatica tra di essi è chiamata:

  • boxing : primitivo → wrapper
  • unboxing : wrapper → primitivo

Questo non è lo stesso di un cast, ma può causare bug a runtime.

7.1 Esempio di autoboxing (intInteger)

int x = 10;
Integer y = x; // autoboxing
System.out.println(y); // 10

7.2 Esempio di auto‑unboxing (Integerint)

Integer x = 10;
int y = x; // auto-unboxing
System.out.println(y); // 10

7.3 Il caso pericoloso: null + Unboxing = Crash

Integer x = null;
int y = x; // NullPointerException

Sembra una normale assegnazione, ma l’unboxing richiede un oggetto reale.

7.4 Trappola del confronto dei wrapper: non usare == per i valori

Integer a = 1000;
Integer b = 1000;

System.out.println(a == b); // may be false

Usa equals() per il confronto di valori:

System.out.println(a.equals(b)); // true

8. Generics e Cast: Comprendere gli avvisi unchecked

Nei progetti reali, vedrai spesso avvisi come:

  • unchecked cast
  • unchecked conversion

Questi avvisi significano:

“Il compilatore non può dimostrare che questo cast sia sicuro dal punto di vista del tipo.”

8.1 Causa comune: Tipi grezzi

import java.util.*;

public class Main {
    public static void main(String[] args) {
        List list = new ArrayList(); // raw type
        list.add("Hello");

        List<Integer> numbers = (List<Integer>) list; // unchecked warning
        Integer x = numbers.get(0); // may crash
        System.out.println(x);
    }
}

Questo è pericoloso perché la lista contiene effettivamente una String.

8.2 Correzione: Usa tipi generici corretti

List<String> list = new ArrayList<>();
list.add("Hello");

String s = list.get(0);
System.out.println(s);

Nessun cast necessario, e il compilatore ti protegge.

8.3 Se devi sopprimere gli avvisi, mantienili al minimo

A volte non è possibile evitare i cast (API legacy, librerie esterne).
In quei casi:

  • cast in un punto piccolo
  • documenta perché è sicuro
  • sopprimi gli avvisi solo nell’ambito più piccolo

Esempio:

@SuppressWarnings("unchecked")
List<String> list = (List<String>) obj;

9. Errori comuni di cast (Esempi brevi da copiare‑incollare)

Il cast è facile da “capire in teoria” ma è comunque facile sbagliare nel codice reale.
Questa sezione mostra gli errori più comuni con brevi esempi che puoi riprodurre rapidamente.

Il modo migliore per imparare il cast è:

  • vedere il fallimento
  • capire perché fallisce
  • apprendere la correzione sicura

9.1 Cast numerico: aspettarsi l’arrotondamento invece della troncatura

Errore: Pensare che (int) arrotondi il valore

double x = 9.9;
int y = (int) x;

System.out.println(y); // 9

Java non arrotonda qui. Trunca la parte decimale.

9.2 Cast numerico: sorpresa della divisione intera

Errore: Aspettarsi 2.5 ma ottenere 2

int a = 5;
int b = 2;

System.out.println(a / b); // 2

Poiché entrambi gli operandi sono int, il risultato è anch’esso int.

Correzione: Casta un operando a double

int a = 5;
int b = 2;

System.out.println((double) a / b); // 2.5

9.3 Cast numerico: overflow quando si castano numeri grandi

Errore: Castare un grande long in int

long l = 3_000_000_000L;
int i = (int) l;

System.out.println(i); // unexpected value

Questo compila ed esegue, ma il valore diventa errato a causa dell’overflow.

9.4 Cast di riferimento: crash del downcasting (ClassCastException)

Errore: Castare un oggetto al sottotipo sbagliato

class Animal {}
class Dog extends Animal {}

public class Main {
    public static void main(String[] args) {
        Animal a = new Animal();
        Dog d = (Dog) a; // ClassCastException
    }
}

Correzione: Usa instanceof

Animal a = new Animal();

if (a instanceof Dog) {
    Dog d = (Dog) a;
    // safe usage
} else {
    System.out.println("Not a Dog, so no cast.");
}

9.5 Cast di riferimento: perdita dei metodi della sottoclasse dopo l’upcasting

Errore: “È un Cane, perché non posso chiamare bark()?”

class Animal {}
class Dog extends Animal {
    void bark() {
        System.out.println("bark");
    }
}

public class Main {
    public static void main(String[] args) {
        Animal a = new Dog();
        // a.bark(); // compile-time error
    }
}

Il tipo della variabile è Animal, quindi Java consente solo i metodi dichiarati in Animal.

9.6 Generics: ignorare il unchecked cast e rompere a runtime

Errore: tipo grezzo + cast non sicuro

import java.util.*;

public class Main {
    public static void main(String[] args) {
        List list = new ArrayList(); // raw type
        list.add("Hello");

        List<Integer> numbers = (List<Integer>) list; // unchecked warning
        Integer x = numbers.get(0); // may crash
        System.out.println(x);
    }
}

Correzione: Usa correttamente i generics

List<String> list = new ArrayList<>();
list.add("Hello");

String s = list.get(0);
System.out.println(s);

9.7 Trappola del confronto dei wrapper: usare == invece di equals()

Errore: confrontare i valori wrapper con ==

Integer a = 1000;
Integer b = 1000;

System.out.println(a == b); // may be false

Correzione: usare equals()

System.out.println(a.equals(b)); // true

9.8 Crash di unboxing nullo (NullPointerException)

Errore: unboxing di null

Integer x = null;
int y = x; // NullPointerException

Correzione: controllare il valore null (o usare i tipi primitivi quando possibile)

Integer x = null;

if (x != null) {
    int y = x;
    System.out.println(y);
} else {
    System.out.println("x is null");
}

10. FAQ (Domande sul casting Java)

10.1 Cos’è il casting (conversione di tipo) in Java?

Il casting significa trattare un valore o un oggetto come di un tipo diverso.

Il casting in Java ha due categorie principali:

  • casting numerico (primitivi)
  • casting di riferimento (oggetti)

10.2 Qual è la differenza tra conversione implicita e casting esplicito?

  • Conversione implicita avviene automaticamente in situazioni sicure (principalmente conversioni di ampliamento).
  • Casting esplicito richiede (tipo) ed è necessario quando i dati possono andare persi (conversioni di restringimento).

10.3 (int) 3.9 arrotonda il numero?

No. Trunca.

System.out.println((int) 3.9); // 3

10.4 Perché il casting da double a int è rischioso?

Perché rimuove le cifre decimali (perdita di dati).
Inoltre, valori grandi possono andare in overflow quando vengono convertiti in tipi numerici più piccoli.

10.5 Qual è la differenza tra upcasting e downcasting?

  • Upcasting (figlio → genitore) è sicuro e di solito implicito.
  • Downcasting (genitore → figlio) è rischioso e richiede un casting esplicito.

Un downcasting errato può causare ClassCastException.

10.6 Cosa causa ClassCastException e come risolverlo?

Accade quando il tipo reale dell’oggetto a runtime non corrisponde al tipo di destinazione del cast.

Risolvi controllando con instanceof prima del cast.

10.7 Dovrei sempre usare instanceof prima del downcasting?

Se c’è anche solo una minima possibilità che il tipo a runtime non corrisponda, sì.
È l’approccio standard sicuro.

Java moderno supporta anche il pattern matching:

if (a instanceof Dog d) {
    d.bark();
}

10.8 È accettabile ignorare gli avvisi di unchecked cast?

Di solito no.

La maggior parte degli avvisi unchecked proviene da tipi grezzi o cast non sicuri.
Risolvi la causa principale usando correttamente i generics.

Se davvero non puoi evitarlo (API legacy), isola il cast e sopprimi gli avvisi nel più piccolo ambito possibile.

10.9 Come posso progettare il codice per evitare troppi cast?

Usa caratteristiche di progettazione orientata agli oggetti come:

  • polimorfismo (sovrascrivere il comportamento nelle sottoclassi)
  • interfacce (definire il comportamento richiesto in un tipo comune)

Se scrivi costantemente catene di instanceof, potrebbe essere un segno che il design necessita di miglioramenti.