Programmare in J2ME: filosofia e tecnica

Questo articolo svelerà i segreti della programmazione in Java 2 Micro Edition.

J2ME è una versione di Java creata dalla Sun per i dispositivi con ridotte capacità computazionali rispetto agli odierni elaboratori. Comunque tali dispositivi sono più potenti dei calcolatori di 15 anni fa, sui quali erano già disponibili gli strumenti dell'informatica moderna: interfacce grafiche, fogli di calcoli, elaboratori di testo, ... Il linguaggio J2ME è ampiamente adottato nei cellulari di ultima generazione per i giochi oppure per creare quelle applicazioni che soddisfino il bisogno di mobilità imposto dalla società moderna.

In questo articolo si spiegheranno tutte le fasi necessarie per realizzare i propri programmi in J2ME. Come esempio un pratico verrà sviluppato un convertitore di valuta.

Per una migliore comprensione è necessaria una conoscenza di base dei linguaggi di programmazione.

Introduzione

In quanto segue è stata riportata un'introduzione, sia pur breve ed imprecisa, al linguaggio Java. Essa ha lo scopo di dare al lettore un'idea delle evoluzioni tecnologiche che hanno portato alla nascita di J2ME.

L'idea base di Java

L'idea alla base di Java è di creare un processore virtuale in grado di eseguire un insieme di istruzioni ottimizzato per semplificare la compilazione dei programmi scritti in linguaggio Java.

In teoria quindi sarebbe possibile compilare anche programmi scritti in altri linguaggi per questo processore virtuale, proprio come accade per i normali compilatori per la famiglia di processori x86 compatibili. In pratica, però, sono state inserite delle limitazioni come una serie di controlli durante l'esecuzione che non permettono ai programmi di compiere operazioni “pericolose” come, ad esempio, accedere direttamente alla memoria. Ciò limita notevolmente il numero di linguaggi compilabili.

Questo processore si chiama Java Virtual Machine (JVM) e non esiste realmente. In pratica esso viene realizzato con un programma in esecuzione su un processore e sistema operativo reali che simula il funzionamento della JVM.

Il linguaggio Java è stato ottenuto prendendo un sottoinsieme del linguaggio C++ ed aggiungendo nuove funzioni presenti in altri linguaggi. In particolare sono state evitate tutte le funzioni che possono creare problemi se non vengono adoperate correttamente, come ad esempio l'accesso diretto alle locazioni di memoria, e sono state aggiunte nuove funzioni come la liberazione automatica dello spazio di memoria occupato dalle istanze degli oggetti non più adoperati. Inoltre al linguaggio di base sono state aggiunte una serie incredibilmente copiosa di librerie che possono soddisfare tutte le possibili esigenze dei programmatori. Compilando un programma Java si ottengono una serie di files con estensione .class che possono essere eseguiti dalla JVM. Per semplicità è possibile raccogliere tutti i .class in un unico file .jar che rappresenta il file eseguibile del programma.

Creando una JVM per una data piattaforma, cioè un dato processore con relativo sistema operativo, è possibile eseguire i programmi Java compilati. In pratica esistono JVM per quasi tutte le piattaforme esistenti. Quindi l'idea centrale è di realizzare e compilare un programma in Java una volta e per tutte. A questo punto esso potrà essere eseguito in ogni piattaforma che disponga di una JVM. L'idea inizialmente era nata per essere adoperata nei micro controllori grazie alle ottime caratteristiche di affidabilità del linguaggio Java. In pratica non ebbe alcun successo ma per fortuna il grande successo di Internet dove un insieme di eterogeneo elaboratori si collegava ad una rete comune diede nuovo slancio a questa idea. Adesso che un insieme eterogeneo di telefoni cellulari dilaga in tutto il mondo cosa c'è di meglio di Java per scriverne i programmi?

Per concludere questo paragrafo si volevano sottolineare alcuni punti fondamentali. L'idea di avere un processore simulato per ottenere i benefici su riportati è molto più antica del progetto Java. D'altro canto il linguaggio Java è una sublime fusione di programmazione orientata agli oggetti, semplicità e completezza delle librerie. Tanto è vero che recentemente sono stati creati dei compilatori che compilano il linguaggio Java per processori specifici come ad esempio la serie Intel x86, per migliorare le prestazioni a discapito della portabilità. Segno che ci sono dei programmatori che apprezzano tanto il linguaggio Java da volerlo adoperare per le proprie creazioni anche senza avere l'esigenza che queste vengano eseguite su piattaforme diverse.

Il problema principale di Java è che per eseguire un programma in realtà se ne devono eseguire due: la JVM e il programma cui siamo interessati. Ciò aumenta di molto la richiesta di memoria e diminuisce la velocità di esecuzione. Per ovviare a questo problema sono state create delle JVM Just In Time (JIT) che sostanzialmente compilano al volo il codice per la JVM nel codice nativo del processore ottenendo un notevole aumento di prestazioni nell'esecuzione dei cicli. Inoltre esistono delle aziende che hanno creato processori con un linguaggio molto simile alla JVM. Un esempio è la ARM che standardizza il linguaggio assemblato e la struttura dei processori che si fregiano della compatibilità con ARM. Nelle ultime versioni di questo standard sono disponibili tre modalità di esecuzione del codice: a 32 bit, a 16 bit ed a 8 bit. Nell'esecuzione ad 8 bit si adopera un sottoinsieme delle istruzioni della JVM. Quindi per eseguire un programma Java basta passare in modalità 8 bit ed implementare le poche funzioni mancanti come la liberazione dello spazio occupato dagli oggetti non più adoperati. In questo modo si incrementa tantissimo la velocità di esecuzione. Di meno si è fatto per l'occupazione di memoria in quanto la tendenza attuale è di creare dispositivi con capacità di memoria sempre maggiori, conseguentemente il problema è meno sentito.

Java 2 Mobile Edition

La Sun ha cominciato con J2SE (Standard Edition) e J2EE (Enterprise Edition) che sostanzialmente differiscono per le librerie presenti. J2ME (Java 2 Mobile Edition) rappresenta una rivoluzione rispetto a queste soluzioni. Infatti per semplificare la compilazione e l'esecuzione su dispositivi più semplici dei PC, alcune librerie di base del linguaggio sono state cambiate. Inoltre sono state aggiunte una serie di librerie specifiche per tali dispositivi.

Configurazioni

In J2ME la JVM per eseguire le sue funzioni si appoggia su quella che viene definita una configurazione. Queste configurazioni implementano le funzioni relative alla parte principale del linguaggio. Attualmente la configurazione più comune è la CLDC (Connected Limited Device Configuration) 1.0. Per esempio questa non implementa l'uso dei numeri in virgola mobile (tipi float e double). Alcune di queste limitazioni sono state superate dalla versione 1.1 che si comincia a trovare nei cellulari di fascia più alta.

Profili

I profili sono le librerie adoperate dai programmatori per le utilità e per l'ingresso ed uscita dei dati. Purtroppo la quantità di librerie presente in J2ME è un insieme ridotto rispetto alla versione di J2SE ma in genere sufficiente per scrivere i propri programmi senza troppa fatica. Attualmente l'insieme di librerie più comune è la MIDP (Mobile Information Device Profile) 1.0 ma nei nuovi cellulari si comincia a trovare la 2.0. Entrambe si appoggiano su CLDC 1.0 quindi in un cellulare è possibile trovare una qualsiasi delle quattro combinazioni. Ovviamente tutte queste classi sono state scritte con i commenti in formato Javadoc che consente di ottenere un'ottima documentazione automaticamente. La suddetta documentazione viene in genere installata automaticamente nella cartella /doc dell'emulatore.

Esiste anche un piccolo numero di librerie di terze parti che, per esempio, implementano il calcolo in virgola fissa utile con CLDC 1.0 oppure funzioni grafiche per realizzare semplicemente istogrammi o grafici a torta. Per adoperare queste librerie è sufficiente aggiungerle al jar contenente il proprio programma. L'unico svantaggio ad adoperare tali librerie è la licenza, in genere limitativa o a pagamento, e che incrementano le dimensioni del codice. La dimensione di un programma J2ME solitamente non supera i 100K byte.

In fine sono sempre presenti anche un insieme di API non standard da affiancare al MIDP per poter adoperare al meglio le funzioni proprio del cellulare oppure di sfruttarne caratteristiche peculiari. E' vivamente consigliabile di non adoperarle perché pregiudicano la portabilità che è proprio l'opposto di quello che si vuole ottenere adoperando Java. Inoltre si consideri il notevole sforzo fatto dalla Sun per realizzare la specifica del MIDP 2.0 che implementa tutte quelle funzioni di utilizzo comune che col MIDP 1.0 potevano solo essere ottenute con le librerie proprietarie.

Ecco una rappresentazione grafica di quanto detto:






In quanto segue sono stati riassunti i pacchetti principali disponibili in J2ME con la descrizione delle classi più importanti:

javax.microedition.midlet

Questo è il pacchetto fondamentale, contiene una sola classe soprannominata MIDlet. Una Midlet è un'applicazione che viene eseguita dal cellulare. Questa applicazione ha tre punti di ingresso, ovvero metodi pubblici: startApp(), pauseApp(), destroyApp(boolean). Il telefono richiamerà l'uno o l'altro a seconda che voglia avviare, mettere in pausa o terminare l'applicazione. Ecco i possibili stati di un'applicazione:






java.lang

Questo pacchetto contiene le classi fondamentali del linguaggio Java.

Object è la classe dalla quale ereditano tutte le altre. Quindi tutte le classi condividono le proprietà di Object, perciò è utile tenere a mente i suoi metodi principali:

Ci sono poi le classi corrispondenti ai tipi fondamentali del linguaggio come: Boolean che rappresenta il tipo primitivo boolean, Byte è un byte e così via per tutti i primitivi. La presenza di queste classi torna utile quando si vuole adoperare una utilità, come i vettori di dimensione variabile, per memorizzare un tipo primitivo, come gli interi. Infatti tali utilità lavorano su Object e quindi possono accettare un oggetto di tipo Integer ma non direttamente un tipo scalare int.

java.util

In questo pacchetto si trovano tutte le classi di utilità del linguaggio. Purtroppo questa classe risulta abbastanza scarna rispetto a quella equivalente in J2SE. Ecco l'elenco delle classi più importanti:

java.io

Questo pacchetto contiene le classi principali per l'ingresso ed uscita attraverso flussi di dati.

Le classi precedenti sono fondamentali per accedere ai RecordStore che manipolano esclusivamente vettori di byte (si veda javax.microedition.rms).

javax.microedition.io

E' un pacchetto fondamentale perché consente alla MIDlet di interagire col mondo esterno. Ecco un elenco delle principali classi che contiene:

javax.microedition.lcdui

E' il pacchetto che contiene le classi per creare l'interfaccia grafica. Ci sono due categorie di classi fondamentali: Screen e Canvas.

javax.microedition.rms

L'unica classe contenuta nel pacchetto è RecordStore. Rappresenta una serie di record di bytes. I record sono numerati sequenzialmente con un indice intero che comincia da 1. Essi possono essere memorizzati sul dispositivo permanentemente per essere ricaricati dal programma quando serve. Inoltre andrebbero preservati anche se viene tolta la batteria. Questa classe è fondamentale se desideriamo che l'applicazione memorizzi dei dati che poi richiamerà alla prossima esecuzione.

I dispositivi J2ME

Sempre più spesso gli apparecchi portatili di recente produzione sono creati per poter eseguire applicazioni J2ME. Ciò è dovuto al fatto che questo linguaggio è ben visto dai produttori perché interagisce con il sistema ad un livello abbastanza alto da renderne quasi impossibile il blocco. Evitare blocchi del sistema operativo è fondamentale nei piccoli dispositivi portatili che conservano tutto in RAM e quindi perderebbero tutte le informazioni in seguito ad un azzeramento totale richiesto per riavviare il sistema operativo. Infatti a differenza degli elaboratori da scrivania teoricamente i sistemi operativi di questi strumenti non dovrebbero mai essere riavviati. Tuttavia chi ha un palmare sa benissimo quanto spesso sia necessario compiere un azzeramento totale per colpa di un'applicazione mal scritta. Ciò accade perché le applicazioni scritte in linguaggi come il C possono accedere direttamente alla memoria. Quindi basta un piccolo errore per scrivere in una zona che appartiene all'area di memoria di un altro programma. Solo i migliori sistemi operativi, come Linux, garantiscono che un errore in un programma non pregiudichi la stabilità di tutto il sistema.

Questo problema è sentito anche nei cellulari dove non si può permettere che un programma comprometta l'integrità del sistema. Ormai tutti i cellulari di ultima generazione hanno una JVM compatibile con J2ME. Le prestazioni e le performance fanno invidia ai computer di dieci anni fa e sono in continuo aumento. I nuovi apparecchi UMTS hanno bisogno di processori più potenti rispetto ai cugini GSM e questo si riflette positivamente anche sulle prestazioni della JVM.

Per i palmari che usano PalmOS è disponibile un'applicazione che implementa una JVM. Altri palmari con funzione di telefono come Black Berry consentono nativamente l'esecuzione di programmi in J2ME. Non ultimo il palmare Zaurus che adopera Linux come sistema operativo consente l'esecuzione applicazioni J2ME.

Installazione dell'ambiente Java

Esistono diversi strumenti per programmare in J2ME. In quanto segue si descriverà l'installazione di quelli della Sun, ciò non toglie che esistono valide alternative come ad esempio quelle di Borland o IBM.

Strumenti di sviluppo Sun

Tutto il software necessario si trova nel sito della Sun per Java: java.sun.com. E' possibile scaricare gratuitamente i componenti richiesti. L'ambiente di sviluppo richiede la registrazione al sito per ottenere una licenza d'utilizzo senza scadenza. E' consigliabile leggere attentamente la licenza dei programmi che si scaricano per capire esattamente qual è il campo consentito di utilizzo.

Inizialmente è necessario installare l'SDK (Software Development Kit) per J2SE. E' possibile sia installare il file binario auto scompattante, sia un pacchetto .rpm. Il file scaricato avrà il nome seguente: j2sdk-1_4_2_06-linux-i586-rpm.bin ovvero j2sdk-1_4_2_06-linux-i586.bin. Nel primo caso, dopo averlo eseguito (se necessario attivare il permesso di esecuzione con chmod 777 j2sdk-1_4_2_06-linux-i586-rpm.bin) creerà un file .rpm. A questo punto, dopo essere diventato amministratore (root), basta il comando rpm -ivh j2sdk-1_4_2_06-linux-i586.rpm per installare l'SDK per J2SE. Il percorso di Java sarà /usr/Java/j2sdk1.4.2_06/bin/java. Nel secondo caso basta eseguire il file .bin nella cartella /usr per creare automaticamente i file necessari.

L'ambiente di sviluppo Sun si chiama Java Studio Mobility 6, brevemente JSM6. Per installarlo basta scaricare Jstudio_M04q3-linux-ml.bin. Si scompatta come il precedente e non richiede installazione. E' consigliabile scompattarlo nella directory /opt ed è necessario essere amministratori (root). All'inizio dell'estrazione verrà chiesto il percorso di Java che è quello ricavato nel paragrafo precedente. Infine si deve dare a tutti gli utenti il permesso di eseguire e modificare i files, viceversa JSM6 non funzionerà correttamente. Per far ciò basta scrivere chmod 777 -R *.

Per avviare JSM6 bisogna eseguire il file seguente: /opt/SUNWjstudio/Mobile04q3/bin/runide.sh. Nel caso si voglia creare una icona per avviare il programma si possono adoperare le icone in: /opt/SUNWjstudio/Mobile04q3/bin/icons/. All'avvio di JSM6 verrà chiesto un codice di licenza per l'utilizzo illimitato nel tempo, viceversa si potrà provare per un mese. Se ci si è registrati correttamente al sito della Sun, questo codice sarà inviato automaticamente via email.

Nel caso di Windows la procedura è identica. Si deve scaricare ed installare rispettivamente l'SDK J2SE e il JSM6. In questo caso sono dei file eseguibili auto installanti. L'icona di avvio viene creata automaticamente sotto il menù start programmi Java Studio Mobility.

L'ambiente di sviluppo contiene tutto il necessario per compilare ed eseguire programmi in J2ME. Sono anche presenti diversi emulatori di cellulari.

Ecco un'immagine JSM6 mentre si sviluppa il programma scelto come esempio per questo articolo:




Altri strumenti

A JSM6 è possibile affiancare un offuscatore di codice. Per far ciò basta scaricare lo strumento Proguard dal sito Source Forge. Quindi copiare il file proguard.jar nella cartella /opt/SUNWJSM6/Mobile04q3/obfuscators.

Lo scopo di un offuscatore è di rendere difficile la de-compilazione di un programma Java. Infatti i nomi degli attributi, metodi, classi etc. vengono memorizzati nell'eseguibile ottenuto dalla compilazione del programma in quanto, per una caratteristica detta riflessività del linguaggio Java, è necessario preservarli. Quindi è semplice creare uno strumento che restituisca il codice sorgente a meno dei commenti. L'offuscatore parte dal programma compilato e comincia a sostituire ogni variabile con le lettere dell'alfabeto. Un ulteriore vantaggio derivante da questa procedura è che la taglia del file viene ridotta ad almeno il 70% dell'originale. Questo effetto torna utile nei telefoni cellulari dove lo spazio disponibile per i programmi è molto piccolo, dell'ordine di 100Kbyte.

A questo punto è necessario andare alla sezione degli sviluppatori del sito del produttore del proprio telefono cellulare. Dopo la consueta registrazione è possibile scaricare il loro SDK con gli emulatori per i vari modelli. In genere viene anche fornito un ambiente di sviluppo minimo ma è decisamente peggiore di JSM6. E' bene scaricare l'SDK perché dopo l'installazione si ha a disposizione un emulatore simile al proprio telefono cellulare, molto più vicino di quello standard di JSM6. Infine JSM6 è in grado di riconoscere ed installare un elevato numero di emulatori di terze parti, esiste una procedura guidata dove è sufficiente inserire il percorso dell'emulatore. Purtroppo spesso il sistema operativo richiesto dagli SDK proprietari è Windows.

Caricare i programmi sul telefono cellulare

Ecco che arrivano le note dolenti. Non è sempre facile caricare i programmi realizzati su un telefono cellulare e soprattutto la procedura cambia da produttore a produttore e da modello a modello. Un sistema universale ma anche costoso è di pubblicare il proprio programma in un sito, come www.getjar.com, e quindi scaricarlo ed installarlo col browser WAP del proprio telefono cellulare. Questa soluzione non è molto economica perché un programma abbastanza complesso richiede un centinaio di prove per ottenere un risultato di qualità, d'altro canto uno semplice come quello proposto nel proseguo di questo articolo ne ha richieste due.

Alcuni modelli consentono di utilizzare la connessione Bluetooth per scaricare le applicazioni Java. Ma si deve acquistare una periferica Bluetooth da collegare al PC. La situazione più comoda si ha con i modelli abilitati all'uso delle applicazioni Java che consentono di scaricare le applicazioni via cavo. A volte non è presente un sistema ufficiale per abilitare questa funzione, ma cercando su Internet si può trovare del software e la procedura per abilitare lo scaricamento delle applicazioni. Si consideri che questa procedura non è approvata dal produttore e fa decadere la garanzia. Inoltre questi software lavorano con funzioni interne del cellulare ideate per le prove in fabbrica, quindi si rischia di bloccarlo per sempre. Quindi tale condotta è da sconsigliare. Infine è necessario procurarsi il cavo di collegamento. Per i produttori più rispettosi degli standard spesso è un normale cavo mini USB dal costo di pochi Euro. Viceversa, se è un tipo proprietario, esistono in commercio cavi di terze parti più economici di quelli originali perché non forniscono alcun software aggiuntivo per sincronizzare il cellulare col proprio elaboratore.

Linguaggio Java

Il linguaggio Java è una sublime miscela di potenza e semplicità. Spesso chi si avvicina alla programmazione in un nuovo linguaggio comincia senza perdere tempo a guardare le librerie o la struttura del linguaggio. Niente di più sbagliato. Conoscendo le potenti funzioni del linguaggio Java è possibile creare programmi estremamente complessi con poche righe di codice. Inoltre dare un'occhiata preventiva alle classi disponibili consentirà poi di risparmiare tempo evitando di creare cose che già esistono. Ovviamente non si possono imparare tutte le classi a memoria, dopo aver letto velocemente quello che è disponibile, si può poi cercare la classe e il metodo giusto al momento del bisogno nella documentazione Java.

Per la descrizione delle funzioni del linguaggio Java si prenderà come esempio un programma di conversione di valuta che poi sarà descritto in dettaglio nella parte finale dell'articolo.

Oggetti e Classi

Un programma Java in esecuzione consiste in un insieme di oggetti che interagiscono fra di loro per ottenere lo scopo voluto. Gli oggetti possono essere immaginati come gli ingranaggi di un orologio o del cambio di un'automobile. La scrittura di un programma consiste nel creare le classi che durante l'esecuzione genereranno gli oggetti che lo compongono. Ecco un esempio di classe:

public final class Valuta {



private String strSimboloValuta;

private int intFattConversione;

private int intCifreDopoVirgola;

public Valuta(String strSimboloValuta, int intCifreDopoVirgola, int intFattConversione) {

this.strSimboloValuta = strSimboloValuta;

...

}

public String getSimbolo () {

return strSimboloValuta;

}

public int getCifreDopoVirgola () {

return intCifreDopoVirgola;

}

public static int conversione (int valore, Valuta daV1, Valuta aV2) {

...

}

}

La classe è un parte di codice ben definita che contiene variabili, dette attributi (strSimboloValuta, intFattConversione, intCifreDopoVirgola), e funzioni, dette metodi (getSimbolo, getCifreDopoVirgola, conversione). Gli attributi memorizzano lo stato dell'oggetto mentre i metodi consentono di alterarlo. Spesso le classi sono modellate per simulare oggetti che esistono nel mondo reale. Bisogna pensare alla classe come ad un timbro, durante l'esecuzione del programma sarà possibile istanziare tutti gli oggetti richiesti di quella classe. Quindi si può anche pensare alla classe come ad un tipo (int, float). Nell'esempio la classe Valuta modella una qualsiasi valuta. Durante l'esecuzione potranno essere istanziati gli oggetti euro e lira di tipo Valuta. Questa idea degli oggetti è una rivoluzione rispetto alla programmazione funzionale. Infatti il programma che si vuole realizzare viene ottenuto mettendo assieme una serie di oggetti proprio come si fa con gli ingranaggi che costituiscono un orologio. Ovviamente spesso questi ingranaggi sono di tipo diverso: ci sono le lancette, le molle, le ruote dentate, ... ogni classe rappresenta il modello per un insieme omogeneo di questi oggetti. Questi gruppi di classi saranno sostanzialmente indipendenti dal mondo esterno e potranno formare quello che in Java si chiama pacchetto. Si può immaginare il pacchetto come l'insieme degli stampi necessari per creare un manufatto completo: il motore di un auto, oppure il suo cambio. Generalmente l'utente si limita a creare l'oggetto principale del pacchetto, questo poi creerà tutti gli altri oggetti necessari al suo funzionamento. A questo punto il pacchetto potrà essere riutilizzato in tutti gli altri programmi dove ricorrono le stesse esigenze. Inoltre la correzione di un errore o l'aggiunta di una miglioria al pacchetto si riflette su tutti i programmi che lo utilizzano. Si pensi alla somiglianza dell'interfaccia di diversi programmi di una stessa azienda, ciò avviene perché condividono gli stessi oggetti che realizzano l'interfaccia! I pacchetti possono essere inseriti uno dentro l'altro a seconda delle rispettive esigenze. Per accedere ad una classe in un pacchetto bisogna scrivere i nomi di tutti i pacchetti, separati da punti, fino ad arrivare alla classe desiderata:

javax.microedition.midlet.MIDlet convertitore;

Si capisce subito che tale sintassi risulta abbastanza prolissa. Per semplificarla è sufficiente importare il pacchetto una volta all'inizio del file sorgente:

import javax.microedition.midlet.MIDlet;

MIDlet convertitore;

Per importarle tutte le classi di un pacchetto si può aggiungere un asterisco alla fine anziché specificarne una in particolare:

import javax.microedition.midlet.*;

MIDlet convertitore;

Ogni classe contiene un metodo speciale chiamato costruttore. Si distingue dagli altri metodi perché non viene dichiarato il tipo restituito ed ha lo stesso nome della classe. Questo metodo viene richiamato automaticamente quando viene istanziato un oggetto della classe, operazione che si ottiene con l'istruzione new. Tale funzione serve per inizializzare tutte le strutture interne della classe. Ecco un esempio:

Valuta valutaEuro = new Valuta (“€”, 2, 193627);

Cosa succede quando la classe valutaEuro non servirà più? Java lancia periodicamente un programma di liberazione della memoria non più adoperata. Quindi se si rende conto che era stato istanziato un oggetto che non viene più puntato da alcuna variabile questo sarà eliminato la prima volta che si attiverà il sistema di pulizia. Volendo è possibile definire un distruttore, che è un metodo da eseguire subito prima della cancellazione dell'oggetto, ma generalmente ciò non è necessario.

La notazione puntata è adoperata per accedere ai metodi o attributi di una classe o di una istanza. Ecco un esempio:

Valuta valutaEuro = new Valuta (“€”, 2, 193627);

valutaEuro.getSimbolo(); //restituisce “€”

valutaEuro.getCifreDopoLaVirgola(); //restituisce 2

Quando dentro una classe ci si vuole riferire alla sua istanza corrente si usa la variabile predefinita di sola lettura this. Si noti che nel costruttore della classe valuta è obbligatorio usare this per distinguere la variabile passata dall'attributo della classe dato che hanno lo stesso nome.

public Valuta(String strSimboloValuta, int intCifreDopoVirgola, int intFattConversione) {

this.strSimboloValuta = strSimboloValuta;

...

}

Infine un po' di regole per la scrittura del proprio codice che sono adoperate in tutte le librerie Java e rendono più semplice la comprensione del codice:

  1. Tutte le classi cominciano con lettera maiuscola, mentre attributi, metodi e pacchetti con la minuscola.

  2. Se una proprietà è formata da più nomi dopo la prima parola seguono tutte le altre con la rispettiva prima lettera maiuscola. Per esempio ecco la classe Valuta e tre sue istanze: valutaEuro, valutaLire, valuta.

  3. Quando si dichiarano gli attributi è bene farli cominciare con un sostantivo che ricordi la classe di appartenenza come ad esempio strOK, strAnnulla sono istanze della classe String.

Qualificatori

La struttura dei pacchetti riuniti in classi consente di ampliare il linguaggio Java semplicemente. Per fare in modo che i pacchetti funzionino nel modo atteso è spesso necessario nascondere all'utente finale dei parametri la cui manipolazione metterebbe a rischio il corretto funzionamento della classe. Per “utente finale” si intende l'oggetto che che adopera tale classe o pacchetto. Per questa ragione sono stati introdotti i seguenti qualificatori:

  1. se non si mette nulla solo le classi del pacchetto potranno manipolare la proprietà;

  2. public chiunque può accedere alla proprietà (esempio: getSimbolo);

  3. private solo la classe può manipolare la proprietà (esempio: strSimboloValuta);

  4. protected solo chi eredita la classe può manipolare la proprietà.

Generalmente non si mette nulla, si scrive public in tutti i metodi di accesso alla classe dall'esterno e private negli altri. Protected si adopera quando si pensa ci sia qualche funzione importante che deve poter essere controllata se la classe venisse ereditata.

Attributi

Si diceva che le classi hanno attributi. Gli attributi rappresentano lo stato dell'oggetto istanziato dalla classe. Quindi nella classe Valuta gli attributi sono il simbolo, il fattore di conversione e il numero di cifre dopo la virgola. Quest'ultimo parametro è richiesto per implementare una semplice aritmetica in virgola fissa dato che non è possibile adoperare le funzioni in virgola mobile col CLDC 1.0. Gli attributi generalmente sono aggiornati dall'oggetto per riflettere le modifiche del suo stato, perciò solitamente vengono solo letti. In ogni caso l'utente può solo modificare gli attributi pubblici, ne esistono di privati che tengono conto di fattori che l'utente non può cambiare direttamente.

Gli attributi possono essere dei tipi primitivi, definiti dal linguaggio Java, come int (intero) o char (carattere). Questi attributi vengono creati assieme alla classe e saranno cancellati con questa. Gli attributi possono anche essere oggetti, cioè istanze di classi (anche della stessa). Un esempio di dichiarazione è:

private int intFattConversione;

dichiara che intFattConversione è di tipo intero. Mentre la definizione:

intFattConversione = 1;

definisce che da questo punto in poi intFattConversione sarà 1. E' possibile unire dichiarazione e definizione:

private int intFattConversione = 1;

Gli attributi appartengono all'istanza della classe. Quindi se intFattConversione fosse pubblico si potrebbe creare una istanza classe Valuta ed accedervi con la notazione puntata:

Valuta valutaEuro = new Valuta (“€”, 2, 193627);

valutaEuro.intFattConversione ++; //Errore intFattConversione è private

Se si vuole associare un'attributo alla classe e non all'istanza questo deve essere preceduto dal qualificatore static. Quindi se intFattConversione fosse statico si potrebbe scrivere:

Valuta valutaEuro = new Valuta (“€”, 2, 193627);

valutaEuro.intFattConversione ++; //Errore perché privato

Valuta.intFattConversione ++; //Errore perché non è statico ed è privato

Gli attributi di classe sono rari e si adoperano quando è necessario conservare qualche informazione comune a tutti gli oggetti. Il classico esempio è un contatore che memorizza il numero di oggetti istanziati per quella classe.

Metodi

L'interazione con un oggetto generalmente non avviene cambiando gli attributi. L'utente modifica gli attributi dell'oggetto, quindi il suo stato, attraverso delle funzioni dette metodi. La dichiarazione di un metodo è del tipo:

TipoRestitutito metodo (TipoIngresso1 tipoIngresso1, ...) {

istruzioni; ...;

return istanzaTipoRestituito;

}

Il TipoRestituito è la classe o il tipo primitivo che verrà restituito, void indica che non si restituisce nulla. In ingresso si danno i parametri tipoIngresso1 che appartiene alla classe TipoIngresso1. Se non ci sono parametri basta aprire e chiudere le tonde senza scrivere nulla. Per restituire il TipoRestitutito si adopera l'istruzione return istanzaTipoRestituito che blocca istantaneamente l'esecuzione delle eventuali istruzioni seguenti e restituisce istanzaTipoRestituito. Nelle classi che non restituiscono nulla spesso si adopera return; per uscire prima della chiusura dell'ultima graffa.

In Java tutti tipi primitivi (int, char, ...) passati ad un metodo vengono copiati. Quindi le modifiche sulle variabili passate ai metodi vengono perse all'uscita di questi. Per gli oggetti viene copiato il riferimento, quindi eventuali modifiche al riferimento verrano perse all'uscita del metodo, mentre le modifiche sull'oggetto sono permanenti.

Si definisce firma di un metodo il suo nome e il tipo degli ingressi nel loro ordine. In Java è possibile definire quanti metodi si vuole purché abbiano firme diverse. Vediamo quest'altra classe adoperata per creare il convertitore di valuta:

public final class Denaro {



private int intAmmontare;

private Valuta valQuestaValuta;

public Denaro(Valuta valQuestaValuta) {

this.intAmmontare = intAmmontare;

this.valQuestaValuta = valQuestaValuta;

}



public boolean set (Denaro denaro) {

int nuovoAmmontare = Valuta.conversione (denaro.intAmmontare, valQuestaValuta, denaro.valQuestaValuta);

...

}

public boolean set (String denaro) {

...

try {

nuovoAmmontare = Integer.parseInt(denaroPulito.toString());

} catch (NumberFormatException nfe) {

}

...

}

public String toString () {

...

}

}

Esistono due diversi comandi set che impostano il valore corrente di Denaro e restituiscono vero se questo valore è uguale a quello che già c'era, falso viceversa. La firma del primo metodo set (String) mentre il secondo è set (Denaro). Il compilatore ha l'onere di capire qual è il metodo giusto da eseguire. In genere questa discriminazione viene compiuta durante l'esecuzione. Infine si noti che il parametro restituito da un metodo non fa parte della sua firma. Quindi è un errore definire metodi con lo stesso nome e parametri in ingresso che restituiscono valori di tipo diverso.

Anche per i metodi si può definire static come per gli attributi. Un esempio è stato mostrato nella classe Valuta, la funzione di conversione:

public static int conversione (int valore, Valuta daV1, Valuta aV2);

Questa infatti viene richiamata nel metodo set (Denaro) direttamente dalla classe Valuta. I metodi statici possono essere richiamati dalla classe e dalle istanze, mentre quelli non statici solo dalle istanze.

Metodi come Attributi

A volte anziché aggiungere un attributo ad una classe con il qualificatore public si preferisce aggiungere due metodi per modificare e leggere tale attributo, rendendo l'attributo privato. Si noti la differenza fra questi due frammenti di codice:

public int area;

oppure:

private int area;

public int getArea () { return area; }

public void setArea (int area) { this.area = area; }

Nel primo caso si definisce un attributo intero e pubblico area. Tutti gli altri oggetti potranno accedervi con la solita notazione puntata. Nel secondo caso invece l'attributo area è privato e lo si può modificare e leggere con gli attributi setArea e getArea. Apparentemente la seconda soluzione è più farraginosa e quindi da evitare. In realtà offre il vantaggio che l'oggetto saprà quando viene letto o modificato il valore dell'area e potrà anche aggiungere delle istruzioni di controllo e verifica del valore inserito, ovvero dei sistemi di protezione dall'accesso contemporaneo per la programmazione parallela. Inoltre si potrebbe non aggiungere il metodo get, se l'attributo può essere soltanto modificato, o non inserire il metodo set, se può essere solo letto. Ecco altri esempi:

public boolean set (Denaro denaro)

public boolean set (String denaro)

public String getSimbolo ()

public int getCifreDopoVirgola ()

Quindi si suggerisce di adoperare la prima forma solo quando si ha la certezza che nulla può essere influenzato dalla modifica dell'attributo in questione. Si noti che spesso piccoli programmi evolvono in mastodontici colossi, come ad esempio è capitato a Linux, quindi si consiglia di meditare bene prima di adoperare direttamente gli attributi. Infine esistono dei componenti Java, detti Java Bean, che richiedono la sintassi mostrata in precedenza per accedere agli attributi. Non ci si dilungherà ulteriormente su questo punto perché riguarda la costruzione dei componenti per J2SE.

Ereditarietà

Ecco la grande novità introdotta dalla programmazione orientata agli oggetti. Spesso quando si crea un programma ci si rende conto che ci sono molte classi simili. In tal caso si può creare una classe di base che contiene solo quello che hanno in comune le classi simili, in termini di attributi e metodi. Tutte le altre classi erediteranno da questa, acquisendo automaticamente i sui metodi ed attributi, ed aggiungeranno i pezzi diversi. In questo modo si risparmia di scrivere tanto codice. Questo processo di generalizzazione può essere applicato più volte. Un esempio potrebbe essere una MIDlet per un telefono cellulare. Questa ha delle proprietà di base, appartenente alla classe MIDlet, che particolarizzate generano il convertitore di valuta. E' sufficiente ereditare MIDlet e personalizzarla per le proprie esigenze in modo da ottenere quanto serve senza riscrivere neppure una virgola della classe MIDlet. Ecco l'esempio:

public final class Convertitore extends MIDlet {

private Display questoDisplay;

private Schermo questoSchermo;

public Convertitore () {

questoDisplay = Display.getDisplay(this);

questoSchermo = new Schermo (this);

}

public void startApp() {

questoDisplay.setCurrent(questoSchermo);

}



public void pauseApp() {

}

public void destroyApp(boolean unconditional) {

notifyDestroyed();

}

}

La classe Convertitore si distingue da una generica MIDlet per le sue particolari implementazioni dei metodi astratti startApp(), pauseApp() e destroyApp(boolean).

Ecco due importati qualificatori inerenti l'ereditarietà:

  1. abstract indica che una classe non è completamente definita, perché almeno uno dei suoi metodi non è stato implementato. Tale metodo è preceduto dalla parola chiave abstract e non ha un corpo. La classe potrà adoperare tale metodo perché la sua firma è definita ma non potrà essere istanziata in quanto il codice per tale metodo non esiste, perciò è obbligatorio definire l'intera classe abstract. Per adoperare una classe astratta questa dovrà essere ereditata e il metodo dovrà essere implementato nelle classi che l'hanno ereditata. Un esempio è MIDlet che è astratta e dichiara come metodi astratti: startApp(), destroyApp(boolean) e pauseApp().

  2. final non permette di modificare il parametro o di ereditare la classe cui si riferisce. final è molto importante perché blocca futuri utilizzatori dal modificare alcuni parametri della classe e aumenta la velocità del codice eseguibile. Inoltre può essere adoperato per definire attributi costanti. La combinazione static final public definisce un attributo della classe, visibile da tutti, ed immutabile.

Tutte le classi Java che non ereditano esplicitamente da un'altra classe, ereditano da quella fondamentale chiamata Object.

Quando dentro una classe ci si vuole riferire all'istanza della sua super classe, quella da cui eredita e che quindi viene istanziata immediatamente prima della corrente, si usa la variabile predefinita super. Essa ha le stesse proprietà di this. super può essere adoperato per eseguire il costruttore della super classe. In genere si adopera quando la super classe ha più costruttori e se ne vuole selezionare uno. Anche this ha questa proprietà ma è adoperata più raramente perché sarebbe ricorsivo.

Quando in una classe che eredita da un'altra viene richiamato il costruttore questo deve prima richiamare quello della classe superiore e così via fino a raggiungere la classe base cioè Object. Ciò viene fatto automaticamente se la classe superiore ha un costruttore che non richiede alcun argomento, viceversa è necessario chiamare esplicitamente il costruttore con super. E' un errore di sintassi inserire istruzioni da eseguire prima che questa catena di chiamate ai costruttori sia stata conclusa. Ciò evita di operare con una classe non del tutto istanziata.

Polimorfismo

Questa funzione è necessaria per sfruttare correttamente l'ereditarietà. Ecco un pezzo di codice della classe Denaro dove è stata usata questa proprietà:

public String toString () {

...

}

La funzione toString () è definita nella classe Object e, come detto in precedenza, tutte le classi che non ereditano esplicitamente da un'altra classe stanno ereditando da Object. Si legga attentamente l'esempio successivo:

Object euro;

euro = new Denaro (new Valuta ("€", 2, 193627));

String s = euro.toString();

La domanda è: cosa eseguirà il compilatore quando viene richiamato il metodo toString() di euro? Il compilatore sa che euro è o un Object o una classe derivata da Object e quindi può avere re implementato tale metodo. In Java automaticamente verrà scelto il metodo implementato dalla istanza puntata da euro durante l'esecuzione, in questo caso Denaro. Questa funzione si chiama Polimorfismo. Esso è molto interessante perché non può essere predeterminato durante la compilazione. Durante l'esecuzione, la chiamata del metodo, genera la ricerca automatica del metodo giusto. Questa operazione ovviamente causa una riduzione delle prestazioni. Per questa ragione linguaggi più complessi, come il C++, richiedono la parola chiave virtual per implementare il polimorfismo, viceversa viene scelto il metodo che si vede dalla compilazione, in questo caso sarebbe l'implementazione fornita in Object di toString().

Interfacce

Spesso sarebbe utile ereditare una classe da più classi. Ovvero avere una classe che contemporaneamente appartiene a più tipi diversi. In questo modo si potrebbe avere uno Schermo che è contemporaneamente di tipo Screen, CommandListener, ItemStateListener. Infatti ciò consentirebbe di adoperare Schermo sia quando è richiesta una classe Screen da visualizzare nel display del telefono cellulare, sia quando è necessario un CommandListener per ascoltare i comandi impartiti dall'utente, sia infine come un ItemStateListener per ascoltare i cambiamenti che l'utente opera sullo schermo. Lo scopo è come al solito di ridurre al minimo la quantità di codice scritto. Alcuni linguaggi consentono che una classe possa ereditare le proprietà da più di una ma non Java. La ragione è che questa procedura crea codice difficile da leggere, capire e gestire. D'altro canto avere oggetti che possano essere di più tipi contemporaneamente è fondamentale per semplificare la scrittura del codice. La soluzione adottata in Java sono le interfacce. L'interfaccia è come una classe astratta che definisce esclusivamente metodi astratti. Cioè si definiscono l'insieme dei metodi, senza riportarne il codice, che un classe deve avere per potere implementare quella data interfaccia. Quindi rappresenta un contratto: chiunque implementi l'interfaccia deve garantire la presenza dei metodi richiesti. Quindi una classe può ereditare da un'altra classe ed implementare tutte le interfacce che desidera.

La definizione di una interfaccia è identica a quella di una classe eccetto che per la parola chiave interface e per i metodi che devono necessariamente essere privi di corpo:

interface MiaInterfaccia {

public static final int variabilePubblicaIntera = 1;

public String toString ();

}

Ecco un esempio di codice che eredita da Form ed implementa le due interfacce CommandListener, ItemStateListener:

public final class Schermo extends Form implements CommandListener, ItemStateListener {



private TextField tfValoreEuro, tfValoreLire;

private Denaro denaroEuro, denaroLire;

private MIDlet laMiaMidlet;

public Schermo(MIDlet laMiaMidlet) {

super("Convertitore");

...

setCommandListener(this);

setItemStateListener(this);

...

}

public void commandAction(Command command, Displayable displayable) {

laMiaMidlet.notifyDestroyed();

}

public void itemStateChanged(Item item) {

...

}

}

Se non si fosse implementata l'interfaccia CommandListener non si poteva eseguire il comando setCommandListener(this). D'altro canto l'avere implementato CommandListener richiede la presenza del metodo commandAction, che verrà richiamato dall'oggetto dove ci si è registrati come CommandListener per gestire un comando.

Lo stesso dicasi per il comando setItemStateListener(this), l'interfaccia ItemStateListener, ed il metodo itemStateChanged.

Istruzioni

Ogni volta che il linguaggio Java si aspetta un'istruzione è possibile inserire un qualsiasi insieme di istruzioni racchiuse da parentesi graffe. Tutte le variabili definite all'interno verranno automaticamente cancellate al termine del blocco. Se le variabili dichiarate internamente hanno lo stesso nome di quelle esterne non sarà più possibile accedere a quelle esterne finché non si esce dal blocco.

Condizioni

L'istruzione condizionale più comune è

if (valoreBoleano) eseguireSeVero; else eseguireSeFalso;

Bisognerebbe evitare di annidare troppi if (mettendo un altro if al posto di “eseguire se falso”) perché la comprensione del codice diventa difficile. In tal caso la condizione switch risulta più chiara. Purtroppo non è sempre possibile sostituirla, if è l'unica soluzione per confronti con funzioni booleane (<, >, equals, ...) o per l'uguaglianza fra oggetti.

L'altra condizione è switch:

switch (valoreScalare) {

case primaPossibilità:

cosaFareSeSiVerificaLaPrimaPossibilità;

break;

case secondaPossibilità:

cosaFareSeSiVerificaLaSecondaPossibilità;

break;

...

default:

cosaFareSeNessunaDellePrecedentiVerifica;

}

Questo comando è estremamente leggibile. Si consiglia di usarlo sempre se si deve verificare più di una condizione di uguaglianza su elementi primitivi del linguaggio.

Infine si ha:

condizioneBoleana ? valoreSeVera : valoreSeFalsa

Questa istruzione è eccellente per semplificare la scrittura del codice. Infatti essa diventa un differente valore in funzione di una condizione. Quindi semplifica, ad esempio, la scrittura della chiamata di una funzione con uno dei parametri che dipende da una condizione. Senza questa potente istruzione si dovrebbe adoperare una variabile intermedia ed un if.

Iterazioni

Ecco le iterazioni disponibili:

for (valore iniziale alla variabile iteratrice; controllo booleano sulla variabile iteratrice; istruzione di modifica della variabile iteratrice)

istruzioneDelCiclo;

Per capire bene il for è meglio aggiungere un esempio:

for (int i = 0; i < 10; ++i) System.out.print (i);

Questo frammento di codice crea una nuova variabile i e la pone uguale a zero. Quindi mentre i è minore di 10 esegue il comando di stampa di i e poi incrementa i e così via finché i arriva a 10. L'uscita prodotta dal programma è:

0123456789

Il for va usato quando si conosce a priori il numero di iterazioni da compiere. A volte è necessario uscire prematuramente dal for, si può ottenere ciò col comando break, oppure per saltare alla successiva condizione senza andare avanti nelle istruzioni il comando da usare è continue.

Un'altra istruzione di iterazione è:

while (condizioneBooleana) istruzione;

Questa iterazione va usata quando non si conosce a priori il numero di iterazioni e il controllo della condizione va fatto prima di eseguire l'istruzione. Ovviamente l'istruzione influenzerà la condizione booleana a meno che il ciclo sia infinito. Per cicli infiniti while (true) è sempre possibile uscire con break. Nel caso si voglia eseguire l'istruzione prima di verificare la condizione booleana esiste:

do istruzione while (condizioneBooleana);

Si noti che queste tre diverse iterazioni, come molte delle nozioni introdotte, hanno una sintassi così potente che possono essere scambiate fra di loro. Si consiglia vivamente di meditare e scegliere sempre quella più adatta, seguendo i consigli riportati, in tal modo si avrà un codice più semplice e leggibile.

Eccezioni

E se qualcosa andasse storto? A volte gli utenti dimenticano di inserire il disco quando dovrebbero... Tutti quei casi in cui si può verificare un errore sono gestiti con le eccezioni. Sostanzialmente quando si verifica un problema il programma si blocca generando un'eccezione. L'eccezione generata risale dalla classe che ha dato l'errore a quella che l'ha chiamata fino a raggiungere un gestore dell'eccezione oppure la JVM che deciderà il da farsi. Per gestire un'eccezione di tipo NumberFormatException si può inserire:

try {

nuovoAmmontare = Integer.parseInt(denaroPulito.toString());

} catch (NumberFormatException nfe) {

//Cosa fare se si verifica l'eccezione NumberFormatException la cui istanza è nfe

...

}

Quando non si vuole gestire l'eccezione, perché a quel dato livello non si hanno abbastanza informazioni per decidere il da farsi, si può dichiarare il metodo nel modo seguente:

void metodo1 (int i) throws Eccezione1 {...}

Questo metodo potrà eventualmente generare l'Eccezione1 e sarà compito di chi lo ha chiamato gestirla o dichiarare che non se ne occupa per mandarla al metodo superiore con un altro throws.

Se si vuole generare un'eccezione nel proprio codice è sufficiente scrivere:

class MiaEccezione extends Exception {...}

throw new MiaEccezione();

L'errore più comune commesso con le eccezioni è di creare delle classi che generino eccezioni in condizioni normali. Questo modo di operare rallenta molto l'esecuzione del codice. L'eccezione, come dice la parola, va adoperata solo quando non è possibile informare l'utente del problema in un altro modo.

E' obbligatorio usare un blocco try catch o un throws quando si adopera un metodo che genera delle eccezioni. Bisogna riportare un catch per ogni possibile eccezione. Se si vuole usare un solo catch per più eccezioni è possibile adoperare la classe dalla quale ereditano le altre eccezioni per catturarle tutte. L'unica deroga all'obbligo di eseguire tutte le istruzioni a rischio eccezione dentro un try è rappresentato da quelle che ereditano RuntimeException. Quindi se si vuole creare una propria eccezione che possa essere trascurata, basta fare in modo che erediti da RuntimeException.

Esecuzione Parallela

In un sistema operativo come Linux è comune eseguire più programmi contemporaneamente come scaricare la posta mentre si naviga in Internet. Il sistema operativo crea degli spazi di memoria completamente separati dove i programmi possono essere eseguiti senza disturbarsi. Inoltre esso fa in modo che il processore dedichi una fetta del suo tempo a ciascun programma. Queste fette sono dell'ordine di diversi millesimi di secondo e danno all'utente l'impressione che tutto avvenga contemporaneamente.

La stessa struttura può essere applicata dentro un programma. E' possibile creare tante unità che verranno eseguite in parallelo nel senso spiegato in precedenza. Lo scopo di avere differenti processi in esecuzione parallela è quello di dare all'utente un programma più pronto. Si immagini un programma in cui si preme il pulsante stampa. La pressione di questo tasto genera l'esecuzione del codice di stampa che può essere molto lungo in quanto richiede un continuo controllo dello stato della stampante per verificare che proceda correttamente. In effetti della stampa si occuperebbe il sistema operativo ma si immagini di avere a che fare con un sistema vetusto. In tal caso l'utente non potrebbe far nulla e dovrebbe attendere la fine della stampa. A parte questo caso limite molte funzioni richiedono un'elaborazione complessa, che può durare un tempo percettibile da un essere umano, durante il quale il sistema non risponde. La soluzione adottata per aumentare la prontezza dei programmi è di far partire un processo parallelo che si occupa di quel compito in modo che l'interfaccia grafica continui a rispondere correttamente. Altro caso in cui si usa la programmazione parallela è quello dell'aumento di prestazioni. Per questa funzione è richiesto un insieme di processori sul quale spalmare i processi.

Per creare un programma da eseguirsi in parallelo al resto ci sono due possibilità in Java. Ereditare dalla classe Thread, implementare il metodo void run() e quindi avviare il processo parallelo istanziando la classe con new e richiamando il metodo start() che andrà ad avviare un processo parallelo che esegue run():

class Parallelo extends Thread {

void run () {

System.out.println (“Sono in esecuzione”);

}

}

(new Parallelo()).start();

Un'altra soluzione da applicare alle classi che già ereditano da un'altra e quindi non potrebbero adoperare il sistema descritto in precedenza è di implementare l'interfaccia Runnable che richiede il metodo run. Per eseguire il processo parallelo è sufficiente istanziare un nuovo Thread cui si passa l'istanza della classe Runnable e dargli il comando start():

class Parallelo1 implements Runnable {

void run () {

System.out.println (“Sono in esecuzione”);

}

}

(new Thread( new Parallelo1())).start();

I processi di un programma condividono la loro memoria mentre i programmi sono eseguiti in aree completamente separate e volendo interagire possono usare dei flussi di dati. Siccome un processo può essere bloccato in qualsiasi istante, anche a metà di una istruzione atomica del linguaggio Java, come int a = 1; oppure a = a * 2; l'altro processo potrebbe trovare qualsiasi strano valore in a se il primo è stato bloccato a metà di una delle sue operazioni di scrittura su a. Per questa ragione i programmi che supportano la programmazione devono provvedere ad un sistema di sincronizzazione per evitare situazioni come quelle descritte. Ecco come si risolve il problema nel linguaggio Java:

synchronized {

...

}

Tutte le istruzioni dentro un blocco synchronized vengono eseguite bloccando gli altri processi. Quindi synchronized va adoperato quando strettamente necessario per non allungare il tempo di risposta del sistema.

Eventi

La struttura degli eventi è molto potente e semplice ed è un ottimo esempio per mostrare l'utilità delle interfacce. Vengono adoperati per gestire degli eventi come la pressione di un bottone su una interfaccia grafica o la cancellazione di un elemento da un database.

Il concetto principale è che c'è una istanza di classe il cui stato è importante per il resto del programma e tante altre istanze che devono ascoltare cosa succede a questa istanza in modo da potersi aggiornare correttamente. Per far ciò tutte le classi ascoltatrici implementano una interfaccia che le obbliga a dichiarare il metodo che poi verrà richiamato in caso di notifica delle modifica allo stato della istanza ascoltata. A questo punto si registrano nella istanza della classe fondamentale, che consente la registrazione solo alle classi che hanno implementato la suddetta interfaccia. Quindi ciò non pone alcuna restrizione sull'ereditarietà. Quando avvengono le condizioni pattuite per una notifica il metodo definito nell'interfaccia di tutte le classi che si sono registrate viene richiamato. In genere le classi notificate dall'evento vengono pure informate sull'istanza che l'ha chiamata e su ciò che ha generato l'evento.

Ecco un semplice esempio:

public final class Schermo extends Form implements CommandListener, ItemStateListener {

...

public Schermo(MIDlet laMiaMidlet) {

...

//Per registrare questa classe con setCommandListener si deve implementare CommandListener

setCommandListener(this);

//Per registrare questa classe con setItemStateListener si deve implementare ItemStateListener

setItemStateListener(this);

...

}

public void commandAction(Command command, Displayable displayable) {

//Questo metodo viene automaticamente richiamato quando l'utente attiva un comando, in quanto ci siamo registrati con setCommandListener

laMiaMidlet.notifyDestroyed();

}

public void itemStateChanged(Item item) {

//Questo metodo viene automaticamente richiamato quando l'utente modifica un elemento dello schermo, in quanto ci siamo registrati con setItemStateListener

...

}

...

}

Commenti

Ed ecco per finire la nota dolente di molti programmi: la mancanza o la scarsa qualità dei commenti. Innanzitutto si noti che del codice ben scritto è auto dichiarativo. Infatti se si riflette bene sui nomi da assegnare alle classi, etc. e si da una buona indentazione il codice risulterà molto chiaro. A questo punto si possono aggiungere dei commenti laddove una operazione possa sembrare poco comprensibile. Quindi non si deve commentare un ciclo iterativo dicendo che scorre tutti gli elementi di un vettore, piuttosto potrebbe essere utile spiegare perché si deve eseguire quell'iterazione nel caso essa risultasse oscura. Ecco due esempi dello stesso codice:

Vector v = new Vector();

public void al (Rec r) {

v.add(r);

}

Vector recordVector = new Vector();

public void addRecord (Record aRecord) {

recordVector.add (aRecord);

}

Come si può vedere il secondo frammento risulta molto più semplice da leggere e non richiede alcun commento esplicativo. Si noti che è tutto scritto in inglese. Questa è la lingua dell'informatica ed è fondamentale usarla se si pensa di condividere il codice con amici sparsi nel mondo. Per aggiungere i commenti è sufficiente cominciare una riga col simbolo //, mentre se questi occupano più righe si comincia con /* e si terminano con */. In Java è stato definito un tipo di commento che genera automaticamente la descrizione della classe, detto Javadoc. Questa idea è molto utile infatti il problema principale per il riutilizzo di classi scritte da altri è la documentazione. La spiegazione della sintassi è abbastanza articolata ed esula da questo articolo.

Programmazione in J2ME

Note sulla programmazione per J2ME

Programmare per un dispositivo a basse prestazioni, come può essere un telefono cellulare, è completamente differente dalla programmazione per un calcolatore da scrivania. Ecco alcuni suggerimenti per scrivere del codice adeguato:

  1. Pensare in grande, pensare per le evoluzioni. Quando si scrive o si pensa ad una particolare funzione non ci si concentri soltanto sul problema che si vuole risolvere. Piuttosto si cerchi di immaginare alle possibili evoluzioni di quel pezzo di codice. In tal modo sarà molto più semplice apportare modifiche e migliorie in futuro. Questo punto è di fondamentale importanza e va tenuto a mente ogni volta che si costruisce qualcosa, di qualunque natura essa sia.

  2. I dispositivi sui quali verranno eseguiti i programmi J2ME hanno prestazioni molto limitate in termini di velocità di esecuzione e di memoria. Quindi essi vanno creati con le funzioni essenziali ed ottimizzati negli algoritmi e nel consumo della memoria.

  3. Data la lentezza dei dispositivi, all'inizio del progetto si deve ponderare bene la complessità delle classi e la loro struttura per evitare computazioni inutili. Invece l'ottimizzazione del codice è l'ultimo passo da seguire. Infatti un codice molto ottimizzato è generalmente poco leggibile e quindi difficile da modificare. Inoltre potrebbe essere difficile capire i punti in cui il programma perde più tempo per l'esecuzione. Quindi a programma terminato si può adoperare uno strumento di analisi delle prestazioni e cominciare ad ottimizzare le parti più lente. Infine si consideri che un'eccessiva ottimizzazione può essere controproducente per l'evoluzione del programma.

  4. Lo schermo dell cellulare è estremamente piccolo. Il sistema di interfacciamento è basato sulla navigazione fra schermi riempiti con campi di inserimento testo, visualizzazione, etc.. Si passa da uno schermo all'altro attraverso delle etichette che compaiono in basso in corrispondenza a due pulsanti generici generalmente posti vicino a tale zona. Le etichette si chiamano comandi. E' fondamentale adoperare interfacce utenti semplici e poco articolate.

  5. Spesso gli utenti di questo tipo di dispositivi non sono abituati a lavorare con i computer e se un'applicazione non rispondesse per più di qualche secondo penserebbero che il telefono cellulare si è bloccato. Quindi bisogna evitare lunghe computazioni che bloccano il programma. Se ciò è impossibile sarà necessario inserire una barra di scorrimento che mostri l'avanzamento dell'esecuzione.

  6. Adoperare un sistema di aggiornamento automatico della versione che memorizza tutti i file del programma è fondamentale. Infatti se ad un centro punto ci si rendesse conto che si sta sbagliando strada è possibile tornare qualche versione indietro. Questo sistema è compreso in JSM6. Nel caso il vostro sistema di sviluppo preferito ne fosse sprovvisto è necessario crearsene uno personale meno efficiente. Si può comprimere tutta la cartella del programma alla fine di ogni sessione di lavoro. Questi file compressi avranno come nome quello del programma più un numero progressivo relativo alla sessione. Infine si potrà aggiungere al programma un file di testo con la storia delle modifiche suddivise per data e sessione di lavoro.

Verifica dei programmi creati

La corretta procedura di sviluppo di un programma è di cominciare le prove con l'emulatore di JSM6. Quindi verificare il funzionamento nell'emulatore del cellulare cui si è interessati. Spesso in questa fase ci si rende conto che qual cosa non funziona o che con l'implementazione del proprio produttore è meglio seguire un'altra strada per semplificare l'interfaccia utente. Infine è possibile scaricare il programma nel cellulare vero e proprio. Purtroppo anche in questa fase a volte le cose non vanno per il meglio. Fortunatamente esistono degli strumenti per testare il programma in esecuzione sul cellulare. Questi variano da produttore a produttore e quindi non è possibile descriverli in questo articolo.

Un ulteriore suggerimento è di non provare i programmi inserendo i valori di ingresso manualmente perché è un'operazione tediosa che spesso viene fatta con scarsa concentrazione. Generalmente è molto più semplice creare un piccolo programma il cui scopo sia di testare il programma realizzato.

Esempio di programma

In questa sezione si vogliono mettere in pratica le nozioni teoriche viste fino ad ora con un esempio pratico. Anziché partire dal classico “ciao mondo” si creerà un programma appena più complesso ed utile. Si tratta di un convertitore di valuta Lira Euro. Potrebbe risultare comodo a chi non è ancora pratico con l'Euro oppure con poche modifiche, per adattarlo ad altre valute, a chi viaggia fuori dalla comunità Europea. Purtroppo non è abbastanza complesso per provare tutte le nozioni mostrate fino ad ora ma dovrebbe essere sufficientemente semplice da essere compreso completamente.

Idea di partenza

Il programma si presenterà con due campi di testo contenenti il valore in Lire ed Euro ed un pulsante per uscire. Quando l'utente modifica uno dei due campi l'altro viene aggiornato di conseguenza. Quando preme il pulsante “Uscita” il programma si chiude.

La struttura delle classi

Per implementare il codice è necessario immaginare le classi che dovranno collaborare per ottenere il risultato finale. Questa fase del progetto è ciclica. Si immagina una struttura per le classi e poi si pensa a come queste verranno implementate, senza andare troppo in dettaglio. In questo modo in genere ci si rende conto di alcune lacune e si ristruttura il programma. I programmi devono essere belli da vedere come un teorema matematico. Se mentre si scrive il codice ci si rende conto di una serie di condizioni particolari che vanno gestite a parte probabilmente si sta sbagliando qualche cosa, bisogna rivedere subito la struttura delle classi. Quando si hanno le idee abbastanza chiare si può cominciare a scrivere le classi. Ovviamente scrivendo il codice spunteranno nuovi problemi che richiederanno di ricominciare questo ciclo fino ad ottenere il risultato richiesto.

Questo sistema funziona bene con programmi di dimensione abbastanza piccola perché il nostro cervello possa memorizzare le caratteristiche salienti di ogni classe. Diventa inefficiente per programmi molto complessi. In tal caso si consiglia di studiare ed adoperare l'UML.

Il programma sarà così strutturato:

Le classi di questo programma sono già state adoperate per spiegare le funzioni di Java. Segue una descrizione delle suddette classi:

Convertitore

Questa classe è banale. Estende MIDlet essendo quella che viene richiamata dal cellulare. Implementa i metodi di avvio e terminazione. Non ci si preoccupa del metodo di pausa che in questo caso non richiede alcuna operazione particolare. Si noti che la funzione di avvio viene richiamata sia all'avvio del programma sia per riprendere il funzionamento dopo una pausa quindi bisogna fare attenzione a non istanziare nuovamente lo schermo.

Schermo

Questa classe eredita da Form per poter inserire elementi grafici sullo schermo. Verrà creata dal Convertitore e settata come schermo corrente. Essa aggiunge sullo schermo due campi modificabili, detti TextBox. Uno si chiama “valore in Euro” e l'altro “valore in Lire”. Inoltre ha un comando per l'uscita dal programma. Imposta se stesso come ascoltatore degli eventi di comando e degli eventi di modifica degli elementi interni. Dato che è presente il solo comando uscita risponde a qualsiasi comando con l'uscita dal programma. Risponde alla notifica di modifica dello stato dello schermo con un controllo su quale campo è stato cambiato e l'aggiornamento dell'altro. In questo modo quando l'utente inserisce un valore in una delle due caselle automaticamente verrà computata e stampata l'altra.

Valuta

E' la classe che gestisce le valute. Deve essere in grado di memorizzare il simbolo della valuta, il relativo coefficiente di conversione e il numero di cifre dopo la virgola. Se si vuole convertire del denaro da una valuta ad un'altra basterà moltiplicare il valore del denaro per l'altro coefficiente di conversione e dividerlo per il proprio. In questo modo il coefficiente di conversione rappresenta il peso dell'istanza della valuta rispetto ad un'altra valuta immaginaria. Bisogna fare attenzione ad eseguire prima le moltiplicazioni e poi le divisioni (operando al contrario si perderebbe precisione dato che si adoperano gli interi) e ad operare con il tipo long per poi riconvertire il risultato in int (allo scopo di evitare problemi di overflow nelle moltiplicazioni). Infine memorizzare il numero di cifre dopo la virgola è importante perché le quantità di denaro vengono conservate in variabili intere e stampate o lette da Schermo con la virgola, quindi ogni valuta può avere caratteristiche diverse. Questa classe implementa il metodo statico di conversione di valuta.

Denaro

E' la classe che gestisce il denaro. Alla sua inizializzazione richiederà una valuta e da questo punto in poi la adopererà per la stampa e lettura da Schermo e per le conversioni fra altre classi Denaro. Adopera il metodo set sia per impostare il denaro da un'altra istanza di Denaro, effettuando la conversione alla propria valuta, sia da una stringa. La caratteristica del metodo set è che restituisce true se si sta impostando un valore diverso da quello precedentemente in memoria. Ciò è molto utile perché con una sola operazione si capisce quale dei campi di testo di Schermo è cambiato. Per la stampa adopera il metodo toString() di Object.

Implementazione del codice

Adesso si comincia a scaldare la tastiera...

Creazione delle classi necessarie necessari

Per prima cosa si deve avviare JSM6.

  1. Quindi dal menù Project, selezionando Project Manager si apre una finestra che, premendo new, crea un nuovo progetto. A questo punto viene chiesto di selezionare una cartella. Si deve creare una nuova cartella chiamata Convertitore. Per crearla, ci si deve spostare dove si desidera con la finestra di navigazione delle cartelle, quindi è sufficiente premere il pulsante a forma di cartella in alto a destra della finestra di salvataggio ed inserire il nome Convertitore.

  2. Si prema File -> New e selezionate MIDP e quindi MIDlet Suite. Si inserisca Convertitore come Package & Class name e Midlet come template e si prema Finish. Ciò creerà la classe base Convertitore, che eredita da MIDlet ed il contenitore di tale classe. Il contenitore è un file jar che contiene i file compilati costituenti il programma in forma compressa.

  3. Si prema File -> New e si selezioni MIDP Form. Il nome da inserire è Schermo, quindi si prema finish.

  4. Si prema File -> New e si selezioni Java class dal menù Java classes. Il nome da inserire è Denaro e si prema finish.

  5. Infine si prema nuovamente File -> New e si selezionate Java class dal menù Java classes. Si dia il nome Valuta e si prema finish.

Ecco cosa dovrebbe apparire nella struttura dei files:






A questo punto ci sono i files dentro i quali si aggiungerà il codice del convertitore. Essi hanno una struttura di base dettata dal modello che è stato selezionato al momento della creazione. Inoltre è stata creata una MIDlet suite Convertitore che può contenere più applicazioni.

Scrittura del codice

Per scrivere il codice si usa lo stesso sistema iterativo visto in precedenza per la concezione della struttura delle classi. Si comincia dalla classe base, e si scrive il codice come se già esistessero i metodi delle altre classi. Ovviamente il compilatore segnalerà diversi errori durante la scrittura perché non trova tali funzioni nelle altre classi che sono vuote. Infatti anche se non si dà il comando di compilazione esiste uno strumento in continua esecuzione che evidenzia gli errori ancor prima di compilare. Quindi si dà una lettura alla classe creata per eliminare o semplificare i metodi delle altre classi appena inventati e quando si ha finito si passa a scrivere tutti metodi appena inventati e così via fino alla fine. Scrivendo le classi dipendenti ci si renderà conto che mancano dei parametri o che sarebbe stato più semplice ottenere il risultato voluto in un altro modo. In questi casi si aggiornano le classi superiori e si ricomincia.

Di seguito è riportato il contenuto dei files che compongono il programma Convertitore:

Convertitore

/*Pacchetto che va importato per adoperare le funzioni grafiche indipendenti dall'hardware*/
import javax.microedition.lcdui.*;
/*Pacchetto che va importato per adoperaer le MIDlet*/
import javax.microedition.midlet.*;

public final class Convertitore extends MIDlet {
    /*Questi attributi memorizzaranno il display della midlet e lo schermo da mostrare sul display*/
    private Display questoDisplay;
    private Schermo questoSchermo;
    
    /*Questo è il costruttore dalla MIDlet, viene richiamato una sola volta all'avvio del programma*/
    public Convertitore () {        
        /*Qui si memorizza il display della MIDlet*/
        questoDisplay = Display.getDisplay(this);
        /*Qui si crea una istanza dello schermo sul quale si basa il programma*/
        questoSchermo = new Schermo (this);        
    }
    
    /*Questo metodo è richiamato dal cellulare all'avvio e all'uscita dalla pausa. */
    public void startApp() {
        /*Qui si imposta questoSchermo come vista corrente*/
        questoDisplay.setCurrent(questoSchermo);
    }
    
    /*Questo metodo è richiamato dal cellulare se l'utente manda in pausa la MIDlet */
    public void pauseApp() {
        /* Non è necessario compiere alcuna operazione nel caso si vada in pausa data la semplicità del programma
           I programmi più sofisticati salvano i loro dati*/
    }
    
    /*Questo metodo viene richiamato dal cellulare se l'utente vuole uscire dalla MIDlet */
    public void destroyApp(boolean unconditional) {
        /*Il comando successivo consente di uscire dalla midlet*/
        notifyDestroyed();
    }
}



Schermo

/*Pacchetto che va importato per adoperare le funzioni grafiche indipendenti dall'hardware*/
import javax.microedition.lcdui.*;
/*Pacchetto che va importato per adoperaer le MIDlet*/
import javax.microedition.midlet.*;


/*Schermo eredita da Form per aggiungere i campi di testo ed essere visualizzabile sul Display
  Schermo implementa CommandListener (che richiede il metodo commandAction) per ascoltare i comandi dell'utente
  Schermo implementa ItemStateListener (che richiede il metodo itemStateChanged) per ascoltare le modifiche gli elementi dello schermo*/
public final class Schermo extends Form implements CommandListener, ItemStateListener {
    
    /*Questi sono i campi di testo che mostreranno il valore in euro e il corrispettivo in lire*/
    private TextField tfValoreEuro, tfValoreLire;

    /*Queste sono le istanze che conterranno il denaro in euro e lire*/
    private Denaro denaroEuro, denaroLire;
    
    /*Questa istanza contiene la MIDlet, è necessario conservarla per dare il comando d'uscita quando l'utente vuole terminare*/
    private MIDlet laMiaMidlet;
    
    /*Questo è il costruttore della classe, ha il compito di creare l'interfaccia e registrarsi come ascoltatore di comandi e cambiamenti di stato del Form*/
    public Schermo(MIDlet laMiaMidlet) {
        super("Convertitore");
        
        /*Si noti che è obbligatorio usare this per distinguere la variabile passata dall'attributo della classe Schermo*/
        this.laMiaMidlet = laMiaMidlet;
        
        /*Siccome non è necessario conservare le Valute queste vengono istanziate direttamente creando le istanze di denaro*/
        denaroEuro = new Denaro (new Valuta ("€",  2, 193627));
        denaroLire = new Denaro (new Valuta ("L.", 0, 10000));
        
        /*Questi sono i campi di testo che conterranno il valore in Euro e Lire*/
        tfValoreEuro = new TextField ("Valore in €",  denaroEuro.toString(), 15, TextField.ANY);
        tfValoreLire = new TextField ("Valore in L.", denaroLire.toString(), 15, TextField.ANY);
        
        /*Qui si aggiuge una stringa e i precedenti campi di testo allo schermo*/
        append("Convertitore Lira <-> Euro");
        append(tfValoreLire);
        append(tfValoreEuro);

        /* Questo comando imposta questo schermo per ascoltare i comandi. In particolare aggiungeremo il comando uscita*/
        setCommandListener(this);
        /* Questo comando imposta questo schermo per ascoltare le modifiche agli elementi interni come i campi di testo*/
        setItemStateListener(this);
        /* Qui si aggiunge il comando uscita*/
        addCommand(new Command("Uscita", Command.EXIT, 1));
    }
    
    /*Questo metodo viene automaticamente richiamato quando l'utente attiva un comando, in quanto ci siamo registrati con setCommandListener*/
    public void commandAction(Command command, Displayable displayable) {
        /*Siccome l'unico comando inserito è uscita, senza controllare command e displayable possiamo uscire dal programma*/
        laMiaMidlet.notifyDestroyed();
    }

    /*Questo metodo viene automaticamente richiamato quando l'utente modifica un elemento dello schermo, in quanto ci siamo registrati con setItemStateListener*/
    public void itemStateChanged(Item item) {
        /*Prova a settare denaroEuro, se Set restituisce true vuol dire che esso è cambiato e quindi aggiorna denaroLire*/
        if (denaroEuro.set(tfValoreEuro.getString())) {
            denaroLire.set(denaroEuro);
            tfValoreLire.setString(denaroLire.toString());
        }
        
        /*Prova a settare denaroLire, se Set restituisce true vuol dire che esso è cambiato e quindi aggiorna denaroEuro*/
        if (denaroLire.set(tfValoreLire.getString())) {
            denaroEuro.set(denaroLire);
            tfValoreEuro.setString(denaroEuro.toString());
        }
    }    
}



Valuta

public final class Valuta {
    /*Contiene il simbolo della valuta*/    
    private String strSimboloValuta;
    /*Contiene il coefficiente di conversione*/
    private int intFattConversione;
    /*Il denaro viene memorizzato come fattore intero più sapendo che una parte del valore va dopo la virgola*/
    private int intCifreDopoVirgola;
    
    public Valuta(String strSimboloValuta, int intCifreDopoVirgola, int intFattConversione) {
        /*Vengono memorizzati il simbolo della valuta e il fattore di conversione
          Si noti che this è obbligatori per distingere gli attributi dalle variabili di passaggio dato che hanno lo stesso nome*/
        this.strSimboloValuta = strSimboloValuta;
        this.intFattConversione = intFattConversione;
        this.intCifreDopoVirgola = intCifreDopoVirgola;
    }
    
    /*Restituisce il simbolo della valuta*/
    public String getSimbolo () {
        return strSimboloValuta;
    }    
    
    /*Restituisce il numero di cifre dopo la virgola per questa valuta*/
    public int getCifreDopoVirgola () {
        return intCifreDopoVirgola;
    }     
    
    /*Converte un intero corrispondente ad un ammontare di denaro da una valuta ad un'altra*/
    public static int conversione (int valore, Valuta daV1, Valuta aV2) {
        return (int) ((long) valore * aV2.intFattConversione / daV1.intFattConversione);
    }
}



Denaro

public final class Denaro {
    /*In ammontare si memorizza la quantità di denaro come intero senza virgola*/
    private int intAmmontare;
    /*Mentre nella successiva variabile si memorizza la valuta*/
    private Valuta valQuestaValuta;
    
    /* Crea una nuova istanza di denaro con valore zero*/
    public Denaro(Valuta valQuestaValuta) {
        this.intAmmontare = intAmmontare;
        this.valQuestaValuta = valQuestaValuta;
    }
    
    /*Imposta questa istanza al valore denaro*/
    public boolean set (Denaro denaro) {
        /*Si adopera il metodo statico conversione della classe Valuta per calcolare il nuovo ammontare*/
        int nuovoAmmontare = Valuta.conversione (denaro.intAmmontare, valQuestaValuta, denaro.valQuestaValuta);
        
        /*Se il nuovo Ammontare è uguale a quello vecchio si restituisce falso, viceversa si aggiorna l'ammontare e si restituisce vero*/
        if (intAmmontare == nuovoAmmontare) return false;
        intAmmontare = nuovoAmmontare;
        return true;
    }
    
    /*Imposta questa istanza alla string denaro, se è già uguare restituisce false viceversa true*/
    public boolean set (String denaro) {
        /*In denaroPulito si memorizzerà i numeri presenti in denaro*/
        StringBuffer denaroPulito = new StringBuffer();
        
        /*Qui crea denaroPulito composto dai numeri presenti nella string denaro
          Si contano pure il numero di cifre dopo la virgola per vedere se è coerente con quello impostato nella valuta*/
        int lung = denaro.length();
        char c;
        boolean virgolaIncontrata = false;
        int cifreDopoVirgola = - valQuestaValuta.getCifreDopoVirgola();
        for (int i=0; i < lung; ++i) {            
            if (Character.isDigit((c=denaro.charAt(i)))) {
                denaroPulito.append (c);
                if (virgolaIncontrata)  ++ cifreDopoVirgola;
            }
            else if (c == ',') virgolaIncontrata = true;
        }
        
        /*Se il numero di cifre dopo la virgola non coincide con quello della valuta si aggiungono zero alla fine o si tolgono cifre*/
        if (cifreDopoVirgola > 0) for (int j=0; j < cifreDopoVirgola; ++j) denaroPulito.deleteCharAt(denaroPulito.length()-1);
        else if (cifreDopoVirgola < 0) for (int j=0; j > cifreDopoVirgola; --j) denaroPulito.append ('0');
        
        /*Si computa il valore intero corrispondente a DenaroPulito*/
        int nuovoAmmontare = 0;
        try {
            nuovoAmmontare = Integer.parseInt(denaroPulito.toString());
        } catch (NumberFormatException nfe) {
            /*Non potrà mai capitare qui perchè tutti i caratteri non cifre sono stati tolti in denaroPulito*/
        }
        
        /*Se il nuovo Ammontare è uguale a quello vecchio si restituisce falso, viceversa si aggiorna l'ammontare e si restituisce vero*/
        if (nuovoAmmontare == intAmmontare) return false;
        intAmmontare = nuovoAmmontare;
        return true;
    }    
    
    /*Ridefinisce il metodo toString della classe object per stampare il denaro*/
    public String toString () {
        /*Si mette in sbAmmontare la conversione di intAmmontare*/
        StringBuffer sbAmmontare = new StringBuffer(Integer.toString(intAmmontare));     
        StringBuffer sbRisultato = new StringBuffer ();       
                
        /*Si copiano in sbRisultato le cifre di sbAmmontare aggiungendo i separatori per le migliaia e la virgola
          Si noti che per semplificare la procedura l'ordine di copia è inverso, verrà corretto alla fine*/
        int i = 0;                
        for (int j = sbAmmontare.length()-1; j >= 0; --j) {            
            if (i > 0) {
                if ( i == valQuestaValuta.getCifreDopoVirgola()) sbRisultato.append(',');            
                else if ( (i - valQuestaValuta.getCifreDopoVirgola()) % 3 == 0 ) sbRisultato.append ('.');
            }
            sbRisultato.append (sbAmmontare.charAt(j));
            ++i;
        }
        
        /*Se sbRisultato ha meno cifre che posti dopo la virgola si aggiungono gli zeri*/
        for (; i <= valQuestaValuta.getCifreDopoVirgola(); ++i) {
            if ( i == valQuestaValuta.getCifreDopoVirgola()) sbRisultato.append(',');
            sbRisultato.append ('0');
        }
        
        /*Si aggiunge la valuta scritta al contrario per coerenza con quanto fatto fino ad ora*/
        for (int j = valQuestaValuta.getSimbolo().length()-1; j >= 0; --j) 
            sbRisultato.append(valQuestaValuta.getSimbolo().charAt(j));
        
        /*Si inverte sbRisultato, si converte in Stringa e si restituisce*/
        return sbRisultato.reverse().toString();                
    }   
}

Verifica del codice

Ecco un'immagine del programma finale in esecuzione sull'emulatore della Sun:






La verifica del codice di questo semplice programma non è difficile. Dopo aver fatto alcune prove sull'emulatore della Sun si è passati a quello proprietario ed infine ad un vero telefono. Tutto ha funzionato correttamente.

Conclusioni

Lo scopo di questo articolo è di dare a tutti la possibilità di creare programmi per il proprio cellulare. Purtroppo la materia è tanto vasta che probabilmente un libro non sarebbe stato sufficiente. Anziché dilungarsi sulle istruzioni e sulle funzioni disponibili si è preferito porre l'accento sulla programmazione Java. Per i dettagli sul linguaggio e sulle librerie ci sono degli ottimi documenti riportati in bibliografia mentre per maturare la filosofia della programmazione è necessaria molta esperienza. Infine due pensieri dell'autore: non è tanto meritorio risolvere un problema, quanto lo è risolverlo nel modo giusto; un programma scritto bene è come una bella poesia o teorema ben dimostrato.

Per sviluppare questo articolo sono stati adoperati diversi programmi con licenza libera (a volte anche con sorgente aperto), ecco la lista dei più importanti: Fedora Core 3, GNOME 2.8, OpenOffice 1.1.2, Java 2 Second Edition SDK, Sun Java Mobility Studio 6, Proguard. I libri citati nella bibliografia sono liberamente scaricabili da Internet. Il programma Convertitore è rilasciato con licenza GNU GPL. Un ringraziamento a mia moglie Stefania che mi ha incoraggiato e supportato nella scrittura di questo articolo.

Autore

Simone Pernice laureato in ingegneria Elettronica presso l'Università degli Studi di Palermo lavora nella progettazione e sviluppo dell'elettronica per i telefoni cellulari di terza generazione. Trascorre parte del tempo libero scrivendo programmi per qualsiasi cosa abbia un processore.

Bibliografia

  1. Thinking in Java, 3rd Edition

  2. The Java Language Specification, Second Edition

  3. JSR118 Mobile Information Device Profile for Java 2 Micro Edition