Vai alla Home Page About me Courseware Federica Living Library Federica Federica Podstudio Virtual Campus 3D La Corte in Rete
 
Il Corso Le lezioni del Corso La Cattedra
 
Materiali di approfondimento Risorse Web Il Podcast di questa lezione

Stefano Russo » 3.Generalità su Java e la programmazione ad oggetti


Sommario degli Argomenti della Lezione

  • Generalità sul linguaggio Java
  • Gestione della memoria
  • Esempio di programma
  • Tecniche di riuso del codice
  • Composizione ed Ereditarietà
  • Polimorfismo e Classi Interne

Il linguaggio Java

  • Generalità
  • Gestione della memoria
  • Esempio di programma

Il Linguaggio Java

Java è stato creato da James Gosling ed altri ingegneri della Sun MicroSystems a partire dal 1991.

L’architettura Java si compone di quattro componenti:

  1. il linguaggio di programmazione;
  2. Application Programming Interface (API);
  3. Java Virtual Machine (JMV);
  4. Java tools a supporto dello sviluppo ed esecuzione di programmi.

Il Linguaggio Java

Il linguaggio di programmazione

Java è un linguaggio:

  • object-oriented;
  • multi-threaded;
  • con gestione delle eccezioni;
  • con garbage collector.

Il Linguaggio Java

Application Programming Interface (API)

Java API è un insieme di librerie standard per poter accedere alle risorse del computer. Le JAVA API presentano una interfaccia standard, e sono implementate invocando gli opportuni metodi nativi offerti dalla macchina su cui sono eseguite.


Il Linguaggio Java

Java Virtual Machine (JMV)

Java tools a supporto dello sviluppo ed esecuzione di programmi.

Il compito della JVM è di caricare i class file ed eseguirli. Viene definito un computer astratto, perché fornisce un’astrazione del sistema operativo e dell’apparec-chiatura hardware della macchina.


Il Linguaggio Java

Java tools a supporto dello sviluppo ed esecuzione di programmi

Java dispone di un insieme di tool a supporto delle fasi di sviluppo di un programma e delle azioni necessarie per la sua esecuzione.
L’insieme dei tool, API e JVM viene spesso definito Java Platform.


Il Linguaggio Java

L’obiettivo alla base della progettazione di Java è la portabilità: si dovrebbe essere in grado di scrivere il programma una sola volta e di poterlo eseguire dovunque (“write once, run everywhere”).

Il codice in C++ non è portabile ovunque, ma va compilato su ogni piattaforma deve essere eseguito!


Il Linguaggio Java

Un apposito compilatore (javac) processa il codice sorgente non in linguaggio macchina specifico della piattaforma su cui girerà il codice, ma in un linguaggio per un processore virtuale detto bytecode.

Ogni JVM dispone di un interprete in grado di tradurre il bytecode in linguaggio macchina ed eseguirlo.


Sviluppo di una applicazione Java

La preparazione e l’esecuzione di un programma Java consiste di 5 fasi:
1) La fase di EDIT

C:\PRG>edit PrimoEsempio.java

Viene creato il file contenente il programma e, successivamente, memorizzato nel disco.
ATTENZIONE
Il nome del file deve coincidere con quello della classe (case sensitive!)
I programmi scritti in Java sono unicamente orientati agli oggetti, di conseguenza tutto il codice deve essere necessariamente incluso in una o più classi.

Per rendere eseguibile un’applicazione Java occorre che una classe faccia da “punto di decollo”, ovvero deve contenere un metodo pubblico main(). Questo è il primo ad essere invocato durante l’esecuzione dell’applicazione.

Sviluppo di una applicazione Java

2) La fase di COMPILAZIONE

C:\PRG>javac PrimoEsempio.java

Il compilatore JAVA traduce il programma nel corrispondente bytecode, il linguaggio compreso dall’interprete java, e lo salva su  disco.


Sviluppo di una applicazione Java

3) La fase di CARICAMENTO

C:\PRG>java PrimoEsempio

Il Class loader carica il bytecode nella memoria principale


Sviluppo di una applicazione Java

4) La fase di VERIFICA del bytecode

Il “bytecode verifier” ha il compito di validare il byte-code e assicurarsi che non si violino i vincoli di sicurezza dell’ambiente JAVA.


Sviluppo di una applicazione Java

5) La fase di ESECUZIONE

L’interprete del byte-code traduce le istruzioni del bytecode nelle istruzioni com-patibili con il processore.


Il modello di sicurezza


Il modello di sicurezza

Prima azione di controllo

Bytecode Verifier: si analizza la sequenza di bytecode e controlla il riferimento ad altre classi. Ad esempio: se una classe utilizza il metodo di un’altra, controlla se questo metodo è pubblico oppure se vengono sforati i limiti di un array. Oppure controlla se una classe che presenta una sottoclasse non presenti il vincolo di non specializzazione.


Il modello di sicurezza

Seconda azione di controllo

Security Manager: speciale classe che può essere implementata dal programmatore che disciplina l’accesso a una risorsa (ad esempio accesso a un file o a una connessione di rete).


Tipi primitivi e Classi

Java opera una distinzione netta tra classi e tipi primitivi, relativamente al modo in cui avviene l’allocazione in memoria:
Quando si dichiara una variabile del tipo intero (int x) vengono subito allocati quattro byte.
Se si utilizza una classe in qualità di tipo per una certa variabile (MyClass c), verrà creata subito una variabile che referenzia l’oggetto (reference) ma non verrà allocata memoria fino alla creazione vera e propria dell’istanza della classe, tramite l’utilizzo dell’operatore new.

Un reference è, dunque, una variabile speciale che tiene traccia di (punta a) istanze di tipi non primitivi. I reference possono tenere traccia soltanto di oggetti di tipo compatibile, ovvero un reference ad un oggetto di tipo MyClass non potrà tenere traccia di oggetti di diverso tipo.

Tipi primitivi e Classi

Nel linguaggio di programmazione C++, sono di comune utilizzo variabili di tipo riferimento: rappresentano dei puntatori costanti alle variabili di cui fanno riferimento, e sono usati per estenderne la visibilità oltre l’ambito in cui sono state definite.
Dichiarando una variabile di tipo riferimento, non è stato allocato nessun oggetto, ma è stato dato un nuovo nome a un oggetto già esistente. Tale variabile, quando viene usata in un’espressione, ha come valore il valore dell’oggetto a cui fa riferimento.

Java si discosta da quanto avviene in C++: i riferimenti puntano solo a oggetti, e non a variabili di tipo primitivo, e non costituiscono dei nomi alternativi. Infatti, sono degli oggetti allocati in area stack che puntano ad aree di memoria allocate nell’heap.

Inoltre, il contenuto di un riferimento non è costante, ma può essere variato a piacimento dal programmatore.

Tipi primitivi e Classi

Supponiamo di istanziare due oggetti di tipo String contenenti entrambi il valore “Pippo”:

String s = new String("Pippo");

String s1 = new String("Pippo").

if(s == s1)_{_  ..._} else { _  ..._}

Quale dei due rami del costrutto if …else viene certamente eseguito?

Le istruzioni che verranno eseguite saranno quelle all’interno dell’else.

Come mai?

Tipi primitivi e Classi

La ragione è legata al fatto che l’operatore “==” esegue la comparazione tra le variabili reference che tengono traccia dei due oggetti e non tra i valori contenuti dagli oggetti stessi.
Mentre tra variabili di tipi primitivi la comparazione è normalmente effettuata tramite l’operatore “==”, per la comparazione tra due istanze di oggetti della stessa classe, sarà necessario, a tale scopo, utilizzare il metodo equals().

String s = new String("Pippo");_String s1 = new String("Pippo");__if(s.equals(s1)
{_  ..._} else { _  ..._}

Tipi primitivi e Classi

C++
int i;
int *pi;
int &ri=i;
pi=new int;

Java
//c è un tipo intero
int i;

I tipi primitivi (boolean, char, byte, short, int, long, float, double):

  • sono sempre allocati nell’area stack (non è possibile allocarli dinamicamente);
  • hanno una dimensione costante al variare dell’architettura sottostante.

… per l’allocazione dinamica (in area heap) si ricorre a delle classi Wrapper (Boolean, Character, Byte, Short, Integer, Long, Float, Double).

Gestione della memoria

Per istanziare un oggetto dinamicamente nell’area heap, si fa ricorso all’istruzione new:
String str = new String("stringa");
Come fare se vogliamo deallocare un oggetto? Esiste in Java una istruzione di delete come in C++?

In JAVA è stato implementato un’apposito “modulo” che, in maniera automatica, recupera la memoria degli oggetti non più utilizzati (cioè che non sono più riferiti)

garbage collector

Quindi in JAVA non esiste più il problema, tipico del C e C++, della “perdita di memoria” dovuto a…
…programmatori un po’ distratti!

Gestione della memoria

Quando un oggetto non è più referenziato dal programma, lo spazio che occupa nell’heap deve essere liberato, così da renderlo disponibile per nuovi oggetti.

Il garbage collector deve rendersi conto di quali oggetti non sono più referenziati e liberarne lo spazio sotteso. Inoltre, deve combattere la frammentazione dovuta alle continue allocazioni e deallocazioni durante il ciclo di vita di un programma.

Vantaggio: Maggiore produttività del programmatore – Garanzie di integrità: un programmatore non può accidentalmente (o maliziosamente) causare un crash della JVM liberando incorrettamente della memoria.

Svantaggio: il garbage collector implica un considerevole overhead che può inficiare le performance del programma sviluppato.

Lo scambio di parametri

A differenza degli altri linguaggi, in JAVA non è possibile scegliere il tipo di scambio di parametri ad una funzione o metodo.

Esiste, invece, una regola fissa:

  • scambio per valore: per tutti i tipi di dati primitivi (int, boolean, char…);
  • scambio per riferimento: per tutti gli oggetti (array, stringhe, oggetti di utente…).

…in effetti, molti puristi amano dire che in Java viene adottato solo lo scambio per valore.

Esempio di programma Java

Persona.java Mostra codice

Prova_Persona.java Mostra codice

Dove, nel codice Persona.java:
Persona (String n) {

nome=new String(n);

}

Costruttore della classe, notare che non è presente alcun distruttore. Se è necessario eseguire delle operazioni prima che l'oggetto sia liberato dal Garbage Collector si deve implementare un metodo finalize().

public String identita() {

return new String(nome);

}

Metodo per ottenere il valore della variabile ‘nome'. Notare che ci crea una copia della variabile.

Esempio di programma Java

Il codice Prova_Persona.java Mostra codice

è il metodo principale che la JVM esegue per lanciare l'applicazione.

static

Un metodo static non richiede l'istanziazione di un oggetto della classe per il suo utilizzo.

Che significare avere una variabile static?

Una variabile static è una variabile che allocata una sola volta indipendentemente dagli oggetti della classe e accessibile da tutti gli oggetti.

final int = 5

Una variabile final è una variabile il cui contenuto è costante.

static final int i = 2;

Una variabile può essere allo stesso tempo static e final... che significa?

E' una variabile allocata una sola volta per tutti gli oggetti, e il cui contenuto è costante

Programmazione OO in Java

  • Tecniche di Riuso del Codice
  • Composizione vs Ereditarietà
  • Visibilità in una gerarchia gen/spev
  • Invocazione metodi di una classe base
  • Polimorfismo e Interfacce
  • Classi Interne

Tecniche di riutilizzo del codice

Una delle caratteristiche più importanti della programmazione orientata agli oggetti (OO, object-oriented) è la possibilità di riuso del codice.

Nei linguaggi procedurali come il C, il riuso è ottenibile copiando codice di precedenti programmi e modificandolo.

Nei linguaggi OO si usano tecniche più raffinate, che creano nuove classi a partire da quelle esistenti senza intaccarle:

  • composizione: inserimento oggetti di classi esistenti in nuove classi;
  • ereditarietà: creazione di una nuova classe come specializzazione di una classe esistente.

Composizione

La composizione si realizza inserendo nella nuova classe dei riferimenti agli oggetti delle classi esistenti.


Composizione

Mostra codice Mostra codice

Il compilatore non istanzia automaticamente gli oggetti per ogni riferimento definito nella classe Macchina. In Java attributi di tipi primitivi sono automaticamente inizializzati a 0, mentre i riferimenti di oggetti sono inizializzati a null.

System.out.println (Macchina()");

I riferimenti sono inizializzati prima che il costruttore viene invocato:

Carrozzeria carrozzeria = new Carrozzeria ();
String s = "Macchina Assemblata";

I riferimenti sono inizializzati nel costruttore:
motore = new Motore();

I riferimenti sono inizializzati prima di essere usati:
ruota1 = new Ruota();
ruota2 = new Ruota()
ruota3 = new Ruota();
ruota4 = new Ruota();

Ereditarietà

L’ereditarietà consente di definire nuove classi ereditando le caratteristiche offerte da classi esistenti. In altre parole si realizzano relazioni tra classi di tipo gen-spec: una classe base, realizza un comportamento comune ad un insieme di entità, mentre le classi derivate (sottoclassi) realizzano comportamenti specializzati rispetto a quelli della classe base.

NB: Nell’ereditarietà non siamo vincolati ad implementare gli stessi metodi della classe base, ma possiamo aggiungerne di nuovi.


Ereditarietà

Sintassi:

class Veicolo : public Oggetto {...};

class Veicolo extends Oggetto {...};

Il legame gen-spec tra due classi è rappresentato per mezzo della parola chiave extends, secondo la seguente sintassi:

{nome_classe_derivata} extends {nome_classe_base}

In Java, a differenza del C++, non è possibile esprimere la modalità di derivazione (public, protected, private). Inoltre, esiste un qualificatore final con cui si dichiara una classe non ulteriormente derivabile.

Es. final class Ferrari [extends Macchina] {..}

Ereditarietà

Un’altra differenza con il C++ è che Java implementa soltanto l’ereditarietà singola, pertanto ciascuna classe in Java può avere
una sola superclasse.

Inoltre, ogni classe in Java è automaticamente e implicitamente una specializzazione della classe Object, da cui eredita un insieme di funzionalità, che può ridefinire:

  • toString(), fornisce una rappresentazione String dell’oggetto su cui viene invocato: l’implemen-tazione base è nome_classe@codice_hash_oggetto;
  • clone(), fornisce un clone dell’oggetto corrente. Il tipo di ritorno è Object quindi va sempre effettuato un casting;
  • equals(), nella sua versione base, verifica se due riferimenti sono uguali, non se due oggetti hanno attributi uguali.

Ereditarietà

Quando si richiama un metodo su un oggetto, l’interprete ne cerca dapprima la definizione nella classe dell’oggetto stesso; se non la trova, cerca nella superclasse e risale la gerarchia fino a trovarla.

Nel caso esistano più metodi con lo stesso nome, tipo restituito e stessi parametri (firma) viene eseguito il metodo trovato per primo.


Ereditarietà

Consideriamo il seguente codice:

Mostra codice

Tune ha come parametro di ingresso oggetti di tipo Instrument, come mai non  ho un errore trasmettendo un tipo Wind?

UPCASTING

La classe Wind estende quella Instrument, quindi un oggetto di tipo Wind è anche di tipo Instrument.

Ereditarietà

Mostra codice Mostra codice

Dove nel secondo codice

protected int esami;
protected int matricola;
protected String facolta;

rappresentano al visibilità dei metodi/attributi

super(nome,sesso,eta)

rappresenta l'inizializzazione della classe base

super.ChiSei();

è l'invocazione metodi della classe base.

Visibilità in una gerarchia gen-spec

Che visibilità hanno i metodi/attributi di una classe base rispetto ad una classe derivata? Dipende dalla parola chiave che li precede:

  • Accesso privato (private): visibilità solo dall’ambito della stessa classe;
  • Accesso di default (friendly): visibilità solo dall’ambito della stesso package;
  • Accesso protetto (protected): visibilità dalle sottoclassi e dallo stesso package;
  • Accesso pubblico (public): visibilità completa.

Visibilità in una gerarchia gen-spec

Applicativi Java è organizzabile in compartimenti che si definiscono attraverso i “package”.
Solo all’inizio del file java si può specificare l’appartenenza ad un package:
package mypackage;
public class MyClass { // . . .

Per utilizzare MyClass dall’esterno del package si possono usare le due modalità:
mypackage.MyClass m = new mypackage.MyClass();
Oppure:
import mypackage.*;
// . . .
MyClass m = new MyClass();

i package sono strutturati in uno schema ad albero:
Es: com.sun.media.rtp
Senza definire uno specifico package una classe verrà inserito nel default package.

Inizializzazione classe base

Quando si crea un oggetto della classe derivata, questo contiene al suo interno un sotto-oggetto della classe base.

Il sotto-oggetto va opportunamente inizializzato per mezzo della chiamata al costruttore (super(..)); Tale chiamata può essere:

  • implicita: se la classe base ha un costruttore a zero argomenti.

Invece deve essere:

  • esplicita: se il costruttore della classe base prevede almeno un argomento.

Quando non viene esplicitata l’invocazione al costruttore della classe base, il compilatore java automaticamente lo pone in testa al costruttore della classe derivata.

Invocazione metodi della classe base

Quando si implementa una classe derivata, è possibile che un suo metodo abbia la stessa firma di un metodo della classe base, e questo viene implementato invocando il metodo della classe base ed aggiungendo altra logica applicativa.

All’interno del metodo chiSei() di studente non è possibile invocare lo stesso metodo di Persona scrivendo “chiSei()“.

Per risolvere questo problema si ricorre alla parola chiave super, quindi in chiSei() di Studente troviamo correttamente:
super.chiSei()
per riferirci al chiSei() di Persona.


Composizione vs Ereditarietà

È prassi comune di combinare composizione ed ereditarietà. Bisogna considerare che mentre il compilatore forza l’inizializzazione delle classi base, non lo fa per l’inizializzazione dei riferimenti delle classi componenti. Quindi bisogna ricordarsi di inizializzare sempre tali riferimenti.

Qual è la differenza tra le due tecniche e quando preferire una rispetto all’altra?

La composizione implementa la relazione “has-a”, ovvero una classe vuole solo incorporare le funzionalità di un’altra;
L’ereditarietà implementa la relazione “is-a”, ovvero una classe non solo vuole incorporare le funzionalità di un’altra, ma vuole presentarsi all’utente con lo stesso vestito (interfaccia).

Polimorfismo

Per polimorfismo si intende la proprietà di una entità di assumere forme diverse nel tempo.

A è un vettore di tipo Figura, composto di N istanze di Triangolo, Rettangolo, Quadrato;
Si consideri il seguente ciclo di istruzioni:

for i = 1 to N do

A[i].disegna();

L’esecuzione del ciclo richiede che sia possibile determinare dinamicamente l’imple-mentazione della operazione disegna() da ese-guire, in funzione del tipo dell’oggetto A[i].


Polimorfismo

Il collegamento tra la chiamata di un metodo con il corpo del metodo stesso prende il nome di binding.
Quando questa operazione si verifica prima che un programma viene eseguito si parla di early binding o statico. Quando avviene a tempo di esecuzione, si dice late binding o dinamico.
Come detto precedentemente, Java realizza sempre late binding a meno che al metodo non è anteposta a parola chiave final, il cui compito è prevenire che un utente possa realizzare l’”override” di un metodo.

Vantaggio del polimorfismo: supporto della proprietà di estensibilità di un sistema: si minimizza la quantità di codice che occorre modificare quando si estende il sistema, cioè si introducono nuove classi e nuove funzionalità.

Esempio di polimorfismo


Esempio di polimorfismo


Polimorfismo

Aggiungendo una nuova classe alla gerarchia …

… modificando Aeroporto …

… il codice esegue senza errori.

Polimorfismo

La classe Velivolo può fungere solo da interfaccia per specificare come gli utenti devono utilizzare le classi derivate, ma non il loro comportamento. Pertanto, Velivolo deve essere solo un’interfaccia, ovvero un insieme di funzioni dummy che poi ogni derivata deve implementare.

Velivolo è implementabile come una classe astratta: almeno uno dei suoi metodi sono astratti, ovvero hanno solo dichiarazioni ma non un corpo.

Non è possibile istan-ziare oggetti di classi astratte o con alcuni metodi astratti.


Polimorfismo


Polimorfismo

Le classi astratte non sono l’unica soluzione che Java mette a disposizione per realizzare interfacce comuni a un insieme di classi. “Interface” è a tutti gli effetti una classe astratta, con metodi astratti e attributi, che sono implicitamente static e final.

Usando delle classi astratte, il programmatore ha a disposizione solo l’eredità singola (una classe eredita solo da una classe astratta), con le inter-facce può realizzare anche un’implementazione multipla (una classe può implementare più interfacce).

I metodi di un’interfaccia hanno automaticamente visibilità public, e la classe che li implementa deve dichiarare una visibilità public, altrimenti per default sono “friendly”.

Polimorfismo

Mostra codice

Dove implements è la parola chiave per indicare che la classe elicottero implementa l'interfaccia Velivolo.

E' corretto indicare il codice così come è scritto?

string peso; ERRORE!!!

Nelle interfacce gli attributi sono implicitamente static e final, quindi vanno inizializzati -> string peso="100"

Mostra codice

Polimorfismo

È possibile derivare un’interfaccia a partire da un’altra interfaccia, ma questo legame di derivazione è solo a livello di specifica dei metodi, e non di implementazione (un’interfaccia non può estendere una classe con metodi concreti perché un’interfaccia deve avere tutti i suoi metodi astratti, anche se alcuni di essi sono ereditati da terze parti).

Esempio:

public interface Remote{...}
...
import java.rmi.Remote;
public interface ISquareRoot extends Remote{
double calculateSquareRoot(double aNumber);
}

Classi interne

In Java è possibile posizionare la definizione di una classe all’interno di un’altra, realizzando quello che prende il nome di classe interna. Ciò consente di raggruppare classi che sono logicamente correlate e di controllare la loro visibilità.

Mostra codice

Nel comando

lass Destination {

private String label;
Destination(String whereTo) {label = whereTo;}
String readLabel() {return label;}

}

non c'è nessuna differenza nell'uso di Destination, bisogna solo ricordarsi che i nomi sono innestati nella classe Document, quindi se si vuole fare riferimento alla classe interna al di fuori di Document (o in metodi statici), la sintassi è Document.Destination.

Classi interne

Ogni classe interna ha completa visibilità degli attributi e dei metodi della classe che la contiene.

Mostra codice

Dove

doc.new Destination("Tanzania"); é un'istanza di una classe interna  ed è ottenibile solo a partire da un'istanza della classe esterna.

Document.this.title; nel corpo di un metodo di una classe interna si può fare esplicito riferimento ai membri della classe contenente mediante la notazione: NomeClasseContenente.this.nomeMembro.

Con private possiamo disciplinare la visibilità della classe interna con le parole chiave public, private e protected.

Infine Document.Destination dec = doc.new Destinatio (Tanzania); non rende possibile istanziare un oggetto della classe interna.

Classi interne

Le classi interne non possono dichiarare metodi statici. Ma è possibile che una classe interna sia static.

Un’istanza di una classe interna implicitamente mantiene un riferimento all’oggetto della classe esterna che lo ha creato. Questo non è vero per classi interne statiche:

  • non si ha bisogno di un oggetto della classe esterna per creare un oggetto della classe interna;
  • non è possibile accedere ad un oggetto della classe esterna da un oggetto della classe interna static;
  • può contenere metodi e attributi statici.

Normalmente non è consentito inserire codice all’interno di una interfaccia, ma una classe interna statica può essere parte di una interfaccia.

Classi interne

Una classe può essere definita all’interno di un metodo, oppure di un blocco di codice (nel ramo di un costrutto if…else): classe interna locale.

Mostra codice

Le classi interne ai metodi hanno completa visibilità di tutte le variabili locali al metodo. Non è possibile specificare esplicitamente l'ambito di visibilità delle classi interne ai metodi – esse sono private ai metodi che le includono per definizione.

Dove:

final serve per rendere visibile all'interno della classe interna il parametro passato al metodo, questo deve essere dichiarato final, altrimenti si ha un errore a tempo di compliazione.

Classi interne

Perché usare classi interne?

Ogni classe interna può ereditare da un’altra classe o implementare un’interfaccia indipendentemente da quanto fatto dalla classe esterna.

Questo fornisce una ulteriore soluzione (oltre al ricorso alle interfacce) al problema dell’impossibilità della derivazione multipla presente in Java.

I materiali di supporto della lezione

Bruce Eckel, “Thinking in Java” capitolo 6-7-8

J. Cohoon e J. Davidson, “Java – Guida alla Programmazione” paragrafo 7.4 e capitolo 9

  • Contenuti protetti da Creative Commons
  • Feed RSS
  • Condividi su FriendFeed
  • Condividi su Facebook
  • Segnala su Twitter
  • Condividi su LinkedIn
Progetto "Campus Virtuale" dell'Università degli Studi di Napoli Federico II, realizzato con il cofinanziamento dell'Unione europea. Asse V - Società dell'informazione - Obiettivo Operativo 5.1 e-Government ed e-Inclusion

Fatal error: Call to undefined function federicaDebug() in /usr/local/apache/htdocs/html/footer.php on line 93