W3docs

Suggerimenti per le prestazioni in Java

Suggerimenti pratici per le prestazioni in Java: misurare prima, evitare ottimizzazioni premature e le micro-ottimizzazioni più efficaci.

Java è abbastanza veloce per quasi tutto, ma il codice lento esiste ancora — di solito per fare troppo lavoro, allocare troppa memoria o scegliere la struttura dati sbagliata. Il consiglio più importante sulle prestazioni non è un trucco: misura prima di cambiare qualsiasi cosa. Questo capitolo spiega come misurare in modo onesto, le poche ottimizzazioni che ripagano più spesso (costruzione di stringhe, scelta della struttura dati, evitare allocazioni inutili) e le insidie che rendono il codice "veloce" in modo ingenuo in realtà lento.

Misura prima, ottimizza dopo

Fare ipotesi sulle prestazioni è il modo in cui si spendono ore ad accelerare codice che non era mai lento. Esegui il profiling su un carico di lavoro reale, trova il percorso caldo (la piccola frazione di codice dove viene speso la maggior parte del tempo), e solo allora ottimizzala. Per esperimenti rapidi, System.nanoTime() fornisce un delta di tempo di muro; per benchmark seri usa uno strumento come JMH (Java Microbenchmark Harness), che riscalda il JIT e tiene conto del rumore di misurazione.

long start = System.nanoTime();
doWork();
long elapsedMs = (System.nanoTime() - start) / 1_000_000;
System.out.println("Took " + elapsedMs + " ms");

Due regole si accompagnano a questo. Prima, evita l'ottimizzazione prematura — codice chiaro che è abbastanza veloce batte codice astuto difficile da leggere. Secondo, il compilatore JIT della JVM ottimizza il codice caldo a runtime, quindi un metodo raggiunge la piena velocità solo dopo essere stato eseguito molte volte; una singola chiamata cronometrata dice poco.

Costruisci le stringhe con StringBuilder

Poiché le stringhe sono immutabili, s += x all'interno di un ciclo crea una nuova stringa e copia ogni carattere precedente ad ogni iterazione — è un lavoro O(n²). StringBuilder mantiene un buffer espandibile e aggiunge in loco, trasformando lo stesso lavoro in O(n).

// Slow: a new String allocated every iteration
String csv = "";
for (String field : fields) {
    csv += field + ",";
}

// Fast: one buffer, appended in place
StringBuilder sb = new StringBuilder();
for (String field : fields) {
    sb.append(field).append(',');
}
String csv2 = sb.toString();

Un singolo a + b + c fuori da un ciclo va bene — il compilatore lo trasforma già in un solo StringBuilder (vedi concatenazione di stringhe per quello che fa il compilatore). Il problema è la concatenazione dentro un ciclo, dove ogni passaggio aggiunge un'altra copia completa.

Scegli la struttura dati giusta

I guadagni maggiori di solito derivano da scelte algoritmiche, non da piccole modifiche. Cercare un valore in un ArrayList scorre ogni elemento (O(n)); una HashMap o HashSet lo fa in tempo approssimativamente costante (O(1)). Scegli la collezione che corrisponde a come accedi effettivamente ai dati.

EsigenzaUsaCosto di ricerca
Accesso per indice, aggiunta in fondoArrayListO(1) per indice, O(n) per valore
Ricerca chiave/valoreHashMapO(1) medio
Test di appartenenza, senza duplicatiHashSetO(1) medio
Chiavi ordinateTreeMapO(log n)
Inserimento/rimozione frequente alle estremitàArrayDequeO(1) alle estremità

Se conosci la dimensione finale, passala al costruttore: new ArrayList<>(10_000) o new HashMap<>(capacity). Questo evita la riallocazione e la copia ripetuta che avviene man mano che una collezione cresce.

Evita la creazione inutile di oggetti

Ogni oggetto allocato deve essere successivamente raccolto, e il garbage collection non è gratuito. Riutilizza i valori immutabili, preferisci i primitivi ai loro wrapper nei cicli stretti, e non creare oggetti che scarti immediatamente.

// Autoboxing: every += boxes a new Integer
Long total = 0L;
for (int i = 0; i < n; i++) total += i;   // slow, allocates boxes

// Primitive: no allocation at all
long sum = 0L;
for (int i = 0; i < n; i++) sum += i;     // fast

Altri guadagni facili: metti in cache gli oggetti Pattern compilati invece di chiamare String.matches() in un ciclo, riutilizza un DateTimeFormatter (è thread-safe e immutabile), e preferisci il for potenziato agli stream nei cicli interni più caldi dove l'allocazione è importante.

java— editable, runs on the server

Cosa trarre dall'esecuzione:

  • Same result? true dimostra che StringBuilder produce esattamente la stessa stringa di +=, quindi sostituirlo cambia solo la velocità, mai la correttezza.
  • Il rapporto "x slower" stampato mostra che la concatenazione in un ciclo costa molte volte di più rispetto all'aggiunta a un buffer, perché ogni += copia l'intera stringa fino a quel momento.
  • Both lists size 100000: true conferma che un ArrayList pre-dimensionato finisce identico a uno cresciuto — il suggerimento al costruttore influisce sull'allocazione, non sul contenuto.
  • Pre-sized faster? true mostra che indicare ad ArrayList la sua capacità in anticipo evita i passaggi ripetuti di ridimensionamento e copia.
  • Map lookups found: 50000 in ... ms dimostra che 50.000 ricerche HashMap si completano in circa un millisecondo, il vantaggio di scegliere l'accesso O(1) rispetto a una scansione di lista O(n).

Errori comuni

Alcuni errori trasformano il codice "ovviamente più veloce" nel suo opposto:

  • Fidarsi di una singola esecuzione cronometrata. Il JIT non si è ancora riscaldato, e il sistema operativo potrebbe aver pianificato qualcos'altro a metà misurazione. Ripeti il lavoro migliaia di volte, o usa JMH, prima di credere a un numero.
  • Micro-ottimizzare il codice freddo. Un metodo che viene eseguito una volta all'avvio non guadagna nulla da un ciclo più stretto. Dedica l'effort solo al percorso caldo che il profiler indica.
  • Costruire stringhe con + in un ciclo. Il rallentamento evitabile più comune — usa StringBuilder ogni volta che concateni dentro un ciclo.
  • Autoboxing nascosto. Un List<Integer>, Map<Integer, Integer>, o un accumulatore Long vagante incapsula ogni valore. In un ciclo numerico stretto, preferisci i primitivi e gli array di primitivi.
  • Ottimizzare prima che funzioni. Prima la correttezza, poi la velocità. Il codice chiaro che puoi profilare batte il codice astuto su cui non puoi ragionare.
  • Misura prima di cambiare qualsiasi cosa — esegui il profiling su un carico di lavoro reale e ottimizza solo il percorso caldo.
  • Costruisci le stringhe con StringBuilder, non con +=, nei cicli.
  • Abbina la collezione al pattern di accesso: HashMap/HashSet per le ricerche, ArrayList per l'accesso indicizzato; pre-dimensiona quando il conteggio è noto.
  • Evita allocazioni inutili: preferisci i primitivi, riutilizza gli oggetti immutabili, e ricorda che ogni oggetto aggiunge lavoro al garbage collection.

Esercitazione

Pratica
Perché usare '+=' per costruire una String dentro un ciclo è lento rispetto a StringBuilder?
Perché usare '+=' per costruire una String dentro un ciclo è lento rispetto a StringBuilder?
Was this page helpful?