Metodologie di Programmazione: Lezione 1

Riccardo Silvestri

Inizio

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.

Il primo programma

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.

Eseguire un programma

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.

Classi

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.

Commenti

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!");
}

Tipi e tipi primitivi

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).

Variabili

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.

Variabili locali

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;

Campi

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 final5. 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.

Operatori aritmetici

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.

Metodi

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 static6. 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.

Package e la struttura di un programma Java

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

Input & Output

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).

Controllo del flusso (di esecuzione)

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.

Operatori relazionali

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();
}

Operatori booleani

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.

Esercizi

[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 ≤ abc 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 cM.

[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


  1. Dal dizionario Treccani: artefatto s. m. Opera che deriva da un processo trasformativo intenzionale da parte dell’uomo.

  2. 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.

  3. In una classe si possono definire anche classi e interfacce.

  4. 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.

  5. Gli altri modificatori per i campi, protected, transient e volatile, sono usati più raramente.

  6. Gli altri modificatori per i metodi, protected, final, synchronized, native e strictfp, sono usati più raramente.