Programmazione delle prestazioni Java, parte 2: il costo del casting

Per questo secondo articolo della nostra serie sulle prestazioni di Java, l'attenzione si sposta sul casting: cos'è, quanto costa e come possiamo (a volte) evitarlo. Questo mese, iniziamo con una rapida revisione delle basi di classi, oggetti e riferimenti, quindi seguiamo con uno sguardo ad alcuni dati sulle prestazioni hardcore (in una barra laterale, per non offendere gli schizzinosi!) E le linee guida sul tipi di operazioni che hanno maggiori probabilità di causare indigestione alla Java Virtual Machine (JVM). Infine, terminiamo con uno sguardo approfondito su come possiamo evitare gli effetti comuni di strutturazione delle classi che possono causare il casting.

Programmazione delle prestazioni Java: leggi l'intera serie!

  • Parte 1. Impara come ridurre il sovraccarico del programma e migliorare le prestazioni controllando la creazione di oggetti e la garbage collection
  • Parte 2. Riduci il sovraccarico e gli errori di esecuzione tramite codice indipendente dai tipi
  • Parte 3. Guarda come le collezioni alternative misurano le prestazioni e scopri come ottenere il massimo da ogni tipo

Tipi di oggetti e riferimenti in Java

Il mese scorso, abbiamo discusso la distinzione di base tra tipi primitivi e oggetti in Java. Sia il numero di tipi primitivi che le relazioni tra di loro (in particolare le conversioni tra tipi) sono fissati dalla definizione del linguaggio. Gli oggetti, d'altra parte, sono di tipi illimitati e possono essere correlati a qualsiasi numero di altri tipi.

Ciascuna definizione di classe in un programma Java definisce un nuovo tipo di oggetto. Ciò include tutte le classi delle librerie Java, quindi qualsiasi programma potrebbe utilizzare centinaia o addirittura migliaia di diversi tipi di oggetti. Alcuni di questi tipi sono specificati dalla definizione del linguaggio Java come aventi determinati usi o gestioni speciali (come l'uso di java.lang.StringBufferper le java.lang.Stringoperazioni di concatenazione). A parte queste poche eccezioni, tuttavia, tutti i tipi vengono trattati sostanzialmente allo stesso modo dal compilatore Java e dalla JVM utilizzata per eseguire il programma.

Se una definizione di classe non specifica (tramite la extendsclausola nell'intestazione della definizione di classe) un'altra classe come genitore o superclasse, estende implicitamente la java.lang.Objectclasse. Ciò significa che ogni classe alla fine si estende java.lang.Object, direttamente o tramite una sequenza di uno o più livelli di classi padre.

Gli oggetti stessi sono sempre istanze di classi e il tipo di un oggetto è la classe di cui è un'istanza. In Java, però, non trattiamo mai direttamente gli oggetti; lavoriamo con riferimenti a oggetti. Ad esempio, la riga:

 java.awt.Component myComponent; 

non crea un java.awt.Componentoggetto; crea una variabile di riferimento di tipo java.lang.Component. Anche se i riferimenti hanno tipi proprio come gli oggetti, non esiste una corrispondenza precisa tra i tipi di riferimento e di oggetto: un valore di riferimento può essere nullun oggetto dello stesso tipo del riferimento o un oggetto di qualsiasi sottoclasse (cioè, classe discendente da) il tipo di riferimento. In questo caso particolare, java.awt.Componentè una classe astratta, quindi sappiamo che non può mai esserci un oggetto dello stesso tipo del nostro riferimento, ma possono certamente esserci oggetti di sottoclassi di quel tipo di riferimento.

Polimorfismo e fusione

Il tipo di un riferimento determina come l' oggetto di riferimento - cioè, l'oggetto che è il valore del riferimento - può essere utilizzato. Ad esempio, nell'esempio precedente, l'utilizzo del codice myComponentpotrebbe richiamare uno qualsiasi dei metodi definiti dalla classe java.awt.Component, o una qualsiasi delle sue superclassi, sull'oggetto di riferimento.

Tuttavia, il metodo effettivamente eseguito da una chiamata non è determinato dal tipo di riferimento stesso, ma piuttosto dal tipo di oggetto a cui si fa riferimento. Questo è il principio di base del polimorfismo : le sottoclassi possono sovrascrivere i metodi definiti nella classe genitore per implementare comportamenti diversi. Nel caso della nostra variabile di esempio, se l'oggetto a cui si fa riferimento fosse effettivamente un'istanza di java.awt.Button, il cambiamento di stato risultante da una setLabel("Push Me")chiamata sarebbe diverso da quello risultante se l'oggetto di riferimento fosse un'istanza di java.awt.Label.

Oltre alle definizioni di classe, i programmi Java utilizzano anche definizioni di interfaccia. La differenza tra un'interfaccia e una classe è che un'interfaccia specifica solo un insieme di comportamenti (e, in alcuni casi, costanti), mentre una classe definisce un'implementazione. Poiché le interfacce non definiscono le implementazioni, gli oggetti non possono mai essere istanze di un'interfaccia. Possono, tuttavia, essere istanze di classi che implementano un'interfaccia. I riferimenti possono essere di tipi di interfaccia, nel qual caso gli oggetti referenziati possono essere istanze di qualsiasi classe che implementa l'interfaccia (direttamente o tramite una classe antenata).

Il casting viene utilizzato per convertire tra tipi, in particolare tra tipi di riferimento, per il tipo di operazione di casting a cui siamo interessati qui. Le operazioni di upcast (chiamate anche conversioni di ampliamento nella specifica del linguaggio Java) convertono un riferimento di sottoclasse in un riferimento di classe predecessore. Questa operazione di casting è normalmente automatica, poiché è sempre sicura e può essere implementata direttamente dal compilatore.

Le operazioni di downcast (chiamate anche conversioni di restringimento nella specifica del linguaggio Java) convertono un riferimento di una classe predecessore in un riferimento di sottoclasse. Questa operazione di casting crea un sovraccarico di esecuzione, poiché Java richiede che il cast venga controllato in fase di esecuzione per assicurarsi che sia valido. Se l'oggetto a cui si fa riferimento non è un'istanza del tipo di destinazione per il cast o una sottoclasse di quel tipo, il tentativo di cast non è consentito e deve generare un file java.lang.ClassCastException.

L' instanceofoperatore in Java consente di determinare se una specifica operazione di casting è consentita o meno senza tentare effettivamente l'operazione. Poiché il costo di prestazione di un controllo è molto inferiore a quello dell'eccezione generata da un tentativo di lancio instanceofnon consentito , è generalmente saggio usare un test ogni volta che non sei sicuro che il tipo di riferimento sia quello che vorresti che fosse . Prima di farlo, tuttavia, dovresti assicurarti di avere un modo ragionevole di trattare un riferimento di un tipo indesiderato, altrimenti potresti anche lasciare che l'eccezione venga lanciata e gestirla a un livello più alto nel tuo codice.

Prestare attenzione ai venti

Il cast consente l'uso della programmazione generica in Java, dove il codice è scritto per funzionare con tutti gli oggetti delle classi discendenti da una classe base (spesso java.lang.Object, per classi di utilità). Tuttavia, l'uso del casting causa una serie unica di problemi. Nella sezione successiva esamineremo l'impatto sulle prestazioni, ma prima consideriamo l'effetto sul codice stesso. Ecco un esempio che utilizza la java.lang.Vectorclasse di raccolta generica :

vettore privato alcuniNumeri; ... public void doSomething () {... int n = ... Numero intero = (Integer) someNumbers.elementAt (n); ...}

Questo codice presenta potenziali problemi in termini di chiarezza e manutenibilità. Se qualcuno diverso dallo sviluppatore originale dovesse modificare il codice ad un certo punto, potrebbe ragionevolmente pensare di poter aggiungere java.lang.Doublea alle someNumbersraccolte, poiché questa è una sottoclasse di java.lang.Number. Tutto si compilerebbe bene se lo provasse, ma a un certo punto indeterminato dell'esecuzione probabilmente verrebbe java.lang.ClassCastExceptionlanciato quando il tentativo di lancio di a è java.lang.Integerstato eseguito per il suo valore aggiunto.

Il problema qui è che l'uso del casting ignora i controlli di sicurezza incorporati nel compilatore Java; il programmatore finisce per cercare errori durante l'esecuzione, poiché il compilatore non li rileva. Questo non è di per sé disastroso, ma questo tipo di errore di utilizzo spesso si nasconde in modo abbastanza intelligente mentre stai testando il tuo codice, solo per rivelarsi quando il programma viene messo in produzione.

Non sorprende che il supporto per una tecnica che consentirebbe al compilatore di rilevare questo tipo di errore di utilizzo è uno dei miglioramenti più richiesti a Java. C'è un progetto in corso nel processo della comunità Java che sta esaminando l'aggiunta di questo supporto: numero di progetto JSR-000014, Aggiungi tipi generici al linguaggio di programmazione Java (vedere la sezione Risorse di seguito per maggiori dettagli.) Nel seguito di questo articolo, in arrivo il mese prossimo, esamineremo questo progetto in modo più dettagliato e discuteremo sia di come è probabile che sia di aiuto sia di dove è probabile che ci lascino desiderare di più.

Il problema delle prestazioni

È noto da tempo che il casting può essere dannoso per le prestazioni in Java e che è possibile migliorare le prestazioni riducendo al minimo il casting nel codice molto utilizzato. Anche le chiamate ai metodi, in particolare le chiamate tramite interfacce, sono spesso menzionate come potenziali colli di bottiglia delle prestazioni. L'attuale generazione di JVM ha fatto molta strada dai loro predecessori, tuttavia, e vale la pena controllare per vedere come questi principi reggono oggi.

Per questo articolo, ho sviluppato una serie di test per vedere quanto siano importanti questi fattori per le prestazioni con le JVM attuali. I risultati del test sono riassunti in due tabelle nella barra laterale, la Tabella 1 che mostra l'overhead della chiamata al metodo e la Tabella 2 il casting overhead. Il codice sorgente completo per il programma di test è anche disponibile online (vedere la sezione Risorse di seguito per maggiori dettagli).

Per riassumere queste conclusioni per i lettori che non vogliono guadare i dettagli nelle tabelle, alcuni tipi di chiamate e cast di metodi sono ancora piuttosto costosi, in alcuni casi richiedono quasi quanto una semplice allocazione di oggetti. Ove possibile, questi tipi di operazioni dovrebbero essere evitati nel codice che deve essere ottimizzato per le prestazioni.

In particolare, le chiamate ai metodi sovrascritti (metodi che vengono sovrascritti in qualsiasi classe caricata, non solo la classe effettiva dell'oggetto) e le chiamate tramite le interfacce sono notevolmente più costose delle semplici chiamate ai metodi. La beta di HotSpot Server JVM 2.0 utilizzata nel test convertirà anche molte semplici chiamate di metodo in codice inline, evitando qualsiasi sovraccarico per tali operazioni. Tuttavia, HotSpot mostra le prestazioni peggiori tra le JVM testate per i metodi e le chiamate sostituite tramite le interfacce.

Per il casting (downcasting, ovviamente), le JVM testate generalmente mantengono il calo delle prestazioni a un livello ragionevole. HotSpot fa un lavoro eccezionale con questo nella maggior parte dei test di benchmark e, come con le chiamate al metodo, in molti casi semplici è in grado di eliminare quasi completamente il sovraccarico del casting. Per situazioni più complicate, come i cast seguiti da chiamate a metodi sovrascritti, tutte le JVM testate mostrano un notevole degrado delle prestazioni.

La versione testata di HotSpot ha anche mostrato prestazioni estremamente scadenti quando un oggetto è stato lanciato a diversi tipi di riferimento in successione (invece di essere sempre lanciato allo stesso tipo di destinazione). Questa situazione si verifica regolarmente in biblioteche come Swing che utilizzano una profonda gerarchia di classi.

In most cases, the overhead of both method calls and casting is small in comparison with the object-allocation times looked at in last month's article. However, these operations will often be used far more frequently than object allocations, so they can still be a significant source of performance problems.

In the remainder of this article, we'll discuss some specific techniques for reducing the need for casting in your code. Specifically, we'll look at how casting often arises from the way subclasses interact with base classes, and explore some techniques for eliminating this type of casting. Next month, in the second part of this look at casting, we'll consider another common cause of casting, the use of generic collections.

Base classes and casting

There are several common uses of casting in Java programs. For instance, casting is often used for the generic handling of some functionality in a base class that may be extended by a number of subclasses. The following code shows a somewhat contrived illustration of this usage:

 // simple base class with subclasses public abstract class BaseWidget { ... } public class SubWidget extends BaseWidget { ... public void doSubWidgetSomething() { ... } } ... // base class with subclasses, using the prior set of classes public abstract class BaseGorph { // the Widget associated with this Gorph private BaseWidget myWidget; ... // set the Widget associated with this Gorph (only allowed for subclasses) protected void setWidget(BaseWidget widget) { myWidget = widget; } // get the Widget associated with this Gorph public BaseWidget getWidget() { return myWidget; } ... // return a Gorph with some relation to this Gorph // this will always be the same type as it's called on, but we can only // return an instance of our base class public abstract BaseGorph otherGorph() { ... } } // Gorph subclass using a Widget subclass public class SubGorph extends BaseGorph { // return a Gorph with some relation to this Gorph public BaseGorph otherGorph() { ... } ... public void anyMethod() { ... // set the Widget we're using SubWidget widget = ... setWidget(widget); ... // use our Widget ((SubWidget)getWidget()).doSubWidgetSomething(); ... // use our otherGorph SubGorph other = (SubGorph) otherGorph(); ... } }