Metodologie di Programmazione: Lezione 1
Il principale scopo del corso è introdurre principi e tecniche per aiutare la progettazione del software, l'organizzazione e la leggibilità del codice, per facilitare l'analisi, il testing e l'estensibilità e modificabilità dei programmi. Come per qualsiasi materia che si occupa di costruire artefatti1, sarebbe vano tentare di insegnare principi e tecniche in astratto. Piuttosto questi vanno introdotti nel concreto dimostrandone l'utilità tramite esempi significativi. Questo ci porta all'altro obiettivo del corso, l'apprendimento dello strumento fondamentale per la costruzione di un programma ovvero un linguaggio di programmazione. Tra i tanti linguaggi di programmazione è stato scelto il linguaggio Java perché è uno dei linguaggi più usati, più collaudati e con un solido supporto pur essendo un linguaggio non proprietario. La scelta di un linguaggio e paradigma di programmazione non è completamente neutra. Un linguaggio orientato agli oggetti favorisce strutture e organizzazioni del codice che possono essere molto diverse da quelle favorite da un linguaggio funzionale. Inoltre molti programmi possono essere meglio congegnati se strutturati tramite un determinato paradigma piuttosto che con un altro. Questa è, probabilmente, una delle principali ragioni dietro la tendenza, in atto ormai da parecchi anni, che spinge i linguaggi di programmazione a incorporare in una certa misura tutti i principali paradigmi: procedurale, orientato agli oggetti e funzionale. E Java, sopratutto grazie alla recente versione 8, è in linea con questa tendenza. Così un linguaggio di programmazione multi-paradigma può alleviare il problema di strutture e organizzazioni del codice idiosincratiche e forzate da un particolare linguaggio che non sempre sono le migliori. Il problema potrebbe ancora persistere per la piattaforma del linguaggio, cioè l'insieme delle librerie standard. Ma anche queste si stanno evolvendo, almeno nel caso di Java, verso l'adozione di più paradigmi.
La prima parte del corso sarà quasi esclusivamente dedicata alle basi del linguaggio Java e alle parti fondamentali della sua piattaforma. Una breve introduzione al linguaggio Java con cenni storici è java e una guida all'installazione è installa. Successivamente saranno discussi principi e tecniche per una buona organizzazione del codice.
Il corso presuppone una buona conoscenza di almeno un linguaggio di programmazione (Python, C, C++, ecc.) per cui, specialmente in questa prima lezione, i concetti di base di Java che sono comuni a molto altri linguaggi come variabili, funzioni o metodi, operatori, strutture di controllo, sono trattati in modo sintetico e veloce. D'altronde presentare ogni aspetto del linguaggio Java fino ad un adeguato livello di approfondimento, porta inevitabilmente a posticipare molti argomenti importanti e a frammentare e ritardare una visione d'assieme del linguaggio. E questo è tanto più vero per un linguaggio ricco come Java. Per questa ragione, dapprima faremo un tour delle principali caratteristiche del linguaggio e poi ritorneremo su quelle che necessitano di un approfondimento.
Java è un linguaggio orientato agli oggetti e tutti i suoi valori, con alcune eccezioni, sono oggetti. Ogni oggetto è istanza di una classe, per questo Java è anche un linguaggio basato sulle classi (class based language). Ogni programma Java risiede interamente in classi e in interfacce. Ad esempio non è possibile definire funzioni o variabili al di fuori di classi e interfacce come in Python o in C. Proprio per questo le funzioni in Java si chiamano metodi. I metodi sono definiti in classi (e più raramente in interfacce). Tutto il codice eseguibile di un programma Java, eccetto quello di inizializzazione, è contenuto in metodi. Generalmente un metodo è relativo agli oggetti che sono istanze della classe in cui è definito, cioè può essere chiamato, o meglio invocato, solamente su tali oggetti. Questi metodi sono appunto chiamati metodi dell'istanza (instance methods). Però si possono anche definire metodi relativi alla classe, questi sono chiamati metodi statici2 e sono marcati con la parola chiave static
. I metodi statici sono invocati relativamente alla classe in cui sono definiti. Il nostro primo programma Java ha un solo metodo ed è statico:
public class First {
public static void main(String[] args) {
System.out.println("Hello Java!");
}
}
Se eseguito stampa a video la stringa "Hello Java!"
. Le parole chiave di Java sono evidenziate. La parola chiave class
inizia la definizione di una classe. Questa e poi seguita dal nome della classe, in questo caso First
. Poi tra parentesi graffe {...}
è definito il corpo della classe. In questo caso, c'è un solo metodo ed è un metodo speciale. L'esecuzione di un programma Java inizia sempre eseguendo il metodo speciale main
di una classe. Come qualsiasi altro metodo è definito dichiarando una intestazione (method header) e un corpo (method body) racchiuso tra parentesi graffe. L'intestazione è composta da eventuali modificatori (modifiers), in questo caso public
e static
, il tipo del valore ritornato (void
, significa che non ritorna nulla), il nome del metodo (main
) e la lista dei parametri (String[] args)
. Il metodo main
deve sempre avere l'intestazione che abbiamo visto. Vedremo in seguito il significato dei modificatori e dei parametri.
Il corpo del main
, in questo caso, contiene solamente l'invocazione del metodo println()
dell'oggetto out
che a sua volta è un campo della classe System
. La classe System
è una delle tantissime classi predefinite della piattaforma Java. Per adesso basti dire che l'effetto di System.out.println("Hello Java!")
è simile a print "Hello Java!"
di python. Si noti che in Java si usa lo stesso operatore di selezione ".
" di Python e C++ per accedere ai campi e ai metodi di una classe o di un oggetto.
In Java è richiesto che il file in cui è scritta una classe abbia lo stesso nome della classe. Se il nome della classe è NomeClasse
allora il file deve chiamarsi NomeClasse.java
. Così nel nostro caso il file che contiene la definizione della classe First
deve chiamarsi First.java
. Attenzione a rispettare le maiuscole e minuscole perché Java è un linguaggio sensibile a questa differenza in tutti i contesti: parole chiave, nomi di variabili, classi, metodi, file, ecc. Tutti i file che contengono codice sorgente in Java devono avere l'estensione .java
e il loro nome deve essere uguale al nome della classe (o interfaccia) definita nel file. Più precisamente, in un file .java
può essere definita una sola classe (o interfaccia) pubblica (identificata appunto dal modificatore public
), però può contenere anche la definizione di altre classi (e interfacce) non pubbliche.
Per eseguire il nostro programma dobbiamo prima di tutto compilarlo nel linguaggio dei byte codes. Il JDK ci mette a disposizione il compilatore javac
che possiamo usare dalla linea di comando scrivendo:
javac First.java
Ovviamente dobbiamo essere posizionati nella directory che contiene il file First.java
. Il risultato della compilazione è scritto nel file First.class
. L'estensione .class
è riservata a file che contengono byte codes. Adesso possiamo eseguirlo con il comando:
java First
Il comando java
si aspetta che l'argomento fornito sia il nome di un file .class
, ma senza specificare l'estensione. Il risultato dell'esecuzione del nostro semplice programma sarà la stampa a video di
Hello Java!
Il comando java
ha sottomesso il file First.class
alla JVM che ha eseguito i byte codes in esso contenuti. Per programmi che contengono più classi e package la compilazione e l'esecuzione richiedono una forma un po' più complicata dei precedenti comandi.
Una breve guida per iniziare ad usare un IDE è intellij.
Un programma Java risiede interamente in classi (e in interfacce, come vedremo più avanti). Generalmente, una classe è usata per definire un tipo di oggetti. Ad esempio, una classe Image
potrebbe definire oggetti che rappresentano immagini intese come matrici di pixels. La classe Image
avrà uno o più metodi che costruiscono gli oggetti di tipo Image
, detti anche istanze della classe Image
, tali metodi sono speciali e sono detti costruttori (constructors). Avrà anche delle variabili che mantengono i dati dell'immagine, queste variabili che sono associate con ogni oggetto della classe sono dette campi (fields) o più precisamente campi dell'istanza o campi dell'oggetto (instance fields). Inoltre, avrà anche dei metodi per effettuare varie operazioni sull'immagine di un oggetto, questi possono essere invocati su ogni istanza della classe e sono detti metodi dell'istanza o metodi dell'oggetto (instance methods).
Java però permette di definire in una classe anche campi e metodi che non sono relativi alle istanze della classe. Tali campi e metodi sono detti campi della classe e metodi della classe (class fields e class methods) o campi statici e metodi statici. Abbiamo già visto un esempio di metodo statico, il metodo main
del primo programma. I campi e metodi statici non appartengono alle singole istanze della classe ma sono relativi alla classe stessa e per essere usati non necessitano di alcun oggetto. Per certi versi i campi e metodi statici non appartengono al paradigma OOP (Object Oriented Programming) ma sembrano essere piuttosto un residuo di programmazione procedurale. Tuttavia nel linguaggio Java sono un utile complemento al mondo degli oggetti e coadiuvano OOP.
Una classe pubblica può essere definita con la seguente sintassi che, seppur semplificata, copre la maggioranza dei casi3
public class NomeClasse { // corpo della classe
definizione-di-costruttori
definizione-di-campi (statici e non)
definizione-di-metodi (statici e non)
}
La parola chiave public
è un modificatore d'accesso (o visibilità) e marca la classe come accessibile senza restrizioni. Se non è presente alcun modificatore, la visibilità della classe è ristretta (vedremo poi in che modo). Dopo la parola chiave class
c'è il nome della classe e poi tra parentesi graffe le definizioni di eventuali costruttori e di membri della classe come campi e metodi. Il nome di una classe è una sequenza di lettere e cifre che inizia con una lettera. In Java le lettere sono i caratteri 'A'
-'Z'
, 'a'
-'z'
, '_'
, '$'
, e qualsiasi carattere Unicode che denota una lettera in una qualche lingua. Analogamente le cifre sono i caratteri '0'
-'9'
e qualsiasi carattere Unicode che denota una cifra in una qualche lingua. Non si possono usare come nomi le parole riservate del linguaggio Java. Inoltre, anche se il carattere '$'
è permesso non dovrebbe essere usato perché è inteso per nomi generati dal compilatore Java e altri strumenti. Queste regole valgono in generale per qualsiasi nome, interfacce, variabili, campi e metodi. Per convenzione i nomi delle classi (e delle interfacce) dovrebbero iniziare con una maiuscola. Questa convenzione è rispettata da tutte le definizioni della piattaforma Java.
Per introdurre e prendere dimestichezza con le caratteristiche di base di Java, come tipi, variabili, operatori, controllo del flusso, I/O, ecc., useremo solamente campi e metodi statici. Poi vedremo come le classi permettono di definire nuovi tipi di oggetti e entreremo nel vivo di OOP.
Java ha tre modi di scrivere commenti. I commenti che iniziano con //
durano fino alla fine della linea, ad esempio
System.out.println("Hello Java!"); // Stampa a video
Commenti che possono prendere più di una linea sono racchiusi tra /*
e */
, ad esempio
System.out.println("Hello Java!"); /* Invoca il metodo println
del campo statico out
della classe System */
Il terzo tipo di commenti possono essere usati per generare la documentazione in modo automatico. Questi iniziano con /**
e terminano con */
. Generalmente sono usati solamente per documentare le definizioni di classi, interfacce, metodi e campi, ad esempio
/** Metodo speciale invocato per iniziare l'esecuzione di un programma Java.
* In questo caso stampa a video "Hello Java!".
* @param args gli argomenti letti dalla linea di comando */
public static void main(String[] args) {
System.out.println("Hello Java!");
}
Tutti i valori in Java sono oggetti eccetto i valori primitivi. Quest'ultimi appartengono a 8 tipi primitivi che sono direttamente supportati dal linguaggio:
Tipo | Valori |
---|---|
byte |
interi 8 bits: da -128 a 127 |
short |
interi 16 bits: da -32768 a 32767 |
int |
interi 32 bits: da -2147483648 a 2147483647 |
long |
interi 64 bits: da -9223372036854775808 a 9223372036854775807 |
float |
numeri in virgola mobile 32 bits (IEEE 754) |
double |
numeri in virgola mobile 64 bits (IEEE 754) |
boolean |
true o false |
char |
caratteri 16-bits Unicode UTF-16 |
I nomi dei tipi primitivi sono parole chiave di Java. I valori dei tipi primitivi non sono oggetti e la loro presenza si deve solamente a ragioni di efficienza4. C'è anche un altro tipo void
che è speciale per due motivi: non ha valori e può (e deve) essere usato solamente per indicare che un metodo non ritorna alcun valore. Tutti e nove questi tipi sono anche contraddistinti dall'avere un nome che inizia con una minuscola. Tutti gli altri tipi dovrebbero iniziare con una lettera maiuscola.
Il tipo char
è in grado di rappresentare oltre ai tradizionali caratteri ASCII anche l'insieme molto più vasto dei caratteri Unicode. Le costanti carattere sono racchiuse tra apici singoli. Ad esempio 'A'
, 'a'
, '0'
, 'w'
, '@'
rappresentano i corrispondenti caratteri. Possono anche essere usate le Unicode escape sequences che sono sequenze del tipo \uxxxx
dove xxxx
è un intero a 16 bits scritto in esadecimale. Ad esempio, '\u0041'
è equivalente ad 'A'
, '\u03C0'
è il carattere pi greco minuscolo π e '\u263A'
è il carattere ☺ cioè l'Unicode code point U+263A White Smiling Face
. Per informazioni complete sui codici Unicode si può consultare il sito unicode. Oltre alle Unicode escape sequences che permettono di definire tutti i caratteri rappresentabili, è possibile usare anche le sequenze di escape: \b
(backspace), \t
(tab), \n
(line feed), \f
(form feed), \r
(carriage return), \"
(double quote), \'
(single quote), e \\
(backslash).
Un qualsiasi tipo che non è né primitivo né void
è detto tipo riferimento (reference type). Quindi ogni tipo che ha come valori oggetti è un tipo riferimento. Questo potrà sembrare strano, perché non si chiama tipo oggetto? La ragione è che una qualsiasi variabile che in apparenza contiene un oggetto in realtà contiene un riferimento a quell'oggetto. Però nella maggior parte dei casi la distinzione tra oggetti e riferimenti ad oggetti non fa differenza e i due termini si possono usare indifferentemente.
Ognuno degli 8 tipi primitivi ha un tipo riferimento corrispondente secondo la tabella:
Tipo primitivo | Tipo riferimento |
---|---|
byte |
Byte |
short |
Short |
int |
Integer |
long |
Long |
float |
Float |
double |
Double |
boolean |
Boolean |
char |
Character |
Ad esempio, i valori del tipo riferimento Integer
sono oggetti che rappresentano interi nello stesso intervallo dei valori di int
. il compilatore Java esegue conversioni automatiche dai tipi primitivi ai corrispondenti tipi riferimento e viceversa, ovunque ciò risulti appropriato. La conversione di un tipo primitivo al corrispondente tipo riferimento è chiamata boxing, mentre quella inversa, dal tipo riferimento al tipo primitivo, è chiamata unboxing. Anche il tipo void
ha un corrispondente tipo riferimento Void
.
Oltre ai tipi primitivi Java fornisce un supporto speciale per le stringhe. Le stringhe sono rappresentate dalla classe predefinita String
e concettualmente sono sequenze di caratteri Unicode. Una costante stringa, come in molti linguaggi, è una sequenza di caratteri tra doppi apici in cui possono essere anche usate sequenze di escape. Ad esempio, "Java\u2122"
è la stringa Java™ (dove ™ è il singolo carattere che significa trademark).
In Java ogni variabile ha un tipo, compresi i parametri di un metodo come vedremo presto. Prima di poter usare una variabile, sia essa un campo dell'oggetto, un campo statico o una variabile locale, deve essere dichiarata anteponendo il nome del tipo al nome della variabile
int valoreInt; // Variabile di tipo int
float val; // Variabile di tipo float
Integer valO; // Variabile di tipo Integer
Si noti che ;
deve sempre terminare ogni enunciato (statement). Per convenzione i nomi delle variabili non dovrebbero iniziare con una maiuscola per meglio distinguerle dai nomi dei tipi a meno che non siano delle costanti.
Come in tanti altri linguaggi, per l'assegnamento si usa il segno di uguale =
valoreInt = 1000;
La dichiarazione di una variabile può essere marcata da uno o più modificatori (modifiers). I modificatori ammessi sono diversi per variabili locali e campi così come l'inizializzazione.
Una variabile locale è una variabile dichiarata all'interno del corpo di un metodo e che quindi non è visibile all'esterno di esso. Non è automaticamente inizializzata e se si tenta di usarla prima di assegnargli un valore, si produce un errore
String hello;
Sytem.out.println(hello); // ERRORE! la variabile hello non è inizializzata
Un assegnamento di valore ad una variabile può essere fatto nella dichiarazione stessa
String hello = "Hello Java!";
oppure dopo
String hello;
hello = "Hello Java!";
Le dichiarazioni di variabili possono essere poste ovunque ed anzi è considerato buono stile dichiararle il più vicino possibile al punto in cui sono usate per la prima volta. Però una variabile locale è visibile solamente nel blocco più interno che ne contiene la dichiarazione. Per blocco si intende una sequenza di istruzioni comprese tra una coppia di parentesi graffe. Inoltre, Java non permette di definire variabili con lo stesso nome in blocchi che si sovrappongono.
Una variabile locale può essere marcata solamente col modificatore final
. Se marcata final
, significa che il valore della variabile può essere assegnato una sola volta, cioè non può più essere modificato. Ad esempio
final double CM_PER_INCH = 2.54;
Un campo (statico o non) è dichiarato all'esterno del corpo di ogni metodo (ma sempre nel corpo di una classe). A differenza delle variabili locali, i campi sono automaticamente inizializzati con valori di default:
Tipo | Valore di default |
---|---|
boolean |
false |
char |
'\u0000' |
byte |
0 |
short |
0 |
int |
0 |
long |
0 |
float |
0.0f |
double |
0.0d |
tipo riferimento | null |
Il valore null
significa assenza di riferimento o riferimento indefinito. Si noti che il valore null
è speciale anche perché è compatibile con qualsiasi tipo riferimento.
I principali modificatori che possono marcare un campo sono public
, private
, static
e final
5. Il modificatore final
ha lo stesso significato delle variabili locali. static
marca un campo come statico, cioè campo della classe, se non ha tale modificatore è un campo dell'oggetto. I modificatori public
, e private
riguardano il livello di accessibilità di un campo. Ad esempio, public
significa che il campo è accessibile da ovunque sia visibile la classe in cui è dichiarato. Ritorneremo su questi e gli altri modificatori in seguito.
Per accedere a un campo statico o si usa l'operatore .
applicato all'oggetto o al nome della classe (per campi statici). Ad esempio System.out
è il campo statico out
della classe System
.
Gli operatori aritmetici +
,-
,*
,/
,%
sono ripresi da C/C++ e sono comuni a molti altri linguaggi come Python e JavaScript, incluse le forme con assegnamento +=
,-=
,*=
,/=
,%=
. Java eredita anche da C/C++ gli operatori ++
,--
di incremento e decremento. Ad esempio, il seguente frammento di programma Java calcola gli interessi maturati in un investimento di 1000 euro per 5 anni al tasso annuo del 4%:
int cap = 1000; // Capitale iniziale
double tasso = 1.04, tassoC5;
tassoC5 = tasso*tasso; // Il tasso composto per 5 anni è il tasso annuale
tassoC5 *= tassoC5*tasso; // elevato alla quinta potenza
double capFin = cap*tassoC5; // Capitale finale
double interessi = capFin - cap; // Interessi maturati dopo 5 anni
Java non ha operator overloading, a differenza di C++ e Python. Così i suddetti operatori possono essere usati solamente coi tipi primitivi numerici (byte
, short
, int
, long
, float
, double
, char
) e, grazie al unboxing, anche coi corrispondenti tipi riferimento (Byte
, Short
, Integer
, Float
, Double
, Character
). Fa eccezione il +
che, grazie allo speciale supporto per il tipo String
, può anche essere usato per concatenare stringhe
String ris = "Capitale Iniziale: " + cap + " Tasso Annuale: "+tasso+"\n" +
"Interessi: " + interessi;
System.out.println(ris);
Come si vede l'operatore +
può essere applicato a un oggetto String
e a un valore di tipo qualsiasi. Se il secondo operando non è una stringa, è convertito in una stringa, vedremo poi come ogni oggetto può essere convertito in una stringa. Il frammento di programma stampa
Capitale Iniziale: 1000 Tasso Annuale: 1.04
Interessi: 216.65290240000013
Gli oggetti di tipo String
sono immutabili, per cui l'operatore +
crea come risultato dell'espressione sempre un nuovo oggetto String
uguale alla concatenazione degli operandi.
Gli altri operatori, relazionali, booleani ecc., li vedremo più avanti.
Il codice eseguibile di un programma Java risiede principalmente in metodi che corrispondono alle funzioni e procedure di altri linguaggi. I metodi sono definiti nel corpo di una classe con la seguente sintassi, un po' semplificata rispetto a quella generale ma copre la maggioranza dei casi,
modificatori tipo-ritornato nomeMetodo(parametri) {
corpo-del-metodo
}
I principali modificatori sono public
, private
e static
6. public
, e private
riguardano il livello di accessibilità di un metodo esattamente come gli omonimi modificatori per i campi. static
marca un metodo come statico, cioè metodo della classe, se non ha tale modificatore è un metodo dell'oggetto. Il tipo-ritornato
è il tipo del valore ritornato dal metodo, se non ritorna alcun valore deve essere void
. Per convenzione il nome di un metodo non dovrebbe iniziare con una maiuscola. Dopo il nome, tra parentesi tonde, sono elencati gli eventuali parametri
separati da virgole, ogni parametro è dichiarato in modo simile ad una variabile locale, è visibile nell'intero corpo del metodo ma l'inizializzazione è data dal valore passato nella invocazione del metodo. Infine, tra parentesi graffe c'è il corpo del metodo. Ecco un semplice esempio
public class First {
public static double square(double x) {
return x*x; // Ritorna il quadrato di x
}
}
La parola chiave return
è ripresa da C/C++ ed ha più o meno lo stesso significato che ha in Python, cioè termina l'esecuzione del metodo e ritorna il valore dell'espressione. L'esecuzione di ogni metodo il cui tipo ritornato non è void
deve sempre terminare con un opportuno return
, a meno che non si verifichi un errore o eccezione.
In Java c'è l'overloading dei metodi. Quindi è possibile definire nella stessa classe più metodi con lo stesso nome purché abbiano una differente lista dei tipi dei parametri.
Il modo con cui si invoca (o si chiama) un metodo dipende se è statico o meno e dal contesto. Se è un metodo dell'oggetto, si usa l'operatore .
sull'oggetto obj
su cui si vuole invocare il metodo: obj.method(...)
. Ad esempio, System.out.println()
invoca il metodo dell'oggetto println
relativamente all'oggetto il cui riferimento è nel campo statico out
della classe System
. Se il metodo è invocato all'interno della stessa classe dell'oggetto sul quale lo si vuole invocare, allora non è necessario specificare l'oggetto: method(...)
. Se è un metodo statico, l'operatore .
va usato sul nome della classe Cls
del metodo: Cls.method(...)
. Ad esempio, il metodo sopra potrebbe essere invocato così First.square(12.5)
. Se il metodo è invocato all'interno della stessa classe in cui è definito, non è necessario specificare il nome della classe: method(...)
. In ogni caso i valori specificati come argomenti nell'invocazione del metodo sono assegnati ai rispettivi parametri all'inizio dell'esecuzione del metodo.
Un programma Java consiste nella definizione di uno o più tipi, cioè classi e interfacce. La piattaforma Java fornisce migliaia di tipi predefiniti (la maggior parte classi). Per organizzare sistemi così vasti e complessi è necessario un meccanismo per raggruppare tipi che sono tra loro legati e per evitare collisioni tra nomi di tipi. Per questo Java permette di raggruppare collezioni di tipi tramite pacchetti (packages). Ogni package è identificato da un nome. Per dichiarare che un tipo appartiene ad un package occorre iniziare il file che contiene la definizione del tipo con una direttiva come quella qui riportata:
package nomePackage;
Se nel file non c'è una direttiva che specifica un package, allora i tipi definiti nel file appartengono ad un unico package di default senza nome. Ogni tipo ha un nome semplice che è il nome che gli è stato assegnato nella sua definizione e un nome completo che include il nome del package a cui appartiene. Ad esempio, la classe della piattaforma Java che rappresenta le stringhe ha nome String
e siccome appartiene al package java.lang
il suo nome completo è java.lang.String
. La classe il cui nome è Scanner
appartiene al package java.util
, così il suo nome completo è java.util.Scanner
. I nomi dei package sono gerarchici con le componenti separate dal carattere .
. Così il package java.util
può essere interpretato come il sub-package util
del package java
. Però questa visione gerarchica dei nomi dei package è solamente un'organizzazione utile per i programmatori ma per il compilatore Java non c'è nessuna relazione tra package nidificati. Ad esempio per il compilatore non c'è nessuna relazione tra java.util
e java.util.jar
. Tuttavia sia il compilatore (comando javac
) che la JVM (comando java
) si aspettano che i sorgenti e i file compilati si trovino in una gerarchia di subdirectory che ricalcano la gerarchia dei package. Ad esempio, se il file sorgente First.java
appartiene al package mp.lezione1
deve essere in una directory lezione1
la quale deve essere in una directory mp
, cioè il percorso relativo del file deve essere mp/lezione1/First.java
. Non a caso gli IDE aiutano a dislocare i sorgenti e i file compilati in una gerarchia di subdirectory che rispettano queste regole.
Siccome i nomi completi possono essere molto lunghi, spesso si usa la direttiva import
che permette di usare il nome semplice di un tipo al posto del nome completo. Ad esempio, se si volesse usare la classe Scanner
(che vedremo fra poco) senza fare l'import dovremmo usare il nome completo, cioè java.util.Scanner
. Se invece usiamo l'import
import java.util.Scanner; // Importa solamente il nome Scanner da java.util
è sufficiente il nome semplice Scanner
. Se si vogliono importare tutti i tipi del package java.util
si può usare la sintassi
import java.util.*; // Importa tutti i nomi di tipi dal package java.util
Il package java.lang
è trattato in modo speciale dal compilatore che lo importa automaticamente, cioè è come se ci fosse un implicito import java.lang.*;
. La direttiva import
da sola non può importare i campi e i metodi statici ma questo a volte sarebbe utile. Ad esempio, invece di scrivere System.out.println
si vorrebbe scrivere più brevemente out.println
. Per fare ciò c'è la direttiva di importazione statica import static
import static java.lang.System.out; // Importa il campo statico out da System
Il sorgente di un programma Java è scritto in uno o più file e ogni file deve rispettare la seguente struttura ordinata:
- Al più una direttiva con il nome del package a cui i tipi definiti
in questo file appartengono
- Eventuali direttive di import
- Una o più definizioni di tipi (classi o interfacce), di cui al più
una può essere public
Le librerie della piattaforma Java forniscono gli strumenti per programmare interfacce utente grafiche, GUI (Graphical User Interface), di tutti i generi da quelle più semplici a quelle più ricche e sofisticate. Però l'uso di tali strumenti richiede una conoscenza più avanzata del linguaggio Java, perciò le vedremo più avanti Per il momento, dovremmo accontentarci dell'input/output forniti dalla cara e vecchia console. Per l'output abbiamo già incontrato System.out.println()
che permette di stampare sullo "standard output stream" (cioè, la finestra della console). Per l'input, cioè, la lettura dallo "standard input stream", la situazione non è così semplice. L'analogo per l'input di System.out
è System.in
ma quest'ultimo oggetto (che per la cronaca è di tipo InputStream
) permette di leggere dallo standard input solamente al livello dei bytes. Si può quindi intuire che se usassimo direttamente System.in
per leggere, ad esempio, un numero o una stringa dovremmo fare parecchio lavoro per tradurre il flusso di bytes nel corrispondente dato (numero o stringa). La piattaforma Java fornisce una classe che fa proprio questa traduzione e si chiama Scanner
, è nel package java.util
e per usarla è sufficiente creare un oggetto di tipo Scanner
che è "attaccato" al flusso di input:
Scanner in = new Scanner(System.in);
Dell'operatore new
e di come si costruisce un oggetto ne discuteremo in seguito. Per ora basti dire che questa istruzione crea un oggetto di tipo Scanner
basato su System.in
e pone il riferimento a tale oggetto nella variabile in
. Gli oggetti di tipo Scanner
hanno vari metodi che permettono di leggere il flusso di input come numeri, parole, linee, ecc. Ad esempio, il metodo nextLine
,
String linea = in.nextLine();
legge la prossima linea dal flusso di input (cioè la sequenza di caratteri fino al prossimo separatore di linea) e la pone in un oggetto stringa. Analogamente il metodo next()
legge il prossimo token (sequenza di caratteri delimitata da whitespaces) e i metodi nextInt()
e nextDouble()
leggono, rispettivamente, il prossimo intero e il prossimo numero in virgola mobile (se presente).
Le istruzioni di Java per il controllo del flusso in un programma sono quasi tutte riprese da C/C++. Quindi Java dispone delle istruzioni if-else
, for
, while
, do-while
e switch-case
. Per illustrarle insieme ad altri operatori scriveremo metodi statici, alcuni dei quali potranno essere utili altrove quindi saranno tutti membri della seguente classe
package mp;
import java.util.Scanner; // Importa la classe Scanner
import static java.lang.System.out; // Importa il campo statico out di System
/** Alcuni metodi e costanti di utilità */
public class Utils {
}
Il codice dei metodi e dei campi che scriveremo nel seguito di questa sezione sarà da intendersi incluso nel corpo della classe Utils
.
if-else
La sintassi delle istruzioni condizionali è quella del C/C++ ed è anche quella di altri linguaggi come JavaScript ma è un po' diversa da quella di Python. La versione più semplice è la seguente:
if (boolExpr) {
una o più istruzioni eseguite solo se boolExpr è true
}
Le parentesi graffe sono necessarie solo se le istruzioni da eseguire sono più di una. Lo stesso vale anche se c'è else
if (boolExpr) {
una o più istruzioni eseguite solo se boolExpr è true
} else {
una o più istruzioni eseguite solo se boolExpr è false
}
Gli if-else
possono essere concatenati per controllare più condizioni a catena
if (cond1) {
blocco eseguito solo se cond1 è true
} else if (cond2) {
blocco eseguito solo se cond1 è false e cond2 è true
} else if (cond3) {
blocco eseguito solo se cond1, cond2 sono false e cond3 è true
}
...
} else {
blocco eseguito solo se tutte le condizioni sono false
}
Java riprende da C/C++ anche l'operatore condizionale ( ? : )
con la sintassi
(cond ? exprTrue : exprFalse)
Se la condizione cond
è true
, il valore è quello dell'espressione exprTrue
, altrimenti è quello dell'espressione exprFalse
.
Gli operatori relazionali ==
, !=
, <
, >
, <=
, >=
sono ripresi da C/C++ e sono in comune anche con altri linguaggi come JavaScript e Python ma con delle differenze importanti, come vedremo. Gli operatori possono essere usati solamente coi 7 valori primitivi numerici e le loro controparti oggetto. Fanno eccezione gli operatori di uguaglianza ==
e disuguaglianza !=
che possono essere usati anche con qualsiasi tipo riferimento. Però testano solamente l'uguaglianza dei riferimenti, cioè obj1 == obj2
è true
solo se obj1
e obj2
hanno lo stesso riferimento. Quindi non possono essere usati per determinare se due oggetti hanno lo stesso contenuto o valore, neanche se gli oggetti sono immutabili come String
o Integer
. Ad esempio, il seguente frammento di codice
String s1 = "una stringa";
String s2 = "una ";
s2 += "stringa";
out.println("\"" + s1 + "\" == \"" + s2 + "\" " + (s1 == s2));
molto probabilmente stamperà
"una stringa" == "una stringa" false
Per testare se due oggetti hanno lo stesso contenuto o valore, si deve usare generalmente il metodo equals
che come vedremo meglio più avanti è comune a tutti gli oggetti. In generale, obj1.equals(obj2)
ritorna true
se e solo se obj1
ha lo stesso valore di obj2
. Per le due stringhe del frammento, s1.equals(s2)
ritorna true
. Per queste ragioni per i tipi riferimento l'operatore di uguaglianza ==
e la sua negazione !=
sono quasi esclusivamente usati per testare se un riferimento è null
.
Vediamo un esempio di metodo che usa if-else
, l'operatore condizionale e alcuni operatori relazionali (ricordiamo che le definizioni sono intese essere membri nella classe Utils
)
public static final long KILOBYTE = 1024;
public static final long MEGABYTE = 1024*KILOBYTE;
public static final long GIGABYTE = 1024*MEGABYTE;
/** Ritorna una stringa che descrive il numero di bytes nb in termini di
* GigaByte, MegaByte, KiloByte e Byte. Ad esempio se nb = 2147535048, ritorna
* "2GB 50KB 200B".
* @param nb numero di bytes
* @return stringa che descrive nb in GB, MB, KB e B */
public static String toGMKB(long nb) {
String s = "";
if (nb >= GIGABYTE)
s += nb/GIGABYTE+"GB";
nb %= GIGABYTE;
if (nb >= MEGABYTE)
s += (s.isEmpty() ? "" : " ")+nb/MEGABYTE+"MB";
nb %= MEGABYTE;
if (nb >= KILOBYTE)
s += (s.isEmpty() ? "" : " ")+nb/KILOBYTE+"KB";
nb %= KILOBYTE;
if (nb > 0)
s += (s.isEmpty() ? "" : " ")+nb+"B";
return s;
}
È stato usato il metodo isEmpty
delle stringhe che ritorna true
se la stringa è vuota. Al posto di s.isEmpty()
avremmo potuto usare s.equals("")
. Per mettere il metodo alla prova possiamo definire un metodo test_toGMKB
che chiede all'utente di digitare un numero di bytes, li passa al metodo toGMKB
e stampa il risultato
private static void test_toGMKB() {
Scanner input = new Scanner(System.in);
out.print("Test metodo toGMKB(), digita un numero di bytes: ");
long nb = input.nextLong();
out.println("toGMKB(" + nb + ") --> " + q(toGMKB(nb)));
}
Abbiamo dichiarato il metodo private
perché non lo intendiamo per un uso esterno alla classe Utils
, a differenza invece di toGMKB
. Abbiamo anche definito il seguente metodo di utilità
public static String q(String s) { return "\"" + s + "\""; }
Per eseguire il test basta invocarlo dal main
public static void main(String[] args) {
test_toGMKB();
}
Gli operatori booleani &&
(AND), ||
(OR) e !
(NOT) sono anch'essi ripresi da C/C++ ma possono essere usati solamente con valori booleani. In Java, quindi, valori non booleani non sono automaticamente convertiti a valori booleani nel contesto di un'espressione booleana come invece accade in altri linguaggi. Vediamo ora un esempio di metodo che usa gli operatori booleani
/** Ritorna true se il primo orario è minore o uguale al secondo. Ogni orario è
* specificato da un numero di ore, minuti e secondi.
* @param h1,m1,s1 primo orario
* @param h2,m2,s2 secondo orario
* @return true se il primo orario è minore o uguale al secondo */
public static boolean timeLEQ(int h1, int m1, int s1, int h2, int m2, int s2) {
if (h1 < h2)
return true;
else if (h1 == h2 && m1 < m2)
return true;
else if (h1 == h2 && m1 == m2 && s1 <= s2)
return true;
else
return false;
}
Potevamo anche definirlo equivalentemente senza usare if-else
public static boolean timeLEQ(int h1, int m1, int s1, int h2, int m2, int s2) {
return h1 < h2 || h1 == h2 && m1 < m2 || h1 == h2 && m1 == m2 && s1 <= s2;
}
Per metterlo alla prova definiamo un metodo test_timeLEQ
che chiede all'utente di digitare due orari, li passa al metodo timeLEQ
e stampa il risultato
private static void test_timeLEQ() {
Scanner input = new Scanner(System.in);
out.print("Test metodo timeLEQ(), digita due orari (h m s): ");
int h1 = input.nextInt(), m1 = input.nextInt(), s1 = input.nextInt();
int h2 = input.nextInt(), m2 = input.nextInt(), s2 = input.nextInt();
out.println("Il primo orario "+(timeLEQ(h1, m1, s1, h2, m2, s2) ? "" :
"non ") + "è minore o uguale al secondo");
}
Ecco un esempio di esecuzione (in italico quello che è stato digitato)
Test metodo timeLEQ(), digita due orari (h m s):
12 30 0
12 20 40
Il primo orario non è minore o uguale al secondo
i whitespace (spazi, newline, e simili) non fanno differenza ma qualsiasi altro carattere estraneo produrrebbe errori in lettura.
for
Anche il costrutto per iterare for
è ripreso da C/C++. La forma generale è la seguente
for ( init ; cond ; update ) {
blocco eseguito finché cond è true
}
Se presente, init
è eseguito e poi è eseguita la prima iterazione. L'esecuzione di ogni iterazione inizia valutando l'espressione booleana cond
, se presente, (se non è presente è come se avesse valore true
) se è false
esce dal for
, se invece è true
esegue il blocco e, se presente, update
e poi passa alla successiva iterazione. Se il blocco è costituito da una sola istruzione (statement), le parentesi graffe non sono necessarie. La forma tipica del for
è la seguente
for (int i = 0 ; i < n ; i++) {
blocco eseguito finché i < n
}
La variabile i
è locale al for
, nel senso che è visibile solamente all'interno del for
. Per un for
in questa forma è cattivo stile di programmazione modificare la variabile i
nel blocco. Ovviamente si possono facilmente ottenere altre sequenze di valori per la variabile i
, ad esempio in ordine inverso
for (int i = n-1 ; i >= 0 ; i--) {
blocco eseguito finché i >= 0
}
Vediamo qualche semplice esempio.
/** Ritorna una stringa ottenuta ripetendo n volte la stringa s. Ad esempio, se
* s = "Tre" e n = 3, ritorna "TreTreTre".
* @param s stringa da ripetere
* @param n numero di ripetizioni
* @return una stringa uguale alla ripetizione n volte di s */
public static String rep(String s, int n) {
String r = "";
for (int i = 0 ; i < n ; i++)
r += s;
return r;
}
Per il prossimo esempio abbiamo bisogno di due metodi delle stringhe, il metodo length()
che ritorna la lunghezza e charAt(i)
che ritorna il carattere (char
) in posizione i
.
/** Ritorna una stringa con la sequenza dei caratteri inversa rispetto a quella
* di s. Ad esempio, se s = "rovescio", ritorna "oicsevor".
* @param s una stringa
* @return la stringa rovesciata */
public static String reverse(String s) {
String r = "";
for (int i = s.length()-1 ; i >= 0 ; i--)
r += s.charAt(i);
return r;
}
Possiamo poi scrivere un metodo per mettere alla prova i metodi reverse
e rep
:
private static void test_reverse_rep() {
Scanner input = new Scanner(System.in);
out.print("Test metodi rep() e reverse(), digita una stringa: ");
String s = input.nextLine();
out.println("reverse(" + q(s) + ") --> " + q(reverse(s)));
out.print("Digita un intero: ");
int n = input.nextInt();
out.println("rep(" + q(s) + ", "+n+") --> " + q(rep(s, n)));
}
Java ha anche un'altra forma di for detto enhanced for
o for-each che vedremo più avanti.
while
e do-while
Il while
pure è ripreso da C/C++ e la sintassi è la seguente
while (cond) {
blocco eseguito finché cond è true
}
Vediamo subito un esempio
/** Ritorna true se n è un numero primo.
* @param n un intero
* @return true se n è primo */
public static boolean prime(long n) {
if (n <= 1) return false;
long d = 2;
while (d*d <= n && n%d != 0) d++;
return d >= n || n%d != 0;
}
La scrittura di un metodo che mette alla prova prime
è lasciato come esercizio. In alcuni casi un do-while
risulta più conveniente di un while
. Anch'esso è ripreso da C/C++ e la sintassi è la seguente
do {
blocco eseguito finché cond è true
} while (cond);
Quindi a differenza del while
il blocco è in ogni caso eseguito almeno una volta.
continue
e break
Se si vuole saltare alla fine dell'attuale iterazione di un loop (for
, while
o do-while
) si può usare l'istruzione continue
. Se si vuole uscire immediatamente da un loop si può usare l'istruzione break
. Il break
fa uscire solamente dal loop più interno che contiene il break
. Inoltre, può essere usato anche per uscire da uno switch
.
switch-case
e il tipo enum
A volte si deve eseguire un'operazione che dipende dal valore di una espressione. Si potrebbe usare una catena di if-else
ma essendo una situazione piuttosto frequente molti linguaggi hanno un'istruzione specializzata. Java riprende l'istruzione di selezione da C/C++ con la sintassi
switch (E) { //E espressione a valori
case v1:
istruzioni eseguite se E ha valore v1
break; //opzionale, se presente esce dallo switch,
//altrimenti esegue il prossimo case
case v2:
istruzioni eseguite se E ha valore v2 o un valore precedente senza break
break;
...
case vK:
istruzioni eseguite se E ha valore vK o un valore precedente senza break
break;
default: //opzionale
istruzioni eseguite se nessun case cattura il valore di E
}
Anche se le istruzioni di un case
sono due o più non sono necessarie le parentesi graffe. Il tipo dei valori dell'espressione E
deve essere uno dei seguenti: char
, byte
, short
, int
, Character
, Byte
, Short
, Integer
, String
, enum
. Il tipo enum
è simile all'omonimo tipo di C/C++ ma a differenza di quest'ultimo che è a valori interi, in Java è un tipo riferimento, quindi i suoi valori sono (riferimenti a) oggetti. Vediamo un esempio
/** Costanti che specificano i diversi tipi di allineamento. */
public static enum Align { LEFT, RIGHT, CENTER, CENTRE }
/** Ritorna la stringa s allineata secondo {@code a} in un campo di lunghezza
* len riempendo la lunghezza mancante con spazi. Ad esempio, se s = "pippo",
* len = 10 e a = Align.CENTER, ritorna " pippo ".
* @param s una stringa
* @param len lunghezza campo
* @param a allineamento
* @return la stringa s allineata */
public static String align(String s, int len, Align a) {
int ns = len - s.length(); // Numero spazi da inserire
switch (a) {
case LEFT:
return s + rep(" ", ns);
case RIGHT:
return rep(" ", ns) + s;
case CENTER:case CENTRE:
return rep(" ", ns/2) + s + rep(" ", ns - ns/2);
}
return s; // Solamente per evitare warning del compilatore
}
Come si vede, un tipo enum
si dichiara similmente ad una classe ma poi nel corpo sono elencati, separati da virgole, tutti i suoi possibili valori. Ogni valore, che è un oggetto e in questo caso di tipo enum
Align
, è specificato da un identificatore. Anche se non è una regola, per convezione gli identificatori di una enum
dovrebbero essere in maiuscolo anche per evidenziare che si tratta di valori costanti. Per poterli usare si deve specificare anche il nome della enum
così come si fa per i campi statici di una classe qualsiasi, ad esempio Align.LEFT
. Però, come si vede nel metodo align
, all'interno di uno switch
si può omettere il nome dell'enum
. Questo è solamente un assaggio delle caratteristiche di base di un enum
, che è molto più potente e versatile.
La scrittura di un metodo che mette alla prova il metodo align
è lasciata come esercizio.
[StringheVerticali] Scrivere un programma che legge tre stringhe e le stampa in verticale l'una accanto all'altra. Ad esempio, se le stringhe sono "gioco"
, "OCA"
e "casa"
allora il programma stampa:
gOc
iCa
oAs
c a
o
[Vocali] Scrivere un programma che legge una linea di testo e per ogni vocale stampa il numero di volte che appare nella linea di testo. Ad esempio, se la linea di testo è "mi illumino di immenso"
allora il programma stampa:
a: 0 e: 1 i: 5 o: 2 u: 1
[TrePiùGrandi] Scrivere un programma che legge una serie di numeri interi positivi (la lettura si interrompe quando è letto un numero negativo) e stampa i tre numeri più grandi della serie. Ad esempio, se la serie di numeri è 2,10,8,7,1,12,2
allora il programma stampa:
I tre numeri più grandi sono: 12, 10, 8
[TriplePitagoriche] Una tripla pitagorica è una tripla di numeri interi a, b, c tali che 1 ≤ a ≤ b ≤ c e a2 + b2 = c2. Ciò equivale a dire che a, b, c sono le misure dei lati di un triangolo rettangolo (da qui il nome). Scrivere un programmma che legge un intero M e stampa tutte le triple pitagoriche con c ≤ M.
[PI] Scrivere un programma che letto un intero k stampa la somma dei primi k termini della serie
4 - 4/3 + 4/5 - 4/7 + 4/9 - 4/11 + ...
La serie converge al numero pi greco. Quanto deve essere grande k per ottenere le prime 8 cifre decimali corrette (3.14159265)?
[CifreLettere] Scrivere un programma che legge un intero n e stampa le cifre di n in lettere. Ad esempio, se n = 2127, il programma stampa: due uno due sette
.
[NumeriPerfetti] Un numero perfetto è un numero intero che è uguale alla somma dei suoi divisori propri, ad esempio 6 è perfetto perché 6 = 1 + 2 + 3, mentre 8 non è perfetto dato che 1 + 2 + 4 non fa 8. Scrivere un programma che letto un intero M stampa tutti i numeri perfetti minori od uguali a M e le relative somme dei divisori. Ad esempio se M = 1000 il programma stampa:
6 = 1 + 2 + 3
28 = 1 + 2 + 4 + 7 + 14
496 = 1 + 2 + 4 + 8 + 16 + 31 + 62 + 124 + 248
[Monete] Scrivere un programma che letto un numero intero rappresentante un importo in centesimi di euro stampa le monete da 50, 20, 10, 5, 2 e 1 centesimi di euro che servono per fare l'importo. Ad esempio, se l'importo è 97 centesimi allora il programma stampa:
1 moneta da 50
2 monete da 20
1 moneta da 5
1 moneta da 2
[Fattorizzazione] La scomposizione in fattori primi di un numero n è l'elenco dei fattori primi di n con le loro molteplicità. Un fattore primo di n è un divisore di n che è un numero primo (un numero è primo se non ha divisori propri). Definire nella classe Utils
un metodo statico String factorize(long n)
che ritorna una stringa che descrive la fattorizzazione di n
. Ad esempio,
factorize(7) --> "7"
factorize(3000) --> "2(3) 3 5(3)"
factorize(10000000000001) --> "11 859 1058313049"
Scrivere anche un metodo che mette alla prova factorize
.
[Prefisso] Scrivere un metodo che prende in input due stringhe e ritorna la lunghezza del più lungo prefisso comune alle due stringhe. Ad esempio, se le due stringhe di input sono "Le cose non sono solo cose"
e "Le cose sono solo cose"
, ritorna 8
.
[Media] Scrivere un programma che prende in input una serie di numeri in virgola mobile (terminata non appena è immesso un numero negativo) e ne stampa la media.
[Palindrome] Una palindroma è una parola o stringa che rimane la stessa letta in entrambi i versi. Scrivere un metodo (magari ricorsivo) che presa in input una stringa determina se è una palindroma. Scrivere anche un programma che mette alla prova il metodo.
18 Feb 2016
Dal dizionario Treccani: artefatto s. m. Opera che deriva da un processo trasformativo intenzionale da parte dell’uomo. ↩
L'aggettivo statici (static) deriva da un uso particolare di questa parola chiave nel C che è stata poi ripresa nel C++. Sicuramente sarebbe stato più appropriato chiamarli metodi della classe piuttosto che metodi statici. ↩
In una classe si possono definire anche classi e interfacce.↩
Ad esempio, ogni valore di tipo int
richiede solamente 4 bytes ma un oggetto che rappresenta un intero richiede molta più memoria. Sopratutto, un array di int
oltre a consumare molta meno memoria di un array di oggetti (cioè un array di puntatori a oggetti) permette un accesso molto più veloce ai suoi elementi.↩
Gli altri modificatori per i campi, protected
, transient
e volatile
, sono usati più raramente.↩
Gli altri modificatori per i metodi, protected
, final
, synchronized
, native
e strictfp
, sono usati più raramente.↩