JavaRush /Blog Java /Random-PL /Przypisywanie i inicjalizacja w Javie
Viacheslav
Poziom 3

Przypisywanie i inicjalizacja w Javie

Opublikowano w grupie Random-PL

Wstęp

Głównym celem programów komputerowych jest przetwarzanie danych. Aby przetwarzać dane, trzeba je w jakiś sposób przechowywać. Proponuję zrozumieć, w jaki sposób dane są przechowywane.
Przypisywanie i inicjalizacja w Javie - 1

Zmienne

Zmienne to kontenery przechowujące dowolne dane. Przyjrzyjmy się oficjalnemu samouczkowi firmy Oracle: Deklarowanie zmiennych składowych . Zgodnie z tym samouczkiem istnieje kilka typów zmiennych:
  • Pola : zmienne zadeklarowane w klasie;
  • Zmienne lokalne : zmienne w metodzie lub bloku kodu;
  • Parametry : zmienne w deklaracji metody (w podpisie).
Wszystkie zmienne muszą mieć typ zmiennej i nazwę zmiennej.
  • Typ zmiennej wskazuje, jakie dane ta zmienna reprezentuje (tzn. jakie dane może przechowywać). Jak wiemy, typ zmiennej może być pierwotny (prymityw ) lub obiektowy , a nie pierwotny (nieprymitywny). W przypadku zmiennych obiektowych ich typ jest opisany przez konkretną klasę.
  • Nazwa zmiennej musi być napisana małymi literami, w formacie wielbłąda. Więcej o nazewnictwie możesz przeczytać w " Zmienne:Nazewnictwo ".
Ponadto, jeśli zmienna na poziomie klasy, tj. jest polem klasy, można dla niego określić modyfikator dostępu. Aby uzyskać więcej informacji, zobacz Kontrolowanie dostępu do członków klasy .

Deklaracja zmiennej

Pamiętamy więc, czym jest zmienna. Aby rozpocząć pracę ze zmienną należy ją zadeklarować. Najpierw spójrzmy na zmienną lokalną. Zamiast IDE, dla wygody skorzystamy z rozwiązania online z tutorialspoint: Online IDE . Uruchommy ten prosty program w ich internetowym IDE:
public class HelloWorld{
    public static void main(String []args){
        int number;
        System.out.println(number);
    }
}
Jak więc widzisz, zadeklarowaliśmy zmienną lokalną z nazwą numberi typem int. Naciskamy przycisk „Wykonaj” i pojawia się błąd:
HelloWorld.java:5: error: variable number might not have been initialized
        System.out.println(number);
Co się stało? Zadeklarowaliśmy zmienną, ale nie zainicjowaliśmy jej wartości. Warto zauważyć, że ten błąd nie wystąpił w czasie wykonywania (tj. nie w czasie wykonywania), ale w czasie kompilacji. Inteligentny kompilator sprawdzał, czy zmienna lokalna zostanie zainicjowana przed uzyskaniem do niej dostępu, czy nie. Dlatego też wynikają z tego następujące stwierdzenia:
  • Dostęp do zmiennych lokalnych można uzyskać dopiero po ich zainicjowaniu;
  • Zmienne lokalne nie mają wartości domyślnych;
  • Wartości zmiennych lokalnych są sprawdzane w czasie kompilacji.
Powiedziano nam więc, że zmienna musi zostać zainicjowana. Inicjowanie zmiennej polega na przypisywaniu wartości do zmiennej. Zastanówmy się więc, co to jest i dlaczego.

Inicjowanie zmiennej lokalnej

Inicjowanie zmiennych jest jednym z najtrudniejszych tematów w Javie, ponieważ... jest bardzo ściśle powiązany z pracą z pamięcią, implementacją JVM, specyfikacją JVM i innymi równie przerażającymi i trudnymi rzeczami. Ale możesz przynajmniej w pewnym stopniu spróbować to rozgryźć. Przejdźmy od prostego do złożonego. Aby zainicjować zmienną, użyjemy operatora przypisania i zmienimy linię w naszym poprzednim kodzie:
int number = 2;
W tej opcji nie będzie żadnych błędów, a wartość zostanie wyświetlona na ekranie. Co się dzieje w tym przypadku? Spróbujmy uzasadnić. Jeśli chcemy przypisać wartość do zmiennej, to chcemy, aby ta zmienna przechowywała wartość. Okazuje się, że wartość musi być gdzieś przechowywana, ale gdzie? Na dysku? Jest to jednak bardzo powolne i może nałożyć na nas ograniczenia. Okazuje się, że jedynym miejscem, w którym możemy szybko i sprawnie przechowywać dane „tu i teraz”, jest pamięć. Oznacza to, że musimy przydzielić trochę miejsca w pamięci. To prawda. Kiedy zmienna zostanie zainicjowana, zostanie na nią przydzielone miejsce w pamięci przydzielonej procesowi Java, w ramach którego nasz program będzie wykonywany. Pamięć przydzielona procesowi Java jest podzielona na kilka obszarów lub stref. To, które z nich przydzieli miejsce, zależy od tego, jakiego typu zmienna została zadeklarowana. Pamięć jest podzielona na następujące sekcje: Heap, Stack i Non-Heap . Zacznijmy od pamięci stosu. Stos jest tłumaczony jako stos (na przykład stos książek). Jest to struktura danych LIFO (Last In, First Out). To znaczy, jak stos książek. Kiedy dodajemy do niego książki, kładziemy je na wierzchu, a gdy je zabieramy, bierzemy tę górną (czyli tę, która była dodawana ostatnio). Uruchamiamy więc nasz program. Jak wiemy, program Java jest wykonywany przez JVM, czyli wirtualną maszynę Java. JVM musi wiedzieć, gdzie powinno rozpocząć się wykonywanie programu. Aby to zrobić, deklarujemy metodę główną, która nazywa się „punktem wejścia”. Do wykonania w JVM tworzony jest główny wątek (wątek). Kiedy wątek jest tworzony, przydzielany jest mu własny stos w pamięci. Ten stos składa się z ramek. Gdy każda nowa metoda zostanie wykonana w wątku, zostanie dla niej przydzielona nowa ramka i dodana na górę stosu (jak nowa książka na stosie książek). Ta ramka będzie zawierać odniesienia do obiektów i typów pierwotnych. Tak, tak, nasz int będzie przechowywany na stosie, ponieważ... int jest typem pierwotnym. Przed przydzieleniem ramki maszyna JVM musi zrozumieć, co ma tam zapisać. Z tego powodu otrzymamy błąd „zmienna mogła nie zostać zainicjowana”, ponieważ jeśli nie zostanie zainicjowana, to JVM nie będzie w stanie przygotować dla nas stosu. Dlatego podczas kompilacji programu inteligentny kompilator pomoże nam uniknąć błędów i zepsucia wszystkiego. (!) Dla jasności polecam superduperowy artykuł: „ Stos i sterta Java: samouczek alokacji pamięci Java ”. Link do równie fajnego filmu:
Po zakończeniu metody ramki przydzielone tym metodom zostaną usunięte ze stosu wątku, a wraz z nimi wyczyszczona zostanie pamięć przydzielona tej ramce ze wszystkimi danymi.

Inicjowanie lokalnych zmiennych obiektu

Zmieńmy ponownie nasz kod na trochę bardziej skomplikowany:
public class HelloWorld{

    private int number = 2;

    public static void main(String []args){
        HelloWorld object = new HelloWorld();
        System.out.println(object.number);
    }

}
Co się tutaj stanie? Porozmawiajmy o tym jeszcze raz. JVM wie, skąd ma wykonać program, tj. widzi główną metodę. Tworzy wątek i przydziela mu pamięć (w końcu wątek musi gdzieś przechowywać dane potrzebne do wykonania). W tym wątku ramka jest przydzielana dla metody głównej. Następnie tworzymy obiekt HelloWorld. Obiekt ten nie jest już tworzony na stosie, ale na stercie. Ponieważ obiekt nie jest typem pierwotnym, ale typem obiektowym. A stos będzie przechowywać jedynie referencję do obiektu na stercie (musimy w jakiś sposób uzyskać dostęp do tego obiektu). Następnie na stosie metody głównej zostaną przydzielone ramki do wykonania metody println. Po wykonaniu głównej metody wszystkie ramki zostaną zniszczone. Jeśli ramka zostanie zniszczona, wszystkie dane zostaną zniszczone. Obiekt obiektu nie zostanie natychmiast zniszczony. Po pierwsze, odniesienie do niego zostanie zniszczone i tym samym nikt nie będzie już odnosił się do obiektu obiektu i dostęp do tego obiektu w pamięci nie będzie już możliwy. Inteligentna maszyna JVM ma do tego swój własny mechanizm - Garbage Collector (w skrócie GC). Następnie usuwa z pamięci obiekty, do których nikt inny się nie odwołuje. Proces ten został ponownie opisany w linku podanym powyżej. Jest nawet film z wyjaśnieniem.

Inicjowanie pól

Inicjalizacja pól określonych w klasie odbywa się w specjalny sposób w zależności od tego, czy pole jest statyczne, czy nie. Jeśli w polu występuje słowo kluczowe static, wówczas pole to odnosi się do samej klasy, a jeśli słowo static nie jest określone, wówczas pole to odnosi się do instancji klasy. Spójrzmy na to na przykładzie:
public class HelloWorld{
    private int number;
    private static int count;

    public static void main(String []args){
        HelloWorld object = new HelloWorld();
        System.out.println(object.number);
    }
}
W tym przykładzie pola są inicjowane w różnym czasie. Pole liczbowe zostanie zainicjowane po utworzeniu obiektu klasy HelloWorld. Jednak pole licznika zostanie zainicjowane, gdy klasa zostanie załadowana przez wirtualną maszynę Java. Ładowanie klas to osobny temat, więc nie będziemy go tutaj mieszać. Warto wiedzieć, że zmienne statyczne są inicjowane, gdy klasa staje się znana w czasie wykonywania. Ważniejsze jest tu coś innego i już to zauważyłeś. Nie określiliśmy nigdzie wartości, ale działa. I rzeczywiście. Zmienne będące polami, jeśli nie mają określonej wartości, są inicjalizowane wartością domyślną. W przypadku wartości liczbowych jest to 0 lub 0,0 w przypadku liczb zmiennoprzecinkowych. Dla wartości logicznej jest to fałszywe. Dla wszystkich zmiennych typu obiektowego wartość będzie równa null (porozmawiamy o tym później). Wydawałoby się, dlaczego tak jest? Ale ponieważ obiekty są tworzone w stercie (w stercie). Praca z tym obszarem odbywa się w środowisku wykonawczym. I możemy inicjować te zmienne w czasie wykonywania, w przeciwieństwie do stosu, dla którego pamięć musi być przygotowana przed wykonaniem. Tak działa pamięć w Javie. Ale jest tu jeszcze jedna funkcja. Ten mały fragment dotyka różnych zakątków pamięci. Jak pamiętamy, dla metody głównej przydzielana jest ramka w pamięci stosu. Ta ramka przechowuje odniesienie do obiektu w pamięci sterty. Ale gdzie w takim razie przechowywana jest liczba? Jak pamiętamy, zmienna ta jest inicjalizowana od razu, zanim obiekt zostanie utworzony na stercie. To naprawdę trudne pytanie. Przed wersją Java 8 istniał obszar pamięci o nazwie PERMGEN. Począwszy od wersji Java 8, obszar ten uległ zmianie i nosi nazwę METASPACE. Zasadniczo zmienne statyczne są częścią definicji klasy, tj. jego metadane. Dlatego logiczne jest, że jest on przechowywany w repozytorium metadanych METASPACE. MetaSpace należy do tego samego obszaru pamięci innej niż sterta i jest jego częścią. Należy również wziąć pod uwagę, że brana jest pod uwagę kolejność deklarowania zmiennych. Na przykład w tym kodzie występuje błąd:
public class HelloWorld{

    private static int b = a;
    private static int a = 1;

    public static void main(String []args){
        System.out.println(b);
    }

}

Co jest zerowe

Jak wspomniano powyżej, zmienne typów obiektów, jeśli są polami klasy, są inicjowane do wartości domyślnych, a ta wartość domyślna ma wartość null. Ale co ma wartość null w Javie? Pierwszą rzeczą do zapamiętania jest to, że typy pierwotne nie mogą mieć wartości null. A wszystko dlatego, że null to specjalne odniesienie, które nie odnosi się nigdzie, do żadnego obiektu. Dlatego tylko zmienna obiektu może mieć wartość null. Drugą rzeczą, którą należy zrozumieć, jest to, że wartość null jest referencją. Odnoszę się również do ich wagi. W tym temacie możesz przeczytać pytanie na temat stackoverflow: „ Czy zmienna null wymaga miejsca w pamięci ”.

Bloki inicjujące

Rozważając inicjalizację zmiennych, grzechem byłoby nie wziąć pod uwagę bloków inicjujących. To wygląda tak:
public class HelloWorld{

    static {
        System.out.println("static block");
    }

    {
        System.out.println("block");
    }

    public HelloWorld () {
        System.out.println("Constructor");
    }

    public static void main(String []args){
        HelloWorld obj = new HelloWorld();
    }

}
Kolejność wyników będzie następująca: blok statyczny, blok, konstruktor. Jak widzimy, bloki inicjujące są wykonywane przed konstruktorem. Czasami może to być wygodny sposób inicjalizacji.

Wniosek

Mam nadzieję, że ten krótki przegląd dał pewien wgląd w to, jak to działa i dlaczego. #Wiaczesław
Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION