2015 Ripetizioni Materie Scientifiche Prof. Ing. Per. Ind. Alessandro Bianco [RIPETIZIONI MATERIE SCIENTIFICHE] Dispensa introduttiva al linguaggio Java Ripetizioni Materie Scientifiche Indice Dalla programmazione strutturale a OOP……………………………………………………..pag. 2 Cos’è il JDK……………………………………………………………………………………pag. 7 Il primo programma in Java……………………………………………………………………pag. 12 IDE e strumenti di sviluppo avanzati…………………………………………………………..pag. 17 Variabili e dichiarazioni……………………………………………………………………….pag. 25 Tipi primitivi di Java e valori………………………………………………………………….pag. 30 Classi wrapper…………………………………………………………………………………pag. 34 Boxing, unboxing e autoboxing……………………………………………………………….pag. 36 Operatori e casting……………………………………………………………………………..pag. 39 If e switch: costrutti condizionali in Java……………………………………………………...pag. 44 Ciclo for e while, costrutti iterativi in Java……………………………………………………pag. 49 Break e Continue………………………………………………………………………………pag. 53 I Metodi in Java………………………………………………………………………………..pag. 55 Overload di metodi e variable arguments……………………………………………………...pag. 60 Metodi statici (static) e metodi di istanza……………………………………………………...pag. 63 Array in Java…………………………………………………………………………………...pag. 66 Stringhe in Java………………………………………………………………………………...pag. 70 Enum, gestire le enumerazioni………………………………………………………………...pag. 74 Principi di OOP………………………………………………………………………………...pag. 81 Classi, oggetti e costruttori…………………………………………………………………….pag. 87 Ereditarietà in Java…………………………………………………………………………….pag. 92 http://ripetizioni.doomby.com - https://www.facebook.com/Prof.Ing.Bianco https://twitter.com/ProfBianco Pag. 1 Ripetizioni Materie Scientifiche Dalla programmazione strutturale a OOP Immaginiamo di dover scrivere un programma per gestire conti bancari dovremo provvedere, per ciascuno dei conti gestiti, a prenderci carico di rappresentare lo stato (un nome, owner ed un importo, amount, per semplicità) e scrivere alcune procedure per modificarlo (funzioni, subroutine), oltre ad una probabilmente per crearlo. Pensiamo ad operazioni come versamento e prelievo alle quali passare ogni volta tra gli argomenti la struttura conto su cui operare. Con l’approccio OO nel medesimo programma potremo invece pensare di avere un tipo di oggetto, ad esempio chiamato Conto che contenga insieme: lo stato (che chiameremo fields, i campi dell’oggetto); le procedure per la sua gestione (che chiameremo methods, metodi). In questo modo possiamo modellare nella medesima sezione del codice (che chiameremo classe), sia lo stato che il “behavior”, il comportamento delle entità che utilizziamo. L’integrazione dello stato e del behavior è comunemente chiamata encapsulation (incapsulamento) ed è la base di quella caratteristica della programmazione OO che viene chiamata information hiding che consiste nel mantenere interno a precise sezioni del codice l’accesso allo stato delle entità utilizzate. È interessante osservare, che OOP è una metodologia ed uno stile di progettazione del software che, tranne in casi particolari, può essere impiegato indipendentemente dal linguaggio ma che risulta estremamente più facile da adottare se il linguaggio (come Java, C++ e altri) ne fornisce le primitive (come le classi in Java). Classi e oggetti in Java Nell’approccio OO in Java non solo si definiscono gli oggetti su cui si intende lavorare ma li si organizzano in categorie: una classe è esattamente la definizione delle proprietà e dei metodi che avrà ogni elemento della categoria. La distinzione tra oggetto e classe è enfatizzata dalle seguenti definizioni: Classe è una collezione di uno o più oggetti contenenti un insieme uniforme di attributi e servizi, insieme ad una descrizione circa come creare nuovi elementi della classe stessa (Edward Yourdan); Un oggetto è dotato di stato, behavior ed identità; la struttura ed il comportamento di oggetti simili sono definiti nelle loro classi comuni; i termini istanza ed oggetto sono intercambiabili (Grady Booch). Definire una classe in Java: Una classe in java si definisce con la keyword class: /** * Classe per rappresentare un Conto */ public class Conto { http://ripetizioni.doomby.com - https://www.facebook.com/Prof.Ing.Bianco https://twitter.com/ProfBianco Pag. 2 Ripetizioni Materie Scientifiche private double amount; private String owner; // costruttore public Conto(String owner, double initialAmount) { this.owner = owner; this.amount = initialAmount; } public void versamento(double qty) { amount += qty; } public boolean prelievo(double qty) { if(amount < qty) return false; amount -= qty; return true; } /* Getters */ public double getAmount() { return amount; } public String getOwner() { return owner; } } dove si può osservare che: il corpo (dichiarazioni di variabili e metodi) della classe è racchiuso tra parentesi graffe; le linee sono terminate da punto-e-virgola (;); le linee che iniziano per doppio slash ( //) sono commenti così come tutto quello che è racchiuso tra ‘/*‘ e ‘*/‘; il doppio asterisco all’inizio di un commento (/**) ha un significato speciale in relazione al comando javadoc che sarà introdotto in seguito e serve per la generazione di documentazione a partire dai commenti nel codice. Qualificatori di visibilità I qualificatori di fronte alle dichiarazioni di tipi o metodi ( public e private) servono per determinare la visibilità degli elementi cui sono applicati: Qualificatore Descrizione Significa che sarà visibile (accessibile se si tratta di un field, chiamabile se si tratta di private un metodo) solo all’interno della classe che lo contiene. significa invece che sarà accessibile anche dall’esterno della classe stessa. public Esistono altri 2 possibilità circa la visibilità: Qualificatore Descrizione accessibile solo dalle classi derivate, ne capiremo il significato in seguito protected http://ripetizioni.doomby.com - https://www.facebook.com/Prof.Ing.Bianco https://twitter.com/ProfBianco Pag. 3 Ripetizioni Materie Scientifiche default Non è una keyword: si ha la visibilità di default se nessuna delle precedenti viene specificata e implica visibilità per tutte le classi che si trovano all’interno del medesimo package (qualche dettaglio più avanti). Costruttore Di particolare rilevanza è il metodo contrassegnato con il commento // costruttore: è diverso dagli altri metodi (versamento e prelievo) in quanto non dichiara un valore di ritorno e serve (come richiesto dalla definizione di classe di Yourdan) per creare nuove istanze di elementi di quella classe. Il costruttore delle classi deve essere utilizzato con la keyword new, la scrittura: Conto myConto = new Conto("Francesca", 1.0e8); Significa che vogliamo costruire (allocare in memoria) una nuova istanza di un oggetto della categoria Conto ed inizializzarlo con owner “Francesca” e amount 1 miliardo. Qualora non avessimo specificato nessun costruttore nella definizione della classe ne sarebbe stato creato uno di default senza argomenti. Si possono aggiungere quanti costruttori si vogliono a patto che abbiano tutti diversa signature (firma, ovvero tipo e numero degli argomenti). I costruttori devono avere nome uguale al nome della classe. this All’interno del costruttore si può osservare anche l’uso della keyword this che in ciascuna classe rappresenta sempre l’istanza corrente. Si osservi come nel costruttore si usi this nella prima riga per disambiguare la variabile owner: senza this l’assegnazione non avrebbe effetti (ed il compilatore la segnalarebbe anche con uno warning) in quanto significherebbe che si assegna il contenuto della variabile owner ad owner stessa, mentre con la qualificazione della variabile significa che stiamo assegnando il contenuto della variabile owner, che è stata passata come argomento al metodo, al field owner della classe Conto (o meglio dell’istanza corrente dell’elemento della classe Conto che stiamo costruendo). Notazione ‘.’ Una volta costruito l’oggetto conto sarà possibile utilizzare i suoi metodi con la notazione: conto.versamento(10); conto.prelievo(100); L’accesso ai field sarebbe possibile con la medesima notation (e.g. conto.amount = 5) ma è proibito per scelta della nostra classe che definisce i field come private e quindi il compilatore ce lo segnalerebbe come errore. Ereditarietà: estendere una classe L’utilità della strutturazione OO diventa palese, a mio parere, se si pensa a questo punto che potrebbe nascere la necessità di estendere l’immaginario programma per la gestione di conti con una banalizzazione di un conto in cui alcune somme vengano vincolate (e.g. nel tempo: sono nel http://ripetizioni.doomby.com - https://www.facebook.com/Prof.Ing.Bianco https://twitter.com/ProfBianco Pag. 4 Ripetizioni Materie Scientifiche conto ma disponibili solo dopo una certa data); in tal caso potremmo “estendere” direttamente la classe Conto: public class ContoVincolato extends Conto { private double importoVincolato; private Date fineVincolo; public ContoVincolato(String super(o,a); importoVincolato=0; } o, double a) { public boolean vincola(double qty, Date fineVincolo) { if(importoVincolato != 0 || getAmount() < qty) return false; else { prelievo(qty); importoVincolato = qty; this.fineVincolo = fineVincolo; return true; } } /** * svincola l'importo vincolato se è passata la data di vincolo */ public boolean svincola() { if(importoVincolato > 0) { Date adesso = new Date(); if( adesso.after(fineVincolo) ) { this.versamento(importoVincolato); this.importoVincolato = 0; return true; } } return false; } //overloaded getAmount public double getAmount(Date quando) { if(importoVincolato == 0 || quando.before(fineVincolo)) return getAmount(); return getAmount() + importoVincolato; } public double getAmount() { svincola(); return super.getAmount(); } } Questo codice serve a mostrare come estendere (cioè aggiungere funzionalità) alla classe Conto senza doverne riscrivere il codice. In questo caso si è aggiunta la funzionalità di vincolare un http://ripetizioni.doomby.com - https://www.facebook.com/Prof.Ing.Bianco https://twitter.com/ProfBianco Pag. 5 Ripetizioni Materie Scientifiche importo fino ad una certa data e inserito un metodo che renderà di nuovo disponibile l’importo vincolato se chiamato dopo la data di scadenza del vincolo. In casi come questi si dice che la classe ContoVincolato deriva da Conto da cui eredita i field conto ed owner ed i metodi versamento e prelievo (che infatti sono usati all’interno di ContoVincolato). La keyword super usata nel costruttore di ContoVincolato serve per riferirsi alla classe padre i.e. quella da cui si deriva. In casi come questo nei quali la classe padre ha un costruttore con parametri la classe figlia (derivata) ha l’obbligo di chiamare esplicitamente il costruttore del padre che invece è implicitamente chiamato nel caso che sia presente un costruttore vuoto (senza argomenti). Il metodo getAmount(Date arg) è invece un esempio di metodo ‘overloaded‘ cioè un metodo definito più volte (in questo caso una volta nelle classe padre ed una volta nella classe derivata, ma non è un obbligo), ma con argomenti diversi; il compilatore capirà quale chiamare sulla base degli argomenti che gli vengono passati. Infine il metodo getAmount() è un esempio di ridefinizione di un metodo da parte della classe derivata; quando chiamato su una istanza di un oggetto di tipo ContoVincolato il metodo getAmount non eseguirà quello definito in Conto ma prima chiamerà la funzione svincola e solo dopo chiamerà la getAmount di Conto; si noti l’uso della keyword super in questo caso necessaria per evitare un loop infinito di chiamate ricorsive. Package Un ultimo concetto interessante in Java per quanto riguarda le classi è quello dei package Ogni classe appartiene ad un package (dichiarato all’inizio del file in cui è definita una classe con la keyword package appunto) e serve a definire a qualche gruppo di classi essa appartiene. Un package è del tutto simile ad un ‘path’ in un filesystem solo che al posto degli slash ( /) le componenti sono separate da punti (.). Nelle prossime lezioni ci sarà occasione di approfondire questo ed altri concetti legati alla struttura delle classi in Java ed alla programmazione OO in generale. http://ripetizioni.doomby.com - https://www.facebook.com/Prof.Ing.Bianco https://twitter.com/ProfBianco Pag. 6 Ripetizioni Materie Scientifiche Cos’è il JDK Preparare il proprio computer per sviluppare con Java è davvero un gioco da ragazzi in quanto scaricando un unico file abbiamo subito tutti i programmi che ci servono per iniziare. Gli strumenti principali che il Java Development Kit (JDK) ci mette a disposizione sono: composto dalla macchina virtuale propriamente detta (JVM), le librerie standard (che compongono la cosidetta Java core API) ed il comando java che serve per far partire la JVM ed eseguire i programmi. Strumento Descrizione È il compilatore, il cui compito è quello di trasformare il codice sorgente Java nel bytecode che sarà poi eseguito dalla macchine virtuale java javac (JVM) JRE (Java Runtime L’ambiente di runtime Environment) Oltre a questi contiene molti tool per lo sviluppo, di seguito introduciamo solamente i più comunemente usati e ci riserviamo di parlare degli altri via via che serviranno: Strumento Descrizione Java Archiver, che utilizzeremo a partire dalla prossima sezione e serve a realizzare jar archivi di classi. Utility che serve per generare la documentazione (in HTML) di codice java a partire da commenti inseriti nei sorgenti stessi. La documentazione generata da javadoc è javadoc quella che quasi ogni progetto Java mette a disposizione e ne è un esempio quella ufficiale della java api. Java Class File Disassembler, una tool per invertire il processo di compilazione, cioè uno strumento che dato un file che contiene la versione compilata (il bytecode) di una javap classe java recupera i nomi ed i tipi dei field ed i metodi della stessa. Tool utilizzata per permettere l’utilizzo di codice scritto in C (detto nativo) da java. javah appletviewer Viewer per applet che consente di eseguirle senza l’ausilio di uno web browser. Java debugger. jdb Oracle, che dal 2010 (anno in cui ha acquistato Sun) è proprietaria del marchio Java, supporta il Java Development Kit su molteplici architetture e sistemi operativi: tutte le versioni di Windows da Vista a 8, le versioni di Windows Server a partire dalla 2008; Mac OS X Mountain Lion e Mavericks; Linux Oracle, RedHat, Suse, Ubuntu oltre a Solaris e Ubuntu su processori ARM. Se utilizzate uno di questi sistemi operativi vi basterà aprire con il vostro browser preferito l’URL: http://www.oracle.com/technetwork/java/javase/downloads/index.html http://ripetizioni.doomby.com - https://www.facebook.com/Prof.Ing.Bianco https://twitter.com/ProfBianco Pag. 7 Ripetizioni Materie Scientifiche e premere il pulsante “DOWNLOAD” per scaricare la release corrente ( Java Platform (JDK) 8, al momento in cui viene scritta la guida). Sarete automaticamente indirizzati alla pagina dei download e dovrete a questo punto accettare i termini della licenza prima di poter procedere al download. Nota: Se state usando Linux o BSD e i termini della licenza Oracle non vi convincono potete provare ad usare OpenJDK Su Windows si dovrà effettuare il download di uno dei 2 file eseguibili jdk-8-windows-i586.exe e jdk-8-windows-x64.exe (entrambi di oltre 150 MB) a seconda che si usi un sistema a 32 o a 64 bit mentre su Linux si puo’ scegliere tra gli archivi tar compressi installabili su tutte le distribuzioni e gli rpm che invece sono indicati per Fedora, SUSE etc. La procedura di installazione è totalmente automatica e sarà sufficiente accettare i default del sistema di installazione. Ad esempio l’installazione su Windows prevede pochi semplici passi: la selezione delle componenti da installare e la scelta della path del JRE. http://ripetizioni.doomby.com - https://www.facebook.com/Prof.Ing.Bianco https://twitter.com/ProfBianco Pag. 8 Ripetizioni Materie Scientifiche Dopo qualche secondo di attesa avrete installato tutti i tool necessari per sviluppare in Java. Il wizard, una volta completata l’istallazione ci suggerisce di visitare il sito della piattaforma Java, standard edition dove potrete trovare molti documenti e tutorial. Meglio tenere questo indirizzo tra i preferiti per avere un riferimento in futuro. Impostare le variabili d’ambiente (Windows / Linux ) Il compilatore (javac) per default non è nella path e quindi per ‘provarlo’ dovrete andare a cercarlo nella direcroty di installazione, ad esempio: C:\Program Files\Java\jdk1.8.0\bin\ Po poterlo utilizzare senza doverci preoccupare di includere tutto il percorso possiamo includerlo nelle variabili di ambiente del sistema (PATH). Vediamo come fare nei casi più comuni. Impostare PATH su Windows http://ripetizioni.doomby.com - https://www.facebook.com/Prof.Ing.Bianco https://twitter.com/ProfBianco Pag. 9 Ripetizioni Materie Scientifiche Per le ultime versioni di Windows i passaggi sono piuttosto simili. Si parte dal pannello di controllo (quello desktop nel caso di Windows 8) e poi cerchiamo l’icona Sistema. Nella maschera che appare clicchiamo su Impostazioni di sistema avanzate (sulla sinistra). Infine nella nuova maschera troviamo il pulsante “Variabili d’ambiente”. Che ci permetterà di accedere al pannello di modifica delle variabili di sistema. Qui non rimane che cercare la variabile Path e aggiungere il percorso relativo allaa nostra installazione (copiamolo tutto fino a [...]bin\). Impostare PATH su Ubuntu Su Ubuntu il procedimento è ancora più semplice (e vale anche per altre distribuzioni che utilizzano bash o derivati). È sufficiente modificare il file .bashrc e aggiungere la riga: export PATH=$PATH:{{percorso di Java}} ad esempio: export PATH=$PATH:/opt/jdk1.8.0/bin Verificare l’installazione Per verificare che l’istallazione abbia avuto successo potete provare ad eseguire la macchina virtuale, ad esempio aprendo l’interprete dei comandi (o il terminale) e provando a lanciare il comando: java -version http://ripetizioni.doomby.com - https://www.facebook.com/Prof.Ing.Bianco https://twitter.com/ProfBianco Pag. 10 Ripetizioni Materie Scientifiche Il risultato dovrebbe essere simile a quello in figura: http://ripetizioni.doomby.com - https://www.facebook.com/Prof.Ing.Bianco https://twitter.com/ProfBianco Pag. 11 Ripetizioni Materie Scientifiche Il primo programma in Java In questa sezione scriveremo il primo programma in Java, “from scratch” e lo compileremo con gli strumenti base messi a disposizione dal JDK. Quindi per seguire questo tutorial è previsto che abbiate installato il compilatore Java ed il JRE, scaricabili da qui la cui installazione è stata descritta nella lezione precedente. Va premesso che nella vita di uno sviluppatore, non accade praticamente mai di scrivere un programma Java interamente a mano: tutti usiamo sempre un IDE (Eclipse ad esempio, che vedremo più avanti), che provvede i tool necessari ad una più semplice gestione dei file e alla manutenzione dei progetti. È comunque molto utile e istruttivo imparare a organizzare tutto “a mano”, passo dopo passo, anche commettendo volutamente i tipici errori in cui si può incorrere la prima volta. Il primo programma che scriveremo è breve ma già permette di osservare molte cose: package my.first.project; public class Primo { public static void main(String[] args) { System.err.println("ciao mondo"); } } Iniziamo copiando il codice in un file in un qualsiasi editor di testi. Salviamo e chiamiamo l’esempio PrimoProgramma.java, poi eseguiamo il compilatore: javac PrimoProgramma.java Il risultato, spiacevole, sarà qualcosa di simile a quello seguante: PrimoProgramma.java:3: error: class Primo is public, should be declared in a file named Primo.java public class Primo { ^ 1 error Il compilatore ci avverte che abbiamo commesso il primo errore (ahimè il primo di una lunga serie). I nomi delle classi in Java, convenzioni In Java infatti ogni classe pubblica ( public class) deve essere contenuta in un file il cui nome sia identico al nome della classe stessa: ad esempio Classe dovrà stare nel file Classe.java e Primo dovrà quindi stare nel file Primo.java. In generale si usa assegnare ad ogni classe il relativo file per convenzione, anche quando non è obbligatorio (se togliamo la keyword public e lasciamo solo class possiamo compilare il programmino senza errori). Si ritiene infatti che questa pratica aiuti ad una buona organizzazione del codice. http://ripetizioni.doomby.com - https://www.facebook.com/Prof.Ing.Bianco https://twitter.com/ProfBianco Pag. 12 Ripetizioni Materie Scientifiche Prima di procedere a rinominare il file ed a fare la prova successiva vale la pena di soffermarsi alla scelta del nome della classe (che noi abbiamo chiamato Primo). In Java le classi possono avere i nomi più disparati: primo, PRIMO, PrImo123, primo_, _primo sarebbero stati tutti nomi validi (documentazione ufficiale) ma esiste una convenzione che prevede che i nomi delle classi inizino con un carattere maiuscolo, continuino con caratteri minuscoli e, se composti da più parole, siano capitalizzate le prime lettere di tutte le componenti (la scrittura di parole composte capitalizzando tutte le prime lettere è comunemente detta CamelCase). Quindi se volessimo creare una classe che si chiami “prima classe del tutorial” la convenzione (non la sintassi) ci indicherebbe di chiamarla PrimaClasseDelTutorial (nei nomi delle classi la sintassi prevede che non si possano usare gli spazi). Rinominiamo a questo punto PrimoProgramma.java in Primo.java e tentiamo ancora la compilazione: javac Primo.java Questa volta non otterremo nessun errore e, accanto al file Primo.java, troveremo un secondo file chiamato Primo.class. Il compilatore javac genererà dei files .class ogni volta che lo utilizzaremo su dei file .java. Il file Primo.class contiene il bytecode del nostro programma java (non provate a leggerlo in quanto non c’è molto al momento da capirci, è un file binario con un sacco di caratteri incomprensibili per noi) che possiamo pensare di eseguire, o più precisamente di chiedere alla macchina virtuale java (JVM) di eseguire: java Primo Qui il nome “Primo” che passiamo come argomento dell’eseguibile java non si riferisce al nome del file .class, ma è proprio il nome della classe da eseguire. Rispettare il namespace L’esecuzione anche questa volta darà un risultato inatteso, che potrebbe essere un semplice: Errore: impossibile trovare o caricare la classe principale Primo oppure un più articolato messaggio di errore: Exception in thread "main" java.lang.NoClassDefFoundError: Primo (wrong name: my /first/project/Primo) at java.lang.ClassLoader.defineClass1(Native Method) at java.lang.ClassLoader.defineClass(ClassLoader.java:792) at java.security.SecureClassLoader.defineClass(SecureClassLoader.java:14 2) ... In ogni caso l’errore è dovuto al fatto che la JVM cerca la nostra classe nella directory my/first/project/; cosa che a una prima osservazione sembrerebbe strana ma che è giustificata dal fatto che nel nostro programma la prima linea dice che la classe Primo appartiene al package “my.first.project”. http://ripetizioni.doomby.com - https://www.facebook.com/Prof.Ing.Bianco https://twitter.com/ProfBianco Pag. 13 Ripetizioni Materie Scientifiche Quindi in fase di esecuzione la JVM si aspetta di trovare la classe in un directory che è formata dal nome del package dove i punti (‘.‘) sono sostituiti da slash (‘/‘). Vale la pena di osservare che il risultato inatteso (errore di esecuzione) è segnalato dalla JVM sotto forma di una Exception (eccezione) che, come impareremo nelle prossime lezioni, sarà sempre il modo in cui la JVM ci comunica eventuali errori di run-time (durante l’esecuzione di un programma). L’eccezione in questo caso è semplice da decifrare semplicemente leggendo la prima riga. La scritta “wrong name: my/first/project/Primo” ci fa infatti capire che NoClassDefFoundError (sostanzialmente “Non ho trovato la definizione della classe”) l’errore Creiamo quindi la directory, spostiamoci entrambi i file (il file .java non sarebbe necessario spostarlo ma la sua collocazione naturale è in quella directory) e riproviamo: mkdir -p my/first/project mv Primo.* my/first/project/ e finalmente eseguendo (va usato il nome completo, fully qualified): java my.first.project.Primo otteniamo: ciao mondo Package e JAR I package in Java sono il modo più naturale di raggruppare le classi in gruppi (moduli, unità, gruppi, categorie) in modo che il nostro codice sia più leggibile e possiamo pensare che ogni componente di un package name rappresenti una directory sul filesystem. Come per i nomi le classi, Java lascia molta libertà allo sviluppatore nella scelta delle componenti di un package name. Tuttavia anche in questo caso una convenzione comunemente utilizzata è quella di utilizzare solo caratteri minuscoli ed eventualmente numeri. Poiché il compilatore genererà sempre almeno un file .class da ogni file .java (da un singolo file .java possono essere generati anche più file .class), i programmi compilati in java potrebbero diventare rapidamente scomodi da gestire in quanto composti da intere directory di file. Per questo motivo insieme al compilatore ed alla JVM viene fornito anche un altri eseguibile il cui nome è jar (java archiver) il cui scopo è esattamente quello di prendere una intera directory di class files e trasformarla in un unico file (detto java archive) più facile da maneggiare e gestire. Ad esempio: jar cf primoprogramma.jar my creerà il file primoprogramma.jar che contiene tutto il nostro albero di directory. In buona sostanza un archivio jar è la versione compressa della directory ed il formato di compression è esattamente lo zip; per “spacchettare” un jar si può addirittura usare qualsiasi programma in grado http://ripetizioni.doomby.com - https://www.facebook.com/Prof.Ing.Bianco https://twitter.com/ProfBianco Pag. 14 Ripetizioni Materie Scientifiche di decomprimere gli zip; anche per crearlo potreste farlo ma in tal caso dovreste creare a mano il file MANIFEST.MF che trovate nella directory META-INF dell”archivio e che jar ha provveduto automaticamente a creare. Per controllare il contenuto del file .jar possiamo eseguire: jar tvf primoprogramma.jar Classpath e jar Naturalmente quando vorremo utilizzare le classi compilate nel jar non avremo bisogno di decompimerlo ma potremo chiedere direttamente alla JVM di utilizzare come ‘classpath’ il file jar: java -cp primoprogramma.jar my.first.project.Primo il classpath è sostanzialmente una sorta di filesystem virtuale dentro nel quale la JVM cerca le classi. Se il nostro programma fosse composto di più classi archiviate in più files jar avremmo potuto passarli tutti alla JVM concatenandoli: java -cp primoprogramma.jar:secondo.jar my.first.project.Primo NOTA: su Windows i file .jar devono essere separati da punto e virgola (‘;‘) e non da due punti (‘:‘) che invece funziona su Unix e OSX. Il main Come ultima nota circa il nostro programmino di 6 righe (parentesi comprese) va osservato che il contenuto della classe è formato da un metodo chiamato main e dichiarato public e static (dei qualificatori parliamo meglio nella lezione sulla programmazione orientata agli oggetti). Il fatto che il metodo si chiami main non è assolutamente un caso: main è precisamente il nome che deve avere il metodo che vogliamo far eseguire per primo alla JVM quando viene lanciata. Non solo: main dovrà avere anche la stessa firma (signature, ovvero gli argomenti ed il valore di ritorno) che abbiamo utilizzato in Primo. Il fatto che il metodo statico main ritorni int significa che sarà possibile restituire un intero al sistema operativo come risultato dell’esecuzione di un programma (questo valore di ritorno è solitamente considerato un modo per segnalare un eventuale errore se è diverso da zero) mentre l’argomento args di tipo String[] (array di stringhe) sta a significare che quel metodo potrà ricevere (dalla JVM e dal sistema operativo che la esegue) un numero arbitrario di argomenti di tipo stringa; se modificate il programma come segue potrete sperimentare questa caratteristica: package my.first.project; public class Primo { public static void main(String[] args) { System.err.println("ciao " + args[0] + " !" ); } } http://ripetizioni.doomby.com - https://www.facebook.com/Prof.Ing.Bianco https://twitter.com/ProfBianco Pag. 15 Ripetizioni Materie Scientifiche Compiliamo l’esempio ed eseguiamolo il comando: java my.first.project.Primo " Java Developer" In questa ultima versione potete anche osservare come sia semplice concatenare stringhe in java utilizzando l’operatore ‘+‘, che tra numeri effettua la somma mentre tra le stringhe è (overloaded) usato con il significato di concatenare. Tanto per non stare con le mani in mano prima di iniziare la prossima lezione potete provare ad osservare cosa succede se: eseguite il programma specificando gli argomenti senza le virgolette eseguite completamente senza argomenti Cercando di spiegarvi anche il motivo dei risultati che ottenete. http://ripetizioni.doomby.com - https://www.facebook.com/Prof.Ing.Bianco https://twitter.com/ProfBianco Pag. 16 Ripetizioni Materie Scientifiche IDE e strumenti di sviluppo avanzati Nella sezione precedente abbiamo visto come poter realizzare un primissimo progetto scritto in Java, come compilarlo e come costruire il file jar, possiamo dire tutto interamente “fatto a mano” e con gli unici e soli strumenti fornitici dal JDK. Tuttavia se dobbiamo utilizzare “daily” questo strumento per progetti medio/grandi, visto l’enorme bagaglio di librerie che a corredo della JRE, le moltissime librerie di terze parti che esistono e che è possibile importare nel progetto e viste le dipendenze che esistono fra i diversi progetti, è abbastanza importante avere a nostra disposizione strumenti avanzati per lo sviluppo, che ci supporti e ci semplifichi la vita. Perciò in questo articolo facciamo una breve rassegna degli ambienti di sviluppo (IDE – Integrated Development Environment) più famosi e più utilizzati nel mondo Java: NetBeans Eclipse IntelliJ Idea Anche la generazione dei un file jar ed in generale la gestione complessiva dei progetti non è pensabile come operazione da fare manualmente senza l’ausilio di altri strumenti che tengano in considerazione tutte le dipendenze, legami e configurazioni presenti fra i diversi elementi che compongono un progetto Java. I sistemi per l’automazione del processo di build ci vengono in aiuto in questo aspetto del ciclo di sviluppo delle applicazioni; in questa categoria prenderemo in considerazione: Ant Maven IDE – Integrated Development Environment L’IDE per uno sviluppatore è un amico fidato con cui passa la maggior parte del tempo. Beh, forse è per questo che sembra che gli sviluppatori si dividano in tifoserie agguerrite a difesa del proprio ambiente di sviluppo preferito. Cercando in rete qualche confronto tra Eclipse, NetBeans e anche IntelliJ Idea si trovano vere e proprie battaglie di opinione. Ma forse la domanda stessa “quale è l’IDE migliore?” è mal posta: non esiste un IDE migliore. Si tratta di applicazioni di grande complessità, estendibili, con centinaia (letteralmente centinaia) di plugin e moduli, tutte hanno le features indispensabili, quelle utili ed http://ripetizioni.doomby.com - https://www.facebook.com/Prof.Ing.Bianco https://twitter.com/ProfBianco Pag. 17 Ripetizioni Materie Scientifiche anche quelle delle quali non sentirete mai la necessità, ma ciascuna le “offre” secondo le sue modalità e punti di vista, quindi è una questione di gusto ed abitudine scegliere l’una o l’altra. Io uso Eclipse (per abitudine) e tutte le volte che nelle prossime lezioni ci sarà la necessità di utilizzare l’IDE (generazione di codice, validazione, gestione dei build e del deploy) lo faremo con Eclipse ma potete star sicuri che si possono effettuare le stesse operazioni anche con NetBeans e Idea. Per scegliere davvero il consiglio è quello di provarli (a più riprese ed a livelli diversi di esperienza con Java, perché le cose possono cambiare) e di non “innamorarsi”: ricordiamoci che sono solo IDE e usare l’uno o l’altro è solo una questione di abitudine. NetBeans Nato come progetto universitario negli anni 90 e poi acquistato da Sun, che decise nel 2000 di renderlo un progetto open source rilasciando tutti i sorgenti alla comunità, NetBeans è da considerarsi l’IDE “ufficiale” per lo sviluppo con Java essendo ad oggi supportato direttamente da Oracle. Per alcuni anni è stato considerato una sorta di progetto all’inseguimento del più “featured” Eclipse, ma con l’impegno diretto di Oracle è oggi un competitor di tutto rilievo e del tutto intercambiabile con gli altri IDE. NetBeans è il primo IDE ad avere supporto completo per le nuove features di Java 8, Java Enterprise 7 e HTML5. Per scaricarlo è sufficiente raggiungere la pagina di download di NetBeans e selezionare la versione desiderata. In prima istanza sarà sufficiente la “Java SE” ma ne esistono altre versioni per lo sviluppo in C/C++, HTML5/PHP, Java Enterprise, ed una che comprende tutte le altre. Per completare l’installazione basta eseguire il file scaricato. Appena aperto, l’IDE vi mostrerà una scheda informativa con l’accesso a documentazione e tutorial; se volete iniziare a programmare chiudete la scheda informativa usando il pulsantino in alto a sinistra. http://ripetizioni.doomby.com - https://www.facebook.com/Prof.Ing.Bianco https://twitter.com/ProfBianco Pag. 18 Ripetizioni Materie Scientifiche Per creare un progetto Java con Netbeans è sufficiente selezionare il menu New > Project. Il wizard ci guida poi fino alla creazione della prima classe nel progetto ed inserisce per noi anche il metodo main. Come si vede in figura l’IDE ci aiuta nella scrittura del codice segnalandoci gli errori (il marker rosso alll’inizio della riga ancora non completa), assistendoci nella scelta dei nomi dei metodi (la dropdown list contiene tutti i metodi dell’oggetto System.out che matchano con la parte scritta) e dandoci immediatamente anche la documentazione (quella generata con javadoc). Infine per compilare ed eseguire il progetto non serve tenere a mente dove si trova il suo main, la struttura delle classi e il classpath, NetBeans lo fa per noi e basta utilizzare il pulsante Play (quello con la freccia verde in alto) per eseguire e vedere l’output senza lasciare l’ambiente di sviluppo. Eclipse Eclipse è certamente l’ambiente di sviluppo più noto e usato in ambito Java (ma anche per molti altri linguaggi). Nasce agli inizi del 2000 per un accordo tra molte grandi società (Borland, IBM, QNX Software Systems, Red Hat, SuSE e molti altri) che concordarono nella creazione di una fondazione (la Eclipse Foundation appunto) per promuovere lo sviluppo e la crescita di un IDE originariamente sviluppata da IBM (ed il cui investimento in termini di tecnologia e sviluppo era a quei tempi stimato in qualcosa come 40 milioni di dollari). http://ripetizioni.doomby.com - https://www.facebook.com/Prof.Ing.Bianco https://twitter.com/ProfBianco Pag. 19 Ripetizioni Materie Scientifiche Oggi la Eclipse Foundation oltre all’IDE vero e proprio rappresenta una delle organizzazioni più floride di progetti open source. Installare Eclipse è semplice: basta scaricare l’archivio ed eseguirlo. Come per NetBeans non c’è molto altro da fare e l’IDE è pronto. Una volta lanciato, ci viene chiesto di selezionare uno workspace, una directory di riferimento che Eclipse utilizza per organizzare i progetti, ciascuno con una sua directory specifica. La pagina di benvenuto di Eclipse è simile a quella di NetBeans e mostra l’immancabile scheda introduttiva con i link alla documentazione ed i tutorial. La creazione di un progetto è guidata da un semplice wizard (lanciato scegliendo, new > java project) . http://ripetizioni.doomby.com - https://www.facebook.com/Prof.Ing.Bianco https://twitter.com/ProfBianco Pag. 20 Ripetizioni Materie Scientifiche Anche in questo caso è possibile con un solo pulsante Play (la freccia verde nella barra in alto) eseguire un programma Java e visualizzarne l’output senza lasciare l’IDE. Eclipse è diventato di uso comune anche perché google offre i plugin per sviluppare applicazioni Android, e Google App Engine e GWT. IntelliJ IDEA Probabilmente il più giovane tra gli IDE che stiamo esaminando ma di certo il più aggressivo: la sua pagina Web riporta nel title “The Best Java and Polyglot IDE”. A differenza delle altre soluzioni proposte, Idea è un prodotto commerciale di JetBrains che ne offre una versione community liberamente scaricabile ed una versione commerciale (“ultimate”). Oltre a tutte le features che caratterizzano anche le altre IDE, Idea offre un egregio supporto per la gestione di progetti basati su maven ed ha i suoi maggiori fan tra gli sviluppatori “enterprise”, dove la flessibilità e la potenza degli strumenti di sviluppo viene messa a dura prova. Build System Anche per quanto riguarda i build system (maven in realtà non è semplicemente un build system, ma un project management & comprehension tool) citiamo qui quelli più diffusi e ne vediamo le caratteristiche e le funzionalità di base, lo scopo di questa guida non è quello di approfondire in dettaglio questi argomenti, ma è utile conoscere l’esistenza dei tool più importanti per la gestione dei progetti Java. Apache Ant Apache Ant (più comunemente Ant) è un tool di sviluppo per l’automazione del processo di building, o semplicemente build system. È un progetto Apache, rilasciato Open Source sotto licenza Apache Software License. http://ripetizioni.doomby.com - https://www.facebook.com/Prof.Ing.Bianco https://twitter.com/ProfBianco Pag. 21 Ripetizioni Materie Scientifiche Per fare un paragone, è simile a Make (famoso tool per la complilazione di progetti C/C++). Anche se implementato completamente in Java ed appositamente progettato per la piattaforma Java, Ant può gestire qualunque tipo di progetto (anche C++). La principale caratteristica di Ant è l’utilizzo di file XML per descrivere il processo di build, tipicamente (per default) si utilizza un file chiamato build.xml. Il build file contiene informazioni su come effettuare il build del progetto e per ogni progetto possono essere presenti più target (azioni come creare directory, compilare i sorgenti, eseguire test, …) e ciascun target può avere dipendenze con altri target. Ecco un esempio di file build.xml per un semplice progetto Java “Hello World”, Vengono definiti 4 target (clean, clobber, compile e jar), ciascuno dei quali ha associata una descrizione. <?xml version="1.0"?> <project name="Hello" default="compile"> <target name="clean" description="remove intermediate files"> <delete dir="classes"/> </target> <target name="clobber" depends="clean" files"> <delete file="hello.jar"/> </target> description="remove all artifact <target name="compile" description="compile the Java source code to class files"> <mkdir dir="classes"/> <javac srcdir="." destdir="classes"/> </target> <target name="jar" depends="compile" description="create a Jar file for the application"> <jar destfile="hello.jar"> <fileset dir="classes" includes="**/*.class"/> <manifest> <attribute name="Main-Class" value="HelloProgram"/> </manifest> </jar> </target> </project> Il target jar come dipendenza compile, questo significa che prima che Ant inizi ad eseguire il target jar dovrà aver prima eseguito correttamente il target compile. All’interno di ogni target sono specificate le operazioni che Ant deve eseguire; queste sono spesso realizzate con task che Ant ha già definiti (built-in). Installare Ant Se stiamo lavorando con uno degli IDE sopra descritti, l’installazione di Ant è quasi gratuita e gli ambienti di sviluppo hanno già Ant incluso nella maggior parte delle loro distribuzioni. Per info e dettagli: http://ripetizioni.doomby.com - https://www.facebook.com/Prof.Ing.Bianco https://twitter.com/ProfBianco Pag. 22 Ripetizioni Materie Scientifiche Ant su netBeans Ant su Eclipse Ant su IntelliJ Idea In alternativa il primo passo è quello di scaricare Ant dal sito della Apache Foundation. Preleviamo l’ultima versione del bynaryfile. A seconda del sistema operativo che abbiamo scarichiamo un archivio tar oppure zip (nel caso di Windows). Una volta scaricato e decompresso il file scegliamo il path “definitivo” per Ant sulla nostra macchina. Infine andranno impostate le variabili d’ambiente JAVA_HOME e ANT_HOME e andrà aggiunto ${ANT_HOME}/bin (Unix) o %ANT_HOME%/bin (Windows) nella PATH. Per ulteriori info e dettagli su come installare e configurare Ant (anche in base al sistema operativo) fate riferimento alla guida ufficiale. Apache Maven Apache Maven (o solo Maven) è un tool per l’automazione della fase di building di un progetto usato principalmente e primariamente per progetti Java. Maven mira principalmente a risolvere due aspetti: 1. descrivere come il programma/progetto deve essere costruito; 2. descrivere le sue dipendenze. Come per Ant, la descrizione del processo di build, le sue dipendenze da moduli e componenti esterne, l’ordine delle operazioni, le directory e i plugin necessari, è fatta attraverso un file XML. Maven scarica automaticamente tutte le librerie Java ed i plugin necessari da uno o più repository (come Maven2 Central Repository) e li salva in una cache locale. I progetti Maven vengono configurati utilizzando un Project Object Model, che è salvato in un file chiamato pom.xml. Di seguito un esempio minimalista: <project> <!-- model version is always 4.0.0 for Maven 2.x POMs --> <modelVersion>4.0.0</modelVersion> <!-- project coordinates, i.e. a group of values which uniquely identify this project --> http://ripetizioni.doomby.com - https://www.facebook.com/Prof.Ing.Bianco https://twitter.com/ProfBianco Pag. 23 Ripetizioni Materie Scientifiche <groupId>com.mycompany.app</groupId> <artifactId>my-app</artifactId> <version>1.0</version> <!-- library dependencies --> <dependencies> <dependency> <!-- coordinates of the required library --> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>3.8.1</version> <!-- this dependency is only used for running and compiling tests -> <scope>test</scope> </dependency> </dependencies> </project> Questo POM definisce un identificatore univoco per il progetto (coordinates) e le sue dipendenze nel framework JUnit. In ogni caso questo è abbastanza per costruire il progetto ed eseguire gli unit test associati (Maven fornisce valori di default per la configurazione del progetto). http://ripetizioni.doomby.com - https://www.facebook.com/Prof.Ing.Bianco https://twitter.com/ProfBianco Pag. 24 Ripetizioni Materie Scientifiche Variabili e dichiarazioni In questa sezione introdurremo le basi della sintassi del Java insieme a concetti utili per la comprensione di alcuni aspetti rilevanti dell’esecuzione di un programma. Molte delle parti sono probabilmente già nel background di chiunque abbia mai utilizzato un qualsiasi linguaggio di programmazione ma nell’introduzione della sintassi qualche breve nota di carattere generale si ritiene utile. Variabili, identificatori e indirizzi Formalmente potremmo definire una variabile in un linguaggio di programmazione come la coppia composta da: un nome simbolico (detto anche identificatore) e un indirizzo di memoria destinato a contenere una determinata quantità, detta comunemente valore della variabile. Il nome simbolico della variabile serve nei programmi per far riferimento all’indirizzo di memoria al fine di accedere e/o modificarne il valore durante l’esecuzione. Dichiarare le variabili In Java (che appartiene alla classe dei linguaggi tipati) ogni variabile ha associato anche un tipo (come Integer, String, boolean, etc.) che definisce le caratteristiche che avranno i valori che la variabile potrà assumere. Ad esempio una variabile di tipo Integer potrà contenere il valore 42 ma non il testo "quarantadue". In generale in Java il tipo di una variabile può essere uno dei tipi predefinitti (primitivi) del linguaggio oppure un tipo definito da noi come vedremo nelle prossime sezioni. La sintassi per la dichiarazione di una variabile in Java è la seguente: [public|protected|private] [static] [final] Tipo identificatore [= value]; dove le parti tra parentesi quadre ‘[]‘ sono opzionali ed il simbolo pipe ‘|‘ deve essere letto “oppure” (il significato delle keywords verrà chiarito nel seguito). Possiamo anche inizializzare la variabile quando è presente il simbolo ‘ =‘ al quale segue il valore che dovrà assumere. http://ripetizioni.doomby.com - https://www.facebook.com/Prof.Ing.Bianco https://twitter.com/ProfBianco Pag. 25 Ripetizioni Materie Scientifiche I nomi delle variabili Sintatticamente l’identificatore (nome) di una variabile è una sequenza di lettere e cifre il cui primo elemento deve essere una lettera oppure il carattere underscore (‘_‘) o ancora il carattere dollaro (‘$‘). Vige comunque la convenzione (non regola sintattica) che i nomi delle variabili inizino con una lettera minuscola e, qualora formati da più parole concatenate, tutte le parole successive alla prima siano capitalizzate e non vengano usati i simboli _ e $ nonostante siano ammessi. Ad esempio: int nomeDellaVariabileIntera; Per i nomi dei tipi, ovvero per le classi, la convenzione prevede invece che la prima lettera sia maiuscola): class LaMiaClasse L’identificatore può essere una qualsiasi stringa ma esistono alcune parole riservare del linguaggio che non possono essere utilizzate come identificatori: abstract assert boolean break byte case catch char class const continue default do double else enum extends final finally float for goto if implements import instanceof int interface long native new package private protected public return short static strictfp super switch synchronized this throw throws transient try void volatile while Tipi di variabili In Java si distinguono tre tipi di variabili: variabili locali, variabili di istanza e variabili di classe. Vediamo in dettaglio di che si tratta. Variabili Locali Si parla di variabili locali quando la dichiarazione avviene all’interno di un metodo. Le variabili locali sono create quando un metodo viene chiamato e scompaiono (vengono cancellate dalla memoria) quando il metodo termina. Ogni variabile dichiarata all’interno del metodo può essere utilizzata solamente all’interno del metodo stesso ed in Java le variabili locali non possono essere utilizzate prima della loro inizializzazione. Per esempio, se provassimo a compilare il seguente metodo: http://ripetizioni.doomby.com - https://www.facebook.com/Prof.Ing.Bianco https://twitter.com/ProfBianco Pag. 26 Ripetizioni Materie Scientifiche void add(long i) { long j; j = j + i; } il compilatore Java ci darebbe un messaggio di errore perchè la variabile j non è stata inizializzata prima del suo uso e non è quindi possibile aggiungere un valore alla variabile fino a che a questa non è stato assegnato un valore. Il codice seguente sarebbe invece compilato senza alcun errore: void add(long i) { long j = 1; j = j + i; } Nell’esecuzione di questo frammento di codice la macchina virtuale Java crea in memoria lo spazio per registrare le variabili locali i e j, per poi cancellarle (liberando lo spazio in memoria) alla fine dell’esecuzione del metodo add. Scope di una variabile Più in generale si definisce scope di una variabile l’area del codice nel quale un identificatore resta associato ad un indirizzo di memoria (e quindi l’area di codice entro il quale una variabile mantiene il suo valore). In Java ogni blocco (cioè ogni gruppo di linee di codice racchiuso da parentesi graffe {}) definisce uno scope e ogni variabile locale ha come scope l’area di codice che inizia dalla definizione della variabile stessa e termina con il blocco corrente. Variabili di istanza Le variabili di istanza, anche note come field o campi, sono dichiarate all’interno di una classe ma all’esterno di ogni metodo. I field hanno come scope l’intero corpo della classe in cui sono dichiarati, compresi i metodi della classe stessa. Quindi sono visibili all’interno di tutti i metodi della classe. Può succedere che una variabile locale in un metodo (oppure il parametro di un metodo) abbia lo stesso nome (identificatore) di una variabile di istanza. In questo caso ha la precedenza la variabile più specifica, cioè la variabile locale o il parametro. Vediamo un esempio: public class Scope { int var = 6; public void primoMetodo(int var) { http://ripetizioni.doomby.com - https://www.facebook.com/Prof.Ing.Bianco https://twitter.com/ProfBianco Pag. 27 Ripetizioni Materie Scientifiche int i = var; // in questo caso ha precedenza il parametro e // i assume il valore che sarà passato come parametro al metodo // ... } public void secondoMetodo() { int var = 7; int i = var; // qui ha precedenza la variabile locale al metodo, quindi // i ha il valore 7 // ... } public void terzoMetodo() { int i = var; // qui semplicemente assegnamo ad i il valore della // variabile di istanza e i prende il valore 6 // ... } public void quartoMetodo(int var) { int i = this.var; // in questo caso i assume il valore 6 indipendentemente // dal valore del parametro poiché abbiamo utilizzato la // keyword 'this', indica di utilizzare la variabile 'var' // che abbiamo definito come field e che appartiene // all'istanza corrente della classe. // ... } } Un’istanza di una variabile (non statica) continua ad esistere nella memoria di un programma fino a quando esiste l’oggetto che la contiene (ed un oggetto “rimane in vita” fino a quando ne esiste almeno una referenza, quindi una variabile associata ad esso). Variabili di classe (static) Le variabili di classe infine, comunemente dette anche static field o campi statici, sono variabili di istanza ma nella loro definizione viene usata la keyword ‘static’. static int var = 6; Una variabile di classe è una variabile visibile da tutte le istanze di quell’oggetto ed il suo valore non cambia da istanza ad istanza, per questo appartiene trasversalmente a tutta la classe. http://ripetizioni.doomby.com - https://www.facebook.com/Prof.Ing.Bianco https://twitter.com/ProfBianco Pag. 28 Ripetizioni Materie Scientifiche Più in dettaglio mentre per le variabili di istanza viene allocata una nuova locazione di memoria per ogni istanza di una classe, per le variabili statiche esiste una unica locazione di memoria legata alla classe e non associata ad ogni singola istanza. Una variabile di classe, statica, vive (cioè mantiene occupata la memoria e continua a mantenere il suo valore) fino al termine del programma. Modificatori di visibilità: public, private, protected, default Le variabili di istanza e di classe possono essere ulteriormente qualificate per mezzo delle keywords public e private che ne determinano la visibilità all’esterno della classe in cui sono dichiarate. Se utilizziamo la keyword private una variabile sarà visibile (accessibile, utilizzabile per far riferimento al suo indirizzo di memoria e quindi al suo valore) solamente all’interno della classe; mentre se qualificheremo la variabile come public indicheremo al compilatore che la variabile potrà essere utilizzata da qualsiasi parte del codice in cui ci sia una istanza della classe (con la notazione idIstanzaClasse.nomeVariabile). Protected significa che la variabile sarà accessibile da ogni altra classe che appartiene al medesimo package della classe che contiene la variabile e da ogni classe che ne deriva (la estende). Se non specifichiamo un qualificatore di visibilità, la variabile sarà lasciata con la visibilità di default che in Java signifca che sarà accessibile solo da tutte le classi nel medesimo package. Variabili final e static final Infine si usa la keyword final per dichiarare una variabile che potrà essere inizializzata una sola volta, sia nella fase di dichiarazione o attraverso una successiva assegnazione. Al contrario delle costanti, il valore delle variabili final non è necesariamente noto a compile-time ma il loro indirizzo di memoria può essere inizializzato una sola volta rendendone possibile l’utilizzo in alcuni contesti in cui sarebbe impossibile utilizzare le normali variabili locali. In Java si definiscono costanti le variabili che vengono qualificate contempraneamente come final e static: è convenzione che i nomi delle variabili final siano in maiuscolo e se il nome è costituito da più parole, queste vengano separate dal carattere underscore (‘ _‘); http://ripetizioni.doomby.com - https://www.facebook.com/Prof.Ing.Bianco https://twitter.com/ProfBianco Pag. 29 Ripetizioni Materie Scientifiche Tipi primitivi di Java e valori Java è un linguaggio tipato, abbiamo già accennato a questo in precedenza, ciò significa che ogni variabile prima di essere utilizzata deve essere dichiarata: dobbiamo quindi assegnarle un nome ed un tipo. Cos’è un tipo Il tipo è l’insieme di caratteristiche che qualsiasi valore assunto da una variabile dovrà soddisfare. Ad esempio: “essere un intero”, “essere una sequenza di carattteri Unicode”, etc. I tipi primitivi in Java Il tipo di una variabile può essere costruito dallo sviluppatore per composizione a partire da un set predefinito di tipi detti comunemente tipi primitivi. I tipi primitivi in Java sono 8 e ciascuno di essi è pensato per rappresentare un certo tipo di informazione e utilizzando una quantià specifica di memoria. Inoltre, parlando di dichiarazione delle variabili, abbiamo detto che le variabili “locali” devono essere inizializzate per evitare errori di compilazione, questo non è vero per le variabili di istanza per le quali, per ogni tipo primitivo, è specificato un valore di default. Tipo Q.tà di Memoria Informazione rappresentata byte 8 bit short 16 bit int 32 bit long 64 bit float 32 bit Variabile con segno (con rappresentazione “two’s complement”, complemento a due) e rappresenta valori in un range [-128 e 127] (estremi inclusi) Numeri interi (con segno) in un range [-32,768, 32,767] Numeri interi (per default con segno, signed) in un range [231, 231-1] . Con Java 8 è stata introdotta la possibilità di utilizzare gli int per rappresentare quantità unsigned che potranno avere range [0, 232-1] (grazie ad appositi metodi statici introdotti nelle classi Integer e Long) Numeri interi (per default con segno, signed) in un range [263, 263-1]. Come per gli interi in Java 8 esiste la possibilità di utilizzarli come quantità unsigned con range (positivo) che arriva fino a 264-1. Numeri in virgola mobile in singola precisione secondo la specifica IEEE 754, utilizzando la rappresentazione segno, http://ripetizioni.doomby.com - https://www.facebook.com/Prof.Ing.Bianco https://twitter.com/ProfBianco Valore di default 0 0 0 0L 0.0f Pag. 30 Ripetizioni Materie Scientifiche mantissa esponente. (-1)segno * mantissa * 2esponente Nella versione a 32bit il range rappresentabile va calcolato pensando ad un bit di segno, una mantissa a 23bit e un esponente a 8bit con valori compresi tra -126 e 127. double 64 bit Inoltre lo standard prevede la rappresentazione di due valori per zero (da destra e da sinistra) due per infinito (positivo e negativo), e di valori NaN (not a number) da utilizzare ad esempio come risultati di operazioni impossibili (es. divisioni per zero). Numeri in virgola mobile in doppia precisione secondo la specifica IEEE 754. La precisione con cui vengono 0.0d rappresentati i numeri aumenta in virtù dell’aumento del numero di bit utilizzati. non specificato, ma sarebbe boolean sufficiente un solo bit serve a rappresentare solamente 2 valori: vero o falso ( true o false false). 16 bit È utilizzato per la memorizzazione di caratteri del charset Unicode) nel range ['\u0000', '\uffff'] (in esadecimale) \u0000 o equivalentemente [0,65535]. char Va aggiunto che ogni variabile di tipo oggetto (cioè di tipo non primitivo) viene per default inizializzata con il valore speciale null. Va notato che la stessa documentazione ufficiale di Java segnala che, benché i valori di default siano garantiti per tutti i field non inizializzati é da considerarsi una cattiva pratica quella di non inizializzare le variabili e quindi si dovrebbe cercare di evitarla. Literals, la codifica dei valori numerici I tipi primitivi sono elementi con cui poter costruire tutti gli altri oggetti da utilizzare nei programmi, perciò sono da considerarsi dei tipi di dato speciali del linguaggio. Per questo c’è l’esigenza utilizzare modalità specifiche per poter definire i valori nel codice. I literals sono appunto le codifiche dei valori di questi tipi nel linguaggio. Vediamo quali sono. I possibili valori del tipo boolean sono esprimibili con le keyword true e false. Valori per i tipi int e long sono esprimibili come interi in base 10 utilizzando la comune notazione posizionale: int lifeUniverseAndEverything = 42; oppure in rappresentazione esadecimale (Hex, in base 16) utilizzando il suffisso ‘0x‘ o binaria (base 2, dalla versione 7 di java) utilizzando il suffisso ‘0b‘: int lifeUniverseAndEverythingBis = 0x2A; // esadecimale http://ripetizioni.doomby.com - https://www.facebook.com/Prof.Ing.Bianco https://twitter.com/ProfBianco Pag. 31 Ripetizioni Materie Scientifiche int lifeUniverseAndEverythingTer = 0b00101010; // binario Questi stessi literals possono essere utilizzati anche per esprimere valori di tipo byte e short mentre per esprimere valori long si pospone alla rappresentazione la lettera ‘ L‘ (è valido anche il carattere minuscolo ma sconsigliato a causa della sua scarsa leggibilità essendo confondibile con il numero ‘1‘): long bigLUE = 4242424242L; Valori di tipo non intero possono essere analogamente espressi separando la parte decimale con il simbolo ‘.‘ (punto) e saranno considerati di tipo double a meno che non sia posposta la lettera ‘ F‘ (o ‘f‘). I literals di tipo double possono essere terminati con la lettera ‘ D‘ (o ‘d‘) qualora per motivi di leggibilità la si ritenga opportuna (ma non è obbligatoria). È ammessa anche la cosiddetta notazione scientifica per i numeri in virgola mobile che consiste nell’utilizzo della lettera E (o e) seguita da un numero che esprime la potenza di 10 da moltiplicare al numero espresso prima di essa: double mille = 1000.0; double milleSci = 1.0e3; float milleFloat = 1000.0f; A partire dalla versione 7 di Java è possibile utilizzare il carattere underscore (‘_‘) in tutti i literal numerici per aumentarne la leggibilità, ad esempio separando le migliaia: float milleEasy = 1_000.0f; il carattere ‘_‘ non ha alcun uso se non quello di facilitarne la lettura e può essere utilizzato esclusivamente tra coppie di numeri (non come primo o ultimo carattere, non adiacente al ‘ .‘ o agli altri caratteri ammessi nelle notazioni). Character and String Literals I valori di tipo carattere possono essere espressi per mezzo di caratteri Unicode (UTF-16) racchiusi tra apici singoli che possono eventualmente essere espressi sotto forma di charcode utilizzando “Unicode escape” : char nCirconflesso = 'ñ'; char nCorconflessoCode = '\u00F1'; Sono supportati anche alcune speciali rappresentazioni (dette escape sequences o sequenze di escape): Escape Carattere backspace (indietro) \b tab \t line feed (fine linea) \n form feed (fine pagina / nuova pagina) \f http://ripetizioni.doomby.com - https://www.facebook.com/Prof.Ing.Bianco https://twitter.com/ProfBianco Pag. 32 Ripetizioni Materie Scientifiche \r \’ \” \\ carriage return (ritorno carrello / a capo) apice singolo doppio apice backslash (\) Stringhe e numeri In Java, accanto agli 8 tipi primitivi sono da considerarsi tipi di dato speciali (detti comunemente Simple Data Objects) anche i tipi String e Number (e derivati) che fungono in qualche modo da controparte dei dati primitivi dove ci sia l’esigenza di utilizzare un oggetto invece che direttamente un tipo (la differenza risulterà più chiara nel corso delle prossime lezioni). Basti sapere al momento che le variabili di tipo String sono sequenze di char che possono essere inizializzate utilizzando le virgolette (doppi apici): String author = "Douglas Noël Adams"; mentre i tipi Integer, Byte, Long, Float e Double (controparti dei medesimi tipi primitivi scritti con la prima lettera minuscola) sono inizializzabili con i medesimi literals presentati per i corrispondenti tipi nativi ma tutti quanti vengono per default inizializzati al literal null (letto nullo) se non assegnamo loro esplicitamente un valore. Il compilatore è quasi sempre in grado di convertire automaticamente i tipi primitivi nei rispettivi simple object (operazione detta boxing ed unboxing), fanno eccezione solo alcuni casi particolari. http://ripetizioni.doomby.com - https://www.facebook.com/Prof.Ing.Bianco https://twitter.com/ProfBianco Pag. 33 Ripetizioni Materie Scientifiche Classi wrapper Come abbiamo già detto nella lezione precedente, in Java per ogni tipo primitivo esiste un corrispondente Simple Data Object o, come si suol dire, una Classe Wrapper. Dato un tipo primitivo (i cui nomi iniziano tutti rigorosamente con la prima lettera minuscola) si ottiene il corrispondente Data Object sostanzialmente capitalizzando il nome come mostrato nella tabella seguente: Tipo primitivo Classe Wrapper byte Byte short Short int Integer long Long float Float double Double char Character boolean Boolean Anche se ad un primo sguardo può sembrare che ci sia poca differenza tra un tipo primitivo e la sua controparte ‘wrapped’ (anche e spesso detta ‘boxed’) tra le due c’è una fondamentale distinzione: i tipi primitivi non sono oggetti e non hanno associata alcuna classe e quindi devono essere trattati in modo diverso rispetto agli altri tipi (ad esempio non è possibile utilizzarli nelle collezioni, che saranno argomento di future lezioni) e non possono avere metodi. Per ovviare a questa distinzione Java mette dunque a disposizione delle classi preconfezionate per contenere, “wrappare” i tipi primitivi. Possiamo infatti pensare ad una classe wrapper esattamente come un involucro (wrap) che ha l’unico scopo di contenere un valore primitivo rendendolo da un lato un oggetto e dall’altro “ornandolo” con metodi che altrimenti non avrebbero una loro naturale collocazione. Tutte le classi wrapper sono definite nel package java.lang e sono qualificate come final, perciò non è possibile derivare da loro. Inoltre tutte queste classi sono immutabili, cioè non è possible dopo la costruzione cambiarne il valore. Mentre Byte e Character derivano direttamente da Object tutti i Data Object di tipo numerico derivano da Number che a sua volta è un discendente diretto di Object. http://ripetizioni.doomby.com - https://www.facebook.com/Prof.Ing.Bianco https://twitter.com/ProfBianco Pag. 34 Ripetizioni Materie Scientifiche Dai tipi semplici ai Data Object Anche se Java 1.5 ha introdotto il concetto di ‘autoboxing/unboxing’, che approfondiremo in una apposita lezione, passare da un tipo primitivo alla sua versione wrappata è in linea di principio semplice ma laborioso: int val = 44; //dato un tipo primitivo si crea la classe wrapper Integer value = new Integer(val); //dalla classe wrapper è possibile "estrarre" il valore int valueBack = value.intValue(); Metodi speciali per il parsing Le classi wrapper sono in Java anche il posto in cui trovano posto gli utilissimi metodi che servono per fare il parsing di stringe e convertirle in valori numerici, ad esempio: String quarantatre = "43"; Integer q = new Integer(quarantatre); Quando utilizziamo questi metodi per la conversione occorre gestire una eventuale generazione di errori. Infatti non tutte le possibili sequenze di caratteri sono numeri, prendiamo ad esempio il seguente snippet: String quarantaquattro = "quarantaquattro"; Integer q = new Integer(quarantaquattro); Il parser non sarebbe in grado di processare con successo la stringa e otterremmo un errore che la JVM segnala attraverso una eccezione di tipo NumberFormatException (più avanti approfond String Tra i tipi wrapper potremo annoverare anche il tipo String che con i Simple Data Object condivide moltissime caratteristiche (ad esempio l’immutabilità). Ma data l’importanza delle stringhe riserveremo a loro un’intera lezione. http://ripetizioni.doomby.com - https://www.facebook.com/Prof.Ing.Bianco https://twitter.com/ProfBianco Pag. 35 Ripetizioni Materie Scientifiche Boxing, unboxing e autoboxing In questa lezione esaminiamo l’Autoboxing in Java: si tratta della caratteristica del linguaggio che, a partire dalla versione 1.5, ci consente di lavorare con tipi primitivi e tipi wrapper in maniera intercambiabile. Boxing In Java generalmente ci occupiamo di utilizzare e definire oggetti come istanze di una classe, quindi con metodi e attributi. Ma per motivi pratici abbiamo spesso a che fare con tipi primitivi (int, double, boolean, …) che non sono oggetti, ma “tipi semplici”. Prima dell’introduzione dell’autoboxing programmando in Java ci trovavamo nella necessità di convertire un tipo primitivo nella sua corrispondente classe wrapper. Lo spezzone di codice che segue potrebbe non risultarvi nuovo: Integer x = new Integer (10); Double y = new Double (5.5); Boolean z = Boolean.parseBoolean("true"); Queste operazioni sono note come operazioni di boxing, cioè “inscatolamento” del tipo primitivo nel relativo tipo wrapper al fine di utilizzare un oggetto e tutte le sue proprietà (ad esempio porre un intero in una lista o operazioni che hanno necessità di maneggiare oggetti). L’autoboxing Veniamo alla novità introdotta da Java 1.5 con un esempio pratico: Integer x = 10; Double y = 5.5f; Boolean z = true; Number n = 0.0f; Attraverso autoboxing gli oggetti scritti nello spezzone di codice vengono automaticamente creati con i valori di riferimento dettati, senza generare errori. Questo permette di scrivere codice più leggibile e maneggevole. Chiaramente alla funzione di boxing è associata l’operazione di unboxing che trae gli stessi vantaggi della precedente: /** Esempio di operazione di unboxing */ int x = -1; Integer y = x; http://ripetizioni.doomby.com - https://www.facebook.com/Prof.Ing.Bianco https://twitter.com/ProfBianco Pag. 36 Ripetizioni Materie Scientifiche Il linguaggio si arricchisce, permettendo allo sviluppatore di non preoccuparsi delle operazioni di conversione (boxing e uboxing, appunto) lasciandole al compilatore del bytecode che si occuperà di gestirle per noi (autoboxing). A basso livello la situazione non è affatto cambiata, la macchina virtuale che esegue il bytecode non ha avuto cambiamenti; ciò che cambia è a livello di compilazione. Infatti le operazioni di conversione a livello di bytecode sono quelle che avremmo svolto manualmente, solo che ci viene in aiuto il compilatore preoccupandosi di effettuarle lui in automatico per noi. Il comportamento dell’operatore di uguaglianza Ovviamente ciò significa che le regole osservate in ambiente pre Java 1.5 continuano a valere. Dal punto di vista della sintassi comunque la cosa produce notevoli vantaggi in quanto ora è possibile associare gli operatori aritmetici e le strutture condizionali ai tipi wrapper (ricordandoci sempre che verrà fatto unboxing automatico): /** Esempio di utilizzo dei costrutti come operatori aritmetici * e statement condizionali */ Integer x = 0; Boolean verify = true; while (verify) { x++; if (x > 10) verify = false; } Il codice scritto mostra come sia possibile utilizzare i costrutti sia con operatori aritmetici sia in statement condizionali (if, while, for, …). Unico caso a cui fare attenzione è il caso dell’uguaglianza tra due istanze di wrapper che puntano al medesimo valore: /** Esempio di uguaglianza tra istanze di wrapper */ Integer x = 1000; Integer y = 1000; x==y ?? La condizione precedente dovrebbe risultare falsa, anche se i valori sono uguali. In pratica di fronte all’operatore di uguaglianza, il compilatore si comporta normalmente, facendo comparazione tra due istanze di oggetto senza effettuare unboxing. L’operazione, tradotta sarebbe: Integer x = new Integer (1000); Integer y = new Integer (1000); x == y; // false! Qui forse è più intuitivo capire perché il risultato è falso. Tutto ciò risulta verificato a meno di alcune situazioni dove la macchina virtuale utilizza delle ottimizzazioni. Nei seguenti casi la JVM crea delle istanze immutabili che vengono riutilizzate (per motivi di performance): http://ripetizioni.doomby.com - https://www.facebook.com/Prof.Ing.Bianco https://twitter.com/ProfBianco Pag. 37 Ripetizioni Materie Scientifiche Valori boolean true e false; Valori del tipo byte; Primo byte del tipo int (valori tra -127 e 127); Primo byte del tipo char; Ad esempio, comparare due Integer che hanno come valore 100 ci darebbe true (fate qualche tentativo). Impatto sull’overload dei metodi Altro comportamento a cui dobbiamo porre attenzione è nel caso dell’overload di metodi, dove c’è la possibilità di confusione in fase di compilazione: /** Esempio di overload di metodi */ public void metodoA(Integer x); public void metodoA(double y); public void usaA() { int d = 0; metodoA(d); } In questo caso il compilatore non effettua alcuna operazione di unboxing, proprio perché non c’è modo di sapere dinamicamente come comportarsi. In generale, di fronte a situazioni di ambiguità, per mantenere anche la compatibilità con le precedenti versioni, il comportamento è quello che ci sarebbe con la versione di Java 1.4. In questo specifico caso la chiamata sarebbe fatta al metodo che accetta come tipo il double. http://ripetizioni.doomby.com - https://www.facebook.com/Prof.Ing.Bianco https://twitter.com/ProfBianco Pag. 38 Ripetizioni Materie Scientifiche Operatori e casting Gli operatori in Java sono simili a quelli che si trovano in altri linguaggi di programmazione: il core di Java prevede per i tipi primitivi operatori algebrici, logici, etc. In questa lezione esaminiamo questi operatori di base, ma oltre a questi possiamo definire, per ogni tipo, metodi che ne implementino di nuovi. Iniziamo con l’operatore “punto” (.) che serve per l’accesso a campi e metodi di classi e oggetti. Per accedere al campo simpleField dell’oggetto myObject scriviamo semplicemente: myObject.simpleField Se myObject espone anche un metodo myMethod(int value), possiamo involarlo scrivendo: myObject.myMethod(100) Casting Prima di proseguire con gli operatori bisogna ricordare che Java è un linguaggio “fortemente tipato”, perciò i tipi sono fondamentali e c’è uno stretto controllo sull’utilizzo dei tipi: ad esempio non è possibile assegnare un carattere ad un intero, cosa possibile nel linguaggio C, ma è anche impossibile assegnare un double ad un float, senza un esplicito casting. Il casting consiste nel forzare un valore di un tipo ad assumere un tipo diverso: se scriviamo 5 ad esempio abbiamo un numero intero, ma se ne facciamo un cast a float indenderemo la versione di 5 “reale”, per farlo basta scrivere: (float) 5; In questo modo possiamo ottenere i famosi assegnamenti di interi a reali e di reali double a reali float, prendiamo ad esempio queste variabili: double v1 = 10.0; float v2; int v3 = 5; Gli assegnamenti che seguono sono errati: v2 = v1; // non si può assegnare un double a un float v1 = v3; // non si può assegnare un intero a un double Per rendere possibili questi assegnamenti, quindi “leciti” per Java, ci dobbiamo servire dei cast: v2 = (float) v1; v1 = (double) v3; http://ripetizioni.doomby.com - https://www.facebook.com/Prof.Ing.Bianco https://twitter.com/ProfBianco Pag. 39 Ripetizioni Materie Scientifiche Operatori artitmetici Come dicevamo abbiamo a disposizione i classici operatori aritmetici (+, -, *, / e %), ma è importante sottolineare che anche le operazioni sono tipate, quindi se sommiamo due interi otteniamo un valore intero, così per tutte le operazioni, in questo modo possiamo sempre prevedere il tipo del risultato di una certa espressione. A questo proposito è importante ricordare che la divisione tra interi, a differenza di altri linguaggi, ritorna un valore intero. Quindi se vogliamo ottenere il numero reale derivato dalla divisione dei due numeri interi x e y, bisogna prima trasformarli in reali: float risultato= (float) x / (float) y; Operatori abbreviati Abbiamo ad un certo punto usato un assegnamento particolare, ovvero +=, questa è una abbreviazione, serve ad incrementare il valore di una variabile senza doverla riscrivere. Se ad esempio vogliamo aggiungere alla variabile a il valore 5, sommandolo al suo valore attuale scriveremmo: a = a + 5; oppure, in modo abbreviato: a += 5; Vale lo stesso per ogni operatore binario (es. -, *, /). Java inoltre offre altri quattro operatori che sono delle abbreviazioni, due di incremento di variabili e due di decremento. Sia X una variabile, possiamo scrivere: X++; // valuta X, poi incrementa X di 1 X--; // valuta X, poi decrementa X di 1 ++X; // incrementa X di 1, poi valuta X --X; // decrementa X di 1, poi valuta X L’espressione X++ è un comando di assegnamento, ma anche una espressione che restituisce un risultato. Il comportamento cambia a seconda che i simboli di incremento o decremento precedano o seguano la variabile: se l’operatore segue la variabile, l’espressione restituisce il valore attuale della variabile prima di modificarlo; se l’operatore precede la variabile, l’espressione restituisce il valore della variabile già modificato. Ad esempio: X = 10; Y = X++; // risultato: X=11 e Y=10 http://ripetizioni.doomby.com - https://www.facebook.com/Prof.Ing.Bianco https://twitter.com/ProfBianco Pag. 40 Ripetizioni Materie Scientifiche Ecco un esempio più complesso da poter provare: class incdec { public static void main(String [] a) { int X,Y,Z,W,V; X=10; System.out.println("X="+X); Y=X++; System.out.println("Y=X++: ho X="+X+",Y="+Y); Z=++Y; System.out.println("Z=++Y: ho Z="+Z+",Y="+Y); W=Z--; System.out.println("W=Z--: ho W="+W+",Z="+Z); V=--W; System.out.println("V=--W: ho V="+V+",W="+W); } } Operatori su stringhe Per le stringhe esiste un operatore di concatenamento: String a = "Pietro " ; String b = a + "Castellu"; String b += "cci" Il risultato sarà "Pietro Castellucci". L’operatore + appende ad una stringa anche caratteri e numeri, ad esempio possiamo costruire una stringa scrivendo: System.out.println("Numero complesso:"+ parteReale + " +i" + parteImmaginaria + " ha modulo " + modulo); Sembra strano questo, anche in considerazione del fatto che abbiamo detto che le espressioni Java sono ben tipate a differenza del C, questo avviene però perché il sistema effettua delle conversioni implicite di tipi primitivi e oggetti in stringhe. Ad esempio definendo un qualsiasi oggetto, se ne definisco un metodo toString(), il sistema che si troverà a lavorare con stringhe e con quest’oggetto ne invocherà automaticamente il metodo toString(). Operatori di confronto Altri operatori sono quelli di confronto, ovvero: Operatore Descrizione > maggiore di espressione Cosa restituisce true se x è strettamente maggiore di y, false x>y altrimenti http://ripetizioni.doomby.com - https://www.facebook.com/Prof.Ing.Bianco https://twitter.com/ProfBianco Pag. 41 Ripetizioni Materie Scientifiche >= < <= == != maggiore o uguale x>=y di x<y minore di minore o uguale di x<=y x==y uguale x!=y diverso true se x è maggiore o uguale di y, false altrimenti true se x è strettamente minore di y, false altrimenti true se x è minore o uguale di y, false altrimenti true se x è uguale a y, false altrimenti true se x è diverso da y, false altrimenti Operatori logici Vi sono gli operatori logici che ci aiutano a definire espressioni booleane , che in altre parole mettono in relazione il verificarsi di più condizioni. Vediamo, attraverso questa tabella come possiamo mettere in relazione due valori (o espressioni) x e y di tipo booleano: Operatore Descrizione espressione Cosa restituisce true se x e y sono entrambe vere, false altrimenti (almeno x&&y and && una delle due false) true se almeno una tra x e y è vera (o entrambe), false x||y or || altrimenti (entrambe false) !x not ! true se x è falsa e false se x è vera. Operatori Bitwise Inoltre vi sono gli operatori binari orientati ai bit, che effettuano le operazioni logiche confrontando, i bit degli operandi: Operatore Descrizione espressione Cosa restituisce il valore determinato dall’operazione di and tra i bit di x e x&y & and ‘bit a bit’ | x|y or ‘bit a bit’ xor (o or esclusivo) ‘bit a x^y bit’ ^ y >> << shift destro (o sinistro) con x>>y segno >>> shift destro x>>>y senza segno ~ complemento ~x il valore determinato dall’operazione di or tra i bit di x e y il valore determinato dall’operazione di xor tra i bit di x e y (lo xor ritorna true (o il bit 1) solo se uno degli operandi è vero e l’altro è falso) sposta i bit della variabile x verso destra (o sinistra) di y posizioni, preservando il bit di segno. Lo slittamento a sinistra prevede l’inserimento di bit 0 in coda, e non presenta particolari problemi. Con lo slittamento a destra le cose cambiano perché coinvolge l’ultimo bit che è speciale, in quanto rappresenta il segno sposta i bit della variabile x verso destra di y posizioni, senza considerare il bit di segno (vengono aggiunti sempre 0 a sinistra) inverte tutti i bit della variabile x (gli 0 diventano 1 e viceversa) Queste operazioni binarie sono molto utili nella costruzione di maschere. http://ripetizioni.doomby.com - https://www.facebook.com/Prof.Ing.Bianco https://twitter.com/ProfBianco Pag. 42 Ripetizioni Materie Scientifiche Operatore condizionale L’ultimo operatore da vedere è l’operatore condizionale, che permette di definire espressioni con due differenti risultati a seconda del valore assunto da una espressione booleana. Ecco la sintassi: (espressione boolana) ? expVero : expFalso; a seconda che il valore dell’espressione risulti vero o falso, viene eseguita la prima espressione dopo il punto interrogativo (?) o quella dopo i due punti (:). Si consideri ad esempio l’espressione seguente: int ValoreAssoluto = (a<0) ? -a : a; ValoreAssoluto assumerà il valore -a se a è negativo oppure a se è positivo o nullo. http://ripetizioni.doomby.com - https://www.facebook.com/Prof.Ing.Bianco https://twitter.com/ProfBianco Pag. 43 Ripetizioni Materie Scientifiche If e switch: costrutti condizionali in Java Possiamo pensare all’esecuzione del codice Java come alla lettura di un libro, che avviene dall’alto verso il basso. I costrutti condizionali sono delle espressioni che consentono di alterare l’usuale modo di esecuzione di un programma e di eseguire una certa porzione di codice in base ad una “scelta”. I costrutti condizionali sono quindi semplicemente il modo di tradurre sotto forma di codice algoritmi simili al seguente: SE condizione1 codice da eseguire se condizione1 è soddisfatta SE INVECE condizione2 codice da eseguire se condizione2 è soddisfatta ... ALTRIMENTI codice da sono soddisfatte eseguire se tutte le precedenti condizioni non In Java esistono sostanzialmente 2 costrutti condizionali, if-else (o if-then-else)e switch-case, in questa lezione li esamineremo entrambi. Il costrutto if in Java Iniziamo da if-else. A volte si tende a chiamare questo costrutto condizionale if-then-else anche se la keyword then non esiste. Lo esamineremo iniziando con il tradurre in Java l’algoritmo che abbiamo scritto in precedenza, in questo modo: if(condizione1) { // ... } else if (condizione2) { // ... } else { //... } Fatto ciò possiamo esplorare le caratteristiche sintattiche di questo costrutto, grazie a qualche osservazione. http://ripetizioni.doomby.com - https://www.facebook.com/Prof.Ing.Bianco https://twitter.com/ProfBianco Pag. 44 Ripetizioni Materie Scientifiche Le condizioni sono espressioni di tipo boolean Iniziamo con il dire che tutte le “condizioni” devono essere espressioni di tipo boolean (quindi risultare in true o false). Ecco qualche esempio: int a = ...; int b = ...; // OK! Sintassi corretta! if(a == b) { ... } // NO! Sintassi errata: 'a' è un 'int' e non un 'boolean' if(a) { } // mentre è valida la scrittura boolean c = ...; if(c) { ... } Meglio specificare questo particolare soprattutto per chi è abituato a linguaggi come C++ o JavaScript in cui espressioni di diversi tipi possono verificare le condizioni degli if. Precedenza negli if multipli Gli if sono valutati in successione e sarà eseguito solamente il primo per il quale la condizione risultarà vera: boolean c1 = true; boolean c2 = true; boolean c3 = false; if(c1) { // il blocco viene eseguito } else if(c2) { // il blocco non viene eseguito anche se 'c2' è 'true' } if(c3) { // il blocco non viene eseguito perché 'c3' è 'false' } else if(c2) { http://ripetizioni.doomby.com - https://www.facebook.com/Prof.Ing.Bianco https://twitter.com/ProfBianco Pag. 45 Ripetizioni Materie Scientifiche // il blocco viene eseguito, poiché 'c2' è 'true' mentre 'c3' è 'false' } ‘else’ e ‘else if’ non sono obbligatori Ci può essere un numero arbitrario di else if (il nostro “SE INVECE“), anche nessuno. Inoltre ci può essere un solo else e non è obbligatorio che ci sia Istruzioni e blocchi di istruzioni Al verificarsi di una condizione il costrutto if esegue l’istruzione oppure il blocco di istruzioni che segue. Per questo le parentesi graffe possono essere omesse nel caso che il blocco da eseguire sia composto da una sola istruzione. I seguenti due snippet sono equivalenti: if(condizione) { System.out.println("Condizione verificata"); } if(condizione) System.out.println("Condizione verificata"); All’interno di un blocco si possono nidificare altri costrutti condizionali, in effetti anche if è un’istuzione. Quindi possiamo avere casi come questo: if(condizioneUno) { if(condizioneDue) System.out.println("Entrambe verificate"); else System.out.println("Verificata solo condizioneUno"); } In questo caso e in generale quando ci sono più condizioni annidate, può essere una buona prassi decidere di utilizzare sempre le parentesi graffe, per non creare problemi di leggibilità e interpretazione del codice. switch-case Pur essendo possibile usare if-else per costruire strutture condizionli arbitarie, Java come molti altri linguaggi (il C ad esempio) ammette anche una secondo costrutto condizionale: switch-case. Vediamo subito un esempio che poi commenteremo esplorando le caratteristiche peculiari di questo costrutto in Java: switch(c) { case value1: ... break; http://ripetizioni.doomby.com - https://www.facebook.com/Prof.Ing.Bianco https://twitter.com/ProfBianco Pag. 46 Ripetizioni Materie Scientifiche case value2: ... break; // eventuali altri case case valueN: ... default: } Tipi e valori Prima di descrivere il funzionamento dello switch-case, soffermiamoci a sottolineare che il parametro dello switch (che abbiamo qui indicato con c) deve essere una espressione (o variabile) di tipo byte, short, char, int (o dei rispettivi tipi boxed) oppure Enum (Parleremo più avanti dei tipi Enum). Dalla versione 7 di Java è contemplato anche il tipo StringTypes). Questo significa che i valori nelle clausole case (value1, value2, …, valueN) devono essere del medesimo tipo del parametro (‘c‘) Il funzionamento dello switch-case La semantica del costrutto switch-case è leggermente più complicata di quella di if-then-else e per comprenderla rapidamente è probabilmente comodo pensare di leggere l’esempio precedente come segue: “dato un valore per c, procedi ad eseguire le istruzioni a partire dal case contrassegnato dallo stesso valore”. La parte su cui fare attenzione della frase precedente è “a partire da” in quanto nello switch-case l’esecuzione letteralmente salta alla riga di codice etichettata con il valore corrispondente al valore dell’argomento di switch e da quella posizione continua eseguendo tutte le istruzioni senza tener conto di eventuali case. Ad esempio: int c = ...; switch (c) { case 1: System.out.print("1 "); case 2: System.out.print("2 "); break; case 3: System.out.println("3 "); case 4: System.out.println("4 "); http://ripetizioni.doomby.com - https://www.facebook.com/Prof.Ing.Bianco https://twitter.com/ProfBianco Pag. 47 Ripetizioni Materie Scientifiche default: System.out.println("4+"); } Facciamo una tabella in cui mettiamo i diversi risultati stampati sulla console al cambiare del valore di c: Valore di ‘c’ Stampa sulla console 3 1 3 4 4+ 1 2 Nel secondo caso il comando break ne terminerà l’esecuzione (e quindi se c == 2 sarà stampato solamente “2″). break Il comando break tecnicamente parlando non fa parte del costrutto switch-case ma è spesso utilizzato in connessione ad esso e come abbiamo visto è opzionale. Vale la pena di osservare che l’istruzione break serve genericamente a terminare l’esecuzione di un blocco di programma (più precisamente ogni blocco associato a un ciclo iterativo o uno switch) ed anche se il costrutto switch-case è di gran lunga il contesto in cui lo si vede utilizzato, è sintatticamente corretto utilizzarla anche altrove. default La clausola default è opzionale e serve a determinare una porzione di codice che sarà comunque eseguita quando non viene verificata nessuna clausola case http://ripetizioni.doomby.com - https://www.facebook.com/Prof.Ing.Bianco https://twitter.com/ProfBianco Pag. 48 Ripetizioni Materie Scientifiche Ciclo for e while, costrutti iterativi in Java Se i costrutti condizionali visti nella precedente sezione rispondono all’esigenza di eseguire diverse parti di un codice subordinatamente ad una determinata condizione, i costrutti iterativi ci consentono di eseguire ripetutamente un determinato blocco di codice (che, al solito, potrà essere una singola linea oppure un intero blocco racchiuso tra parentesi graffe ‘{}’). In Java i costrutti iterativi sono sostanzialmente 3, comunemente denominati in base alle keywords che li contraddistinguono: while, do-while e for. while Il ciclo while esegue una istruzione o un blocco di codice finché rimane verificata una certa condizione. In italiano diremmo: “fino a quando la condizione è vera esegui il blocco” ecco un esempio: while(condizione) { // ... } dove condizione deve essere una variabile o una espressione di tipo booleano. In questo caso quindi esprimiamo la volontà di eseguire tutte le istruzioni del blocco ripetutamente fino a quando condizione ha valore true. Interessante capire cosa succede la prima volta che si accede al ciclo. Se condizione è false quando l’esecuzione arriva per la prima volta allo statement while il blocco di codice non sarà eseguito neanche una volta. do-while il ciclo do-while è simile al while tanto da poter essere considerato una sua variante, ed infatti il frammento di codice: do { // ... } while(condizione); analogamente a quello sopra presentato, causa l’esecuzione del blocco tra graffe fino a quando la condizione è vera ma con la importante differenza che in questo caso condizione viene presa in http://ripetizioni.doomby.com - https://www.facebook.com/Prof.Ing.Bianco https://twitter.com/ProfBianco Pag. 49 Ripetizioni Materie Scientifiche considerazione alla fine del blocco. Quindi l’esecuzione del blocco di istruzioni viene effettuata almeno una volta, anche se la condizione risulta da subito false. Continuando il parallelo con la lingua italiana questo codice andrebbe letto: “esegui il blocco di codice e, poi, se condizione è vera fallo di nuovo, altrimenti smetti”. for Il ciclo for è un costrutto tra i più conosciuti, comune praticamente a tutti i linguaggi e, pur servendo come i precedenti ad eseguire ripetutamente un blocco, fornisce una semplice sintassi per accomodare: una espressione di inizializzazione eseguita solo una volta prima di iniziare il ciclo; una espressione di ‘aggiornamento’ (tipicamente un incremento) da eseguire al termine di ogni esecuzione del blocco; una condizione di terminazione (o uscita) dall’esecuzione iterativa. Con un codice simile al seguente: for(inizializzazione; condizione; incremento) { // ... } si ottiene un programma che esegue esattamente una volta inizializzazione, esegue poi il blocco, quindi effettua l’incremento, valuta condizione e, se questa risulta vera (true, come al solito condizione deve essere di tipo boolean), esegue di nuovo blocco, alla fine del quale ripete il test e così via. È semplice convincersi con un esempio che un ciclo for ed uno while sono facilmente intercambiabili: int i=0; while(i < 10) { // ... i++; } è identico a: for(int i=0; i<10; i++) { // ... } dove si vede anche la comune pratica di definire le variabili di iterazione ( i nell’esempio) direttamente all’interno della sezione di inzializzazione rendendole locali al blocco (ed alle sezioni di incremento e terminazione). http://ripetizioni.doomby.com - https://www.facebook.com/Prof.Ing.Bianco https://twitter.com/ProfBianco Pag. 50 Ripetizioni Materie Scientifiche Cicli infiniti Si osserva che essendo inizializzazione, incremento e condizione opzionali non è strano trovare casi in cui vengano omesse fino all’estremo: for(;;) { // ... } letto anche “for-ever” o ciclo infinito, poiché la condizione di terminazione è omessa e per default considerata come true (il medesimo comportamento si otterrebbe naturalmente con “ while(true) {}“). for each Una importante variante del ciclo for è quella che potremmo definire for-each (detta anche for-in) che serve nel caso particolare (ma comune) in cui si voglia eseguire un determinato blocco di codice per ogni elemento di una data collezione (o array). Pur dovendo rimandare una completa descrizione di questa variante a quando parleremo di Collection e array possiamo comunque mostrarne la sintassi: for( Type item : itemCollection ) { // ... } che, continuando i paralleli con la lingua italiana si legge come: “prendi uno ad uno gli elementi della collezione itemCollection, assegna ciascuno di essi alla variabile item ed esegui per ciascun elemento il blocco (che potrà quindi usare item al suo interno)”. for-each, le Collection e i tipi generics Anche se parleremo più avanti di “Collection” e “generics”, in questa fase ci basta sapere che si tratta di collezioni di oggetti alle quali si applicano i cosiddetti iteratori per effettuare una scansione di tutti gli elementi. Ecco un esempio pratico di una tipica routine di iterazione degli elementi di una Collection che utilizzi i tipi generics: Queue<String> queue = new LinkedList<String>(); for(Iterator<String> it = queue.iterator(); it.hasNext(); ) { String tmp = it.next(); // ... qui fa qualcosa } http://ripetizioni.doomby.com - https://www.facebook.com/Prof.Ing.Bianco https://twitter.com/ProfBianco Pag. 51 Ripetizioni Materie Scientifiche Senza l’ausilio dei tipi generics la cosa diventa ancora più ardua, poiché bisogna effettuare il cast (su it.next()) con il rischio di un’eccezione a runtime. In realtà, seppure con la miglioria dei tipi generics, questa iterazione non è pulita in quanto ci obbliga a utilizzare una struttura dati (Iterator) che di fatto non utilizziamo. Come abbiamo visto invece, il ciclo for-each permette una definizione automatica di tutto ciò in un solo comando integrato: for(String tmp:queue) { //... } L’utilizzo della struttura nel secondo esempio verrà tradotto con la codifica definita nel primo, facendoci però perdere qualsiasi riferimento all’iteratore che lavorerà dietro le quinte. Si tratta quindi di una semplificazione che sicuramente dà dei benefici in termine di migliore codifica ma assolutamente non intacca le prestazioni né in positivo né in negativo. public class ForeachTest { public static void main(String[] args) { Collection<string> coll = new ArrayList<String>(); // utilizziamo l'array degli argomenti con for-each // e popoliamo la collezione for(String tmp:args){ coll.add(tmp); } // stampiamo la collezione for(String tmp:coll) { System.out.println(tmp); } } } </string> Per semplicità lavoriamo con un array di String (l’array degli argomenti, args, del main) e una Collection<String> dove ci metteremo il contenuto del primo array. Come si vede dal codice nel primo caso facciamo una semplice iterazione, utilizzando l’oggetto tmp (che ha ciclo di vita all’interno del for). Nel secondo iteriamo sulla collezione stampando il risultato. http://ripetizioni.doomby.com - https://www.facebook.com/Prof.Ing.Bianco https://twitter.com/ProfBianco Pag. 52 Ripetizioni Materie Scientifiche Break e Continue Break e continue sono comunemente detti costrutti di branching ed in Java servono a controllare l’esecuzione di un blocco di codice in un modo sostanzialmente opposto a come operano i costrutti iterativi e condizionali. Questi ultimi infatti, focalizzano sull’esecuzione (eventuelmente ripetuta) di un intero blocco di codice, mentre break e continue servono per terminare (in un senso che sarà chiarito sotto) l’esecuzione di un blocco di codice in un ciclo o uno statement switch. break Lo statement break (già visto nella sua forma più semplice e comune quando abbiamo parlato di switch-case) serve per terminare l’esecuzione di uno o più blocchi di codice. In altre parole serve per “saltare fuori” da costrutti iterativi o switch-case. La forma completa di break, detta labeled per la presenza di una etichetta, è la seguente: etichetta: loop-or-switch { // blocco prima del break loop-or-switch-2 { // ... break etichetta; } // blocco dopo il break // (senza etichetta il programma salterebbe qui!) } // fine del blocco etichettato (il programma salta qui!) dove: http://ripetizioni.doomby.com - https://www.facebook.com/Prof.Ing.Bianco https://twitter.com/ProfBianco Pag. 53 Ripetizioni Materie Scientifiche etichetta è un identificatore (come definito nella sezione sulle variabili) e serve per assegnare un nome allo statement in modo che possa essere utilizzato per specificare quale blocco si vuole terminare al momento del break; loop-or-switch può essere un costrutto tra for, while, do, switch Quando in un programma viene trovato un break etichettato l’esecuzione del codice letteralmente salta alla fine del blocco contrassegnato dall’etichetta (quindi se ci sono più blocchi innestati salta alla fine del livello identificato dall’etichetta). Se l’etichetta è omessa, per default, il programma salta alla fine del blocco corrente. continue Similmente a break lo statement continue serve per saltare alla fine di un blocco ma in questo caso non viene terminato il ciclo ma solamente interrotta l’iterazione corrente e l’esecuzione salta immediatamente alla valutazione della condizione di terminazione. Eventualmente poi si procederà ad una nuova iterazione. Naturalmente non avrebbe senso utilizzare continue con lo statement switch ed infatti è sintatticamente proibito. La peculiarità di continue è forse più facilmente comprensibile per mezzo dei seguenti esempi: int sum = 0; for(int i = 1; i < 10; i++) { if(i%2 == 0) { break; // il blocco che viene interrotto con break // è quello "corrente" nel senso // "del ciclo corrente" (il for) } sum++; } System.err.println(sum); Questo snippet produce come output “ 1“, quindi le istruzioni dopo il break sono state eseguite solamente una volta mentre in questo esempio in cui sostituiamo break con continue: int sum = 0; for(int i = 1; i < 10; i++) { if(i % 2 == 0) { continue; } sum++; } System.err.println(sum); http://ripetizioni.doomby.com - https://www.facebook.com/Prof.Ing.Bianco https://twitter.com/ProfBianco Pag. 54 Ripetizioni Materie Scientifiche otteniamo come output “5“. Questa volta le istruzioni dopo continue sono state eseguite per tutti i numeri tra 1 e 10 (escluso) che non sono divisibili per 2 (1,3,5,7,9 appunto 5 volte). Anche per continue esiste una versione labeled del tutto simile a quella vista per brerak che consente di saltare alla fine della corrente iterazione del ciclo contrassegnato dall’etichetta. Nonostante esistano contesti in cui break e continue sono molto comodi (per alcuni si veda anche qui) si osserva che la leggibilità del codice viene di solito molto ridotta dal loro uso (sicuramente fatta eccezione per l’istruzione switch in cui break è spesso chiaramente indispensabile) e quindi prima di cimentarsi in strutture complicate da leggere (e quindi da documentare e soprattutto mantenere) si invita a riflettere se non esista un approccio più chiaro. I Metodi in Java In questa lezione esaminiamo più da vicino i metodi, una parte fondamentale della programmazione Java. Vedremo cosa sono, come è possibile definirli, identificarne la “firma” e come utilizzarli. Cos’è un metodo Tutti i linguaggi di programmazione forniscono la possibilità di definire, sotto un solo nome, interi gruppi di istruzioni o insiemi di linee di codice o blocchi di espressioni (statements), che dir si voglia. In questo modo possiamo riutilizzare un blocco di codice in molte parti del programma, semplicemente richiamando il nome con cui l’abbiamo definito, senza dover riscrivere tutto il blocco ogni volta. Questi aggregati, a seconda del linguaggio, si chiamano funzioni, procedure, subroutines o sottoprogrammi. In Java utilizziamo la logica definita dai blocchi istruzioni per rappresentare il comportamento di classi di oggetti e questi blocchi di codice prendono il nome di metodi. Definire un metodo in Java La sintassi per la definizione di un metodo è molto simile a quella per la definizione di una variabile con la quale mutua peraltro numerose caratteristiche: [public|protected|private] [static] [final] Tipo identificatore([Tipo1 parametro1, Tipo2 parametro2, ..., TipoN parametroN]) [throws Eccezione1, Eccezzione2, ...] { // blocco di codice appartenente al metodo return varTipo; } http://ripetizioni.doomby.com - https://www.facebook.com/Prof.Ing.Bianco https://twitter.com/ProfBianco Pag. 55 Ripetizioni Materie Scientifiche Come di consuetidine tutte le parti tra parentesi quadre sono da considerarsi opzionali e, come vedremo tra poco lo statement return è opzionale nel caso in cui Tipo (il tipo di fronte all’identificatore) sia void. Nella definizione di un metodo, l’identificatore è il nome assegnato al blocco di codice e che dovrà essere utilizzato per chiamare (eseguire) il metodo; sintatticamente si applicano esattamente le medesime considerazioni fatte per le variabili e come per esse vige la convenzione di far iniziare i nomi dei metodi con un carattere minuscolo e proseguire con il consueto camelcase. I parametri La sezione [Tipo1 parametro1, ... TipoN parametroN] è detto insieme dei parametri (formali) del metodo in cui si dichiara il tipo ed il nome simbolico delle variabili che il blocco di codice dovrà ricevere dal programma chiamante per poter svolgere il proprio compito. Ad esempio, per scrivere un metodo che calcoli l’area di un parallelelogramma dovremo scrivere del codice che, noti i valori della lunghezza della base e dell’altezza esegua l’operazione base * altezza. La dichiarazione: public double areaParallelogramma(double base, double altezza) { // ... } ha lo scopo di avvertire il compilatore che nel corpo del metodo (tra le parentesi graffe come al solito) i nomi base e altezza fanno riferimento ai valori di tipo double che saranno passati al metodo quando lo utilizzeremo. Per inciso i valori che inseriamo al momento della chiamata del metodo sono detti parametri attuali. Quindi possiamo scrivere il metodo in questo modo: public double areaParallelogramma(double base, double altezza) { double a = base * altezza; return a; } Richiamare il metodo Per chiamare il nostro metodo all’interno della classe in cui lo stiamo definendo (fuori dalla classe di definizione occorre una sintassi differente, che vedremo in seguito), sarà sufficiente scrivere qualcosa del genere: double areaCalcolata; areaCalcolata = areaParallelogramma(7.0, 6.0); Una volta eseguito questo codice la variabile areaCalcolata avrà valore 42.0. http://ripetizioni.doomby.com - https://www.facebook.com/Prof.Ing.Bianco https://twitter.com/ProfBianco Pag. 56 Ripetizioni Materie Scientifiche Possiamo pensare che la macchina virtuale durante l’esecuzione del codice della chiamata al metodo salti letteralmente all’inizio della dichiarazione del metodo, inizializzi le variabili (formali) base ed altezza con i valori (attuali) 7.0 e 6.0, esegua quindi il corpo del metodo fino a quando non incontri la keyword return e a quel punto prenda il valore dell’argomento di return e lo utilizzi per assegnare il valore alla variabile areaCalcolata. Return e il valore di ritorno del metodo Il valore specificato accanto a return deve essere del medesimo tipo specificato nella dichiarazione del metodo ma deve essere omesso se il metodo è stato dichiarato come void. Traducibile come “vuoto” dichiarare un metodo void significa dire che il metodo non ritornerà alcun valore ed in tal caso la keyword return può essere anche omessa. Lo statement return merita una certa attenzione anche perché, pur essendo strettamente legato ai metodi, potremmo accomunarlo a break e continue introdotti in precedenza. Infatti anche l’esecuzione del return provoca un salto nell’esecuzione, in questo caso un salto fuori dal metodo. Questa caratteristica risulta più chiara se immaginiamo di voler modificare il calcolo dell’area in modo che il prodotto venga eseguito solo se i parametri sono diversi da 0: public double areaParallelogramma(double base, double altezza) { if(base == 0.0 || altezza == 0.0) return 0.0; // questo return causa l'uscita dal metodo double a = base * altezza; return a; } Se uno dei due parametri è 0, viene eseguito il primo return e il controllo passa al codice chiamante. Quindi il calcolo del prodotto viene effettuato solo nel caso in cui entrambi i parametri siano diversi da 0. Throws, sollevare o rilanciare le eccezioni Della keyword throws e dei tipi che la seguono parleremo in una opportuna sezione sulle eccezioni ma l’idea generale è che Java ci consente di sollevare delle eccezioni circa le operazioni di un metodo e queste eccezioni possono essere poi utilizzate dal nostro codice per gestire le condizioni di errore. Per fissare le idee possiamo immaginare, sempre nel caso dell’area, di voler gestire il fatto che sia base che altezza sono da considerarsi lunghezze e quindi non ha senso che possano essere specificate come quantità negative (ma d’altro canto il tipo double non è abbaastanza espressivo per vincolare le nostre variabili ad essere positive); scriveremmo in tal caso qualcosa del tipo: public double areaParallelogramma(double base, double altezza) throws IllegalArgumentException { if(base < 0) http://ripetizioni.doomby.com - https://www.facebook.com/Prof.Ing.Bianco https://twitter.com/ProfBianco Pag. 57 Ripetizioni Materie Scientifiche throw new IllegalArgumentException("base negativa"); if(altezza < 0) throw new IllegalArgumentException("altezza negativa"); if(base == 0.0 || altezza == 0.0) return 0.0; double a = base * altezza; return a; } Senza dilungarci troppo, anche throw termina l’esecuzione del blocco corrente come return ma senza che debba essere specificato un valore di ritorno. Signature, la firma dei metodi È importante notare che in Java un metodo è univocamente determinato (oltre che dalla classe a cui appartiene naturalmente) dal suo identificatore e dalla lista dei tipi dei parametri che riceve in input. Tutto questo definisce la signature del metodo (la firma) il che significa che siamo liberi di ridefinire il medesimo metodo più volte a patto che ogni definizione abbia una lista di parametri diversa (tipi dei parametri, i nomi non contano, non conta nemmeno il tipo di ritorno). In questo modo possiamo sovraccaricare un identificatore di metodo con diverse definizioni ed effettuare il cosiddetto overloading. Capita spesso quindi che un metodo abbia diversi overload. Sarebbe quindi del tutto lecito definire, accanto al metodo che prende in input il valore della base e dell’altezza definire, ad esempio, un metodo areaParallelogramma che prenda in input le lunghezze di due lati incidenti e l’angolo tra di loro: public double areaParallelogramma(double c1, double c2, double alfa) throws IllegalArgumentException { if(c1 < 0) throw new IllegalArgumentException("c1 negativa"); if(c2 < 0) throw new IllegalArgumentException("c2 negativa"); if(c1 == 0.0 || c2 == 0.0) return 0.0; double a = c1 * c2 * Math.sin(alpha); return a; } sarà il compilatore a scegliere al momento della chiamata quale metodo utilizzare sulla base del tipo (e del numero) degli argomenti attuali passati. // chiama il primo areaParallelogramma(5.0, 4.0); // chiama il secondo http://ripetizioni.doomby.com - https://www.facebook.com/Prof.Ing.Bianco https://twitter.com/ProfBianco Pag. 58 Ripetizioni Materie Scientifiche areaParallelogramma(5.0, 4.0, 0.5); // errore in fase di compilazione: // non esiste un metodo che prende solo un argomento areaParallelogramma(5.0); Modificatori di visibilità Resta a questo punto da parlare solamente dei qualificatori del metodo i primi (public, private e protected) hanno il medesimo significato che avevano per le variabili e detetminano chi (quale parte del codice) possa vedere (utilizzare) in metodo: public significa che sarà visibile dovunque, private solo alla classe, protected solo dalle classi che stanno nel medesimo package di quella in cui è definito il metodo e dalle classi da essa derivate, infine default (come al solito identificato dalla omissione del qualificatore) implicherà visibilità alle classi nel medesimo package. Metodi final Il qualificatore final serve, nel caso dei metodi, per rendere un metodo non ridefinibile dalle sottoclassi (avremo modo di vederne esempi in futuro): se un metodo viene contrassegnato come final le sottoclassi lo potranno utilizzare e lo erediteranno (quindi lo avranno disponibile) ma non potranno modificarlo (o come si dice comunemente: non potranno fare override del metodo). http://ripetizioni.doomby.com - https://www.facebook.com/Prof.Ing.Bianco https://twitter.com/ProfBianco Pag. 59 Ripetizioni Materie Scientifiche Overload di metodi e variable arguments Come abbiamo detto parlando di signature dei metodi, tra le caratteristiche note di Java possiamo citare quella dell’overloading di metodi, che ci consente di “sovraccaricare” un metodo di una classe con diverse varianti, in base ai parametri passati come riferimento. Dal punto di vista dell’utilizzo della classe ciò ci consente di definire in maniera dinamica il metodo che meglio si adatta alla nostra esigenza. Poniamo come esempio la classe Prodotto che gestisce un prodotto e le relative caratteristiche. Un prodotto può contenere diverse note allegate, una serie di caratteristiche che lo descrivono. Il codice di una semplice classe con relativo costruttore sarebbe qualcosa del genere. /** * Esempio di una classe con un costruttore */ package it.html; public class Prodotto { private int id; // ... public Prodotto(int id, String desc) { // ... } public Prodotto(int id, String desc1, String desc2) { // ... } public Prodotto(int id, String desc1, String desc2, String desc3) { http://ripetizioni.doomby.com - https://www.facebook.com/Prof.Ing.Bianco https://twitter.com/ProfBianco Pag. 60 Ripetizioni Materie Scientifiche // ... } } In questo caso l’overload dei metodi ci permette di avere una serie di costruttori in base a quante informazioni vogliamo passare. Se pensiamo di proseguire con l’introduzione di nuovi parametri intuiamo la limitazione di dover inserire un numero crescente di dichiarazioni. Varargs A partire dalla versione 1.5 di Java 1.5, abbiamo però il cosiddetto varargs (Variable Arguments), un meccanismo per definire un numero di argomenti indefinito. Utilizzando il modificatore … (puntini sospensivi o ellipsis) è possibile definire la presenza di un numero variabile di argomenti. Vediamo subito la cosa applicata al nostro caso: /** * Esempio di utilizzo di varargs */ package it.html; public class Prodotto { private int id; // ... public Prodotto(int id, String... desc) { // ... } } Il metodo si aspetta 0 o N parametri di tipo String da utilizzare in dipendenza della logica del metodo (o del costruttore, come in questo caso). Dal punto di vista della macchina virtuale è come definire un metodo che si aspetta una array di String: public Prodotto(int id, String[] desc) { // ... } È molto importante ricordare questo per capire il funzionamento e utilizzare i parametri all’interno del metodo. A questo punto ci si potrebbe chiede a che giova utilizzare i varargs: il vantaggio sta nel poter passare parametri ai medodi in modo più sintetico: // chiamata con varargs metodoX(1,"uno","due","tre"); // chiamata senza varargs metodoX(1,new String["uno","due","tre"]); Il codice risulta evidentemente più elegante, nonché chiaro, nel primo esempio, mentre del secondo non capiamo bene il perché sia necessaria una presenza di un array. Ecco un semplice programma per testare il funzionamento del costrutto: http://ripetizioni.doomby.com - https://www.facebook.com/Prof.Ing.Bianco https://twitter.com/ProfBianco Pag. 61 Ripetizioni Materie Scientifiche /** * Test del varargs */ public class VarargsTest { public void testVarargs(String... list) { System.out.println("Metodo 1"); for (String tmp:list) { System.out.println("#" + tmp); } } public static void main(String[] args) { System.out.println("Test varargs"); VarargsTest va=new VarargsTest(); va.testVarargs("do","re","mi","fa","sol","la","si"); va.testVarargs("1","2","3","4"); } } Come si vede dal corpo del metodo utilizziamo il costrutto for-each e di fatto utilizziamo il parametro list come un array. Per verificare che alla fine la struttura dati utilizzata è proprio l’array, inseriamo e mandiamo in esecuzione la seguente riga di codice: va.testVarargs(new String[]{"do","re","mi","fa","sol","la","si"}); Il risultato sarà lo stesso di prima. Unica cosa a cui bisogna fare attenzione è il caso in cui l’overload possa causare ambiguità. Proviamo ad aggiungere il seguente metodo alla classe: /** * Test di ambiguità sul varargs */ public void testVarargs(String arg1,String... list) { // ... } Il compilatore procederà correttamente ma ci darà un errore al momento di utilizzare il metodo, in quanto si trova nell’impossibilità di segliere il metodo corretto (non avendo indicazione su quale scegliere). Infatti, in questo caso, non possiamo sapere se la prima stringa fa parte della lista di parametri o è un parametro a sé stante. In queste situazioni, utilizzare un array potrebbe essere un modo per risolvere. http://ripetizioni.doomby.com - https://www.facebook.com/Prof.Ing.Bianco https://twitter.com/ProfBianco Pag. 62 Ripetizioni Materie Scientifiche Metodi statici (static) e metodi di istanza Tra tutti i qualificatori utilizzabili nella dichiarazione di un metodo quello che più di ogni altro ne modifica il funzionamento (nel senso che chiariremo tra poco) è static. Per comprendere l’uso della keyword static in Java bisogna ricordare innanzi tutto che, come detto, ogni metodo appartiene ad una classe ed una classe è, in qualche modo, un “pacchetto” di dati e metodi. Sappiamo che da una classe possiamo ottenere molteplici istanze e per ciascuna istanza si hanno variabili dai nomi identici ma dai valori distinti (forse “che puntano a locazioni di memoria diverse” sarebbe una definizione più chiara). Se poi vogliamo che una variabile sia la medesima per tutte le istanze di una classe sappiamo che la dobbiamo invece definire come static. Per i metodi avviene sostanzialmente la medesima cosa: possiamo pensare che dei metodi definiti in una classe ne esista normalmente (cioè se non si specifica static) una “copia” per ogni istanza della classe, mentre dei metodi statici ne esista una sola copia associata alla classe stessa. Per scendere più in dettaglio: i metodi non statici sono associati ad ogni singola istanza di una classe e perciò il loro contesto di esecuzione (quindi l’insieme delle variabili cui possono accedere) è relativo all’istanza stessa: possono accedere e modificare le variabili dell’istanza e modificarne lo stato; in contapposizione i metodi statici non sono associati ad una istanza ma solo ad una classe. Quindi non potranno interagire con le variabili di istanza, ma solamente con quelle statiche. Questa distinzione tra metodi statici e metodi di istanza si riflette anche in una diversa sintassi che si deve utilizzare per eseguire i 2 tipi di metodi: http://ripetizioni.doomby.com - https://www.facebook.com/Prof.Ing.Bianco https://twitter.com/ProfBianco Pag. 63 Ripetizioni Materie Scientifiche Tipo di metodo Sintassi NomeClasse.nomeMetodo(...) Statico Non statico (di istanza) nomeIstanza.nomeMetodo(...) Per NomeClasse si intende il nome di una classe e non di una istanza (come si potrebbe intuire anche dalla convenzione per cui le istanze non iniziano mai con una lettera maiuscola). Per la precisione anche i metodi statici possono essere richiamati utilizzando una istanza invece che il nome della classe, ma questa è considerata una cattiva pratica e segnalata dal compilatore con un warning. Cerchiamo di chiarire ulteriormente la differenza tra i diversi tipi di metodi utilizzando il codice seguente in cui si presenta la struttura di base di una micro-libreria per la rappresentazione di figure piane (solo i parallelogrammi sono definiti naturalmente). Innanzi tutto definiamo una classe di metodi di servizio utilizzabili dovunque e non legati ad una specifica istanza, quindi tutti statici: public class Geometry2DUtils { public static final double PiGreco = 3.141592; public static double areaParallelogramma(double base, double altezza) { } public static double areaParallelogramma(double c1, double c2, double alpha) { } // ... public static double areaCerchio(double r) { // anche se static potrà usare la variabile statica PiGreco return PiGreco * r * r; } } continuiamo poi con una classe che rappresenti i parallelogrammi public class Parallelogramma { private double base, altezza; public Parallelogramma(double base, double altezza) { // inzializza le variabili di istanza this.base = base; this.altezza = altezza; } // questo metodo area non ha bisogno dei // parametri in quanto utilizza le variabili della classe public double area() { // utilizza il metodo (pubblico e statico) nella classe di utility // chiamandolo usando il nome della classe (poi il compilatore // sceglie quello giusto sulla base dei parametri) return Geometry2DUtils.areaParallelogramma(base, altezza); http://ripetizioni.doomby.com - https://www.facebook.com/Prof.Ing.Bianco https://twitter.com/ProfBianco Pag. 64 Ripetizioni Materie Scientifiche } // metodi per l'accesso (getters) delle variabili private della classe, // garantiscono, tra l'altro, che i field possano essere utilizzati ma non // modificati public double getBase() { return base; } public double getAltezza() { return altezza; } } A questo punto potremo utilizzare le nostre classi in un immaginario metodo main (static) public class Programma { public static void main(String[] args) { // creiamo una istanza della classe Parallelogramma // e la assegnamo alla variabile p (che ha il tipo opportuno) Parallelogramma p = new Parallelogramma(4.0, 8.0); // creiamo una seconda istanza di Parallelogramma che avrà // metodi e variabili con i medesimi nomi di p ma distinti // quanto a valore e contesto di esecuzione Parallelogramma p1 = new Parallelogramma(11.0, 18.0); double pArea = p.area(); // assegna a pArea 4*8 double p1Area = p1.area(); // assegna a p1Area 11*18 // il medesimo risulato di pArea si avrebbe naturalmente con double spArea = Geometry2DUtils.areaParallelogramma(p.getBase(), p.getAltezza()); // ma effattuata. con una minore chiarezza circa l'oggetto dell'operazione //... } } http://ripetizioni.doomby.com - https://www.facebook.com/Prof.Ing.Bianco https://twitter.com/ProfBianco Pag. 65 Ripetizioni Materie Scientifiche Array in Java che permette di gestire una sequenza di lunghezza fissa di elementi tutti del medesimo tipo. Il numero di elementi in un array, detto lunghezza dell’array, deve essere dichiarato al momento della sua allocazione e non può essere cambiato. Dichiarare un array in Java La sintassi per la dichiarazione di una variabile di tipo array è la seguente: Tipo[] nome; nella quale si deve osservare l’uso delle parentesi quadre ‘ []‘ tra il tipo ed il nome della variabile (anche se la documentazione riporta sempre questa forma della dichiarazione è ammissibile anche postporre le parentesi quadre dopo l’identificatore: Tipo nome[]; Tipo può essere sia un tipo primitivo di java, sia il nome di una classe. Allocare l’array Per default le variabili di tipo array sono inizializzate con il valore null. Quindi, prima di poterle usare, dovremo inizializzarle allocando per esse la memoria per mezzo della consueta keyword ‘new’: nome = new Tipo[n]; http://ripetizioni.doomby.com - https://www.facebook.com/Prof.Ing.Bianco https://twitter.com/ProfBianco Pag. 66 Ripetizioni Materie Scientifiche che riserva (o ‘allocà) la memoria necessaria per contenere n elementi di tipo Tipo. Tutti gli elementi dell’array sono inizializzati con il valore di default previsto dal tipo che abbiamo indicato. Ad esempio, se Tipo è una classe tutti gli elementi verranno inizializzati a null e quindi andranno a loro volta allocati. Usare gli array Una volta creato l’array, possiamo accedere ai singoli elementi indicandone la posizione (detta indice) all’interno contentitore, grazie all’operatore ‘ []‘ (parentesi quadre). Ad esempio: nome[3]; // indica il quarto elemento della collezione Bisogna ricordare che gli elementi sono numerati a partire da zero, quindi nome[0] farà riferimento al primo elemento, nome[1] al secondo e così via fino all’ultimo indicato da nome[lunghezza-1]. Come detto sopra la lunghezza di un array è fissata in fase di creazione e si può in qualsiasi momento accedere al suo valore utilizzando la proprietà length che è presente in ogni array java con la sintassi: nome.length. Gli array sono spesso utilizzati per mantenere informazioni statiche all’interno di un programma; ad esempio per determinare quanti giorni abbia un mese dato il suo numero potremmo pensare di scrivere un metodo del tipo: int giorniMese(int numeroMese) { return numeroGiorniPerMese[numeroMese -1]; } che per funzionare ha bisogno di un array che contenga, mese per mese, il numero di giorni. Quindi: int[] numeroGiorniPerMese dell'array; = new int[12]; // dichiarazione e allocazione giorniPerMese[0] = 31; // gennaio, primo mese, posizione 0 giorniPerMese[1] = 28; // febbraio (non funziona per i bisestili ma non ci interessa) // etc ... giorniPerMese[12] = 31; // dicembre Inizializzare array con liste (literals) L’inizializzazione degli elementi dell’array è prolissa, poco leggibile e decisamente scomoda da collocare nel codice (soprattuto se vogliamo dichiarare numeroGiorniPerMese come static, provate per fare pratica !!). Ma Java ammette una sintassi per allocare ed inizializzare gli array in modo più diretto: int [] numeroGiorniPerMese = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}; Questa modalità prevede che i valori degli elementi dell’array possano essere elencati in una lista racchiusa tra parentesi graffe e separati da virgole. Naturalmente tutti i valori della lista devono http://ripetizioni.doomby.com - https://www.facebook.com/Prof.Ing.Bianco https://twitter.com/ProfBianco Pag. 67 Ripetizioni Materie Scientifiche essere del tipo specificato per l’array (o ad esso assegnabile, ma questa affermazione risulterà probabilmente più chiara quando avremo preso confidenza con con il concetto di derivazione di una classe da un’altra). Errori con gli array Quando utilizziamo gli array è nostra responsabilità non tentare di accedere ad elementi esterni al range definito. Ad esempio: int l = 5; int [] a = new int[l]; a[9] = 10; Quando mandiamo in esecuzione di questo pezzo di codice, esso genera un errore di runtime (non di compilazione): la JVM, quando chiediamo di accedere al decimo elemento dell’array, emette una eccezione di tipo ArrayIndexOutOfBoundsException. Array multidimensionali Java permette anche l’utilizzo di array di array (detti anche array multimensionali) di profondità arbitraria (o con numero di dimensioni arbitrario) e la sintassi per la loro dichiarazione ed allocazione si ottiene semplicemente ripetendo le parentesi quadre tante volte quante il numero di dimensioni, per esempio: int [][][] arrayConDimensione3 = new int[4][5][6]; Analogamente a quanto detto per gli array unidimensionali, Java prevede una sintassi speciale per l’inizializzazione degli array multidimensionali: float[][] mat { { { }; = new 1, 2, 4, 5, 7, 8, int[][] = { 3 }, 6 }, 9, 10, 11 } L’ultima riga mostra anche come sia possiblie creare array multidimensionali ‘ragged‘, cioè nei quali i sotto-array non abbiano tutti la medesima lunghezza. È possibile creare array ragged anche senza inizializzarli immediatamente, ad esempio con la sintassi che segue: int [][] ar = new int[10][]; for(int i =0; i< 10; i++) { ar[i] = new int[(i%3) +1]; } http://ripetizioni.doomby.com - https://www.facebook.com/Prof.Ing.Bianco https://twitter.com/ProfBianco Pag. 68 Ripetizioni Materie Scientifiche che risulta chiara se si pone attenzione al fatto che dato un array n-dimensionale possiamo sempre ottenerne una sezione k dimensionale (con k < n) semplicemente specificando le parentesi quadre solo per le n-k dimensioni che vogliamo fissare ed omettendole per quelle che si vogliono lasciare libere: int [][][] cubo = new int[10][10][10]; int[][] quadrato1 = cubo[0]; int[][] quadrato2 = cubo[1]; int[] segmento = quadrato1[0]; int i = segmento[4]; int j = quadrato2[3][5]; int k = cubo[3][5][7]; java.lang.Arrays Gli array sono un costrutto classico di praticamente ogni linguaggio di programmazione e, nonostante il limite notevole di non poter cambiare dimensione (size) dopo la creazione (e qualche complicatezza sintattica degli array multidimensionali), il loro utilizzo è estremamente comune in molti ambiti. Perciò Java mette a disposizione la classe java.lang.Arrays con numerosi algoritmi per operare sugli array: ricerca; sorting (ordinamento); copia; e altri strumenti per la manipolazione, tutti sotto forma di metodi statici. http://ripetizioni.doomby.com - https://www.facebook.com/Prof.Ing.Bianco https://twitter.com/ProfBianco Pag. 69 Ripetizioni Materie Scientifiche Stringhe in Java Testi, messaggi e codici sono solo alcune delle applicazioni che hanno le stringhe in programmazione. In Java esse sono rappresentate come sequenze di caratteri unicode (UTF-16) e possiamo crearle e manipolarle grazie alla classe String, messa a disposizione nel core di Java (java.lang.String). Vediamo quindi come dichiarare le stringhe ed effettuare su di esse le operazioni più classiche. Definire una stringa in Java Il modo più semplice e diretto per creare un oggetto di tipo String è assegnare alla variabile un insieme di caratteri racchiusi fra virgolette: String titolo = "Lezione sulle stringhe"; questo è possibile in quanto il compilatore crea una variabile di tipo String ogni volta che incontra una sequenza racchiusa fra doppi apici; nell’esempio la stringa "Lezione sulle stringhe" viene trasformata in un oggetto String e assegnato alla variabile titolo. Questa forma di inizializzazione è detta string literal Oltre alla modalità “literal”, poiché si tratta comunque di oggetti, le variabili di tipo String possono essere inizializzate anche utilizzando la keyword new e un costruttore come si vede nell’esempio seguente: http://ripetizioni.doomby.com - https://www.facebook.com/Prof.Ing.Bianco https://twitter.com/ProfBianco Pag. 70 Ripetizioni Materie Scientifiche public void initString() { // Inizializzazione con una new String titolo = new String("Titolo dell'opera"); // Inizializzazione che fa uso di un array di caratteri char[] arraySottotitolo = {'s','o','t','t','o','t','i','t','o','l','o','!'}; String sottotitolo = new String(arraySottotitolo); } Dalla versione 8 di Java, String conta ben 13 costruttori (oltre a 2 deprecati ed una decina di metodi statici che in qualche modo creano istanze di tipo stringa a partire da altri tipi di variabili). Length, la lunghezza di una stringa La classe String espone anche numerosi metodi per l’accesso alle proprietà della stringa sulla quale stiamo lavorando; uno di questi è il metodo length(), che ritorna il numero di caratteri contenuti nell’oggetto. La sua signature è: int length() Per esempio le seguenti linee di codice: public void printLength() { String descrizione = "Articolo sulle stringhe ..."; int length = descrizione.length(); System.out.println("Lunghezza: "+length); } stampano come risultato: Lunghezza: 27 In questo esempio è interessante notare anche come il compilatore interpreti automaticamente l’espressione "Lunghezza: "+length, creando un oggetto di tipo String ottenuto concatenando la stringa che troviamo in prima posizione e la stringa ottenuta dalla rappresentazione decimale del valore di length (variabile di tipo int). In sostanza viene svolta per noi una conversione di tipo senza la quale avremmo dovuto scrivere qualcosa di questo genere: "Lunghezza: " + String.valueOf(length); Il metodo (statico) valueOf di String, restituisce la rappresentazione testuale del parametro di tipo int che riceve in ingresso. Stringa = array di caratteri http://ripetizioni.doomby.com - https://www.facebook.com/Prof.Ing.Bianco https://twitter.com/ProfBianco Pag. 71 Ripetizioni Materie Scientifiche Possiamo pensare a una stringa esattamente come a un array di caratteri, questo significa che possiamo considerare i singoli caratteri come elementi di array. Consideriamo questa stringa: String str = "Ciao HTML.it"; Il carattere 'C' è alla posizione 0, il carattere 'i' è alla posizione 1, … il carattere 't' finale è alla posizione 11, che coincide con str.length()-1. Per accedere ai singoli caratteri non possiamo usare l’operatore ‘[]‘ come negli array, ma possiamo sfruttare il metodo charAt: char charAt(int index); utf-16 Se vogliamo utilizzare le stringhe come array dovremo porre la massima attenzione alla rappresentazione unicode/utf-16 che prevede che i cosiddetti “supplementary characters” occupino 2 posizioni nell’array; magari questo non ha molta rilevanza con il nostro sistema locale ma le cose potrebbero diventare complicate. Concatenare le stringhe L’operazione di concatenazione di stringhe può essere effettuata in modi diversi. La classe String fornisce il metodo concat per la concatenazione di stringhe la cui signature è: String concat(String str); Quindi: String str1 = new String("Nome "); String str2 = new String("Cognome "); String str3 = str1.concat(str2); assegna a str3 una nuova stringa formata da str1 con str2 aggiunto alla fine; insomma "Nome Cognome". Avremmo potuto ottenere la stessa cosa in modopiù semplice utilizzando l’operatore ‘ +‘: String str1 = "Nome"; String str2 = "Cognome"; String str3 = str1+str2; Oppure avremmo potuto costruire la stringa concatenata direttamente tramite literals: String str3 = "Nome"+"Cognome"; Substring, estrarre una sottostringa Per prelevare e manipolare solo una porzione di una stringa possiamo utilizzare il metodo substring, presente in 2 forme (overloaded): http://ripetizioni.doomby.com - https://www.facebook.com/Prof.Ing.Bianco https://twitter.com/ProfBianco Pag. 72 Ripetizioni Materie Scientifiche String substring(int beginIndex); String substring(int beginIndex, int endIndex); La prima ritorna una stringa (sottostringa di quella di partenza) a partire dall’indice specificato fino alla fine della stringa; la seconda invece, ritorna una stringa che è anch’essa sottostringa di quella di partenza, ma che parte dall’indice beginIndex e termina in endIndex. Per esempio: String String String String titolo = "I promessi Sposi"; a = titolo.substring(2); // a vale "promessi Sposi" b = titolo.substring(12); // b vale "Sposi" c = titolo.substring(2,9); // c vale "promessi" Nota: sia l’operazione di concatenamento sia quella di estrazione di una sottostringa (e tutti i metodi che operano sulle stringhe per la verità), sono caratterizzati dal fatto di non modificare la stringa su cui vengono applicate ma di ritornarne una nuova. Ad esempio titolo.substring(12) non modifica titolo ma ritorna una nuova variabile di tipo String che contiene la sottostringa "Sposi"; Stringhe, oggetti “immutabili” Anche se cercassimo con attenzione non troveremmo come fare l’operazione di ‘estrazione’ direttamente su una stringa: in Java le stringhe sono oggetti immutabili, cioè il loro valore non può essere cambiato dopo la loro creazione (come gli array non possono cambiare lunghezza per fare un parallelo). L’immutabilità dell’oggetto String deve sempre essere tenuta presente ogni volta le si manipolano, non è infatti infrequente cadere in errori come questo: String messaggio = "Ciao XX"; messaggio.replace("XX", "Mondo"); System.out.println(messaggio); nel quale semplicemente il risultato dell’operazione di sostituzione è non utilizzato. Possiamo comunque assegnare il nuovo oggetto literal allo stesso riferimento: messaggio = messaggio("XX", "Mondo"); Ma questo significa abbandonare l’oggetto precedente. In altre parole avremo nella memoria il nuovo oggetto "Ciao Mondo" puntato dalla variabile messaggio e l’oggetto "Ciao XX" abbandonato a se stesso senza riferimenti. Per modificare il contenuto di una stringa di caratteri è consigliabile utilizzare le classi StringBuffer o StringBuilder che, al contrario di String, possono essere modificati senza lasciare oggetti inutilizzati e secondo i casi possono risultare quindi assai piu’ preformanti (e comodi). I metodi per manipolare le stringhe Oltre al replace, la classe String mette a disposizione molti altri metodi per manipolare le stringhe, esaminiamone alcuni: Metodo Descrizione http://ripetizioni.doomby.com - https://www.facebook.com/Prof.Ing.Bianco https://twitter.com/ProfBianco Pag. 73 Ripetizioni Materie Scientifiche boolean contains(CharSequence s) boolean equals(Object anObj) boolean isEmpty() String[] split(String regex) String trim() ritorna true se e solo se la stringa contiene la sequenza di caratteri specificati dal parametro s confronta la stringa con l’oggetto obj specificato ritorna true se e solo se la lunghezza della stringa è 0 suddivide la stringa intorno ad ogni occorrenza con l’espressione regex e ritorna array con tutte le sottostringhe ritorna una copia della stringa di partenza eliminando tutti gli spazi bianchi all’inizio e alla fine della stringa Enum, gestire le enumerazioni A partire dalla versione 5, in Java è stato introdotto uno speciale tipo chiamato Enum o Enumerated Type che, almeno in prima approssimazione, può essere semplicemente pensato come un modo per vincolare una variabile a poter assumere solo un determinato (dallo sviluppatore) set di valori. Se immaginiamo di scrivere un programma che gestisca un calendario probabilmente avremo bisogno di un modo per identificare i giorni della settimana; nulla vieta di definire, ad esempio, la variabile giornoDellaSettimana come int (byte basterebbe ma siamo a fare un esempio) e tenere a mente che 0 corrisponde a Lunedì, 1 a Martedì e così via. Così facendo giornoDellaSettimana potrà certamente avere tutti i valori desiderati: int giornoDellaSettimana = 4; // VEN per la nostra convenzione e con quache costante (static final) potremmo anche rendere leggibile il codice: public static final int LUN = 0; // ... public static final int VEN = 4; int giornoDellaSettimana = VEN; http://ripetizioni.doomby.com - https://www.facebook.com/Prof.Ing.Bianco https://twitter.com/ProfBianco Pag. 74 Ripetizioni Materie Scientifiche ma purtroppo, per il compilatore, giornoDellaSettimana è e resta una variabile int e quindi non potrà mai verificare che non ci sia mai nel nostro codice una linea in cui viene assegnato il valore 9 (o -4 essendo gli interi signed), facendoci perdere una delle più preziose comodità di un linguaggio staticamente tipato come Java: il type checking al momento della compilazione. D’altro canto, se invece di cercare di usare (male) un int, oppure un altro qualsiasi tipo non pensato per rappresentare un giorno della settimana, definiamo: public enum Giorno { LUNEDI, MARTEDI, MERCOLEDI, GIOVEDI, VENERDI, SABATO, DOMENICA // opzionalmente può terminare con ";" } e poi Giorno giornoDellaSettimana; avremo una variabile che potrà contenere solamente un valore appartenente al set specificato nella definizione dell’enum Giorno e contemporaneamente avremo a disposizione anche i nomi simbolici (le costanti di prima) da usare nella scrittura del programma: giornoDellaSettimana = Giorno.VENERDI; Ricordando la lezione sui costrutti condizionali, è interessante poter usare enumerazioni come quelle definite dal tipo Giorno anche con lo statement switch. /** EnumTest.java*/ public class EnumTest { public enum Giorno { LUNEDI, MARTEDI, MERCOLEDI, GIOVEDI, VENERDI, SABATO, DOMENICA }; public static void main(String[] args) { // scegliamo un valore Giorno giornoDellaSettimana = Giorno.GIOVEDI; // definiamo una logica switch(giornoDellaSettimana){ case LUNEDI: System.out.println("Oggi break; case MARTEDI: System.out.println("Oggi break; case MERCOLEDI: System.out.println("Oggi break; case GIOVEDI: System.out.println("Oggi break; è Lunedì"); è Martedì"); è Mercoledì"); è Giovedì"); http://ripetizioni.doomby.com - https://www.facebook.com/Prof.Ing.Bianco https://twitter.com/ProfBianco Pag. 75 Ripetizioni Materie Scientifiche case VENERDI: System.out.println("Oggi è Venerdì"); break; case SABATO: System.out.println("Oggi è Sabato"); break; case DOMENICA: System.out.println("Oggi è Domenica"); break; } } } Il risultato sarà: Oggi è Giovedì Le caratteristiche della classe enum Tecnicamente parlando in Java una enum è una classe come le altre ma che “implicitamente” (cioè senza che lo scriviamo noi) estende sempre la classe java.lang.Enum, cosa che ha l’unico inconveniente di rendere impossibile di scrivere enum che derivino da altri tipi. Il trattamento speciale che Java riserva agli enum riserva anche qualche interessante sorpresa: il compilatore per ogni classe enum sintetizza per noi un metodo statico (values) che ritorna un array di tutti i possibili valori che potranno assumere le variabili cha varanno come tipo l’enum, quindi nel nostro esempio il frammento di codice: for( Giorno d : Giorno.values() ) { System.err.println(d); } stamperebbe tutti i nomi dei giorni della settimana (anche i nomi vengono convertiti automaticamente in stringhe, con il medesimo case). Analogamente il compilatore ci offre la possibilità di convertire stringhe in valori del nostro enum: Giorno g = Giorno.valueOf("SABATO"); assegnerà alla variabile g il valore Giorno.SABATO (attenzione, la stinga deve avere il medesimo case e non può contenere spazi, infatti l’esecuzione (non la compilazione) di: Giorno g = Giorno.valueOf("Sabato"); genererebbe un errore di tipo “java.lang.IllegalArgumentException”. In generale infine, il fatto che le enum siano a tutti gli effetti classi, apre la possibilità di aggiungere dentro di essi metodi e field e, e questo è più sorprendente, anche costruttori. Esaminimo un pò di sintassi. Se la definizione dell’enum contiene metodi e/o field: http://ripetizioni.doomby.com - https://www.facebook.com/Prof.Ing.Bianco https://twitter.com/ProfBianco Pag. 76 Ripetizioni Materie Scientifiche la lista dei field deve essere terminata da ‘ ;‘ (che altrimenti è ammesso ma non obbligatorio). Gli eventuali costruttori devono essere privati (o package private) e non possono essere chiamati esplicitamente, sono unicamente a disposizione del compilatore. La lista dei valori ammissibili nel caso in cui siano definiti dei costruttori non deve essere considerata, come abbiamo fatto fino ad ora, come una lista di etichette ma come una forma compatta per istruire il compilatore a costruire determinate istanze della classe ed assegnare loro un nome simbolico. Non ci sono restrizioni circa i metodi e i field che possono essere inclusi nel corpo di un enum. Enumerazioni: creare oggetti specifici Oltre alla sintassi, che tutto sommato non presenta particolari aspetti interessanti, circa la versatilità delle enum vale la pena di provare ad immaginarne un contesto d’uso. Immaginiamo di dover gestire in un programa gli elementi chimici (fare riferimento ad esempio alla tavola periodica degli elementi) potremmo probabilmente costruire un enum (grosso, sono oltre 100) che li contenga tutti. Poiché il numero di elementi è finito e prefissato, un enum potrebbe essere una scelta opportuna ma ci dovremmo di sicuro preoccupare di associare ad ogni elemento alcune informazioni aggiuntive, diciamo numero atomico e massa atomica per fissare le idee, e predisporre opportuni metodi per accederli, l’enum dovrebbe quindi contenere qualcosa del tipo: private int numeroAtomico; private double massaAtomica; private String simbolo; public int getNumeroAtomico() { return numeroAtomico; } // ... altri getter/setter e un costruttore per permettere (costringere sarebbe il verbo giusto) l’inizializzazione dei field, quindi: private Elemento(String simbolo, int numeroAtomico, double massaAtomica) { this.simbolo = simbolo; this.numeroAtomico = numeroAtomico; this.massaAtomica = massaAtomica; } Se a questo punto tentassimo di definire i valori dell’enum come abbiamo fatto per i giorni public enum Elemento { IDROGENO, ELIO, // ... ; } http://ripetizioni.doomby.com - https://www.facebook.com/Prof.Ing.Bianco https://twitter.com/ProfBianco Pag. 77 Ripetizioni Materie Scientifiche Otterremmo un errore di compilazione che ci avviserebbe che il costruttore default (quello senza argomenti) della classe Elementi non esiste. Infatti il costruttore lo abbiamo definito noi e prevede che vangano specificati alcuni parametri e così siamo obbligati a costruire i nostri valori nell’enum, con il costruttore che abbiamo fornito, la definizione giusta sarà quindi: /** Elemento.java */ public enum Elemento { IDROGENO("H", 1, 1.008), ELIO("He", 2, 4.003), // ... altri elementi LITIO("Li", 3, 6.491); private int numeroAtomico; private double massaAtomica; private String simbolo; public int getNumeroAtomico() { return numeroAtomico; } public String getSimbolo() { return simbolo; } private Elemento(String simbolo, int numeroAtomico, double massaAtomica) { this.simbolo = simbolo; this.numeroAtomico = numeroAtomico; this.massaAtomica = massaAtomica; } } Copiamo questa definizione in un file Elemento.java e la utilizziamo per una prova su strada all’interno di un “main”: /** main.java */ public class main { public static void main(String[] args) { for( Elemento e : Elemento.values() ) { System.out.printf("%s\t|\t%d|\t%s\n", e.getNumeroAtomico(), e); } } } e.getSimbolo(), Lanciamo il programma e otteniamo: H He Li | | | 1| 2| 3| IDROGENO ELIO LITIO http://ripetizioni.doomby.com - https://www.facebook.com/Prof.Ing.Bianco https://twitter.com/ProfBianco Pag. 78 Ripetizioni Materie Scientifiche Le variabili di tipo Elemento per costruzione, saranno sempre elementi validi e con le proprietà che ci interessano sempre inizializzate e disponibili. Enumerazioni: sintesi delle caratteristiche Enum risolve quindi un’esigenza reale, quella di definire un insieme di valori predefiniti, senza ricorrere alla mediazione di costanti intere, con la possibilità di avere una classe. Come abbiamo visto anche le enumerazioni possono essere utilizzate nei cicli for-each (for-in) e nei costrutti switch. Il metodo toString(), di default, è uguale al nome assegnato alla variabile, ma vedremo come poterlo modificare. Ecco una breve lista delle caratteristiche delle enumerazioni che ci aiuta a comprenderne la logica per utilizzarle al meglio: Una enumerazione è una classe, in particolare l’estensione della classe java.lang.Enum, quindi come tale ha tutte le attenzioni sul controllo dei tipi in fase di compilazione. I tipi definiti in una enumerazione sono istanze di classe, non tipi interi. I valori di una enumerazione sono public final static, quindi immutabili. Il metodo == è sovrascritto, quindi può essere usato in maniera intercambiabile al metodo equals. Esiste la coppia di metodi valueOf()/toString() che possono essere sovrascritti. Enum, utilizzo avanzato Possiamo finire approfondendo alcuni aspetti avanzati che possono aiutarci nella produzione di codice. Lo facciamo con un esempio di enumerazione che rappresenti un player di tracce audio prese da un archivio. Vogliamo che il player sappia riconoscere tre formati diversi (mp3, pcm e dolby digital) e che ognuno di essi concretamente sia riprodotto in base alle proprie specifiche. Creiamo quindi il tipo enum Audio e il relativo file (Audio.java) e lo definiamo con la stessa sintassi che utilizziamo per una classe (alla fine sempre di una classe si tratta), quindi: variabili di istanza, costruttori e metodi. In particolare definiamo un metodo astratto reproduce (questa parte si capirà meglio dopo aver parlato della programmazione orientata agli oggetti). /** Audio.java*/ public enum Audio { // Lasciamo lo spazio per gli elementi dell'enumerazione private final String channel; private final int bitrate; Audio(String channel,int bitrate) { this.channel=channel; this.bitrate=bitrate; } Audio(String channel) { this.channel=channel; bitrate = -1; http://ripetizioni.doomby.com - https://www.facebook.com/Prof.Ing.Bianco https://twitter.com/ProfBianco Pag. 79 Ripetizioni Materie Scientifiche } public abstract String reproduce(String archive); // getter e setter public String getChannel() { return channel; } public int getBitrate() { return bitrate; } } Per realizzare quanto detto sopra, ogni tipo concreto creato (ogni definizione) dovrà implentare concretamente il metodo reproduce ed eventualmente potrà definire una sua propria logica nel suo corpo di classe: MP3("mp3",128) { @Override public String reproduce(String archive) { return archive+" > file MP3"; } }, PCM("PCM"){ @Override public String reproduce(String archive) { return archive+" > file PCM"; } }, DD("Dolby Digital",256){ @Override public String reproduce(String archive){ return archive+" > file Dolby Digital"; } @Override public String toString(){ //Override del metodo toString() return "Dolby"; } }; // qui seguono le definizioni della classe private final String channel; private final int bitrate; Audio(String channel,int bitrate) { // Etc... Gli elementi, così definiti, li inseriamo nella prima parte della dichiarazione dell’enum. Esaminando gli elementi vediamo che abbiamo creato ogni singolo tipo concreto sfruttando i costruttori (l’uno o l’altro, badate bene), sempre però identificando il tipo con un identificativo (MP3, PCM, DD). Ognuno dei metodi implementa concretamente il player scelto (qui simulato con http://ripetizioni.doomby.com - https://www.facebook.com/Prof.Ing.Bianco https://twitter.com/ProfBianco Pag. 80 Ripetizioni Materie Scientifiche il ritorno di una stringa particolare), inoltre l’ultimo player effettua l’override del metodo toString(). Vediamo un main di esempio e l’utilizzo dell’enumerazione appena creata: public class main { public static void main(final String[] args) { for( Audio a : Audio.values() ) { System.out.printf("%s\t| %d\t| a.getChannel(), a.reproduce("myFile")); } } } %s\t | %s\n", a, a.getBitrate(), Ecco il risultato: MP3 | 128 | mp3 | myFile > file MP3 PCM | -1 | PCM | myFile > file PCM Dolby | 256 | Dolby Digital | myFile > file Dolby Digital Qui c’è da notare che, per default, il toString() del tipo enum è l’identificativo assegnato ( MP3, PCM) mentre, laddove abbiamo effettuato l’override, ovviamente, è il valore che noi abbiamo assegnato (Dolby e non DD). Principi di OOP Nelle sezioni precedenti, esaminando alcune caratteristiche di Java, abbiamo incontrato alcuni concetti di OOP ed alcuni dei suoi elementi cardine come, ad esempio, la definizione di classe e la distinzione tra classe e istanza. Tuttavia per avere la nozione di programmazione orientata agli oggetti comprende qualcosa in più, che prescinde dal linguaggio di programmazione specifico e riguarda in generale lo schema mentale da assumere nell’affrontare la modellazione, l’analisi e l’implementazione della soluzione di un problema. «The fact that you know Java doesn’t mean that you have the ability to transform that knowledge into well-designed object oriented systems.» Developing Applications with Java and UML – Paul R. Reed Questa citazione ci ricorda che la padronanza della sintassi di un linguaggio di programmazione orientato agli oggetti, anche fosse perfetta, non basta per organizzare e strutturare sistemi e programmi che rispecchino il paradigma OOP. La programmazione orientata agli oggetti infatti ha una sua complessità che richiede sia la conoscenza di nozioni specifiche (alcune delle quali squisitamente teoriche), sia un opportuno metodo ed una disciplina. http://ripetizioni.doomby.com - https://www.facebook.com/Prof.Ing.Bianco https://twitter.com/ProfBianco Pag. 81 Ripetizioni Materie Scientifiche In questa e nelle prossime lezioni esaminiamo alcuni concetti fondamentali della OOP in Java, passando per la notazione UML e arrivando a parlare di OOD (Object Oriented Design). Design a oggetti vs Structured-design Già dalla fine degli anni ’90, quando gran parte della teoria OO è stata messa a punto, è emerso come analisi e design orientati agli oggetti fossero fondamentalmente differenti dalla tradizionale metodologia del design strutturato. Il focus sugli oggetti ha bisogno di un diverso approccio alla decomposizione dei problemi e non deve sorprendere quindi che produca architetture software molto dissimili da quelle realizzate per mezzo dello structured-design: Con l’approccio strutturale ci si concentra sulla scomposizione di algoritmi in procedure. Nell’approccio “a oggetti” ci si focalizza sull’interazione di elementi (oggetti) che comunicano (scambiano messaggi) tra loro. La OOP consente la descrizione di dinamiche complesse e la realizzazione di “procedure” più facilmente implementabili (e mantenibili) favorendo le interazioni tra oggetti sostanzialmente semplici. La complessità dei problemi non deve essere rappresentata spesso da oggetti complessi ma da interazioni complesse tra oggetti semplici. Cerchiamo di comprendere quindi cos’è la programmazione orientata agli oggetti e …cosa non è, seguendo quanto più possibile la visione di G. Booch (“OBJECT-ORIENTED ANALYSIS AND DESIGN”) che ne è stato uno dei padri, ma tenendo anche presente la seguente affermazione di T. Rentsch (“A Generalized Object Model” ACM SIGPLAN NOTICES – v17, n9): "My guess is that object-oriented programming will be in the 1980s what structured programming was in the 1970s. Everyone will be in favor of it. Every manufacturer will promote his products as supporting it. Every manager will pay lip service to it. Every programmer will practice it (differently). And no one will know just what it is". Booch commentava che la medesima affermazione era ancora vera alla fine degli anni novanta e la si applica ancora abbastanza bene. OOP, OOD & OOA In modo del tutto analogo a quanto successo per il design strutturale (sviluppatosi in modo da riuscire a costruire sistemi complessi utilizzando gli algoritmi come loro elemento), l’objectoriented design si è evoluto in modo da dare supporto agli sviluppatori che utilizzano le classi e gli oggetti come loro elemento base nella fase di sviluppo di un progetto. Per la verità il modello ad oggetti è di importanza capitale nella descrizione di numerose dinamiche anche non legate allo sviluppo del software e ha quindi subito influenze da diversi ambiti. Perciò il concetto di “modello ad oggetti” finale unifica visioni provenienti da mondi diversi: dall’informatica, alla strutturazione dei database, alla costruzione e design di interfacce fino ad arrivare all’architettura dei computer. http://ripetizioni.doomby.com - https://www.facebook.com/Prof.Ing.Bianco https://twitter.com/ProfBianco Pag. 82 Ripetizioni Materie Scientifiche OOP: Object Oriented Programming Si definisce Object Oriented Programming (o semplicemente OOP) un metodo di implementazione in cui i programmi sono organizzati attraverso un insieme di oggetti, ognuno dei quali è un’istanza di una classe, e queste classi sono tutte parte di una gerarchia di entità unite fra di loro da una relazione di ereditarietà. Nella precedente definizione ci sono tre parti particolarmente importanti: 1. OOP utilizza un insieme di oggetti (non algoritmi, e gli oggetti sono la parte fondamentale della costruzione logica); 2. ogni oggetto è istanza di una classe; 3. ogni classe è legata alle altre attraverso una relazione detta eredità. Se in un programma manca anche solo una di queste caratteristiche, non lo si può definire objectoriented. Tra i molti che hanno cercato di dare, con esiti alterni, una definizione delle caratteristiche che un linguaggo deve avere per essere esso stesso definito object oriented, è inreressante la prospettiva di Cardelli e Wegner (On Understanding Types, Data Abstraction, and Polymorphism – Computing Surveys, Vol 17 n. 4, pp 471-522) per i quali un linguaggio può essere definito come orientato agli oggetti se e solo se soddisfa le seguenti caratteristiche: 1. Supporta l’astrazione dei dati in una forma che permetta di parlare di strutture per le quali è definito un insieme di operazioni che possono ad essa essere applicate ed è al contempo in grado di garantire l’isolamento dello stato delle variabili interne alla struttura. 2. Gli oggetti hanno dei tipi (detti classi) associati che ne definiscono i possibili comportamenti. 3. I tipi possono essere messi in una relazione di interdipendenza, detta eredità che permette ad ogni tipo di poter ereditare attributi da altri tipi che in questo caso sono detti “superclassi” Questa caratteristica di un linguaggio di programmazione apparentemente fumosa risulta chiara se si pensa che significa che in un linguaggio OO deve essere possibile esprimere la frase “è un” come relazione fra le classi. Per esempio la “Gibson Les Paul” è una chitarra elettrica, che è una chitarra, che è uno strumento musicale e, nell’immaginario caso in cui si stia pensando ad un sistema di acquisti online, è un oggetto di un dato peso e con un imballo di una data dimensione. La relazione di eredità tra classi tanto importante da far concludere che se un linguaggio di programmazione consente di creare tipi (indi classi) ma non supporta il concetto di eredità, non lo si può definire object oriented. OOD: Object-Oriented Design Possiamo definire l’object-oriented design come una metodologia di progettazione che comprende il processo di decomposizione ad oggetti e una notazione per rappresentare modelli logica e fisica nonché gli aspetti statici e dinamici del sistema in fase di progettazione. C’è da osservare che la definizione di OOD prevede una decomposizione delle problematiche in oggetti (in contrapposizione al design procedurale che mira a decomporre le problematiche in http://ripetizioni.doomby.com - https://www.facebook.com/Prof.Ing.Bianco https://twitter.com/ProfBianco Pag. 83 Ripetizioni Materie Scientifiche algoritmi) e che già nella sua definizione prevede l’adozione di una notazione (astratta, cioè slegata dal particolare linguaggio di programmazione che verrà eventualmente usato alla fine per l’implementazione del design) atta a descrivere le interazioni tra gli oggetti ed a comunicare e modellare le peculiarità del sistema in esame. Questa notazione astratta è oggi universalmente riconosciuta essere il linguaggio di modellazione detto UML: Unified Modelling Language del quale cercheremo di dare rudimenti pratici nelle prossime sezioni. OOA: Object-Oriented Analysis Object-oriented analysis è l’analisi di un problema e la costruzione di modelli del mondo reale con una visione orientata agli oggetti: in altre parole è una metodologia di analisi che esamina le necessità di un problema dal punto di vista delle classi e degli oggetti. Quale relazione lega OOA, OOD e OOP? Fondamentalmente, i prodotti dell’analisi orientata agli oggetti servono come modelli da cui si possa avviare una progettazione orientata agli oggetti; i prodotti di progettazione orientata agli oggetti possono poi essere utilizzati come modelli per l’attuazione completa di un sistema utilizzando metodi di programmazione orientati agli oggetti. Anche se in teoria questo approccio che prevede OOA seguita da OOD e OOP (con successive iterazioni e revisioni) sembra una colossale (e burocratica) complicazione ed un inutile allungamento dei tempio di sviluppo, l’esperienza conferma che seguendo buone pratiche OO il software prodotto risulta migliore e soprattutto mantenibile e modificabile. Caratteristiche dello “stile” object oriented Si individuano solitamente nello stile Object Oriented quattro elementi caratterizzanti: Astrazione; Encapsulation (Incapsulamento); Gerarchia; Modularità. Ogni modello che manca di anche solo una di queste caratteristiche non può essere definito “orientato agli oggetti”. Astrazione, applicazione pratica alla progettazione L’astrazione ci consente di evidenziare le caratteristiche fondamentali di un oggetto e di classificarlo simile ad altri dello stesso tipo e distinto da tutti gli altri tipi e quindi ci consente di tracciare confini concettuali ben definiti per descriverlo all’interno di un certo contesto di osservazione che ci interessa. Stabilire il giusto insieme di elementi di astrazione per un dato oggetto è il problema centrale nella progettazione object-oriented. Cerchiamo di rendere più chiaro quanto appena detto con un esempio. http://ripetizioni.doomby.com - https://www.facebook.com/Prof.Ing.Bianco https://twitter.com/ProfBianco Pag. 84 Ripetizioni Materie Scientifiche Riprendiamo l’esempio della chitarra Gibson e consideriamo questo oggetto nel contesto della realizzazione di un sistema per il trasporto di beni. Sarebbe probabilmente una buona astrazione quella di considerare lo strumento musicale come appartenente alla categoria degli oggetti trasportabili, con associato un peso ed un volume. Anche l’appartenenza alla categoria degli oggetti fragili e di valore potrebbe essere corretta ma al fine della specifica prospettiva non avrebbe molto senso. Si può classificarlo poi come oggetto ad uso dei gruppi musicali pop oppure in quello degli strumenti a corda: entrambe le affermazioni sono vere ma non pertinenti alla astrazione consona al problema in oggetto. È importante osservare che l’astrazione va utilizzata come strumento che permette di focalizzare l’attenzione su una visione esterna di un oggetto, in modo da separare quella che è l’implementazione di un comportamento dal suo ruolo nella dinamica globale di un determinato processo. Sempre parlando della nostra chitarra nel contesto di un software per l’impacchettamento di item in immaginari mezzi di trasporto, il fatto di averla classificata come “oggetto fragile” significherà che dovrà essere possibile chiedere all’oggetto Chitarra quale sia il massimo peso che può sopportare (quando impilata in un container) che servirà per schedulare l’ordine degli oggetti quando caricati. Non conta come l’oggetto Chitarra calcolerà il massimo peso sostenibile (implementazione), quello che è importante è che l’oggetto sia in grado di darci l’informazione (interazione). Al contempo, se tra gli oggetti da trasportare si aggiungeranno (anche in un secondo momento) le “scatole per uova” non sarà necessario al sistema di conoscere come il massimo peso sostenibile sia calcolato nel caso delle uova ed in cosa questo differisca da quello per le chitarre (come avremmo forse dovuto fare in un contesto procedurale se avessimo previsto una procedura per calcolare il massimo peso sostenibile di un dato collo) ma basterà che anche le scatole per uova implementino la medesima interfaccia delle chitarre, cioè quella prevista per gli oggetti fragili. Incapsulamento Come si capisce dall’esempio dei “colli fragili” c’è un secondo concetto che scaturisce naturalmente dall’astrazione e che è rappresentato dal fatto che le implementazioni del calcolo del massimo peso sostenibile per le chitarre e le uova vengono definite in opportune sezioni “private” cioè pertinenti solamente a quegli specifici oggetti, incapsulati insomma in precise aree di codice. I concetti di astrazione e di incapsulamento sono quindi complementari: l’astrazione focalizza l’attenzione sul comportamento e le caratteristiche osservabili di un oggetto, mentre l’incapsulamento si concentra sull’implementazione che riesce a riprodurre queste caratteristiche. L’incapsulamento è molto spesso ottenuto attraverso l’aggregazione di informazioni, che è il processo di nascondere tutte le informazioni private di un oggetto che non contribuiscono a definire quelle che sono le sue caratteristiche essenziali nell’interazione con le altre entità del sistema in esame. Tipicamente la struttura di un oggetto viene mantenuta nascosta, così come l’implementazione dei suoi metodi consentendoci di creare delle vere e proprie barriere fra i diversi tipi di astrazioni che possono essere definite per un oggetto, senza creare alcun tipo di confusione e soprattutto senza dover duplicare le informazioni che e diverse astrazioni condividono. http://ripetizioni.doomby.com - https://www.facebook.com/Prof.Ing.Bianco https://twitter.com/ProfBianco Pag. 85 Ripetizioni Materie Scientifiche In definitiva si può definire l’incapsulamento come segue: l’incapsulamento è il processo di suddivisione degli elementi di un’astrazione che ne costituiscono la struttura e il comportamento; incapsulamento serve a separare l’interfaccia costruita per un certo tipo di astrazione e la sua attuazione. Britton e Parnas chiamano questi elementi incapsulati i "segreti" di un’astrazione. Gerarchia “Abstraction is a good thing, but in all except the most trivial applications, we may find many more different abstractions than we can comprehend at one time.” Nella maggior parte dei problemi che si affrontano non esiste una unica astrazione da tenere in considerazione ma spesso ne esistono molteplici e la possibilità di organizzarle in gerarchie è di vitale importanza per una modellazione efficace. L’incapsulamneto ci aiuta nella gestione di questa complessità cercando di mantenere nascosta all’utente finale la nostra astrazione e la modularità ci da il modo per gestire le relazioni logiche dell’astrazione. Tuttavia questo non sembra essere abbastanza, infatti molto spesso molte astrazioni formano insieme una gerachia e identificando queste gerarchie riusciamo a semplificare enormemente il problema. Le gerarchie che comunemente si descrivono nella programmazione sono quelle che potremmo definire strutturali (un determinato oggetto “è un” cioè “appartiene alla tal categoria”) oppure quelli compontamentali “si comporta come un”. Le chitarre nel sistema per il carico dei container è un item fragile ma magari ha senso dire che si comporta anche come un oggetto vendibile e che quindi ha associato un metodo che ne ritorna il prezzo utilizzabile per redigere il valore degli item che compongono un carico. La distinzione tra “è un” e “si comporta come un” scompare quasi completamente in linguaggi che, come il C++, supportano il concetto di ereditarietà multipla ma è invece importante da tenere a mente in Java che forza una unica appartenenza strutturale. Modularità Il significato di modularità può essere visto come l’azione di partizionare un programma (o se vogliamo un qualsiasi problema) in componenti che riescano a ridurne il grado di complessità. Risulta infatti evidente nella modellazione di sistemi complessi che la sola astrazione in entità (e gerarchie) e l’incapsulamento non bastano per renndere trattabili alcuni problemi ma è necessario ricorrere anche alla modularizzazione, cioè alla individuazione di gruppi di entità (classi) affini per funzionalità, area di utilizzo, comportamento e conseguente divisione del problema in sottoproblemi più facilmente affrontabili. Questa partizione in moduli consente anche di creare un certo numero elementi ben definiti e separati all’interno del programma stesso (eventualmente anche compilati separatamente), ma che sia possibile connettere fra di loro per orchestrare una soluzione globale al problema in esame. http://ripetizioni.doomby.com - https://www.facebook.com/Prof.Ing.Bianco https://twitter.com/ProfBianco Pag. 86 Ripetizioni Materie Scientifiche Determinare come modularizzare un determinato problema è una operazione certamente iterativa ed è caratterizzata dal medesimo livello di complessità della astrazione: quest’ ultima consiste nella dereminazione delle entità che è necessario modellare nelal soluzione del problema in oggetto mentre la modularizzazione consiste nel determinare gruppi ‘assimilabilì di oggetti da relegare in un modulo da sviluppare separatamente. Classi, oggetti e costruttori Un programma (o un sistema) realizzato secondo l’approccio Object Oriented, semplice o complesso che sia, basa il suo funzionamento sull’interazione tra oggetti: entità che possono comunicare e modificare il proprio stato. Per questo è fondamentale approfondire i concetti di classe, oggetto e costruttori, cosa che facciamo in questa lezione, anche introducendo la rappresentazione UML, ovvero il liguaggio visuale nato proprio per modellare sistemi orientati agli oggetti. Gli oggetti Nella vita reale siamo abituati a classificare e a vedere oggetti dalle caratteristiche tangibili e riconoscibili, nel mondo OO invece, il concetto di oggetto si amplia e un oggetto può contenere elementi concreti, ma anche entità come processi (pensiamo a quelli in una filiera manifatturiera) o concetti teorici e astratti (un modello 3D nella modellazione solida, o cose intangibili come folla, etc.). http://ripetizioni.doomby.com - https://www.facebook.com/Prof.Ing.Bianco https://twitter.com/ProfBianco Pag. 87 Ripetizioni Materie Scientifiche Gli oggetti di cui stiamo parlando non hanno caratteristiche fisicamente distinguibili ma sono identificati dall’avere: stato, l’insieme delle variabili interne che ne definiscono le caratteristiche in un certo istante dell’esecuzione; “comportamento“, le modalità di azione o di reazione di un oggetto, in termini di come il suo stato cambia e come i messaggi passano. Interazione tra oggetti In OOP i problemi si affrontano concentrandosi sulle interazioni, perciò la spedizione di messaggi risulta essere basilare per rappresentare l’evoluzione di un sistema. È utile quindi che sia ben compreso e descrivibile in maniera chiara attraverso gli stumenti di modellazione. L’interazione tra diversi oggetti avviene attraverso lo scambio di messaggi: un oggetto, detto sender del messaggio agisce su un altro oggetto, detto recipient, spedendogli uno dei messaggi che il ricevente è in grado di accettare. In altre parole: l’oggetto sender chiama uno dei metodi “esposti” dall’oggetto ricevente. In Java intendiamo per “oggetto” l’istanza particolare di una certa classe, e esso può possedere (o esporre) alcuni metodi. Quindi un oggetto può ricevere un certo messaggio se possiede un metodo che l’oggetto sender è in grado di chiamare (con la opportuna visibilità). In UML lo scambio di messaggi è rappresentato con un diagramma (un semplice schema) chiamato sequence simile a quello riportato in figura: che va letta come: l’oggetto Sender spedisce il messaggio corrispondente al metodo methodXYZ all’oggetto Recipient, il rettangolo vicino alla freccia (sotto Recipient) è comunemente detto activation e rappresenta l’elaborazione necessaria a Recipient per operare sul suo stato e poter reagire al messaggio ricevuto. È naturalmente possibile che, come reazione al messaggio methodXYZ (ad esempio al fine di reperire informazioni per una eventuale risposta al messaggio ricevuto) Recipient operi su altri http://ripetizioni.doomby.com - https://www.facebook.com/Prof.Ing.Bianco https://twitter.com/ProfBianco Pag. 88 Ripetizioni Materie Scientifiche oggetti generando una cascata di messaggi verso altre entità del sistema ( OtherObj nello schema d’esempio). Classi Per ogni oggetto è necessario sapere qual è il set dei messaggi che è in grado di ricevere. Non possiamo infatti inviare messaggi ad un oggetto se essi non sono ammessi nell’insieme delle azioni previste. Una classe è esattamente il “blueprint” (il prototipo) di un oggetto in cui vengono definiti tutti i messaggi che ciascuna istanza sarà in grado di ricevere. Nella rappresentazione di una classe sarà quindi importante evidenziare: l’insieme dei metodi che la classe supporta (messaggi ricevibili) l’insieme delle variabili di stato (attributi) che ne rappresentano lo stato. Grazie all’UML possiamo utilizzare il Class Diagram come notazione e disegnare una classe in modo simile allo schema che segue: Lo schema mostra sia la notazione per la rappresentazione di una classe (con riportati attributi e metodi) sia quella per rappresentare una istanza. Nel secondo caso i medodi non hanno ragione di essere riportati (sono disponibili per il fatto che aa è istanza di ClasseXYZ). Sono riportati invece i valori degli attributi che sono significativi per ogni singola istanza (mentre il loro valore non avrebbe senso, tranne che per l’inizializzazione, se associato alla definizione di classe). Aggiungere dettagli alla rappresentazione Come l’analisi di ogni problema deve essere affrontata in maniera iterativa, allo stesso modo la rappresentazione UML delle entità di un sistema va immaginato come un processo dinamico che non pretende da subito di catturare ogni dettaglio ma procede da idee generali verso quelle più specifiche e dettagliate. http://ripetizioni.doomby.com - https://www.facebook.com/Prof.Ing.Bianco https://twitter.com/ProfBianco Pag. 89 Ripetizioni Materie Scientifiche Perciò gli schemi sopra riportati vanno condiderati solo di primo livello e potranno essere dettagliati aggiungendo per ogni attributo il relativo tipo e valore iniziale: typeAttribute: data_type = initial_valueClass mentre per ogni messaggio (metodo) si dovrà procedere a dettagliare la firma (signature): Metod ( arg: type, …, arg: type ) : return_type Analogamente sia per gli attributi che per i metodi andrà specificata in una fase più avanzata di analisi la visibilità che, nella notazione UML, consiste nell’anteporre al nome dei metodi e degli attributi: Segno Tipo di visibilità public + private protected # Java’s package visibility ~ mentre si usa sottolineare (o prefissare con $) i metodi e gli attirbuti static. Un esempio La ragione di cercare di rappresentare in UML ogni aspetto delle classi (e delle loro relazioni e dinamiche, come avremo modo di vedere più avanti) deriva da un lato dal fatto che per avere una buona manutenibilità di un codice occorre che ogni sua parte sia documentata ed anche i dettagli siano quanto più possibile definiti prima che il codice venga scritto ma anche dal fatto che con gli opportuni strumenti è possibile che l’implementazione venda generata automaticamente a partire dalla descrizone formale salvando tempo di sviluppo a favore di quello di analisi e design. A titolo di esempio riprendiamo schema della classe Conto, esaminato nella lezione introduttiva sulla classi in Java ed il cui codice riportiamo anche di seguito. /** * Classe per rappresentare un Conto */ public class Conto { private double amount; private String owner; // costruttore public Conto(String owner, double initialAmount) { this.owner = owner; this.amount = initialAmount; } public void versamento(double qty) { amount += qty; } public boolean prelievo(double qty) { if(amount < qty) http://ripetizioni.doomby.com - https://www.facebook.com/Prof.Ing.Bianco https://twitter.com/ProfBianco Pag. 90 Ripetizioni Materie Scientifiche return false; amount -= qty; return true; } public double getAmount() { return amount; } public String getOwner() { return owner; } } Nel quale è mostrato anche come sia possibile in UML qualificare elementi per mezzo di quelli che sono comunemente chiamati strereotype (<< ... >>) e che servono per aggiungere significati alle entità non descritti direttamente in UML e rappresentano un naturale sistema di estenzione per l’UML stesso. Costruttori Il costruttore è una delle componenti di una classe che merita una trattazione accurata. Partiamo da queste due eminenti definizioni: “Una classe è una collezione di uno o più oggetti contenenti un insieme uniforme di attributi e servizi, insieme ad una descrizione circa come creare nuovi elementi della classe stessa” (E. Yourdan) “An object has state, behavior, and identity” (G. Booch) Il costruttore è quel metodo di una classe il cui compito è proprio quello di creare nuove istanze, oltre ad essere il punto del programma in cui un nuovo elemento (quindi una nuova identità) viene creato ed è reso disponibile per l’interazione con il resto del sistema. http://ripetizioni.doomby.com - https://www.facebook.com/Prof.Ing.Bianco https://twitter.com/ProfBianco Pag. 91 Ripetizioni Materie Scientifiche Come nell’esempio Conto sopra riportato il costruttore è spesso usato per effettuare le inizializzazioni dello stato delle nuove istanze. In Java possono esserci molteplici costruttori per una medesima classe (ognuno con parametri di diversi) e ne esiste sempre almeno uno. Se infatti per una data classe non viene specificato alcun costruttore, il compilatore ne sintetizza automaticamente uno senza argomenti, detto costruttore default. Ereditarietà in Java Tra gli elementi più importanti che caratterizzano il paradigma di sviluppo OOP c’è la Gerarchia. Tenendo a mente questo fatto non è difficile convincersi di quanto il concetto di ereditarietà sia centrale nella programmazione Object Oriented. Definizione di ereditarietà Si dice che una classe A è una sottoclasse di B (e analogamente che B è una superclasse di A) quando: A eredita da B sia il suo stato che il suo behavior (comportamento) e quindi un’istanza della classe A è utilizzabile in ogni parte del codice in cui sia possibile utilizzare una istanza della classe B. Questa ultima parte della definizione va sotto il nome di “Principio di sostituzione di Liskov” ed è un invariante di importanza capitale che va tenuto presente ogni volta che si pensa a come strutturare una gerarchia di classi. Ereditatiertà strutturale (sub-typing) In questa sezione analizziamo la più semplice forma di ereditarietà disponibile in Java (che potremmo definire strutturale o sub-typing) ma vale la pena di tenere da subito presente che la definizione di ereditarietà che abbiamo sopra dato è realizzabile in almeno 3 modi fondamentalmente dissimili: Il primo consiste nell’esprimere il fatto che un elemento della classe A “è anche” un elemento della classe B (un cerchio è anche una figura piana); il secondo consiste nel dire che gli elementi della classe A si comportano come elementi della classe B (il cerchio ha un’area ed un bounding-rectangle proprio come tutte le figure piane); il terzo, infine, sintatticamente sensibilmente più complicato, quando si arriva al principio di sostituzione, esprime la relazione di ereditarietà per incapsulamento ed è leggibile come gli http://ripetizioni.doomby.com - https://www.facebook.com/Prof.Ing.Bianco https://twitter.com/ProfBianco Pag. 92 Ripetizioni Materie Scientifiche elementi della classe A hanno un elemento della classe B che può quindi essere usato quando ce ne fosse l’esigenza. Nel seguito ci occuperemo esclusivamente della prima forma di ereditarietà (che va sotto il nome di ereditarietà strutturale ed anche sub-typing) lasciando la seconda alla discussione sulle interfacce mentre la terza verrà in qualche modo affrontata quando parleremo di OO Design e Patterns. Extends, estendere (o derivare da) una classe in Java In Java la relazione di derivazione viene resa con la keyword extends che deve essere usata nella dichiarazione della classe: class A extends B { // ... } Implica che ogni istanza della classe A sarà anche di tipo B ed avrà a disposizione tutti i metodi della classe B (potrà ricevere tutti i messaggi che può ricevere la classe B, usando la terminologia delle precedenti sezioni) e nel suo stato saranno presenti tutte le variabili che si trovano nella super classe B. Nella notazione UML si esprime la relazione di ereditarietà fra A e B mediante una freccia che va da A a B, come mostrato dalla seguente figura. Esempio di ereditarietà in Java Un esempio concreto di ereditarietà potrebbe essere il seguente, nel quale esempio intendiamo dare una bozza di modello per un generico magazzino in cui parte dei colli sono per la vendita. http://ripetizioni.doomby.com - https://www.facebook.com/Prof.Ing.Bianco https://twitter.com/ProfBianco Pag. 93 Ripetizioni Materie Scientifiche /** * Collo è la classe "base" */ public class Collo { // dati private int x_size, y_size, z_size; protected int weight; // funzione getter di Weight public int getWeight() { return weight; } // Costruttore public Collo(int w, int xs, int ys, int zs) { this.weight this.x_size this.y_size this.z_size = = = = w; xs; ys; zs; } public int getVolume() { return x_size * y_size * z_size; } } /** * ColloInVendita è la classe "derivata" */ public class ColloInVendita extends Collo { // dati (oltre quelli di Collo) private int price; // coefficienti da applicare alla vendita private static final float A0 = 1; private static final float B0 = 1.2; private static final float C0 = 1.5; public int getPrice() { return price; } // Costruttore della classe derivata public ColloInVendita(int w, int xs, int ys, int zs, int price) { // richiama il costruttore della classe base super(w, xs, ys, zs); this.price = price; } public float getDeliveryCost() { return A0*weight + B0*getVolume() + C0*price; } } http://ripetizioni.doomby.com - https://www.facebook.com/Prof.Ing.Bianco https://twitter.com/ProfBianco Pag. 94 Ripetizioni Materie Scientifiche È utile osservare come, nella dichiarazione di ColloInVendita, siano utilizzabili sia i metodi che i field (a patto che siano public o protected) della super-classe Collo, che possono essere eventualmente prefissati con la keyword super utile per eventuali disambiguazioni. Una nota speciale merita il costruttore, infatti il costruttore della classe ColloInVendita (derivata) deve essere in grado di costruire una istanza della classe Collo e quindi se per quest’ultima non è previsto un costruttore ‘default’ (senza argomenti) la classe derivata lo dovrà chiamare esplicitamente passandogli gli argomenti necessari con la sintassi super(…) che deve essere obbligatoriamente il primo statment del costruttore della classe figlia. Leggendo la dichiarazione della classe Collo, dove non compare la keyword extends, potrebbe nascere la convinzione che questa classe non derivi da nessuna “super-class”; per la verità vale la pena di osservare che in Java ogni classe deriva da almeno una classe genitrice che, se la keyword extends viene omessa, è per default la classe java.lang.Object, dalla quale ogni oggetto Java eredita, per esempio, i metodi hashCode(), getClass(), toString(). Strutturare la gerarchia delle classi Scegliere le gerarchie di oggetti in una analisi OO non è operazione semplice e difficilmente priva di ambiguità e dalla cui buona riuscita dipende l’efficacia dell’intera struttura ad oggetti che stiamo costruendo. Quando ci si accinge a modellare uno specifico problema con il paradigma ad oggetti è probabilmente importante tenere presente che non esiste una gerarchia di classi giusta in assoluto, ma conta lo specifico problema che si intende risolvere. A tal proposito è interessante riflettere sul classico paradosso della modellazione di cerchi ed ellissi riassunto dalla domanda “is a circle a kind of ellipse?” da cui prende il titolo anche una sezione della C++ faq. Domandandosi infatti se nel contesto della realizzazione di una gerarchia di classi per la modellazione di figure piane sia opportuno ottenere la classe cerchio come derivata della classe ellisse ci si trova a dover concludere che benchè matematicamente non ci possano essere dubbi che la relazione è ben definita (un cerchio è di sicuro una ellisse con assi della medesima lunghezza), dal punto di vista OO se la classe ellisse fornisce un metodo per cambiare asimmetricamente le proprie dimensioni (ad esempio setSize(size_x, size_y)) la relazione di sub-typing non può essere utilizzata in quanto ogni eventuale utilizzo di questo metodo richiederebbe che la classe cerchio si trasformasse nella classe ellisse; cosa che, almeno in Java, non è possibile. http://ripetizioni.doomby.com - https://www.facebook.com/Prof.Ing.Bianco https://twitter.com/ProfBianco Pag. 95