Fondamenti di programmazione Java

Fondamenti di programmazione Java
Buoncompagni Luca
26 gennaio 2014
2
Indice
1 Introduzione al Corso
1.1 Note dell’autore . . . . . . . . . . . . . . . . . . . . . . . . . . . .
1.2 Programma del Corso . . . . . . . . . . . . . . . . . . . . . . . .
2 La programmazione di un Calcolatore
2.1 Introduzione . . . . . . . . . . . . . . . . .
2.2 Tipi di Dati . . . . . . . . . . . . . . . . .
2.3 Operazioni su Dati . . . . . . . . . . . . .
2.4 Cicli e Rami Decisionali . . . . . . . . . .
2.5 Classi e struttura del codice . . . . . . . .
2.6 Esercizio 2.0: Hello World test con Eclipse
2.7 Esercizio 2.1: Ordinare un’array numerico
2.8 Esercizio 2.2: Ordinamento alfabetico . .
3
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
5
5
6
7
7
9
11
14
17
22
26
30
4
INDICE
Capitolo 1
Introduzione al Corso
1.1
Note dell’autore
Questo documento contiene una linea guida alle lezione di laboratorio sulla programmazione in Java per il corso di Scienze Cognitive e Processi Decisionali dell’Università degli Studi di Milano. L’elaborato non intende essere un manuale alla programmazione e si consiglia vivamente di vedere riferimenti allegati per una
descrizione più formale e meglio contestualizzata della funzionalità proposte da
Java. Infatti, il laboratorio ha l’obiettivo di porre le basi della programmazione
a oggetti e dare indicazioni su dove e come ricercare informazioni a riguardo
sensa potersi, purtroppo, soffermare sulle infinite potenzalità di tale linguaggio.
Questo tipo di trattazione permette di apprendere capacità pratiche non solo
limitate all’utilizzo della macchina virtuale Java (JWM), ma anche pronte per
essere usate da un qualsiasi teorico calcolo numero; come quello applicato nel
Machine Learning (ML).
Internet è saturo di informazioni riguardanti il fondamenti della programmazione in generale, tanto quanto per affrontare i problemi pratici nell’implementazione attraverso SDK (software development kit). Questa eccessiva mole
di informazione può creare problemi per trovare la giusta parola chiave da dare
ad un motore di ricerca per carpire informazioni utili al suo utilizzo. Per aiutarvi in questo snaturerò la lingua italiana con termini in inglese che spesso sono
proprio le giuste parole da inserire in Google e capire come si fa una certa operazione, per esempio. Suggerisco fortemente a tutti di documentarsi attenendosi
a fonti scritte in inglese, questo non solo da la possibilità di avere un numero
di informazioni infinitamente più grande di quello che si possa avere in italiano,
ma evita traduzioni spesso non coerenti e fonte di incomprensioni. Tuttavia, per
completezza, propongo qui una lista di risorse consigliate per avere informazioni
riguardo la programmazione in Java:
• ORACLE è una delle associazione sviluppatrice di Java. Da questo sito è
possibile ottenere informazioni sulle varie versioni del software, sui bug o
releases. Inoltre fornisce il download gratuito e le guide di installazione dei
pacchetti standard di Java. Di particolare interesse, c’è anche un tutorial
interattivo per principianti e non.
• Stack Overflow e un forum completo e molto frequentato che raccoglie
domande su tutti i linguaggi di programmazione. La sezione dedicata a
5
6
CAPITOLO 1. INTRODUZIONE AL CORSO
Java è già ampia e perciò capita di rado di dover proporre una nuova
domanda.
• un’altro ottimo modo per apprendere i concetti fondamentali è quello
di seguire gratuitamente le video-lezioni del corso CS106A del professore
Mehran Sahami per l’univeristà di Stanford.
• Infine, tra i tanti libri disponibili sui fondamenti della programmazione ad
oggetti consiglio Java 2 by Example, consultabile da questo link.
Questi documenti collezionano la maggior parte di quello che serve sapere per
poter programmare, ulteriori riferimenti più specifici ai casi trattati saranno
proposti in seguito.
1.2
Programma del Corso
Il corso si sviluppa in quattro capitoli, ognuno di questi è diviso in una parte
teorica ed una pratica, basata sull’utilizzo dell (integrated development environment) IDE Eclipse 3.7. Quest’ultimo è un editor di testo in grado di semplificare
lo sviluppo di un programma grazie alle sue funzioni di intercettazione di errori
e suggerimenti.
Nel primo Capitolo verranno analizzati i concetti fondamentali per implementare una generica logica di programmazione. Questo richiede di definire
una notevole mole di concetti usati nei codici e che hanno significati specifici
che influenzano il comportamento delle programma che si vuole scrivere. Tali
concetti non verranno trattati tutti nel dettaglio ma si darà solo una carrellata
generale, soffermandosi su quelli più importanti. I link di sopra contengono ogni
possibile approfondimento a riguardo. Per fissare e comprendere meglio il comportamento di questi concetti verranno proposti alcuni esempi ed esercitazioni
preliminari.
Il secondo, si focalizza sul saper comprendere ed utilizzare librerie esterne.
Si vedrà inoltre come queste possano essere utilizzate per ampliare facilmente le
capacità del programma che si vuole scrivere. In particolare, l’esempio pratico
proposto a riguardo sarà quello di manipolare file testuali.
Continuando, il terzo capitolo contiene un’introduzione alla creazione di interfacce grafiche attraverso alcuni pacchetti standard di Java. Inoltre, per l’esempio proposto in questo capitolo, verrà analizzato un software in grado di auto
generare il codice che descrive una porzione di grafica impostata attraverso l’utilizzo del mouse (Drug and Drop). In analogia con il primo capitolo la trattazione
proposta qui non vuole essere completamente esaudiente ma proporre solo alcuni punti di partenza per quello che è un campo della programmazione molto
ampio. Come sopra, i link della sezione precedente contengono la maggior parte
degli approfondimenti a riguardo.
Infine, il quarto capitolo, contiene una descrizione preliminare di una Rete
Neurale e una possibile sua implementazione. Inoltre, verranno velocemente
affrontati anche alcuni concetti legati all’implementazione dei processi necessari alla validazione di un algoritmo statistico. Gli esempi per questo parte
delle lezioni saranno basati sul riconoscimento di semplici funzioni logiche e di
immagini di volti umani.
Capitolo 2
La programmazione di un
Calcolatore
2.1
Introduzione
Una domanda che nasce spontanea nell’introdursi all’uso di un calcolatore elettronico è: Quale è la peculiarità che permette di utilizzarlo in un numero veramente alto di applicazioni?. Le risposte possono essere tante ma a mio avviso
quella che si distingue tra tutte e quella chiamata Modularity. Utilizzando un
approccio intuitivo, la modularità è quella capacità di incapsulare una certa
operazione come se fosse una scatola che prende in ingresso alcuni parametri
e restituisce un informazione di uscita. La scatola, in questa analogia, si dice
modulare quando non si necessita di altre informazioni per poterla usare. Ergo,
potete completamente ignorare i meccanismi che risiedono al suo interno. Un
esempio palpabile di questa proprietà è quello di un comune browser che può
essere usato per esplorare risorse di diversa natura ignorando completamente
tutti i complessi meccanismi che risiedono dentro la comunicazione internet.
Questo concetto è estremamente importante per riuscire a programmare in
modo soddisfacente e si traduce in modo pratico sulla suddivisione di un problema complesso in più sotto-problemi semplici. Se questi vengono risolti adeguatamente risulterà possibile chiudere la scatola e non dovremmo più preoccuparci
se lo stesso problema si presenterà nuovamente. Inoltre, e forse più importante,
diventerà più semplice usare complesse rete di procedure (scatole) aumentando
così le capacità del software che si sta creando. Questo ultimo concetto viene
chiamato dalla comunità come un aumento del livello di astrazione ed è quello
che ha reso la programmazione così popolare. Un esempio pratico può essere
visto durante lo sviluppo di un’interfaccia grafica, in fatti nel momento in cui
si vuole creare una finestra dello schermo non dovremmo preoccuparci di come
il pixel reagisce ad una diversa intensità di corrente ma basterà dare le sue
coordinate x e y del piano formato dallo schermo. Capite bene che questo semplifica notevolmente la programmazione. La traduzione da coordinate spaziali
e intensità di corrente da mandare allo schermo è gestita da un basso livello
di astrazione, solitamente un driver video, e su di esso si poggiano altri livelli
come ad esempio quello del sistema operativo su cui lavorate. Questo basso liv7
8
CAPITOLO 2. LA PROGRAMMAZIONE DI UN CALCOLATORE
Figura 2.1: grafica rappresentazione di pragrammazione modulare per livelli di
astrazione.
ello è stato incapsulato molto bene e vi permette di usare la grafica rimanendo
completamente allo scuro di tutti questi passaggi.
Un modo intuitivo di rappresentazione del flusso di informazioni all’interno del calcolatore è proposto in figura 2.1. In questo grafico si vede come un
problema complesso come quello di gestire input, output e manipolazione di
dati all’interno di un computer, è stato suddiviso in molti piccoli sotto-problemi
più semplici (rappresentati come rettangoli o scatole nell’analogia di prima).
Da notare che queste sono organizzate in modo gerarchico, quindi è possibile
che un software sia formato da un’insieme di più procedure a loro volta incapsulate. Durante questo corso ci collocheremo al livello di astrazione più alto
e scriveremo le procedure rappresentate da un rettangolo vuoto. Allo stesso
tempo però è importante sapere che utilizzeremo le procedure rappresentate
come scatole nere, senza sapere quali sono i complessi processi che avvengono
al loro interno. Per capire da dove deriva il nome: innalzamento di astrazione,
basta considerare che al livello più basso, quello macchina, le uniche operazioni
possibile sono quelle Booleane della And e Or logica. Mentre, come vedremo,
al livello di applicazione la complessità delle operazioni possibili con una sola
riga di comando sono ben più complesse. In questo scenario, si può dire che i
compiti dello sviluppatore software sono quelli di: saper suddividere un problema complesso in tanti sotto problemi, scrivere le istruzioni che risolve ognuno
di questi problemi in modo il più possibile indipendente dagli altri, infine, ma
non meno importante, gestire in modo efficiente la comunicazione tra uscite e
ingressi delle diverse soluzioni implementate (rappresentate in figura come frecce). Inoltre è importante sapere che ogni volta che un nuovo programma viene
scritto e necessario compilarlo, cioè darlo in pasto ad un software in grado di
percorrere questa sorta di piramide dal livello più alto al più basso. Il suo com-
2.2. TIPI DI DATI
9
pito è quello di tradurre le linee di comando scritte dallo sviluppatore in un
file che contiene solo i simboli 0 e 1 interpretabili dalla macchina. Quando si
vuole usare quel determinato programma basterà lanciare questo file tradotto e
la CPU del calcolatore svolgerà i calcoli specificati.
Fino ad ora abbiamo visto molto velocemente e intuitivamente come si struttura un generico codice di programmazione. Ma rimane ancora in sospeso cosa
si trova dentro alle scatole. Più formalmente questa viene detta funzione o
metodo che implementa, in certo linguaggio di programmazione, un algoritmo.
La definizione di algoritmo può essere controversa e complicata, e dal punto di
vista scientifico si basa sulla macchina di turing. Tuttavia, in modo più intuitivo
lo si può definire come:
Un algoritmo è un insieme ordinato di istruzioni
non ambigue che, terminando in un tempo finito,
risolvono una determinata classe di problemi.
Caratteristica comune a tutti gli algoritmi è che questi hanno dei parametri
in ingresso e, dopo un certo tempo, restituiscono un’uscita. Successivamente
vedremo come gestire input e output per fare in modo che più algoritmi possano
collaborare tra di loro ma prima verranno proposte alcune sezioni per descrive
quali sono le istruzioni che possono essere inserite all’interno di un algoritmo
implementato in Java.
Infine è importante notare che per controllare in modo soddisfacente un
flusso di dati così complicato tra algoritmi con basso e alto livello di astrazione
è indispensabile una cura vagamente maniacale dell’ordine con cui il software
viene progettato e scritto. La prima regola è quella di usare nomi che descrivano
la loro funzione in modo tale che il linguaggio di programmazione risulti il più
possibile vicino al linguaggio naturale. Altrettanto importante è la cura dell’indentazione e delle convenzioni tipicamente usate nei programmi. Inoltre è
possibile introdurre commenti direttamente nel codice in modo che non vengano
considerati come linee di comando ma solo come informazione per lo sviluppatore. Infine è possibile creare una documentazione che sia automaticamente
generata e standardizzata sotto forma di pagine web html: JavaDoc.
2.2
Tipi di Dati
Fondamentalmente un algoritmo non fa altro che manipolare dati in ingresso per
ottenere un risultato voluto in uscita. Tuttavia, dato che quando viene eseguito,
ogni programma è tradotto in 0 e 1 dal compilatore, c’è bisogno di descrivere
ogni possibile tipo di dato in questi termini. Per questo motivo c’è bisogno ad
esempio di dare un significato al simbolo 5, questo può essere trattato come
un numero intero, oppure come un carattere alfanumerico o addirittura come
il mese di Maggio. Vedremo successivamente come tipi di dati possano essere
definiti e quindi creati a piacimento dello sviluppatore, ma è bene sapere che
esistono, oltre hai dati primitivi ,alcuni tipi predefiniti e ampiamente utilizzati:
• Boolean è dato logico, può assumere valori true o false.
• Integer è dato numerico di tipo intero (senza virgola) positivo o negativo
• Float e Double sono dati numerici di tipo reale (con la virgola) positivi
o negativi.
10
CAPITOLO 2. LA PROGRAMMAZIONE DI UN CALCOLATORE
• Char carattere alfanumerico. L’alfabeto e vari simboli sono descritti
in modo sequenziale dalla tabella ASCII estremamente importante per
compiere operazione sui caratteri trattandoli come numeri.
• String è un vettore di Char e quindi definisce una successione ordinata
di caratteri; una parola per esempio. Nel codice le stringe di caratteri
vengono sempre scritte tra apici (es: questa è una stringa testuale).
• Array è una generica collezione ordinata di dati tutti dello stesso tipo,
è definito dal simboli [] dopo il nome del tipo di dato. Ad esempio una
Stringa è definita come Char[] mentre un vettore di numeri interi: Integer[]. Si può adottare questo tipo di notazione per ogni possibile tipo di
dato. Gli elementi all’interno della successione sono indirizzati attraverso
un indice intero che indica la posizione di un elemento nella successione; è
bene ricordare che il valore di questo indice parte da 0 fino a (dimenzione
dell’array - 1). La loro dimensione è sempre fissa e quindi è bene usarli
quando si è sicuro che la successione non varia mai la sua lunghezza.
• ArrayList<?> è una generica collezione ordinata che si comporta in modo simile agli Array ma è in grado di modificare la sua massima lunghezza.
Dove il simbolo ? può essere tradotto in un qualsiasi tipo di dato. Ad esempio, un testo può essere pensato come una successione di linee (String)
e quindi: List<String>.
• HashSet<?> si comporta come una List ma non garantisce che i suoi
componenti siano sempre ordinati allo stesso modo. Per questo motivo
non ha senso usare un indice per recuperare i dati al suo interno ma se ne
può solamente chiedere un generico elemento.
• HashMap<?,?> costituisce una tabella che mette in relazione una key
con un value senza un preciso ordine. Ad esempio un elenco telefonico
potrebbe essere descritto come HashMap< String, Integer> dove una
Stringa testuale definisce la chiave: il nome della persona. Mentre il valore
di questa mappa è un numero intero che definisce il numero di telefono
di quella determinata persona. Alternativamente, rimane corretto definire
una rubrica anche come HasMap< String, String>.
Gli ultimi quattro elementi di questa lista prendono il nome di tipi composti, perché sono una collezione di più dati semplici. Tuttavia è sempre possibile creare
dati composti di dati composti, quindi ad esempio può esistere ArrayList<
ArrayList< Integer> > che rappresenta una tabella (matrice) di valori interi. Analogamente, se la struttura non cambia di dimensione e la tabella ha un
numero di righe uguali a quelle di colonne si può usare un Array bidimensionale definito come: Integer[][]. Questa struttura potrebbe essere utilizzata per
rappresentare tutti i pixel di una foto, ad esempio. Un’altro esempio potrebbe
essere quello di voler implementare un videogame inspirato alla battaglia navale;
dove il valore 0 indica l’assenza di una barca, 1 che la barca c’è, 2 che è affondata. Ovviamente si può creare strutture dati di qualsiasi dimensione, e non
solo su due come in questo esempio. In questa lista sono riassunti solo i tipi più
comunemente utilizzati, ma ne sono disponibili molti altri. La scelta di un tipo
di dato rispetto ad un altro dipende da come quel determinato dato deve essere
manipolato.
2.3. OPERAZIONI SU DATI
11
Il processo di creazione di un qualsiasi tipo di dato può essere fatto inserendo
nel codice la linea di comando:
T ipoV ariabile nome = new T ipoV ariabile();
(2.1)
ad esempio:
1
2
3
4
Integer x = new Integer();
String text = new String();
ArrayList< Boolean> listOfFlags = new ArrayList< Boolean>();
HasMap< String, String> = new HashMap< String, String>();
Se prendiamo in considerazione la prima linea del codice, ma il discorso è analogo
per tutte, questa chiede al compilatore di creare un’area di memoria vuota, cioè
con il valore null, adatta per contenere un numero intero; L’area di memoria
appena creata avrà la possibilità di essere indirizzata attraverso il suo nome x.
Il dato null è un dato particolare che sta ad indicare l’assenza di informazione
associata alla variabile x, quindi non ha senso fare operazioni su questo tipo di
dato perché non si conosce il loro valore. Ad esempio, il risultato di
x + 1 = null + 1 = ??
NullPointerException
che restituisce un errore o Exception. L’unica operazioni che è possibile fare
appena il dato è stato creato è quello di assegnargli un valore iniziale, ad esempio:
1
2
x = 12;
x + 1;
ora la cella di memoria indirizzata da x non è più vuota ma contiene il numero
12. Cosi, il risultato della seconda operazione non è più quello di generare
un’errore, ma quello di restituire un valore pari a 13. Questo sta ad indicare che
ogni variabile che si crea deve essere inizializzata ad un valore prima di essere
utilizzata, altrimenti il compilatore ferma il programma e notifica un’ eccezione.
Per i dati composti come: array, List, Set, Map il processo di inizializzazione
è più complesso ma automaticamente gestito alla sua creazione. Tuttavia, per
strutture più complesse è necessario farlo manualmente. Per inizializzare una
matrice, ad esempio, c’è bisogno di creare l’area di memoria vuota pronta a
contenere le colonne della tabella usando il comando:
1
new ArrayList< ArrayLisr< Integer>>
e successivamente, per ogni colonna, dovremmo inizializzare le sue righe ripetendo l’operazione
1
new ArrayLisr< Integer>
per tutta la sua lunghezza.
2.3
Operazioni su Dati
Abbiamo visto come dati possono essere strutturati in diverse forme, e vedremo
come la loro forma può cambiare sostanzialmente il tipo di operazioni richieste
12
CAPITOLO 2. LA PROGRAMMAZIONE DI UN CALCOLATORE
per arrivare allo stesso risultato. Per fare questo c’è bisogno di fare un passo
avanti e vedere come i dati possano essere manipolati. Abbiamo già visto l’operazione di assegnazione definita con il simbolo =. Questa fa in modo che il nome
di una variabile (x nella sezione precedente) sia collegata ad una particolare area
di memoria che contiene il valore di quella variabile. Grazie a questa definizione
rimane intuitivo leggere la seguente successione di linee di comando:
1
2
3
4
5
Integer x = new Integer();
Integer y = new Integer();
x = 12;
y = x;
y = y + 1
Da notare però, che questa non è l’operazione di uguaglianza comunemente descritta con il simbolo =. Infatti il valore di x rimane sempre 12 mentre quello
di y è 13, cioè la quarta linea non descrive che x e y sono uguali. In particolare, queste poche linee vengono lette durante l’esecuzione in modo sequenziale.
Quindi prima si crea un’area di memoria vuota collegata al nome x, e poi una
riferita a y. Successivamente si inizializza la memoria riservata a x con il valore
intero 12 e poi si inizializza l’area di memoria di y allo stesso valore di quello che
c’è attualmente in x. In altre parole si fa una copia del valore della variabile x
(=12) e lo si posiziona all’interno della variabile y che era vuota. Alla fine, nella
quinta linea, si fa ancora lo stesso procedimento: si copia il valore dato dall’operazione (y + 1) (=13) e lo si pone all’interno della variabile y sovrascrivendo il
precedente risultato di 12.
Durante questo esempio si è visto il simbolo che identifica l’operazione di
somma “+”, e allo stesso modo si possono usare sottrazione “-”, moltiplicazione
“*”, divisione “/”, elevamento a potenza “ˆ” e resto della divisione intera, detto
modulo “%” ad esempio (5 % 2) vale 1 (molto usato per sapere se un numero
è multiplo di un altro, in particolare α è multiple di β se (α mod β) = 0).
Inoltre ma non meno importante parentesi tonde si possono usare per esprimere
una priorità di calcolo per ogni tipo di operazione; esattamente come utilizzato
comunemente in matematica. Questo tipo di operazione nasce per essere utilizzata prevalentemente su dati di tipo numerico, a eccezione del simbolo “+”
che, se viene usato tra dati di tipo String indica la concatenazione di più serie
di caratteri in una sola.
Come abbiamo già visto nell’introduzione, l’unica cosa che il calcolatore è in
grado di fare al livello di astrazione più basso sono le operazioni logiche, dette
anche booleane e si usano solo con i dati di tipo Boolean. Queste si possono
usare scrivendo: “&&” per la And logica (il risultato è vero solo se tutte le
variabili sono vere), la OR logica: “||” (il risultato è vero anche se una sola
variabile è vera) e la NOT, negazione logica: “!”.
Altro, e ultimo tipo di operazione tra dati elementari sono quelle di controllo.
Ad esempio si può ricevere una riposta positiva (true) se due variabili sono uguali
utilizzando il simbolo “==”, oppure se sono diverse scrivendo “!=”, “>” per
maggiore, “<” per minore e “<=” o “>=” per le comparazioni non strette.
quindi risulta pienamente corretto scrivere ad esempio queste linee di seguito
all’esempio precedente:
1
2
x = ( ( y * x)^ 2) / (( y + ( x / 19));
y = x % y; //qualsiasi calcolo
2.3. OPERAZIONI SU DATI
3
4
5
6
7
8
13
x == y;
//fornisce il valore true o false
Boolean b = new Boolean();
Boolean b1 = new Boolean;
b = true;
a = true;
( ! ( a && b)) // implementa la Nand, fornisce vero se non sono mai
tutte e due vere
Ancora una volta queste sono le operazioni più comunemente usate ma ne esistono altre, vedere qui per dettagli e schematizzazione. Da notare che nei
commenti sopra viene usata la parola: fornisce; vedremo successivamente cosa
è possibile fare il valore fornito.
Risulta estremamente importante da tenere a mente il fatto che operazione
si possano fare solo ed esclusivamente tra tipi di dati dello stesso tipo. In linea
teorica perché non ha senso sommare un valore Integer (12) ad un valore Boolean
(false). In linea pratica perchè l’area di memoria in cui vengono conservati
i valori sono di grandezze diverse, quindi un Integer occupa più memoria di
un Boolean. Consideriamo il seguente esempio per analizzare meglio questa
considerazione:
1
2
3
Integer x = 15;
String str = "1875";
x + str
in questo caso il risultato della terza operazione risulterà pari ad una stringa
concatenata del tipo: “187515”. Mentre invece ci aspetteremmo il risultato 1890,
che si otterrebbe nel caso in cui str sia dello stesso tipo di x: Integer. Per ovviare
a questo problema si può pensare di convertire un dato String in uno Integer e
poi fare l’operazione di addizione:
1
2
3
4
Integer x = 15;
String str = "1875";
Integer strInt = Integer.valueOf( str);
x + strInt
e in questo caso si vede che il risultato della quarta operazione è: 1890. Dove
l’istruzione valueOf(. . . ) permette di convertire una serie di caratteri in un
numero intero. Questo meccanismo è compatibile per la maggior parte dei dati
semplici, quindi ad esempio è corretto scrivere Bollean.valueOf(. . . ). Un’altra
conversione ampiamente usata è quella da da numero a stringa di caratteri, ad
esempio queste linee
1
2
3
Integer x = 15;
String str = "1875";
x.toString() + str
ritornano lo stesso risultato del primo esempio: “187515”. Nel mondo pratico
quasi tutti i tipi di dati hanno il modo per essere convertiti e quindi nascono
così moltissimi metodi che possono essere usati per convertire dati di natura
diversa. Il metodo più generalmente usato è quello del Casting
1
Integer x = 15;
14
2
3
4
CAPITOLO 2. LA PROGRAMMAZIONE DI UN CALCOLATORE
String str = "1875";
Integer strInt = (Integer) str;
x + strInt
che ritorna nuovamente con il risultato numerico 1890. Dove, alla terza riga,
tra parentesi tonde si indica il tipo di dato in cui si vuole convertire.
2.4
Cicli e Rami Decisionali
Abbiamo visto i tipi di dati e alcune possibili operazioni che si possono fare
con essi. Tuttavia, spesso nasce la necessità di dovere ripetere le operazioni
per un certo determinato numero di volte o di dover decidere se svolgere delle
operazioni o altre. Vediamo ora alcune statament che ci permettono di compiere
queste operazioni.
1
2
3
4
5
6
7
8
9
10
11
12
13
// inizializza automaticamente la variabile
Boolean b = new Boolean( true);
Integer i = new Integer( 10);
Boolean f = ! b; // not b
if( i >= 11){
// se il risultato e’ true
// esegui una serie di operazioni
....
} else {
// se il risultato e’ false
// esegui un’altra serie di operazioni
....
}
14
15
16
17
18
19
20
21
22
23
24
25
26
27
if( b){
// se b e’ true
// esegui una serie di operazioni
....
} else if( f && (i < 11)){
// se b e’ false e se f e’ true e se (i < 11) e’ true
// esegui un’altra serie di operazioni
....
} else {
//se b e’ false e se (f && (i < 11)) e’ false
// esegui ancora un’altra serie di operazioni
....
}
Questo esempio mostra come si può scrivere un ramo decisionale if else. Prima
viene mostrata la versione più semplice della struttura, mentre dopo una più
complessa. In particolare, se l’espressione all’interno delle parentesi tonde dopo
la parola chiave if è vera, o se il suo valore lo è in caso sia un booleano, solo le
linee di codice all’interno delle prime parentesi graffe verrà eseguita; altrimenti
verranno eseguite quelle dopo la parola chiave else. Si noti che non c’è limite al
numero massimo di strutture indentabili ma nel caso in cui ce ne sia bisogno di
molte risulta più comodo la struttura switch-case. Un suo esempio tratto dal
tutorial Oracle:
2.4. CICLI E RAMI DECISIONALI
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
15
Integer month = new Integer( 8);
String monthString = new String();
switch (month) {
case 1: monthString = "January";
break;
case 2: monthString = "February";
break;
case 3: monthString = "March";
break;
case 4: monthString = "April";
break;
case 5: monthString = "May";
break;
case 6: monthString = "June";
break;
case 7: monthString = "July";
break;
case 8: monthString = "August";
break;
case 9: monthString = "September";
break;
case 10: monthString = "October";
break;
case 11: monthString = "November";
break;
case 12: monthString = "December";
break;
default: monthString = "Invalid month number";
break;
}
In pratica la struttra controlla se la variabile month è uguale ad un valore indicato dalla parola chiave case, se si svolge le operazioni al suo interno altrimenti
svolge le operazioni definite dalla parola chiave default. Il comando di break
può essere utilizzato in qulasiasi statament e indica al compilatore di uscire dallo
stesso. Ad esempio, considerando che non siano presenti le istruzioni di break
nel precedente esempio e che month sia uguale a 11 il programma assegnerebbe
a month “November”, poi “December” e infine “Invalid month number”. Infine è importante ricordare che, di defualt, l’operazione case prende solo valori
numerici.
Tuttavia a volte può capitare di dover fare un numero di operazioni non
ben definito o comunque molto alto. Per questo ci vengo in aiuto i cicli (loop
statament) che possono avere tre forme diverse ma analogamente funzionali.
1
2
3
4
5
6
7
8
9
Integer limit = new Integer( 100000);
Integer counter = new Integer( 0);
while( count < limit){
// esegui alcune operazioni
// fino a che la condizione
// (count < limit) e’ true
....
counter = counter + 1;
}
16
CAPITOLO 2. LA PROGRAMMAZIONE DI UN CALCOLATORE
Questo è l’ esempio di un ciclo While. In particolare le operazioni all’interno
delle parentesi graffe vengono eseguite fino a che l’espressione definita accanto
alla parola chiave è true. In questo caso il numero di volte che le operazioni
vengono eseguite è di 99999 perché il controllo è fatto in testa. Infatti, quando
la variabile counter diventa uguale a quella di limit il ciclo termina. Alternativa
a questo difetto è quella di utilizzare un ciclo Repeat Until come nel prossimo esempio dove l’istruzione while e stata impostata per ciclare all’infinito e
l’istruzione di break viene usata per uscire dal ciclo quando una determinata
condizione si verifica. Da notare che, visto che il controllo è fatto in coda, questa
volta le operazioni verranno svolte 100000 volte.
1
2
3
4
5
6
7
8
9
10
Integer limit = new Integer( 100000);
Integer counter = new Integer( 0);
while( true){
// cicla sempre
....
if( count < limit){
break; // esci dallo statament
}
counter = counter + 1;
}
Ultimo tipo di statament per implementare i cicli viene usato per semplificare
la scrittura del codice quando il numero di volte per cui e necessario ciclare è
sempre costante.
1
2
3
4
5
6
7
8
9
Integer arraySize = 5; // lunghezza del vettore
// creo un’array di 5 elementi vuoti
Integer[] array = new Integer[ arraySize];
// per tutti gli indici che sono all’interno dell’array
for( Integer index = 0; index < arraySize; index++){
// inizializzo tutti gli elementi dell’array a 0
array[ index] = 0;
....
}
Da notare che l’istruzione index++ è uquivalente a scrivere: index = index +
1. Inoltre è importante vedere che anche in questo caso il numero di volte per
cui si cicla è arraySize-1 che è corretto visto che un array di 5 elementi ha gli
indirizzi che variano tra 0 e 4. Java inoltre propone una versione semplificata di
questo tipo di struttura particolarmente utile per i dati di tipo composto perchè
ci permette di trascendere dal concetto di indice. Ad esempio
1
2
3
4
5
Integer arraySize = 5; // lunghezza del vettore
ArrayList< Integer> array = new ArrayList< String>( arraySize);
for( Integer arrayElement : array){
array.add( 0);
}
Questo caso il risultato ottenuto è lo stesso di quel precedente, cioè si crea un
vettore di 5 componenti e li si inizializzano tutti a 0. Da notare la sintassi della
quarta riga che significa: per il successivo elemento del array, arrayElement di
2.5. CLASSI E STRUTTURA DEL CODICE
17
tipo Integer, svolgi le operazioni tra graffe. Se l’array non ha altri elementi esci.
Questo ciclo risulta utile anche per leggere facilmente gli elementi di una certa
struttura dati. Consideriamo di avere una HashSet già inizializzato a qualche
valore incognito che vogliamo copiare in un’altra variabile, si può scrivere
1
2
3
4
5
// data la variabiledi nome set di tipo HasSet<String> gia’
inizializzata
HashSet< String> copy = new HashSet< String>();// creo un Set vuoto
for( String str : set){ // per ogni elemento
copy.add( str); // copia un elemento
}
L’ultimo tipo di statament che si può avere in Java viene usato per intercettare eventuali errori che possono avvenire nel codice. In particolare alcuni
metodi possono riportare un Exception; ad esempio durante l’apertura di un
file si può avere un FileNotFoundException. Per questo, la funzione definita
dalla parola chiave Trhow permette di ricevere un errore e propagarlo ulteriormente fino al livello più alto che reagisce bloccando l’esecuzione del programma
e stampando a video il tipo di errore. Altrimenti si può decidere di intercettarlo
per compiere delle operazioni su di esso. In quest’ultimo caso la sintassi da
utilizzare è:
1
2
3
4
5
6
7
8
9
10
try{
// qualche operazione che puo’ generare errore
....
} catch( Exception ex){
// se l’errore si genera questa parte del codice viene eseguita
e poi si esce dalla struttura
...
} finally {
// se l’errore non e’ stato generato, e se le operazioni di
sopra sono finite esegue questi comandi e poi esci dalla struttura
...
}
2.5
Classi e struttura del codice
Fino ad ora abbiamo visto come si definiscono alcuni tipi di dati e come questi
possono essere manipolati attraverso operatori matematici e logici possibilmente
in modo ripetitivo o decisionale. Seguendo l’analogia della prima sezione di
questo capitolo abbiamo visto come una scatola possa essere plasmata con il
fine di implementare un determinato algoritmo. Analizziamo ora come questo
algoritmo possa essere incapsulato all’interno di una funzione richiamabile da
altre parti del codice. Lo scopo di questo procedimento e quello appunto di
isolare sotto-problemi in modo che il codice risulti più robusto, flessibile e di più
facile comprensione e sviluppo.
Tuttavia per riuscire a comprendere a pieno questo meccanismo, e quindi
cominciare a programmare in maniera oppurtuna in Java c’è bisogno di conosce
il concetto di Object, da cui deriva il nome Object Oriented Programming
Language, di cui Java è un esempio. Praticamente quello che caratterizza la
18
CAPITOLO 2. LA PROGRAMMAZIONE DI UN CALCOLATORE
Figura 2.2: Astrazione intuiva del ruolo di Object, Class e Instances variables.
programmazione in Java è il fatto che ogni entità che si può definire, e quindi
usare, è un Object. Come si nota dal grafico intuitivo di 2.5 tutto quello che
Java mette a disposizione è di un tipo di dato primitivo: l’oggetto. Gli oggetti
hanno funzioni e caratteristiche particolari che affronteremo a breve ma quello
che è importante capire è cosa sia veramente in grado di fare la parola chiave
new. In pratica ogni volta che viene utilizzata, questa crea un’istanza dell’
oggetto chiamato. Instance diverse, come ad esempio il numero intero a e b
sono completamente indipendenti tra loro a meno di particolari condizioni. Una
nuova classe, che definisce un determinato oggetto, creata dallo sviluppatore è
soggetta agli stessi processi di qualsiasi altro. Un problema non banale, che è
stato risolto dai sviluppatori di Java, è quello di regolare il flusso di dati tra
le varie Instance, che possono essere anche nell’ordine delle centinaia di unità,
in modo che siano sempre coerenti. Per fare questo sono state introdotte le
seguenti parole chiave che possono essere utilizzate per qualificare una variabile
o una funzione:
• this: identifica l’Instance corrente.
• public: permette a qualsiasi Instance di accedere a questo dato.
• private: permette solo alla Instance this di accedere a questo dato.
• static: indica che questo dato è condiviso tra tute le Instance appartenenti
allo stesso insieme. In questo contento la parola chiave this e new non ha
alcun significato e quindi non può essere utilizzata.
• final: indica che il dato, dopo essere stato creato, non può essere modificato da nessuno. Utilizzato per definire principalmente costanti.
ancora una volta queste sono quelle più usate, ne esistono altre, come ad esempio
protected o synchronized usate in progetti più complessi di quelli affrontati
in questo corso.
2.5. CLASSI E STRUTTURA DEL CODICE
19
Prima di poter analizzare la struttura di una classe ci manca un ultimo
concetto: la sintassi di base per una generica funzione o metodo.
1
2
3
4
5
6
modifiers OutputDataType methodName( InputDataType1 inputParam1Name,
InputDataType2 inputParam2Name, ...){
//body of the method
...
return( outputParameter) // of type OutputType
}
un’esempio più pratico può essere:
1
2
3
4
5
6
7
8
9
10
static public String getDataTime( String dataFormatParam){
// create un nuovo oggetto di tipo Date
Date date = new Date();
// create un nuovo oggetto di tipo SimpleDateFormat inizializzato
con la stringa di formattazione
SimpleDateFormat dateFormat = new SimpleDateFormat( dataFormatParam);
// fai il parsing (conversione) dall’oggetto alla stringa
String str = dateFormat.format( date);
// ritorna la stringa alla funzione chiamante
return( str);
}
Questa è una metodo di nome: getDataTime, che richiede in ingresso una variabile di tipo String che assume il nome di dataFormatParam. Infine questa
funzione restituisce una stringa che contiene la data corrente visualizzata in
accordo con il parametro di ingresso. Ammettiamo che questo metodo risieda all’interno della classe di nome DataClass sarà possibile scrivere in un’altra
parte del codice:
1
String actualData = DataClass.getDataTime( "yyyy/MM/dd HH:mm:ss");
e cosi il valore della variabile actualData data sarà per esempio: “2013/10/15
16:16:39”. Per maggiore informazioni rispetto alla stringa di formattazione per
la classe SimpleDateFormat, vedere qui. Se ad esempio volessimo stampare a
video una generica stringa potremmo pensare di creare il metodo:
1
2
3
public void printString( String input){
System.out.println( input); // stampa a video
}
Si noti che la funzione non ritorna nessun tipo di parametro in uscita e quindi
si deve utilizzare la parola chiave void ed eliminare return. Ammettendo
che anche questa funzione sia all’interno della classe DataClass, sarà possibile
chiamarla digitando le seguenti linee di programmazione in un qualsiasi altro
punto del codice:
1
2
3
4
5
DataClass instance = new DataClass(); // costruttore vuoto
// chiamata ad un metodo statico
String actualData = instance.getDataTime( "yyyy/MM/dd HH:mm:ss");
// chiamata ad un metodo dinamico, ho bisogno dell’Instance variable
instance.printString( actualData);
20
CAPITOLO 2. LA PROGRAMMAZIONE DI UN CALCOLATORE
Ho introdotto in questi esempi il concetto di classe anche se ancora non si è visto come sono definite. Tuttavia risulta davvero importante capire come diversi
oggetti vengono creati e trasferiti da un metodo ad un altro in modo da implementare un programma che abbia il comportamento voluto. Fate attenzione
hai nomi delle variabili e al loro valore. Inoltre cercate di ricostruire i passaggi
sequenziali che vengono effettuati partendo dalla funzione chiamanta, descritta
nelle ultimo set di linee. In pratica quando un generico metodo viene chiamato
da un altro questo deve fornire dei parametri in ingresso che siano compatibili
con il tipo richiesto. Dopo di che il passaggio dell’esecuzione del programma
viene dato alla funzione chiamata che fa una copia dei parametri di ingresso,
li manipola per arrivare ad una certo risultato e quando ha finito è costretto
a ritornare un tipo di dato sempre coerente con la sua definizione. A questo
punto il comando dell’esecuzione ritorna al programma chiamante che passa all’istruzione successiva. Questo è uno dei concetti più importanti per imparare
la programmazione, e se volete approfondire le vostre conoscenze propongo qui
un interessante parte del tutorial.
Ora siamo finalmente pronti per vedere la struttura completa di una classe,
definita dalla parola chiave class. Per convenzione tutte le classi hanno un
nome che inizia con la lettere maiuscola, nel nostro esempio MyClass, nomi non
posso contenere spazi, devono essere diversi da ogni parola chiave e possono
contenere numeri a patto che non siano posti come primo carattere. I nomi
delle costanti vengono solitamente descritti con lettere tutte maiuscole. Tutte
le classi presentano un constructor che è quel particolare metodo che viene
lanciato automaticamente quando si crea una nuova Instance, questo metodo
non ritorna nessun valore e non necessita della parola chiave void, infine deve
avere il nome esattamente uguale a quello della classe. Solitamente questo viene
utilizzato per inizializzare i diversi attributi della classe. Gli attribute che sono
variabili visibili in qualsiasi parte delle classe stessa. Si noti che variabili definite in un certo punto del programma sono visibili solo all’interno delle parentesi
graffe che la contengono, buona norma per creare un codice incapsulato è quella
di avere variabili che siano visibili per la più piccola porzione di codice possibile.
Ergo le variabili create come attributi devo essere del minor numero possibile.
Infine ci sono i vari metodi che la classe implementa, la loro forma dipende
strettamente da quello che la classe intende implementare. Un tipo particolare
di metodi vengono detti Getter e Setter e servono principalmente per poter
fornire in uscita a acquisire in ingresso attributi. Per fare questo potremmo pensare di impostare gli attributi come pubblici e renderli accessibili dall’esterno,
tuttavia questo porta alla creazione di un programma poco modulare e quindi
è vivamente sconsigliato. Un generale esempio di una classe può essere scritto
come nell’esempio successivo che vuole essere una linea guida nella stesura e
comprensione delle diverse parti che formano una classe, ma che alla fine sono
sempre definite come variabili ed il loro tipo, e come funzioni e il loro tipo di
ingresso e uscita.
1
2
// full package qualifier
package package.name.path.to.MyClassName;
3
4
5
6
// importa classi esterne da instansiare per creare oggetti
import java.lang;
import ...
2.5. CLASSI E STRUTTURA DEL CODICE
21
7
8
9
10
11
12
13
// dichiarazione della classe
public class MyClassName{
// ###### attribute DELLA CLASSE ############
// esempio di una costante stringa
// (si intende implicitamente new String("stringa sempre costante"))
public static final String MY_CONSTANT_STRING = "stringa sempre
costante";
14
15
16
17
// esempio di una variabile condivisa in tutta la classe
private Integer counter = 0; // (si intende implicitamente: new
Integer(0))
private static Boolean created = false; // new Boolean( false)
18
19
20
21
22
23
// ###### CONSTRUCTOR DELLA CLASSE ############
public MyClassName( DataType inputParameter){
// il costruttore viene chiamato automaticamente quando la
classe viene creata utilizzando l’istruzione new. Solitamente e’
utilizato per inizializzare i attribute.
...
}
24
25
26
27
28
29
30
31
// ###### METHOD DELLA CLASSE ############
// metodo che puo’ essere chiamato solo dalla classe stessa
private Boolean checkState(){
// metodo senza parametri in ingresso che puo’ fare una generica
operazione, ad esempio controllare lo stato dei attribute e reagire
in base hai loro valori.
...
return( ...);
}
32
33
34
35
36
37
// esempio di metodo che puo’ essere chiamato da una qualsiasi
classe esterna che conosce questa instanza
public void printState(){
// metodo senza parametri ne in ingresso ne in uscita che puo’
fare una generica operazione, ad esempio stampare sullo schermo i
valori dei attribute
...
}
38
39
40
41
42
43
44
45
// metodi setter e getter indispensabili per far modificare, in
ingresso ed uscita un attribute da una classe esterna, visto che
questi devono essere privati a meno che non abbiano il modificatore
configurato come final.
public void setCounter( Integer counterIn){
this.counter = counterIn
}
public Integer getCounter(){
return( this.counter);
}
46
47
// esempio metodo statico accessibile dall’esterno. Questo puo’
modificare solo variabili statiche.
22
public static Boolean isCreated(){
return( created);
}
48
49
50
51
CAPITOLO 2. LA PROGRAMMAZIONE DI UN CALCOLATORE
}
2.6
Esercizio 2.0: Hello World test con Eclipse
Ora che sono state introdotti i concetti fondamentali della programmazione in
Java passiamo alla parte pratica e cerchiamo di fissarli meglio. Il primo esempio
introduce l’editor di testo Eclipse ed implementa una versione leggermente più
complessa del popolare test Hello World. Questa documentazione suppone di
avere già installato il software development kit SDK 6.0, che contiene la Java
Virtual Machine JVM, tipicamente installata in ogni computers per lanciare e
quindi utilizzare qualsiasi applicazione Java, e anche alcune librerie volte a creare
e compilare nuovi programmi. Inoltre, si utilizzerà un famoso integrated development environment IDE in grado di gestire complessi progetti semplificando
notevolmente lo sviluppo.
Vediamo come è possibile creare un esempio per testare che l’installazione
e andata a buon fine e per prendere confidenza con la parte pratica della programmazione. Per prima cosa aprite ECLIPSE, vi comparirà una finestra che
chiede di selezionare una cartella. Questa conterrà tutte le configurazioni di
ECLIPSE e anche i programmi in fase di sviluppo; tipicamente prende il nome
di WorkSpace. Una volta creata e confermata l’operazione, si apre una scheda
di benvenuto, una volta chiusa e accettato il cambio al Prospective di default si
arriva alla schermata base dell’editor. Per creare un nuovo progetto selezionate:
File → New → Project → Java Project
e premete nel tasto Next. La successiva finestra richiede il nome del vostro
progetto, ad esempio helloWorld, e mostra le configurazioni standard di un
progetto Java. Premete Finish e così creerete il progetto. Ora è possibile vedere
sulla sinistra della finestra la gerarchia degli oggetti a disposizione. Questa
contiene una cartella vuota: src, ed una serie di librerie caricate di default
durante la sua creazione. Dentro questa cartella risiederanno tutti i programmi
durante il loro sviluppo.
Qualsiasi programma necessita di un punto da cui far partire la sua esecuzione, in termini più informatici un Main method che risieda dentro una
classe. Per crearla cliccate con il tasto destro del mouse sulla cartella src, vista
prima, e poi degitate:
New → Class
Facendo così si apre una finestra che contiene le impostazioni di default per
creare una classe e richiede: il suo nome, ad esempio MainMethod ed un’eventuale nome del pacchetto, che potete lasciare vuoto. Prima di premere Finish
ricordatevi di spuntare l’opzione:
1
public static void main(String[] args)
per indicare che questa è un tipo di classe speciale, quella che darà inizio all’esecuzione. Una volta premuto Finish si nota che questa operazione ha creato un
2.6. ESERCIZIO 2.0: HELLO WORLD TEST CON ECLIPSE
23
file testuale che descrive la classe appena creata. Ripetete la stessa procedura
per crearne una nuova con il nome HelloWorld ma questa volta inserite nel campo Package il nome: tests. La classe che si vuole creare è di tipo standard quindi
non risulta necessario cambiare nessuna opzione. Una volta premuto Finish, a
sinistra della schermata, si potrà notare la presenza di una nuova classe all’interno del pacchetto test. Modificate il file di testo appena creato in modo che
risulti:
1
package tets;
2
3
public class HelloWorld {
4
5
6
7
8
/**
* Stringa constante da visualizzare di default nella console
*/
private static final String textToDisply = "hello world !!!";
9
10
11
12
13
/**
* Stringa da visualizzare nella console
*/
private String text = null;
14
15
16
17
18
19
20
21
/**
* Costruttore della classe, e’ il primo metodo a partire.
* Setta la stringa da stampare di default
*/
public HelloWorld(){
text = textToDisply;
}
22
23
24
25
26
27
28
29
30
/**
* Se chiamata restituisce il testo da visualizzare.
*
* @return il testo da visualizzare
*/
public String getText() {
return text;
}
31
32
33
34
35
36
37
38
39
40
/**
* Se chiamata sostituisce l’attuale stringa da
* visualizzare con quella esterna.
*
* @param text la nuova stringa da visualizzare
*/
public void setText( String externalText) {
text = externalText;
}
41
42
43
44
45
46
/**
* Se chiamata scrive sulla console la stringa testuale
* presente in questa classe. Se questa e’ uguale a quella
* di default restituisce vero alla funzione che
* l’ha chiamata, altrimenti ritorna false.
24
CAPITOLO 2. LA PROGRAMMAZIONE DI UN CALCOLATORE
*
* @return vero se la stringa e’ uguale a quella di default
*/
public Boolean printOnConsole(){
// scivi stinga sulla console
System.out.println( text);
47
48
49
50
51
52
53
// se la stringa e’ uguale a quella di defualt
// ritorna vero, altrimenti false
if( text.equals( textToDisply)){
return( true);
} else {
return( false);
}
54
55
56
57
58
59
60
}
61
62
63
}
Inoltre, modificate la prima classe creata, MainMethod, in modo che risulti
uguale a:
1
2
// importa la classe e rendila visibile
import tets.HelloWorld;
3
4
public class MainMethod {
5
6
7
8
9
10
11
/**
* lancia il programma hello world.
*
* @param args parameteri non usati
*/
public static void main(String[] args) {
12
13
14
15
16
// crea una nuova classe hello world e salvala nella
variabile di nome "cl"
HelloWorld cl = new HelloWorld();
// stampa la stringa di defualt e ignora il risultato di
ritorno
cl.printOnConsole();
17
18
19
20
21
22
23
24
25
26
// ottieni la stringa conservata nella classe cl e
salvala nella variabile di nome str
String str = cl.getText();
// aggiungi un’altri caratteri nella stringa
str = str + " -- altri caratteri --";
// risetta la stringa nella classe h
cl.setText( str);
// stampa la nuova riga e conserva il risultato vero se
e’ uguale a quella
// di defualt nella variabile flag
Boolean flag = cl.printOnConsole();
2.6. ESERCIZIO 2.0: HELLO WORLD TEST CON ECLIPSE
25
27
// controlla se la stringa e’ uguale a quella di default
checkChanges( flag);
28
29
30
// crea una nuova classe HelloWorld con nome cl2
indipendente da cl
HelloWorld cl2 = new HelloWorld();
// stampa il testo e controlla se e’ quello di default
checkChanges( cl2.printOnConsole());
}
31
32
33
34
35
36
/**
* Dato un valore vero o falso in ingresso questo metogo
* scrive su schermo se la stringa e’ quella di default o meno.
*
* @param bool
*/
public static void checkChanges( Boolean bool){
if( bool == true){
// se si, stampa nella console che la stringe
non e’ stata cambiata
System.out.println( "la stringa non e’ stata
cambiata");
} else {
// se no, stampa che la stringa e’ stata cambiata
System.out.println( "la stringa e’ stata
cambiata");
}
}
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
}
Questo semplice programma implementa una classe in grado di stampare
una stringa testuale e usa il metodo main per gestirla. Il codice è ampiamente
commentato, o come si usa in termini informatici self-documented, e propone
un ottimo punto di partenza per abituarsi al tipo di formalismo generalmente
utilizzato nei linguaggi di programmazione. Per lanciare il programma basterà
cliccare nella barra alta di ECLIPSE su:
Run -> Run
Una volta lanciato tutto, il metodo main(. . . ) viene eseguito sequenzialmente
ed il risultato delle operazioni verrà visualizzato nella console che si trova in
basso. A meno di errori si potrà leggere:
hello world !!!
hello world !!! – altri caratteri –
la stringa è stata cambiata
hello world !!!
la stringa non è stata cambiata
Inoltre è importante notare la gerarchia delle classi che abbiamo appena
creato. Da qui si nota che è possibile descrivere le classi in modo gerarchico
e questo è ben visibile sotto forma di cartelle se va all’interno di src presente
26
CAPITOLO 2. LA PROGRAMMAZIONE DI UN CALCOLATORE
Figura 2.3: gerarchia delle classi
nel workspace di Eclipse. Tale proprietà suggerisce come classi possano essere
definite in modo gerarchico, questo permetterebbe di fornire gerarchie logiche
tra classi che descrivono diversi oggetti. In fatti in maniera similare all’organizzazione ontologica, le classi possono essere relazionate tra di loro fornendo così
un potentissimo software perfettamente modulare. Si è detto che tutto quello che è descritto in Java è un’Object, ad esempio String, Integer, cl, cl2. . . ,
in realtà anche HelloWorld è un Object. Un oggetto di tipo molto particolare
chiamato Class; questo è un file testuale in grado di essere compilato insieme
all’istruzione new, così da poter richiedere al calcolatore di creare e manipolare
nuovi oggetti. In particolare, il primo oggetto che l’istruzione crea, sarà dello
stesso tipo della classe: cl, cl2. Così facendo, è vero anche che i file testuali possono avere delle relazioni logiche tra di loro, ma non è detto che gli oggetti che
ne vengono creati lo abbiano. Per esempio una persona potrebbe appartenere
alla classe degli animali così come un cane, ma non è detto che si influenzano
a vicenda. Certo rimane che una parte della definizione di persona e uguale a
quella di un cane, sono entrambi animali.
2.7
Esercizio 2.1: Ordinare un’array numerico
Qui si propone lo sviluppo di una classe in grado di ordinare un vettore numerico
in modo crescente o decrescente. Inoltre questa classe presenta anche la capacità
di tenere a memoria del proprio stato, quindi di ricordarsi cosa ha fatto la volta
prima che è stata chiamata. Si consiglia di guardare la classe SimpleSorter in
modo da capire come è strutturata, per poi seguire passa passo, partendo dalla
prima all’ultima istruzione del metodo main della classe Runner, saltando parti
dei codici chiamate da ciascuna funzione in modo sequenziale. Lo scopo è capire
qual’è il valore delle variabili ad ogni instante computazionale e perché viene
generato quel particolare output testuale.
1
package tests;
2
3
public class SimpleSorter {
4
5
// valore constante da settare di default
2.7. ESERCIZIO 2.1: ORDINARE UN’ARRAY NUMERICO
6
7
8
27
public static final Integer[] DEFAULT_TEST =
new Integer[]{
5,12,9,8,2,6,6,2,2,8,0,3,2,1,56,45,6,43,6,5,12,43,265,
4765,756,43,4234,48,77,63,21,54,69,1,0,39,78,54};
9
10
11
12
13
14
15
16
// attribute, rappresentano lo stato della classe (cosa sa di
se).
// la lista data da ordinare
private Integer[] toSort;
// ultima lista ordinata
private Integer[] sorted;
// ultima lista sorted deriva da quella toSort, o ne e’ stata
immessa un’altra? si, no
private Boolean isSorted = false;
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// constructor di defualt
public SimpleSorter( Integer dimension){
// crea l’oggeto: array di interi vuoto
toSort = new Integer[ dimension];
// inizializza l’array toSort con i primi dimension-1
elementi di DEFAULT_TEST
for( int i = 0; i < dimension; i++){
toSort[ i] = DEFAULT_TEST[ i];
}
}
// constructor configurabile
public SimpleSorter( Integer[] incomingTest){
// il dato in input e’ considerato gia’ creato e
inizializzato
toSort = incomingTest;
}
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
// algoritmo che implementa ordinamento per irsezione,
// se parametero in ingresso e’ true ordina in modo crescente,
// se e’ falso in modo decresente
public Integer[] sortList( Boolean orderInputPar){
// per tutti gli elementi dell’array
for(int i = 1; i < toSort.length; i++) {
Integer key = toSort[i]; // ottieni un’elemento (i-esimo)
Integer k = i - 1;
//while(( k >= 0) && (toSort[k] > key)) { //implements
only ordine crescente
while(( k >= 0) && ( ascendingOrdering( toSort[k], key,
orderInputPar))) {
// in modalita’ crescente: se negli elementi
prima c’e’ ne uno maggiore spostalo piu’ avanti
toSort[k + 1] = toSort[k];
k--;
//eventualmente stampa passo passo
dell’algoritmo.
//System.out.println( this.toString());
}
toSort[k + 1] = key;
}
28
CAPITOLO 2. LA PROGRAMMAZIONE DI UN CALCOLATORE
51
52
53
54
// ora che l’operazione e’ stata fatta aggiorna lo stato
della classe
sorted = toSort;
isSorted = true;
55
return( sorted);
56
57
}
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
// metodi privati per rerndere l’algoritmo modulare
private Boolean ascendingOrdering( Integer toSortElement,
Integer key, Boolean order){
if( order){ // true -> ascendending order
if( toSortElement > key){
return( true);
} else {
return( false);
}
} else { // false -> discendending orger
if( toSortElement < key){
return( true);
} else {
return( false);
}
}
}
75
76
77
78
79
80
81
82
83
84
85
// getter e setter per il campo toSort
public Integer[] getToSort() {
return toSort;
}
public void setToSort(Integer[] toSort) {
this.toSort = toSort;
// se e’ stat settata una nuova lista da ordinare
// non sara’ di sicuro gia’ stata ordinata
this.isSorted = false;
}
86
87
88
89
90
91
92
93
94
95
96
// getter e setter per il campo sorted
public Integer[] getSorted() {
// ritorna nul nel caso in cui venga chiamato prima di
sortList()
return sorted;
}
// no!!!!!!!!!!!!
//public void setSorted(Integer[] sorted) {
//
this.sorted = sorted;
//}
// nessuno dall’esterno mi dara’ mai una lista ordinata
97
98
99
100
101
// getter e setter per il campo isSorted
public Boolean getIsSorted() {
return isSorted;
}
2.7. ESERCIZIO 2.1: ORDINARE UN’ARRAY NUMERICO
//
//
//
//
//
lista
102
103
104
105
106
29
no!!!!!!!!!!
public void setIsSorted(Boolean isSorted) {
this.isSorted = isSorted;
}
nessuno dall’esterno mi puo’ dire e’ se ho ordinato o meno la
107
public String toString(){
String output = "";
for( int i = 0; i < toSort.length; i++){
output = output + toSort[i].toString() + " , ";
}
return( output);
}
108
109
110
111
112
113
114
115
}
di seguito la classe che parte all’avvio ed utilizza quella precedente
1
import tests.SimpleSorter;
2
3
public class SortRunner {
4
5
6
7
8
9
// metodo main, da dove parte l’esecuzione
public static void main(String[] args) {
// flag per configurare il tipo di ordinazione
// true per crescente o false per decrescente
Boolean ascendingOrder = true;
10
11
12
13
14
15
16
17
18
19
// creo nuovo oggetto della classe sorter,
// con i primi dieci elementi di default
SimpleSorter sorter = new SimpleSorter( 10);
// ordino la lista
Integer[] sorted = sorter.sortList( ascendingOrder);
// stampo l’oggetto array
System.out.println( "lista ordinata : " + sorted);
// stampo l’oggetto SimpleSorter
System.out.println( "lista ordinata : " +
sorter.toString() + "... e’ ordinata? " + sorter.getIsSorted());
20
21
System.out.println("-----------------------------------------");
22
23
24
25
26
27
28
29
30
31
32
33
34
// creo una nuova lista
Integer[] newListToOrdered = new Integer[]{9,8,6,3,1,34};
// setto la lista all’interno dell sorter
sorter.setToSort( newListToOrdered);
// chiedo al sorter se la lista e’ ordinata
Boolean flag = sorter.getIsSorted();
// ordino lista
sorted = sorter.sortList( ascendingOrder);
// stampo l’oggetto array in modo comprensibile
System.out.print( "lista ordinata : ");
for( int i = 0; i < sorted.length; i++){
// stampa sensa andare a capo
30
System.out.print( sorted[i] + " , ");
}
// stampo se la lista era ed e’ ordinata e vado a capo
System.out.print( "... era ordinata? " + flag);
System.out.print( "... e’ ordinata? " +
sorter.getIsSorted() + "\n");
}
35
36
37
38
39
40
41
CAPITOLO 2. LA PROGRAMMAZIONE DI UN CALCOLATORE
}
L’output generato dal metodo mail sarà simile a:
lista ordinata : [Ljava.lang.Integer;@16181be
lista ordinata : 2 , 2 , 2 , 5 , 6 , 6 , 8 , 8 , 9 , 12 , ... e’ ordinata? true
—————————————–
lista ordinata : 1 , 3 , 6 , 8 , 9 , 34 , ... era ordinata? false... e’ ordinata? true
2.8
Esercizio 2.2: Ordinamento alfabetico
Provate come esercizio, a scrive un programma simile a quello dell’esempio 1.1
dove però si tenta di ordinare una stringa seguendo il metodo alfanumerico.
Ad esempio, inserendo in ingresso il valore “JavaProgramming” il risultato voluto è: “JPaaaggimmnorrv” considerando un ordinamento crescente, oppure
“vrronmmiggaaaPJ” considerandone uno decrescente.
Ci possono essere molti metodi per implementare una cosa simile. In un caso
reale di programmazione si predilige usare librerie già disponibili nelle classi de
default di Java in quanto permettono di avere un risultato più stabile e robusto,
ad esempio considerate che il caso di prima può essere risolto anche scrivendo
semplicemente:
1
2
3
4
5
6
Integer[] newListToOrdered = new Integer[]{9,8,6,3,1,34};
Arrays.sort( newListToOrdered);
for(int i = 0; i < newListToOrdered.length; i++){
// stampa risultato
System.err.print( newListToOrdered[i] + " ");
}
Tuttavia è utile vedere i meccanismi che stanno dietro queste istruzioni almeno
una volta per comprendere a pieno il linguaggio. Una strada sicuramente modulare può essere quella di usare il codice ASCII per assegnare ad ogni lettera
un numero e convertire così una stringa lista di numeri interi. Dopo di che
sarà possibile riutilizzare la classe SimpleSorter per ordinare un array di numeri. Tuttavia vi serve sapere che la conversione da numero Integer a Stringa,
e viceversa, seconda la mappatura ASCII si può ottenere con le istruzioni:
1
2
3
4
Integer asciiCode = 65; // = ’A’ ascii code to str
String str = ((char) (int) asciiCode);
String str1 = "A";
Integer asciiCode1 = ((int) (char) str1); // = 65 str to asci code