6. Istanziazione dei tipi
Indice
- 6.1 Introduzione
- 6.2 Inizializzazione predefinita delle variabili
- 6.3 Tipi wrapper
- 6.4 Uguaglianza in Java
6.1 Introduzione
In Java, un tipo può essere un tipo primitivo (come int, double, boolean, ecc.) oppure un tipo reference (classi, interfacce, array, enum, record, ecc.). Vedi: Tipi di dato Java e casting
Il modo in cui vengono create le istanze dipende dalla categoria del tipo:
- Tipi primitivi
Le istanze dei tipi primitivi vengono create semplicemente dichiarando una variabile.
La JVM alloca automaticamente la memoria per contenere il valore e non è necessaria alcuna keyword esplicita.
int age = 30; // crea un primitivo int con valore 30
boolean flag = true; // crea un primitivo boolean con valore true
double pi = 3.14159; // crea un primitivo double con valore 3.14159
- Tipi reference (oggetti)
Le istanze dei tipi classe vengono create usando la keywordnew(eccetto alcuni casi speciali come i letterali String, i record con costruttori canonici, o i metodi factory). La keywordnewalloca memoria nell’heap e invoca un costruttore della classe.
String name = new String("Alice"); // crea esplicitamente un nuovo oggetto String
Person p = new Person(); // crea un nuovo oggetto Person usando il suo costruttore
È anche comune affidarsi a letterali o metodi factory per la creazione degli oggetti.
String text = "Hello World";
List<String> list = List.of("A", "B", "C"); // factory method immutabile
Map<String, Integer> map = Map.of("one", 1, "two", 2); // factory method immutabile
Optional<String> opt = Optional.of("value"); // factory method
LocalDate date = LocalDate.of(2025, 3, 15);
Integer boxed = Integer.valueOf(10);
Important
I letterali String non richiedono new e sono memorizzati nello String pool.
Usare new String("x") crea invece sempre un nuovo oggetto nell’heap.
6.1.1 Gestione dei tipi primitivi
6.1.1.1 Dichiarare un primitivo
Dichiarare un tipo primitivo (come per i tipi reference) significa riservare spazio in memoria per una variabile di un determinato tipo, senza necessariamente assegnarle un valore.
Warning
A differenza dei primitivi, la cui dimensione dipende dal tipo specifico (es. int vs long), i tipi reference occupano sempre la stessa dimensione fissa in memoria — ciò che varia è la dimensione dell’oggetto a cui puntano.
- Esempi di sintassi (solo dichiarazione):
int number;
boolean active;
char letter;
int x, y, z; // Dichiarazioni multiple in un’unica istruzione: Java consente di dichiarare più variabili dello stesso tipo
Important
I modificatori e il tipo dichiarati all’inizio di una dichiarazione multipla di variabili si applicano a tutte le variabili dichiarate in quella stessa istruzione.
Eccezione: quando si dichiarano array usando le parentesi quadre dopo il nome della variabile, le parentesi fanno parte del dichiaratore della singola variabile, non del tipo base.
- Esempi
static int a, b, c;
// è equivalente a:
static int a;
static int b;
static int c;
int[] a, b; // entrambi sono arrays di int
int c[], d; // solo c è un array, d è un normale int
6.1.1.2 Assegnare un primitivo
Assegnare un tipo primitivo (come per i tipi reference) significa memorizzare un valore in una variabile dichiarata di quel tipo.
Per i primitivi, la variabile contiene il valore stesso; per i tipi reference, la variabile contiene l’indirizzo di memoria (un reference) dell’oggetto puntato.
- Esempi di sintassi:
int number; // Dichiarazione di un int: variabile chiamata "number"
number = 10; // Assegnazione del valore 10 a questa variabile
char letter = 'A'; // Dichiarazione e assegnazione in un’unica istruzione: dichiarazione e assegnazione possono essere combinate
int a1, a2; // Dichiarazioni multiple
int a = 1, b = 2, c = 3; // Dichiarazioni multiple e assegnazioni
char b1, b2, b3 = 'C'; // Dichiarazioni miste (2 dichiarazioni + 1 assegnazione)
double d1, double d2; // ERROR - NOT LEGAL
int v1; v2; // ERROR - NOT LEGAL
Important
Quando scrivi un numero direttamente nel codice (un letterale numerico), Java assume per default che sia di tipo int.
Se il valore non entra in un int, il codice non compila a meno che il letterale non sia marcato esplicitamente con il suffisso corretto.
- Esempio di sintassi per un letterale numerico:
long exNumLit = 5729685479; // ❌ Does not compile.
// Anche se il valore potrebbe rientrare in un long,
// un literal numerico semplice è considerato un int,
// e questo numero è troppo grande per un int.
// Changing the declaration adding the correct suffix (L or l) will solve:
long exNumLit = 5729685479L;
or
long exNumLit = 5729685479l;
Dichiarare un tipo reference significa riservare spazio in memoria per una variabile che conterrà un reference (puntatore) a un oggetto del tipo specificato.
A questo stadio non viene ancora creato alcun oggetto — la variabile ha solo la potenzialità di puntarne uno.
Warning
A differenza dei primitivi, la cui dimensione dipende dal tipo specifico (es. int vs long), le variabili reference occupano sempre la stessa dimensione fissa in memoria (sufficiente per memorizzare un reference).
Ciò che varia è la dimensione dell’oggetto puntato, che viene allocato separatamente nell’heap.
- Esempi di sintassi (solo dichiarazione):
String name;
Person person;
List<Integer> numbers;
Person p1, p2, p3; // Dichiarazioni multiple in un’unica istruzione
String a = "abc", b = "def", c = "ghi"; // Dichiarazioni multiple e assegnazioni
String b1, b2, b3 = "abc" // Dichiarazioni miste (b1, b2) con una assegnazione (b3)
String d1, String d2; // ERROR - NOT LEGAL
String v1; v2; // ERROR - NOT LEGAL
6.1.2 Gestione dei tipi reference
6.1.2.1 Creare e assegnare un reference
Assegnare un tipo reference significa memorizzare nella variabile l’indirizzo di memoria di un oggetto.
Questo si fa normalmente dopo la creazione dell’oggetto con la keyword new e un costruttore, oppure usando un letterale o un metodo factory.
Un reference può anche essere assegnato a un altro oggetto dello stesso tipo o di tipo compatibile.
I tipi reference possono anche essere assegnati a null, il che significa che non faranno riferimento ad alcun oggetto.
- Esempi di sintassi:
Person person = new Person(); // Esempio con 'new' e un costruttore 'Person()':
// 'new Person()' crea un nuovo oggetto Person nell’heap
// e restituisce il suo reference, che viene memorizzato nella variabile 'person'.
String greeting = "Hello"; // Esempio con letterale (per String).
List<Integer> numbers = List.of(1, 2, 3); // Esempio con un metodo factory.
6.1.2.2 Costruttori
Nell’esempio, Person() è un costruttore — un tipo speciale di metodo usato per inizializzare nuovi oggetti.
Ogni volta che chiami new Person(), il costruttore viene eseguito e imposta la nuova istanza creata.
I costruttori hanno tre caratteristiche principali:
- Il nome del costruttore deve corrispondere esattamente al nome della classe (case-sensitive).
- I costruttori non dichiarano un tipo di ritorno (nemmeno
void). - Se non definisci alcun costruttore nella tua classe, il compilatore fornisce automaticamente un costruttore di default senza argomenti che non fa nulla.
Warning
Se vedi un metodo che ha lo stesso nome della classe ma dichiara anche un tipo di ritorno, non è un costruttore. È semplicemente un metodo normale (anche se iniziare i nomi dei metodi con una lettera maiuscola va contro le convenzioni di naming in Java).
Lo scopo di un costruttore è inizializzare lo stato di un oggetto appena creato — tipicamente assegnando valori ai suoi campi, con valori di default oppure usando parametri passati al costruttore.
- Esempio 1: Costruttore di default (senza parametri)
public class Person {
String name;
int age;
// Default constructor
public Person() {
name = "Unknown";
age = 0;
}
}
Person p1 = new Person(); // name = "Unknown", age = 0
- Esempio 2: Costruttore con parametri
public class Person {
String name;
int age;
// Constructor with parameters
public Person(String newName, int newAge) {
name = newName;
age = newAge;
}
}
Person p2 = new Person("Alice", 30); // name = "Alice", age = 30
- Esempio 3: Costruttori multipli (overloading dei costruttori)
public class Person {
String name;
int age;
// Default constructor
public Person() {
this("Unknown", 0); // calls the other constructor
}
// Constructor with parameters
public Person(String newName, int newAge) {
name = newName;
age = newAge;
}
}
Person p1 = new Person(); // name = "Unknown", age = 0
Person p2 = new Person("Bob", 25); // name = "Bob", age = 25
Important
- I costruttori non sono ereditati: se una superclasse definisce costruttori, non sono automaticamente disponibili nella sottoclasse — devi dichiararli esplicitamente.
- Se dichiari un qualsiasi costruttore in una classe, il compilatore non genera il costruttore di default senza argomenti: se ti serve ancora un costruttore senza argomenti, devi dichiararlo manualmente.
6.1.2.3 Blocchi di inizializzazione istanza
Oltre ai costruttori, Java offre un meccanismo chiamato initializer blocks per l'inizializzazione degli oggetti.
Sono blocchi di codice all’interno di una classe, racchiusi tra { }, che vengono eseguiti ogni volta che viene creata un’istanza, subito prima dell’esecuzione del corpo del costruttore.
Caratteristiche
- Chiamati anche instance initializer blocks.
- Eseguiti, insieme all'inizializzazione dei campi, nell’ordine in cui appaiono nella definizione della classe ma sempre prima dei costruttori.
- Utili quando più costruttori devono condividere un codice comune di inizializzazione.
Esempio: usare un Instance Initializer Block
public class Person {
String name;
int age;
// Instance initializer block
{
System.out.println("Instance initializer block executed");
age = 18; // default age for every Person
}
// Default constructor
public Person() {
name = "Unknown";
}
// Constructor with parameters
public Person(String newName) {
name = newName;
}
}
Person p1 = new Person(); // prints "Instance initializer block executed"
Person p2 = new Person("Alice"); // prints "Instance initializer block executed"
Note
In questo esempio, il blocco di inizializzazione viene eseguito prima del corpo di entrambi i costruttori. Sia p1 che p2 partiranno con age = 18, indipendentemente da quale costruttore viene usato.
Blocchi di inizializzazione multipli: se una classe contiene più initializer blocks, essi vengono eseguiti nell’ordine in cui compaiono nel file sorgente:
- Esempio:
public class Example {
{
System.out.println("First block");
}
{
System.out.println("Second block");
}
}
Example ex = new Example();
// Output:
// First block
// Second block
Note
I blocchi di inizializzazione d’istanza sono meno comuni nella pratica, perché una logica simile può spesso essere messa direttamente nei costruttori. È importante sapere che: - Vengono sempre eseguiti prima del corpo del costruttore. - Sono eseguiti nell’ordine di dichiarazione nella classe. - Possono essere combinati con i costruttori per evitare duplicazioni di codice.
Warning
Ordine di inizializzazione quando si crea un oggetto 1. Campi statici 2. Blocchi di inizializzazione statici 3. Campi di istanza 4. Blocchi di inizializzazione d’istanza 5. Corpo del costruttore
6.2 Inizializzazione predefinita delle variabili
6.2.1 Variabili di istanza e di classe
- Una variabile di istanza (un field) è un valore definito all’interno di un’istanza di un oggetto;
- Una variabile di classe (definita con la keyword static) è definita a livello di classe ed è condivisa tra tutti gli oggetti (istanze della classe)
Se non inizializzate, variabili di istanza e di classe ricevono un valore di default dal compilatore.
- Tabella dei valori di default per variabili di istanza e di classe:
| Type | Default Value |
|---|---|
| Object | null |
| Numeric | 0 |
| boolean | false |
| char | '\u0000' (NUL) |
6.2.2 Variabili final di istanza
A differenza delle normali variabili di istanza e di classe, le variabili final non vengono inizializzate di default dal compilatore.
Una variabile final deve essere assegnata esplicitamente esattamente una sola volta, altrimenti il codice non compila.
Questo vale sia per:
- variabili final di istanza
- variabili di classe static final
Note
Possiamo assegnare un valore null a una variabile final di istanza o di classe, purché venga impostato esplicitamente.
Java impone questa regola perché una variabile final rappresenta un valore che deve essere noto e fissato prima dell’uso.
Final Instance Variables
Una variabile final di istanza deve essere assegnata esattamente una sola volta, e l’assegnazione deve avvenire in una delle seguenti modalità:
- Nel punto di dichiarazione
- In un blocco di inizializzazione d’istanza
- All’interno di ogni costruttore
Se la classe ha più costruttori, la variabile deve essere assegnata in tutti.
- Esempio:
public class Person { final int id; // deve essere assegnata prima che il costruttore termini String name; // Costruttore 1 public Person(int id, String name) { this.id = id; // ok this.name = name; } // Costruttore 2 public Person() { this.id = 0; // richiesto anche qui this.name = "Unknown"; } }
Warning
Provare a compilare senza assegnare id dentro ogni costruttore produce un errore a compile-time:
variable id might not have been initialized
Variabili di classe static final (Costanti)
Una variabile static final appartiene alla classe, non a una specifica istanza.
Deve anch’essa essere assegnata esattamente una sola volta, ma l’assegnazione può avvenire in uno dei seguenti punti:
- Nel punto di dichiarazione
-
Dentro un blocco di inizializzazione statico
-
Esempio:
public class AppConfig { static final int TIMEOUT = 5000; // assegnata nella dichiarazione static final String VERSION; // assegnata nel blocco static static { VERSION = "1.0.0"; // ok } }
Tentare di assegnare una static final in un costruttore non è consentito.
Regole chiave per i campi final
| Scenario | Allowed? | Notes |
|---|---|---|
| Assign at declaration | ✔ | Most common pattern |
| Assign in constructor | ✔ | All constructors must assign it |
| Assign in instance initializer | ✔ | Before constructor body runs |
| Assign in static initializer (static final only) | ✔ | For class-level constants |
| Assign multiple times | ❌ | Compilation error |
| Default initialization | ❌ | Must be explicitly assigned |
Esempio di situazione illegale:
public class Example {
final int x; // non inizializzata
}
Example e = new Example(); // ❌ compile-time error
Perché le variabili final non vengono inizializzate di default?
Perché:
- Il loro valore deve essere noto e immutabile, e
- Java deve garantire che il valore sia impostato prima dell’uso,
- L’inizializzazione di default creerebbe una situazione in cui
0,null, ofalsepotrebbero diventare involontariamente il valore permanente.
Per questo Java costringe gli sviluppatori a inizializzare esplicitamente i campi final.
Tip
final significa assegnato una volta, non oggetto immutabile.
Un reference final può comunque puntare a un oggetto mutabile.
final List<String> list = new ArrayList<>();
list.add("ok"); // consentito
list = new ArrayList<>(); // ❌ non puoi riassegnare il reference
6.2.3 Variabili locali
Le variabili locali sono variabili definite all’interno di un costruttore, di un metodo o di un blocco di inizializzazione;
Le variabili locali non hanno valori di default e devono essere inizializzate prima di poter essere usate. Se provi a usare una variabile locale non inizializzata, il compilatore segnalerà un ERRORE.
- Esempio
public int localMethod {
int firstVar = 25;
int secondVar;
secondVar = 35;
int firstSum = firstVar + secondVar; // OK: entrambe le variabili sono inizializzate prima dell’uso
int thirdVar;
int secondSum = firstSum + thirdVar; // ERROR: la variabile thirdVar non è stata inizializzata prima dell’uso; se non provi a usare thirdVar il compilatore non segnalerà nulla
}
6.2.3.1 Inferire i tipi con var
In certe condizioni puoi usare la keyword var al posto del tipo appropriato quando dichiari variabili locali;
Warning
- var NON è una parola riservata in Java;
- var può essere usata solo per variabili locali: NON può essere usata per parametri del costruttore, variabili di istanza o parametri dei metodi;
- Il compilatore inferisce il tipo guardando SOLO il codice sulla riga della dichiarazione; una volta inferito il tipo, non puoi riassegnare a un altro tipo.
- Esempio
public int localMethod {
var inferredInt = 10; // Il compilatore inferisce int dal contesto
inferredInt = 25; // OK
inferredInt = "abcd"; // ERROR: il compilatore ha già inferito il tipo della variabile come int
var notInferred;
notInferred = 30; // ERROR: per inferire il tipo, il compilatore guarda SOLO la riga con la dichiarazione
var first, second = 15; // ERROR: var non può essere usata per definire due variabili nella stessa istruzione
var x = null; // ERROR: var non può essere inizializzata con null ma può essere riassegnata a null purché il tipo sottostante sia un tipo reference.
}
Warning
Le variabili locali non ricevono mai valori di default. Le variabili di istanza e di classe (static) sì, sempre.
6.3 Tipi wrapper
In Java, i tipi wrapper sono rappresentazioni a oggetti degli otto tipi primitivi.
Ogni primitivo ha una corrispondente classe wrapper nel package java.lang:
| Primitive | Wrapper Class |
|---|---|
byte |
Byte |
short |
Short |
int |
Integer |
long |
Long |
float |
Float |
double |
Double |
char |
Character |
boolean |
Boolean |
Gli oggetti wrapper sono immutabili — una volta creati, il loro valore non può cambiare.
6.3.1 Scopo dei tipi wrapper
- Consentono di usare i primitivi in contesti che richiedono oggetti (es. collezioni, generics).
- Forniscono metodi di utilità per parsing, conversione e manipolazione dei valori.
- Supportano costanti come
Integer.MAX_VALUEoDouble.MIN_VALUE.
6.3.2 Autoboxing e unboxing
Da Java 5, il compilatore converte automaticamente tra primitivi e wrapper:
- Autoboxing: primitivo → wrapper
- Unboxing: wrapper → primitivo
Integer i = 10; // autoboxing: int → Integer
int n = i; // unboxing: Integer → int
Integer int1 = Integer.valueOf(11);
long long1 = int1; // Unboxing --> implicit cast OK
Long long2 = 11; // ❌ Does not compile.
// 11 is an int literal → requires autoboxing + widening → illegal
Character char1 = null;
char char2 = char1; // WARNING: NullPointerException
Integer arr1 = {11.5, 13.6} // WARNING: Does not compile!!
Double[] arr2 = {11, 22}; // WARNING: Does not compile!!
Tip
Java non esegue mai autoboxing + widening/narrowing in un solo passo.
Warning
- AUTOBOXING e cast implicito non sono consentiti nella stessa istruzione: non puoi fare entrambe le cose contemporaneamente. (vedi esempio sopra)
- Questa regola vale anche nelle chiamate ai metodi.
6.3.3 Parsing e conversione
I wrapper forniscono metodi statici per convertire stringhe o altri tipi in primitivi:
int x = Integer.parseInt("123"); // returns primitive int
Integer y = Integer.valueOf("456"); // returns Integer object
double d = Double.parseDouble("3.14");
// On the numeric wrapper class valueOf() throws a NumberFormatException on invalid input.
// Example:
Integer w = Integer.valueOf("two"); // NumberFormatException
// On Boolean, the method returns Boolean.TRUE for any value that matches "true" ignoring case, otherwise Boolean.false
// Example:
Boolean.valueOf("true"); // true
Boolean.valueOf("TrUe"); // true
Boolean.valueOf("TRUE"); // true
Boolean.valueOf("false"); // false
Boolean.valueOf("FALSE"); // false
Boolean.valueOf("xyz"); // false
Boolean.valueOf(null); // false
// The numeric integral classes Byte, Short, Integer and Long include an overloaded **valueOf(String str, int base)** method which takes a base value
// Example with base 16 (hexadecimal) which includes character 0 -> 9 and A -> F (ignore case)
Integer.valueOf("6", 16); // 6
Integer.valueOf("a", 16); // 10
Integer.valueOf("A", 16); // 10
Integer.valueOf("F", 16); // 15
Integer.valueOf("G", 16); // NumberFormatException
Note
I metodi parseXxx() restituiscono un primitivo mentre valueOf() restituisce un oggetto wrapper.
6.3.4 Metodi di supporto
Tutte le classi wrapper numeriche estendono la classe Number e, per questo, ereditano alcuni metodi di supporto come: byteValue(), shortValue(), intValue(), longValue(), floatValue(), doubleValue().
Le classi wrapper Boolean e Character includono: booleanValue() e charValue().
- Esempio:
// In trying to convert those helper methods can result in a loss of precision.
Double baseDouble = Double.valueOf("300.56");
double wrapDouble = baseDouble.doubleValue();
System.out.println("baseDouble.doubleValue(): " + wrapDouble); // 300.56
byte wrapByte = baseDouble.byteValue();
System.out.println("baseDouble.byteValue(): " + wrapByte); // 44 -> There is no 300 in byte
int wrapInt = baseDouble.intValue();
System.out.println("baseDouble.intValue(): " + wrapInt); // 300 -> The value is truncated
6.3.5 Valori null
A differenza dei primitivi, i tipi wrapper possono contenere null.
Tentare di fare unboxing di null causa una NullPointerException:
Integer val = null;
int z = val; // ❌ NullPointerException at runtime
6.4 Uguaglianza in Java
Java fornisce due meccanismi distinti per verificare l’uguaglianza:
==(operatore di uguaglianza).equals()(metodo definito inObjecte ridefinito in molte classi)
Capirne la differenza è essenziale.
6.4.1 Uguaglianza con i tipi primitivi
Per i valori primitivi (int, double, char, boolean, ecc.),
l’operatore == confronta il loro reale valore numerico o booleano.
Esempio:
int a = 5;
int b = 5;
System.out.println(a == b); // true
char c1 = 'A';
char c2 = 65; // stesso code point Unicode
System.out.println(c1 == c2); // true
6.4.1.1 Punti chiave
==esegue un confronto di valori per i primitivi.- I tipi primitivi non hanno un metodo
.equals(). - Tipi primitivi misti seguono le regole di promozione numerica
(es.int == long→intpromosso along).
6.4.2 Uguaglianza con i tipi reference
Con gli oggetti (tipi reference), il significato di == cambia.
6.4.2.1 == (Confronto di identità)
== verifica se due riferimenti puntano allo stesso oggetto in memoria.
String s1 = new String("Hi");
String s2 = new String("Hi");
System.out.println(s1 == s2); // false → oggetti diversi
Anche se i contenuti sono identici, == è false a meno che entrambe le variabili non si riferiscano
allo stesso identico oggetto.
6.4.2.2 .equals() (Confronto logico)
Molte classi ridefiniscono .equals() per confrontare i valori, non gli indirizzi di memoria.
System.out.println(s1.equals(s2)); // true → stesso contenuto
6.4.2.3 Punti chiave
.equals()è definito inObject.- Se una classe non ridefinisce
.equals(), si comporta come==. - Classi come
String,Integer,List, ecc. ridefiniscono.equals()
per fornire un confronto di valori significativo.
6.4.3 String Pool e uguaglianza
I letterali String sono memorizzati nello String pool, quindi letterali identici che si riferiscono allo stesso oggetto.
String a = "Java";
String b = "Java";
System.out.println(a == b); // true → stesso literal nel pool
Ma usare new crea un oggetto diverso:
String x = new String("Java");
String y = "Java";
System.out.println(x == y); // false → x non è nello pool
System.out.println(x.equals(y)); // true
Errori comuni
String x = "Java string literal";
String y = " Java string literal".trim();
System.out.println(x == y); // false → x e y non sono lo stesso a compile-time
String a = "Java string literal";
String b = "Java ";
b += "string literal";
System.out.println(a == b); // false
Warning
Qualsiasi String creata a runtime non entra nel pool automaticamente.
Si usi intern() se si vuole il pooling.
Tip
"Hello" == "Hel" + "lo" → true (costante a compile-time)
"Hello" == getHello() → false (concatenazione a runtime)
String x = "Hello";
String y = "Hel" + "lo"; // compile-time → stesso literal
String z = "Hel";
z += "lo"; // runtime → nuova String
System.out.println(x == y); // true
System.out.println(x == z); // false
6.4.3.1 Il metodo intern
Puoi anche dire a Java di usare una String dallo String Pool (nel caso esista già) tramite il metodo intern():
String x = "Java";
String y = new String("Java").intern();
System.out.println(x == y); // true
6.4.4 Uguaglianza con i Tipi Wrapper
Le classi wrapper (Integer, Double, Boolean, ecc.) si comportano come oggetti normali.
Pertanto:
==→ confronta i riferimenti degli oggetti.equals()→ confronta i valori numerici
Esempio:
Integer a = 100;
Integer b = 100;
System.out.println(a == b); // true → cached
Integer c = 1000;
Integer d = 1000;
System.out.println(c == d); // false → oggetti diversi
System.out.println(c.equals(d)); // true → stesso valore numerico
Poiché, come precedentemente ricordato, tutte le classi wrapper sono immutabili, il loro valore interno non può essere modificato una volta create.
Operazioni che sembrano modificare un wrapper in realtà creano un nuovo oggetto.
Esempio:
Integer i = 5;
i++;
Questo è concettualmente equivalente a:
i = Integer.valueOf(i.intValue() + 1);
Quindi un nuovo oggetto Integer viene creato e assegnato a i.
6.4.4.1 Caching dei wrapper
Per ridurre l’uso della memoria e la creazione di oggetti, Java riutilizza alcune istanze di wrapper.
I seguenti valori sono memorizzati in cache:
- Tutti i valori
Boolean(trueefalse) - Tutti i valori
Byte - Tutti i valori
Characterda\u0000a\u007f(0–127) - Tutti i valori
Shortda −128 a 127 - Tutti i valori
Integerda −128 a 127
A causa di questo meccanismo di caching:
Integer a = 100;
Integer b = 100;
System.out.println(a == b); // true
Entrambe le variabili fanno riferimento allo stesso oggetto in cache.
Tuttavia, valori al di fuori dell’intervallo della cache producono oggetti distinti:
Integer c = 1000;
Integer d = 1000;
System.out.println(c == d); // false
System.out.println(c.equals(d)); // true
6.4.4.2 La keyword new aggira la cache
Quando un oggetto wrapper viene creato usando new, una nuova istanza viene sempre creata, anche se esiste un valore in cache.
Esempio:
Integer i = 10; // oggetto in cache
Integer j = 10; // stesso oggetto in cache
Integer k = new Integer(10); // nuovo oggetto (non in cache)
System.out.println(i == j); // true
System.out.println(i == k); // false
Tuttavia, i costruttori dei wrapper sono stati deprecati in Java 9 e marcati per la rimozione.
Il codice moderno dovrebbe usare autoboxing o metodi factory come Integer.valueOf().
6.4.4.3 Confronto dei wrapper
Quando due riferimenti wrapper vengono confrontati usando ==, il risultato dipende dal fatto che si riferiscano allo stesso oggetto, non dal fatto che i loro valori siano uguali.
Pertanto, i test di uguaglianza tra wrapper dovrebbero normalmente usare:
equals()
invece di ==.
6.4.4.4 Tipi wrapper diversi non possono essere confrontati
Oggetti wrapper di tipi diversi non possono essere confrontati usando ==.
Esempio:
Byte b = 1;
Integer i = 1;
b == i; // errore di compilazione
Gli operandi devono essere tipi compatibili, altrimenti il confronto non è valido.
Warning
Fare molta attenzione quando si confrontano oggetti wrapper con ==.
A causa del caching dei wrapper, i confronti possono a volte restituire true e a volte false a seconda del valore.
6.4.5 Uguaglianza e null
== nullè sempre sicuro.- Chiamare
.equals()su un referencenullgenera unaNullPointerException.
String s = null;
System.out.println(s == null); // true
// s.equals("Hi"); // ❌ NullPointerException
6.4.6 Tabella riepilogativa
| Comparison | Primitives | Objects / Wrappers | Strings |
|---|---|---|---|
== |
compares value | compares reference | identity (affected by String pool) |
.equals() |
N/A | compares content if overridden | content comparison |