2013-08-21 23 views
22

Co pozwala utworzyć instancję klasy wewnątrz samej klasy?Jak działa tworzenie klasy wewnątrz samej klasy?

public class My_Class 
{ 

     My_Class new_class= new My_Class(); 
} 

wiem, że to jest możliwe i zrobić to sam, ale nie mogę jeszcze zrobić sobie wierzyć, że nie jest to coś w stylu „kto był pierwszy - kura czy jajko” rodzaj problemu. Mogę być zadowolony z otrzymania odpowiedzi, która wyjaśni to z perspektywy programowania, a także z perspektywy JVM/kompilatora. Myślę, że zrozumienie tego pomoże mi wyjaśnić kilka bardzo ważnych koncepcji wąskiego gardła programowania OO.

Otrzymałem kilka odpowiedzi, ale żadne nie są jasne w stopniu, jakiego oczekiwałem.

+2

Tak długo, jak długo nie tworzysz 'MyClass 'w konstruktorze' MyClass' '(Yay dla nieskończonej rekurencji) nie ma absolutnie żadnego problemu. Wzornictwo wzoru kompozytowego jest nawet oparte na tym. Naprawdę nie rozumiem, o co chodzi. Ponadto 'public void class' nigdy się nie skompiluje. –

+0

Wiem, że można to zrobić, ale moje pytanie brzmi: czy to nie jest tak, że używasz funkcji bez tworzenia funkcji na pierwszym miejscu. Jak możesz wyjaśnić to komuś, kto zna tylko programowanie funkcjonalne? –

+0

Która część procesu ładowania klas i tworzenia instancji jest nadal niejasna? – Joni

Odpowiedz

31

Nie ma absolutnie żadnego problemu w tworzeniu wystąpień klasy w samej klasie. Pozorny problem z kurczakiem lub jajkiem jest rozwiązywany na różne sposoby, podczas gdy program jest kompilowany i kiedy jest uruchamiany.

kompilacji

Kiedy klasa, która tworzy instancję sobie jest kompilowany, kompilator uzna, że ​​klasa ma circular dependency na siebie. Ta zależność jest łatwa do rozwiązania: kompilator wie, że klasa jest już kompilowana, więc nie będzie próbowała skompilować jej ponownie. Zamiast tego udaje, że klasa już istnieje, generuje odpowiedni kod.

Run-time

Największym problemem kurczaka lub jajko z klasą tworząc obiekt sam w sobie jest, gdy klasa ma jeszcze nawet nie istnieją; to znaczy, gdy klasa jest ładowana. Ten problem został rozwiązany przez zerwanie ładowania klas na dwa etapy: najpierw klasa jest zdefiniowana, a następnie jest zainicjowana.

Definiowanie oznacza rejestrację klasy w systemie uruchomieniowym (JVM lub CLR), aby wiedział, jaka jest struktura obiektów klasy i jaki kod powinien zostać uruchomiony po wywołaniu jego konstruktorów i metod.

Po zdefiniowaniu klasy następuje jej inicjalizacja. Odbywa się to poprzez inicjowanie elementów statycznych i uruchamianie statycznych bloków inicjalizujących i innych rzeczy zdefiniowanych w danym języku. Przypomnij sobie, że klasa jest już zdefiniowana w tym momencie, więc środowisko wykonawcze wie, jakie obiekty klasy wyglądają i jaki kod powinien zostać uruchomiony, aby je utworzyć. Oznacza to, że nie ma problemu z tworzeniem obiektów klasy podczas jej inicjowania.

Oto przykład, który ilustruje, jak i instancji klasy inicjalizacji współdziałać w Javie:

class Test { 
    static Test instance = new Test(); 
    static int x = 1; 

    public Test() { 
     System.out.printf("x=%d\n", x); 
    } 

    public static void main(String[] args) { 
     Test t = new Test(); 
    } 
} 

Załóżmy, krok po kroku, w jaki sposób JVM będzie uruchamiać ten program. Najpierw JVM ładuje klasę Test.Oznacza to, że klasa jest pierwsza zdefiniowane tak, że JVM wie, że

  1. klasy o nazwie Test istnieje i że ma main metody i konstruktora, a
  2. klasę Test ma dwa statyczne zmienne, jeden o nazwie x i inny o nazwie instance i
  3. jaki jest układ obiektu klasy Test. Innymi słowy: jak wygląda obiekt; jakie ma atrybuty. W tym przypadku Test nie ma żadnych atrybutów instancji.

Teraz, gdy klasa jest zdefiniowana, jest to zainicjowana. Po pierwsze, domyślna wartość 0 lub null jest przypisana do każdego atrybutu statycznego. To ustawia x na 0. Następnie JVM wykonuje inicjatory pól statycznych w kolejności kodu źródłowego. Istnieją dwa:

  1. Utwórz instancję klasy Test i przypisać ją do instance. Aby utworzyć instancję, wykonaj dwa kroki:
    1. Pierwsza pamięć jest przydzielona dla obiektu. JVM może to zrobić, ponieważ zna już układ obiektu z fazy definiowania klasy.
    2. Konstruktor Test() jest wywoływany w celu zainicjowania obiektu. JVM może to zrobić, ponieważ ma już kod konstruktora z fazy definicji klasy. Konstruktor wypisze bieżącą wartość x, która jest 0.
  2. Ustaw zmienną statyczną x na 1.

Dopiero teraz klasa zakończyła ładowanie. Zauważ, że maszyna JVM utworzyła instancję klasy, mimo że nie była jeszcze w pełni załadowana. Masz dowód na to, ponieważ konstruktor wydrukował początkową domyślną wartość 0 dla x.

Po załadowaniu tej klasy przez JVM wywołuje ona metodę main w celu uruchomienia programu. Metoda main tworzy kolejny obiekt klasy Test - drugi w wykonaniu programu. Ponownie konstruktor wypisze bieżącą wartość x, która jest teraz 1. Pełne wyjście programu jest:

x=0 
x=1 

Jak widać nie ma problemu z kurczaka albo jaj: oddzielenie klasy załadunku do definicji i fazy inicjalizacji unika problemu całkowicie.

Co zrobić, gdy instancja obiektu chce utworzyć inną instancję, jak w poniższym kodzie?

class Test { 
    Test buggy = new Test(); 
} 

Po utworzeniu obiektu tej klasy ponownie nie występuje nieodłączny problem. JVM wie, jak obiekt powinien zostać umieszczony w pamięci, aby mógł przydzielić dla niego pamięć. Ustawia wszystkie atrybuty na ich wartości domyślne, więc jest ustawione na null. Następnie JVM rozpoczyna inicjowanie obiektu.Aby to zrobić, musi utworzyć inny obiekt klasy Test. Tak jak wcześniej, JVM wie już, jak to zrobić: alokuje pamięć, ustawia atrybut na null i rozpoczyna inicjowanie nowego obiektu ... co oznacza, że ​​musi utworzyć trzeci obiekt z tej samej klasy, a następnie czwarty, piąty itd., dopóki nie skończy się miejsce na stosie lub pamięć sterty.

Nie ma tutaj problemu koncepcyjnego: jest to zwykły przypadek nieskończonej rekursji w źle napisanym programie. Rekursję można kontrolować na przykład za pomocą licznika; konstruktor tej klasy używa rekursji do sieci obiektów:

class Chain { 
    Chain link = null; 
    public Chain(int length) { 
     if (length > 1) link = new Chain(length-1); 
    } 
} 
+0

Czy to oznacza, że ​​wyrażenia wewnątrz definicji klasy, która tworzy instancję własnej klasy, nie są wykonywane/kompilowane przed innymi wyrażeniami? Powiedziałeś, że klasa jest już zdefiniowana w tym miejscu i to prawda, ale w jaki sposób instancja może uzyskać dostęp do metody, która nie jest zdefiniowana do tego momentu. Czy możesz wyjaśnić swoją odpowiedź w niewielkim stopniu, aby wyjaśnić to komuś, kto zawsze używał języka interpretowanego (co oznacza, że ​​kod wykonuje linię po linii) –

+0

Wszystkie metody, konstruktory i inne elementy są zdefiniowane, gdy klasa jest zdefiniowana, przed jakimkolwiek kodem w klasa jest wykonywana. – Joni

+0

Proszę, wyczyść moje zamieszanie tutaj, czy przypadek klasy własnej klasy również nie jest członkiem klasy w powyższym kodzie? Próbuję oczyścić swoje myśli, Joni. –

-1

Tworzenie instancji obiektu wewnątrz obiektu może prowadzić do StackOverflowError ponieważ za każdym razem, aby utworzyć wystąpienie z tej klasy „Test” ty będzie tworzyć kolejną instancję i kolejną instancję itd. staraj się unikać tej praktyki!

public class Test { 

    public Test() { 
     Test ob = new Test();  
    } 

    public static void main(String[] args) { 
     Test alpha = new Test(); 
    } 
} 
+0

Sformatuj swój fragment kodu, proszę. – leppie

+0

Jest to prawdą tylko wtedy, gdy jest wykonywane w konstruktorze, dla tego samego konstruktora i bezwarunkowo, ponieważ powoduje nieskończoną rekurencję. Jeśli jest to wykonywane poza konstruktorem (powiedzmy, w metodzie 'Clone'), to nie ma zastosowania. Jeśli jest to wykonywane warunkowo, to jest to rekursja, ale nie nieskończona rekursja. – Servy

+0

@Servy, tworzenie instancji * tego samego obiektu klasy w momencie deklaracji spowoduje również nieskończony wyjątek rekurencji i stackoverflow. To nie jest tylko konstruktor. Ten kod * (taki sam jak pytanie) * da wyjątek przepełnienia stosu w czasie wykonywania 'public class My_Class {My_Class new_class = new My_Class();}', a także nie jestem pewien, czy java zachowuje się inaczej, ale w języku C# byłby to wyjątek. – Habib

1

Inne odpowiedzi w większości dotyczyły pytania. Jeśli pomaga to owinąć mózg wokół niego, a może przykład?

Problem z kurczakiem i jajkiem został rozwiązany, ponieważ każdy problem rekursywny jest: podstawowym przypadkiem, który nie powoduje więcej pracy/przypadków/cokolwiek.

Wyobraź sobie, że stworzyłeś klasę, aby automatycznie obsługiwać wywoływanie zdarzeń z wątkami krzyżowymi, gdy jest to konieczne. Bardzo istotne dla wątkowych WinForm. Następnie chcesz, aby klasa ujawniała zdarzenie, które pojawia się, gdy coś się rejestruje lub wyrejestrowuje z obsługi, i oczywiście powinno obsługiwać również wywoływanie wątków krzyżowych.

Możesz napisać kod, który obsługuje go dwa razy, raz dla samego zdarzenia i raz dla zdarzenia statusu, lub napisz raz i użyj ponownie.

Większość zajęć została obcięta, ponieważ nie jest istotna w dyskusji.

public sealed class AutoInvokingEvent 
{ 
    private AutoInvokingEvent _statuschanged; 

    public event EventHandler StatusChanged 
    { 
     add 
     { 
      _statuschanged.Register(value); 
     } 
     remove 
     { 
      _statuschanged.Unregister(value); 
     } 
    } 

    private void OnStatusChanged() 
    { 
     if (_statuschanged == null) return; 

     _statuschanged.OnEvent(this, EventArgs.Empty); 
    } 


    private AutoInvokingEvent() 
    { 
     //basis case what doesn't allocate the event 
    } 

    /// <summary> 
    /// Creates a new instance of the AutoInvokingEvent. 
    /// </summary> 
    /// <param name="statusevent">If true, the AutoInvokingEvent will generate events which can be used to inform components of its status.</param> 
    public AutoInvokingEvent(bool statusevent) 
    { 
     if (statusevent) _statuschanged = new AutoInvokingEvent(); 
    } 


    public void Register(Delegate value) 
    { 
     //mess what registers event 

     OnStatusChanged(); 
    } 

    public void Unregister(Delegate value) 
    { 
     //mess what unregisters event 

     OnStatusChanged(); 
    } 

    public void OnEvent(params object[] args) 
    { 
     //mess what calls event handlers 
    } 

} 
2

Najważniejsze zawsze widzę siebie tworzenia instancji od wewnątrz klasy, kiedy staram się odwołać wyrobom statycznego w statycznym kontekście, na przykład gdy robię ramkę do gry lub cokolwiek innego, używam głównej metody do faktycznego ustawienia ramki. Możesz go również użyć, gdy jest coś w konstruktorze, który chcesz ustawić (jak poniżej, sprawię, że moja ramka JFrame nie będzie równa zeru):

public class Main { 
    private JFrame frame; 

    public Main() { 
     frame = new JFrame("Test"); 
    } 

    public static void main(String[] args) { 
     Main m = new Main(); 

     m.frame.setResizable(false); 
     m.frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); 
     m.frame.setLocationRelativeTo(null); 
     m.frame.setVisible(true); 
    } 
}