Java Tutorial - Parte 2 0.1
Un tutorial per esempi
Caricamento in corso...
Ricerca in corso...
Nessun risultato
Appendice

Introduzione

Questo capitolo contiene le sezioni di approfondimento di alcuni concetti che sono stati solo accennati nel corso di questo tutorial. La lettura di queste sezioni è opzionale nel senso che i concetti esposti non si applicano al linguaggio Java in modo specifico.

JSON vs XML

Tradotto da Introducing JSON:

JSON (JavaScript Object Notation) è un formato di scambio dati leggero. È facile da leggere e scrivere per gli esseri umani. È facile da analizzare e generare per le macchine. Si basa su un sottoinsieme dello standard del linguaggio di programmazione JavaScript ECMA-262 3a edizione - dicembre 1999. JSON è un formato di testo completamente indipendente dal linguaggio ma che utilizza convenzioni familiari ai programmatori della famiglia di linguaggi C, tra cui C, C++, C#, Java, JavaScript, Perl, Python e molti altri. Queste proprietà rendono JSON un linguaggio di scambio dati ideale.

Le caratteristiche principali di JSON sono le seguenti;

  • è un formato di puro testo, quindi compatibile con tutte le piattaforme ed architetture hardware/software esistenti
  • ha una sintassi simile a molti linguaggi di programmazione, tra cui Java; un programmatore ha familiarità con la sintassi JSON
  • è facile da leggere e da scrivere sia per gli umani che per le macchine
  • JSON supporta nativamente il concetto di numero, stringa, booleano, array e oggetto (inteso come struttura)
  • si impara in cinque minuti

Prendiamo come esempio il seguente messaggio di presentazione di MasterMind:

  • type: 103
  • id: 10
  • field[0]: Lukas (il nome del giocatore)
  • field[1]: 66051 (la versione della applicazione)

Nel formato descritto in Il formato del messaggio appare come segue:

MM0;103;10;Lukas;66051;null;null;FILLER

Di seguito, il confronto tra come apparirebbe in XML ed in JSON:

       XML                          JSON              
---------------------------  -------------------- 
<?xml version="1.0"         |{
     encoding="UTF-8"?>     |   "type" : 103,
<messaggio>                 |   "id"   : 10,
    <type>103</type         |   "fields" :
    <id>10</id>             |   [
    <fields>                |      "Lukas",
        <field>Lukas</field>|      66051,
        <field>66051</field>|      null,
        <field>null</field> |      null
        <field>null</field> |   ]
    </fields>               |}
</messaggio>                |

Come potete osservare da soli, non c'è paragone di leggibilità tra i due formati: JSON si legge al volo mentre XML non è così intuitivo. Per approfondire la sintassi di JSON ed imparare ad usarlo leggete il link di cui sopra o cercate nel web: ci sono migliaia di articoli interessanti.

Gli orologi del sistema

Nei sistemi informatici ci sono diversi orologi ognuno con la propria specifica funzione:

Il clock di sistema

Il system clock (=orologio di sistema) è la frequenza di funzionamento del microprocessore e si misura in Herz: questo valore non ha nulla a che fare con il elapsed time (=tempo trascorso) in quanto dipende dal microprocessore

Il timer del sistema

Per misurare il tempo trascorso i sistemi possiedono un system timer (un timer di sistema) il cui "tichettio" aggiorna un contatore seriale; questo contatore misura i secondi trascorsi da una data che, per convenzione, viene posta al 1 gennaio 1970. Questa data è detta epoch (=epoca).
In Java, il timer di sistema è rappresentato dalla classe java.time.Instant ma è possibile ottenere il valore del timer in millisecondi con il metodo statico System.currentTimeMillis.
Vi è da osservare che il timer di sistema garantisce una precisione al millisecondo ma non la sua granularità: può essere che il "tichettio" del timer non sia al millisecondo, ma anche più fine (decimi di millisecondo) o più grossa, dipende dal sistema.

Il timer della JVM

La libreria Java definisce il metodo statico System.nanoTime che restituisce il tempo trascorso, in nanosecondi (miliardesimi di secondo) dalla esecuzione della Java Virtual Machine corrente.
Questo valore si affida ad un orologio ad alta risoluzione, se esso esiste nel sistema, altrimenti la sua granularità è la stessa del timer di sistema.

Il timer ad alta risoluzione viene usato per misurare il tempo di esecuzione delle applicazioni o, meglio, di alcuni dei suoi metodi che dipendono fortemente dai tempi.
Il timer ad alta risoluzione non è adatto a misurare il tempo inteso come data e ora corrente: ci sono classi dedicate a questo.

La data di sistema

Per misurare il trascorrere del tempo inteso come data e ora, Java mette a disposizione la classe java.time.LocalDateTime che ha un range di utilizzo di un paio di miliardi di anni e usa il formato ISO-8601, senza fuso orario, per la rappresentazione. Per esempio la stringa 2024-12-03T10:15:30 indica le ore 10:15:30 UTC del 3 dicembre 2024.
La classe LocalDateTime usa, per gli anni bisestili, le regole del calendario gregoriano, quello introdotto da Papa Gregorio XIII nel 1582. Queste regole vengono applicate a tutte le date sia nel futuro che nel passato e questo può andare bene per la maggior parte delle applicazioni moderne ma daranno risultati imprecisi per le date antecedenti la riforma gregoriana dal momento che, fino al 4 ottobre 1582 era in vigore il calendario giuliano, introdotto da Caio Giulio Cesare nel 46 a.C, che aveva regole diverse per gli anni bisestili.

Le operazioni atomiche

Nella sezione La sincronizzazione abbiamo imparato che alcune operazioni sono critiche e che devono essere portate a termine senza interruzioni da parte di threads separati altrimenti si corre il rischio di incappare nel problema della inconsistenza della memoria. Come abbiamo imparato nella sezione a riferimento, i metodi che eseguono queste operazioni critiche devono essere sincronizzati: l'uno non deve mai interrompere l'altro.
La strategia adottata per sincronizzare i metodi è quella dei semafori. Lo stesso linguaggio Java mette a disposizione del programmatore una classe specializzata a questo scopo: java.util.concurrent.Semaphore. Il semaforo funziona più o meno così:

public static final short ROSSO = 1, VERDE = 0;
private int semaforo;
public void synchronizedMethod()
{
while ( semaforo == ROSSO ) {
Thread.sleep( 100 );
}
semaforo = ROSSO;
... operazioni critiche ...
semaforo = VERDE;
}

Il metodo aspetta che il semaforo diventi VERDE e, qundo accade, lo imposta a ROSSO in modo che qualunque altro thread che usa un metodo sincronizzato resti bloccato. Alla fine delle operazioni il semaforo viene nuovamente impostato a VERDE. Il codice suesposto, benchè apparentemente perfetto, NON FUNZIONERA' MAI. Il problema sta nel fatto che due threads distinti potrebbero eseguire il ciclo while nello stesso identico istante e, trovando il semaforo VERDE, lo impostano a ROSSO procedendo entrambi nelle operazioni. Certo che le probabilità di un simile evento sono scarse, tendenzialmente prossime allo ZERO ma, come diceva un grande saggio: "qualsiasi evento, per quanto improbabile, dato un numero sufficiente di occasionhi, si verificherà".
Pertanto, una applicazione che si basa su un semaforo implementato con il codice suespoto potrebbe funzionare per mesi, anni o anche decenni ma, prima o poi, andrà in crash.

Il problema è che le due operazioni di verifica ed impostazione del semaforo devono essere eseguite in modalità atomica, cioè senza alcuna interruzione potenziale; trattandosi però di due istruzioni separate, non potranno mai essere eseguite in modalità atomica. Nessun linguaggio di alto livello è in grado di eseguire le due operazioni in modalità atomica; ma il codice macchina nativo può farlo.
Tutti i microprocessori possiedono nel loro set di istruzioni la XCHG che è il codice assembly per la istruzione eXCHanGe (=scambia) il cui compito è quello di scambiare il contenuto di due operandi: di solito, uno degli operandi è un registro del processore e l'altro è una cella di memoria cioè una variabile che rappresenta il semaforo. Scritto in pseudo-codice potrebbe apparire così:

SCAMBIA registro CON semaforo

Poichè il microprocessore garantisce che ogni singola istruzione in codice macchina viene eseguita in modalità atomica (quindi se una istruzione viene iniziata il processore garantisce che sarà completata senza interruzioni), il semaforo può essere implementato in questo modo:

IMPOSTA registro a ROSSO
SCAMBIA registro CON semaforo

Dopo lo scambio avremo due possibili situazioni:

  • registro è VERDE: il semaforo era verde, possiamo procedere con le operazioni perchè con lo scambio è diventato rosso in una singola istruzione
  • registro è ROSSO: il semaforo era rosso e non possiamo procedere; con lo scambio è diventato rosso ma poco importa: era già rosso

Indice Generale