Scarica Dal linguaggio C al linguaggio Java parte 1 e più Dispense in PDF di Programmazione Java solo su Docsity!
Dal linguaggio C al linguaggio Java
(Prima parte)
Riccardo Silvestri
Queste dispense sono intese fornire una introduzione alla programmazione orientata agli oggetti e al
linguaggio Java. Si assume che il lettore conosca il linguaggio C cosicchè le caratteristiche di Java che
derivano direttamente dal C sono trattate il più rapidamente possibile. L'approccio adottato è pragmatico
e concreto, nel senso che si preferisce introdurre una nuova astrazione sulla base di una esigenza
concreta piuttosto che fare l'opposto (prima l'astrazione e poi le applicazioni concrete). Quindi, gli
esempi e gli esercizi assumono un'importanza centrale per l'introduzione e la spiegazione di nuovi
concetti. Infine è bene sottolineare che, allo scopo di rendere rapida l'acquisizione di una buona
dimestichezza con la programmazione ad oggetti e con Java, alcuni aspetti non sono trattati con la
dovizia di dettagli che meriterebbero. Questo è giustificato dalla convinzione che una volta acquisita una
buona padronanza del linguaggio diventa poi più facile comprendere le sottigliezze non solo del
linguaggio stesso ma anche della programmazione ad oggetti.
Sommario della prima parte
Che cos'è Java
Breve storia di Java
La macchina virtuale
La piattaforma
Confronto con altri linguaggi
Le basi procedurali di Java
Il primo programma
Tipi primitivi, stringhe, variabili e operatori Stringhe Funzioni matematiche
Input & Output
Controllo del flusso
Esercizi Stringa_verticale Parole_verticali Vocali Tre_più_grandi Cornice Triple_Pitagoriche Pi_greco
Cifre->lettere Lettere->cifre Numeri_perfetti Sostituzione Monete Fattori_primi Palindrome Frasi_palindrome Potenze Monete_sbilanciate
Struttura di un programma Java
Classi e oggetti
Orientamento agli oggetti
Classi e oggetti Campi Metodi Costruttori Campi e metodi statici Un esempio
La prima classe
Esercizi Metodi_che_accedono Strisce_orizzontali Scacchiera_su_misura Metti_alla_prova Piramidi
Piramidi_capovolte Date Date+ Differenza_di_date Date_in_stringhe Data_di_nascita Razionali Razionali+
Tipi, riferimenti e variabili Tipi primitivi e tipi riferimento Inizializzazioni e valori di default
Errori in compilazione e in esecuzione
Classi nidificate
Esercizi Errori Immutabilità Liste_di_interi Date_vicine Code_di_stringhe Pile_di_stringhe
Pile_di_interi&stringhe Espressioni
Che cos'è Java
Java è un linguaggio di programmazione orientato agli oggetti ( Object Oriented ) con una sintassi
simile a quella dei linguaggi C e C++. È un linguaggio potente che evita però quelle complesse
caratteristiche che rendono poco maneggevoli altri linguaggi orientati agli oggetti come il C++.
Breve storia di Java
Nel 1991 un team di ingegnieri della Sun Microsystems, guidato da Patrick Naughton e James Gosling,
iniziò la progettazione di un linguaggio con l'obbiettivo di agevolare la programmazione di piccoli
apparecchi elettronici. Siccome tali apparecchi non avevano grandi quantità di memoria e le CPU
potevano essere le più diverse, era importante che il linguaggio producesse codice snello e che non fosse
legato ad una particolare architettura hardware. Questi requisiti portarono il team ad ispirarsi ad un
modello che era stato adottato da alcuni implementatori del linguaggio Pascal ai tempi dei primi personal
computer. Infatti, l'inventore del Pascal, Niklaus Wirth era stato il primo a introdurre l'idea di un
linguaggio portabile basato sulla generazione di codice intermedio per un computer ipotetico detto
macchina virtuale ( virtual machine ). Gli ingegnieri del team adottarono questo modello ma basarono il
nuovo linguaggio sul C++ piuttosto che sul Pascal (questo perché la loro formazione era radicata nel
mondo UNIX). All'epoca, comunque, il nuovo linguaggio non era visto come un fine ma solamente come
un mezzo per la programmazione di piccoli apparecchi elettronici. Inizialmente Gosling pensò di
chiamarlo "Oak" (quercia) ispirato da una grande quercia che poteva ammirare dal suo studio. Ma, presto
si accorsero che esisteva già un linguaggio con quel nome. Il nome Java fu poi suggerito durante una
pausa in un coffee shop.
Intanto, agli inizi degli anni '90, il mercato degli apparecchi elettronici "intelligenti" non era ancora
sufficientemente sviluppato e l'intero progetto rischiava il fallimento. Però, il World Wide Web e internet
stavano crescendo a ritmi elevatissimi e il team si rese conto che la nascente tecnologia dei browser
poteva essere notevolmente potenziata proprio da un linguaggio con le caratteristiche di Java
(indipendente dall'architettura hardware, real-time, affidabile e sicuro). Nel 1995 alla conferenza
SunWorld presentarono il browser HotJava scritto interamente in Java che poteva eseguire codice (Java)
contenuto in pagine web (ciò che oggi è chiamato applet ). Agli inizi del 1996 la Sun rilasciò la prima
versione di Java e il linguaggio iniziò a suscitare un interesse crescente. La versione 1.0 di Java non era
certo adeguata per lo sviluppo serio di applicazioni. Ma da allora il linguaggio, attraversando parecchie
versioni, è stato via via arricchito e le sue librerie sono state enormemente potenziate ed ampliate fino
alla più recente versione 6, rilasciata nel 2006. Attualmente Java è un linguaggio maturo ed è usato per
sviluppare applicazioni su grande scala, per potenziare le funzionalità di server web, per fornire
applicazioni per apparecchi elettronici di largo consumo come cellulari e PDA e per tanti altri scopi.
La macchina virtuale
Quando un programma scritto in Java è compilato, il compilatore lo converte in bytecodes che sono le
istruzioni di un linguaggio macchina di una CPU virtuale chiamata appunto Macchina Virtuale Java
( Java Virtual Machine ), in breve JVM. La JVM non corrisponde a nessuna CPU reale anche se, in linea
di principio, potrebbe essere realizzata direttamente in hardware. La JVM è sempre implementata sotto
forma di un software che esegue le istruzioni bytecodes tramite un opportuno interprete. Ovvero la JVM
traduce i bytecodes nelle istruzioni macchina della CPU del computer reale sul quale si vuole eseguire il
programma Java. Quindi per poter eseguire un programma Java su un certo sistema hardware e software
(ad esempio, un PC Pentium Intel con Mac OS X) è necessario vi sia installata una specifica JVM
(capace di eseguire la traduzione per quel sistema).
A questo punto ci si potrebbe chiedere quali sono i vantaggi di questo passaggio per un linguaggio
Esistono altri linguaggi simili a Java. Sicuramente quello che più di tutti è simile a Java è il C#. Però
questo linguaggio a differenza di Java, e anche del C e del C++, non è disponibile per sistemi operativi
diversi da Windows.
Le basi procedurali di Java
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 come Java che è molto più
complesso di un linguaggio come il C. Per questa ragione, dapprima faremo un tour delle principali
caratteristiche del linguaggio e poi ritorneremo su quelle che necessitano di un adeguato
approfondimento. Diamo per scontata una buona conoscenza del linguaggio C e quindi non ci
soffermeremo più dello stretto necessario sulle caratteristiche di Java che derivano direttamente da tale
linguaggio.
Il primo programma
In Java un programma è composto da classi. Per ora, una classe può essere vista come una struct del
C in cui però oltre ai campi è possibile definire anche delle funzioni che in Java sono chiamate metodi.
Come in C la definizione di una struct può essere usata solamente allocando i corrispondenti elementi
così in Java una classe per poter essere usata deve essere istanziata in oggetti. Tuttavia, in Java, come
vedremo presto, una classe può essere usata anche senza che venga istanziata. Anzi i primi esempi
riguarderanno proprio programmi che usano una classe senza istanziarla.
public class Primo { public static void main(String[] args) { System.out.println("Primo programma Java"); } }
L'effetto di questo programma è semplicemente quello di stampare a video la stringa "Primo programma
Java". In grassetto sono state evidenziate le parole chiave di Java. La parole chiave class inizia la
definizione di una classe. Questa e poi seguita dal nome della classe, in questo caso Primo. Poi tra
parentesi graffe è definito il corpo della classe, cioè tutti i suoi membri (campi e metodi). In questo caso,
c'è un solo metodo ed è un metodo speciale perchè può essere visto come il corrispettivo in Java della
funzione main del C. Infatti, l'esecuzione di un programma Java inizia sempre eseguendo il metodo main
di una classe. Il metodo main come qualsiasi altro metodo è definito dichiarando una intestazione
( method header ) e un corpo ( method body ) racchiuso tra parentesi graffe. L'intestazione a sua volta
comprende, nell'ordine, degli eventuali modificatori ( modifiers ), in questo caso public e static, il tipo
del valore ritornato (void), il nome del metodo (main) e la lista dei parametri (String[] args). Il
metodo main essendo speciale 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 la invocazione di un metodo. Si noti che abbiamo
usato il termine "invocazione" per indicare ciò che in C corrisponderebbe alla chiamata di una funzione.
Infatti questo è il termine che si usa in Java. Il metodo invocato è println() che appartiene all'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("Primo
programma Java") è perfettamente simile a quello di printf("Primo programma Java\n") in C. Si
noti anche che in Java si usa lo stesso operatore di selezione "." del 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 Primo deve chiamarsi Primo.java. Attenzione a rispettare le
maiuscule e minuscole perchè Java è un linguaggio sensibile a questa differenza in tutti i contesti: parole
chiave, nomi di variabili, classi, metodi, file, ecc. Quindi 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 definita nel
file. Più precisamente, in un file .java può essere definita una sola classe pubblica (identificata appunto
dal modificatore public), però può contenere anche la definizione di altre classi non pubbliche.
Tipi primitivi, stringhe, variabili e operatori
I tipi primitivi di Java sono simili a quelli del C ma con importanti differenze. La seguente tabella
descrive i tipi primitivi di Java:
boolean true o false
char carattere 16-bits Unicode UTF-16 (senza segno)
byte intero da 8 bits: da -128 a 127
short intero da 16 bits: da -32768 a 32767
int intero da 32 bits: da -2147483648 a 2147483647
long intero da 64 bits: da -9223372036854775808 a 9223372036854775807
float numero in virgola mobile da 32 bits (IEEE 754)
double numero in virgola mobile da 64 bits (IEEE 754)
I tipi numerici byte,short,int,long,float,double sono molto simili a quelli del C. Il tipo char può
essere visto come una estensione del corrispondente tipo del C e ne discuteremo fra poco. La
dichiarazione delle variabili e la loro inizializzazione ricalca la sintassi del C. Ecco alcune dichiarazioni
ed inizializzazioni:
byte interopiccolissimo = -2; short interopiccolo = 50; int interogrande = 120000; long interograndissimo = 345000000000000;
Come in C il simbolo "=" rappresenta l'assegnamento e il ";" termina ogni istruzione ( statement ). Anche
gli operatori sono gli stessi del C. 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 capitaleIniziale = 1000; //capitale iniziale double tasso = 1.04, tassoComposto5; // il tasso composto per 5 anni e' il tasso annuale elevato alla quinta potenza tassoComposto5 = tasso*tasso; tassoComposto5 *= tassoComposto5; tassoComposto5 = tasso; double capitaleFinale = capitaleInizialetassoComposto5; double interessi = capitaleFinale - capitaleIniziale;
Come si intuisce da questo esempio gli operatori aritmetici +,-,*,/,% hanno lo stesso significato che
hanno nel C, incluse le forme con assegnamento +=,-=,*=,/=,%= e gli operatori ++,-- di incremento e
decremento. Anche i commenti si scrivono nello stesso modo: // per quelli su una singola linea e /*
... */ per quelli che possono occupare più linee. Inoltre le conversioni automatiche tra i tipi numerici
seguono regole simili a quelle del C.
Il tipo char è differente dall'omonimo del C. Infatti è in grado di rappresentare oltre ai tradizionali
caratteri ASCII anche l'insieme molto più vasto dei caratteri Unicode. Le costanti carattere, come in C,
sono racchiuse tra apici singoli. Ad esempio 'A','a','0','w','@' rappresentano i corrispondenti
caratteri. Inoltre, possono anche essere usate le Unicode escape sequences. Queste 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. Per informazioni complete sui codici Unicode si può
consultare il sito: http://www.unicode.org/. Oltre alle Unicode escape sequences che permettono di
definire tutti i caratteri rappresentabili, è possibile usare anche delle sequenze di escape simili a quelle
del C: \b (backspace), \t (tab), \n (line feed), \f (form feed), \r (carriage return), " (double quote), '
if (distanza(x, y, 1.0, 1.0) <= 1.0) // controlla se il punto puntiIn++; // cade nel cerchio unitario } System.out.println("Il valore di \u03C0 è "+Math.PI); double approxPI = (4*( double )puntiIn)/numeroPunti; System.out.println("Il valore approssimato è "+approxPI); } }
Come si vede il for, l'if e vari operatori hanno la stessa sintassi e lo stesso significato che hanno nel
linguaggio C (ritorneremo su di essi fra poco). L'esecuzione del programma produce il seguente risultato:
Il valore di π è 3. Il valore approssimato è 3. 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 del linguaggio Java piuttosto approfondita. Almeno
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). Per fortuna, sempre la piattaforma Java, ci fornisce una classe
che fa proprio questa traduzione. La classe si chiama Scanner 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,
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).
Come esempio consideriamo un programma che calcola l'importo della rata per la restituzione di un
prestito avendo fornito in input il capitale, il tasso annuo e il numero complessivo di rate mensili. La rata
è calcolata applicando le formule:
RATA = CAPITALE *( TM * TC )/( TC - 1)
TM = (1 + TA /100)1/12^ - 1
TC = (1 + TM ) NR
inoltre TA è il tasso annuo e NR è il numero di rate. Così 100* TM risulta essere il tasso su base mensile e
100*( TC - 1) è l'interesse composto relativo all'intero periodo di restituzione del prestito.
import java.util.*; public class Rata { // metodo statico che calcola il tasso mensile a partire da quello annuo
public static double tassoMensile( double ta) { return 100(Math.pow(1 + ta/100, 1.0/12.0) - 1); } public static void main(String[] args) { Scanner in = new Scanner(System.in); // creazione dell'oggetto Scanner System.out.print("Capitale: "); int capitale = in.nextInt(); // legge l'importo del capitale System.out.print("Tasso annuo: "); double tassoAnnuo = in.nextDouble(); // legge il tasso annuo System.out.print("Numero rate mensili: "); int numeroRate = in.nextInt(); // legge il numero di rate double tassoMensile = tassoMensile(tassoAnnuo); System.out.println("Il tasso mensile è "+tassoMensile+"%"); double tm = tassoMensile/100; // calcola l'importo della double tc = Math.pow(1 + tm, numeroRate); // rata applicando la double rata = capitale((tm*tc)/(tc - 1)); // formula System.out.println("L'importo della rata è "+rata); } }
La linea import java.util.*; è necessaria perché la classe Scanner appartiene al package java.util.
Tutte le volte che si usa una classe che non appartiene al package di base java.lang (System, String e
Math appartengono a questo package) è necessario dichiarare il package di appartenenza tramite una
direttiva import. Parleremo più dettagliatamente dei packages e della direttiva import in seguito. Una
possibie esecuzione del programma produce il seguente risultato:
Capitale: 20000 Tasso annuo: 15 Numero rate mensili: 36 Il tasso mensile è 1.171491691985338% L'importo della rata è 684. Controllo del flusso
Tutte le istruzioni di Java per il controllo del flusso in un programma sono riprese da quelle del C, con
una sola eccezione che discuteremo più avanti. Quindi Java dispone delle istruzioni if-else, while, do-
while, for e switch-case con la stessa sintassi del C. Vediamo subito alcuni semplici esempi. Il
seguente programma prende in input tre numeri e li stampa ordinati in senso cresecente:
import java.util.*; public class Ordine { public static void main(String[] args) { Scanner in = new Scanner(System.in); System.out.println("Digita tre numeri: "); double a = in.nextDouble(), b = in.nextDouble(), c = in.nextDouble(); String risultato = "I tre numeri ordinati sono "; if (a <= b) { if (b <= c) risultato += a + " " + b + " " + c; else if (a <= c) risultato += a + " " + c + " " + b; else risultato += c + " " + a + " " + b; } else { if (a <= c) risultato += b + " " + a + " " + c; else if (b <= c) risultato += b + " " + c + " " + a; else risultato += c + " " + b + " " + a; } System.out.println(risultato); } }
Come si intuisce da questo esempio anche tutti gli operatori relazionali <,<=,>=,>,==,!= sono uguali a
quelli del C. Il prossimo programma prende in input una stringa e conta il numero di vocali presenti nella
Scanner in = new Scanner(System.in); int n = in.nextInt(); String msg; switch (n) { case 1: msg = "Hai digitato 1"; break ; case 2: msg = "Hai digitato 2"; break ; case 3: msg = "Hai digitato 3"; break ; default : msg = "Hai digitato qualcosa di diverso da 1,2,3"; } System.out.println(msg); } }
Ovviamente, ritroveremo tutti questi costrutti per il controllo del flusso molto spesso nel seguito usati in
esempi ed esercizi. Inoltre, anche in Java è possibile scrivere metodi ricorsivi. Ecco un semplice
programma che implementa un metodo ricorsivo per il calcolo del fattoriale:
import java.util.; public class Fattoriale { public static long fattoriale( int n) { // metodo ricorsivo if (n <= 1) return 1; else return nfattoriale(n - 1); } public static void main(String[] args) { Scanner in = new Scanner(System.in); System.out.print("Digita un intero: "); int n = in.nextInt(); System.out.println("Il fattoriale di "+n+" è "+fattoriale(n)); } } Esercizi
In alcuni esercizi può essere utile usare il metodo print() che è uguale a println() eccetto che non
va a capo. Inoltre, si tenga presente che entrambi i metodi accettano come parametro anche un singolo
char.
[Stringa_verticale] Scrivere un programma che legge una stringa (cioè una linea di testo) e la stampa
in verticale. Ad esempio, se la stringa letta è "gioco" allora il programma stampa:
g i o c o
[Parole_verticali] Scrivere un programma che legge tre parole e le stampa in verticale l'una accanto
all'altra. Ad esempio, se le parole 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
[Tre_più_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
[Cornice] Scrivere un programma che legge un intero n e stampa una cornice quadrata di lato n fatta di
caratteri '*'. Ad esempio, se n = 5 , il programma stampa:
[Triple_Pitagoriche] Una tripla pitagorica è una tripla di numeri interi a , b , c tali che 1 ≤ a ≤ b ≤ c
e a^2 + b^2 = c^2. 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_greco] Scrivere un programma che letto un intero k stampa la somma dei primi k termini della serie
La serie converge al numero pi greco. Quanto deve essere grande k per ottenere le prime 8 cifre decimali
corrette (3.14159265)?
[Cifre->lettere] 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.
[Lettere->cifre] Scrivere un programma che esegue la trasformazione inversa di quella del programma
precedente. Letta una linea di testo, se questa è composta di parole rappresentanti numeri da 1 a 9,
stampa il numero corrispondente. Ad esempio, se legge "due uno due sette" allora stampa 2127.
[Numeri_perfetti] 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:
[Sostituzione] Scrivere un programma che legge una linea di testo e se questa contiene la parola
"mille" allora stampa la linea di testo in cui però tutte le occorrenze della parola "mille" sono
sostituite con la parola "cento". Ad esempio, se la linea di testo è "mille non più mille" allora il
programma stampa: "cento non più cento".
[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 è di 97 centesimi allora il programma stampa:
1 moneta da 50 2 monete da 20 1 moneta da 5 1 moneta da 2
[Fattori_primi] La scomposizione in fattori primi di un numero n è l'elenco dei fattori primi di n con le
loro molteplicità. Si ricorda che un fattore primo di n è un divisore di n che è un numero primo (un
numero è primo se non ha divisori propri). Scrivere un programma che legge un numero intero n e
stampa la scomposizione in fattori primi di n. Il comportamento del programma per alcuni valori di n è il
seguente:
Questi elementi devono apparire nell'ordine dato.
Classi e oggetti
Finora abbiamo visto quegli aspetti di Java che non sono dissimili da quelli di un qualsiasi linguaggio
di programmazione procedurale. Adesso iniziamo a entrare nel vivo del linguaggio Java, considerando
quelle caratteristiche che lo rendono un linguaggio orientato agli oggetti.
Orientamento agli oggetti
Cosa significa dire che un linguaggio è "orientato agli oggetti"? Per rispondere a questa domanda
conviene fare un passo indietro e ricordarsi qual'è l'obbiettivo di un linguaggio di programmazione. Il
principale obbiettivo è rendere facile la vita dei programmatori in tutte quelle fasi dello sviluppo del
software in cui il linguaggio di programmazione usato riveste un ruolo importante (ad esempio, nella
progettazione, nella scrittura del codice, nel debugging, nel testing e nella manutenzione del codice). E
come fa un linguaggio a tentare di raggiungere questo obbiettivo? Cercando di gestire al meglio la
complessità che è intrinseca in un qualsiasi sistema software di dimensioni non banali. E c'è,
essenzialmente, un solo modo per fronteggiare la complessità: cercare di decomporre l'intero in parti
meno complesse. I diversi linguaggi di programmazione usano filosofie e meccanismi differenti per
aiutare i programmatori ad attuare questa strategia.
I linguaggi procedurali, come ad esempio il C, offrono pochi mezzi: funzioni o procedure e la
possibilità di costruire nuovi tipi aggregando altri tipi (ad esempio, tramite le struct). Una procedura
permette di isolare una parte del sistema software dal resto. Così che il resto del sistema può
disinteressarsi di come è fatta la procedura al suo interno e considerare solamente ciò che serve per poter
usare la procedura (la sua interfaccia). Questo riduce la complessità riducendo il numero delle potenziali
relazioni fra le varie parti del sistema. Sostanzialmente, è il principio dell' information hiding o
incapsulamento : suddividere il sistema in parti in modo tale che le loro interazioni si possano definire in
base a semplici interfacce che risultano indipendenti da come le parti sono implementate. Questo
principio è così consolidato e naturale che ormai è quasi dato per scontato. Oltre al principio
dell'incapsulamento ci sono altri aspetti di un linguaggio che possono aiutare a fronteggiare la
complessità anche se non sono altrettanto importanti. La possibilità di costruire nuovi tipi tramite
semplice aggregazione di altri tipi aiuta a ridurre la complessità tramite la diminuzione della distanza tra
la natura delle informazioni reali e la loro rappresentazione nel sistema. Questo a sua volta migliora la
leggibilità del codice e quindi anche la capacità di modificare ed estendere le funzionalità del sistema.
I linguaggi orientati agli oggetti come Java offrono mezzi più sofisticati per fronteggiare la complessità
del software. Uno dei più importanti trae la sua forza dalla combinazione delle due caratteristiche sopra
menzionate. Infatti una classe può essere vista, in prima approssimazione, come una struct che oltre ad
avere campi ha anche delle funzioni che in Java si chiamano metodi. Così una classe è più efficace
nell'isolare una parte del sistema software perché può comprendere sia procedure sia dati che sono
intimamente connesse le une agli altri. Ad esempio, in un sistema software per la gestione dei dati del
personale di una azienda, ci potrebbe essere una classe Impiegato che serve a rappresentare e
manipolare i dati relativi agli impiegati. Questa classe conterrà, oltre ai soliti campi (nome, cognome,
data_di_nascita, ecc.), anche dei metodi: un metodo età() che calcola l'età attuale, un metodo
stipendio() che calcola la busta paga, un metodo stampa() per la stampa formattata dei dati
dell'impiegato, ecc. Ogni istanza della classe Impiegato, che in Java si chiama oggetto , rappresenta uno
specifico impiegato. Complessivamente i valori dei campi di un oggetto sono lo stato di quell'oggetto. Il
comportamento di un oggetto, cioè il risultato dell'invocazione di un qualsiasi metodo relativamente a
quell'oggetto, dipende dallo stato dell'oggetto. Così, se rossi è il riferimento all'oggetto che rappresenta
l'impiegato Mario Rossi, l'invocazione del metodo rossi.età() ritorna proprio l'età di Mario Rossi, così
come verdi.età() ritorna invece l'età di Giuseppe Verdi, se verdi è un riferimento all'oggetto che
rappresenta l'impiegato Giuseppe Verdi.
Tutto questo, oltre ad ampliare le possibilità di applicazione del principio dell'information hiding,
permette anche di migliorare la leggibilità e sopratutto la riusabilità del codice. La riusabilità è un aspetto
di grandissima importanza per rendere l'attività della programmazione più proficua ed efficiente. Quando
infatti la strategia della programmazione orientata agli oggetti è applicata alla realizzazione di librerie
software (ad esempio, manipolazione di stringhe, gestione di collezioni di elementi, accesso a file, ecc.)
mostra tutta la sua forza realizzando strumenti di uso generale che possono essere usati e riusati in
tantissime situazioni. Il successo e la continua crescita della piattaforma Java ne è una solida prova.
Nel linguaggio Java, al pari degli altri linguaggi orientati agli oggetti, il meccanismo base delle classi e
degli oggetti è coadiuvato da altri meccanismi. Tra i più importanti c'è il meccanismo dell' ereditarietà
che consente di estendere in modo naturale una classe (cioè, modificare o aggiungere funzionalità) per
definire altre classi. Questo meccanismo a sua volta permette il polimorfismo che è molto utile per
trattare in modo uniforme le funzionalità di oggetti appartenenti a classi differenti. E poi ci sono la
genericità e l' overloading.
Classi e oggetti
Per il momento vedremo solamente la versione base della definizione di una classe. Poi, mano a
mano, avremo modo di introdurre tutte le altre caratteristiche. Lo schema semplificato della definizione
di una classe pubblica può essere descritto così:
public class NomeClasse { dichiarazioni di campi dichiarazioni di costruttori dichiarazioni di metodi }
Il modificatore di accesso ( access modifier ) public indica proprio che la classe è pubblica, cioè è
visibile e quindi accessibile da qualsiasi altra parte del programma. Non c'è nessun vincolo sull'ordine
con cui sono elencate le dichiarazioni all'interno del corpo della classe. Di solito però sono disposte in
quell'ordine.
Campi Con il termine campo si intende una variabile che appartiene ad una classe e la dichiarazione di
un campo è simile alle dichiarazioni di variabili che abbiamo già incontrato. Però può essere preceduta
da dei modificatori, tra questi quelli che vedremo subito sono i modificatori di accesso public e
private. Ad esempio,
public double valore; private int status; int codice;
la prima dichiarazione riguarda una variabile valore di tipo double che è pubblica, cioè qualsiasi parte
del programma, anche al di fuori della classe e del package della classe, può accedere al campo valore,
cioè può leggerlo o scriverlo. Mentre la variabile status essendo dichiarata privata è accessibile
solamente dall'interno della classe in cui è definita. La variabile codice, non avendo alcun modificatore
di accesso specificato, è accessibile solamente dall'interno del package a cui appartiene la classe. Di
solito i campi di una classe sono dichiarati privati per evitare che dall'esterno della classe si possa
modificarne i valori senza che questo sia controllato dalla classe. Quindi l'uso del modificatore private
aiuta l'applicazione del principio dell'incapsulamento.
Metodi Un metodo è una funzione che appartiene ad una classe. La dichiarazione di un metodo rispetta
il seguente schema semplificato che comprende una intestazione e un corpo:
modificatori tipo-ritornato nomeMetodo ( lista-parametri ) { corpo-del-metodo }
L' intestazione del metodo è formata da uno o più modificatori, il nome tipo-ritornato del tipo del valore
ritornato, il nome del metodo e la lista dei parametri. Se il metodo non ritorna alcun valore allora il tipo-
ritornato è void, come nel C. La lista dei parametri può essere vuota ed è simile alla lista dei parametri
di una funzione del C. Inoltre, il passaggio dei parametri è, come nel C, per valore. La signature (firma)
secondi non dipendono da nessun oggetto della classe. I campi e i metodi statici possono essere visti
come campi e metodi condivisi da tutti gli oggetti della classe. Ad esempio, un campo statico potrebbe
mantenere un valore costante che è uguale per tutti gli oggetti della classe. Esempi di campi statici sono i
campi out e in della classe System o il campo PI della classe Math. Un metodo statico potrebbe essere
un metodo che combina in qualche modo due oggetti della classe creandone un terzo oppure un metodo
che non ha bisogno dello stato di un oggetto specifico per essere calcolato. Esempi di metodi statici sono
tutti i metodi della classe Math, come sqrt(), pow(), ecc. Per dichiarare un campo o un metodo statico si
usa il modificatore static.
Un esempio Consideriamo come esempio una semplice classe che rappresenta studenti. Ogni oggetto
della classe ha tre campi matricola, nome e cognome. Inoltre ha un costruttore e alcuni metodi pubblici.
La classe ha anche un campo statico matricolaCorrente che serve a mantenere l'ultima matricola usata
e un metodo statico privato che produce una nuova matricola. La classe ha anche un metodo statico
pubblico che permette di cambiare la matricola di uno studente.
public class Studente { // dichiarazione e inizializzazione di un campo statico private static long matricolaCorrente = 1000000; // metodo publico statico public static void cambiaMatricola(Studente s) { s.matricola = nuovaMatricola(); } private static long nuovaMatricola() { // metodo privato statico matricolaCorrente++; return matricolaCorrente; } private long matricola; // dichiarazione di campi (non statici) private String nome, cognome; public Studente(String nome, String cognome) { // costruttore matricola = nuovaMatricola(); this .nome = nome; this .cognome = cognome; } public String getNome() { return nome; } // metodi pubblici public String getCognome() { return cognome; } public long getMatricola() { return matricola; } public void stampa() { System.out.println("Matricola: " + matricola); System.out.println("Cognome: " + cognome + " Nome: " + nome); } }
Si osservi che il metodo statico cambiaMatricola() può accedere al campo privato dell'oggetto
Studente perché appartiene alla stessa classe. Nel costruttore è usata la parola chiave this che
rappresenta il riferimento all'oggetto stesso. Qui this è usato per potersi riferire ai campi nome e
cognome dell'oggetto che altrimenti sarebbero stati mascherati dagli omonimi argomenti del costruttore.
La suddetta classe è usata nel seguente programma.
public class Main { public static void main(String[] args) { // crea due oggetti di tipo Studente Studente stu1 = new Studente("Mario", "Rossi"); Studente stu2 = new Studente("Maria", "Verdi"); stu1.stampa(); // stampa i dati dei due studenti stu2.stampa(); // cambia la matricola del primo studente Studente.cambiaMatricola(stu1); // e la stampa System.out.println("Nuova matricola: " + stu1.getMatricola());
Si noti che i metodi (non statici), come ad esempio stampa(), possono essere invocati solamente in
relazione ad uno specifico oggetto, in questo caso gli oggetti stu1 e stu2 di tipo Studente. Mentre i
metodi statici, come cambiaMatricola(), possono essere invocati solamente in relazione alla classe,
proprio perché non appartengono ad alcun oggetto ma appartengono invece alla classe.
La prima classe
Consideriamo una classe, che chiameremo CharRect, i cui oggetti rappresentano rettangoli di caratteri
che possono essere visualizzati sulla console. Inizialmente la classe sarà molto spartana e permetterà di
rappresentare solamente rettangoli riempiti con il carattere '*'. Sarà via via raffinata ed ampliata
esemplificando nel contempo nuove caratteristiche del linguaggio Java e anche alcune tecniche di
progettazione.
La prima versione della classe permette di costruire un nuovo rettangolo fornendo la posizione del suo
carattere in alto a sinistra, la larghezza (numero di colonne) e l'altezza (numero di righe). La posizione è
data relativamente ad un ipotetico sistema di riferimento che numera le righe dall'alto verso il basso
partendo da 0 e le colonne da sinistra verso destra partendo sempre da 0. La classe ha un solo metodo il
quale stampa il rettangolo. Ecco una definizione di questa classe:
import static java.lang.System.; public class CharRect { private int left, top; // posizione del primo crattere in alto a sinistra private int width, height; // dimensioni del rettangolo // costruttore public CharRect( int l, int t, int w, int h) { left = l; top = t; width = w; height = h; } public void draw() { // stampa il rettangolo for ( int i = 0 ; i < top ; i++) out.println(); for ( int r = 0 ; r < height ; r++) { int right = left + width; for ( int c = 0 ; c < right ; c++) out.print(c < left? ' ' : ''); out.println(); } } }
La direttiva import static permette di "importare" i campi e i metodi statici di una classe.
Ovviamente, la classe va scritta in un file di nome CharRect.java. I campi sono tutti privati perchè
fanno parte dell'implementazione della classe e quindi non dovrebbero essere visibili dall'esterno.
Mentre, il costruttore e il metodo draw() devono essere pubblici per poter essere invocati liberamente
dall'esterno. Il costruttore inizializza i campi che definiscono l'oggetto rettangolo con i valori che saranno
forniti al momento della creazione. Quando, come in questo caso, è definito un metodo con almeno un
parametro, e non è esplicitamente definito il costruttore senza parametri, il costruttore di default non può
essere invocato. Vale a dire, non si può scrivere new CharRect().
Vediamo subito come questa classe può essere usata. Per fare ciò occorre una classe che implementa
un metodo main(). Definiamo quindi una classe che chiameremo Test (in un file di nome Test.java):
public class Test { public static void main(String[] args) { CharRect rectA = new CharRect(3, 0, 10, 5); CharRect rectB = new CharRect(6, 1, 12, 3);
Per l'implementazione conviene introdurre un metodo ausiliario che stampa una linea del rettangolo e che
può essere usato in entrambi i metodi di stampa. La nuova versione della classe è la seguente:
import static java.lang.System.; public class CharRect { private static final char DEF_FILLCHAR = ''; private static final char DEF_FILLCHAR2 = 'o'; private int left, top; private int width, height; private char fillChar = DEF_FILLCHAR, fillChar2 = DEF_FILLCHAR2; public CharRect( int l, int t, int w, int h) { left = l; top = t; width = w; height = h; } public void setChar( char c) { fillChar = c; } public void setChar( char c, char c2) { fillChar = c; fillChar2 = c2; } public void draw() { for ( int i = 0 ; i < top ; i++) out.println(); for ( int r = 0 ; r < height ; r++) drawLine(fillChar, fillChar); } public void drawVStripes() { for ( int i = 0 ; i < top ; i++) out.println(); for ( int r = 0 ; r < height ; r++) drawLine(fillChar, fillChar2); } // metodo ausiliario (privato) private void drawLine( char ch1, char ch2) { int right = left + width; for ( int k = 0 ; k < right ; k++) { char ch = ' '; if (k >= left) ch = ((k - left) % 2 == 0? ch1 : ch2); out.print(ch); } out.println(); } }
Il metodo drawLine() è privato perché è utile per implementare i metodi pubblici draw() e
drawVStripes() ma non deve essere accessibile dall'esterno della classe. Si osservi che, grazie
all'overloading, i due metodi setChar() hanno lo stesso nome. Un programma che mette alla prova la
nuova versione è il seguente:
public class Test { public static void main(String[] args) { CharRect rectA = new CharRect(3, 0, 10, 5); CharRect rectB = new CharRect(6, 1, 12, 3); CharRect rectC = new CharRect(10, 1, 4, 4); rectA.draw(); rectB.drawVStripes(); rectC.draw(); rectA.drawVStripes(); rectB.setChar('#', '!'); rectB.drawVStripes(); } }
Ed ecco il risultato:
oooooo oooooo oooooo
ooooo ooooo ooooo ooooo oooo*o #!#!#!#!#!#! #!#!#!#!#!#! #!#!#!#!#!#!
Questa è ancora una versione rudimentale della classe CharRect, più avanti vedremo delle versioni
molto più versatili e potenti.
Ed ora alcune considerazioni circa lo stile di programmazione che sono importanti perché se applicate
con costanza e coerenza migliorano la leggibilità del codice. Il nome di una classe di solito è un
sostantivo singolare che si riferisce direttamente all'oggetto della classe. Inoltre è consuetudine che i
nomi delle classi inizino con una maiuscola. Questo per meglio dstinguerli dagli altri identificatori (nomi
di metodi e variabili) che dovrebbero sempre iniziare con una minuscola. I nomi delle costanti invece,
come nel C, dovrebbero contenere solo maiuscole. I nomi dei metodi che semplicemente modificano i
valori dei campi della classe dovrebbero iniziare con set (come il metodo setChar()). Mentre quelli
che ritornano il valore di un campo dovrebbero iniziare con get (se ci fosse un simile metodo nella
nostra classe si chiamerebbe getChar()). Inutile, forse, aggiungere quanto sia importante per la
leggibilità, sopratutto per un linguaggio complesso come Java, una corretta e coerente indentazione del
codice. Per un lettore umano, può essere persino più importante della correttezza sintattica.
Esercizi
[Metodi_che_accedono] Aggiungere alla classe CharRect dei metodi per leggere i campi fillChar e
fillChar2 e inoltre aggiungere un metodo per modificare la posizione del rettangolo.
[Strisce_orizzontali] Aggiungere alla classe CharRect un metodo drawHStripes() che stampa il
rettangolo a strisce orizzontali come nell'esempio qui sotto:
oooooo
oooooo
L'implementazione può sfruttare il metodo drawLine()?
[Scacchiera] Aggiungere alla classe CharRect un metodo drawChessboard() che stampa il rettangolo
a mo' di scacchiera, come nell'esempio qui sotto:
oooo oooo oooo
Si può modifcare il metodo drawLine() in modo tale che risulti utile anche per stampare i rettangoli a