
// Allocazione dinamica: il problema dell'aliasing

public class Esempio12 {
	static int n;
	static int[] a,b; 
	
	// Modifica un vettore in una posizione
	static void modifica(int[] v, int indice, int valore) {
		v[indice] = valore;
		indice = 45; // Irrilevante! Non ha effetto una volta terminata la procedura!
		// Anche un assegnamento v=... sarebbe irrilevante.
		// Solo v[...]=... ha un effetto visibile dopo la fine della procedura.
	}
	
	public static void main(String[] args) {
		// Alloco un vettore
		a = new int[10];
		
		// Inizializzo a
		for (n=0 ; n<a.length ; n++) {
			a[n] = n;
		}
		
		// Non alloco b, ma gli assegno a (!!)
		b = a;
		
		// Ora a e b sono *esattamente* lo stesso vettore.
		// Non sono due vettori distinti che hanno lo stesso valore
		// sulle componenti, ma sono proprio "fisicamente" lo stesso.
		//
		// Osservate questo:
		a[3] = 10;
		System.out.println("b[3] = " + b[3]); // Ora anche b[3] vale 10 (!)
		b[2] = 20;
		System.out.println("a[2] = " + a[2]); // Ora anche a[2] vale 20 (!)
		
		// Questo comportamento è chiamato aliasing: sopra ho usato una "new" sola
		// per creare solo un vettore, e poi ho assegnato il risultato della new
		// a due variabili, che condividono quindi tutte le componenti.
		// Notate la differenza con creare due vettori con le stesse componenti:
		// in tal caso è ovviamente possibile modificarne uno senza influenzare
		// l'altro.
		
		// Nella chiamata di procedura (o funzione) ho un effetto analogo
		modifica(a,5,600);
		System.out.println("a[5] = " + a[5]); // 600
		// Ora a[5] vale 600. Non ho passato alla procedura semplicemente
		// il valore delle componenti del vettore a, ma gli ho passato
		// proprio "l'identità" del vettore appena allocato. In tal modo,
		// se la procedura può modificare le componenti del vettore.
	
		
		
		// *****************************************
		// Cenni sulla garbage collection
		
		// Considerate il programma che segue:
		a = new int[10];
		b = a; // aliasing
		a[0] = 10; // ...
		a = new int[20];
		a[0] = 20; // ...
		// Questo programma riserva prima 10 celle di memoria, le usa, e poi
		// ne chiede altre 20. In questo momento le prime 10 celle sono accessibili 
		// tramite b[0] .. b[9] , mentre a[0] .. a[19] denotano le seconde 20.
		
		// Eseguiamo dunque:
		b = a; // aliasing
		// Ora b[0] .. b[19] denotano le nuove 20 celle.
		// E le prime 10? Al momento sono riservate, ma **non più accessibili** dal programma.
		// Di conseguenza, il programma sta tenendo "occupata" della memoria senza averne
		// effettivamente bisogno.
		
		// Questo farebbe sì che la memoria consumata da un programma cresca
		// sempre con l'uso, ad ogni esecuzione delle new, senza mai potere diminuire.
		// Per programmi complessi, ciò è inaccettabile. Ci sono due soluzioni a questo problema.
		
		// Una soluzione è quella di consentire al programmatore di "rilasciare"
		// la memoria allocata precedentemente, in modo che venga possibilmente riusata
		// dalle new successive, o anche da altri programmi che stanno eseguendo
		// contemporaneamente sul computer. Per esempio il C++ ha un comando
		//      delete b;
		// che serve a questo scopo.
		// Questa è una soluzione semplice dal punto di vista tecnologico, ma che richiede
		// al programmatore di prestare molta attenzione. Infatti, dopo una "delete" nulla
		// impedisce al programmatore di continuare ad usare la memoria:
		//      delete b;
		//      ... // molto dopo
		//      b[5] = 4;
		// Questo assegnamento ha un effetto impredicibile, in quanto va a scrivere in
		// una cella di memoria non più riservata. Le conseguenze sono le stesse di quelle
		// di usare un indice del vettore fuori dall'intervallo di definizione (es: b[-10]).
		// Quindi nei casi fortunati il programma si blocca subito, in quelli più sfortunati
		// si va a sovrascrivere una parte di altro array allocato tra la delete e l'accesso
		// a b[5], o una variabile intera impredicibile, un pezzo di una stringa...
		// Siccome questo richiede al programmatore di assumersi una grossa responsabilità,
		// la maggior parte dei linguaggi moderni usano un'altra soluzione.
		
		// L'altra soluzione è quella di progettare il linguaggio di programmazione in
		// modo che, una volta che una zona di memoria diventa "irraggiungibile" per il programma,
		// dopo un qualche tempo la zona venga liberata automaticamente, come se fosse stata
		// eseguita una "delete".
		// Per esempio, Java ogni "tot" comandi eseguiti ferma l'esecuzione del programma per
		// eseguire una scansione della memoria alla ricerca di zone ormai irraggiungibili.
		// Dopo la scansione, le zone trovate vengono rilasciate, e il programma continua ad
		// eseguire.
		// In questo modo, il programmatore viene liberato dalla responsabilità di dovere rilasciare
		// la memoria, al prezzo di un rallentamento dell'esecuzione del programma.
		// Questa tecnica è nota come "garbage collection" (raccolta rifiuti).
		//
		// È oggetto di forte discussione se questo rallentamento sia significativo o meno.
		// Con opportuni algoritmi per la ricerca delle zone in questione, l'effettivo rallentamento
		// si rivela piuttosto contenuto. Questo è giustificato sia da risultati teorici che da
		// esperimenti "empirici" in vari tipi di programmi.
		//
		// Quello che sembra invece assodato è che, in un programma complesso, è veramente arduo
		// scrivere programmi che usano la "delete" esplicita senza che contengano errori di gestione
		// della memoria (per es. delete mancanti, oppure di troppo).
		
		
		
	}

}
