Introduzione
Una delle caratteristiche principali di Java è quella della portabilità del codice che grazie all'astrazione della JVM permette di eseguire senza modifiche lo stesso programma in codice
bytecode, su ogni tipo di piattaforma che supporti la macchina virtuale definita da Sun.
Tale portabilità è intrinseca alla natura del linguaggio, per cui in teoria è sufficiente utilizzare Java come piattaforma di sviluppo per ottenere una applicazione multipiattaforma.
In realtà le cose non sono così immediate e possono insorgere problemi legati ad una non corretta mentalità di programmazione multipiattaforma: non si tratta infatti di carenze del linguaggio o di bug della JVM, ma piuttosto di problematiche derivanti dalle diversità che sussistono fra le varie architetture su cui si vuole eseguire la propria applicazione Java.
Sun ha pensato di affrontare il problema definendo il 100% Pure Java (100PJ) Program: tale progetto prevede, tra le altre cose, il rilascio della documentazione relativa all'analisi del problema, l'organizzazione di corsi, e la definizione di alcuni meccanismi per la certificazione 100PJ Application.
Il processo di certificazione prevede in caso di applicazioni conformi con lo standard 100%PJ la possibilità di loghizzare il pacchetto e la registrazione negli archivi Sun,
potendo usufruire da parte della casa di forme di trattamento privilegiate.
Al momento è presto per dire se tale iniziativa avrà successo o meno, ma anche se l'operazione è sicuramente interessante, credo che in buona parte possa essere considerata come l'ennesima trovata pubblicitaria, un po' sullo stile del logo Pentium inside o dei PC marchiati per Win 95. Tutti questi stemmi e controstemmi in alcuni casi si sono rivelati utili, ma molto più spesso è stato il mercato a dettare le regole: che ne dite di un PC non compatibile con Win 95? C'è bisogno di avere lo stemma attaccato?
Il sito relativo al programma 100PJ è, un tipico esempio di quello che in genere definisco, forse senza troppa originalità, bel-web (non cercate di analizzare il significato di bel in inglese scompattando le varie lettere, significa semplicemente ed ironicamente bello): buona parte del sito ha una bella grafica, titoli accattivanti ma in definitiva è un po' inconcludente, ed i concetti sono ripetuti più volte in varie parti dei documenti ipertestuali.
La parte più interessante è contenuta nel cookbook: come suggerisce il titolo si tratta di una serie di ricette per il programmatore al fine di evitare di intraprendere strade pericolose.
Nella prima parte di questo articolo sono riportate le regole principali per progettare una applicazione 100PJ; successivamente sono analizzati i principali problemi che possono portare a codice non portabile e le tecniche per risolvere il problema. Dove possibile sono riportate anche le eventuali soluzioni alternative per scavalcare il problema.
In questa prima parte dell'articolo parleremo dei problemi legati alla portabilità, mentre nella seconda parte, che verrà pubblicato il mese prossimo, parleremo degli aspetti relativi alle applet ed ai Java Beans, sperando che nel frattempo Sun rilasci la documentazione relativa, attesa per la metà del mese di maggio. Verrà inoltre analizzata la suite di programmi che Sun mette a disposizione per testare le proprie classi Java.
Definizione di 100% Pure Java
Il manuale del cuoco provetto riporta una serie di consigli su come evitare di incorrere in situazioni non portabili. La documentazione originale riporta una parte introduttiva sui concetti elementari legati alla portabilità: nella maggior parte dei casi si tratta di concetti ormai noti anche ai neofiti, per cui penso di tralasciare questa sezione.
Portabilità e Purity
Il primo punto interessante è quello legato alla definizione di portabilità: allo stato attuale della tecnologia, la diffusione di architetture diverse fra loro, rende alquanto difficile dare una definizione univocamente valida relativamente al concetto di codice portabile.
Si potrebbe ad esempio dire che portabile è quel programma che produce gli stessi risultati su architetture diverse, ma in realtà tale definizione è alquanto imprecisa, se si pensa ad esempio ad una routine che semplicemente ricava il nome del sistema operativo e lo stampa a video. Per il tipo di obiettivo prefissato si potrebbe dire che si tratta di codice portabile per antonomasia, eppure a rigore porta a risultati differenti su ogni tipo di piattaforma diversa. Anche se l'esempio è banale, si capisce come non sia semplice proporre una definizione universalmente valida del concetto di codice portabile.
Visto che in definitiva la portabilità è legata alla funzionalità di una applicazione, Sun propone il concetto di Purity: visto che il significato di tale termine è intuitivo, per evitare imbarazzanti traduzioni, lo userò in lingua originale.
Secondo tale termine il problema della portabilità viene considerato guardando dal basso in alto dalla interfaccia della piattaforma (connessione JVM \<-\> hardware), piuttosto che dal punto di vista dell'utente.
La purity non è perfettamente equivalente alla portabilità, ma è un buon modo per garantirne la presenza. In modo molto diplomatico viene tralasciato il problema di fornire l'esatta definizione di purity, ed invece viene descritta una procedura per accertare se il codice in questione risponde alle caratteristiche di purity.
Il progetto 100PJ prevede una serie di test da effettuare sulla base di regole proposte da Sun e che verranno esposte in questo articolo. Vediamo adesso di analizzare precisamente le regole principali per costruire una applicazione 100PJ secondo le indicazioni fornite da Sun: per ogni aspetto sono proposte le varie soluzioni, e dove possibile la strada alternativa da seguire.
Regola numero zero: autocontenimento
- Primo requisito che ovviamente un programma deve soddisfare per la purity è la caratteristica di autocontenimento: non si devono infatti avere riferimenti a risorse esterne al set di classi che compone l'applicazione.
Un programma 100PJ deve dipendere solo dalla piattaforma Java. L'implementazione di una struttura client server non pregiudica il vincolo di autocontenimento, anzi permette di semplificare le cose: un client può essere 100PJ anche se il server non segue tale filosofia ed è scritto in un altro linguaggio.
Regola numero uno: utilizzazione del linguaggio Java
- Ovviamente tutto il pacchetto deve essere scritto in Java che grazie alla sua caratteristica principale di astrazione permette di svincolarsi dalla particolare implementazione hardware utilizzata. Infatti, grazie alla JVM, si può far uso di questa potente caratteristica che però ovviamente è solo una ipotesi necessaria e non sufficiente.
Regola numero due: no ai metodi nativi
- Anche se i vantaggi che si possono trarre dalla implementazione di metodi nativi sono non indifferenti, primo fra tutti l'aumento delle prestazioni, si viola in tal modo il requisito fondamentale di far uso esclusivamente della JVM: viene a mancare il concetto di astrazione e tutte le principali caratteristiche di Java come ad esempio il meccanismo del garbage collector, o gli automatismi di class-net-loading.
Inoltre l'uso di puntatori e riferimenti diretti a risorse della macchina, pregiudicano ogni aspetto di sicurezza tanto importante in Java: le conseguenze derivanti da un uso scorretto o disonesto delle risorse sono cosa ormai ben nota, per cui non mi dilungherò oltre nell'analisi di questi aspetti.
Regola numero tre: usare le specifiche delle API
- Se si fa riferimento alle specifiche delle interfaccie del JDK fornito da Sun permette di disporre di un gran numero di servizi completamente compatibili col 100PJ, senza bisogno di avventurarsi in percorsi pericolosi autonomi.
Regola numero quattro: non fare riferimento alle specifiche a basso livello delle API
- La regola precedente può sembrare ovvia, ma è estremamente importante: le specifiche non documentate ufficialmente sono divulgate solo agli sviluppatori low level e non sono pensate per il la realizzazione di programmi client della JVM (applet, applications,...). Inoltre in tal caso non si ha la garanzia che il codice prodotto sia compatibile con le versioni successive della JVM.
Un altro sottile modo per infrangere il limite della portabilità è fare riferimento alle interfaccie delle classi per la creazione di GUI relativamente al package java.awt.peer. Queste interfaccie sono documentate solo per gli implementatori di oggetti AWT, e quindi per i poveri mortali conviene tenersi alla larga da tali ambiti. Un programma portabile utilizza la AWT, non la implementa od estende. In piena era di OOP e di Internet credo che ormai questo concetto sia ben assimilato da tutti, anche se tutti abbiamo avuto, prima o poi, l'irrefrenabile e mostruosa tentazione di reinventare ogni volta la ruota.
Regola numero cinque: usare librerie 100PJ
- Ovviamente per garantire la massima compatibilità con 100PJ anche le librerie utilizzate dovranno essere sicure e controllate oppure facenti parte del JDK. In tal caso si può avere la certezza che non verranno introdotte impurezze nell'applicazione finale.
Considerazioni relative alle applet e JavaBeans
Anche se le problematiche di Purity analizzate fin ora relativamente alle application sono ripetibili per le applet e per i JavaBeans, bisogna tener conto del fatto che una applet viene eseguita all'interno di un browser, e che quindi prendere in considerazione i vincoli dettati dalle varie implementazioni del security manager ed i protocolli di comunicazioni usati in Applet/AppletContext.
Discorso analogo può esser fatto per i JavaBeans per i quali bisogna fare ulteriore attenzione relativamente ai protocolli di comunicazione ed alla sicurezza dell'ambiente che li ospita.
Al momento Sun non ha ancora ufficialmente rilasciato nessun documento ufficiale in merito a questo argomenti, anche se si attende qualcosa per il mese di Maggio 1997.
Ricette per massimizzare la portabilità
Vediamo adesso di esporre alcuni trucchi per evitare di incorrere in implementazioni poco portabili: cercheremo dove possibile di fornire la soluzione al problema e/o una strada alternativa per scavalcare il punto critico. La schematizzazione riportata nella documentazione della Sun è molto chiara, per cui mi scuseranno i lettori più critici se anch'io seguirò tale schema a scapito di originalità.
Problema: meccanismo di scheduling dei thread
Il meccanismo di scheduling dei thread può differire a seconda della piattaforma utilizzata, per cui una applicazione che faccia affidamento esclusivamente sul valore di priorità per sincronizzare gli eventi e gli accessi a risorse condivise, può restituire risultati diversi a seconda del' ambiente utilizzato. In questo caso non si può parlare di bug veri e propri, ma semplicemente di imprevedibilità dei risultati: in ogni ambiente il programma si comporta in maniera corretta ma tipica di quella piattaforma, per cui risulta difficile individuare queste problematiche.
Soluzione: anche se Java permette di evitare di preoccuparsi dei dettagli legati al coordinamento del lavoro parallelo, solo attraverso l'implementazione manuale di una specifica politica di organizzazione dei thread si ha la garanzia di evitare di incorrere in soluzioni pericolose. Per chi volesse approfondire tali argomenti, si suggerisce la lettura di Concurrent Programming in Java (di Doug Lea Addison Wesley 1997 ISBN 0-201-6958-2), che affronta tali aspetti in modo specifico per Java.
Alternativa: una soluzione bruta al problema potrebbe essere quella di sincronizzare tutti i thread, in modo da forzare il flusso del programma nelle cosiddette zone critiche. Questa soluzione anche se non molto elegante, permette di individuare eventuali anomalie di progettazione, trasformando una silente ed errata interpretazione della JVM del flusso delle operazioni, in un deadlock: si tratta di una soluzione del tipo alla caccia dei bug, e richiede ulteriori modifiche al codice quando si individuano problemi.
Maggiori chiarimenti sulla programmazione multithread si possono trovare nell'articolo Threading Programming: introduzione teorica alla programmazione multithreading in Java apparso in [](../../01/index.htm)
Problema: uso di codice nativo
Uno degli aspetti fondamentali della natura di Java è il concetto di astrazione, che permette la ormai famosa Write Once, Run Anywhere .
Anche se l'accesso a basso livello alle risorse della particolare piattaforma utilizzata sono in netto contrasto con questo principio, a volte si rende impossibile non seguire tale strada: tale necessità, ad esempio, scaturisce quando si ha la necessità di aumentare l'efficienza, o quando si deve poter disporre di una certa risorsa di sistema non disponibile nella JVM.
La prima raccomandazione che mi viene di suggerire è quella di ridurre al minimo indispensabile tali sezioni, verificando eventualmente se una strada alternativa non possa in qualche modo risolver i problemi. Ad esempio alcune particolari architetture distribuite (C/S o 3-Tier) possono aumentare enormemente le prestazioni e le potenzialità.
Soluzione: se proprio non potete evitare di utilizzare codice nativo, ecco cosa fare per ridurre al minimo i danni
- Definire un semplice protocollo di comunicazione per ottenere un servizio dal sistema, e quindi utilizzante tale protocollo di comunicazione dal vostro client 100PJ.
- Confinate le chiamate al codice nativo in un metodo di una classe e preoccupatevi di implementare una versione di tale metodo e quindi di tale classe per ogni piattaforma disponibile dl JDK: ovviamente questa è solo una soluzione patchwork, e richiede un costante aggiornamento del codice da voi generato per supportare eventuali nuove piattaforme della JVM. Purtroppo in alcuni casi non è possibile accedere a tali servizi a basso livello come ad esempio nelle JavaStation.
Nel caso in cui si debba usare codice nativo, si implementi almeno un meccanismo di fallback che permetta di lanciare del codice equivalente o di scorta nel caso in cui la routine di accesso low level fallisca per incompatibilità: questo meccanismo può essere fatto cacciando l'eccezione UnsatisfiedLinkError, informando eventualmente l'utente della problematica insorta.
Si tenga comunque conto del fatto che in ogni caso anche utilizzando il fallback il programma sarà sempre di difficile certificazione 100PJ. e sicuramente meno portabile di uno scritto interamente in Java.
Problema: uso di exec.
Per risolvere i problemi legati all'invocazioni di applicazioni eseguibili esiste un unico modo: non fate mai uso di chiamate di tal tipo.
Si tenga presente che la java.lang.Runtime.exec non è affatto portabile, che non tutte le piattaforme mettono a disposizione applicazioni eseguibili e che, infine, non sempre è disponibile lo standard input o output.
In ogni caso sicuramente l'applicazione compilata non è portabile, o semplicemente il formalismo di denominazione non è univoco per tutte le piattaforme.
Problema: reflection.
- Questa nuova caratteristica introdotta col JDK 1.1 aggiunge enormi potenzialità al linguaggio, ma contemporaneamente introduce un certo livello di incertezza relativamente a cosa il programma farà. Method.invoke può essere usato con tutti i metodi, inclusi quello che possono avere problemi di portabilità.
Soluzione: siate prudenti - figliuoli ;).
Problema: uso diretto della AWT
- Come detto in precedenza il protocollo per la comunicazione diretta con tali classi è dipendente dalla piattaforma. Rimanere sempre sul lato client dell'AWT.
Problema: path e filename
L'uso diretto di nomi di file e di path concatenati con il nome del file porta sicuramente a problemi di portabilità.
Soluzione: usare sempre la classe File per ogni riferimento al filesystem, ed usare le proprietà di sistema, come ad esempio il carattere path separator, per eseguire tutte le operazioni di I/O.
Inoltre una soluzione molto portabile è quella di usare un file dialog per lasciare all'utente la responsabilità di sbagliare. Intercettare ogni tipo di eccezione legata ad errori di sintassi dei nomi è buona norma.
Problema: fine linea
- Anche se il problema è piuttosto banale, può portare a fastidiosi inconvenienti nella gestione di dati.
Soluzione: usare println in output o il carattere platform dependent line.separator. In input usare i metodi readLine legato terminazione delle linee ma
Problema:esecuzione da linea di comando
- L'uso di System.in, System.out, o System.err non è sicura a causa della loro mancanza in certe implementazioni dei concetti di stream. Inoltre anche se l'interpretazione dei parametri da linea di comando è lasciata a carico del programmatore, la sintassi e le convenzioni possono essere leggermente differenti a seconda di dove viene eseguito il programma.
Soluzione E' sicuramente preferibile usare interfaccie grafiche per interagire con l'utente ove possibile, o perlomeno predisporre in alternativa anche tale soluzione.
Per l'interpretazione dei comandi, la soluzione migliore sarebbe non usare parametri a linea di comando, ma in certi casi risulta essere l'unica soluzione attuabile, come ad esempio nel caso di esecuzioni batch.
Si può ovviare al problema usando la convenzione POSIX, che offre una notevole standardizzazione.
In alternativa si prenda sempre in considerazione l'uso di una GUI o di un file, dettagliatamente documentato, contente i parametri operativi. Nel caso che si usino applet si può fare largo uso del tag \<PARAM\> facilmente gestibile.
Problema:internationalization
- Il poter distribuire una applicazione in ogni parte del mondo, specie se l'applicazione in esame è una applet, porta inevitabilmente a dover risolver problematiche legate alle differenti convenzioni linguistiche.
Soluzione: come è ben noto dalla versione 1.1 del JDK mette a disposizione i meccanismi di internationalization e localization, che risolvono buona parte dei problemi. Attenzione non tutte le piattaforme rendono disponibile il set di UNICODE, per cui usate tale set di caratteri solo in una politica di localizzazione, o nel testo ottenuto dall'utente. La documentazione ufficiale i merito la si può trovare presso <http://java.sun.com/products/jdk/1.1/docs/guide/intl>.
Problema: aspetto delle GUI
L'aspetto di un form o di un dialog di input varia fortemente in dipendenza della piattaforma e del settaggio del device grafico (es pixel o numero di colori del monitor). Si possono avere grandi differenze nella posizione e nella dimensione dei vari oggetti della AWT. Ogni posizione hard-coded porta inevitabilmente ad una imposizione da parte del programmatore che può dare risultati a volte inaspettati su certi sistemi.
Soluzione
Anche se negli ultimi tempi stanno proliferando tool di sviluppo Visual qui e Visual là, si ricordi che una delle nuove caratteristiche Java sono i cosiddetti layout manager, ma per la loro difficile messa a punto spesso si preferisce posizionare gli oggetti con tre mosse di mouse.
La programmazione visuale è senza dubbio molto comoda, e permette di creare interfaccie grafiche dall'aspetto sicuramente più accattivante. Uno dei compiti all'interno di MB è, fra le altre cose, di curare la grafica, e nello svolgere tale lavoro cerco sempre di dare un bell'aspetto alla rivista rimanendo però nei vincoli del limitato html. A che serve una bella grafica se non tutti la possono vedere, o se in alcuni casi rende scomoda la lettura? Personalmente preferisco sempre affidarmi alla stupidità del layout manager del html, compensando le varie lacune con un grosso sforzo di fantasia. Spero di non risultare troppo presuntuoso ed anzi se qualcuno di voi avesse da criticare qualcosa sull'impaginazione ecco la mia mail: puliti@infomedia.it.
In definitiva direi di non fare troppo affidamento ai magici ambienti di sviluppo visuale, ed il consiglio è quello quindi di tener ben presente il tipo di target che volete dare alla vostra applicazione, e di scegliere di conseguenza la strada da seguire. Chi volesse avere maggiori chiarimenti sui layout manager può leggere l'articolo di Marie Alm in [](../../../1996/10/index.htm).
Sempre per mantenere la portabilità grafica è bene ricavare gli attributi grafici di sistema attraverso routine apposite: per mezzo di chiamate a metodi come java.awt.Toolkit.getScreenSize(), o java.awt.SystemColor class si possono parametrizzare le applicazioni in funzione dell'ambiente in cui operano.
Per i font attenzione al fatto che solo alcuni sono multipiattaforma, e dal JDK 1.0 al 1.1 ci sono state delle variazioni. Alla luce di questa breve parentesi, chi di voi mi sa dire come mai ho scelto questo tipo di font per MB?
Problema: incompatibilità legate ai nomi dei file
- Si possono verificare problemi non banali a causa delle limitazioni che alcune piattaforme impongono sulla definizione dei nomi dei file.
Le restrizioni maggiori sono dovute a:
- Lunghezza del nome dei file (si possono avere incompatibilità fra la semplice associazione che Java instaura fra nomeclasse e nomefile e la convenzione che il particolare sistema impone sul nome dei file);
- Sistemi non case sensitive: Java è un linguaggio case sensitive, ma nei sistemi in cui non c'è distinzione fra lettere maiuscole e minuscole, ci possono essere problemi se si usano classi che differiscono solo per un carattere minuscolo maiuscolo. Anche se questo problema può essere facilmente risolto utilizzando una saggia politica di denominazione, bisogna tenerne comunque conto.
- Non tutte le piattaforme supportano correttamente il codice UNICODE
- Alcuni sistemi assegnano significati particolare a certi file come LPT o con:
- Soluzione A parte il problema della lunghezza dei file, si può ovviare a tutti questi inconvenienti con una saggia e predefinita politica di denominazione delle classi e dei relativi file: credo che sia semplicemente autolesionista dover risolvere i problemi dovuti alla presenza di una MiaClasse e una miaclasse.
Il problema della definizione dei nomi dei file può essere comunque eliminato utilizzando un file .JAR se si utilizza la versione 1.1 del JDK, oppure un .ZIP se si lavora con la 1.0.
Soluzioni alternative
Abbiamo parlato in più punti di implementare soluzioni alternative da lanciare in funzione dell'ambiente in cui si esegue il programma. Sicuramente questo aumenta le dimensioni del codice, ed il relativo sforzo per garantire la 100PJ, ma garantisce ovviamente maggiori garanzie.
Per lanciare la routine adatta in funzione del sistema utilizzato si può ad esempio utilizzare i valori restituiti da java.class.version o os.name.
Eventualmente bisogna decidere dove e come lanciare l'alternativa: se prevenire è meglio che curare si può implementare una tecnica di prevenzione utilizzando, in anticipo i valori restituiti dalle routine precedenti; tale strategia può essere utilizzata se l'applicazione svolge compiti mission critical o ad esempio deve gestire dati importanti in ambienti distribuiti. Se invece non necessitano particolari attenzioni e protezioni, intercettare le eccezioni è la soluzione più semplice da implementare e da gestire.
Test
Abbiamo fin ora preso in considerazione le tecniche per evitare a priori i problemi legati alla programmazione multipiattaforma. La fase successiva prevede eventualmente il test dell'applicazione per mezzo di routine appositamente rilasciate da Sun per verificare la Purity del programma.
Conclusione
Concludiamo qui questa prima parte. Il mese prossimo parleremo della suite di test 100PJ, e delle nuove specifiche rilasciate da Sun: ricordo a tal proposito che la documentazione attualmente rilasciata da Sun è in continuo aggiornamento e modifica, per cui al momento in cui leggerete tale articolo sarà possibile riscontrare alcune incompatibilità con quanto rilasciato in rete.
Per chi volesse approfondire gli argomenti trattati ecco un alcuni indirizzi utili
per maggiori informazioni sull'uso di Java
novità per gli sviluppatori
lista dei licenstari Java
- <http://java.sun.com/nav/licensee-list.html>
informazioni generali sulla piattaforma Java
sito dedicato alla suite di certificazione
- <http://www.suntest.com>
ulteriori informazioni relativamente alla iniziativa 100PJ si possono trovare presso
- <http://java.sun.com/100percent>
