Non c'è alcun problema nella creazione di istanze di una classe nella classe stessa. L'apparente problema di pollo o uovo viene risolto in diversi modi mentre il programma viene compilato e quando viene eseguito.
fase di compilazione
Quando una classe che crea un'istanza della stessa viene compilato, il compilatore rileva che la classe ha un circular dependency su se stessa. Questa dipendenza è facile da risolvere: il compilatore sa che la classe è già in fase di compilazione, quindi non tenterà di compilarla nuovamente. Invece, finge che la classe già esistente generi codice di conseguenza.
di run-time
Il problema più grande di pollo-o-uovo con una classe creando un oggetto di per sé è quando la classe non ha nemmeno esisteva ancora; cioè, quando la classe viene caricata. Questo problema viene risolto interrompendo il caricamento della classe in due passaggi: prima la classe è definita e quindi è inizializzata.
Definire significa registrare la classe con il sistema di runtime (JVM o CLR), in modo che conosca la struttura che hanno gli oggetti della classe e quale codice debba essere eseguito quando vengono chiamati i suoi costruttori e metodi.
Una volta definita la classe, viene inizializzata. Questo viene fatto inizializzando i membri statici ed eseguendo blocchi di inizializzazione statici e altre cose definite nella lingua specifica. Ricorda che la classe è già definita a questo punto, quindi il runtime sa quali oggetti della classe assomigliano e quale codice deve essere eseguito per crearli. Ciò significa che non c'è alcun problema a creare oggetti della classe durante l'inizializzazione.
Ecco un esempio che illustra come l'inizializzazione di classe e instanziazione interagiscono in Java:
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();
}
}
Facciamo un passo attraverso il modo JVM sarebbe eseguire questo programma. Per prima cosa la JVM carica la classe Test
.Ciò significa che la classe è prima definita, in modo che la JVM sa che
- una classe denominata
Test
esiste e che ha un metodo main
e un costruttore, e che
- classe
Test
ha due statico variabili, una chiamata x
e un'altra chiamata instance
e
- qual è il layout dell'oggetto della classe
Test
. In altre parole: come appare un oggetto; che attributi ha. In questo caso, Test
non ha attributi di istanza.
Ora che la classe è definita, è inizializzata. Prima di tutto, il valore predefinito 0
o null
viene assegnato a ogni attributo statico. Questo imposta x
a 0
. Quindi la JVM esegue gli inizializzatori del campo statico nell'ordine del codice sorgente. Ci sono due:
- Creare un'istanza della classe
Test
e assegnarlo a instance
. Esistono due passaggi per la creazione dell'istanza:
- La prima memoria è allocata per l'oggetto. La JVM può farlo perché conosce già il layout dell'oggetto dalla fase di definizione della classe.
- Il costruttore
Test()
viene chiamato per inizializzare l'oggetto. La JVM può farlo perché ha già il codice per il costruttore dalla fase di definizione della classe. Il costruttore stampa il valore corrente di x
, che è 0
.
- Imposta variabile statica
x
a 1
.
Solo ora la classe ha terminato il caricamento. Si noti che JVM ha creato un'istanza della classe, anche se non è ancora stata caricata completamente. Hai la prova di questo fatto perché il costruttore ha stampato il valore predefinito iniziale 0
per x
.
Ora che la JVM ha caricato questa classe, chiama il metodo main
per eseguire il programma. Il metodo main
crea un altro oggetto della classe Test
- il secondo nell'esecuzione del programma. Anche in questo caso il costruttore stampa il valore corrente di x
, che ora è 1
. L'uscita completa del programma è:
x=0
x=1
Come potete vedere non v'è alcun problema di pollo-o-uovo: la separazione di classe di carico nelle fasi di definizione e di inizializzazione evita completamente il problema.
E quando un'istanza dell'oggetto vuole creare un'altra istanza, come nel codice qui sotto?
class Test {
Test buggy = new Test();
}
Quando si crea un oggetto di questa classe, di nuovo non vi è alcun problema inerente. La JVM sa come deve essere disposto l'oggetto in memoria in modo che possa allocare memoria per esso. Imposta tutti gli attributi sui valori predefiniti, quindi buggy
è impostato su null
. Quindi la JVM avvia l'inizializzazione dell'oggetto.Per fare ciò deve creare un altro oggetto della classe Test
. Come in precedenza, la JVM sa già come farlo: assegna la memoria, imposta l'attributo a null
e inizia l'inizializzazione del nuovo oggetto ... il che significa che deve creare un terzo oggetto della stessa classe e quindi un quarto, un quinto, e così via, finché non esaurisce lo spazio di stack o la memoria heap.
Non c'è nessun problema concettuale qui a mente: questo è solo un caso comune di una ricorsione infinita in un programma scritto male. La ricorsione può essere controllata ad esempio usando un contatore; il costruttore di questa classe utilizza la ricorsione a fare una catena di oggetti:
class Chain {
Chain link = null;
public Chain(int length) {
if (length > 1) link = new Chain(length-1);
}
}
Finché non si sta creando un 'MyClass 'nel costruttore' MyClass' '(Yay per la ricorsione infinita) non ci sono assolutamente problemi nel farlo. Anche il pattern di design composito è basato su questo. Non capisco davvero di cosa si tratta. Inoltre, 'public void class' non verrà mai compilato. –
So che può essere fatto ma la mia domanda è: non è come se si stesse usando la funzione senza creare la funzione in primo luogo. Come puoi spiegare questo a qualcuno che conosce solo la programmazione funzionale? –
Quale parte del processo di caricamento e istanziazione della classe non è ancora chiara? – Joni