"Immagina di essere uno sviluppatore di applicazioni software, il linguaggio di programmazione che usi è il C o il C++ ( o qualunque altro linguaggio che hai imparato). Usi questo linguaggio da parecchio tempo ma il tuo lavoro non sembra diventare più facile, inoltre negli ultimi anni hai assistito alla crescita di molte architetture hardware incompatibili, ognuna supporta un proprio sistema operativo ed ha una propria interfaccia grafica. Ora tu dovresti affrontare tutto questo per creare le tue applicazioni di lavoro in un ambiente distribuito client-server. La crescita di Internet, del World Wide Web, e del commercio elettronico hanno introdotto una nuova complessità nel processo di sviluppo del software. Gli strumenti che usi per lo sviluppo di applicazioni non sembrano esserti di grande aiuto. Stai ancora affrontando le stesse problematiche; la nuova moda della tecnologia object-oriented sembra aver aggiunto nuovi problemi senza risolvere quelli vecchi. Dici a te stesso e ai tuoi amici, "Ci deve essere un modo migliore" ! Ora c’è un modo migliore - questo è Java ™ programming language environment della Sun Microsystems. Immagina, se vuoi, questo ambiente di sviluppo ...
The Java Language Environment - A White Paper
James Gosling, Henry McGilton
Sun Microsystems Computer Company
Java ha avuto origine come parte di un progetto di ricerca volto a sviluppare programmi per dispositivi elettronici, lo scopo era quello di un software di sviluppo piccolo, riusabile, portabile e distribuito. All’inizio del progetto il linguaggio scelto fu il C++, ma poi una serie crescente di difficoltà portarono alla necessità di creare un linguaggio completamente nuovo. Il risultato è stato un linguaggio che si è dimostrato ideale per lo sviluppo di applicazioni sicure e distribuite. Adatto allo sviluppo di applicazioni in diversi ambienti , dal network al World Wide Web al desktop. La crescita massiccia di Internet e del World Wide Web sta portando ad un modo completamente nuovo di distribuire il software, lo schema tradizionale di distribuzione del software, rilascio di una versione, correzioni, aggiornamenti, e cosi via si stanno dimostrando non validi in un ambiente multipiattaforma ed eterogeneo quale è il network. Java con le sue caratteristiche di essere ad architettura neutrale, portabile, adattabile dinamicamente, si adatta a questo nuovo scenario meglio di altri linguaggi. Un linguaggio rivoluzionario
Esso ha un ambiente di sviluppo orientato agli oggetti pulito ed efficiente, gli è stato dato un aspetto simile al C++ per permettere ai programmatori che già utilizzano questo linguaggio di passare agevolmente a Java. Progettato per creare software altamente affidabile, fornisce ampi controlli in fase di compilazione, seguito da ulteriori controlli in fase di esecuzione. Il linguaggio rappresenta una guida ai programmatori verso l’abitudine a produrre programmi affidabili: gestione automatica della memoria, nessun puntatore da gestire, nessun codice "oscuro". Java è nato per operare in ambiente distribuito, ciò significa che l’argomento sicurezza e di grande importanza. E’ stata dedicata particolare attenzione alla sicurezza sia a livello di linguaggio sia a livello di sistema run-time. Java permette di costruire applicazioni che possono difficilmente essere invase da altre applicazioni. Java supporta applicazioni che saranno adoperate in ambienti eterogenei di rete. In tali ambienti le applicazioni devono essere capaci di eseguirsi su architetture hardware diverse e sistemi operativi diversi. Per risolvere questa diversità di ambienti operativi, il compilatore Java genera i bytecode, un formato di codice intermedio tra il codice ad alto livello e quello macchina, progettati per essere efficientemente trasportati su piattaforme hardware e software diverse. L’architettura neutrale è solo una parte di un sistema veramente portabile. Java fa fare alla portabilità un passo avanti, precisando e specificando la grandezza dei tipi di dati e il comportamento degli operatori aritmetici, i programmi sono gli stessi su ogni piattaforma, non ci sono incompatibilità di tipi di dati attraverso diverse architetture hardware e software. L’ambiente architettura neutrale e linguaggio portabile e conosciuto come Java Virtual Machine (JVM), esse sono le specifiche di una macchina astratta per la quale il compilatore Java genera il codice. Una specifica implementazione della JVM per piattaforme hardware e software specifiche provvede alla realizzazione concreta di questa macchina virtuale. L’implementazione della Java Virtual Machine per una nuova architettura sarà relativamente semplice. Le prestazioni sono sempre da considerare: Java ottiene ottime prestazioni adottando uno schema attraverso il quale l’interprete può eseguire i bytecode alla massima velocità senza necessariamente controllare l’ambiente run-time. Una applicazione automatica, garbage collector, eseguita in background assicura con elevata probabilità che la memoria richiesta sia disponibile. Applicazioni particolari o applicazioni che richiedono grossa potenza di calcolo possono essere scritte in codice nativo della macchina ed interfacciate con l’ambiente Java. L’interprete Java può eseguire i bytecode Java su ogni macchina alla quale l’interprete e il sistema run-time è stato portato. In un ambiente interpretato come il sistema Java la fase di link di un programma è semplice e leggera, il ciclo di sviluppo del software diventa molto più rapido. Le moderne applicazioni di rete come i browser del World Wide Web, hanno bisogno di fare più cose contemporaneamente, la capacità multithreading di Java dà i mezzi per costruire applicazioni con più attività concorrenti: Java supporta il multithreading a livello di linguaggio con aggiunta di sofisticate primitive di sincronizzazione. La libreria del linguaggio contiene la classe Thread e il sistema run-time fornisce i monitor e condizioni di lock. A livello di libreria, in più , il sistema Java è stato scritto per essere sicuro nella gestione del multithreding: delle funzionalità provvedono che le librerie siano disponibili senza conflitti tra thread concorrenti in esecuzione. La compilazione avviene con controlli severi, il linguaggio Java è dinamico nella fase di link; le classi sono collegate solo quando occorre, nuovi moduli di codice possono essere collegati a richiesta da una varietà di sorgenti, anche da sorgenti disponibili attraverso la rete ciò permette l’aggiornamento trasparente di applicazioni. Il sistema base di Java
java.lang
sun.tools.debug La stessa libreria Java è organizzata in questo modo, il livello superiore e denominato java quello successivo comprende nomi come io, net, util, e awt. L’ultimo di questi ha altri sottolivelli che includono il package image e il package peer. Per convenzione il primo livello della gerarchia specifica il nome dell’azienda che ha sviluppato il package. Così i nomi delle classi della Sun Microsystems, che non fanno parte dell’ambiente standard di Java, iniziano tutti con il prefisso sun. Il package standard, java, costituisce un’eccezione a questa regola perché rappresenta la componente centrale del sistema. La Sun Microsystems ha specificato una procedura più formale per la denominazione dei package, destinata ad essere utilizzata in futuro. Il nome del package di livello più alto si riserva ai nomi dei domini, in maiuscolo, di alto livello di Internet (EDU, COM, GOV, IT e così via); questi nomi riservati, formano la prima parte di tutti i nuovi nomi di package; così secondo questa procedura il package sun dovrebbe diventare COM.sun. Nel caso di un’università, si avrebbe un nome simile al seguente: IT.unisa.diiie.nomePackage Poiché viene già garantita l’univocità dei nomi di dominio, questo sistema risolve il problema dei conflitti tra i nomi e in più gli applet e i package creati dai programmatori Java, potenzialmente milioni, sarebbero automaticamente inseriti in una gerarchia. Poiché ogni classe Java si trova solitamente in un file separato, il raggruppamento di classi fornito da una gerarchia di package è analogo al raggruppamento di file in una gerarchia di directory. Il compilatore Java rafforza questa analogia, poiché richiede di creare una gerarchia di directory al di sotto della propria directory di classi, che corrisponda esattamente alla gerarchia dei package, e di inserire ogni classe in una directory con lo stesso nome del package in cui è definita, in questo modo l’interprete Java in fase di esecuzione di un programma cerca le classi nella directory che corrisponde alla gerarchia dei package, che sarà univoca. Nel seguito verranno illustrate le caratteristiche di Java e la ragione della loro importanza.
Mentre mantiene un aspetto simile al C/C++, Java acquista in semplicità dalla rimozione sistematica di alcuni particolari "dubbiosi" del C/C++; il team che ha progettato Java esaminando gli aspetti "moderni" del linguaggio C/C++ ha determinato le caratteristiche che dovevano essere eliminate in un contesto di linguaggio di programmazione orientato agli oggetti. Java segue il C/C++ fino ad un certo punto, ciò porta i benefici di essere familiare a molti programmatori, ma si diversifica per altre cose, al di la dei tipi di dati primitivi discussi qui di seguito, tutto è un oggetto. Anche i tipi di dati primitivi possono essere incapsulati in oggetti. Ci sono solo tre gruppi di tipi primitivi di dati: numerico, booleano e array. Tipi di Dati Numerici
I tipi di dati numerico reale sono float a 32-bit e double a 64-bit. I valori letterali in virgola mobile sono considerati double per default; bisogna eseguire esplicitamente il "cast" a float se si desidera assegnarli a variabili float. Il tipo di dato carattere di Java si sposta dai linguaggio tradizionali. Il tipo char di Java è definito come carattere Unicode a 16-bit. Il set di caratteri Unicode sono valori a 16-bit senza segno definiti nell’intervallo 0 - 65535, adottando lo standard Unicode per questo tipo di dati le applicazioni Java sono disponibili per supportare caratteri internazionali.
Nel linguaggio Java le stringhe sono degli oggetti. Ci sono due tipi di oggetti stringa: la classe String per oggetti stringa di sola lettura (immutabili), la classe StringBuffer per oggetti stringa che possono essere manipolati. Anche se in Java le stringhe sono degli oggetti veri e propri, il linguaggio prevede una sintassi conveniente a trattare le stringhe come se fossero dei tipi primitivi, così quando un letterale stringa compare in un programma, Java crea automaticamente un’istanza della classe String con il valore indicato, inoltre sono comprese nel linguaggio alcune facilitazioni sintattiche per aiutare i programmatori a fare comuni operazioni sulle stringhe, concatenazioni di oggetti String, conversione da altri tipi possono essere fatte esplicitamente, ma Java effettua gran parte di queste operazioni automaticamente per permettere una maggiore chiarezza nella scrittura dei programmi. Gestione della memoria e Garbage Collection
Il modello della gestione della memoria di Java si basa su oggetti e riferimenti ad essi, Java non ha puntatori, tutti i riferimenti alla memoria allocata, in pratica tutti i riferimenti a oggetti, sono "memorizzati" dal sistema Java, quando un oggetto non ha più riferimenti questa è candidato per la garbage collection. Il garbage collector di Java ottiene alte prestazioni sfruttando il comportamento degli utenti che interagendo con le applicazioni software hanno molte pause naturali, il sistema run-time di Java sfrutta queste pause per eseguire il garbage collector in un thread a bassa priorità, il garbage collector riunisce e compatta la memoria non più in uso, aumentando la probabilità che adeguate risorse di memoria siano disponibile quando occorrono. Infine Java è semplice grazie anche alle sue dimensioni contenute; l’interprete Java occupa circa 40 kByte di memoria RAM, escludendo il supporto per il multithreading e le librerie standard, che richiedono altri 175 kByte. La semplicità di Java consente di risparmiare tempo nella codifica e nella ricerca degli errori, dedicandosi maggiormente all’analisi dei problemi e delle soluzioni e alla soddisfazione del cliente. Tale semplicità, inoltre, consente ai programmi Java di funzionare
su computer che offrono modelli di memoria limitata, la quantità
di memoria dell’interprete Java con le sue librerie standard risulta insignificante
se confrontata con altri ambienti e linguaggi di programmazione.
Un linguaggio di programmazione per essere considerato "object oriented" dovrebbe soddisfare almeno le seguenti caratteristiche :
Un oggetto è un modello di programmazione software, esso ha uno stato definito nelle variabili istanze e un comportamento definito dai metodi. I metodi manipolano le variabili istanze per creare nuovi stati o anche nuovi oggetti. La Figura 1.1 è la comune rappresentazione di un oggetto, essa ne illustra la struttura software concettuale. Un oggetto è come una cella con un involucro esterno che si interfaccia con il mondo, è un nucleo protetto dall’involucro.
Figura 1.1 Un oggetto in Java
Le classi
Da una classe è possibile creare una sottoclasse, che eredita tutte le caratteristiche della classe madre (o superclasse) e in genere ne fornisce altre. Per creare una gerarchia si utilizza il meccanismo di eredità singola, il che significa che una classe ha una sola superclasse. Uno dei vantaggi principali del linguaggio Java è che ciascuno dei suoi oggetti è auto-contenuto, pertanto ogni modulo è intrinsecamente riutilizzabile. Inoltre, ciascun modulo è estensibile e ciò significa che i programmatori possono aggiungere nuove procedure e nuove sottoclassi a qualunque oggetto. Quando si dichiara una nuova classe in Java, si possono specificare i livelli di accesso permessi alle variabili istanza e ai metodi. Esistono quattro livelli di accesso. Tre di questi livelli devono essere specificati esplicitamente se si vogliono usare, essi sono public, protected e private. Il quarto livello non ha un nome, esso è chiamato "friendly" ed è il livello di accesso predefinito quando non si specifica nessun livello esplicitamente. Il livello di accesso "friendly" indica che le variabili istanza e i metodi sono accessibili a tutti e soli agli oggetti dello stesso package.
In alcuni casi si desidera creare una variabile condivisa che sia visibile e utilizzabile da tutte le proprie istanze, si tratta di una variabile di classe il cui modificatore deve essere la parola static, di questa variabile ne esiste un’unica copia e tutte le istanze della classe la condividono. Utilizzando static è possibile dichiarare anche metodi di classe. Il modificatore final
Le applicazioni devono poter essere eseguite da ovunque sul network senza precedente conoscenza della piattaforma hardware e software, se i programmatori sviluppano software per una specifica piattaforma, la distribuzione di codice binario attraverso il network che è eterogeneo diventa impossibile. La soluzione che il sistema Java adotta per risolvere il problema della distribuzione del codice è un formato di codice binario indipendente dall’architettura hardware e software, il formato di questo codice binario è ad architettura neutrale. Se il sistema run-time di Java viene reso disponibile per determinate (o tutte) piattaforme hardware e software, le applicazioni scritte in Java possono essere eseguite senza il bisogno di eseguire il "porting" su quelle piattaforme. Byte Code
L’approccio ad architettura neutrale si dimostra utile non solo nelle applicazioni di rete, ma anche nella distribuzione del software su singoli sistemi soprattutto oggigiorno dove il mercato dei computer si diversifica per architetture hardware e software. Usando Java, con l’Abstrach Windows Toolkit, la stessa versione di applicazione può eseguirsi su tutte le piattaforme per le quali e stato reso disponibile l’interprete Java ed avrà l’aspetto interfaccia grafica tipico di quella piattaforma. Il fatto che Java sia neutro rispetto all’architettura contribuisce notevolmente alla portabilità del linguaggio; ma c’è di più, esso specifica la dimensione di tutti i tipi di dati primitivi, per cui il comportamento di questi sarà lo stesso su tutte le piattaforme: byte 8 bit, complemento a due short 16 bit, complemento a due int 32 bi, complemento a due long 64 bit, complemento a due float 32 bit, standard IEEE 754 per i numeri in virgola mobile double 64 bit, standard IEEE 754 per i numeri in virgola mobile char 16 bit, carattere Unicode Queste scelte sono state fatte tenendo presente le architetture dei microprocessori moderni, che essenzialmente condividono queste caratteristiche. Lo stesso ambiente Java è facilmente portabile su nuove architetture e sistemi operativi, in compilatore Java è scritto anch’esso in linguaggio Java, mentre il sistema run-time di Java è scritto in ANSI C con un chiaro riguardo alla portabilità nel senso che non ci sono implementazioni dipendenti. Java è stato progettato per sviluppare software che deve essere robusto, affidabile e sicuro. Il compilatore Java effettua particolari e severi controlli in fase
di compilazione, in modo che ogni errore possa essere individuato prima
che il programma sia eseguito, in Java tutte le dichiarazioni di tipo devono
essere esplicite e non supporta lo stile delle dichiarazioni implicite.
Il linker di Java ripete tutte le operazioni di controllo di tipo per evitare
incongruenze di interfaccia o di metodi.
Il compilatore Java non compila utilizzando riferimenti a valori numerici, esso passa riferimenti simbolici al verificatore di bytecode e all’interprete. L’interprete Java esegue la risoluzione dei nomi quando la classe viene effettivamente collegata, quando il nome è risolto, il riferimento viene scritto come un offset numerico permettendo all’interprete Java di procedere velocemente. La posizione di un oggetto in memoria non è stabilita dal compilatore ma è definita dall’interprete in fase di esecuzione. Nuove variabili istanze e nuovi metodi possono essere aggiunti a una classe senza il bisogno di ricompilare l’intera applicazione. L’esecuzione dinamica in pratica, consente di ottenere veri e propri moduli software plug-and-play. Rappresentazione Run-Time
I thread rappresentano un modo veloce per ottenere elaborazioni simultanee in un ambiente a singolo processo, essi sono anche chiamati processi leggeri o contesti di esecuzione. La libreria di classi Java fornisce la classe Thread che supporta una ricca collezione di metodi per avviare un thread, eseguirlo, fermarlo e per tenere traccia dello stato di un thread. Il supporto per i thread di Java include un sofisticato set di primitive di sincronizzazione basate sull’uso dei monitor. La gestione dei thread in genere e di tipo preemptive, anche se il più delle volte dipende dalla piattaforma sulla quale Java è in esecuzione. Nei sistemi in cui la gestione è non preemptive può accadere che un solo thread acquisisca il controllo del processore, impedendo così ad altri thread di essere eseguiti, in tal caso Java fornisce il metodo yield() che dà, ad un altro thread, la possibilità di essere comunque eseguito. Java supporta il multithreading a livello di linguaggio, attraverso
il sistema run-time e attraverso gli oggetti thread. A livello di linguaggio,
i metodi dichiarati synchronized, all’interno
di una classe, non possono essere eseguiti simultaneamente da più
thread; tali metodi vengono eseguiti sotto il controllo di un monitor
per assicurare che le variabili rimangano in uno stato "consistente". Ogni
classe e ogni oggetto istanziato ha il suo proprio monitor che entra in
gioco quando richiesto. Quando un metodo synchronized
entra in un thread, esso acquisisce un monitor sull’oggetto corrente, il
monitor preclude dall’esecuzione qualsiasi altro metodo synchronized
dichiarato in quella classe; quando il metodo synchronized
ritorna in qualsiasi modo, il suo monitor è rilasciato, allora altri
metodi synchronized dello stesso oggetto
sono liberi per poter essere eseguiti. I monitor Java sono rientranti:
un metodo può acquisire lo stesso monitor più di una volta,
inoltre la libreria Java run-time è thread-safe nel senso
che ogni metodo che potrebbe cambiare il valore di una variabile istanza
è stato dichiarato synchronized,
questo assicura che solo un metodo può cambiare lo stato di un oggetto
in un determinato momento.
Uno dei primi livelli di difesa del compilatore Java è il modello di allocazione della memoria. Prima di tutto, le decisioni in riguardo all’allocazione della memoria non sono prese dal compilatore del linguaggio Java, piuttosto sono rinviate al sistema run-time che è diverso a secondo della piattaforma hardware e software su cui il sistema Java è in esecuzione. Secondariamente, Java non ha più il concetto esplicito di puntatore,
il codice Java compilato ha dei riferimenti alla memoria simbolici, riferimenti
che sono risolti in indirizzi di memoria reali in fase di esecuzione, dall’interprete
Java. I programmatori Java non devono più preoccuparsi di eventuali
riferimenti fuori dalla memoria, anche perché l’allocazione della
memoria e il modello dei riferimenti è completamente opaco al programmatore
ed è controllato completamente da sistema run-time di Java.
Anche se il compilatore Java assicura che il codice Java non viola le regole di sicurezza, quando una applicazione (per esempio un browser del Web) importa dei pezzi di codice Java da qualunque parte, essa non sa se il codice segue le regole di sicurezza imposte da Java: il codice può non essere stato prodotto da un compilatore Java conosciuto. Il sistema run-time di Java parte dal presupposto di non avere fiducia nel codice e sottopone i bytecode a una serie di processi di verifica. I test vanno dalla semplice verifica che il formato del codice sia corretto, a tutta una serie di controlli che verificano se:
Figura .2 Il sistema di controllo e verifica dei bytecode in Java Il verificatore dei bytecode esamina un bytecode per volta, costruendo informazioni riguardanti lo stato dei tipi e verifica che il tipo di ciascun parametro, argomento e risultato sia corretto. Sia il caricatore di che il verificatore non fanno nessuna assunzione sulla sorgente primaria del codice che può essere un sistema locale o il network; il verificatore di bytecode agisce come un "portinaio": esso assicura che il codice passato all’interprete Java sia in buono stato e può essere eseguito senza pericoli di interruzione. Quando il codice viene importato non è permessa l’esecuzione finché non ha superato tutti i testi del verificatore. Una volta che il verificatore ha terminato un importante numero di proprietà sono conosciute:
Il verificatore, quindi, rappresenta la parte cruciale del sistema di sicurezza di Java e il suo buon funzionamento dipende dalla corretta implementazione del sistema di esecuzione. Inizialmente solo la Sun Microsystems ha prodotto i sistemi di esecuzione di Java e questi sono sicuri. Successivamente altre società hanno stretto accordi con la Sun Microsystems è produrranno una loro versione dell’ambiente di esecuzione di Java. In futuro la Sun Microsystems ha intenzione di implementare tecniche di convalida per i sistemi di esecuzione, compilatori e così via, in modo da verificare la sicurezza e il corretto funzionamento. Sicurezza nel caricatore di bytecode
In particolare, il caricatore delle classi non consente mai a una classe proveniente da un name spaces "meno protetto" di sostituire una classe di un name spaces più protetto. Quando una classe è importata dal network essa è messa in un name spaces privato associato con la sua origine. Se fa riferimento a un’altra classe, essa è cercata prima nel name spaces del sistema locale, poi nel name spaces successivo più esterno e così via , così si garantisce anche che in nessun modo una classe proveniente dall’esterno possa sostituire o "ingannare" una classe del sistema Java stesso. In più, le classi di un name spaces non possono richiamare i metodi delle classi di altri name spaces, a meno che queste classi non abbiano dichiarato esplicitamente i metodi come public. Per esempio, le primitive di input/output del sistema di gestione dei file, per le quali occorre essere molto attenti, sono tutte definite in una classe locale di Java, per cui, le classi al di fuori del computer locale non possono nemmeno vedere i metodi di input/output del sistema di gestione dei file. Il caricatore delle classi in sostanza ripartisce il mondo delle classi di Java in gruppi piccoli e protetti, sui quali è possibile fare, in tutta sicurezza, delle assunzioni che saranno "sempre" vere. l gestore della sicurezza
Risulta sempre installata un’istanza di una certa sottoclasse di SecurityManager
come gestore della sicurezza corrente. Essa ha il controllo completo in
riguardo a quali metodi, all’interno di un gruppo ben definito di metodi
"rischiosi", possano essere richiamati da una certa classe. In questo gruppo
ben definito di metodi sono contenute le operazioni di input/output su
file, metodi che creano e utilizzano connessioni di rete sia in ingresso
che in uscita e i metodi che consentono a un thread di accedere, controllare,
e manipolare altri thread.
|