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
© Copyright 2024 Paperzz