Java Tutorial - Parte 1 0.1
Un tutorial per esempi
Caricamento in corso...
Ricerca in corso...
Nessun risultato
Gli oggetti Java

Nel programma scritto nel capitolo precedente abbiamo usato il linguaggio Java come se fosse un linguaggio procedurale: i metodi erano dichiarati static in modo da poterli richiamare specificandone l'identificativo. In questo capitolo verrà affrontato il tema della programmazione ad oggetti, OOP (Object Oriented Programming); scriveremo una semplice classe che gestisce le date del calendario gregoriano. Si tratta di una breve panoramica su come vengono gestite in Java le strutture di dati complesse ma non è una trattazione esaustiva di tutte le caratteristiche della programmazione ad oggetti. Le caratteristiche più complesse saranno affrontate nei capitoli successivi.

I files del capitolo

Nella sottocartella javatutor1/projects/caldate troverete i files sorgente di tutto il codice descritto in questo capitolo. Di seguito l'elenco dei files ed una loro breve descrizione:

Nome del file Descrizione
caldate.java la classe CalendarDate
test.java una suite di test per la classe CalendarDate
docoptions.txt usato per la documentazione
overview.txt usato per la documentazione
JDNAlgorithm.pdf una descrizione in inglese del algoritmo per il calcolo del JDN

Introduzione agli oggetti

Java è un linguaggio fortemente orientato agli oggetti: tutto in Java è un oggetto e qualsiasi tipo di dato o metodo non può esistere al di fuori di un oggetto. In senso lato, un oggetto Java è un pò come le strutture che abbiamo già visto in Tipi di dato complessi. Anzichè usare la parola chiave struct, per definire un oggetto Java si usa la parola chiave class.

Una classe viene definita in questo modo:

class MyClass
{
... corpo della classe ...
}

Nel corpo della classe andremo a definire:

  • i membri dati
  • i metodi che agiscono sui membri dati

Riprendiamo l'esempio della struttura data-di-calendario visto in Tipi di dato complessi

// Questo è linguaggio "C"
struct CalendarDate
{
int day;
int month;
int year;
};
CalendarDate moonLanding;
moonLanding.day = 20;
moonLanding.month = 7;
moonLanding.year = 1969;

E' chiaramente più semplice e pulito usare il nuovo tipo di dato per, esempio, passare una data come argomento ad una funzione che stampa a terminale la data piuttosto che dover passare i tre numeri interi:

// costrutto semplice e leggibile
void printDate( CalendarDate date )
{
...
}
// costrutto più difficile da leggere
void printDate( int day, int month, int year )
{
...
}

In una struttura in stile "C" l'accesso ai membri dati, chiamati fields (=campi) avviene attraverso l'operatore '.' (punto) ed è consentito a tutti e ciò determina il problema di mantenere i valori dei membri dati coerenti, vale a dire impedire al programmatore di impostare valori fuori range nel campo day e nel campo month: per esempio quest'ultimo può contenere solo i valori da 1 a 12, ovviamente. Posso definire una funzione a questo scopo:

void setDate( CalendarDate date, int day, int month, int year )
{
// eseguo prima i controlli di validità
...
// se tutto ok, imposto i valori nella struttura passata come argomento
date.day = day;
date.month = month;
date.year = year;
}
CalendarDate italyUnity;
setDate( italyUnity, 17, 3, 1861 );

In realtà, la funzione scritta sopra, benchè formalmente corretta, è concettualmente errata se fosse in linguaggio "C" ma lasciamola così in modo da essere più comprensibile. Con questa funzione ho un certo controllo su quali valori possono essere inseriti nei campi della struttura ma il problema di fondo rimane: non è affatto obbligatorio usare la funzione e l'acceso ai campi della struttura è sempre consentito nel linguaggio "C".

In java e in tutti i liguaggi orientati agli oggetti, i dati interni di una classe sono normalmente dichiarati privati della classe e l'accesso ad essi è consentito solo attraverso le funzioni definite nella classe stessa. Quindi in Java la classe estende il concetto di struttura di dati: non solo la classe contiene i campi della struttura ma contiene anche le funzioni che operano su questi dati: in Java le funzioni si chiamano metodi.

class CalendarDate
{
private int day;
private int month;
private int year;
// il metodo 'setDate'
public void setDate( int d, int m, int y )
{
// eseguo i controlli di validità su: d, m ed y
// e se tutto OK, memorizzo i valori nei membri dati
day = d;
month = m;
year = y;
}
...
}

La parola chiave private scritta come attributo dei membri dati indica al compilatore che essi sono privati della classe e sono accessibili solo ai metodi della classe. La parola chiave public scritta come attributo del metodo setDate indica al compilatore che quel metodo è pubblico, richiamabile anche dal codice esterno alla classe CalendarDate.

In Java, per creare un oggetto di classe CalendarDate devo usare l'operatore new il quale alloca lo spazio necessario a contenere l'oggetto:

CalendarDate waterlooBattle = new CalendarDate();
waterlooBattle.setDate( 18, 6, 1815 );

Anche l'accesso ai metodi, al pari dell'accesso ai membri dati, avviene in Java attraverso l'operatore '.' (punto).

Il this pointer

Notiamo subito una grande differenza tra il linguaggio "C" ed il linguaggio Java: in "C" ho solo funzioni e per modificare una struttura devo passarla come argomento alla funzione (in realtà devo passare il suo puntatore, non la struttura stessa) mentre in Java richiamo il metodo attraverso l'oggetto stesso che rappresenta la classe:

// in linguaggio C
CalendarDate oggi;
setDate( oggi, 4, 5, 2025 );
// in linguaggio Java
CalendarDate oggi = new CalendarDate();
oggi.setDate( 4, 5, 2025 );

Quello che succede a basso livello, però, è diverso: i metodi non esistono. Il costrutto che noi usiamo in Java esiste solo nei linguaggi ad alto livello ma a livello macchina quello che accade è lo stesso di ciò che avviene in linguaggio "C".
A basso livello viene richiamata una funzione che ha un nome univoco spesso composto dal nome della classe e dal nome del metodo; a questa funzione viene passato come argomento un riferimento nascosto che altro non è che l'indirizzo di memoria (il puntatore) alla variabile oggi. Questo puntatore è presente nel metodo setDate Java anche se non è dichiarato nella lista degli argomenti ed è conosciuto col nome di this pointer (= il puntatore a questo oggetto).

Voglio farvi notare un altra grande differenza tra il linguaggio "C" e Java che riguarda il come si possono creare oggetti di tipo CalendarDate:

// in linguaggio C
CalendarDate oggi;
// in linguaggio Java
CalendarDate oggi = new CalendarDate();

Nel linguaggio "C" posso semplicemente definire la variabile specificandone il tipo e l'identificativo. In Java, di contro, non basta: devo usare l'operatore new per creare effettivamente l'oggetto. Su questo punto torneremo quando affronteremo l'argomento delle "istanze" di oggetti.

Visibilità delle variabili

Affrontiamo subito il discorso della visibilità delle variabili nei metodi. In ogni metodo una variabile è visibile nel momento in cui essa viene dichiarata. I parametri al metodo, invece, sono visibili subito all'inizio del metodo ma, se essi hanno lo stesso nome dei membri dati della classe, quali variabili vengono effettivamente indirizzate?
Supponiamo di avere scritto un sorgente come questo:

class CalendarDate
{
private int day;
private int month;
private int year;
public void setDate( int day, int month, int year )
{
day = day;
month = month;
year = year;
}

Quale risultato darebbe la istruzione day=day? Beh, come avrete capito, nessun risultato, è una istruzione totalmente inutile dal momento che assegna ad una variabile il valore di se stessa!
Ma non era quello che intendava fare il programmatore: il metodo deve assegnare al membro dati day il valore del argomento day. Ebbene, quando viene definita una variabile all'interno di un metodo si dice che essa oscura la visibilità di una variabile con lo stesso nome definita in un altro contesto:

class MyClass
{
// il membro dati alfa
private int alfa;
public void myMethod( int alfa ) // l'argomento oscura il membro dati
{
int alfa; // la variabile locale oscura l'argomento al metodo
...
alfa = 0; // ho impostato la variabile locale
}
}

La regole di visibilità in Java sono le seguenti:

  • l'argomento al metodo con lo stesso nome di un membro dati oscura la visibilità del membro dati
  • la variabile locale con lo stesso nome di un argomento al metodo oscura la visibilità del argomento al metodo

Allora non posso usare gli stessi nomi dei membri dati come argomenti ai metodi? Si che posso, poichè i membri dati sono accessibili attraverso il this pointer:

public void setDate( int day, int month, int year )
{
this.day = day;
this.month = month;
this.year = year;
}

Astrazione dei dati

In questo capitolo affronteremo la prima delle tre caratteristiche peculiari della programmazione ad oggetti: la astrazione dei dati.
Questa caratteristica non si limita a mantenere i dati privati ma consente al programmatore di organizzarli come meglio crede, nel modo più efficente possibile. Dal momento che vogliamo scrivere una classe che gestisce le date del calendario, dobbiamo elencare quali funzionalità dobbiamo implementare per una gestione efficiente:

  • verificare se una data è valida
  • ottenere il numero di giorni di uno specifico mese/anno
  • sapere se uno specifico anno è bisestile oppure no
  • ottenere il giorno, mese ed anno di una data
  • ottenere il giorno della settimana di una data
  • aggiungere e sottrarre giorni da una data ed ottenere la data risultante
  • ottenere la differenza, in numero di giorni, tra due date

Ci rendiamo subito conto che gli ultimi tre punti non sono semplici da implementare: calcolare la differenza tra due date non è affatto banale come fare una semplice sottrazione, non siamo mica in Star Trek dove la data era una data astrale e si manteneva in un numero seriale come per esempio: "duemilacinquecentoquattordici-punto-tre".
Come sarebbe facile aggiungere, sottrarre giorni o restituire una differenza tra due date se il nostro calendario fosse un numero di giorni seriale!

Ma, perchè non possiamo farlo davvero? In fondo, il dato della classe CalendarDate è privato e l'accesso ad esso avviene attraverso i metodi della classe. Quello che dobbiamo fare è trovare ed implementare un algoritmo per convertire una data nel formato giorno,mese,anno in un numero seriale e viceversa.

Il nostro calendario è stato istituito da papa Gregorio XIII ed è stato introdotto ufficialmente il 4 ottobre 1582. Prima di tale data era in vigore il calendario giuliano, istituito da Caio Giulio Cesare, imperatore di Roma, nel 46 a.C. Quest'ultimo calendario aveva una regola per gli anni bisestili diversa da quella che usiamo oggi e questa regola era inesatta al punto che nel 1582 il calendario giuliano aveva accumulato un ritardo di 10 giorni rispetto alla data solare.
Per recuperare i giorni di ritardo, papa Gregorio stabilì che il giorno successivo al 4 ottobre 1582, data di entrata in vigore del calendario riformato, sarebbe stato il 15 ottobre e non il 5 ottobre, come vorrebbe la logica.
Un algoritmo corretto dovrebbe anche tenere presente questo fatto. Inoltre, per calcolare un numero di giorni seriale dobbiamo anche stabilire da quale data partire a contare e deve essere una data piuttosto lontana nel passato. Questa data, che coincide con il giorno ZERO, è detta epoch (=epoca) e praticamente tutti i calendari si basano su una epoch; anche il calendario gregoriano ha la sua epoch: la nascita di Gesù Cristo.

Per nostra fortuna, non dobbiamo reinventare la ruota; l'algortimo per mantenere la data come numero seriale esiste già ed è noto con il nome di giorno giuliano (da non confondere col calendario giuliano) o, per dirla in inglese, col nome di Julian Day Number (JDN). Questo metodo di calcolare la data è stato proposto da Giuseppe Scaligero nel 1583, proprio a ridosso della riforma gregoriana, e comincia a contare i giorni da lunedì 1 gennaio 4713 a.C. una data molto lontana nel passato.
Il JDN è un numero reale che conta i giorni nella parte intera e le ore, minuti e secondi nella parte frazionaria. Per esempio, adesso sono le 19:05 del 12 settembre 2024 che corrisponde al JDN: 2460566,295138889. Il vantaggio di usare un algoritmo già pronto è che si trovano in Internet numerose implementazioni come quella presente in Wikipedia - Giorno Giuliano scritta in pseudo-codice ma facilmente adattabile a qualsiasi linguaggio, anche Java.

La nostra classe di gestione delle date manterrà quindi la data come un numero seriale, un JDN, e non come giorno,mese,anno:

class CalendarDate
{
private double jdn;
public void setDate( int d, int m, int y )
{
jdn = gregorianToJDN( y, m, d, /*ore*/ 0, /*min*/ 0, /*sec*/ 0 );
}
// converte una data del calendario gregoriano in JDN
public static double gregorianToJDN( int year, int month, int day, int hour, int min, int sec )
...
// converte un JDN in una data del calendario gregoriano
public static int[] JDNToGregorian( double jdn )
...
}

Il metodo gregorianToJDN converte una data del calendario gregoriano (o del calendario giuliano se antecedente al 15 ottobre 1582) in un JDN, un numero reale di tipo double. Il metodo ritorna questo numero.
Gli argomenti al metodo gregorianToJDN sono di facile lettura; poichè il JDN ritornato può contenere anche le ore/minuti/secondi nella parte frazionaria, passiamo ad esso il valore ZERO dal momento che la nostra classe è interessata solo alla data del calendario e non alle ore.
Il metodo che converte una data gregoriana in JDN viene presentato in Il metodo gregorianToJDN mentre la sua controparte, cioè il metodo che converte un JDN in una data del calendario gregoriano o giuliano viene presentato in Il metodo JDNToGregorian

I costruttori

In una classe Java vi sono dei metodi speciali che hanno le seguenti caratteristiche:

  • non hanno un valore di ritorno, nemmeno il void
  • il loro nome è uguale a quello della classe

Questi metodi si chiamano costruttori e svolgono un compito particolare e cioè quello di inizializzare l'oggetto. L'allocazione della memoria necessaria a contenere l'oggetto avviene per mezzo del operatore new il quale richiama il costruttore della classe o, per meglio dire, uno dei suoi costruttori. Per il resto i costruttori sono metodi come gli altri e quindi dovranno avere un corpo, racchiuso tra le parentesi graffe, e possono avere degli argomenti.
Non è strettamente necessario definire uno o più costruttori: se non ci pensa il programmatore, il compilatore ne fornisce uno cosidetto di default. Il costruttore di default non ha argomenti e non ha codice: il compilatore inizializza semplicemente i membri dati al loro valore di default che, per i numeri reali, è lo ZERO.
Il costruttore di default della classe CalendarDate pertanto imposterebbe la data uguale alla epoch del JDN che corrisponde al 1 gennaio 4713 a.C. Dal momento che la epcca del JDN è troppo lontana nel passato per essere utile, scriveremo un costruttore di default che imposti come data di default il 1 gennaio 1970 che corisponde alla epoch di quasi tutti i personal computer.

public CalendarDate()
{
jdn = gregorianToJDN( 1970, 1, 1, 0, 0, 0 );
}

Abbiamo bisogno di altri costruttori? In teoria no. Una volta costruito un oggetto di tipo CalendarDate possiamo richiamare il metodo setDate per impostare qualsiasi data e fare poi i calcoli che ci servono.

In pratica, però, è buona abitudine di programmazione prevedere un numero sufficente di costruttori in modo da venire incontro alle esigenze dei programmatori esterni che usano la nostra classe (o che usiamo noi stessi):

  • un costruttore che accetti come parametro un numero di tipo double che rappresenta il JDN
  • un costruttore che accetta i tre parametri giorno, mese, anno in modo da non dover poi richiamare il metodo setDate; lo farà il costruttore stesso
  • un costruttore che accetta come parametro un numero intero che viene interpretato come un offset in giorni dalla epoch
  • un costruttore che accetta come parametro una stringa che descrive la data come per esempio "17 marzo 1861".

Per quanto riguarda l'ultimo costruttore abbiamo un piccolo problema: le stringhe che rappresentano una data del calendario gregoriano possono avere numerosi formati, Eccone alcuni:

  • 17 marzo 1861: un formato esteso
  • 17 mar 1861: un formato abbreviato
  • 17/03/1861: un formato numerico, europeo
  • 03/17/1861: un formato numerico, statunitense, nel quale il mese precede il giorno

Quale di questi formati usare? Ebbene, benchè nel nostro paese si usi indifferentemente almeno i primi tre formati, è consigliabile usare un formato internazionale, conosciuto da tutto il mondo o quasi. Un tale formato esiste ed è stato standardizzato da un ente di fama mondiale: il International Standards Organization (ISO) col nome di ISO 8601 che prevede che una data sia espressa nel formato:

YYYY-MM-DD

dove YYYY è l'anno, a quattro cifre, MM è il mese (da 1 a 12) a due cifre e DD è il giorno del mese (da 1 a 31) a due cifre. E questo è il formato che noi utilizzeremo.

Ed ecco il codice degli altri tre costruttori:

public CalendarDate( double jdn )
{
this.jdn = jdn;
}
public CalendarDate( int day, int month, int year )
{
setDate( day, month, year );
}
public CalendarDate( String isoDate )
{
setDate( isoDate );
}
public CalendarDate( int offset )
{
this();
jdn += offset;
}

Il codice dei costruttori è piuttosto facile ma l'ultimo merita un commento: avremmo potuto semplicemente richiamare il metodo setDate per impostare la data del 1 gennaio 1970 e poi aggiungere l'offset al JDN ma poichè questa operazione viene eseguita anche dal costruttore di default abbiamo preferito usare il costrutto this() che non è altro che un richiamo al costruttore di default.
Allo stesso modo potremmo richiamare altri costruttori semplicemente fornendo loro i relativi argomenti. Ci sono dei limiti al richiamo dei costruttori:

  • possono essere richiamati solo da altri costruttori: non è possibile richiamare un costruttore in un metodo
  • il richiamo al costruttore deve essere la prima istruzione di un altro costruttore
  • un costruttore non può richiamare se stesso: sarebbe una ricorsione infinita

Il metodo setDate richiamato nei vari costruttori sarà descritto in Il metodo setDate.

Classi, oggetti ed istanze

Come accennato in precedenza, ogni classe ha almeno un costruttore: se il costruttore di default non viene definito dal programmatore, il compilatore ne crea uno automaticamente inizializzando i membri dati ai valori di default.
Il costruttore viene richiamato quando si crea una istanza della classe cioè un oggetto reale. L'istanziazione della classe avviene per mezzo del operatore new il quale alloca la memoria necessaria a contenere l'oggetto e ne richiama il costruttore specifico:

CalendarDate italyUnity = new CalendarDate( 17, 3, 1861 ); // unità di Italia
CalendarDate independenceDay = new CalendarDate( 4, 7, 1776 ); // giorno dell'indipendenza USA
CalendarDate endSecondWar = new CalendarDate( 2, 9, 1945 ); // fine seconda guerra mondiale
CalendarDate aDate;

Nel codice suesposto, le variabili italyUnity, independenceDay e endSecondWar sono reference (=riferimenti) ad istanze della classe CalendarDate. Le istanze di una classe vengono anche chiamate oggetti.
In senso stretto, una classe non è un oggetto: una classe è un tipo di dato come potrebbe esserlo int oppure double. Un oggetto è l'istanza della classe cioè un dato reale che contiene il valore specifico, nel nostro caso una data del calendario.
Per concludere, un commento sull'ultima istruzione; cosa conterrà la variabile aDate? Essa è stata definita ma non abbiamo costruito alcuna istanza della classe e quindi a che cosa si riferisce? al nulla, direte voi! Ed in effetti è proprio così, poichè alla variabile aDate non è stato assegnato un oggetto reale, essa conterrà il valore null.
Definire una variabile senza istanziare un oggetto reale è come scrivere:

CalendarDate aDate = null;

Il valore null è una parola riservata del linguaggio Java e significa nessun riferimento; questo valore può essere usato come qualsiasi altro valore nei costrutti come per esempio:

if ( aDate == null ) {
aDate = new CalendarDate( 1, 1, 2000 );
}

In questo spezzone di codice confronto il valore della variabile aDate con null e se il risultato del confronto è vero, istanzio un oggetto di quella classe e ne assegno il riferimento a aDate che, pertanto, non avrà più il valore null.

Il garbage collector

Abbiamo imparato che l'operatore new alloca lo spazio in memoria per contenere i dati di ogni istanza di una classe e, successivamente, ne richiama il costruttore che provvede ad inizializzare i dati interni. Ma la domanda è: chi provvede a liberare la memoria allocata quando l'oggetto non è più necessario?

In alcuni linguaggi come il "C" ed il "C++" esiste la controparte al costrutto di allocazione della memoria: in "C" la memoria viene allocata con la funzione malloc (Memory ALLOCation) e liberata con la sua controparte: la funzione free. Il "C++" può usare indifferentemente le funzioni del "C" per allocare e liberare la memoria oppure l'operatore new (esatto, come in Java). Questo operatore ha la sua controparte per liberare la memoria: l'operatore delete.

nei linguaggi "C" e "C++" è responsabilità del programmatore liberare la memoria quando essa non è più necessaria; in altre parole, quando l'oggetto o il blocco di memoria non serve più, esso deve essere esplicitamente liberato o per mezzo della funzione free o per mezzo dell'operatore delete.

In Java la gestione della memoria è affidata alla JVM: non ci sono operatori per liberare la memoria allocata; quando un oggetto non viene più usato esso viene inserito in una collezione di oggetti non usati e la memoria allocata per questi oggetti viene liberata: questa collezione di oggetti divenuti inutili si chiama garbage collection.
Ma come fà la JVM a sapere che un oggetto è diventato inutile: semplice, quando non ci sono più riferimenti attivi per esso. Eccovi alcuni esempi:

public CalendarDate myMethod()
{
CalendarDate italyUnity = new CalendarDate( 17, 3, 1861 );
CalendarDate independenceDay = new CalendarDate( 4, 7, 1776 );
CalendarDate endSecondWar = new CalendarDate( 2, 9, 1945 );
... elaborazioni ...
independenceDay = null;
... elaborazioni ...
return endSecondWar;
}

L'oggetto che contiene la data della indipendenza degli USA viene creato nella seconda riga di codice e, dopo alcune elaborazione, il suo riferimento viene posto a null; pertanto l'oggetto riferito si trova senza alcun riferimento attivo nel codice ed è diventato, a tutti gli effetti, inutile.
L'oggetto che contiene la data dell'unità di Italia creato nella prima riga di codice ha il suo riferimento nella variabile italyUnity che è una variabile locale al metodo myMethod. Come tutte le variabili locali, essa non esisterà più quando il metodo rientra e, pertanto, l'oggetto rimane senza riferimento; esso è diventato inutile.
L'oggetto che contiene la data della fine della seconda guerra mondiale allocato nella terza riga ha il suo riferimento nella variabile endSecondWar la quale è una variabile locale e quindi destinata ad essere distrutta alla fine del metodo. Tuttavia, questo riferimento viene restituito dal metodo e, presumibilmente, memorizzato in una altra variabile definita dal chiamante:

public void callerMethod()
{
CalendarDate date = myMethod();
...
}

Quindi l'oggetto contenente la data di fine della seconda guerra ha ancora un riferimento attivo nella variabile date; esso non andrà a confluire nella garbage collection. Ovviamente, alla fine del metodo callerMethod la variabile locale date sarà distrutta e, se il riferimento alla data di fine della guerra non sarà in qualche modo riassegnato ad una altra variabile, l'oggetto riferito finirà nel garbage (= letteralmente significa "spazzatura").

I metodi di istanza

Si chiamano instance methods quei metodi che devono essere richiamati attraverso un oggetto reale cioè una istanza della classe. Questi sono i metodi che restituiscono il dato desiderato di quella particolare istanza e che abbiamo riepilogato nei punti precedenti ( vedi Astrazione dei dati).

class CalendarDate
{
private double jdn;
... costruttori ...
// instance methods
public void setDate( int d, int m, int y ) { ... }
public int getDay() { ... }
public int getMonth() { ... }
public int getYear() { ... }
public int getWeekday() { ... }
public int addDays( int days ) { ... }
public int subtractDays( int days ) { ... }
public int diffDate( CalendarDate other ) { ... }
}

Il metodo setDate

Questo metodo imposta in questo oggetto (quello per mezzo del quale il metodo viene richiamato, in inglese rende meglio l'idea perchè si dice: this object con esplicito riferimento al puntatore a this, di cui ho scritto in Il this pointer) la data specificata nei suoi parametri: giorno, mese ed anno.

Dal momento che la nostra classe mantiene il dato interno come un JDN non dobbiamo fare altro che convertire i tre parametri in un JDN; siccome abbiamo trovato l'algoritmo già pronto (sarà discusso in Il metodo gregorianToJDN) non ci resta altro da fare che richiamarlo:

public void setDate( int day, int month, int year )
{
jdn = gregorianToJDN( year, month, day, 0, 0, 0 );
}

Gli ultimi tre argomenti posti a ZERO sono le ore, minuti e secondi che l'algoritmo prevede in ingresso ma dal momento che la nostra classe si interessa solo della data, essi vengono posti uguali alla ora 00:00:00.

I metodi getDay, getMonth, getYear

Questi metodi sono molto simili: per ottenere il giorno, mese ed anno di una data espressa come JDN dobbiamo usare l'algoritmo inverso di quello visto in precedenza. Chiameremo questo metodo JDNToGregorian e, come peraltro ovvio, accetterà un solo argomento: il numero seriale JDN.
Cosa restituisce il metodo JDNToGregorian? Beh, deve restituire anno, mese, giorno, ora, minuti e secondi di una data gregoriana. Giusto? Certo, ma un metodo può restituire UNO ed UN SOLO valore, come la mettiamo?

In effetti, ogni metodo ha un solo valore di ritorno ma esso può essere di qualsiasi tipo, può restituire non solo tipi primitivi ma anche oggetti (ovviamente, riferimenti ad oggetti). E poichè le arrays sono oggetti (vedi Le arrays) il metodo restituirà una array di sei interi i cui elementi rappresentano, nell'ordine: anno, mese, giorno, ora, minuti e secondi della data gregoriana.

public int getDay()
{
int[] data = JDNToGregorian( jdn );
return data[2];

Il metodo getMonth è uguale senonchè il valore restituito è ad indice 1 nella array mentre nel metodo getYear il valore da restituire è ad indice ZERO.

Il metodo getWeekday

Prendiamo una data in formato gregoriano come la fine della seconda guerra mondiale, 2 settembre 1945; che giorno della settimana era? Non è facile da calcolare a mano ma non è nemmeno così semplice scrivere un algoritmo per farlo calcolare da un computer a meno che ... a meno che non abbiamo usato la strategia di memorizzare la data come un numero seriale!!!!
Se il giorno ZERO del JDN era lunedì, è ovvio che tutti i multipli di 7 del numero seriale sono di lunedì. Pertanto non ci resta che calcolare il resto della divisione per 7 (il modulo sette) del JDN ed avremo il giorno della settimana dove 0=lunedì e 6=domenica:

public int getWeekday()
{
return (int)(jdn + 0.5) % 7;
}

I metodi addDays e subtractDays

Come per il caso precedente, aggiungere o sottrarre giorni da una data del calendario gregoriano non è una operazione banale a meno che non si sia scelto di mantenere il dato come un numero seriale:

public void addDays( int days )
{
jdn += days;
}

Il metodo subtractDays nemmeno lo presento, sono sicuro che lo avete già scritto da soli.

Il metodo diffDate

Anche in questo caso, calcolare una differenza in numero di giorni tra due date del calendario mantenute come JDN è una operazione banale: è una sottrazione, con l'accortezza di un cast esplicito sul risultato:

public int diffDate( CalendarDate other )
{
return (int) (jdn - other.jdn);
}

I metodi statici

Quelli che abbiamo scritto finora si chiamano instance methods (= metodi di istanza) dal momento che non avrebbe senso richiamarli senza un oggetto reale, una istanza della classe. Per esempio, il metodo getWeekday che restituisce il giorno della settimana non avrebbe senso senza fornire una data specifica.
Vi sono altri metodi, invece, che non hanno affatto bisogno di conoscere una data specifica per restituire un risultato: questi sono i static methods (= i metodi statici).

Un esempio di essi è il metodo che deve restituire un boolean (vero o falso) se uno specifico anno è bisestile oppure no. C'è forse bisogno di una data per ottenere questa informazione? assolutamente no, è sufficente fornire l'anno di interesse come argomento al metodo:

public static boolean isLeapYear( int year )
{
boolean r = false;
if ( year <= 1582 ) {
r = (year % 4) == 0;
}
else {
r = ((year % 400) == 0 ) || (( (year % 4) == 0 )
&& ( (year % 100) != 0 ));
}
return r;
}

Un metodo statico si definisce aggiungendo l'attributo static nella definizione del metodo. Raramente i metodi statici sono privati: dal momento che essi non hanno accesso ai dati privati di una istanza possono essere richiamati anche dall'esterno alla classe.

Il metodo della nostra classe implementa le regole per gli anni bisestili:

  • per il calendario giuliano, quindi fino all'anno 1582 compreso l'anno è bisestile se è multiplo di 4
  • per il calendario gregoriano l'anno è bisestile se è multiplo di 400 oppure se multiplo di 4 ma NON multiplo di 100

Come si richiama un metodo statico? Poichè non è necessaria una istanza della classe il metodo statico si richiama specificando il nome della classe prima del metodo:

boolean r = CalendarDate.isLeapYear( 2024 ); // r = TRUE
boolean r = CalendarDate.isLeapYear( 2023 ); // r = FALSE

Il metodo daysInYear

Anche questo è un metodo statico pubblico: esso ritorna il numero di giorni presenti in un anno, quello fornito come argomento. Sappiamo che il numero di giorni di un anno è 365 a meno che l'anno non sia bisestile, nel qual caso i giorni sono 366. Il codice è davvero semplice e non merita ulteriori commenti:

public static int daysInYear( int year )
{
int numDays = 365;
if ( isLeapYear( year )) {
numDays++;
}
return numDays;
}

Il metodo daysInMonth

Anche questo metodo è statico e pubblico e ritorna il numero di giorni di un particolare mese, fornito come argomento. Ci possono essere molti modi per implementare questo metodo: possiamo scrivere una serie di if..else oppure un blocco switch che per ogni valore del mese restituisce il numero di giorni di quel mese: 28, 30 o 31.
Una complicazione riguarda il mese di febbraio: se l'anno è bisestile il numero di giorni da restiuire è 29 e non 28. Ecco perchè l'anno di riferimento deve essere fornito come argomento al metodo.

La soluzione che io ho scelto è quella di creare una array di 12 elementi di tipo int ed inizializzarla col numero di giorni per ogni elemento con l'indice ZERO che rappresenta gennaio e l'indice 11 che è dicembre. Se l'anno è bisestile, modifico l'elemento ad indice 1 (=febbraio) ed inserisco il valore 29. Il mese in argomento diminuito di una unità è l'indice del elemento della array da restituire:

public static int daysInMonth( int month, int year )
{
int[] numDays = { 31,28,31,30,31,30,31,31,30,31,30,31 };
if ( isLeapYear( year )) {
numDays[1] = 29;
}
return numDays[month - 1];
}

Il metodo isValid

Benchè la nostra classe sia funzionante con i valori congrui, essa ha dei limiti importanti dovuti all'algoritmo di conversione delle date in un JDN. Questo algoritmo funziona se i parametri giorno, mese ed anno sono all'interno del range previsto ma, se osserviamo il costruttore della nostra classe, notiamo che potremmo fornire ad esso qualunque numero intero sia positivo che negativo:

CalendarDate date1 = new CalendarDate( -1256, 674, -987653 );

Il codice suesposto sarebbe perfettamente legale per il compilatore ma non lo è in senso logico. Dobbiamo quindi prevedere un metodo, anch'esso statico, che verifica la validità dei dati forniti come giorno, mese ed anno. La verifica della validità di una data si basa sulle seguenti regole:

  • il giorno del mese deve essere compreso tra 1 ed il numero di giorni del mese
  • il mese deve essere compreso tra 1 e 12
  • l'anno non deve essere minore del 4713 a.C. che rappresenta la epoch del JDN
public static boolean isValid( int day, int month, int year )
{
boolean r = false;
if ( year >= -4712 ) {
if ( month >= 1 && month <= 12 ) {
if ( day >= 1 && day <= daysInMonth( month, year )) {
r = true;
}
}
}
return r;
}

Probabilmente vi chiederete perchè il confronto di validità dell'anno è stato scritto in questo modo: ( year >= -4712 ) se invece la epoch del JDN comincia col 4713 a.C. e non col 4712. Ebbene questo è dovuto perchè nè nel calendario gregoriano nè il quello giuliano esisteva l'anno ZERO. Quindi l'anno precedente al 1 d.C. è il 1 a.C. ma, numericamente, l'anno 1 a.C. è l'anno ZERO nel JDN mentre il 2 a.C. assume il valore numerico -1.
Useremo questo metodo prima di impostare una data in una istanza della classe CalendarDate. Se la data non è valida il metodo o il costruttore solleveranno una eccezione di tipo IllegalArgumentException.

Il metodo gregorianToJDN

Ed arriviamo finalmente al metodo che implementa la conversione tra una data del calendario gregoriano / giuliano in un JDN. Il metodo accetta sei argomenti di tipo intero, nell'ordine: anno, mese, giorno, ore, minuti e secondi. Il metodo restituisce un double che rappresenta il JDN per la data/ora fornita come argomento; nella nostra classe abbiamo scelto di usare solo la data e quindi gli ultimi tre argomenti sono tutti ZERO.
Poichè il JDN comincia a mezzogiorno del 1 gennaio 4713 a.C. le ore 00:00:00 di una qualsiasi data corrispondono ad un JDN con la parte frzionaria sempre uguale a ,5.

Prima di calcolare il JDN, il metodo controlla la validità dei dati forniti in ingresso (non per le ore,minuti e secondi) e, se essi non sono validi, ritorna -1.
Non riporto qui il codice del metodo; potete vederlo nel listato sorgente caldate.java che trovate nella sottocartella javatutor/projects/caldate. Il codice Java presentato nel metodo gregorianToJDN è la implementazione in Java dell'algortimo scritto in pseudo-codice presentato nella pagina Wikipedia - Giorno Giuliano.

Il metodo JDNToGregorian

L'algoritmo inverso prende come argomento un JDN, un numero reale e ritorna una array di sei elementi di tipo intero che rappresenta, a partire dal indice ZERO: anno, mese, giorno, ora, minuti e secondi della data di calendario. Se il JDN in ingresso è negativo esso non è considerato valido ed il metodo ritorna null.
Anche in questo caso, non riporto qui il codice del metodo; potete vederlo nel listato sorgente caldate.java che trovate nella sottocartella javatutor/projects/caldate. Il codice Java presentato nel metodo JDNToGregorian è la implementazione in Java dell'algortimo scritto in pseudo-codice presentato nella pagina Wikipedia - Giorno Giuliano.

I membri dati

I membri dati di istanza

Ogni istanza di una classe, cioè ogni oggetto possiede una copia dei membri dati e questo è assolutamente prevedibile: poichè ogni oggetto rappresenta una data diversa ognuno di essi possiede una copia privata del membro dati jdn e questo anche se le date contenute negli oggetti sono le stesse. Mi spiego meglio:

CalendarDate date1 = new CalendarDate( 2, 9, 1945 );
CalendarDate date2 = new CalendarDate( 2, 9, 1945 );

Le due variabili date1 e date2 contengono la stessa identica data: la fine della seconda guerra mondiale; tuttavia, ognuna di esse ha la propria copia personale del membro dati jdn e questo potrebbe sembrare uno spreco di memoria: non sarebbe meglio condividere il dato in modo da risparmiare memoria?. Assolutamente no! Altrimenti come potrei modificare una delle due date senza modificare anche l'altra variabile?

CalendarDate date1 = new CalendarDate( 2, 9, 1945 );
CalendarDate date2 = new CalendarDate( 2, 9, 1945 );
date2.setDate( 4, 7, 1776 );

Se il dato fosse condiviso l'ultima istruzione imposterebbe la data dell'indipendenza degli USA in entrambe le variabili ma non è quello che intendevo fare io: solo date2 deve essere modificata, non date1.

Diverso è il caso se io assegno ad una variabile il valore di una altra variabile:

CalendarDate date1 = new CalendarDate( 2, 9, 1945 );
CalendarDate date2 = date1;
date2.setDate( 4, 7, 1776 );

In questo spezzone di codice date2 si riferisce allo stesso oggetto a cui si riferisce date1. Non esiste una seconda istanza della classe poichè ne ho creata una sola con l'operatore new: entrambe le variabii si riferiscono allo stesso oggetto in memoria, quindi la modifica di una variabile si riflette inesorabilmente anche sull'altra variabile.

L'operatore di uguaglianza di Java

Ma se io confronto le due variabili, esse sono uguali? In altre parole quale è il risultato dell'operatore di confronto (==) tra date1 e date2.
Nel linguaggio Java dobbiamo distinguere due casi:

  • per i tipi di dato primitivi (vedi I tipi primitivi) l'operatore di uguaglianza ritorna true se le due variabili sono dello stesso tipo (o compatibili) ed hanno lo stesso valore.
  • per gli oggetti, l'operatore di uguaglianza ritorna true solo se le due variabili si riferiscono alla stessa istanza; istanze diverse che hanno lo stesso valore non sono "uguali"

In sostanza:

CalendarDate date1 = new CalendarDate( 2, 9, 1945 );
CalendarDate date2 = new CalendarDate( 2, 9, 1945 );
CalendarDate date3 = date1;
boolean r;
r = date1 == date2; // false
r = date1 == date3; // true

Anche se le due variabili date1 e date2 contengono la stessa identica data (la fine della seconda guerra mondiale) l'operatore di uguaglianza ritorna FALSE poichè esse si riferiscono a due oggetti distinti. Potrebbe sembrare illogico a prima vista ma invece questa regola è stata introdotta su specifico design del linguaggio per poter permettere al programmatore di sapere se due variabili si riferiscono allo stesso oggetto oppure no.
Per conoscere se due variabili sono "uguali" nel senso che contengono lo stesso valore, in Java si usa sovrascrivere il metodo equals. Questo metodo accetta un argomento: la variabile da confrontare e restituisce true se le due date coincidono:

public boolean equals( Object obj )
{
if ( obj == null ) {
return false;
}
if ( this == obj ) {
// se questo oggetto e 'obj' sono la stessa istanza, sono certamente uguali
return true;
}
boolean r = false;
if ( obj instanceof CalendarDate ) {
// confronta i due JDN solo se 'obj' è una istanza di CalendarDate
r = jdn == ((CalendarDate)obj).jdn;
}
return r;
}

Per convenzione, se l'oggetto da confrontare è null il metodo equals ritorna sempre false. Se non è così, il metodo confronta i due membri dati jdn: quello di questo oggetto con quello dell'altro oggetto. Se i due JDN sono uguali, allora le date sono uguali.
Riprendendo l'esempio di cui sopra:

CalendarDate date1 = new CalendarDate( 2, 9, 1945 );
CalendarDate date2 = new CalendarDate( 2, 9, 1945 );
CalendarDate date3 = date1;
boolean r;
r = date1 == date2; // false
r = date1 == date3; // true
r = date1.equals( date2); // true

Questa strategia del linguaggio potrebbe sembrare illogica ma ha un suo preciso motivo; i più curiosi possono leggere la sezioni dedicata a questo argomento in L'operatore di uguaglianza di Java.

I membri dati statici

Come per i metodi, anche i membri dati possono essere static. Il membro dati jdn è un membro dati d'istanza: come abbiamo imparato nella sezione I membri dati di istanza ogni oggetto (istanza della classe) ha la sua copia personale della variabile jdn. Tuttavia, ci sono casi in cui non è necessario avere tante copie di un membro dati: ne basta una sola condivisa tra tutte le istanze della classe: queste "variabili" sono di solito nomi mnemonici per costanti che non cambiano mai nel corso del programma.

Per esempio, vogliamo definire alcune date cruciali nella storia, come quelle che abbiamo usato poc'anzi, in modo che non sia necessario, per chi vuole usare la nostra classe, ricordarsele a memoria:

  • la data dell'unità di Italia
  • il giorno dell'indipendenza americana
  • la fine della seconda guerra mondiale

E' ovvio che non serve avere questi membri dati in ogni istanza della classe poichè esse non cambiano mai. Possiamo allora definire questi membri dati con attributo static in modo che, al pari dei metodi, possono essere riferite con il nome della classe anzichè attraverso una sua istanza. Tali variabili sono conosciute col nome di costanti mnemoniche ed hanno le seguenti caratteristiche:

  • il loro nome è scritto tutto in maiuscolo
  • se il nome è composto da più parole esse sono separate col carattere underscore
  • sono dichiarate static e final

Per esempio, il seguente spezzone di codice definisce il JDN per alcune date importanti. La parola chiave final indica al compilatore che la variabile non può essere riassegnata rendendola, di fatto, una costante.

class CalendarDate
{
public static final double ITALY_UNITY = 2400851.5;
public static final double INDEPENDENCE_DAY = 2369915.5;
public static final double END_SECOND_WAR = 2431700.5;
...
}

Usando le costanti mnemoniche è possibile creare un oggetto data di calendario che contiene quella data anche se non si ricorda la data esatta:

CalendarDate date = new CalendarDate( CalendarDate.END_SECOND_WAR );

Il costrutto suesposto crea una nuova istanza della classe CalendarDate passandovi come argomento il JDN specifico della data della fine della seconda guerra mondiale.

Nell'esempio precedente abbiamo definito delle costanti menmoniche che contengono tre double, cioè dei tipi di dato primitivi. E' anche possibile definire membri dati statici che contengono degli oggetti, non solo dati primitivi:

class CalendarDate
{
public static final CalendarDate ITALY_UNIT = new CalendarDate( 17,3,1861 );
public static final CalendarDate INDEPENDENCE_DAY = new CalendarDate( 4,7,1776 );
public static final CalendarDate END_SECOND_WAR = new CalendarDate( 2,9,1945 );
...
}

Questi si usano nello stesso modo ma non è necessario creare una nuova istanza della classe, è sufficente assegnare il riferimento alla variabile statica in una nuova variabile.

CalendarDate date = CalendarDate.END_SECOND_WAR;

Considerazioni finali

La classe CalendarDate contiene anche un metodo main: quindi la classe può essere usata come una applicazione CLI. La documentazione completa in formato javadoc di questo progetto è disponibile al seguente link: Il progetto caldate

Benchè la classe CalendarDate sia funzionante, essa non è utile ad altro se non a questo scopo didattico. La libreria standard di Java possiede classi ben più complete e flessibili per gestire una data del calendario: vedi a questo proposito le classi LocalDate e GregorianCalendar.

Argomento precedente - Argomento successivo - Indice Generale