finale (Java) - final (Java)

Nel linguaggio di programmazione Java , la final parola chiave viene utilizzata in diversi contesti per definire un'entità che può essere assegnata solo una volta.

Una volta final assegnata, una variabile contiene sempre lo stesso valore. Se una final variabile contiene un riferimento a un oggetto, lo stato dell'oggetto può essere modificato dalle operazioni sull'oggetto, ma la variabile farà sempre riferimento allo stesso oggetto (questa proprietà di final è chiamata non transitività ). Questo vale anche per gli array, perché gli array sono oggetti; se una final variabile contiene un riferimento a un array, i componenti dell'array possono essere modificati dalle operazioni sull'array, ma la variabile farà sempre riferimento allo stesso array.

Classi finali

Una classe finale non può essere sottoclasse. Poiché ciò può conferire vantaggi in termini di sicurezza ed efficienza, molte delle classi della libreria standard Java sono definitive, come java.lang.System e java.lang.String .

Esempio:

public final class MyFinalClass {...}

public class ThisIsWrong extends MyFinalClass {...} // forbidden

Metodi finali

Un metodo finale non può essere sovrascritto o nascosto da sottoclassi. Viene utilizzato per prevenire comportamenti imprevisti da parte di una sottoclasse che altera un metodo che può essere cruciale per la funzione o la coerenza della classe.

Esempio:

public class Base
{
    public       void m1() {...}
    public final void m2() {...}

    public static       void m3() {...}
    public static final void m4() {...}
}

public class Derived extends Base
{
    public void m1() {...}  // OK, overriding Base#m1()
    public void m2() {...}  // forbidden

    public static void m3() {...}  // OK, hiding Base#m3()
    public static void m4() {...}  // forbidden
}

Un malinteso comune è che dichiarare un metodo come final migliori l'efficienza consentendo al compilatore di inserire direttamente il metodo ovunque venga chiamato (vedere espansione inline ). Poiché il metodo viene caricato in fase di esecuzione , i compilatori non sono in grado di farlo. Solo l'ambiente di runtime e il compilatore JIT sanno esattamente quali classi sono state caricate, quindi solo loro sono in grado di prendere decisioni su quando eseguire l'inline, indipendentemente dal fatto che il metodo sia definitivo o meno.

Fanno eccezione i compilatori di codice macchina che generano codice macchina specifico della piattaforma eseguibile direttamente . Quando si utilizza il collegamento statico , il compilatore può tranquillamente presumere che i metodi e le variabili calcolabili in fase di compilazione possano essere inseriti in linea.

Variabili finali

Una variabile finale può essere inizializzata solo una volta, tramite un inizializzatore o un'istruzione di assegnazione. Non ha bisogno di essere inizializzato al momento della dichiarazione: questa è chiamata variabile "blank final". Una variabile di istanza finale vuota di una classe deve essere assegnata definitivamente in ogni costruttore della classe in cui è dichiarata; analogamente, una variabile statica finale vuota deve essere definitivamente assegnata in un inizializzatore statico della classe in cui è dichiarata; in caso contrario, in entrambi i casi si verifica un errore in fase di compilazione. (Nota:. Se la variabile è un riferimento, questo significa che la variabile non può essere ri-bound per fare riferimento a un altro oggetto, ma l'oggetto a cui fa riferimento è ancora mutabile ., Se era in origine mutabile)

A differenza del valore di una costante , il valore di una variabile finale non è necessariamente noto in fase di compilazione. È considerata buona pratica rappresentare le costanti finali in tutte le maiuscole, utilizzando il carattere di sottolineatura per separare le parole.

Esempio:

public class Sphere {

    // pi is a universal constant, about as constant as anything can be.
    public static final double PI = 3.141592653589793;

    public final double radius;
    public final double xPos;
    public final double yPos;
    public final double zPos;

    Sphere(double x, double y, double z, double r) {
         radius = r;
         xPos = x;
         yPos = y;
         zPos = z;
    }

    [...]
}

Qualsiasi tentativo di riassegnare radius , xPos , yPos , o zPos si tradurrà in un errore di compilazione. Infatti, anche se il costruttore non imposta una variabile finale, il tentativo di impostarla al di fuori del costruttore risulterà in un errore di compilazione.

Per illustrare che la finalità non garantisce l'immutabilità: supponiamo di sostituire le tre variabili di posizione con una sola:

    public final Position pos;

dove pos è un oggetto con tre proprietà pos.x , pos.y e pos.z . Quindi pos non può essere assegnato a, ma le tre proprietà possono, a meno che non siano definitive stesse.

Come la piena immutabilità , l'uso delle variabili finali ha grandi vantaggi, soprattutto nell'ottimizzazione. Ad esempio, Sphere probabilmente avrà una funzione che restituisce il suo volume; sapere che il suo raggio è costante ci permette di memorizzare il volume calcolato. Se abbiamo relativamente pochi se Sphere abbiamo bisogno dei loro volumi molto spesso, il miglioramento delle prestazioni potrebbe essere sostanziale. Fare il raggio di a Sphere final informa sviluppatori e compilatori che questo tipo di ottimizzazione è possibile in tutto il codice che utilizza Sphere s.

Sebbene sembri violare il final principio, la seguente è una dichiarazione legale:

for (final SomeObject obj : someList) {
   // do something with obj
}

Poiché la variabile obj esce dall'ambito con ogni iterazione del ciclo, viene effettivamente ridichiarata ogni iterazione, consentendo di utilizzare lo stesso token (cioè obj ) per rappresentare più variabili.

Variabili finali in oggetti annidati

Le variabili finali possono essere utilizzate per costruire alberi di oggetti immutabili. Una volta costruiti, è garantito che questi oggetti non cambieranno più. Per ottenere ciò, una classe immutabile deve avere solo campi finali e questi campi finali possono avere solo tipi immutabili. I tipi primitivi di Java sono immutabili, così come le stringhe e molte altre classi.

Se la costruzione di cui sopra viene violata avendo un oggetto nell'albero che non è immutabile, l'aspettativa non ritiene che qualcosa di raggiungibile tramite la variabile finale sia costante. Ad esempio, il codice seguente definisce un sistema di coordinate la cui origine deve essere sempre a (0, 0). L'origine è implementata usando un java.awt.Point pensiero e questa classe definisce i suoi campi come pubblici e modificabili. Ciò significa che anche quando si raggiunge l' origin oggetto su un percorso di accesso con solo variabili finali, quell'oggetto può comunque essere modificato, come dimostra il codice di esempio riportato di seguito.

import java.awt.Point;

public class FinalDemo {

    static class CoordinateSystem {
        private final Point origin = new Point(0, 0);

        public Point getOrigin() { return origin; }
    }

    public static void main(String[] args) {
        CoordinateSystem coordinateSystem = new CoordinateSystem();

        coordinateSystem.getOrigin().x = 15;

        assert coordinateSystem.getOrigin().getX() == 0;
    }
}

La ragione di ciò è che dichiarare una variabile final significa solo che questa variabile punterà allo stesso oggetto in qualsiasi momento. L'oggetto a cui punta la variabile non è però influenzato da quella variabile finale. Nell'esempio sopra, le coordinate xey dell'origine possono essere modificate liberamente.

Per prevenire questa situazione indesiderata, un requisito comune è che tutti i campi di un oggetto immutabile devono essere finali e che i tipi di questi campi devono essere immutabili essi stessi. Ciò squalifica java.util.Date e java.awt.Point e molte altre classi dall'utilizzo in tali oggetti immutabili.

Classi finali e interne

Quando una classe interna anonima viene definita all'interno del corpo di un metodo, tutte le variabili dichiarate final nell'ambito di tale metodo sono accessibili dall'interno della classe interna. Per i valori scalari, una volta assegnato, il valore della final variabile non può cambiare. Per i valori degli oggetti, il riferimento non può cambiare. Ciò consente al compilatore Java di "catturare" il valore della variabile in fase di esecuzione e memorizzare una copia come campo nella classe interna. Una volta che il metodo esterno è terminato e il relativo stack frame è stato rimosso, la variabile originale è scomparsa ma la copia privata della classe interna persiste nella memoria della classe.

import javax.swing.*;

public class FooGUI {

    public static void main(String[] args) {
        //initialize GUI components
        final JFrame jf = new JFrame("Hello world!"); //allows jf to be accessed from inner class body
        jf.add(new JButton("Click me"));

        // pack and make visible on the Event-Dispatch Thread
        SwingUtilities.invokeLater(new Runnable() {
            @Override
            public void run() {
                jf.pack(); //this would be a compile-time error if jf were not final
                jf.setLocationRelativeTo(null);
                jf.setVisible(true);
            }
        });
    }
}

Finale in bianco

Il blank final , che è stato introdotto in Java 1.1, è una variabile finale la cui dichiarazione manca di un inizializzatore. Prima di Java 1.1, era necessaria una variabile finale per avere un inizializzatore. Un finale vuoto, per definizione "finale", può essere assegnato una sola volta. cioè deve essere annullato quando si verifica un'assegnazione. Per fare ciò, un compilatore Java esegue un'analisi di flusso per assicurarsi che, per ogni assegnazione a una variabile finale vuota, la variabile sia definitivamente non assegnata prima dell'assegnazione; altrimenti si verifica un errore in fase di compilazione.

final boolean hasTwoDigits;
if (number >= 10 && number < 100) {
  hasTwoDigits = true;
}
if (number > -100 && number <= -10) {
  hasTwoDigits = true; // compile-error because the final variable might already be assigned.
}

Inoltre, prima dell'accesso deve essere assegnato in modo definitivo anche un finale vuoto.

final boolean isEven;

if (number % 2 == 0) {
  isEven = true;
}

System.out.println(isEven); // compile-error because the variable was not assigned in the else-case.

Si noti tuttavia che anche una variabile locale non finale deve essere assegnata in modo definitivo prima di accedervi.

boolean isEven; // *not* final

if (number % 2 == 0) {
  isEven = true;
}

System.out.println(isEven); // Same compile-error because the non-final variable was not assigned in the else-case.

Analogo C / C ++ delle variabili finali

In C e C ++ , il costrutto analogo è la const parola chiave . Questo differisce sostanzialmente da final Java, fondamentalmente essendo un qualificatore di tipo : const è parte del tipo , non solo parte dell'identificatore (variabile). Ciò significa anche che la costanza di un valore può essere modificata mediante casting (conversione di tipo esplicita), in questo caso noto come "const casting". Tuttavia, eliminare la costanza e quindi modificare l'oggetto produce un comportamento indefinito se l'oggetto è stato originariamente dichiarato const . Java final è una regola rigida tale che è impossibile compilare codice che infrange o aggira direttamente le restrizioni finali. Utilizzando la riflessione , tuttavia, è spesso possibile modificare ancora le variabili finali. Questa funzionalità viene utilizzata principalmente durante la deserializzazione di oggetti con membri finali.

Inoltre, poiché C e C ++ espongono direttamente puntatori e riferimenti, esiste una distinzione tra se il puntatore stesso è costante e se i dati puntati dal puntatore sono costanti. Applicare const a un puntatore stesso, come in SomeClass * const ptr , significa che i contenuti a cui si fa riferimento possono essere modificati, ma il riferimento stesso non può (senza casting). Questo utilizzo si traduce in un comportamento che imita il comportamento di un final riferimento a una variabile in Java. Al contrario, quando si applica const solo ai dati di riferimento, come in const SomeClass * ptr , il contenuto non può essere modificato (senza casting), ma il riferimento stesso può. Sia il riferimento che il contenuto a cui si fa riferimento possono essere dichiarati come const .

Analoghi C # per la parola chiave finale

C # può essere considerato simile a Java, in termini di caratteristiche del linguaggio e sintassi di base: Java ha JVM, C # ha .Net Framework; Java ha bytecode, C # ha MSIL; Java non ha supporto per i puntatori (memoria reale), C # è lo stesso.

Per quanto riguarda la parola chiave finale, C # ha due parole chiave correlate:

  1. La parola chiave equivalente per metodi e classi è sealed
  2. La parola chiave equivalente per le variabili è readonly

Si noti che una differenza fondamentale tra la parola chiave derivata C / C ++ const e la parola chiave C # readonly è che const viene valutata in fase di compilazione, mentre readonly viene valutata in fase di esecuzione e quindi può avere un'espressione che viene calcolata e corretta solo in seguito (in fase di esecuzione).

Riferimenti