wersja ostateczna (Java) - final (Java)

W języku programowania Java , final słów kluczowych jest używany w kilku kontekstach, aby określić podmiot, który może być przypisany tylko raz.

Po przypisaniu final zmiennej zawsze zawiera ona tę samą wartość. Jeśli final zmienna zawiera odniesienie do obiektu, to stan obiektu może zostać zmieniony przez operacje na obiekcie, ale zmienna będzie zawsze odnosić się do tego samego obiektu (ta właściwość final nazywa się nieprzechodniością ). Dotyczy to również tablic, ponieważ tablice są obiektami; jeśli final zmienna zawiera odniesienie do tablicy, wówczas składniki tablicy mogą zostać zmienione przez operacje na tablicy, ale zmienna będzie zawsze odnosić się do tej samej tablicy.

Zajęcia końcowe

Końcowy klasa nie może być podklasy. Ponieważ może to przynieść korzyści w zakresie bezpieczeństwa i wydajności, wiele klas standardowych bibliotek Java jest ostatecznych, takich jak java.lang.System i java.lang.String .

Przykład:

public final class MyFinalClass {...}

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

Metody końcowe

Ostatniej metody nie można przesłonić ani ukryć przez podklasy. Służy to zapobieganiu nieoczekiwanemu zachowaniu podklasy zmieniającej metodę, która może mieć kluczowe znaczenie dla funkcji lub spójności klasy.

Przykład:

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
}

Powszechnym nieporozumieniem jest to, że deklarowanie metody jako final poprawia wydajność, umożliwiając kompilatorowi bezpośrednie wstawianie metody w każdym miejscu, w którym jest ona wywoływana (patrz rozwijanie wbudowane ). Ponieważ metoda jest ładowana w czasie wykonywania , kompilatory nie mogą tego zrobić. Tylko środowisko uruchomieniowe i kompilator JIT dokładnie wiedzą, które klasy zostały załadowane, więc tylko one mogą podejmować decyzje o tym, kiedy wstawić, czy metoda jest ostateczna.

Wyjątkiem są kompilatory kodu maszynowego, które generują bezpośrednio wykonywalny kod maszynowy specyficzny dla platformy . Korzystając z łączenia statycznego , kompilator może bezpiecznie założyć, że metody i zmienne obliczalne w czasie kompilacji mogą być wbudowane.

Zmienne końcowe

Końcowe zmienne można zainicjować tylko jeden raz, za pośrednictwem instrukcji inicjowania lub przydziału. Nie trzeba jej inicjować w momencie deklaracji: nazywa się to „pustą końcową” zmienną. Pusta końcowa zmienna instancji klasy musi być ostatecznie przypisana w każdym konstruktorze klasy, w której jest zadeklarowana; podobnie, pusta końcowa zmienna statyczna musi być ostatecznie przypisana w statycznym inicjatorze klasy, w której jest zadeklarowana; w przeciwnym razie w obu przypadkach wystąpi błąd kompilacji. (Uwaga: jeśli zmienna jest odwołaniem, oznacza to, że zmienna nie może być ponownie powiązana z odniesieniem do innego obiektu. Jednak obiekt, do którego się odwołuje, jest nadal modyfikowalny , jeśli pierwotnie był zmienny).

W przeciwieństwie do wartości stałej , wartość zmiennej końcowej niekoniecznie jest znana w czasie kompilacji. Uważa się, że dobrą praktyką jest przedstawianie końcowych stałych dużymi literami, przy użyciu podkreślenia do oddzielania słów.

Przykład:

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;
    }

    [...]
}

Każda próba przypisanie radius , xPos , yPos , lub zPos spowoduje błąd kompilacji. W rzeczywistości, nawet jeśli konstruktor nie ustawia końcowej zmiennej, próba ustawienia jej poza konstruktorem spowoduje błąd kompilacji.

Aby zilustrować, że ostateczność nie gwarantuje niezmienności: załóżmy, że zastąpimy trzy zmienne pozycji jedną:

    public final Position pos;

w którym pos jest obiekt z trzech właściwości pos.x , pos.y i pos.z . Wtedy pos nie można ich przypisać, ale trzy właściwości mogą, chyba że same są ostateczne.

Podobnie jak pełna niezmienność , użycie zmiennych końcowych ma ogromne zalety, zwłaszcza w optymalizacji. Na przykład Sphere prawdopodobnie będzie miał funkcję zwracającą jego objętość; wiedząc, że jego promień jest stały, pozwala nam zapamiętać obliczoną objętość. Jeśli mamy stosunkowo niewiele Sphere siatek i bardzo często potrzebujemy ich objętości, wzrost wydajności może być znaczny. Podanie promienia a Sphere final informuje programistów i kompilatory, że ten rodzaj optymalizacji jest możliwy w każdym kodzie, który używa Sphere s.

Chociaż wydaje się, że narusza final zasadę, następujące oświadczenie prawne:

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

Ponieważ zmienna obj wychodzi poza zakres z każdą iteracją pętli, w rzeczywistości jest ponownie deklarowana w każdej iteracji, umożliwiając użycie tego samego tokenu (tj. obj ) Do reprezentowania wielu zmiennych.

Zmienne końcowe w obiektach zagnieżdżonych

Zmienne końcowe mogą służyć do konstruowania drzew niezmiennych obiektów. Po zbudowaniu obiekty te nie ulegną już zmianie. Aby to osiągnąć, niezmienna klasa musi mieć tylko końcowe pola, a te końcowe pola mogą mieć same niezmienne typy. Typy prymitywne Javy są niezmienne, podobnie jak łańcuchy znaków i kilka innych klas.

Jeśli powyższa konstrukcja zostanie naruszona przez posiadanie w drzewie obiektu, który nie jest niezmienny, nie można oczekiwać, że cokolwiek osiągalne poprzez zmienną końcową jest stałe. Na przykład poniższy kod definiuje układ współrzędnych, którego początek powinien zawsze znajdować się w (0, 0). Pochodzenie jest implementowane przy użyciu java.awt.Point choć, a ta klasa definiuje jej pola jako publiczne i modyfikowalne. Oznacza to, że nawet po dotarciu do origin obiektu przez ścieżkę dostępu zawierającą tylko końcowe zmienne, obiekt ten można nadal modyfikować, jak pokazuje poniższy przykładowy kod.

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;
    }
}

Powodem tego jest to, że zadeklarowanie zmiennej jako ostatecznej oznacza tylko, że ta zmienna będzie wskazywać ten sam obiekt w dowolnym momencie. Na obiekt wskazywany przez zmienną nie ma jednak wpływu ta zmienna końcowa. W powyższym przykładzie współrzędne x i y początku można dowolnie modyfikować.

Aby zapobiec tej niepożądanej sytuacji, powszechnym wymaganiem jest to, że wszystkie pola niezmiennego obiektu muszą być ostateczne, a typy tych pól same muszą być niezmienne. To dyskwalifikuje java.util.Date i java.awt.Point i kilka innych klas z używania w takich niezmiennych obiektach.

Klasy końcowe i wewnętrzne

Gdy anonimowa klasa wewnętrzna jest zdefiniowana w treści metody, wszystkie zmienne zadeklarowane final w zakresie tej metody są dostępne z poziomu klasy wewnętrznej. W przypadku wartości skalarnych po przypisaniu wartość final zmiennej nie może się zmienić. W przypadku wartości obiektów odwołanie nie może się zmienić. Pozwala to kompilatorowi Java na „przechwycenie” wartości zmiennej w czasie wykonywania i zapisanie jej kopii jako pola w klasie wewnętrznej. Po zakończeniu działania metody zewnętrznej i usunięciu jej ramki stosu oryginalna zmienna jest usuwana, ale prywatna kopia klasy wewnętrznej pozostaje we własnej pamięci klasy.

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);
            }
        });
    }
}

Pusty finał

Puste końcowy , który został wprowadzony w Javie 1.1, jest ostateczna deklaracja zmiennej, której brakuje inicjatora. Przed wersją Java 1.1 ostateczna zmienna musiała mieć inicjator. Pusty finał, z definicji „ostateczny”, można przypisać tylko raz. tzn. musi być nieprzypisane, gdy nastąpi przypisanie. W tym celu kompilator języka Java przeprowadza analizę przepływu, aby upewnić się, że dla każdego przypisania do pustej zmiennej końcowej zmienna jest definitywnie nieprzypisana przed przypisaniem; w przeciwnym razie wystąpi błąd w czasie kompilacji.

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.
}

Ponadto przed uzyskaniem dostępu należy ostatecznie przypisać pusty finał.

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.

Należy jednak pamiętać, że nieostateczna zmienna lokalna również musi zostać ostatecznie przypisana przed uzyskaniem do niej dostępu.

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.

Analog C / C ++ zmiennych końcowych

W C i C ++ analogiczną konstrukcją jest const słowo kluczowe . Różni się to zasadniczo od final języka Java, głównie tym, że jest kwalifikatorem typu : const jest częścią typu , a nie tylko częścią identyfikatora (zmiennej). Oznacza to również, że stałość wartości można zmienić przez rzutowanie (jawna konwersja typu), w tym przypadku nazywane „rzutowaniem stałym”. Niemniej jednak odrzucenie stałości, a następnie zmodyfikowanie obiektu powoduje niezdefiniowane zachowanie, jeśli obiekt został pierwotnie zadeklarowany const . Java final to ścisła reguła uniemożliwiająca skompilowanie kodu, który bezpośrednio łamie lub omija ostateczne ograniczenia. Jednak wykorzystując refleksję , często można nadal modyfikować końcowe zmienne. Ta funkcja jest najczęściej używana podczas deserializacji obiektów z elementami końcowymi.

Ponadto, ponieważ C i C ++ bezpośrednio ujawniają wskaźniki i referencje, istnieje różnica między tym, czy sam wskaźnik jest stały, a czy dane wskazywane przez wskaźnik są stałe. Zastosowanie const do samego wskaźnika, tak jak w SomeClass * const ptr , oznacza, że ​​zawartość, do której się odwołuje, może być modyfikowana, ale samo odwołanie nie może (bez rzutowania). To użycie skutkuje zachowaniem, które naśladuje zachowanie final odwołania do zmiennej w Javie. W przeciwieństwie do tego, gdy stosuje się const tylko do danych, do których się odwołuje, tak jak w const SomeClass * ptr , zawartość nie może być modyfikowana (bez rzutowania), ale samo odwołanie może. Zarówno odwołanie, jak i zawartość, do której się odwołuje, można zadeklarować jako const .

Analogi C # dla końcowego słowa kluczowego

C # można uznać za podobny do Javy, jeśli chodzi o jego funkcje językowe i podstawową składnię: Java ma JVM, C # ma .Net Framework; Java ma kod bajtowy, C # ma MSIL; Java nie obsługuje wskaźników (rzeczywistej pamięci), C # jest taki sam.

Jeśli chodzi o ostatnie słowo kluczowe, C # ma dwa powiązane słowa kluczowe:

  1. Odpowiednim słowem kluczowym dla metod i klas jest sealed
  2. Odpowiednim słowem kluczowym dla zmiennych jest readonly

Należy zauważyć, że kluczowa różnica między słowem kluczowym pochodnym C / C ++ const a słowem kluczowym C # readonly polega na tym, że const jest oceniany w czasie kompilacji, podczas gdy readonly jest oceniany w czasie wykonywania, a zatem może mieć wyrażenie, które jest obliczane i naprawiane dopiero później (w czasie wykonywania).

Bibliografia